mirror of
https://github.com/hrydgard/ppsspp.git
synced 2024-11-23 21:39:52 +00:00
Always use array textures for framebuffers in Vulkan for simplicity.
This commit is contained in:
parent
2bea495981
commit
91259aaad7
@ -101,7 +101,7 @@ public:
|
||||
|
||||
void InvalidateCachedState() override;
|
||||
|
||||
void BindTextures(int start, int count, Texture **textures) override;
|
||||
void BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) override;
|
||||
void BindNativeTexture(int index, void *nativeTexture) override;
|
||||
void BindSamplerStates(int start, int count, SamplerState **states) override;
|
||||
void BindVertexBuffers(int start, int count, Buffer **buffers, const int *offsets) override;
|
||||
@ -1381,7 +1381,7 @@ Framebuffer *D3D11DrawContext::CreateFramebuffer(const FramebufferDesc &desc) {
|
||||
return fb;
|
||||
}
|
||||
|
||||
void D3D11DrawContext::BindTextures(int start, int count, Texture **textures) {
|
||||
void D3D11DrawContext::BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) {
|
||||
// Collect the resource views from the textures.
|
||||
ID3D11ShaderResourceView *views[MAX_BOUND_TEXTURES];
|
||||
_assert_(start + count <= ARRAY_SIZE(views));
|
||||
|
@ -538,7 +538,7 @@ public:
|
||||
|
||||
void GetFramebufferDimensions(Framebuffer *fbo, int *w, int *h) override;
|
||||
|
||||
void BindTextures(int start, int count, Texture **textures) override;
|
||||
void BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) override;
|
||||
void BindNativeTexture(int index, void *nativeTexture) override;
|
||||
|
||||
void BindSamplerStates(int start, int count, SamplerState **states) override {
|
||||
@ -912,7 +912,7 @@ Texture *D3D9Context::CreateTexture(const TextureDesc &desc) {
|
||||
return tex;
|
||||
}
|
||||
|
||||
void D3D9Context::BindTextures(int start, int count, Texture **textures) {
|
||||
void D3D9Context::BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) {
|
||||
_assert_(start + count <= MAX_BOUND_TEXTURES);
|
||||
for (int i = start; i < start + count; i++) {
|
||||
D3D9Texture *tex = static_cast<D3D9Texture *>(textures[i - start]);
|
||||
|
@ -397,7 +397,7 @@ public:
|
||||
curPipeline_->depthStencil->stencilPass);
|
||||
}
|
||||
|
||||
void BindTextures(int start, int count, Texture **textures) override;
|
||||
void BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) override;
|
||||
void BindNativeTexture(int sampler, void *nativeTexture) override;
|
||||
|
||||
void BindPipeline(Pipeline *pipeline) override;
|
||||
@ -1127,7 +1127,7 @@ Pipeline *OpenGLContext::CreateGraphicsPipeline(const PipelineDesc &desc, const
|
||||
}
|
||||
}
|
||||
|
||||
void OpenGLContext::BindTextures(int start, int count, Texture **textures) {
|
||||
void OpenGLContext::BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) {
|
||||
_assert_(start + count <= MAX_TEXTURE_SLOTS);
|
||||
for (int i = start; i < start + count; i++) {
|
||||
OpenGLTexture *glTex = static_cast<OpenGLTexture *>(textures[i - start]);
|
||||
|
@ -4,9 +4,12 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef> // for size_t
|
||||
|
||||
#include "Common/Common.h"
|
||||
|
||||
// GLSL_1xx and GLSL_3xx each cover a lot of sub variants. All the little quirks
|
||||
// that differ are covered in ShaderLanguageDesc.
|
||||
// Defined as a bitmask so stuff like GetSupportedShaderLanguages can return combinations.
|
||||
// TODO: We can probably move away from this distinction soon, now that we mostly generate/translate shaders.
|
||||
enum ShaderLanguage {
|
||||
GLSL_1xx = 1,
|
||||
GLSL_3xx = 2,
|
||||
@ -30,7 +33,6 @@ enum class ShaderStage {
|
||||
|
||||
const char *ShaderStageAsString(ShaderStage lang);
|
||||
|
||||
|
||||
struct ShaderLanguageDesc {
|
||||
ShaderLanguageDesc() {}
|
||||
explicit ShaderLanguageDesc(ShaderLanguage lang);
|
||||
@ -91,14 +93,18 @@ struct UniformDef {
|
||||
int index;
|
||||
};
|
||||
|
||||
enum class SamplerFlags {
|
||||
ARRAY_ON_VULKAN = 1,
|
||||
};
|
||||
ENUM_CLASS_BITOPS(SamplerFlags);
|
||||
|
||||
struct SamplerDef {
|
||||
int binding; // Might only be used by some backends.
|
||||
const char *name;
|
||||
bool array;
|
||||
SamplerFlags flags;
|
||||
// TODO: Might need unsigned samplers, 3d samplers, or other types in the future.
|
||||
};
|
||||
|
||||
|
||||
// For passing error messages from shader compilation (and other critical issues) back to the host.
|
||||
// This can run on any thread - be aware!
|
||||
// TODO: See if we can find a less generic name for this.
|
||||
|
@ -465,15 +465,6 @@ void ShaderWriter::DeclareSamplers(Slice<SamplerDef> samplers) {
|
||||
samplerDefs_ = samplers;
|
||||
}
|
||||
|
||||
const SamplerDef *ShaderWriter::GetSamplerDef(const char *name) const {
|
||||
for (int i = 0; i < (int)samplers_.size(); i++) {
|
||||
if (!strcmp(samplers_[i].name, name)) {
|
||||
return &samplers_[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ShaderWriter::DeclareTexture2D(const SamplerDef &def) {
|
||||
switch (lang_.shaderLanguage) {
|
||||
case HLSL_D3D11:
|
||||
@ -484,7 +475,7 @@ void ShaderWriter::DeclareTexture2D(const SamplerDef &def) {
|
||||
break;
|
||||
case GLSL_VULKAN:
|
||||
// In the thin3d descriptor set layout, textures start at 1 in set 0. Hence the +1.
|
||||
if ((flags_ & ShaderWriterFlags::FS_AUTO_STEREO) && def.array) {
|
||||
if (def.flags & SamplerFlags::ARRAY_ON_VULKAN) {
|
||||
F("layout(set = 0, binding = %d) uniform sampler2DArray %s;\n", def.binding + texBindingBase_, def.name);
|
||||
} else {
|
||||
F("layout(set = 0, binding = %d) uniform sampler2D %s;\n", def.binding + texBindingBase_, def.name);
|
||||
@ -518,7 +509,7 @@ ShaderWriter &ShaderWriter::SampleTexture2D(const char *sampName, const char *uv
|
||||
break;
|
||||
default:
|
||||
// Note: we ignore the sampler. make sure you bound samplers to the textures correctly.
|
||||
if (samp && samp->array) {
|
||||
if (samp && (samp->flags & SamplerFlags::ARRAY_ON_VULKAN)) {
|
||||
const char *index = (flags_ & ShaderWriterFlags::FS_AUTO_STEREO) ? "float(gl_ViewIndex)" : "0.0";
|
||||
F("%s(%s, vec3(%s, %s))", lang_.texture, sampName, uv, index);
|
||||
} else {
|
||||
@ -542,7 +533,7 @@ ShaderWriter &ShaderWriter::SampleTexture2DOffset(const char *sampName, const ch
|
||||
break;
|
||||
default:
|
||||
// Note: we ignore the sampler. make sure you bound samplers to the textures correctly.
|
||||
if (samp->array) {
|
||||
if (samp && (samp->flags & SamplerFlags::ARRAY_ON_VULKAN)) {
|
||||
const char *index = (flags_ & ShaderWriterFlags::FS_AUTO_STEREO) ? "float(gl_ViewIndex)" : "0.0";
|
||||
F("%sOffset(%s, vec3(%s, %s), ivec3(%d, %d))", lang_.texture, sampName, uv, index, offX, offY);
|
||||
} else {
|
||||
@ -566,8 +557,9 @@ ShaderWriter &ShaderWriter::LoadTexture2D(const char *sampName, const char *uv,
|
||||
break;
|
||||
default:
|
||||
// Note: we ignore the sampler. make sure you bound samplers to the textures correctly.
|
||||
if ((flags_ & ShaderWriterFlags::FS_AUTO_STEREO) && samp->array) {
|
||||
F("texelFetch(%s, %s, %d)", sampName, uv, level);
|
||||
if (samp && (samp->flags & SamplerFlags::ARRAY_ON_VULKAN)) {
|
||||
const char *index = (flags_ & ShaderWriterFlags::FS_AUTO_STEREO) ? "gl_ViewIndex" : "0";
|
||||
F("texelFetch(%s, vec3(%s, %s), %d)", sampName, uv, index, level);
|
||||
} else {
|
||||
F("texelFetch(%s, %s, %d)", sampName, uv, level);
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ struct VaryingDef {
|
||||
enum class ShaderWriterFlags {
|
||||
NONE = 0,
|
||||
FS_WRITE_DEPTH = 1,
|
||||
FS_AUTO_STEREO = 2, // Automatically makes sampler 0 an array sampler, and samples it by gl_ViewIndex. Useful for stereo rendering.
|
||||
FS_AUTO_STEREO = 2, // Automatically indexes makes samplers tagged with `array` by gl_ViewIndex. Useful for stereo rendering.
|
||||
};
|
||||
ENUM_CLASS_BITOPS(ShaderWriterFlags);
|
||||
|
||||
@ -119,8 +119,6 @@ private:
|
||||
|
||||
void Preamble(Slice<const char *> extensions);
|
||||
|
||||
const SamplerDef *GetSamplerDef(const char *name) const;
|
||||
|
||||
char *p_;
|
||||
const ShaderLanguageDesc &lang_;
|
||||
const ShaderStage stage_;
|
||||
|
@ -123,6 +123,13 @@ bool VulkanTexture::CreateDirect(VkCommandBuffer cmd, int w, int h, int depth, i
|
||||
_assert_(res == VK_ERROR_OUT_OF_HOST_MEMORY || res == VK_ERROR_OUT_OF_DEVICE_MEMORY || res == VK_ERROR_TOO_MANY_OBJECTS);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additionally, create an array view, but only if it's a 2D texture.
|
||||
if (view_info.viewType == VK_IMAGE_VIEW_TYPE_2D) {
|
||||
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
res = vkCreateImageView(vulkan_->GetDevice(), &view_info, NULL, &arrayView_);
|
||||
_assert_(res == VK_SUCCESS);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -245,6 +252,9 @@ void VulkanTexture::Destroy() {
|
||||
if (view_ != VK_NULL_HANDLE) {
|
||||
vulkan_->Delete().QueueDeleteImageView(view_);
|
||||
}
|
||||
if (arrayView_ != VK_NULL_HANDLE) {
|
||||
vulkan_->Delete().QueueDeleteImageView(arrayView_);
|
||||
}
|
||||
if (image_ != VK_NULL_HANDLE) {
|
||||
_dbg_assert_(allocation_ != VK_NULL_HANDLE);
|
||||
vulkan_->Delete().QueueDeleteImageAllocation(image_, allocation_);
|
||||
|
@ -51,6 +51,9 @@ public:
|
||||
// Used for sampling, generally.
|
||||
VkImageView GetImageView() const { return view_; }
|
||||
|
||||
// For use with some shaders, we might want to view it as a single entry array for convenience.
|
||||
VkImageView GetImageArrayView() const { return arrayView_; }
|
||||
|
||||
int32_t GetWidth() const { return width_; }
|
||||
int32_t GetHeight() const { return height_; }
|
||||
int32_t GetNumMips() const { return numMips_; }
|
||||
@ -62,6 +65,7 @@ private:
|
||||
VulkanContext *vulkan_;
|
||||
VkImage image_ = VK_NULL_HANDLE;
|
||||
VkImageView view_ = VK_NULL_HANDLE;
|
||||
VkImageView arrayView_ = VK_NULL_HANDLE;
|
||||
VmaAllocation allocation_ = VK_NULL_HANDLE;
|
||||
|
||||
int16_t width_ = 0;
|
||||
|
@ -338,10 +338,16 @@ public:
|
||||
if (vkTex_) {
|
||||
vkTex_->Touch();
|
||||
return vkTex_->GetImageView();
|
||||
} else {
|
||||
// This would be bad.
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
return VK_NULL_HANDLE; // This would be bad.
|
||||
}
|
||||
|
||||
VkImageView GetImageArrayView() {
|
||||
if (vkTex_) {
|
||||
vkTex_->Touch();
|
||||
return vkTex_->GetImageArrayView();
|
||||
}
|
||||
return VK_NULL_HANDLE; // This would be bad.
|
||||
}
|
||||
|
||||
private:
|
||||
@ -417,7 +423,7 @@ public:
|
||||
void SetStencilParams(uint8_t refValue, uint8_t writeMask, uint8_t compareMask) override;
|
||||
|
||||
void BindSamplerStates(int start, int count, SamplerState **state) override;
|
||||
void BindTextures(int start, int count, Texture **textures) override;
|
||||
void BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) override;
|
||||
void BindNativeTexture(int sampler, void *nativeTexture) override;
|
||||
|
||||
void BindPipeline(Pipeline *pipeline) override {
|
||||
@ -520,6 +526,7 @@ private:
|
||||
MAX_FRAME_COMMAND_BUFFERS = 256,
|
||||
};
|
||||
AutoRef<VKTexture> boundTextures_[MAX_BOUND_TEXTURES];
|
||||
TextureBindFlags boundTextureFlags_[MAX_BOUND_TEXTURES];
|
||||
AutoRef<VKSamplerState> boundSamplers_[MAX_BOUND_TEXTURES];
|
||||
VkImageView boundImageView_[MAX_BOUND_TEXTURES]{};
|
||||
|
||||
@ -994,7 +1001,11 @@ VkDescriptorSet VKContext::GetOrCreateDescriptorSet(VkBuffer buf) {
|
||||
FrameData *frame = &frame_[vulkan_->GetCurFrame()];
|
||||
|
||||
for (int i = 0; i < MAX_BOUND_TEXTURES; ++i) {
|
||||
key.imageViews_[i] = boundTextures_[i] ? boundTextures_[i]->GetImageView() : boundImageView_[i];
|
||||
if (boundTextures_[i]) {
|
||||
key.imageViews_[i] = (boundTextureFlags_[i] & TextureBindFlags::VULKAN_BIND_ARRAY) ? boundTextures_[i]->GetImageArrayView() : boundTextures_[i]->GetImageView();
|
||||
} else {
|
||||
key.imageViews_[i] = boundImageView_[i];
|
||||
}
|
||||
key.samplers_[i] = boundSamplers_[i];
|
||||
}
|
||||
key.buffer_ = buf;
|
||||
@ -1296,11 +1307,21 @@ void VKContext::UpdateBuffer(Buffer *buffer, const uint8_t *data, size_t offset,
|
||||
memcpy(buf->data_ + offset, data, size);
|
||||
}
|
||||
|
||||
void VKContext::BindTextures(int start, int count, Texture **textures) {
|
||||
void VKContext::BindTextures(int start, int count, Texture **textures, TextureBindFlags flags) {
|
||||
_assert_(start + count <= MAX_BOUND_TEXTURES);
|
||||
for (int i = start; i < start + count; i++) {
|
||||
boundTextures_[i] = static_cast<VKTexture *>(textures[i - start]);
|
||||
boundImageView_[i] = boundTextures_[i] ? boundTextures_[i]->GetImageView() : GetNullTexture()->GetImageView();
|
||||
boundTextureFlags_[i] = flags;
|
||||
if (boundTextures_[i]) {
|
||||
// NOTE: These image views are actually not used, it seems - they get overridden in GetOrCreateDescriptorSet
|
||||
if (flags & TextureBindFlags::VULKAN_BIND_ARRAY) {
|
||||
boundImageView_[i] = boundTextures_[i]->GetImageArrayView();
|
||||
} else {
|
||||
boundImageView_[i] = boundTextures_[i]->GetImageView();
|
||||
}
|
||||
} else {
|
||||
boundImageView_[i] = GetNullTexture()->GetImageView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/Common.h"
|
||||
#include "Common/GPU/DataFormat.h"
|
||||
#include "Common/GPU/Shader.h"
|
||||
#include "Common/Data/Collections/Slice.h"
|
||||
@ -598,6 +599,12 @@ struct RenderPassInfo {
|
||||
|
||||
const int ALL_LAYERS = -1;
|
||||
|
||||
enum class TextureBindFlags {
|
||||
NONE = 0,
|
||||
VULKAN_BIND_ARRAY = 1,
|
||||
};
|
||||
ENUM_CLASS_BITOPS(TextureBindFlags);
|
||||
|
||||
class DrawContext {
|
||||
public:
|
||||
virtual ~DrawContext();
|
||||
@ -688,7 +695,7 @@ public:
|
||||
virtual void SetStencilParams(uint8_t refValue, uint8_t writeMask, uint8_t compareMask) = 0;
|
||||
|
||||
virtual void BindSamplerStates(int start, int count, SamplerState **state) = 0;
|
||||
virtual void BindTextures(int start, int count, Texture **textures) = 0;
|
||||
virtual void BindTextures(int start, int count, Texture **textures, TextureBindFlags flags = TextureBindFlags::NONE) = 0;
|
||||
virtual void BindVertexBuffers(int start, int count, Buffer **buffers, const int *offsets) = 0;
|
||||
virtual void BindIndexBuffer(Buffer *indexBuffer, int offset) = 0;
|
||||
|
||||
|
@ -37,7 +37,7 @@ static const VaryingDef varyings[1] = {
|
||||
};
|
||||
|
||||
static const SamplerDef samplers[1] = {
|
||||
{ 0, "tex" },
|
||||
{ 0, "tex", SamplerFlags::ARRAY_ON_VULKAN },
|
||||
};
|
||||
|
||||
const UniformDef g_draw2Duniforms[2] = {
|
||||
@ -88,8 +88,8 @@ Draw2DPipelineInfo GenerateDraw2DCopyColorRect2LinFs(ShaderWriter &writer) {
|
||||
}
|
||||
|
||||
Draw2DPipelineInfo GenerateDraw2DCopyDepthFs(ShaderWriter &writer) {
|
||||
writer.DeclareSamplers(samplers);
|
||||
writer.SetFlags(ShaderWriterFlags::FS_WRITE_DEPTH);
|
||||
writer.DeclareSamplers(samplers);
|
||||
writer.BeginFSMain(Slice<UniformDef>::empty(), varyings);
|
||||
writer.C(" vec4 outColor = vec4(0.0, 0.0, 0.0, 0.0);\n");
|
||||
writer.C(" gl_FragDepth = ").SampleTexture2D("tex", "v_texcoord.xy").C(".x;\n");
|
||||
@ -103,8 +103,8 @@ Draw2DPipelineInfo GenerateDraw2DCopyDepthFs(ShaderWriter &writer) {
|
||||
}
|
||||
|
||||
Draw2DPipelineInfo GenerateDraw2D565ToDepthFs(ShaderWriter &writer) {
|
||||
writer.DeclareSamplers(samplers);
|
||||
writer.SetFlags(ShaderWriterFlags::FS_WRITE_DEPTH);
|
||||
writer.DeclareSamplers(samplers);
|
||||
writer.BeginFSMain(Slice<UniformDef>::empty(), varyings);
|
||||
writer.C(" vec4 outColor = vec4(0.0, 0.0, 0.0, 0.0);\n");
|
||||
// Unlike when just copying a depth buffer, here we're generating new depth values so we'll
|
||||
@ -123,8 +123,8 @@ Draw2DPipelineInfo GenerateDraw2D565ToDepthFs(ShaderWriter &writer) {
|
||||
}
|
||||
|
||||
Draw2DPipelineInfo GenerateDraw2D565ToDepthDeswizzleFs(ShaderWriter &writer) {
|
||||
writer.DeclareSamplers(samplers);
|
||||
writer.SetFlags(ShaderWriterFlags::FS_WRITE_DEPTH);
|
||||
writer.DeclareSamplers(samplers);
|
||||
writer.BeginFSMain(g_draw2Duniforms, varyings);
|
||||
writer.C(" vec4 outColor = vec4(0.0, 0.0, 0.0, 0.0);\n");
|
||||
// Unlike when just copying a depth buffer, here we're generating new depth values so we'll
|
||||
@ -182,6 +182,10 @@ void Draw2D::Ensure2DResources() {
|
||||
|
||||
if (!draw2DVs_) {
|
||||
char *vsCode = new char[8192];
|
||||
ShaderWriterFlags flags = ShaderWriterFlags::NONE;
|
||||
if (gstate_c.Use(GPU_USE_SINGLE_PASS_STEREO)) {
|
||||
flags = ShaderWriterFlags::FS_AUTO_STEREO;
|
||||
}
|
||||
ShaderWriter writer(vsCode, shaderLanguageDesc, ShaderStage::Vertex);
|
||||
GenerateDraw2DVS(writer);
|
||||
_assert_msg_(strlen(vsCode) < 8192, "Draw2D VS length error: %d", (int)strlen(vsCode));
|
||||
@ -315,6 +319,7 @@ void Draw2D::DrawStrip2D(Draw::Texture *tex, Draw2DVertex *verts, int vertexCoun
|
||||
draw_->UpdateDynamicUniformBuffer(&ub, sizeof(ub));
|
||||
|
||||
if (tex) {
|
||||
// This won't work since all the shaders above expect array textures on Vulkan.
|
||||
draw_->BindTextures(TEX_SLOT_PSP_TEXTURE, 1, &tex);
|
||||
}
|
||||
draw_->BindSamplerStates(TEX_SLOT_PSP_TEXTURE, 1, linearFilter ? &draw2DSamplerLinear_ : &draw2DSamplerNearest_);
|
||||
|
@ -1140,7 +1140,7 @@ void FramebufferManagerCommon::DrawPixels(VirtualFramebuffer *vfb, int dstX, int
|
||||
|
||||
Draw::Texture *pixelsTex = MakePixelTexture(srcPixels, srcPixelFormat, srcStride, width, height);
|
||||
if (pixelsTex) {
|
||||
draw_->BindTextures(0, 1, &pixelsTex);
|
||||
draw_->BindTextures(0, 1, &pixelsTex, Draw::TextureBindFlags::VULKAN_BIND_ARRAY);
|
||||
|
||||
// TODO: Replace with draw2D_.Blit() directly.
|
||||
DrawActiveTexture(dstX, dstY, width, height, vfb->bufferWidth, vfb->bufferHeight, u0, v0, u1, v1, ROTATION_LOCKED_HORIZONTAL, flags);
|
||||
@ -3042,7 +3042,7 @@ void FramebufferManagerCommon::BlitUsingRaster(
|
||||
draw_->BindTexture(0, nullptr);
|
||||
// This will get optimized away in case it's already bound (in VK and GL at least..)
|
||||
draw_->BindFramebufferAsRenderTarget(dest, { Draw::RPAction::KEEP, Draw::RPAction::KEEP, Draw::RPAction::KEEP }, tag ? tag : "BlitUsingRaster");
|
||||
draw_->BindFramebufferAsTexture(src, 0, pipeline->info.readChannel == RASTER_COLOR ? Draw::FB_COLOR_BIT : Draw::FB_DEPTH_BIT, 0);
|
||||
draw_->BindFramebufferAsTexture(src, Draw::ALL_LAYERS, pipeline->info.readChannel == RASTER_COLOR ? Draw::FB_COLOR_BIT : Draw::FB_DEPTH_BIT, 0);
|
||||
|
||||
if (destX1 == 0.0f && destY1 == 0.0f && destX2 >= destW && destY2 >= destH) {
|
||||
// We overwrite the whole channel of the framebuffer, so we can invalidate the current contents.
|
||||
|
Loading…
Reference in New Issue
Block a user