mirror of
https://github.com/narzoul/DDrawCompat
synced 2024-12-30 08:55:36 +01:00
parent
f2ea02b960
commit
0b974e2dd7
DDrawCompat
@ -48,6 +48,7 @@
|
||||
#include <Config/Settings/TextureFilter.h>
|
||||
#include <Config/Settings/ThreadPriorityBoost.h>
|
||||
#include <Config/Settings/VertexBufferMemoryType.h>
|
||||
#include <Config/Settings/VertexFixup.h>
|
||||
#include <Config/Settings/VSync.h>
|
||||
#include <Config/Settings/WinVersionLie.h>
|
||||
|
||||
@ -103,6 +104,7 @@ namespace Config
|
||||
Settings::TextureFilter textureFilter;
|
||||
Settings::ThreadPriorityBoost threadPriorityBoost;
|
||||
Settings::VertexBufferMemoryType vertexBufferMemoryType;
|
||||
Settings::VertexFixup vertexFixup;
|
||||
Settings::VSync vSync;
|
||||
Settings::WinVersionLie winVersionLie;
|
||||
}
|
||||
|
22
DDrawCompat/Config/Settings/VertexFixup.h
Normal file
22
DDrawCompat/Config/Settings/VertexFixup.h
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <Config/EnumSetting.h>
|
||||
|
||||
namespace Config
|
||||
{
|
||||
namespace Settings
|
||||
{
|
||||
class VertexFixup : public EnumSetting
|
||||
{
|
||||
public:
|
||||
enum Values { CPU, GPU };
|
||||
|
||||
VertexFixup()
|
||||
: EnumSetting("VertexFixup", "gpu", { "cpu", "gpu" })
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
extern Settings::VertexFixup vertexFixup;
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
#include <Config/Settings/SpriteFilter.h>
|
||||
#include <Config/Settings/SpriteTexCoord.h>
|
||||
#include <Config/Settings/TextureFilter.h>
|
||||
#include <Config/Settings/VertexFixup.h>
|
||||
#include <Common/Comparison.h>
|
||||
#include <Common/Log.h>
|
||||
#include <D3dDdi/Device.h>
|
||||
@ -88,6 +89,8 @@ namespace D3dDdi
|
||||
, m_vertexShaderConstB{}
|
||||
, m_vertexShaderConstI{}
|
||||
, m_vertexDecl(nullptr)
|
||||
, m_vertexFixupConfig(Config::vertexFixup.get())
|
||||
, m_vertexFixupData{}
|
||||
, m_changedStates(0)
|
||||
, m_maxChangedTextureStage(0)
|
||||
, m_texCoordIndexes(0)
|
||||
@ -100,6 +103,8 @@ namespace D3dDdi
|
||||
const UINT D3DBLENDOP_ADD = 1;
|
||||
const UINT UNINITIALIZED_STATE = 0xBAADBAAD;
|
||||
|
||||
m_vertexFixupData.multiplier[3] = 1.0f;
|
||||
|
||||
m_device.getOrigVtable().pfnSetDepthStencil(m_device, &m_current.depthStencil);
|
||||
m_device.getOrigVtable().pfnSetRenderTarget(m_device, &m_current.renderTarget);
|
||||
m_device.getOrigVtable().pfnSetViewport(m_device, &m_current.viewport);
|
||||
@ -298,6 +303,10 @@ namespace D3dDdi
|
||||
{
|
||||
updateTextureStages();
|
||||
}
|
||||
if (m_changedStates & CS_VERTEX_FIXUP)
|
||||
{
|
||||
updateVertexFixupShaderConst();
|
||||
}
|
||||
|
||||
m_changedStates = 0;
|
||||
m_maxChangedTextureStage = 0;
|
||||
@ -1012,18 +1021,10 @@ namespace D3dDdi
|
||||
auto resource = (texture == m_app.textures[stage]) ? getTextureResource(stage) : m_device.getResource(texture);
|
||||
if (resource)
|
||||
{
|
||||
D3DDDIARG_SETVERTEXSHADERCONST data = {};
|
||||
data.Register = 253;
|
||||
data.Count = 1;
|
||||
|
||||
auto& si = resource->getFixedDesc().pSurfList[0];
|
||||
ShaderConstF reg = { static_cast<float>(si.Width), static_cast<float>(si.Height),
|
||||
m_vertexShaderConst[253][2], m_vertexShaderConst[253][3] };
|
||||
|
||||
if (0 != memcmp(®, &m_vertexShaderConst[data.Register], sizeof(reg)))
|
||||
{
|
||||
pfnSetVertexShaderConst(&data, ®);
|
||||
}
|
||||
m_vertexFixupData.texCoordAdj[0] = static_cast<float>(si.Width);
|
||||
m_vertexFixupData.texCoordAdj[1] = static_cast<float>(si.Height);
|
||||
m_changedStates |= CS_VERTEX_FIXUP;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1107,6 +1108,7 @@ namespace D3dDdi
|
||||
m_changedStates |= CS_RENDER_STATE | CS_RENDER_TARGET | CS_SHADER | CS_TEXTURE_STAGE;
|
||||
m_changedRenderStates.set(D3DDDIRS_COLORKEYENABLE);
|
||||
m_changedRenderStates.set(D3DDDIRS_MULTISAMPLEANTIALIAS);
|
||||
m_vertexFixupConfig = Config::vertexFixup.get();
|
||||
|
||||
for (auto& ps : m_pixelShaders)
|
||||
{
|
||||
@ -1211,7 +1213,7 @@ namespace D3dDdi
|
||||
{
|
||||
const float sx = static_cast<float>(scaledVp.Width) / vp.Width;
|
||||
const float sy = static_cast<float>(scaledVp.Height) / vp.Height;
|
||||
updateVertexFixupConstants(vp.Width, vp.Height, sx, sy);
|
||||
updateVertexFixupData(vp.Width, vp.Height, sx, sy);
|
||||
}
|
||||
|
||||
const bool isScissorRectNeeded = isTransformed && 0 != memcmp(&vp, &m_app.viewport, sizeof(vp));
|
||||
@ -1230,7 +1232,16 @@ namespace D3dDdi
|
||||
{
|
||||
setPixelShader(mapPixelShader(m_app.pixelShader));
|
||||
setVertexShaderDecl(m_app.vertexShaderDecl);
|
||||
setVertexShaderFunc(getVertexDecl().isTransformed ? getVsVertexFixup() : m_app.vertexShaderFunc);
|
||||
|
||||
if (Config::Settings::VertexFixup::GPU == m_vertexFixupConfig &&
|
||||
getVertexDecl().isTransformed)
|
||||
{
|
||||
setVertexShaderFunc(getVsVertexFixup());
|
||||
}
|
||||
else
|
||||
{
|
||||
setVertexShaderFunc(m_app.vertexShaderFunc);
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceState::updateStreamSource()
|
||||
@ -1321,25 +1332,51 @@ namespace D3dDdi
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceState::updateVertexFixupConstants(UINT width, UINT height, float sx, float sy)
|
||||
void DeviceState::updateVertexFixupData(UINT width, UINT height, float sx, float sy)
|
||||
{
|
||||
D3DDDIARG_SETVERTEXSHADERCONST data = {};
|
||||
data.Register = 253;
|
||||
data.Count = 3;
|
||||
|
||||
const float stc = static_cast<float>(Config::spriteTexCoord.getParam()) / 100;
|
||||
const float apc = Config::alternatePixelCenter.get();
|
||||
const auto& zr = m_current.zRange;
|
||||
|
||||
ShaderConstF registers[3] = {
|
||||
{ m_vertexShaderConst[253][0], m_vertexShaderConst[253][1], stc, stc },
|
||||
{ 0.5f + apc - 0.5f / sx - width / 2, 0.5f + apc - 0.5f / sy - height / 2, -zr.MinZ, 0.0f },
|
||||
{ 2.0f / width, -2.0f / height, 1.0f / (zr.MaxZ - zr.MinZ), 1.0f }
|
||||
};
|
||||
m_vertexFixupData.texCoordAdj[2] = stc;
|
||||
m_vertexFixupData.texCoordAdj[3] = stc;
|
||||
|
||||
if (0 != memcmp(registers, &m_vertexShaderConst[data.Register], sizeof(registers)))
|
||||
if (Config::Settings::VertexFixup::GPU == m_vertexFixupConfig)
|
||||
{
|
||||
pfnSetVertexShaderConst(&data, registers);
|
||||
m_vertexFixupData.offset[0] = 0.5f + apc - 0.5f / sx - width / 2;
|
||||
m_vertexFixupData.offset[1] = 0.5f + apc - 0.5f / sy - height / 2;
|
||||
m_vertexFixupData.offset[2] = -zr.MinZ;
|
||||
|
||||
m_vertexFixupData.multiplier[0] = 2.0f / width;
|
||||
m_vertexFixupData.multiplier[1] = -2.0f / height;
|
||||
m_vertexFixupData.multiplier[2] = 1.0f / (zr.MaxZ - zr.MinZ);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_vertexFixupData.offset[0] = 0.5f + apc - 0.5f / sx;
|
||||
m_vertexFixupData.offset[1] = 0.5f + apc - 0.5f / sy;
|
||||
|
||||
m_vertexFixupData.multiplier[0] = sx;
|
||||
m_vertexFixupData.multiplier[1] = sy;
|
||||
}
|
||||
|
||||
m_changedStates |= CS_VERTEX_FIXUP;
|
||||
}
|
||||
|
||||
void DeviceState::updateVertexFixupShaderConst()
|
||||
{
|
||||
if (m_current.vertexShaderFunc == m_app.vertexShaderFunc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
D3DDDIARG_SETVERTEXSHADERCONST data = {};
|
||||
data.Register = 253;
|
||||
data.Count = 3;
|
||||
|
||||
if (0 != memcmp(&m_vertexFixupData, &m_vertexShaderConst[data.Register], sizeof(m_vertexFixupData)))
|
||||
{
|
||||
pfnSetVertexShaderConst(&data, &m_vertexFixupData.texCoordAdj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,6 +109,13 @@ namespace D3dDdi
|
||||
bool isTransformed;
|
||||
};
|
||||
|
||||
struct VertexFixupData
|
||||
{
|
||||
ShaderConstF texCoordAdj;
|
||||
ShaderConstF offset;
|
||||
ShaderConstF multiplier;
|
||||
};
|
||||
|
||||
DeviceState(Device& device);
|
||||
|
||||
HRESULT pfnCreatePixelShader(D3DDDIARG_CREATEPIXELSHADER* data, const UINT* code);
|
||||
@ -153,9 +160,11 @@ namespace D3dDdi
|
||||
void flush();
|
||||
const State& getAppState() const { return m_app; }
|
||||
const State& getCurrentState() const { return m_current; }
|
||||
bool getSpriteMode() const { return m_spriteMode; }
|
||||
Resource* getTextureResource(UINT stage);
|
||||
UINT getTextureStageCount() const;
|
||||
const VertexDecl& getVertexDecl() const;
|
||||
const VertexFixupData& getVertexFixupData() const { return m_vertexFixupData; }
|
||||
bool isLocked() const { return m_isLocked; }
|
||||
void onDestroyResource(Resource* resource, HANDLE resourceHandle);
|
||||
void updateConfig();
|
||||
@ -168,7 +177,8 @@ namespace D3dDdi
|
||||
CS_RENDER_TARGET = 1 << 1,
|
||||
CS_SHADER = 1 << 2,
|
||||
CS_STREAM_SOURCE = 1 << 3,
|
||||
CS_TEXTURE_STAGE = 1 << 4
|
||||
CS_TEXTURE_STAGE = 1 << 4,
|
||||
CS_VERTEX_FIXUP = 1 << 5
|
||||
};
|
||||
|
||||
struct PixelShader
|
||||
@ -230,7 +240,8 @@ namespace D3dDdi
|
||||
void updateShaders();
|
||||
void updateTextureColorKey(UINT stage);
|
||||
void updateTextureStages();
|
||||
void updateVertexFixupConstants(UINT width, UINT height, float sx, float sy);
|
||||
void updateVertexFixupData(UINT width, UINT height, float sx, float sy);
|
||||
void updateVertexFixupShaderConst();
|
||||
|
||||
Device& m_device;
|
||||
State m_app;
|
||||
@ -243,6 +254,8 @@ namespace D3dDdi
|
||||
std::array<ShaderConstI, 16> m_vertexShaderConstI;
|
||||
std::map<HANDLE, VertexDecl> m_vertexShaderDecls;
|
||||
VertexDecl* m_vertexDecl;
|
||||
UINT m_vertexFixupConfig;
|
||||
VertexFixupData m_vertexFixupData;
|
||||
UINT m_changedStates;
|
||||
UINT m_maxChangedTextureStage;
|
||||
UINT m_texCoordIndexes;
|
||||
|
@ -12,6 +12,14 @@ namespace
|
||||
const UINT INDEX_BUFFER_SIZE = D3DMAXNUMPRIMITIVES * 3 * sizeof(UINT16);
|
||||
const UINT VERTEX_BUFFER_SIZE = 1024 * 1024;
|
||||
|
||||
enum VertexFixupFlags
|
||||
{
|
||||
VF_XY = 1 << 0,
|
||||
VF_Z = 1 << 1,
|
||||
VF_RHW = 1 << 2,
|
||||
VF_TEXCOORD = 1 << 3
|
||||
};
|
||||
|
||||
UINT getVertexCount(D3DPRIMITIVETYPE primitiveType, UINT primitiveCount)
|
||||
{
|
||||
switch (primitiveType)
|
||||
@ -57,6 +65,7 @@ namespace D3dDdi
|
||||
, m_indexBuffer(device, m_vertexBuffer ? INDEX_BUFFER_SIZE : 0)
|
||||
, m_streamSource{}
|
||||
, m_batched{}
|
||||
, m_vertexFixupFlags(0)
|
||||
{
|
||||
LOG_ONCE("Dynamic vertex buffers are " << (m_vertexBuffer ? "" : "not ") << "available");
|
||||
LOG_ONCE("Dynamic index buffers are " << (m_indexBuffer ? "" : "not ") << "available");
|
||||
@ -325,6 +334,74 @@ namespace D3dDdi
|
||||
{
|
||||
auto vertices = m_streamSource.vertices + base * m_streamSource.stride;
|
||||
m_batched.vertices.insert(m_batched.vertices.end(), vertices, vertices + count * m_streamSource.stride);
|
||||
|
||||
if (0 == m_vertexFixupFlags)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto& vertexFixupData = m_device.getState().getVertexFixupData();
|
||||
auto firstVertex = &m_batched.vertices[m_batched.vertices.size() - count * m_streamSource.stride];
|
||||
|
||||
if (m_vertexFixupFlags & VF_XY)
|
||||
{
|
||||
auto vPos = firstVertex;
|
||||
for (unsigned i = 0; i < count; ++i)
|
||||
{
|
||||
auto v = reinterpret_cast<D3DTLVERTEX*>(vPos);
|
||||
v->sx += vertexFixupData.offset[0];
|
||||
v->sy += vertexFixupData.offset[1];
|
||||
v->sx *= vertexFixupData.multiplier[0];
|
||||
v->sy *= vertexFixupData.multiplier[1];
|
||||
vPos += m_streamSource.stride;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_vertexFixupFlags & VF_Z)
|
||||
{
|
||||
auto zPos = reinterpret_cast<BYTE*>(&reinterpret_cast<D3DTLVERTEX*>(firstVertex)->sz);
|
||||
for (unsigned i = 0; i < count; ++i)
|
||||
{
|
||||
auto& z = *reinterpret_cast<float*>(zPos);
|
||||
if (z > 1)
|
||||
{
|
||||
z = 1;
|
||||
}
|
||||
zPos += m_streamSource.stride;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_vertexFixupFlags & VF_RHW)
|
||||
{
|
||||
auto rhwPos = reinterpret_cast<BYTE*>(&reinterpret_cast<D3DTLVERTEX*>(firstVertex)->rhw);
|
||||
for (unsigned i = 0; i < count; ++i)
|
||||
{
|
||||
auto& rhw = *reinterpret_cast<float*>(rhwPos);
|
||||
if (0 == rhw || INFINITY == rhw)
|
||||
{
|
||||
rhw = 1;
|
||||
}
|
||||
rhwPos += m_streamSource.stride;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_vertexFixupFlags & VF_TEXCOORD)
|
||||
{
|
||||
auto tcPos = firstVertex + m_device.getState().getVertexDecl().texCoordOffset[0];
|
||||
for (unsigned i = 0; i < count; ++i)
|
||||
{
|
||||
float* tc = reinterpret_cast<float*>(tcPos);
|
||||
tc[0] *= vertexFixupData.texCoordAdj[0];
|
||||
tc[1] *= vertexFixupData.texCoordAdj[1];
|
||||
tc[0] += vertexFixupData.texCoordAdj[2];
|
||||
tc[1] += vertexFixupData.texCoordAdj[3];
|
||||
tc[0] = roundf(tc[0]);
|
||||
tc[1] = roundf(tc[1]);
|
||||
tc[0] /= vertexFixupData.texCoordAdj[0];
|
||||
tc[1] /= vertexFixupData.texCoordAdj[1];
|
||||
tcPos += m_streamSource.stride;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawPrimitive::clearBatchedPrimitives()
|
||||
@ -456,6 +533,7 @@ namespace D3dDdi
|
||||
{
|
||||
auto& state = m_device.getState();
|
||||
auto vertexCount = getVertexCount(data.PrimitiveType, data.PrimitiveCount);
|
||||
m_vertexFixupFlags = 0;
|
||||
if (!state.isLocked())
|
||||
{
|
||||
if (0 == m_batched.primitiveCount)
|
||||
@ -477,6 +555,7 @@ namespace D3dDdi
|
||||
state.setSpriteMode(false);
|
||||
}
|
||||
state.flush();
|
||||
setVertexFixupFlags(data.VStart, 0);
|
||||
}
|
||||
|
||||
if (0 == m_batched.primitiveCount || flagBuffer ||
|
||||
@ -521,6 +600,7 @@ namespace D3dDdi
|
||||
|
||||
auto indexCount = getVertexCount(data.PrimitiveType, data.PrimitiveCount);
|
||||
auto vStart = data.BaseVertexOffset / static_cast<INT>(m_streamSource.stride);
|
||||
m_vertexFixupFlags = 0;
|
||||
if (!state.isLocked())
|
||||
{
|
||||
if (m_streamSource.vertices && data.PrimitiveType >= D3DPT_TRIANGLELIST)
|
||||
@ -537,6 +617,7 @@ namespace D3dDdi
|
||||
state.setSpriteMode(false);
|
||||
}
|
||||
state.flush();
|
||||
setVertexFixupFlags(vStart, indices[0]);
|
||||
}
|
||||
|
||||
auto [min, max] = std::minmax_element(indices, indices + indexCount);
|
||||
@ -870,4 +951,44 @@ namespace D3dDdi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DrawPrimitive::setVertexFixupFlags(INT baseVertexIndex, UINT16 index)
|
||||
{
|
||||
auto& state = m_device.getState();
|
||||
if (!state.getVertexDecl().isTransformed ||
|
||||
state.getCurrentState().vertexShaderFunc != state.getAppState().vertexShaderFunc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto& vertexFixupData = state.getVertexFixupData();
|
||||
if (0 != vertexFixupData.offset[0] ||
|
||||
0 != vertexFixupData.offset[1] ||
|
||||
1 != vertexFixupData.multiplier[0] ||
|
||||
1 != vertexFixupData.multiplier[1])
|
||||
{
|
||||
m_vertexFixupFlags |= VF_XY;
|
||||
}
|
||||
|
||||
auto vertex = reinterpret_cast<const D3DTLVERTEX*>(
|
||||
m_streamSource.vertices + (baseVertexIndex + index) * m_streamSource.stride);
|
||||
if (vertex->sz > 0)
|
||||
{
|
||||
m_vertexFixupFlags |= VF_Z;
|
||||
}
|
||||
|
||||
if (0 == vertex->rhw || INFINITY == vertex->rhw)
|
||||
{
|
||||
m_vertexFixupFlags |= VF_RHW;
|
||||
}
|
||||
|
||||
const UINT D3DDECLTYPE_FLOAT2 = 1;
|
||||
if (state.getSpriteMode() &&
|
||||
Config::Settings::SpriteTexCoord::ROUND == Config::spriteTexCoord.get() &&
|
||||
0 != Config::spriteTexCoord.getParam() &&
|
||||
D3DDECLTYPE_FLOAT2 == state.getVertexDecl().texCoordType[0])
|
||||
{
|
||||
m_vertexFixupFlags |= VF_TEXCOORD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,7 @@ namespace D3dDdi
|
||||
HRESULT flush(const UINT* flagBuffer);
|
||||
HRESULT flushIndexed(const UINT* flagBuffer);
|
||||
bool isSprite(INT baseVertexIndex, UINT16 index0, UINT16 index1, UINT16 index2);
|
||||
void setVertexFixupFlags(INT baseVertexIndex, UINT16 index);
|
||||
INT loadIndices(const void* indices, UINT count);
|
||||
INT loadVertices(UINT count);
|
||||
UINT getBatchedVertexCount() const;
|
||||
@ -87,5 +88,6 @@ namespace D3dDdi
|
||||
StreamSource m_streamSource;
|
||||
std::map<HANDLE, BYTE*> m_sysMemVertexBuffers;
|
||||
BatchedPrimitives m_batched;
|
||||
UINT m_vertexFixupFlags;
|
||||
};
|
||||
}
|
||||
|
@ -214,6 +214,7 @@
|
||||
<ClInclude Include="Config\Settings\TextureFilter.h" />
|
||||
<ClInclude Include="Config\Settings\ThreadPriorityBoost.h" />
|
||||
<ClInclude Include="Config\Settings\VertexBufferMemoryType.h" />
|
||||
<ClInclude Include="Config\Settings\VertexFixup.h" />
|
||||
<ClInclude Include="Config\Settings\VSync.h" />
|
||||
<ClInclude Include="Config\Settings\WinVersionLie.h" />
|
||||
<ClInclude Include="D3dDdi\Adapter.h" />
|
||||
|
@ -717,6 +717,9 @@
|
||||
<ClInclude Include="Common\Disasm.h">
|
||||
<Filter>Header Files\Common</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Config\Settings\VertexFixup.h">
|
||||
<Filter>Header Files\Config\Settings</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Gdi\Gdi.cpp">
|
||||
|
@ -25,6 +25,7 @@
|
||||
#include <Config/Settings/StatsPosY.h>
|
||||
#include <Config/Settings/StatsTransparency.h>
|
||||
#include <Config/Settings/TextureFilter.h>
|
||||
#include <Config/Settings/VertexFixup.h>
|
||||
#include <Config/Settings/VSync.h>
|
||||
#include <D3dDdi/Device.h>
|
||||
#include <Gdi/GuiThread.h>
|
||||
@ -66,6 +67,7 @@ namespace
|
||||
{ &Config::statsPosY, []() { Gdi::GuiThread::getStatsWindow()->updatePos(); } },
|
||||
{ &Config::statsTransparency, [&]() { Gdi::GuiThread::getStatsWindow()->setAlpha(Config::statsTransparency.get()); }},
|
||||
{ &Config::textureFilter, &D3dDdi::Device::updateAllConfig },
|
||||
{ &Config::vertexFixup, &D3dDdi::Device::updateAllConfig },
|
||||
{ &Config::vSync }
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user