mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
GPU: Use postshader for direct VRAM draws again.
Also centralize the pixel texture code while at it.
This commit is contained in:
1 parent
762b656ea2
commit
2faab0e082
13 files changed
+82
-212
No files matched your search
@@ -700,6 +700,71 @@ void FramebufferManagerCommon::CopyFramebufferForColorTexture(VirtualFramebuffer
|
||||
}
|
||||
}
|
||||
|
||||
Draw::Texture *FramebufferManagerCommon::MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) {
|
||||
// TODO: We can just change the texture format and flip some bits around instead of this.
|
||||
// Could share code with the texture cache perhaps.
|
||||
auto generateTexture = [&](uint8_t *data, const uint8_t *initData, uint32_t w, uint32_t h, uint32_t d, uint32_t byteStride, uint32_t sliceByteStride) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
const u16_le *src16 = (const u16_le *)srcPixels + srcStride * y;
|
||||
const u32_le *src32 = (const u32_le *)srcPixels + srcStride * y;
|
||||
u32 *dst = (u32 *)(data + byteStride * y);
|
||||
switch (srcPixelFormat) {
|
||||
case GE_FORMAT_565:
|
||||
if (preferredPixelsFormat_ == Draw::DataFormat::B8G8R8A8_UNORM)
|
||||
ConvertRGB565ToBGRA8888(dst, src16, width);
|
||||
else
|
||||
ConvertRGBA565ToRGBA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_5551:
|
||||
if (preferredPixelsFormat_ == Draw::DataFormat::B8G8R8A8_UNORM)
|
||||
ConvertRGBA5551ToBGRA8888(dst, src16, width);
|
||||
else
|
||||
ConvertRGBA5551ToRGBA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_4444:
|
||||
if (preferredPixelsFormat_ == Draw::DataFormat::B8G8R8A8_UNORM)
|
||||
ConvertRGBA4444ToBGRA8888(dst, src16, width);
|
||||
else
|
||||
ConvertRGBA4444ToRGBA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_8888:
|
||||
if (preferredPixelsFormat_ == Draw::DataFormat::B8G8R8A8_UNORM)
|
||||
ConvertRGBA8888ToBGRA8888(dst, src32, width);
|
||||
else
|
||||
memcpy(dst, src32, 4 * width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_INVALID:
|
||||
_dbg_assert_msg_(G3D, false, "Invalid pixelFormat passed to DrawPixels().");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Draw::TextureDesc desc{
|
||||
Draw::TextureType::LINEAR2D,
|
||||
preferredPixelsFormat_,
|
||||
width,
|
||||
height,
|
||||
1,
|
||||
1,
|
||||
false,
|
||||
"DrawPixels",
|
||||
{ (uint8_t *)srcPixels },
|
||||
generateTexture,
|
||||
};
|
||||
// TODO: On Vulkan, use a custom allocator? Important to use an allocator:
|
||||
// Hot Shot Golf (#12355) does tons of these in a frame in some situations! So actually,
|
||||
// we do use an allocator. In fact, I've now banned allocator-less textures.
|
||||
Draw::Texture *tex = draw_->CreateTexture(desc);
|
||||
if (!tex)
|
||||
ERROR_LOG(G3D, "Failed to create drawpixels texture");
|
||||
return tex;
|
||||
}
|
||||
|
||||
void FramebufferManagerCommon::DrawFramebufferToOutput(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, bool applyPostShader) {
|
||||
textureCache_->ForgetLastTexture();
|
||||
shaderManager_->DirtyLastShader();
|
||||
@@ -712,8 +777,7 @@ void FramebufferManagerCommon::DrawFramebufferToOutput(const u8 *srcPixels, GEBu
|
||||
draw_->BindTextures(0, 1, &pixelsTex);
|
||||
|
||||
int uvRotation = useBufferedRendering_ ? g_Config.iInternalScreenRotation : ROTATION_LOCKED_HORIZONTAL;
|
||||
// TODO: Currently we can't access the texture from Draw.
|
||||
//if (!applyPostShader) {
|
||||
if (!applyPostShader) {
|
||||
// TODO: Does this need to bind the backbuffer? What is this doing?
|
||||
float x, y, w, h;
|
||||
CenterDisplayOutputRect(&x, &y, &w, &h, 480.0f, 272.0f, (float)pixelWidth_, (float)pixelHeight_, uvRotation);
|
||||
@@ -727,7 +791,7 @@ void FramebufferManagerCommon::DrawFramebufferToOutput(const u8 *srcPixels, GEBu
|
||||
flags = flags | DRAWTEX_TO_BACKBUFFER;
|
||||
SetViewport2D(0, 0, pixelWidth_, pixelHeight_);
|
||||
DrawActiveTexture(x, y, w, h, (float)pixelWidth_, (float)pixelHeight_, u0, v0, u1, v1, uvRotation, flags);
|
||||
/*} else {
|
||||
} else {
|
||||
OutputFlags flags = g_Config.iBufFilter == SCALE_LINEAR ? OutputFlags::LINEAR : OutputFlags::NEAREST;
|
||||
if (needBackBufferYSwap_) {
|
||||
flags |= OutputFlags::BACKBUFFER_FLIPPED;
|
||||
@@ -741,10 +805,9 @@ void FramebufferManagerCommon::DrawFramebufferToOutput(const u8 *srcPixels, GEBu
|
||||
std::swap(v0, v1);
|
||||
}
|
||||
|
||||
// TODO
|
||||
presentation_->SourceTexture();
|
||||
presentation_->SourceTexture(pixelsTex);
|
||||
presentation_->CopyToOutput(flags, uvRotation, u0, v0, u1, v1, uniforms);
|
||||
}*/
|
||||
}
|
||||
|
||||
pixelsTex->Release();
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ public:
|
||||
protected:
|
||||
virtual void PackFramebufferSync_(VirtualFramebuffer *vfb, int x, int y, int w, int h);
|
||||
void SetViewport2D(int x, int y, int w, int h);
|
||||
virtual Draw::Texture *MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) = 0;
|
||||
Draw::Texture *MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1);
|
||||
virtual void DrawActiveTexture(float x, float y, float w, float h, float destW, float destH, float u0, float v0, float u1, float v1, int uvRotation, int flags) = 0;
|
||||
virtual void Bind2DShader() = 0;
|
||||
|
||||
@@ -397,6 +397,7 @@ protected:
|
||||
int bloomHack_ = 0;
|
||||
|
||||
bool needGLESRebinds_ = false;
|
||||
Draw::DataFormat preferredPixelsFormat_ = Draw::DataFormat::R8G8B8A8_UNORM;
|
||||
|
||||
struct TempFBOInfo {
|
||||
Draw::Framebuffer *fbo;
|
||||
|
||||
@@ -129,6 +129,7 @@ FramebufferManagerD3D11::FramebufferManagerD3D11(Draw::DrawContext *draw)
|
||||
ShaderTranslationInit();
|
||||
|
||||
presentation_->SetLanguage(HLSL_D3D11);
|
||||
preferredPixelsFormat_ = Draw::DataFormat::B8G8R8A8_UNORM;
|
||||
}
|
||||
|
||||
FramebufferManagerD3D11::~FramebufferManagerD3D11() {
|
||||
@@ -178,54 +179,6 @@ void FramebufferManagerD3D11::SetDrawEngine(DrawEngineD3D11 *td) {
|
||||
drawEngine_ = td;
|
||||
}
|
||||
|
||||
Draw::Texture *FramebufferManagerD3D11::MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) {
|
||||
auto generateTexture = [&](uint8_t *data, const uint8_t *initData, uint32_t w, uint32_t h, uint32_t d, uint32_t byteStride, uint32_t sliceByteStride) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
const u16_le *src16 = (const u16_le *)srcPixels + srcStride * y;
|
||||
const u32_le *src32 = (const u32_le *)srcPixels + srcStride * y;
|
||||
u32 *dst = (u32 *)(data + byteStride * y);
|
||||
switch (srcPixelFormat) {
|
||||
case GE_FORMAT_565:
|
||||
ConvertRGB565ToBGRA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_5551:
|
||||
ConvertRGBA5551ToBGRA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_4444:
|
||||
ConvertRGBA4444ToBGRA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_8888:
|
||||
ConvertRGBA8888ToBGRA8888(dst, src32, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_INVALID:
|
||||
_dbg_assert_msg_(G3D, false, "Invalid pixelFormat passed to DrawPixels().");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Draw::TextureDesc desc{
|
||||
Draw::TextureType::LINEAR2D,
|
||||
Draw::DataFormat::B8G8R8A8_UNORM,
|
||||
width,
|
||||
height,
|
||||
1,
|
||||
1,
|
||||
false,
|
||||
"DrawPixels",
|
||||
{ (uint8_t *)srcPixels },
|
||||
generateTexture,
|
||||
};
|
||||
Draw::Texture *tex = draw_->CreateTexture(desc);
|
||||
if (!tex)
|
||||
ERROR_LOG(G3D, "Failed to create drawpixels texture");
|
||||
return tex;
|
||||
}
|
||||
|
||||
void FramebufferManagerD3D11::DrawActiveTexture(float x, float y, float w, float h, float destW, float destH, float u0, float v0, float u1, float v1, int uvRotation, int flags) {
|
||||
struct Coord {
|
||||
Lin::Vec3 pos; float u, v;
|
||||
|
||||
@@ -68,7 +68,6 @@ protected:
|
||||
|
||||
private:
|
||||
void Bind2DShader() override;
|
||||
Draw::Texture *MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) override;
|
||||
void PackDepthbuffer(VirtualFramebuffer *vfb, int x, int y, int w, int h);
|
||||
void SimpleBlit(
|
||||
Draw::Framebuffer *dest, float destX1, float destY1, float destX2, float destY2,
|
||||
|
||||
@@ -117,6 +117,7 @@ static const D3DVERTEXELEMENT9 g_FramebufferVertexElements[] = {
|
||||
ShaderTranslationInit();
|
||||
|
||||
presentation_->SetLanguage(HLSL_DX9);
|
||||
preferredPixelsFormat_ = Draw::DataFormat::B8G8R8A8_UNORM;
|
||||
}
|
||||
|
||||
FramebufferManagerDX9::~FramebufferManagerDX9() {
|
||||
@@ -159,54 +160,6 @@ static const D3DVERTEXELEMENT9 g_FramebufferVertexElements[] = {
|
||||
drawEngine_ = td;
|
||||
}
|
||||
|
||||
Draw::Texture *FramebufferManagerDX9::MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) {
|
||||
auto generateTexture = [&](uint8_t *data, const uint8_t *initData, uint32_t w, uint32_t h, uint32_t d, uint32_t byteStride, uint32_t sliceByteStride) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
const u16_le *src16 = (const u16_le *)srcPixels + srcStride * y;
|
||||
const u32_le *src32 = (const u32_le *)srcPixels + srcStride * y;
|
||||
u32 *dst = (u32 *)(data + byteStride * y);
|
||||
switch (srcPixelFormat) {
|
||||
case GE_FORMAT_565:
|
||||
ConvertRGB565ToBGRA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_5551:
|
||||
ConvertRGBA5551ToBGRA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_4444:
|
||||
ConvertRGBA4444ToBGRA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_8888:
|
||||
ConvertRGBA8888ToBGRA8888(dst, src32, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_INVALID:
|
||||
_dbg_assert_msg_(G3D, false, "Invalid pixelFormat passed to DrawPixels().");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Draw::TextureDesc desc{
|
||||
Draw::TextureType::LINEAR2D,
|
||||
Draw::DataFormat::B8G8R8A8_UNORM,
|
||||
width,
|
||||
height,
|
||||
1,
|
||||
1,
|
||||
false,
|
||||
"DrawPixels",
|
||||
{ (uint8_t *)srcPixels },
|
||||
generateTexture,
|
||||
};
|
||||
Draw::Texture *tex = draw_->CreateTexture(desc);
|
||||
if (!tex)
|
||||
ERROR_LOG(G3D, "Failed to create drawpixels texture");
|
||||
return tex;
|
||||
}
|
||||
|
||||
void FramebufferManagerDX9::DrawActiveTexture(float x, float y, float w, float h, float destW, float destH, float u0, float v0, float u1, float v1, int uvRotation, int flags) {
|
||||
// TODO: StretchRect instead when possible?
|
||||
float coord[20] = {
|
||||
|
||||
@@ -76,7 +76,6 @@ protected:
|
||||
void UpdateDownloadTempBuffer(VirtualFramebuffer *nvfb) override;
|
||||
|
||||
private:
|
||||
Draw::Texture *MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) override;
|
||||
void PackFramebufferSync_(VirtualFramebuffer *vfb, int x, int y, int w, int h) override;
|
||||
void PackDepthbuffer(VirtualFramebuffer *vfb, int x, int y, int w, int h);
|
||||
bool GetRenderTargetFramebuffer(LPDIRECT3DSURFACE9 renderTarget, LPDIRECT3DSURFACE9 offscreen, int w, int h, GPUDebugBuffer &buffer);
|
||||
|
||||
@@ -202,6 +202,8 @@ bool FramebufferManagerDX9::NotifyStencilUpload(u32 addr, int size, bool skipZer
|
||||
float u1 = 1.0f;
|
||||
float v1 = 1.0f;
|
||||
Draw::Texture *tex = MakePixelTexture(src, dstBuffer->format, dstBuffer->fb_stride, dstBuffer->bufferWidth, dstBuffer->bufferHeight, u1, v1);
|
||||
if (!tex)
|
||||
return false;
|
||||
|
||||
device_->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_STENCIL, D3DCOLOR_RGBA(0, 0, 0, 0), 0.0f, 0);
|
||||
|
||||
|
||||
@@ -166,56 +166,6 @@ FramebufferManagerGLES::~FramebufferManagerGLES() {
|
||||
delete [] convBuf_;
|
||||
}
|
||||
|
||||
Draw::Texture *FramebufferManagerGLES::MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) {
|
||||
// TODO: We can just change the texture format and flip some bits around instead of this.
|
||||
// Could share code with the texture cache perhaps.
|
||||
auto generateTexture = [&](uint8_t *data, const uint8_t *initData, uint32_t w, uint32_t h, uint32_t d, uint32_t byteStride, uint32_t sliceByteStride) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
const u16_le *src16 = (const u16_le *)srcPixels + srcStride * y;
|
||||
const u32_le *src32 = (const u32_le *)srcPixels + srcStride * y;
|
||||
u32 *dst = (u32 *)(data + byteStride * y);
|
||||
switch (srcPixelFormat) {
|
||||
case GE_FORMAT_565:
|
||||
ConvertRGBA565ToRGBA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_5551:
|
||||
ConvertRGBA5551ToRGBA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_4444:
|
||||
ConvertRGBA4444ToRGBA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_8888:
|
||||
memcpy(dst, src32, 4 * width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_INVALID:
|
||||
_dbg_assert_msg_(G3D, false, "Invalid pixelFormat passed to DrawPixels().");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Draw::TextureDesc desc{
|
||||
Draw::TextureType::LINEAR2D,
|
||||
Draw::DataFormat::R8G8B8A8_UNORM,
|
||||
width,
|
||||
height,
|
||||
1,
|
||||
1,
|
||||
false,
|
||||
"DrawPixels",
|
||||
{ (uint8_t *)srcPixels },
|
||||
generateTexture,
|
||||
};
|
||||
Draw::Texture *tex = draw_->CreateTexture(desc);
|
||||
if (!tex)
|
||||
ERROR_LOG(G3D, "Failed to create drawpixels texture");
|
||||
return tex;
|
||||
}
|
||||
|
||||
// x, y, w, h are relative coordinates against destW/destH, which is not very intuitive.
|
||||
// TODO: This could totally use fbo_blit in many cases.
|
||||
void FramebufferManagerGLES::DrawActiveTexture(float x, float y, float w, float h, float destW, float destH, float u0, float v0, float u1, float v1, int uvRotation, int flags) {
|
||||
|
||||
@@ -73,7 +73,6 @@ private:
|
||||
void CreateDeviceObjects();
|
||||
void DestroyDeviceObjects();
|
||||
|
||||
Draw::Texture *MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) override;
|
||||
void Bind2DShader() override;
|
||||
void CompileDraw2DProgram();
|
||||
|
||||
|
||||
@@ -177,8 +177,11 @@ bool FramebufferManagerGLES::NotifyStencilUpload(u32 addr, int size, bool skipZe
|
||||
|
||||
float u1 = 1.0f;
|
||||
float v1 = 1.0f;
|
||||
Draw::Texture *tex = MakePixelTexture(src, dstBuffer->format, dstBuffer->fb_stride, dstBuffer->width, dstBuffer->height, u1, v1);
|
||||
textureCacheGL_->ForgetLastTexture();
|
||||
Draw::Texture *tex = MakePixelTexture(src, dstBuffer->format, dstBuffer->fb_stride, dstBuffer->width, dstBuffer->height, u1, v1);
|
||||
if (!tex)
|
||||
return false;
|
||||
|
||||
draw_->BindTextures(TEX_SLOT_PSP_TEXTURE, 1, &tex);
|
||||
|
||||
// We must bind the program after starting the render pass, and set the color mask after clearing.
|
||||
|
||||
@@ -176,58 +176,6 @@ void FramebufferManagerVulkan::Init() {
|
||||
Resized();
|
||||
}
|
||||
|
||||
Draw::Texture *FramebufferManagerVulkan::MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) {
|
||||
auto generateTexture = [&](uint8_t *data, const uint8_t *initData, uint32_t w, uint32_t h, uint32_t d, uint32_t byteStride, uint32_t sliceByteStride) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
const u16_le *src16 = (const u16_le *)srcPixels + srcStride * y;
|
||||
const u32_le *src32 = (const u32_le *)srcPixels + srcStride * y;
|
||||
u32 *dst = (u32 *)(data + byteStride * y);
|
||||
switch (srcPixelFormat) {
|
||||
case GE_FORMAT_565:
|
||||
ConvertRGBA565ToRGBA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_5551:
|
||||
ConvertRGBA5551ToRGBA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_4444:
|
||||
ConvertRGBA4444ToRGBA8888(dst, src16, width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_8888:
|
||||
memcpy(dst, src32, 4 * width);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_INVALID:
|
||||
_dbg_assert_msg_(G3D, false, "Invalid pixelFormat passed to DrawPixels().");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Hot Shot Golf (#12355) does tons of these in a frame in some situations! So actually,
|
||||
// we do use an allocator. In fact, I've now banned allocator-less textures.
|
||||
|
||||
Draw::TextureDesc desc{
|
||||
Draw::TextureType::LINEAR2D,
|
||||
Draw::DataFormat::R8G8B8A8_UNORM,
|
||||
width,
|
||||
height,
|
||||
1,
|
||||
1,
|
||||
false,
|
||||
"DrawPixels",
|
||||
{ (uint8_t *)srcPixels },
|
||||
generateTexture,
|
||||
};
|
||||
// TODO: Use allocator_ somehow?
|
||||
Draw::Texture *tex = draw_->CreateTexture(desc);
|
||||
if (!tex)
|
||||
ERROR_LOG(G3D, "Failed to create drawpixels texture");
|
||||
return tex;
|
||||
}
|
||||
|
||||
void FramebufferManagerVulkan::DrawActiveTexture(float x, float y, float w, float h, float destW, float destH, float u0, float v0, float u1, float v1, int uvRotation, int flags) {
|
||||
float texCoords[8] = {
|
||||
u0,v0,
|
||||
|
||||
@@ -78,9 +78,6 @@ protected:
|
||||
void UpdateDownloadTempBuffer(VirtualFramebuffer *nvfb) override;
|
||||
|
||||
private:
|
||||
// The returned texture does not need to be free'd, might be returned from a pool (currently single entry)
|
||||
Draw::Texture *MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) override;
|
||||
|
||||
void InitDeviceObjects();
|
||||
void DestroyDeviceObjects();
|
||||
|
||||
|
||||
@@ -164,6 +164,9 @@ bool FramebufferManagerVulkan::NotifyStencilUpload(u32 addr, int size, bool skip
|
||||
float u1 = 1.0f;
|
||||
float v1 = 1.0f;
|
||||
Draw::Texture *tex = MakePixelTexture(src, dstBuffer->format, dstBuffer->fb_stride, dstBuffer->bufferWidth, dstBuffer->bufferHeight, u1, v1);
|
||||
if (!tex)
|
||||
return false;
|
||||
|
||||
if (dstBuffer->fbo) {
|
||||
draw_->BindFramebufferAsRenderTarget(dstBuffer->fbo, { Draw::RPAction::KEEP, Draw::RPAction::KEEP, Draw::RPAction::CLEAR });
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user