Commit 55df3ec1 by John Plate Committed by Angle LUCI CQ

CL: Add remaining enqueue commands

Add support for remaining OpenCL 1.2 enqueue commands to front end and pass-through back end. Bug: angleproject:6015 Change-Id: Iab650e42d51e2105dc826088d3606c56d5cd0fd5 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/2944966Reviewed-by: 's avatarJamie Madill <jmadill@chromium.org> Reviewed-by: 's avatarCody Northrop <cnorthrop@google.com> Commit-Queue: John Plate <jplate@google.com>
parent 5e5d17cd
...@@ -33,6 +33,8 @@ using ProgramCB = void(CL_CALLBACK *)(cl_program program, void *user_data); ...@@ -33,6 +33,8 @@ using ProgramCB = void(CL_CALLBACK *)(cl_program program, void *user_data);
using EventCB = void(CL_CALLBACK *)(cl_event event, cl_int event_command_status, void *user_data); using EventCB = void(CL_CALLBACK *)(cl_event event, cl_int event_command_status, void *user_data);
using UserFunc = void(CL_CALLBACK *)(void *args);
template <typename T = void> template <typename T = void>
struct Dispatch struct Dispatch
{ {
......
...@@ -187,6 +187,60 @@ class CommandQueue final : public _cl_command_queue, public Object ...@@ -187,6 +187,60 @@ class CommandQueue final : public _cl_command_queue, public Object
cl_event *event, cl_event *event,
cl_int &errorCode); cl_int &errorCode);
cl_int enqueueUnmapMemObject(cl_mem memobj,
void *mappedPtr,
cl_uint numEventsInWaitList,
const cl_event *eventWaitList,
cl_event *event);
cl_int enqueueMigrateMemObjects(cl_uint numMemObjects,
const cl_mem *memObjects,
MemMigrationFlags flags,
cl_uint numEventsInWaitList,
const cl_event *eventWaitList,
cl_event *event);
cl_int enqueueNDRangeKernel(cl_kernel kernel,
cl_uint workDim,
const size_t *globalWorkOffset,
const size_t *globalWorkSize,
const size_t *localWorkSize,
cl_uint numEventsInWaitList,
const cl_event *eventWaitList,
cl_event *event);
cl_int enqueueTask(cl_kernel kernel,
cl_uint numEventsInWaitList,
const cl_event *eventWaitList,
cl_event *event);
cl_int enqueueNativeKernel(UserFunc userFunc,
void *args,
size_t cbArgs,
cl_uint numMemObjects,
const cl_mem *memList,
const void **argsMemLoc,
cl_uint numEventsInWaitList,
const cl_event *eventWaitList,
cl_event *event);
cl_int enqueueMarkerWithWaitList(cl_uint numEventsInWaitList,
const cl_event *eventWaitList,
cl_event *event);
cl_int enqueueMarker(cl_event *event);
cl_int enqueueWaitForEvents(cl_uint numEvents, const cl_event *eventList);
cl_int enqueueBarrierWithWaitList(cl_uint numEventsInWaitList,
const cl_event *eventWaitList,
cl_event *event);
cl_int enqueueBarrier();
cl_int flush();
cl_int finish();
public: public:
using PropArray = std::vector<cl_queue_properties>; using PropArray = std::vector<cl_queue_properties>;
...@@ -198,6 +252,9 @@ class CommandQueue final : public _cl_command_queue, public Object ...@@ -198,6 +252,9 @@ class CommandQueue final : public _cl_command_queue, public Object
const Context &getContext() const; const Context &getContext() const;
const Device &getDevice() const; const Device &getDevice() const;
// Get index of device in the context.
size_t getDeviceIndex() const;
CommandQueueProperties getProperties() const; CommandQueueProperties getProperties() const;
bool isOnHost() const; bool isOnHost() const;
bool isOnDevice() const; bool isOnDevice() const;
......
...@@ -172,9 +172,7 @@ inline const DevicePtrs &Context::getDevices() const ...@@ -172,9 +172,7 @@ inline const DevicePtrs &Context::getDevices() const
inline bool Context::hasDevice(const _cl_device_id *device) const inline bool Context::hasDevice(const _cl_device_id *device) const
{ {
return std::find_if(mDevices.cbegin(), mDevices.cend(), [=](const DevicePtr &ptr) { return std::find(mDevices.cbegin(), mDevices.cend(), device) != mDevices.cend();
return ptr.get() == device;
}) != mDevices.cend();
} }
template <typename T> template <typename T>
......
...@@ -108,7 +108,6 @@ cl_int Device::getInfo(DeviceInfo name, size_t valueSize, void *value, size_t *v ...@@ -108,7 +108,6 @@ cl_int Device::getInfo(DeviceInfo name, size_t valueSize, void *value, size_t *v
case DeviceInfo::GlobalMemSize: case DeviceInfo::GlobalMemSize:
case DeviceInfo::MaxConstantBufferSize: case DeviceInfo::MaxConstantBufferSize:
case DeviceInfo::LocalMemSize: case DeviceInfo::LocalMemSize:
case DeviceInfo::ExecutionCapabilities:
case DeviceInfo::QueueOnHostProperties: case DeviceInfo::QueueOnHostProperties:
case DeviceInfo::QueueOnDeviceProperties: case DeviceInfo::QueueOnDeviceProperties:
case DeviceInfo::PartitionAffinityDomain: case DeviceInfo::PartitionAffinityDomain:
...@@ -224,6 +223,10 @@ cl_int Device::getInfo(DeviceInfo name, size_t valueSize, void *value, size_t *v ...@@ -224,6 +223,10 @@ cl_int Device::getInfo(DeviceInfo name, size_t valueSize, void *value, size_t *v
copyValue = &mInfo.mMemBaseAddrAlign; copyValue = &mInfo.mMemBaseAddrAlign;
copySize = sizeof(mInfo.mMemBaseAddrAlign); copySize = sizeof(mInfo.mMemBaseAddrAlign);
break; break;
case DeviceInfo::ExecutionCapabilities:
copyValue = &mInfo.mExecCapabilities;
copySize = sizeof(mInfo.mExecCapabilities);
break;
case DeviceInfo::QueueOnDeviceMaxSize: case DeviceInfo::QueueOnDeviceMaxSize:
copyValue = &mInfo.mQueueOnDeviceMaxSize; copyValue = &mInfo.mQueueOnDeviceMaxSize;
copySize = sizeof(mInfo.mQueueOnDeviceMaxSize); copySize = sizeof(mInfo.mQueueOnDeviceMaxSize);
......
...@@ -85,7 +85,7 @@ cl_int Kernel::getWorkGroupInfo(cl_device_id device, ...@@ -85,7 +85,7 @@ cl_int Kernel::getWorkGroupInfo(cl_device_id device,
if (device != nullptr) if (device != nullptr)
{ {
const DevicePtrs &devices = mProgram->getContext().getDevices(); const DevicePtrs &devices = mProgram->getContext().getDevices();
while (index < devices.size() && devices[index].get() != device) while (index < devices.size() && devices[index] != device)
{ {
++index; ++index;
} }
......
...@@ -118,9 +118,7 @@ inline const DevicePtrs &Program::getDevices() const ...@@ -118,9 +118,7 @@ inline const DevicePtrs &Program::getDevices() const
inline bool Program::hasDevice(const _cl_device_id *device) const inline bool Program::hasDevice(const _cl_device_id *device) const
{ {
return std::find_if(mDevices.cbegin(), mDevices.cend(), [=](const DevicePtr &ptr) { return std::find(mDevices.cbegin(), mDevices.cend(), device) != mDevices.cend();
return ptr.get() == device;
}) != mDevices.cend();
} }
inline bool Program::isBuilding() const inline bool Program::isBuilding() const
......
...@@ -127,6 +127,42 @@ bool operator!=(nullptr_t, const RefPointer<T> &ptr) noexcept ...@@ -127,6 +127,42 @@ bool operator!=(nullptr_t, const RefPointer<T> &ptr) noexcept
return ptr.get() != nullptr; return ptr.get() != nullptr;
} }
template <typename T, typename U>
bool operator==(const RefPointer<T> &left, const RefPointer<U> &right) noexcept
{
return left.get() == right.get();
}
template <typename T, typename U>
bool operator!=(const RefPointer<T> &left, const RefPointer<U> &right) noexcept
{
return left.get() != right.get();
}
template <typename T, typename U>
bool operator==(const RefPointer<T> &left, const U *right) noexcept
{
return left.get() == right;
}
template <typename T, typename U>
bool operator==(const T *left, const RefPointer<U> &right) noexcept
{
return left == right.get();
}
template <typename T, typename U>
bool operator!=(const RefPointer<T> &left, const U *right) noexcept
{
return left.get() != right;
}
template <typename T, typename U>
bool operator!=(const T *left, const RefPointer<U> &right) noexcept
{
return left != right.get();
}
} // namespace cl } // namespace cl
#endif // LIBANGLE_CLREFPOINTER_H_ #endif // LIBANGLE_CLREFPOINTER_H_
...@@ -40,6 +40,7 @@ class Platform; ...@@ -40,6 +40,7 @@ class Platform;
class Program; class Program;
class Sampler; class Sampler;
using BufferPtr = RefPointer<Buffer>;
using CommandQueuePtr = RefPointer<CommandQueue>; using CommandQueuePtr = RefPointer<CommandQueue>;
using ContextPtr = RefPointer<Context>; using ContextPtr = RefPointer<Context>;
using DevicePtr = RefPointer<Device>; using DevicePtr = RefPointer<Device>;
...@@ -50,9 +51,11 @@ using PlatformPtr = RefPointer<Platform>; ...@@ -50,9 +51,11 @@ using PlatformPtr = RefPointer<Platform>;
using ProgramPtr = RefPointer<Program>; using ProgramPtr = RefPointer<Program>;
using SamplerPtr = RefPointer<Sampler>; using SamplerPtr = RefPointer<Sampler>;
using BufferPtrs = std::vector<BufferPtr>;
using DevicePtrs = std::vector<DevicePtr>; using DevicePtrs = std::vector<DevicePtr>;
using EventPtrs = std::vector<EventPtr>; using EventPtrs = std::vector<EventPtr>;
using KernelPtrs = std::vector<KernelPtr>; using KernelPtrs = std::vector<KernelPtr>;
using MemoryPtrs = std::vector<MemoryPtr>;
using PlatformPtrs = std::vector<PlatformPtr>; using PlatformPtrs = std::vector<PlatformPtr>;
using ProgramPtrs = std::vector<ProgramPtr>; using ProgramPtrs = std::vector<ProgramPtr>;
......
...@@ -164,6 +164,51 @@ class CLCommandQueueImpl : angle::NonCopyable ...@@ -164,6 +164,51 @@ class CLCommandQueueImpl : angle::NonCopyable
CLEventImpl::CreateFunc *eventCreateFunc, CLEventImpl::CreateFunc *eventCreateFunc,
cl_int &errorCode) = 0; cl_int &errorCode) = 0;
virtual cl_int enqueueUnmapMemObject(const cl::Memory &memory,
void *mappedPtr,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) = 0;
virtual cl_int enqueueMigrateMemObjects(const cl::MemoryPtrs &memObjects,
cl::MemMigrationFlags flags,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) = 0;
virtual cl_int enqueueNDRangeKernel(const cl::Kernel &kernel,
cl_uint workDim,
const size_t *globalWorkOffset,
const size_t *globalWorkSize,
const size_t *localWorkSize,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) = 0;
virtual cl_int enqueueTask(const cl::Kernel &kernel,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) = 0;
virtual cl_int enqueueNativeKernel(cl::UserFunc userFunc,
void *args,
size_t cbArgs,
const cl::BufferPtrs &buffers,
const std::vector<size_t> bufferPtrOffsets,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) = 0;
virtual cl_int enqueueMarkerWithWaitList(const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) = 0;
virtual cl_int enqueueMarker(CLEventImpl::CreateFunc &eventCreateFunc) = 0;
virtual cl_int enqueueWaitForEvents(const cl::EventPtrs &events) = 0;
virtual cl_int enqueueBarrierWithWaitList(const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) = 0;
virtual cl_int enqueueBarrier() = 0;
virtual cl_int flush() = 0;
virtual cl_int finish() = 0;
protected: protected:
const cl::CommandQueue &mCommandQueue; const cl::CommandQueue &mCommandQueue;
}; };
......
...@@ -53,7 +53,8 @@ class CLDeviceImpl : angle::NonCopyable ...@@ -53,7 +53,8 @@ class CLDeviceImpl : angle::NonCopyable
cl_uint mImagePitchAlignment = 0u; cl_uint mImagePitchAlignment = 0u;
cl_uint mImageBaseAddressAlignment = 0u; cl_uint mImageBaseAddressAlignment = 0u;
cl_uint mMemBaseAddrAlign = 0u; cl_uint mMemBaseAddrAlign = 0u;
cl_uint mQueueOnDeviceMaxSize = 0u; cl::DeviceExecCapabilities mExecCapabilities;
cl_uint mQueueOnDeviceMaxSize = 0u;
std::string mBuiltInKernels; std::string mBuiltInKernels;
NameVersionVector mBuiltInKernelsWithVersion; NameVersionVector mBuiltInKernelsWithVersion;
std::string mVersionStr; std::string mVersionStr;
......
...@@ -163,6 +163,51 @@ class CLCommandQueueCL : public CLCommandQueueImpl ...@@ -163,6 +163,51 @@ class CLCommandQueueCL : public CLCommandQueueImpl
CLEventImpl::CreateFunc *eventCreateFunc, CLEventImpl::CreateFunc *eventCreateFunc,
cl_int &errorCode) override; cl_int &errorCode) override;
cl_int enqueueUnmapMemObject(const cl::Memory &memory,
void *mappedPtr,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) override;
cl_int enqueueMigrateMemObjects(const cl::MemoryPtrs &memObjects,
cl::MemMigrationFlags flags,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) override;
cl_int enqueueNDRangeKernel(const cl::Kernel &kernel,
cl_uint workDim,
const size_t *globalWorkOffset,
const size_t *globalWorkSize,
const size_t *localWorkSize,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) override;
cl_int enqueueTask(const cl::Kernel &kernel,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) override;
cl_int enqueueNativeKernel(cl::UserFunc userFunc,
void *args,
size_t cbArgs,
const cl::BufferPtrs &buffers,
const std::vector<size_t> bufferPtrOffsets,
const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) override;
cl_int enqueueMarkerWithWaitList(const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) override;
cl_int enqueueMarker(CLEventImpl::CreateFunc &eventCreateFunc) override;
cl_int enqueueWaitForEvents(const cl::EventPtrs &events) override;
cl_int enqueueBarrierWithWaitList(const cl::EventPtrs &waitEvents,
CLEventImpl::CreateFunc *eventCreateFunc) override;
cl_int enqueueBarrier() override;
cl_int flush() override;
cl_int finish() override;
private: private:
const cl_command_queue mNative; const cl_command_queue mNative;
}; };
......
...@@ -90,7 +90,8 @@ CLDeviceImpl::Info CLDeviceCL::createInfo(cl::DeviceType type) const ...@@ -90,7 +90,8 @@ CLDeviceImpl::Info CLDeviceCL::createInfo(cl::DeviceType type) const
!GetDeviceInfo(mNative, cl::DeviceInfo::Image3D_MaxWidth, info.mImage3D_MaxWidth) || !GetDeviceInfo(mNative, cl::DeviceInfo::Image3D_MaxWidth, info.mImage3D_MaxWidth) ||
!GetDeviceInfo(mNative, cl::DeviceInfo::Image3D_MaxHeight, info.mImage3D_MaxHeight) || !GetDeviceInfo(mNative, cl::DeviceInfo::Image3D_MaxHeight, info.mImage3D_MaxHeight) ||
!GetDeviceInfo(mNative, cl::DeviceInfo::Image3D_MaxDepth, info.mImage3D_MaxDepth) || !GetDeviceInfo(mNative, cl::DeviceInfo::Image3D_MaxDepth, info.mImage3D_MaxDepth) ||
!GetDeviceInfo(mNative, cl::DeviceInfo::MemBaseAddrAlign, info.mMemBaseAddrAlign)) !GetDeviceInfo(mNative, cl::DeviceInfo::MemBaseAddrAlign, info.mMemBaseAddrAlign) ||
!GetDeviceInfo(mNative, cl::DeviceInfo::ExecutionCapabilities, info.mExecCapabilities))
{ {
return Info{}; return Info{};
} }
......
...@@ -694,14 +694,12 @@ cl_int GetEventProfilingInfo(cl_event event, ...@@ -694,14 +694,12 @@ cl_int GetEventProfilingInfo(cl_event event,
cl_int Flush(cl_command_queue command_queue) cl_int Flush(cl_command_queue command_queue)
{ {
WARN_NOT_SUPPORTED(Flush); return command_queue->cast<CommandQueue>().flush();
return 0;
} }
cl_int Finish(cl_command_queue command_queue) cl_int Finish(cl_command_queue command_queue)
{ {
WARN_NOT_SUPPORTED(Finish); return command_queue->cast<CommandQueue>().finish();
return 0;
} }
cl_int EnqueueReadBuffer(cl_command_queue command_queue, cl_int EnqueueReadBuffer(cl_command_queue command_queue,
...@@ -956,8 +954,8 @@ cl_int EnqueueUnmapMemObject(cl_command_queue command_queue, ...@@ -956,8 +954,8 @@ cl_int EnqueueUnmapMemObject(cl_command_queue command_queue,
const cl_event *event_wait_list, const cl_event *event_wait_list,
cl_event *event) cl_event *event)
{ {
WARN_NOT_SUPPORTED(EnqueueUnmapMemObject); return command_queue->cast<CommandQueue>().enqueueUnmapMemObject(
return 0; memobj, mapped_ptr, num_events_in_wait_list, event_wait_list, event);
} }
cl_int EnqueueMigrateMemObjects(cl_command_queue command_queue, cl_int EnqueueMigrateMemObjects(cl_command_queue command_queue,
...@@ -968,8 +966,8 @@ cl_int EnqueueMigrateMemObjects(cl_command_queue command_queue, ...@@ -968,8 +966,8 @@ cl_int EnqueueMigrateMemObjects(cl_command_queue command_queue,
const cl_event *event_wait_list, const cl_event *event_wait_list,
cl_event *event) cl_event *event)
{ {
WARN_NOT_SUPPORTED(EnqueueMigrateMemObjects); return command_queue->cast<CommandQueue>().enqueueMigrateMemObjects(
return 0; num_mem_objects, mem_objects, flags, num_events_in_wait_list, event_wait_list, event);
} }
cl_int EnqueueNDRangeKernel(cl_command_queue command_queue, cl_int EnqueueNDRangeKernel(cl_command_queue command_queue,
...@@ -982,8 +980,9 @@ cl_int EnqueueNDRangeKernel(cl_command_queue command_queue, ...@@ -982,8 +980,9 @@ cl_int EnqueueNDRangeKernel(cl_command_queue command_queue,
const cl_event *event_wait_list, const cl_event *event_wait_list,
cl_event *event) cl_event *event)
{ {
WARN_NOT_SUPPORTED(EnqueueNDRangeKernel); return command_queue->cast<CommandQueue>().enqueueNDRangeKernel(
return 0; kernel, work_dim, global_work_offset, global_work_size, local_work_size,
num_events_in_wait_list, event_wait_list, event);
} }
cl_int EnqueueNativeKernel(cl_command_queue command_queue, cl_int EnqueueNativeKernel(cl_command_queue command_queue,
...@@ -997,8 +996,9 @@ cl_int EnqueueNativeKernel(cl_command_queue command_queue, ...@@ -997,8 +996,9 @@ cl_int EnqueueNativeKernel(cl_command_queue command_queue,
const cl_event *event_wait_list, const cl_event *event_wait_list,
cl_event *event) cl_event *event)
{ {
WARN_NOT_SUPPORTED(EnqueueNativeKernel); return command_queue->cast<CommandQueue>().enqueueNativeKernel(
return 0; user_func, args, cb_args, num_mem_objects, mem_list, args_mem_loc, num_events_in_wait_list,
event_wait_list, event);
} }
cl_int EnqueueMarkerWithWaitList(cl_command_queue command_queue, cl_int EnqueueMarkerWithWaitList(cl_command_queue command_queue,
...@@ -1006,8 +1006,8 @@ cl_int EnqueueMarkerWithWaitList(cl_command_queue command_queue, ...@@ -1006,8 +1006,8 @@ cl_int EnqueueMarkerWithWaitList(cl_command_queue command_queue,
const cl_event *event_wait_list, const cl_event *event_wait_list,
cl_event *event) cl_event *event)
{ {
WARN_NOT_SUPPORTED(EnqueueMarkerWithWaitList); return command_queue->cast<CommandQueue>().enqueueMarkerWithWaitList(num_events_in_wait_list,
return 0; event_wait_list, event);
} }
cl_int EnqueueBarrierWithWaitList(cl_command_queue command_queue, cl_int EnqueueBarrierWithWaitList(cl_command_queue command_queue,
...@@ -1015,8 +1015,8 @@ cl_int EnqueueBarrierWithWaitList(cl_command_queue command_queue, ...@@ -1015,8 +1015,8 @@ cl_int EnqueueBarrierWithWaitList(cl_command_queue command_queue,
const cl_event *event_wait_list, const cl_event *event_wait_list,
cl_event *event) cl_event *event)
{ {
WARN_NOT_SUPPORTED(EnqueueBarrierWithWaitList); return command_queue->cast<CommandQueue>().enqueueBarrierWithWaitList(num_events_in_wait_list,
return 0; event_wait_list, event);
} }
cl_int EnqueueSVMFree(cl_command_queue command_queue, cl_int EnqueueSVMFree(cl_command_queue command_queue,
...@@ -1141,22 +1141,19 @@ cl_mem CreateImage3D(cl_context context, ...@@ -1141,22 +1141,19 @@ cl_mem CreateImage3D(cl_context context,
cl_int EnqueueMarker(cl_command_queue command_queue, cl_event *event) cl_int EnqueueMarker(cl_command_queue command_queue, cl_event *event)
{ {
WARN_NOT_SUPPORTED(EnqueueMarker); return command_queue->cast<CommandQueue>().enqueueMarker(event);
return 0;
} }
cl_int EnqueueWaitForEvents(cl_command_queue command_queue, cl_int EnqueueWaitForEvents(cl_command_queue command_queue,
cl_uint num_events, cl_uint num_events,
const cl_event *event_list) const cl_event *event_list)
{ {
WARN_NOT_SUPPORTED(EnqueueWaitForEvents); return command_queue->cast<CommandQueue>().enqueueWaitForEvents(num_events, event_list);
return 0;
} }
cl_int EnqueueBarrier(cl_command_queue command_queue) cl_int EnqueueBarrier(cl_command_queue command_queue)
{ {
WARN_NOT_SUPPORTED(EnqueueBarrier); return command_queue->cast<CommandQueue>().enqueueBarrier();
return 0;
} }
cl_int UnloadCompiler() cl_int UnloadCompiler()
...@@ -1200,8 +1197,8 @@ cl_int EnqueueTask(cl_command_queue command_queue, ...@@ -1200,8 +1197,8 @@ cl_int EnqueueTask(cl_command_queue command_queue,
const cl_event *event_wait_list, const cl_event *event_wait_list,
cl_event *event) cl_event *event)
{ {
WARN_NOT_SUPPORTED(EnqueueTask); return command_queue->cast<CommandQueue>().enqueueTask(kernel, num_events_in_wait_list,
return 0; event_wait_list, event);
} }
} // namespace cl } // namespace cl
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