Commit 17448956 by Frank Henigman Committed by Commit Bot

Vulkan: vertex attributes in client memory.

Support vertex data stored in client memory passed to glVertexAttribPointer. Only GL_FLOAT data is supported at this time. Includes a simple test. BUG=angleproject:1683 Change-Id: I3bc0cdefe02b02c046b0e85822019a0f1762235e Reviewed-on: https://chromium-review.googlesource.com/425137 Commit-Queue: Frank Henigman <fjhenigman@chromium.org> Reviewed-by: 's avatarJamie Madill <jmadill@chromium.org>
parent b66de58f
...@@ -67,7 +67,8 @@ ContextVk::ContextVk(const gl::ContextState &state, RendererVk *renderer) ...@@ -67,7 +67,8 @@ ContextVk::ContextVk(const gl::ContextState &state, RendererVk *renderer)
mRenderer(renderer), mRenderer(renderer),
mCurrentDrawMode(GL_NONE), mCurrentDrawMode(GL_NONE),
mVertexArrayDirty(false), mVertexArrayDirty(false),
mTexturesDirty(false) mTexturesDirty(false),
mStreamingVertexData(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, 1024 * 1024)
{ {
memset(&mClearColorValue, 0, sizeof(mClearColorValue)); memset(&mClearColorValue, 0, sizeof(mClearColorValue));
memset(&mClearDepthStencilValue, 0, sizeof(mClearDepthStencilValue)); memset(&mClearDepthStencilValue, 0, sizeof(mClearDepthStencilValue));
...@@ -82,6 +83,7 @@ void ContextVk::onDestroy(const gl::Context *context) ...@@ -82,6 +83,7 @@ void ContextVk::onDestroy(const gl::Context *context)
VkDevice device = mRenderer->getDevice(); VkDevice device = mRenderer->getDevice();
mDescriptorPool.destroy(device); mDescriptorPool.destroy(device);
mStreamingVertexData.destroy(device);
} }
gl::Error ContextVk::initialize() gl::Error ContextVk::initialize()
...@@ -156,6 +158,8 @@ gl::Error ContextVk::initPipeline(const gl::Context *context) ...@@ -156,6 +158,8 @@ gl::Error ContextVk::initPipeline(const gl::Context *context)
gl::Error ContextVk::setupDraw(const gl::Context *context, gl::Error ContextVk::setupDraw(const gl::Context *context,
GLenum mode, GLenum mode,
DrawType drawType, DrawType drawType,
int firstVertex,
int lastVertex,
vk::CommandBuffer **commandBuffer) vk::CommandBuffer **commandBuffer)
{ {
if (mode != mCurrentDrawMode) if (mode != mCurrentDrawMode)
...@@ -169,22 +173,16 @@ gl::Error ContextVk::setupDraw(const gl::Context *context, ...@@ -169,22 +173,16 @@ gl::Error ContextVk::setupDraw(const gl::Context *context,
ANGLE_TRY(initPipeline(context)); ANGLE_TRY(initPipeline(context));
} }
const auto &state = mState.getState(); const auto &state = mState.getState();
const gl::Program *programGL = state.getProgram(); const gl::Program *programGL = state.getProgram();
ProgramVk *programVk = vk::GetImpl(programGL); ProgramVk *programVk = vk::GetImpl(programGL);
const gl::VertexArray *vao = state.getVertexArray(); const gl::VertexArray *vao = state.getVertexArray();
VertexArrayVk *vkVAO = vk::GetImpl(vao); VertexArrayVk *vkVAO = vk::GetImpl(vao);
const auto *drawFBO = state.getDrawFramebuffer(); const auto *drawFBO = state.getDrawFramebuffer();
FramebufferVk *vkFBO = vk::GetImpl(drawFBO); FramebufferVk *vkFBO = vk::GetImpl(drawFBO);
Serial queueSerial = mRenderer->getCurrentQueueSerial(); Serial queueSerial = mRenderer->getCurrentQueueSerial();
uint32_t maxAttrib = programGL->getState().getMaxActiveAttribLocation(); uint32_t maxAttrib = programGL->getState().getMaxActiveAttribLocation();
// Process vertex attributes. Assume zero offsets for now.
// TODO(jmadill): Offset handling.
const auto &vertexHandles = vkVAO->getCurrentArrayBufferHandles();
angle::MemoryBuffer *zeroBuf = nullptr;
ANGLE_TRY(context->getZeroFilledBuffer(maxAttrib * sizeof(VkDeviceSize), &zeroBuf));
// TODO(jmadill): Need to link up the TextureVk to the Secondary CB. // TODO(jmadill): Need to link up the TextureVk to the Secondary CB.
vk::CommandBufferNode *renderNode = nullptr; vk::CommandBufferNode *renderNode = nullptr;
ANGLE_TRY(vkFBO->getRenderNode(context, &renderNode)); ANGLE_TRY(vkFBO->getRenderNode(context, &renderNode));
...@@ -233,9 +231,11 @@ gl::Error ContextVk::setupDraw(const gl::Context *context, ...@@ -233,9 +231,11 @@ gl::Error ContextVk::setupDraw(const gl::Context *context,
} }
(*commandBuffer)->bindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, mCurrentPipeline->get()); (*commandBuffer)->bindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, mCurrentPipeline->get());
ContextVk *contextVk = vk::GetImpl(context);
ANGLE_TRY(vkVAO->streamVertexData(contextVk, &mStreamingVertexData, firstVertex, lastVertex));
(*commandBuffer) (*commandBuffer)
->bindVertexBuffers(0, maxAttrib, vertexHandles.data(), ->bindVertexBuffers(0, maxAttrib, vkVAO->getCurrentArrayBufferHandles().data(),
reinterpret_cast<const VkDeviceSize *>(zeroBuf->data())); vkVAO->getCurrentArrayBufferOffsets().data());
// Update the queue serial for the pipeline object. // Update the queue serial for the pipeline object.
ASSERT(mCurrentPipeline && mCurrentPipeline->valid()); ASSERT(mCurrentPipeline && mCurrentPipeline->valid());
...@@ -264,7 +264,7 @@ gl::Error ContextVk::setupDraw(const gl::Context *context, ...@@ -264,7 +264,7 @@ gl::Error ContextVk::setupDraw(const gl::Context *context,
gl::Error ContextVk::drawArrays(const gl::Context *context, GLenum mode, GLint first, GLsizei count) gl::Error ContextVk::drawArrays(const gl::Context *context, GLenum mode, GLint first, GLsizei count)
{ {
vk::CommandBuffer *commandBuffer = nullptr; vk::CommandBuffer *commandBuffer = nullptr;
ANGLE_TRY(setupDraw(context, mode, DrawType::Arrays, &commandBuffer)); ANGLE_TRY(setupDraw(context, mode, DrawType::Arrays, first, first + count - 1, &commandBuffer));
commandBuffer->draw(count, 1, first, 0); commandBuffer->draw(count, 1, first, 0);
return gl::NoError(); return gl::NoError();
} }
...@@ -286,7 +286,8 @@ gl::Error ContextVk::drawElements(const gl::Context *context, ...@@ -286,7 +286,8 @@ gl::Error ContextVk::drawElements(const gl::Context *context,
const void *indices) const void *indices)
{ {
vk::CommandBuffer *commandBuffer; vk::CommandBuffer *commandBuffer;
ANGLE_TRY(setupDraw(context, mode, DrawType::Elements, &commandBuffer)); // TODO(fjhenigman): calculate the index range and pass to setupDraw()
ANGLE_TRY(setupDraw(context, mode, DrawType::Elements, 0, 0, &commandBuffer));
if (indices) if (indices)
{ {
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
#include <vulkan/vulkan.h> #include <vulkan/vulkan.h>
#include "libANGLE/renderer/ContextImpl.h" #include "libANGLE/renderer/ContextImpl.h"
#include "libANGLE/renderer/vulkan/StreamingBuffer.h"
#include "libANGLE/renderer/vulkan/vk_cache_utils.h" #include "libANGLE/renderer/vulkan/vk_cache_utils.h"
namespace rx namespace rx
...@@ -165,6 +166,8 @@ class ContextVk : public ContextImpl ...@@ -165,6 +166,8 @@ class ContextVk : public ContextImpl
gl::Error setupDraw(const gl::Context *context, gl::Error setupDraw(const gl::Context *context,
GLenum mode, GLenum mode,
DrawType drawType, DrawType drawType,
int firstVertex,
int lastVertex,
vk::CommandBuffer **commandBuffer); vk::CommandBuffer **commandBuffer);
RendererVk *mRenderer; RendererVk *mRenderer;
...@@ -176,7 +179,7 @@ class ContextVk : public ContextImpl ...@@ -176,7 +179,7 @@ class ContextVk : public ContextImpl
std::unique_ptr<vk::PipelineDesc> mPipelineDesc; std::unique_ptr<vk::PipelineDesc> mPipelineDesc;
// The descriptor pool is externally sychronized, so cannot be accessed from different threads // The descriptor pool is externally sychronized, so cannot be accessed from different threads
// simulataneously. Hence, we keep it in the ContextVk instead of the RendererVk. // simultaneously. Hence, we keep it in the ContextVk instead of the RendererVk.
vk::DescriptorPool mDescriptorPool; vk::DescriptorPool mDescriptorPool;
// Triggers adding dependencies to the command graph. // Triggers adding dependencies to the command graph.
...@@ -186,6 +189,8 @@ class ContextVk : public ContextImpl ...@@ -186,6 +189,8 @@ class ContextVk : public ContextImpl
// Cached clear value for color and depth/stencil. // Cached clear value for color and depth/stencil.
VkClearValue mClearColorValue; VkClearValue mClearColorValue;
VkClearValue mClearDepthStencilValue; VkClearValue mClearDepthStencilValue;
StreamingBuffer mStreamingVertexData;
}; };
} // namespace rx } // namespace rx
......
//
// Copyright 2018 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// StreamingBuffer:
// Create, map and flush buffers as needed to hold data, returning a handle and offset for each
// chunk.
//
#include "StreamingBuffer.h"
#include "anglebase/numerics/safe_math.h"
#include "libANGLE/renderer/vulkan/ContextVk.h"
#include "libANGLE/renderer/vulkan/RendererVk.h"
namespace rx
{
StreamingBuffer::StreamingBuffer(VkBufferUsageFlags usage, size_t minSize)
: mUsage(usage),
mMinSize(minSize),
mNextWriteOffset(0),
mLastFlushOffset(0),
mSize(0),
mMappedMemory(nullptr)
{
}
StreamingBuffer::~StreamingBuffer()
{
}
gl::Error StreamingBuffer::allocate(ContextVk *context,
size_t allocationSize,
uint8_t **ptrOut,
VkBuffer *handleOut,
VkDeviceSize *offsetOut)
{
RendererVk *renderer = context->getRenderer();
// TODO(fjhenigman): Update this when we have buffers that need to
// persist longer than one frame.
updateQueueSerial(renderer->getCurrentQueueSerial());
angle::base::CheckedNumeric<size_t> checkedNextWriteOffset = mNextWriteOffset;
checkedNextWriteOffset += allocationSize;
if (!checkedNextWriteOffset.IsValid() || checkedNextWriteOffset.ValueOrDie() > mSize)
{
VkDevice device = context->getDevice();
if (mMappedMemory)
{
mMemory.unmap(device);
mMappedMemory = nullptr;
}
renderer->releaseResource(*this, &mBuffer);
renderer->releaseResource(*this, &mMemory);
VkBufferCreateInfo createInfo;
createInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
createInfo.pNext = nullptr;
createInfo.flags = 0;
createInfo.size = std::max(allocationSize, mMinSize);
createInfo.usage = mUsage;
createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.queueFamilyIndexCount = 0;
createInfo.pQueueFamilyIndices = nullptr;
ANGLE_TRY(mBuffer.init(device, createInfo));
ANGLE_TRY(vk::AllocateBufferMemory(renderer, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, &mBuffer,
&mMemory, &mSize));
ANGLE_TRY(mMemory.map(device, 0, mSize, 0, &mMappedMemory));
mNextWriteOffset = 0;
mLastFlushOffset = 0;
}
ASSERT(mBuffer.valid());
*handleOut = mBuffer.getHandle();
ASSERT(mMappedMemory);
*ptrOut = mMappedMemory + mNextWriteOffset;
*offsetOut = mNextWriteOffset;
mNextWriteOffset += allocationSize;
return gl::NoError();
}
gl::Error StreamingBuffer::flush(ContextVk *context)
{
if (mNextWriteOffset > mLastFlushOffset)
{
VkMappedMemoryRange range;
range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
range.pNext = nullptr;
range.memory = mMemory.getHandle();
range.offset = mLastFlushOffset;
range.size = mNextWriteOffset - mLastFlushOffset;
ANGLE_VK_TRY(vkFlushMappedMemoryRanges(context->getDevice(), 1, &range));
mLastFlushOffset = mNextWriteOffset;
}
return gl::NoError();
}
void StreamingBuffer::destroy(VkDevice device)
{
mBuffer.destroy(device);
mMemory.destroy(device);
}
} // namespace rx
//
// Copyright 2018 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// StreamingBuffer:
// Create, map and flush buffers as needed to hold data, returning a handle and offset for each
// chunk.
//
#ifndef LIBANGLE_RENDERER_VULKAN_STREAMING_BUFFER_H_
#define LIBANGLE_RENDERER_VULKAN_STREAMING_BUFFER_H_
#include "libANGLE/renderer/vulkan/vk_utils.h"
namespace rx
{
class StreamingBuffer : public ResourceVk
{
public:
StreamingBuffer(VkBufferUsageFlags usage, size_t minSize);
~StreamingBuffer();
gl::Error allocate(ContextVk *context,
size_t amount,
uint8_t **ptrOut,
VkBuffer *handleOut,
VkDeviceSize *offsetOut);
gl::Error flush(ContextVk *context);
void destroy(VkDevice device);
private:
VkBufferUsageFlags mUsage;
size_t mMinSize;
vk::Buffer mBuffer;
vk::DeviceMemory mMemory;
size_t mNextWriteOffset;
size_t mLastFlushOffset;
size_t mSize;
uint8_t *mMappedMemory;
};
} // namespace rx
#endif // LIBANGLE_RENDERER_VULKAN_STREAMING_BUFFER_H_
...@@ -23,10 +23,12 @@ namespace rx ...@@ -23,10 +23,12 @@ namespace rx
VertexArrayVk::VertexArrayVk(const gl::VertexArrayState &state) VertexArrayVk::VertexArrayVk(const gl::VertexArrayState &state)
: VertexArrayImpl(state), : VertexArrayImpl(state),
mCurrentArrayBufferHandles{}, mCurrentArrayBufferHandles{},
mCurrentArrayBufferOffsets{},
mCurrentArrayBufferResources{}, mCurrentArrayBufferResources{},
mCurrentElementArrayBufferResource(nullptr) mCurrentElementArrayBufferResource(nullptr)
{ {
mCurrentArrayBufferHandles.fill(VK_NULL_HANDLE); mCurrentArrayBufferHandles.fill(VK_NULL_HANDLE);
mCurrentArrayBufferOffsets.fill(0);
mCurrentArrayBufferResources.fill(nullptr); mCurrentArrayBufferResources.fill(nullptr);
mPackedInputBindings.fill({0, 0}); mPackedInputBindings.fill({0, 0});
...@@ -41,6 +43,53 @@ void VertexArrayVk::destroy(const gl::Context *context) ...@@ -41,6 +43,53 @@ void VertexArrayVk::destroy(const gl::Context *context)
{ {
} }
gl::Error VertexArrayVk::streamVertexData(ContextVk *context,
StreamingBuffer *stream,
int firstVertex,
int lastVertex)
{
const auto &attribs = mState.getVertexAttributes();
const auto &bindings = mState.getVertexBindings();
const gl::Program *programGL = context->getGLState().getProgram();
// TODO(fjhenigman): When we have a bunch of interleaved attributes, they end up
// un-interleaved, wasting space and copying time. Consider improving on that.
for (auto attribIndex : programGL->getActiveAttribLocationsMask())
{
const auto &attrib = attribs[attribIndex];
const auto &binding = bindings[attrib.bindingIndex];
gl::Buffer *bufferGL = binding.getBuffer().get();
if (attrib.enabled && !bufferGL)
{
// TODO(fjhenigman): Work with more formats than just GL_FLOAT.
if (attrib.type != GL_FLOAT)
{
UNIMPLEMENTED();
return gl::InternalError();
}
// Only [firstVertex, lastVertex] is needed by the upcoming draw so that
// is all we copy, but we allocate space for [0, lastVertex] so indexing
// will work. If we don't start at zero all the indices will be off.
// TODO(fjhenigman): See if we can account for indices being off by adjusting
// the offset, thus avoiding wasted memory.
const size_t firstByte = firstVertex * binding.getStride();
const size_t lastByte =
lastVertex * binding.getStride() + gl::ComputeVertexAttributeTypeSize(attrib);
uint8_t *dst = nullptr;
ANGLE_TRY(stream->allocate(context, lastByte, &dst,
&mCurrentArrayBufferHandles[attribIndex],
&mCurrentArrayBufferOffsets[attribIndex]));
memcpy(dst + firstByte, static_cast<const uint8_t *>(attrib.pointer) + firstByte,
lastByte - firstByte);
}
}
ANGLE_TRY(stream->flush(context));
return gl::NoError();
}
void VertexArrayVk::syncState(const gl::Context *context, void VertexArrayVk::syncState(const gl::Context *context,
const gl::VertexArray::DirtyBits &dirtyBits) const gl::VertexArray::DirtyBits &dirtyBits)
{ {
...@@ -94,6 +143,8 @@ void VertexArrayVk::syncState(const gl::Context *context, ...@@ -94,6 +143,8 @@ void VertexArrayVk::syncState(const gl::Context *context,
mCurrentArrayBufferResources[attribIndex] = nullptr; mCurrentArrayBufferResources[attribIndex] = nullptr;
mCurrentArrayBufferHandles[attribIndex] = VK_NULL_HANDLE; mCurrentArrayBufferHandles[attribIndex] = VK_NULL_HANDLE;
} }
// TODO(jmadill): Offset handling. Assume zero for now.
mCurrentArrayBufferOffsets[attribIndex] = 0;
} }
else else
{ {
...@@ -107,6 +158,11 @@ const gl::AttribArray<VkBuffer> &VertexArrayVk::getCurrentArrayBufferHandles() c ...@@ -107,6 +158,11 @@ const gl::AttribArray<VkBuffer> &VertexArrayVk::getCurrentArrayBufferHandles() c
return mCurrentArrayBufferHandles; return mCurrentArrayBufferHandles;
} }
const gl::AttribArray<VkDeviceSize> &VertexArrayVk::getCurrentArrayBufferOffsets() const
{
return mCurrentArrayBufferOffsets;
}
void VertexArrayVk::updateDrawDependencies(vk::CommandBufferNode *readNode, void VertexArrayVk::updateDrawDependencies(vk::CommandBufferNode *readNode,
const gl::AttributesMask &activeAttribsMask, const gl::AttributesMask &activeAttribsMask,
Serial serial, Serial serial,
...@@ -115,8 +171,8 @@ void VertexArrayVk::updateDrawDependencies(vk::CommandBufferNode *readNode, ...@@ -115,8 +171,8 @@ void VertexArrayVk::updateDrawDependencies(vk::CommandBufferNode *readNode,
// Handle the bound array buffers. // Handle the bound array buffers.
for (auto attribIndex : activeAttribsMask) for (auto attribIndex : activeAttribsMask)
{ {
ASSERT(mCurrentArrayBufferResources[attribIndex]); if (mCurrentArrayBufferResources[attribIndex])
mCurrentArrayBufferResources[attribIndex]->onReadResource(readNode, serial); mCurrentArrayBufferResources[attribIndex]->onReadResource(readNode, serial);
} }
// Handle the bound element array buffer. // Handle the bound element array buffer.
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
namespace rx namespace rx
{ {
class BufferVk; class BufferVk;
class StreamingBuffer;
class VertexArrayVk : public VertexArrayImpl class VertexArrayVk : public VertexArrayImpl
{ {
...@@ -25,10 +26,15 @@ class VertexArrayVk : public VertexArrayImpl ...@@ -25,10 +26,15 @@ class VertexArrayVk : public VertexArrayImpl
void destroy(const gl::Context *context) override; void destroy(const gl::Context *context) override;
gl::Error streamVertexData(ContextVk *context,
StreamingBuffer *stream,
int firstVertex,
int lastVertex);
void syncState(const gl::Context *context, void syncState(const gl::Context *context,
const gl::VertexArray::DirtyBits &dirtyBits) override; const gl::VertexArray::DirtyBits &dirtyBits) override;
const gl::AttribArray<VkBuffer> &getCurrentArrayBufferHandles() const; const gl::AttribArray<VkBuffer> &getCurrentArrayBufferHandles() const;
const gl::AttribArray<VkDeviceSize> &getCurrentArrayBufferOffsets() const;
void updateDrawDependencies(vk::CommandBufferNode *readNode, void updateDrawDependencies(vk::CommandBufferNode *readNode,
const gl::AttributesMask &activeAttribsMask, const gl::AttributesMask &activeAttribsMask,
...@@ -49,6 +55,7 @@ class VertexArrayVk : public VertexArrayImpl ...@@ -49,6 +55,7 @@ class VertexArrayVk : public VertexArrayImpl
const gl::VertexAttribute &attrib); const gl::VertexAttribute &attrib);
gl::AttribArray<VkBuffer> mCurrentArrayBufferHandles; gl::AttribArray<VkBuffer> mCurrentArrayBufferHandles;
gl::AttribArray<VkDeviceSize> mCurrentArrayBufferOffsets;
gl::AttribArray<ResourceVk *> mCurrentArrayBufferResources; gl::AttribArray<ResourceVk *> mCurrentArrayBufferResources;
ResourceVk *mCurrentElementArrayBufferResource; ResourceVk *mCurrentElementArrayBufferResource;
......
...@@ -748,6 +748,8 @@ ...@@ -748,6 +748,8 @@
'libANGLE/renderer/vulkan/SamplerVk.h', 'libANGLE/renderer/vulkan/SamplerVk.h',
'libANGLE/renderer/vulkan/ShaderVk.cpp', 'libANGLE/renderer/vulkan/ShaderVk.cpp',
'libANGLE/renderer/vulkan/ShaderVk.h', 'libANGLE/renderer/vulkan/ShaderVk.h',
'libANGLE/renderer/vulkan/StreamingBuffer.h',
'libANGLE/renderer/vulkan/StreamingBuffer.cpp',
'libANGLE/renderer/vulkan/SurfaceVk.cpp', 'libANGLE/renderer/vulkan/SurfaceVk.cpp',
'libANGLE/renderer/vulkan/SurfaceVk.h', 'libANGLE/renderer/vulkan/SurfaceVk.h',
'libANGLE/renderer/vulkan/SyncVk.cpp', 'libANGLE/renderer/vulkan/SyncVk.cpp',
......
...@@ -333,6 +333,18 @@ TEST_P(SimpleOperationTest, DrawQuad) ...@@ -333,6 +333,18 @@ TEST_P(SimpleOperationTest, DrawQuad)
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
} }
// Simple quad test with data in client memory, not vertex buffer.
TEST_P(SimpleOperationTest, DrawQuadFromClientMemory)
{
ANGLE_GL_PROGRAM(program, kBasicVertexShader, kGreenFragmentShader);
drawQuad(program.get(), "position", 0.5f, 1.0f, false);
ASSERT_GL_NO_ERROR();
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
}
// Simple double quad test. // Simple double quad test.
TEST_P(SimpleOperationTest, DrawQuadTwice) TEST_P(SimpleOperationTest, DrawQuadTwice)
{ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment