mirror of
https://github.com/libsdl-org/SDL.git
synced 2025-09-06 19:38:14 +00:00
GPU Vulkan: Fix recursive Submit calls causing defrag to fail (#12718)
--------- Co-authored-by: Sam Lantinga <slouken@libsdl.org>
This commit is contained in:
@@ -1195,7 +1195,7 @@ struct VulkanRenderer
|
|||||||
|
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
|
|
||||||
static bool VULKAN_INTERNAL_DefragmentMemory(VulkanRenderer *renderer);
|
static bool VULKAN_INTERNAL_DefragmentMemory(VulkanRenderer *renderer, VulkanCommandBuffer *commandBuffer);
|
||||||
static bool VULKAN_INTERNAL_BeginCommandBuffer(VulkanRenderer *renderer, VulkanCommandBuffer *commandBuffer);
|
static bool VULKAN_INTERNAL_BeginCommandBuffer(VulkanRenderer *renderer, VulkanCommandBuffer *commandBuffer);
|
||||||
static void VULKAN_ReleaseWindow(SDL_GPURenderer *driverData, SDL_Window *window);
|
static void VULKAN_ReleaseWindow(SDL_GPURenderer *driverData, SDL_Window *window);
|
||||||
static bool VULKAN_Wait(SDL_GPURenderer *driverData);
|
static bool VULKAN_Wait(SDL_GPURenderer *driverData);
|
||||||
@@ -5578,6 +5578,7 @@ static void VULKAN_PopDebugGroup(
|
|||||||
|
|
||||||
static VulkanTexture *VULKAN_INTERNAL_CreateTexture(
|
static VulkanTexture *VULKAN_INTERNAL_CreateTexture(
|
||||||
VulkanRenderer *renderer,
|
VulkanRenderer *renderer,
|
||||||
|
bool transitionToDefaultLayout,
|
||||||
const SDL_GPUTextureCreateInfo *createinfo)
|
const SDL_GPUTextureCreateInfo *createinfo)
|
||||||
{
|
{
|
||||||
VkResult vulkanResult;
|
VkResult vulkanResult;
|
||||||
@@ -5805,15 +5806,17 @@ static VulkanTexture *VULKAN_INTERNAL_CreateTexture(
|
|||||||
&nameInfo);
|
&nameInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's transition to the default barrier state, because for some reason Vulkan doesn't let us do that with initialLayout.
|
if (transitionToDefaultLayout) {
|
||||||
VulkanCommandBuffer *barrierCommandBuffer = (VulkanCommandBuffer *)VULKAN_AcquireCommandBuffer((SDL_GPURenderer *)renderer);
|
// Let's transition to the default barrier state, because for some reason Vulkan doesn't let us do that with initialLayout.
|
||||||
VULKAN_INTERNAL_TextureTransitionToDefaultUsage(
|
VulkanCommandBuffer *barrierCommandBuffer = (VulkanCommandBuffer *)VULKAN_AcquireCommandBuffer((SDL_GPURenderer *)renderer);
|
||||||
renderer,
|
VULKAN_INTERNAL_TextureTransitionToDefaultUsage(
|
||||||
barrierCommandBuffer,
|
renderer,
|
||||||
VULKAN_TEXTURE_USAGE_MODE_UNINITIALIZED,
|
barrierCommandBuffer,
|
||||||
texture);
|
VULKAN_TEXTURE_USAGE_MODE_UNINITIALIZED,
|
||||||
VULKAN_INTERNAL_TrackTexture(barrierCommandBuffer, texture);
|
texture);
|
||||||
VULKAN_Submit((SDL_GPUCommandBuffer *)barrierCommandBuffer);
|
VULKAN_INTERNAL_TrackTexture(barrierCommandBuffer, texture);
|
||||||
|
VULKAN_Submit((SDL_GPUCommandBuffer *)barrierCommandBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
@@ -5863,6 +5866,7 @@ static void VULKAN_INTERNAL_CycleActiveBuffer(
|
|||||||
|
|
||||||
static void VULKAN_INTERNAL_CycleActiveTexture(
|
static void VULKAN_INTERNAL_CycleActiveTexture(
|
||||||
VulkanRenderer *renderer,
|
VulkanRenderer *renderer,
|
||||||
|
VulkanCommandBuffer *commandBuffer,
|
||||||
VulkanTextureContainer *container)
|
VulkanTextureContainer *container)
|
||||||
{
|
{
|
||||||
VulkanTexture *texture;
|
VulkanTexture *texture;
|
||||||
@@ -5880,8 +5884,15 @@ static void VULKAN_INTERNAL_CycleActiveTexture(
|
|||||||
// No texture is available, generate a new one.
|
// No texture is available, generate a new one.
|
||||||
texture = VULKAN_INTERNAL_CreateTexture(
|
texture = VULKAN_INTERNAL_CreateTexture(
|
||||||
renderer,
|
renderer,
|
||||||
|
false,
|
||||||
&container->header.info);
|
&container->header.info);
|
||||||
|
|
||||||
|
VULKAN_INTERNAL_TextureTransitionToDefaultUsage(
|
||||||
|
renderer,
|
||||||
|
commandBuffer,
|
||||||
|
VULKAN_TEXTURE_USAGE_MODE_UNINITIALIZED,
|
||||||
|
texture);
|
||||||
|
|
||||||
if (!texture) {
|
if (!texture) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5945,6 +5956,7 @@ static VulkanTextureSubresource *VULKAN_INTERNAL_PrepareTextureSubresourceForWri
|
|||||||
SDL_GetAtomicInt(&textureContainer->activeTexture->referenceCount) > 0) {
|
SDL_GetAtomicInt(&textureContainer->activeTexture->referenceCount) > 0) {
|
||||||
VULKAN_INTERNAL_CycleActiveTexture(
|
VULKAN_INTERNAL_CycleActiveTexture(
|
||||||
renderer,
|
renderer,
|
||||||
|
commandBuffer,
|
||||||
textureContainer);
|
textureContainer);
|
||||||
|
|
||||||
textureSubresource = VULKAN_INTERNAL_FetchTextureSubresource(
|
textureSubresource = VULKAN_INTERNAL_FetchTextureSubresource(
|
||||||
@@ -6726,6 +6738,7 @@ static SDL_GPUTexture *VULKAN_CreateTexture(
|
|||||||
|
|
||||||
texture = VULKAN_INTERNAL_CreateTexture(
|
texture = VULKAN_INTERNAL_CreateTexture(
|
||||||
renderer,
|
renderer,
|
||||||
|
true,
|
||||||
createinfo);
|
createinfo);
|
||||||
|
|
||||||
if (texture == NULL) {
|
if (texture == NULL) {
|
||||||
@@ -6900,7 +6913,7 @@ static void VULKAN_INTERNAL_ReleaseBuffer(
|
|||||||
renderer->buffersToDestroy[renderer->buffersToDestroyCount] = vulkanBuffer;
|
renderer->buffersToDestroy[renderer->buffersToDestroyCount] = vulkanBuffer;
|
||||||
renderer->buffersToDestroyCount += 1;
|
renderer->buffersToDestroyCount += 1;
|
||||||
|
|
||||||
vulkanBuffer->markedForDestroy = 1;
|
vulkanBuffer->markedForDestroy = true;
|
||||||
vulkanBuffer->container = NULL;
|
vulkanBuffer->container = NULL;
|
||||||
|
|
||||||
SDL_UnlockMutex(renderer->disposeLock);
|
SDL_UnlockMutex(renderer->disposeLock);
|
||||||
@@ -10417,7 +10430,7 @@ static bool VULKAN_Submit(
|
|||||||
Uint32 swapchainImageIndex;
|
Uint32 swapchainImageIndex;
|
||||||
VulkanTextureSubresource *swapchainTextureSubresource;
|
VulkanTextureSubresource *swapchainTextureSubresource;
|
||||||
VulkanMemorySubAllocator *allocator;
|
VulkanMemorySubAllocator *allocator;
|
||||||
bool presenting = false;
|
bool presenting = (vulkanCommandBuffer->presentDataCount > 0);
|
||||||
|
|
||||||
SDL_LockMutex(renderer->submitLock);
|
SDL_LockMutex(renderer->submitLock);
|
||||||
|
|
||||||
@@ -10440,6 +10453,15 @@ static bool VULKAN_Submit(
|
|||||||
swapchainTextureSubresource);
|
swapchainTextureSubresource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (presenting &&
|
||||||
|
renderer->allocationsToDefragCount > 0 &&
|
||||||
|
!renderer->defragInProgress) {
|
||||||
|
if (!VULKAN_INTERNAL_DefragmentMemory(renderer, vulkanCommandBuffer))
|
||||||
|
{
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s", "Failed to defragment memory, likely OOM!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!VULKAN_INTERNAL_EndCommandBuffer(renderer, vulkanCommandBuffer)) {
|
if (!VULKAN_INTERNAL_EndCommandBuffer(renderer, vulkanCommandBuffer)) {
|
||||||
SDL_UnlockMutex(renderer->submitLock);
|
SDL_UnlockMutex(renderer->submitLock);
|
||||||
return false;
|
return false;
|
||||||
@@ -10477,11 +10499,7 @@ static bool VULKAN_Submit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Present, if applicable
|
// Present, if applicable
|
||||||
bool result = true;
|
|
||||||
|
|
||||||
for (Uint32 j = 0; j < vulkanCommandBuffer->presentDataCount; j += 1) {
|
for (Uint32 j = 0; j < vulkanCommandBuffer->presentDataCount; j += 1) {
|
||||||
presenting = true;
|
|
||||||
|
|
||||||
presentData = &vulkanCommandBuffer->presentDatas[j];
|
presentData = &vulkanCommandBuffer->presentDatas[j];
|
||||||
|
|
||||||
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
|
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
|
||||||
@@ -10519,60 +10537,51 @@ static bool VULKAN_Submit(
|
|||||||
(presentData->windowData->frameCounter + 1) % renderer->allowedFramesInFlight;
|
(presentData->windowData->frameCounter + 1) % renderer->allowedFramesInFlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we can perform any cleanups
|
// If presenting, check if we can perform any cleanups
|
||||||
|
if (presenting) {
|
||||||
|
for (Sint32 i = renderer->submittedCommandBufferCount - 1; i >= 0; i -= 1) {
|
||||||
|
vulkanResult = renderer->vkGetFenceStatus(
|
||||||
|
renderer->logicalDevice,
|
||||||
|
renderer->submittedCommandBuffers[i]->inFlightFence->fence);
|
||||||
|
|
||||||
for (Sint32 i = renderer->submittedCommandBufferCount - 1; i >= 0; i -= 1) {
|
if (vulkanResult == VK_SUCCESS) {
|
||||||
vulkanResult = renderer->vkGetFenceStatus(
|
VULKAN_INTERNAL_CleanCommandBuffer(
|
||||||
renderer->logicalDevice,
|
renderer,
|
||||||
renderer->submittedCommandBuffers[i]->inFlightFence->fence);
|
renderer->submittedCommandBuffers[i],
|
||||||
|
false);
|
||||||
if (vulkanResult == VK_SUCCESS) {
|
|
||||||
VULKAN_INTERNAL_CleanCommandBuffer(
|
|
||||||
renderer,
|
|
||||||
renderer->submittedCommandBuffers[i],
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (renderer->checkEmptyAllocations) {
|
|
||||||
SDL_LockMutex(renderer->allocatorLock);
|
|
||||||
|
|
||||||
for (Uint32 i = 0; i < VK_MAX_MEMORY_TYPES; i += 1) {
|
|
||||||
allocator = &renderer->memoryAllocator->subAllocators[i];
|
|
||||||
|
|
||||||
for (Sint32 j = allocator->allocationCount - 1; j >= 0; j -= 1) {
|
|
||||||
if (allocator->allocations[j]->usedRegionCount == 0) {
|
|
||||||
VULKAN_INTERNAL_DeallocateMemory(
|
|
||||||
renderer,
|
|
||||||
allocator,
|
|
||||||
j);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer->checkEmptyAllocations = false;
|
if (renderer->checkEmptyAllocations) {
|
||||||
|
SDL_LockMutex(renderer->allocatorLock);
|
||||||
|
|
||||||
SDL_UnlockMutex(renderer->allocatorLock);
|
for (Uint32 i = 0; i < VK_MAX_MEMORY_TYPES; i += 1) {
|
||||||
}
|
allocator = &renderer->memoryAllocator->subAllocators[i];
|
||||||
|
|
||||||
// Check pending destroys
|
for (Sint32 j = allocator->allocationCount - 1; j >= 0; j -= 1) {
|
||||||
VULKAN_INTERNAL_PerformPendingDestroys(renderer);
|
if (allocator->allocations[j]->usedRegionCount == 0) {
|
||||||
|
VULKAN_INTERNAL_DeallocateMemory(
|
||||||
|
renderer,
|
||||||
|
allocator,
|
||||||
|
j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Defrag!
|
renderer->checkEmptyAllocations = false;
|
||||||
if (
|
|
||||||
presenting &&
|
SDL_UnlockMutex(renderer->allocatorLock);
|
||||||
renderer->allocationsToDefragCount > 0 &&
|
}
|
||||||
!renderer->defragInProgress) {
|
|
||||||
result = VULKAN_INTERNAL_DefragmentMemory(renderer);
|
VULKAN_INTERNAL_PerformPendingDestroys(renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark command buffer as submitted
|
// Mark command buffer as submitted
|
||||||
// This must happen after defrag, because it will try to acquire new command buffers.
|
|
||||||
VULKAN_INTERNAL_ReleaseCommandBuffer(vulkanCommandBuffer);
|
VULKAN_INTERNAL_ReleaseCommandBuffer(vulkanCommandBuffer);
|
||||||
|
|
||||||
SDL_UnlockMutex(renderer->submitLock);
|
SDL_UnlockMutex(renderer->submitLock);
|
||||||
|
|
||||||
return result;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool VULKAN_Cancel(
|
static bool VULKAN_Cancel(
|
||||||
@@ -10599,43 +10608,28 @@ static bool VULKAN_Cancel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool VULKAN_INTERNAL_DefragmentMemory(
|
static bool VULKAN_INTERNAL_DefragmentMemory(
|
||||||
VulkanRenderer *renderer)
|
VulkanRenderer *renderer,
|
||||||
|
VulkanCommandBuffer *commandBuffer)
|
||||||
{
|
{
|
||||||
VulkanMemoryAllocation *allocation;
|
|
||||||
VulkanMemoryUsedRegion *currentRegion;
|
|
||||||
VulkanBuffer *newBuffer;
|
|
||||||
VulkanTexture *newTexture;
|
|
||||||
VkBufferCopy bufferCopy;
|
|
||||||
VkImageCopy imageCopy;
|
|
||||||
VulkanCommandBuffer *commandBuffer;
|
|
||||||
VulkanTextureSubresource *srcSubresource;
|
|
||||||
VulkanTextureSubresource *dstSubresource;
|
|
||||||
Uint32 i, subresourceIndex;
|
|
||||||
|
|
||||||
renderer->defragInProgress = 1;
|
renderer->defragInProgress = 1;
|
||||||
|
|
||||||
commandBuffer = (VulkanCommandBuffer *)VULKAN_AcquireCommandBuffer((SDL_GPURenderer *)renderer);
|
|
||||||
if (commandBuffer == NULL) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
commandBuffer->isDefrag = 1;
|
commandBuffer->isDefrag = 1;
|
||||||
|
|
||||||
SDL_LockMutex(renderer->allocatorLock);
|
SDL_LockMutex(renderer->allocatorLock);
|
||||||
|
|
||||||
allocation = renderer->allocationsToDefrag[renderer->allocationsToDefragCount - 1];
|
VulkanMemoryAllocation *allocation = renderer->allocationsToDefrag[renderer->allocationsToDefragCount - 1];
|
||||||
renderer->allocationsToDefragCount -= 1;
|
renderer->allocationsToDefragCount -= 1;
|
||||||
|
|
||||||
/* For each used region in the allocation
|
/* For each used region in the allocation
|
||||||
* create a new resource, copy the data
|
* create a new resource, copy the data
|
||||||
* and re-point the resource containers
|
* and re-point the resource containers
|
||||||
*/
|
*/
|
||||||
for (i = 0; i < allocation->usedRegionCount; i += 1) {
|
for (Uint32 i = 0; i < allocation->usedRegionCount; i += 1) {
|
||||||
currentRegion = allocation->usedRegions[i];
|
VulkanMemoryUsedRegion *currentRegion = allocation->usedRegions[i];
|
||||||
|
|
||||||
if (currentRegion->isBuffer && !currentRegion->vulkanBuffer->markedForDestroy) {
|
if (currentRegion->isBuffer && !currentRegion->vulkanBuffer->markedForDestroy) {
|
||||||
currentRegion->vulkanBuffer->usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
currentRegion->vulkanBuffer->usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||||
|
|
||||||
newBuffer = VULKAN_INTERNAL_CreateBuffer(
|
VulkanBuffer *newBuffer = VULKAN_INTERNAL_CreateBuffer(
|
||||||
renderer,
|
renderer,
|
||||||
currentRegion->vulkanBuffer->size,
|
currentRegion->vulkanBuffer->size,
|
||||||
currentRegion->vulkanBuffer->usage,
|
currentRegion->vulkanBuffer->usage,
|
||||||
@@ -10645,6 +10639,7 @@ static bool VULKAN_INTERNAL_DefragmentMemory(
|
|||||||
|
|
||||||
if (newBuffer == NULL) {
|
if (newBuffer == NULL) {
|
||||||
SDL_UnlockMutex(renderer->allocatorLock);
|
SDL_UnlockMutex(renderer->allocatorLock);
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s", "Failed to allocate defrag buffer!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10663,6 +10658,7 @@ static bool VULKAN_INTERNAL_DefragmentMemory(
|
|||||||
VULKAN_BUFFER_USAGE_MODE_COPY_DESTINATION,
|
VULKAN_BUFFER_USAGE_MODE_COPY_DESTINATION,
|
||||||
newBuffer);
|
newBuffer);
|
||||||
|
|
||||||
|
VkBufferCopy bufferCopy;
|
||||||
bufferCopy.srcOffset = 0;
|
bufferCopy.srcOffset = 0;
|
||||||
bufferCopy.dstOffset = 0;
|
bufferCopy.dstOffset = 0;
|
||||||
bufferCopy.size = currentRegion->resourceSize;
|
bufferCopy.size = currentRegion->resourceSize;
|
||||||
@@ -10702,20 +10698,22 @@ static bool VULKAN_INTERNAL_DefragmentMemory(
|
|||||||
|
|
||||||
VULKAN_INTERNAL_ReleaseBuffer(renderer, currentRegion->vulkanBuffer);
|
VULKAN_INTERNAL_ReleaseBuffer(renderer, currentRegion->vulkanBuffer);
|
||||||
} else if (!currentRegion->isBuffer && !currentRegion->vulkanTexture->markedForDestroy) {
|
} else if (!currentRegion->isBuffer && !currentRegion->vulkanTexture->markedForDestroy) {
|
||||||
newTexture = VULKAN_INTERNAL_CreateTexture(
|
VulkanTexture *newTexture = VULKAN_INTERNAL_CreateTexture(
|
||||||
renderer,
|
renderer,
|
||||||
|
false,
|
||||||
¤tRegion->vulkanTexture->container->header.info);
|
¤tRegion->vulkanTexture->container->header.info);
|
||||||
|
|
||||||
if (newTexture == NULL) {
|
if (newTexture == NULL) {
|
||||||
SDL_UnlockMutex(renderer->allocatorLock);
|
SDL_UnlockMutex(renderer->allocatorLock);
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_GPU, "%s", "Failed to allocate defrag buffer!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_GPUTextureCreateInfo info = currentRegion->vulkanTexture->container->header.info;
|
SDL_GPUTextureCreateInfo info = currentRegion->vulkanTexture->container->header.info;
|
||||||
for (subresourceIndex = 0; subresourceIndex < currentRegion->vulkanTexture->subresourceCount; subresourceIndex += 1) {
|
for (Uint32 subresourceIndex = 0; subresourceIndex < currentRegion->vulkanTexture->subresourceCount; subresourceIndex += 1) {
|
||||||
// copy subresource if necessary
|
// copy subresource if necessary
|
||||||
srcSubresource = ¤tRegion->vulkanTexture->subresources[subresourceIndex];
|
VulkanTextureSubresource *srcSubresource = ¤tRegion->vulkanTexture->subresources[subresourceIndex];
|
||||||
dstSubresource = &newTexture->subresources[subresourceIndex];
|
VulkanTextureSubresource *dstSubresource = &newTexture->subresources[subresourceIndex];
|
||||||
|
|
||||||
VULKAN_INTERNAL_TextureSubresourceTransitionFromDefaultUsage(
|
VULKAN_INTERNAL_TextureSubresourceTransitionFromDefaultUsage(
|
||||||
renderer,
|
renderer,
|
||||||
@@ -10723,12 +10721,14 @@ static bool VULKAN_INTERNAL_DefragmentMemory(
|
|||||||
VULKAN_TEXTURE_USAGE_MODE_COPY_SOURCE,
|
VULKAN_TEXTURE_USAGE_MODE_COPY_SOURCE,
|
||||||
srcSubresource);
|
srcSubresource);
|
||||||
|
|
||||||
VULKAN_INTERNAL_TextureSubresourceTransitionFromDefaultUsage(
|
VULKAN_INTERNAL_TextureSubresourceMemoryBarrier(
|
||||||
renderer,
|
renderer,
|
||||||
commandBuffer,
|
commandBuffer,
|
||||||
|
VULKAN_TEXTURE_USAGE_MODE_UNINITIALIZED,
|
||||||
VULKAN_TEXTURE_USAGE_MODE_COPY_DESTINATION,
|
VULKAN_TEXTURE_USAGE_MODE_COPY_DESTINATION,
|
||||||
dstSubresource);
|
dstSubresource);
|
||||||
|
|
||||||
|
VkImageCopy imageCopy;
|
||||||
imageCopy.srcOffset.x = 0;
|
imageCopy.srcOffset.x = 0;
|
||||||
imageCopy.srcOffset.y = 0;
|
imageCopy.srcOffset.y = 0;
|
||||||
imageCopy.srcOffset.z = 0;
|
imageCopy.srcOffset.z = 0;
|
||||||
@@ -10780,8 +10780,7 @@ static bool VULKAN_INTERNAL_DefragmentMemory(
|
|||||||
|
|
||||||
SDL_UnlockMutex(renderer->allocatorLock);
|
SDL_UnlockMutex(renderer->allocatorLock);
|
||||||
|
|
||||||
return VULKAN_Submit(
|
return true;
|
||||||
(SDL_GPUCommandBuffer *)commandBuffer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format Info
|
// Format Info
|
||||||
|
Reference in New Issue
Block a user