Fix Mesh data downloading to support not yet streamed vertex/index data gather
#932
This commit is contained in:
@@ -527,9 +527,8 @@ namespace FlaxEngine
|
||||
/// <returns>The gathered data.</returns>
|
||||
public Vertex0[] DownloadVertexBuffer0(bool forceGpu = false)
|
||||
{
|
||||
var vertices = VertexCount;
|
||||
var result = new Vertex0[vertices];
|
||||
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.VB0))
|
||||
var result = (Vertex0[])Internal_DownloadBuffer(__unmanagedPtr, forceGpu, typeof(Vertex0), (int)InternalBufferType.VB0);
|
||||
if (result == null)
|
||||
throw new Exception("Failed to download mesh data.");
|
||||
return result;
|
||||
}
|
||||
@@ -541,9 +540,8 @@ namespace FlaxEngine
|
||||
/// <returns>The gathered data.</returns>
|
||||
public Vertex1[] DownloadVertexBuffer1(bool forceGpu = false)
|
||||
{
|
||||
var vertices = VertexCount;
|
||||
var result = new Vertex1[vertices];
|
||||
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.VB1))
|
||||
var result = (Vertex1[])Internal_DownloadBuffer(__unmanagedPtr, forceGpu, typeof(Vertex1), (int)InternalBufferType.VB1);
|
||||
if (result == null)
|
||||
throw new Exception("Failed to download mesh data.");
|
||||
return result;
|
||||
}
|
||||
@@ -560,10 +558,8 @@ namespace FlaxEngine
|
||||
{
|
||||
if (!HasVertexColors)
|
||||
return null;
|
||||
|
||||
var vertices = VertexCount;
|
||||
var result = new Vertex2[vertices];
|
||||
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.VB2))
|
||||
var result = (Vertex2[])Internal_DownloadBuffer(__unmanagedPtr, forceGpu, typeof(Vertex2), (int)InternalBufferType.VB2);
|
||||
if (result == null)
|
||||
throw new Exception("Failed to download mesh data.");
|
||||
return result;
|
||||
}
|
||||
@@ -582,7 +578,7 @@ namespace FlaxEngine
|
||||
var vb1 = DownloadVertexBuffer1(forceGpu);
|
||||
var vb2 = DownloadVertexBuffer2(forceGpu);
|
||||
|
||||
var vertices = VertexCount;
|
||||
var vertices = vb0.Length;
|
||||
var result = new Vertex[vertices];
|
||||
for (int i = 0; i < vertices; i++)
|
||||
{
|
||||
@@ -618,9 +614,8 @@ namespace FlaxEngine
|
||||
/// <returns>The gathered data.</returns>
|
||||
public uint[] DownloadIndexBuffer(bool forceGpu = false)
|
||||
{
|
||||
var triangles = TriangleCount;
|
||||
var result = new uint[triangles * 3];
|
||||
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.IB32))
|
||||
var result = (uint[])Internal_DownloadBuffer(__unmanagedPtr, forceGpu, typeof(uint), (int)InternalBufferType.IB32);
|
||||
if (result == null)
|
||||
throw new Exception("Failed to download mesh data.");
|
||||
return result;
|
||||
}
|
||||
@@ -633,9 +628,8 @@ namespace FlaxEngine
|
||||
/// <returns>The gathered data.</returns>
|
||||
public ushort[] DownloadIndexBufferUShort(bool forceGpu = false)
|
||||
{
|
||||
var triangles = TriangleCount;
|
||||
var result = new ushort[triangles * 3];
|
||||
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.IB16))
|
||||
var result = (ushort[])Internal_DownloadBuffer(__unmanagedPtr, forceGpu, typeof(ushort), (int)InternalBufferType.IB16);
|
||||
if (result == null)
|
||||
throw new Exception("Failed to download mesh data.");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -746,75 +746,11 @@ enum class InternalBufferType
|
||||
IB32 = 4,
|
||||
};
|
||||
|
||||
void ConvertMeshData(Mesh* mesh, InternalBufferType type, MonoArray* resultObj, void* srcData)
|
||||
MonoArray* Mesh::DownloadBuffer(bool forceGpu, MonoReflectionType* resultType, int32 typeI)
|
||||
{
|
||||
auto vertices = mesh->GetVertexCount();
|
||||
auto triangles = mesh->GetTriangleCount();
|
||||
auto indices = triangles * 3;
|
||||
auto use16BitIndexBuffer = mesh->Use16BitIndexBuffer();
|
||||
|
||||
void* managedArrayPtr = mono_array_addr_with_size(resultObj, 0, 0);
|
||||
switch (type)
|
||||
{
|
||||
case InternalBufferType::VB0:
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, srcData, sizeof(VB0ElementType) * vertices);
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::VB1:
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, srcData, sizeof(VB1ElementType) * vertices);
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::VB2:
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, srcData, sizeof(VB2ElementType) * vertices);
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::IB16:
|
||||
{
|
||||
if (use16BitIndexBuffer)
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, srcData, sizeof(uint16) * indices);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto dst = (uint16*)managedArrayPtr;
|
||||
auto src = (uint32*)srcData;
|
||||
for (int32 i = 0; i < indices; i++)
|
||||
{
|
||||
dst[i] = src[i];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::IB32:
|
||||
{
|
||||
if (use16BitIndexBuffer)
|
||||
{
|
||||
auto dst = (uint32*)managedArrayPtr;
|
||||
auto src = (uint16*)srcData;
|
||||
for (int32 i = 0; i < indices; i++)
|
||||
{
|
||||
dst[i] = src[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, srcData, sizeof(uint32) * indices);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Mesh::DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 typeI)
|
||||
{
|
||||
Mesh* mesh = this;
|
||||
InternalBufferType type = (InternalBufferType)typeI;
|
||||
auto mesh = this;
|
||||
auto type = (InternalBufferType)typeI;
|
||||
auto model = mesh->GetModel();
|
||||
ASSERT(model && resultObj);
|
||||
|
||||
ScopeLock lock(model->Locker);
|
||||
|
||||
// Virtual assets always fetch from GPU memory
|
||||
@@ -823,23 +759,7 @@ bool Mesh::DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 typeI)
|
||||
if (!mesh->IsInitialized() && forceGpu)
|
||||
{
|
||||
LOG(Error, "Cannot load mesh data from GPU if it's not loaded.");
|
||||
return true;
|
||||
}
|
||||
if (type == InternalBufferType::IB16 || type == InternalBufferType::IB32)
|
||||
{
|
||||
if (mono_array_length(resultObj) != mesh->GetTriangleCount() * 3)
|
||||
{
|
||||
LOG(Error, "Invalid buffer size.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mono_array_length(resultObj) != mesh->GetVertexCount())
|
||||
{
|
||||
LOG(Error, "Invalid buffer size.");
|
||||
return true;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MeshBufferType bufferType;
|
||||
@@ -859,35 +779,96 @@ bool Mesh::DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 typeI)
|
||||
bufferType = MeshBufferType::Index;
|
||||
break;
|
||||
default:
|
||||
return true;
|
||||
return nullptr;
|
||||
}
|
||||
BytesContainer data;
|
||||
int32 dataCount;
|
||||
if (forceGpu)
|
||||
{
|
||||
// Get data from GPU
|
||||
// TODO: support reusing the input memory buffer to perform a single copy from staging buffer to the input CPU buffer
|
||||
auto task = mesh->DownloadDataGPUAsync(bufferType, data);
|
||||
if (task == nullptr)
|
||||
return true;
|
||||
return nullptr;
|
||||
task->Start();
|
||||
model->Locker.Unlock();
|
||||
if (task->Wait())
|
||||
{
|
||||
LOG(Error, "Task failed.");
|
||||
return true;
|
||||
return nullptr;
|
||||
}
|
||||
model->Locker.Lock();
|
||||
|
||||
// Extract elements count from result data
|
||||
switch (bufferType)
|
||||
{
|
||||
case MeshBufferType::Index:
|
||||
dataCount = data.Length() / (Use16BitIndexBuffer() ? sizeof(uint16) : sizeof(uint32));
|
||||
break;
|
||||
case MeshBufferType::Vertex0:
|
||||
dataCount = data.Length() / sizeof(VB0ElementType);
|
||||
break;
|
||||
case MeshBufferType::Vertex1:
|
||||
dataCount = data.Length() / sizeof(VB1ElementType);
|
||||
break;
|
||||
case MeshBufferType::Vertex2:
|
||||
dataCount = data.Length() / sizeof(VB2ElementType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get data from CPU
|
||||
int32 count;
|
||||
if (DownloadDataCPU(bufferType, data, count))
|
||||
return true;
|
||||
if (DownloadDataCPU(bufferType, data, dataCount))
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ConvertMeshData(mesh, type, resultObj, data.Get());
|
||||
return false;
|
||||
// Convert into managed array
|
||||
MonoArray* result = mono_array_new(mono_domain_get(), mono_type_get_class(mono_reflection_type_get_type(resultType)), dataCount);
|
||||
void* managedArrayPtr = mono_array_addr_with_size(result, 0, 0);
|
||||
const int32 elementSize = data.Length() / dataCount;
|
||||
switch (type)
|
||||
{
|
||||
case InternalBufferType::VB0:
|
||||
case InternalBufferType::VB1:
|
||||
case InternalBufferType::VB2:
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, data.Get(), data.Length());
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::IB16:
|
||||
{
|
||||
if (elementSize == sizeof(uint16))
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, data.Get(), data.Length());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto dst = (uint16*)managedArrayPtr;
|
||||
auto src = (uint32*)data.Get();
|
||||
for (int32 i = 0; i < dataCount; i++)
|
||||
dst[i] = src[i];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::IB32:
|
||||
{
|
||||
if (elementSize == sizeof(uint16))
|
||||
{
|
||||
auto dst = (uint32*)managedArrayPtr;
|
||||
auto src = (uint16*)data.Get();
|
||||
for (int32 i = 0; i < dataCount; i++)
|
||||
dst[i] = src[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, data.Get(), data.Length());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -321,6 +321,6 @@ private:
|
||||
API_FUNCTION(NoProxy) bool UpdateMeshUShort(int32 vertexCount, int32 triangleCount, MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj, MonoArray* colorsObj);
|
||||
API_FUNCTION(NoProxy) bool UpdateTrianglesUInt(int32 triangleCount, MonoArray* trianglesObj);
|
||||
API_FUNCTION(NoProxy) bool UpdateTrianglesUShort(int32 triangleCount, MonoArray* trianglesObj);
|
||||
API_FUNCTION(NoProxy) bool DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 typeI);
|
||||
API_FUNCTION(NoProxy) MonoArray* DownloadBuffer(bool forceGpu, MonoReflectionType* resultType, int32 typeI);
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -516,64 +516,12 @@ enum class InternalBufferType
|
||||
IB32 = 4,
|
||||
};
|
||||
|
||||
void ConvertMeshData(SkinnedMesh* mesh, InternalBufferType type, MonoArray* resultObj, void* srcData)
|
||||
{
|
||||
auto vertices = mesh->GetVertexCount();
|
||||
auto triangles = mesh->GetTriangleCount();
|
||||
auto indices = triangles * 3;
|
||||
auto use16BitIndexBuffer = mesh->Use16BitIndexBuffer();
|
||||
|
||||
void* managedArrayPtr = mono_array_addr_with_size(resultObj, 0, 0);
|
||||
switch (type)
|
||||
{
|
||||
case InternalBufferType::VB0:
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, srcData, sizeof(VB0SkinnedElementType) * vertices);
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::IB16:
|
||||
{
|
||||
if (use16BitIndexBuffer)
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, srcData, sizeof(uint16) * indices);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto dst = (uint16*)managedArrayPtr;
|
||||
auto src = (uint32*)srcData;
|
||||
for (int32 i = 0; i < indices; i++)
|
||||
{
|
||||
dst[i] = src[i];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::IB32:
|
||||
{
|
||||
if (use16BitIndexBuffer)
|
||||
{
|
||||
auto dst = (uint32*)managedArrayPtr;
|
||||
auto src = (uint16*)srcData;
|
||||
for (int32 i = 0; i < indices; i++)
|
||||
{
|
||||
dst[i] = src[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, srcData, sizeof(uint32) * indices);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SkinnedMesh::DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 typeI)
|
||||
MonoArray* SkinnedMesh::DownloadBuffer(bool forceGpu, MonoReflectionType* resultType, int32 typeI)
|
||||
{
|
||||
SkinnedMesh* mesh = this;
|
||||
InternalBufferType type = (InternalBufferType)typeI;
|
||||
auto model = mesh->GetSkinnedModel();
|
||||
ASSERT(model && resultObj);
|
||||
ScopeLock lock(model->Locker);
|
||||
|
||||
// Virtual assets always fetch from GPU memory
|
||||
forceGpu |= model->IsVirtual();
|
||||
@@ -581,23 +529,7 @@ bool SkinnedMesh::DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 type
|
||||
if (!mesh->IsInitialized() && forceGpu)
|
||||
{
|
||||
LOG(Error, "Cannot load mesh data from GPU if it's not loaded.");
|
||||
return true;
|
||||
}
|
||||
if (type == InternalBufferType::IB16 || type == InternalBufferType::IB32)
|
||||
{
|
||||
if (mono_array_length(resultObj) != mesh->GetTriangleCount() * 3)
|
||||
{
|
||||
LOG(Error, "Invalid buffer size.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mono_array_length(resultObj) != mesh->GetVertexCount())
|
||||
{
|
||||
LOG(Error, "Invalid buffer size.");
|
||||
return true;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MeshBufferType bufferType;
|
||||
@@ -610,37 +542,89 @@ bool SkinnedMesh::DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 type
|
||||
case InternalBufferType::IB32:
|
||||
bufferType = MeshBufferType::Index;
|
||||
break;
|
||||
default: CRASH;
|
||||
return true;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
BytesContainer data;
|
||||
int32 dataCount;
|
||||
if (forceGpu)
|
||||
{
|
||||
// Get data from GPU
|
||||
// TODO: support reusing the input memory buffer to perform a single copy from staging buffer to the input CPU buffer
|
||||
auto task = mesh->DownloadDataGPUAsync(bufferType, data);
|
||||
if (task == nullptr)
|
||||
return true;
|
||||
return nullptr;
|
||||
task->Start();
|
||||
model->Locker.Unlock();
|
||||
if (task->Wait())
|
||||
{
|
||||
LOG(Error, "Task failed.");
|
||||
return true;
|
||||
return nullptr;
|
||||
}
|
||||
model->Locker.Lock();
|
||||
|
||||
// Extract elements count from result data
|
||||
switch (bufferType)
|
||||
{
|
||||
case MeshBufferType::Index:
|
||||
dataCount = data.Length() / (Use16BitIndexBuffer() ? sizeof(uint16) : sizeof(uint32));
|
||||
break;
|
||||
case MeshBufferType::Vertex0:
|
||||
dataCount = data.Length() / sizeof(VB0SkinnedElementType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get data from CPU
|
||||
int32 count;
|
||||
if (DownloadDataCPU(bufferType, data, count))
|
||||
return true;
|
||||
if (DownloadDataCPU(bufferType, data, dataCount))
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Convert into managed memory
|
||||
ConvertMeshData(mesh, type, resultObj, data.Get());
|
||||
return false;
|
||||
// Convert into managed array
|
||||
MonoArray* result = mono_array_new(mono_domain_get(), mono_type_get_class(mono_reflection_type_get_type(resultType)), dataCount);
|
||||
void* managedArrayPtr = mono_array_addr_with_size(result, 0, 0);
|
||||
const int32 elementSize = data.Length() / dataCount;
|
||||
switch (type)
|
||||
{
|
||||
case InternalBufferType::VB0:
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, data.Get(), data.Length());
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::IB16:
|
||||
{
|
||||
if (elementSize == sizeof(uint16))
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, data.Get(), data.Length());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto dst = (uint16*)managedArrayPtr;
|
||||
auto src = (uint32*)data.Get();
|
||||
for (int32 i = 0; i < dataCount; i++)
|
||||
dst[i] = src[i];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case InternalBufferType::IB32:
|
||||
{
|
||||
if (elementSize == sizeof(uint16))
|
||||
{
|
||||
auto dst = (uint32*)managedArrayPtr;
|
||||
auto src = (uint16*)data.Get();
|
||||
for (int32 i = 0; i < dataCount; i++)
|
||||
dst[i] = src[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform::MemoryCopy(managedArrayPtr, data.Get(), data.Length());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -199,6 +199,6 @@ private:
|
||||
#if !COMPILE_WITHOUT_CSHARP
|
||||
API_FUNCTION(NoProxy) bool UpdateMeshUInt(MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* blendIndicesObj, MonoArray* blendWeightsObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj);
|
||||
API_FUNCTION(NoProxy) bool UpdateMeshUShort(MonoArray* verticesObj, MonoArray* trianglesObj, MonoArray* blendIndicesObj, MonoArray* blendWeightsObj, MonoArray* normalsObj, MonoArray* tangentsObj, MonoArray* uvObj);
|
||||
API_FUNCTION(NoProxy) bool DownloadBuffer(bool forceGpu, MonoArray* resultObj, int32 typeI);
|
||||
API_FUNCTION(NoProxy) MonoArray* DownloadBuffer(bool forceGpu, MonoReflectionType* resultType, int32 typeI);
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -274,9 +274,8 @@ namespace FlaxEngine
|
||||
/// <returns>The gathered data.</returns>
|
||||
public Vertex0[] DownloadVertexBuffer0(bool forceGpu = false)
|
||||
{
|
||||
var vertices = VertexCount;
|
||||
var result = new Vertex0[vertices];
|
||||
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.VB0))
|
||||
var result = (Vertex0[])Internal_DownloadBuffer(__unmanagedPtr, forceGpu, typeof(Vertex0), (int)InternalBufferType.VB0);
|
||||
if (result == null)
|
||||
throw new Exception("Failed to download mesh data.");
|
||||
return result;
|
||||
}
|
||||
@@ -292,7 +291,7 @@ namespace FlaxEngine
|
||||
|
||||
var vb0 = DownloadVertexBuffer0(forceGpu);
|
||||
|
||||
var vertices = VertexCount;
|
||||
var vertices = vb0.Length;
|
||||
var result = new Vertex[vertices];
|
||||
for (int i = 0; i < vertices; i++)
|
||||
{
|
||||
@@ -319,9 +318,8 @@ namespace FlaxEngine
|
||||
/// <returns>The gathered data.</returns>
|
||||
public uint[] DownloadIndexBuffer(bool forceGpu = false)
|
||||
{
|
||||
var triangles = TriangleCount;
|
||||
var result = new uint[triangles * 3];
|
||||
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.IB32))
|
||||
var result = (uint[])Internal_DownloadBuffer(__unmanagedPtr, forceGpu, typeof(uint), (int)InternalBufferType.IB32);
|
||||
if (result == null)
|
||||
throw new Exception("Failed to download mesh data.");
|
||||
return result;
|
||||
}
|
||||
@@ -334,9 +332,8 @@ namespace FlaxEngine
|
||||
/// <returns>The gathered data.</returns>
|
||||
public ushort[] DownloadIndexBufferUShort(bool forceGpu = false)
|
||||
{
|
||||
var triangles = TriangleCount;
|
||||
var result = new ushort[triangles * 3];
|
||||
if (Internal_DownloadBuffer(__unmanagedPtr, forceGpu, result, (int)InternalBufferType.IB16))
|
||||
var result = (ushort[])Internal_DownloadBuffer(__unmanagedPtr, forceGpu, typeof(ushort), (int)InternalBufferType.IB16);
|
||||
if (result == null)
|
||||
throw new Exception("Failed to download mesh data.");
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user