mirror of
https://github.com/love2d/love.git
synced 2026-08-21 13:09:56 +02:00
vulkan: add support for custom depth/stencil tex
This commit is contained in:
@@ -240,6 +240,11 @@ void Graphics::present(void *screenshotCallbackdata)
|
|||||||
if (!isActive())
|
if (!isActive())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (isRenderTargetActive())
|
||||||
|
throw love::Exception("present cannot be called while a render target is active.");
|
||||||
|
|
||||||
|
deprecations.draw(this);
|
||||||
|
|
||||||
submitGpuCommands(true);
|
submitGpuCommands(true);
|
||||||
|
|
||||||
VkPresentInfoKHR presentInfo{};
|
VkPresentInfoKHR presentInfo{};
|
||||||
@@ -266,6 +271,13 @@ void Graphics::present(void *screenshotCallbackdata)
|
|||||||
else if (result != VK_SUCCESS)
|
else if (result != VK_SUCCESS)
|
||||||
throw love::Exception("failed to present swap chain image");
|
throw love::Exception("failed to present swap chain image");
|
||||||
|
|
||||||
|
drawCalls = 0;
|
||||||
|
renderTargetSwitchCount = 0;
|
||||||
|
drawCallsBatched = 0;
|
||||||
|
|
||||||
|
updatePendingReadbacks();
|
||||||
|
updateTemporaryResources();
|
||||||
|
|
||||||
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
|
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
|
||||||
|
|
||||||
beginFrame();
|
beginFrame();
|
||||||
@@ -720,7 +732,7 @@ bool Graphics::dispatch(int x, int y, int z)
|
|||||||
|
|
||||||
vkCmdBindPipeline(commandBuffers.at(currentFrame), VK_PIPELINE_BIND_POINT_COMPUTE, computeShader->getComputePipeline());
|
vkCmdBindPipeline(commandBuffers.at(currentFrame), VK_PIPELINE_BIND_POINT_COMPUTE, computeShader->getComputePipeline());
|
||||||
|
|
||||||
computeShader->cmdPushDescriptorSets(commandBuffers.at(currentFrame), currentFrame, VK_PIPELINE_BIND_POINT_COMPUTE);
|
computeShader->cmdPushDescriptorSets(commandBuffers.at(currentFrame), static_cast<uint32_t>(currentFrame), VK_PIPELINE_BIND_POINT_COMPUTE);
|
||||||
|
|
||||||
vkCmdDispatch(commandBuffers.at(currentFrame), static_cast<uint32_t>(x), static_cast<uint32_t>(y), static_cast<uint32_t>(z));
|
vkCmdDispatch(commandBuffers.at(currentFrame), static_cast<uint32_t>(x), static_cast<uint32_t>(y), static_cast<uint32_t>(z));
|
||||||
|
|
||||||
@@ -1475,8 +1487,8 @@ VkPresentModeKHR Graphics::chooseSwapPresentMode(const std::vector<VkPresentMode
|
|||||||
{
|
{
|
||||||
int vsync = Vulkan::getVsync();
|
int vsync = Vulkan::getVsync();
|
||||||
|
|
||||||
auto begin = availablePresentModes.begin();
|
const auto begin = availablePresentModes.begin();
|
||||||
auto end = availablePresentModes.end();
|
const auto end = availablePresentModes.end();
|
||||||
|
|
||||||
switch (vsync) {
|
switch (vsync) {
|
||||||
case -1:
|
case -1:
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ void Shader::unloadVolatile()
|
|||||||
descriptorSetsVector.clear();
|
descriptorSetsVector.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<VkPipelineShaderStageCreateInfo>& Shader::getShaderStages() const
|
const std::vector<VkPipelineShaderStageCreateInfo> &Shader::getShaderStages() const
|
||||||
{
|
{
|
||||||
return shaderStages;
|
return shaderStages;
|
||||||
}
|
}
|
||||||
@@ -232,7 +232,7 @@ VkPipeline Shader::getComputePipeline() const
|
|||||||
return computePipeline;
|
return computePipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
static VkDescriptorImageInfo* createDescriptorImageInfo(graphics::Texture* texture, bool sampler)
|
static VkDescriptorImageInfo* createDescriptorImageInfo(graphics::Texture *texture, bool sampler)
|
||||||
{
|
{
|
||||||
auto vkTexture = (Texture*)texture;
|
auto vkTexture = (Texture*)texture;
|
||||||
|
|
||||||
@@ -409,13 +409,13 @@ void Shader::attach()
|
|||||||
vgfx->setComputeShader(this);
|
vgfx->setComputeShader(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Shader::getVertexAttributeIndex(const std::string& name)
|
int Shader::getVertexAttributeIndex(const std::string &name)
|
||||||
{
|
{
|
||||||
auto it = attributes.find(name);
|
auto it = attributes.find(name);
|
||||||
return it == attributes.end() ? -1 : it->second;
|
return it == attributes.end() ? -1 : it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Shader::UniformInfo* Shader::getUniformInfo(const std::string& name) const
|
const Shader::UniformInfo* Shader::getUniformInfo(const std::string &name) const
|
||||||
{
|
{
|
||||||
return &uniformInfos.at(name);
|
return &uniformInfos.at(name);
|
||||||
}
|
}
|
||||||
@@ -425,7 +425,13 @@ const Shader::UniformInfo* Shader::getUniformInfo(BuiltinUniform builtin) const
|
|||||||
return builtinUniformInfo[builtin];
|
return builtinUniformInfo[builtin];
|
||||||
}
|
}
|
||||||
|
|
||||||
void Shader::sendTextures(const UniformInfo* info, graphics::Texture** textures, int count)
|
void Shader::updateUniform(const UniformInfo* info, int count)
|
||||||
|
{
|
||||||
|
if (current == this)
|
||||||
|
Graphics::flushBatchedDrawsGlobal();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader::sendTextures(const UniformInfo *info, graphics::Texture **textures, int count)
|
||||||
{
|
{
|
||||||
for (unsigned i = 0; i < count; i++)
|
for (unsigned i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
@@ -447,7 +453,7 @@ void Shader::calculateUniformBufferSizeAligned()
|
|||||||
uniformBufferSizeAligned = factor * minAlignment;
|
uniformBufferSizeAligned = factor * minAlignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Shader::buildLocalUniforms(spirv_cross::Compiler& comp, const spirv_cross::SPIRType &type, size_t baseoff, const std::string &basename)
|
void Shader::buildLocalUniforms(spirv_cross::Compiler &comp, const spirv_cross::SPIRType &type, size_t baseoff, const std::string &basename)
|
||||||
{
|
{
|
||||||
using namespace spirv_cross;
|
using namespace spirv_cross;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// LÖVE
|
// LÖVE
|
||||||
#include <graphics/Shader.h>
|
#include "graphics/Shader.h"
|
||||||
#include <graphics/vulkan/ShaderStage.h>
|
#include "graphics/vulkan/ShaderStage.h"
|
||||||
#include "Vulkan.h"
|
#include "Vulkan.h"
|
||||||
|
|
||||||
// Libraries
|
// Libraries
|
||||||
@@ -39,7 +39,7 @@ public:
|
|||||||
|
|
||||||
VkPipeline getComputePipeline() const;
|
VkPipeline getComputePipeline() const;
|
||||||
|
|
||||||
const std::vector<VkPipelineShaderStageCreateInfo>& getShaderStages() const;
|
const std::vector<VkPipelineShaderStageCreateInfo> &getShaderStages() const;
|
||||||
|
|
||||||
const VkPipelineLayout getGraphicsPipelineLayout() const;
|
const VkPipelineLayout getGraphicsPipelineLayout() const;
|
||||||
|
|
||||||
@@ -51,26 +51,23 @@ public:
|
|||||||
|
|
||||||
std::string getWarnings() const override { return ""; }
|
std::string getWarnings() const override { return ""; }
|
||||||
|
|
||||||
int getVertexAttributeIndex(const std::string& name) override;
|
int getVertexAttributeIndex(const std::string &name) override;
|
||||||
|
|
||||||
const UniformInfo* getUniformInfo(const std::string& name) const override;
|
const UniformInfo *getUniformInfo(const std::string &name) const override;
|
||||||
const UniformInfo* getUniformInfo(BuiltinUniform builtin) const override;
|
const UniformInfo *getUniformInfo(BuiltinUniform builtin) const override;
|
||||||
|
|
||||||
// Not needed right now, since the logic that links the values of the uniforms to the shader is done in cmdPushDescriptorSets
|
void updateUniform(const UniformInfo *info, int count) override;
|
||||||
// which gets called from the vulkan::Graphics class whenever a draw call happens.
|
|
||||||
// I'll have to reevaluate the use of this function in the future though.
|
|
||||||
void updateUniform(const UniformInfo* info, int count) override {}
|
|
||||||
|
|
||||||
void sendTextures(const UniformInfo* info, graphics::Texture** textures, int count) override;
|
void sendTextures(const UniformInfo *info, graphics::Texture **textures, int count) override;
|
||||||
void sendBuffers(const UniformInfo* info, love::graphics::Buffer** buffers, int count) override {}
|
void sendBuffers(const UniformInfo *info, love::graphics::Buffer **buffers, int count) override {}
|
||||||
|
|
||||||
bool hasUniform(const std::string& name) const override;
|
bool hasUniform(const std::string &name) const override;
|
||||||
|
|
||||||
void setVideoTextures(graphics::Texture* ytexture, graphics::Texture* cbtexture, graphics::Texture* crtexture) override;
|
void setVideoTextures(graphics::Texture *ytexture, graphics::Texture *cbtexture, graphics::Texture *crtexture) override;
|
||||||
|
|
||||||
// fixme: use normal methods for this in the future.
|
// fixme: use normal methods for this in the future.
|
||||||
void setUniformData(BuiltinUniformData& data);
|
void setUniformData(BuiltinUniformData &data);
|
||||||
void setMainTex(graphics::Texture* texture);
|
void setMainTex(graphics::Texture *texture);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void calculateUniformBufferSizeAligned();
|
void calculateUniformBufferSizeAligned();
|
||||||
@@ -79,10 +76,10 @@ private:
|
|||||||
void createPipelineLayout();
|
void createPipelineLayout();
|
||||||
void createStreamBuffers();
|
void createStreamBuffers();
|
||||||
void buildLocalUniforms(
|
void buildLocalUniforms(
|
||||||
spirv_cross::Compiler& comp,
|
spirv_cross::Compiler &comp,
|
||||||
const spirv_cross::SPIRType& type,
|
const spirv_cross::SPIRType &type,
|
||||||
size_t baseoff,
|
size_t baseoff,
|
||||||
const std::string& basename);
|
const std::string &basename);
|
||||||
|
|
||||||
VkDescriptorSet allocateDescriptorSet();
|
VkDescriptorSet allocateDescriptorSet();
|
||||||
|
|
||||||
@@ -112,7 +109,7 @@ private:
|
|||||||
bool isCompute = false;
|
bool isCompute = false;
|
||||||
|
|
||||||
std::unordered_map<std::string, graphics::Shader::UniformInfo> uniformInfos;
|
std::unordered_map<std::string, graphics::Shader::UniformInfo> uniformInfos;
|
||||||
UniformInfo* builtinUniformInfo[BUILTIN_MAX_ENUM];
|
UniformInfo *builtinUniformInfo[BUILTIN_MAX_ENUM];
|
||||||
|
|
||||||
std::unique_ptr<StreamBuffer> uniformBufferObjectBuffer;
|
std::unique_ptr<StreamBuffer> uniformBufferObjectBuffer;
|
||||||
std::vector<uint8> localUniformStagingData;
|
std::vector<uint8> localUniformStagingData;
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ bool Texture::loadVolatile()
|
|||||||
|
|
||||||
auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
|
auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
|
||||||
|
|
||||||
if (computeWrite)
|
if (isPixelFormatDepthStencil(format))
|
||||||
|
imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||||
|
else if (isPixelFormatDepth(format))
|
||||||
|
imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
|
||||||
|
else if (computeWrite)
|
||||||
imageLayout = VK_IMAGE_LAYOUT_GENERAL;
|
imageLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||||
else
|
else
|
||||||
imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||||
@@ -103,7 +107,7 @@ bool Texture::loadVolatile()
|
|||||||
sliceCount = slices.getSliceCount();
|
sliceCount = slices.getSliceCount();
|
||||||
for (int slice = 0; slice < sliceCount; slice++)
|
for (int slice = 0; slice < sliceCount; slice++)
|
||||||
{
|
{
|
||||||
auto* id = slices.get(slice, mip);
|
auto *id = slices.get(slice, mip);
|
||||||
if (id != nullptr)
|
if (id != nullptr)
|
||||||
uploadImageData(id, mip, slice, 0, 0);
|
uploadImageData(id, mip, slice, 0, 0);
|
||||||
}
|
}
|
||||||
@@ -200,29 +204,54 @@ void Texture::clear()
|
|||||||
{
|
{
|
||||||
auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
|
auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
|
||||||
|
|
||||||
auto clearColor = getClearValue();
|
|
||||||
|
|
||||||
VkImageSubresourceRange range{};
|
VkImageSubresourceRange range{};
|
||||||
|
if (isPixelFormatDepthStencil(format))
|
||||||
|
range.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||||
|
else if (isPixelFormatDepth(format))
|
||||||
|
range.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||||
|
else
|
||||||
range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||||
range.baseMipLevel = 0;
|
range.baseMipLevel = 0;
|
||||||
range.levelCount = VK_REMAINING_MIP_LEVELS;
|
range.levelCount = VK_REMAINING_MIP_LEVELS;
|
||||||
range.baseArrayLayer = 0;
|
range.baseArrayLayer = 0;
|
||||||
range.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
range.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
||||||
|
|
||||||
if (imageLayout != VK_IMAGE_LAYOUT_GENERAL)
|
if (imageLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
||||||
{
|
{
|
||||||
Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage,
|
Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage,
|
||||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
0, range.levelCount, 0, range.layerCount);
|
0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS);
|
||||||
|
|
||||||
|
auto clearColor = getClearValue();
|
||||||
|
|
||||||
vkCmdClearColorImage(commandBuffer, textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1, &range);
|
vkCmdClearColorImage(commandBuffer, textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearColor, 1, &range);
|
||||||
|
|
||||||
Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage,
|
Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage,
|
||||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||||
0, range.levelCount, 0, range.layerCount);
|
0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS);
|
||||||
|
}
|
||||||
|
else if (imageLayout == VK_IMAGE_LAYOUT_GENERAL)
|
||||||
|
{
|
||||||
|
auto clearColor = getClearValue();
|
||||||
|
|
||||||
|
vkCmdClearColorImage(commandBuffer, textureImage, VK_IMAGE_LAYOUT_GENERAL, &clearColor, 1, &range);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
vkCmdClearColorImage(commandBuffer, textureImage, VK_IMAGE_LAYOUT_GENERAL, &clearColor, 1, &range);
|
{
|
||||||
|
Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage,
|
||||||
|
imageLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||||
|
0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS);
|
||||||
|
|
||||||
|
VkClearDepthStencilValue depthStencilColor{};
|
||||||
|
depthStencilColor.depth = 0.0f;
|
||||||
|
depthStencilColor.stencil = 0;
|
||||||
|
|
||||||
|
vkCmdClearDepthStencilImage(commandBuffer, textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &depthStencilColor, 1, &range);
|
||||||
|
|
||||||
|
Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage,
|
||||||
|
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, imageLayout,
|
||||||
|
0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VkClearColorValue Texture::getClearValue()
|
VkClearColorValue Texture::getClearValue()
|
||||||
|
|||||||
@@ -789,6 +789,33 @@ void Vulkan::cmdTransitionImageLayout(VkCommandBuffer commandBuffer, VkImage ima
|
|||||||
sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
destinationStage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
|
destinationStage = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
}
|
}
|
||||||
|
else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED &&
|
||||||
|
(newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL || newLayout == VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL))
|
||||||
|
{
|
||||||
|
barrier.srcAccessMask = 0;
|
||||||
|
barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
|
||||||
|
|
||||||
|
sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||||
|
destinationStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
||||||
|
}
|
||||||
|
else if ((oldLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL || oldLayout == VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL) &&
|
||||||
|
newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
|
||||||
|
{
|
||||||
|
barrier.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
|
||||||
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||||
|
|
||||||
|
sourceStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
||||||
|
destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
|
}
|
||||||
|
else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL &&
|
||||||
|
(newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL || newLayout == VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL))
|
||||||
|
{
|
||||||
|
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||||
|
barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
|
||||||
|
|
||||||
|
sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
|
||||||
|
destinationStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
throw std::invalid_argument("unsupported layout transition!");
|
throw std::invalid_argument("unsupported layout transition!");
|
||||||
|
|
||||||
@@ -801,6 +828,7 @@ void Vulkan::cmdTransitionImageLayout(VkCommandBuffer commandBuffer, VkImage ima
|
|||||||
1, &barrier
|
1, &barrier
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // vulkan
|
} // vulkan
|
||||||
} // graphics
|
} // graphics
|
||||||
} // love
|
} // love
|
||||||
|
|||||||
Reference in New Issue
Block a user