From 35010391a7e4a3b0b025809a8978a0585ae9a15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 13 Jul 2026 11:42:55 +0200 Subject: [PATCH 1/4] Correct framebuffer offset, after the refactor. Fixes #21915 --- GPU/Common/GPUStateUtils.cpp | 31 +++++++++++++--------------- GPU/Common/VertexShaderGenerator.cpp | 2 +- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/GPU/Common/GPUStateUtils.cpp b/GPU/Common/GPUStateUtils.cpp index a0076a5291..0bfcbccaa7 100644 --- a/GPU/Common/GPUStateUtils.cpp +++ b/GPU/Common/GPUStateUtils.cpp @@ -518,15 +518,14 @@ ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) { } void ConvertViewportAndScissor(const DisplayLayoutConfig &config, bool useBufferedRendering, float renderWidth, float renderHeight, int bufferWidth, int bufferHeight, ViewportAndScissor &out) { - float renderWidthFactor, renderHeightFactor; - float renderX = 0.0f, renderY = 0.0f; - float displayOffsetX, displayOffsetY; - if (useBufferedRendering) { - displayOffsetX = 0.0f; - displayOffsetY = 0.0f; - renderWidthFactor = (float)renderWidth / (float)bufferWidth; - renderHeightFactor = (float)renderHeight / (float)bufferHeight; - } else { + float renderWidthFactor = (float)renderWidth / (float)bufferWidth; + float renderHeightFactor = (float)renderHeight / (float)bufferHeight; + float renderX = 0.0f; + float renderY = 0.0f; + float displayOffsetX = 0.0f; + float displayOffsetY = 0.0f; + + if (!useBufferedRendering) { float pixelW = PSP_CoreParameter().pixelWidth; float pixelH = PSP_CoreParameter().pixelHeight; FRect frame = GetScreenFrame(config.bIgnoreScreenInsets, pixelW, pixelH); @@ -540,31 +539,29 @@ void ConvertViewportAndScissor(const DisplayLayoutConfig &config, bool useBuffer renderHeightFactor = renderHeight / 272.0f; } - // Scissor + // Scissor. The scissor needs to be offset by the framebuffer offset. int scissorX1 = gstate.getScissorX1(); int scissorY1 = gstate.getScissorY1(); int scissorX2 = gstate.getScissorX2() + 1; int scissorY2 = gstate.getScissorY2() + 1; if (scissorX2 < scissorX1 || scissorY2 < scissorY1) { + // Bad scissor, kill all drawing with a valid scissor setup. out.scissorX = 0; out.scissorY = 0; out.scissorW = 0; out.scissorH = 0; } else { - out.scissorX = displayOffsetX + scissorX1 * renderWidthFactor; - out.scissorY = displayOffsetY + scissorY1 * renderHeightFactor; + out.scissorX = displayOffsetX + (scissorX1 + gstate_c.curRTOffsetX) * renderWidthFactor; + out.scissorY = displayOffsetY + (scissorY1 + gstate_c.curRTOffsetY) * renderHeightFactor; out.scissorW = (scissorX2 - scissorX1) * renderWidthFactor; out.scissorH = (scissorY2 - scissorY1) * renderHeightFactor; } - int curRTWidth = gstate_c.curRTWidth; - int curRTHeight = gstate_c.curRTHeight; - out.viewportX = displayOffsetX; out.viewportY = displayOffsetY; - out.viewportW = curRTWidth * renderWidthFactor; - out.viewportH = curRTHeight * renderHeightFactor; + out.viewportW = gstate_c.curRTWidth * renderWidthFactor; + out.viewportH = gstate_c.curRTHeight * renderHeightFactor; } static const BlendFactor genericALookup[11] = { diff --git a/GPU/Common/VertexShaderGenerator.cpp b/GPU/Common/VertexShaderGenerator.cpp index c2c3635871..2f9831b145 100644 --- a/GPU/Common/VertexShaderGenerator.cpp +++ b/GPU/Common/VertexShaderGenerator.cpp @@ -1042,7 +1042,7 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag } // Convert to NDC space, using the framebuffer offset and size stored in u_xywh. - WRITE(p, " outPos.xy = ((outPos.xy - u_xywh.xy) / u_xywh.zw) * 2.0 - 1.0;\n"); + WRITE(p, " outPos.xy = ((outPos.xy + u_xywh.xy) / u_xywh.zw) * 2.0 - 1.0;\n"); if (gstate_c.Use(GPU_ROUND_DEPTH_TO_16BIT)) { // Actually 15-bit. Truncate here fixes Afterburner (similarly to the min/max clipping above). From a2fb013cf6badc54063f5d8da4427421e35629b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 13 Jul 2026 11:46:55 +0200 Subject: [PATCH 2/4] ConvertViewportAndScissor: Have a nice clean path for the common case. --- GPU/Common/GPUStateUtils.cpp | 84 +++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/GPU/Common/GPUStateUtils.cpp b/GPU/Common/GPUStateUtils.cpp index 0bfcbccaa7..e07665b8be 100644 --- a/GPU/Common/GPUStateUtils.cpp +++ b/GPU/Common/GPUStateUtils.cpp @@ -517,51 +517,65 @@ ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) { return REPLACE_BLEND_STANDARD; } +// Viewport and scissor really could be treated entirely separately, but the non-buffered case is nicer by doing them "together". void ConvertViewportAndScissor(const DisplayLayoutConfig &config, bool useBufferedRendering, float renderWidth, float renderHeight, int bufferWidth, int bufferHeight, ViewportAndScissor &out) { - float renderWidthFactor = (float)renderWidth / (float)bufferWidth; - float renderHeightFactor = (float)renderHeight / (float)bufferHeight; - float renderX = 0.0f; - float renderY = 0.0f; - float displayOffsetX = 0.0f; - float displayOffsetY = 0.0f; + // Scissor. The scissor needs to be offset by the framebuffer offset. + const int scissorX1 = gstate.getScissorX1(); + const int scissorY1 = gstate.getScissorY1(); + const int scissorX2 = gstate.getScissorX2() + 1; + const int scissorY2 = gstate.getScissorY2() + 1; - if (!useBufferedRendering) { + if (useBufferedRendering) { + const float renderWidthFactor = (float)renderWidth / (float)bufferWidth; + const float renderHeightFactor = (float)renderHeight / (float)bufferHeight; + + if (scissorX2 < scissorX1 || scissorY2 < scissorY1) { + // Bad scissor, kill all drawing with a valid scissor setup. + out.scissorX = 0; + out.scissorY = 0; + out.scissorW = 0; + out.scissorH = 0; + } else { + out.scissorX = (scissorX1 + gstate_c.curRTOffsetX) * renderWidthFactor; + out.scissorY = (scissorY1 + gstate_c.curRTOffsetY) * renderHeightFactor; + out.scissorW = (scissorX2 - scissorX1) * renderWidthFactor; + out.scissorH = (scissorY2 - scissorY1) * renderHeightFactor; + } + out.viewportX = 0.0f; + out.viewportY = 0.0f; + out.viewportW = gstate_c.curRTWidth * renderWidthFactor; + out.viewportH = gstate_c.curRTHeight * renderHeightFactor; + } else { + // Hacky path for non-buffered rendering. float pixelW = PSP_CoreParameter().pixelWidth; float pixelH = PSP_CoreParameter().pixelHeight; FRect frame = GetScreenFrame(config.bIgnoreScreenInsets, pixelW, pixelH); FRect rc; CalculateDisplayOutputRect(config, &rc, 480, 272, frame, ROTATION_LOCKED_HORIZONTAL); - displayOffsetX = rc.x; - displayOffsetY = rc.y; + const float displayOffsetX = rc.x; + const float displayOffsetY = rc.y; renderWidth = rc.w; renderHeight = rc.h; - renderWidthFactor = renderWidth / 480.0f; - renderHeightFactor = renderHeight / 272.0f; + const float renderWidthFactor = renderWidth / 480.0f; + const float renderHeightFactor = renderHeight / 272.0f; + if (scissorX2 < scissorX1 || scissorY2 < scissorY1) { + // Bad scissor, kill all drawing with a valid scissor setup. + out.scissorX = 0; + out.scissorY = 0; + out.scissorW = 0; + out.scissorH = 0; + } else { + out.scissorX = displayOffsetX + (scissorX1 + gstate_c.curRTOffsetX) * renderWidthFactor; + out.scissorY = displayOffsetY + (scissorY1 + gstate_c.curRTOffsetY) * renderHeightFactor; + out.scissorW = (scissorX2 - scissorX1) * renderWidthFactor; + out.scissorH = (scissorY2 - scissorY1) * renderHeightFactor; + } + + out.viewportX = displayOffsetX; + out.viewportY = displayOffsetY; + out.viewportW = gstate_c.curRTWidth * renderWidthFactor; + out.viewportH = gstate_c.curRTHeight * renderHeightFactor; } - - // Scissor. The scissor needs to be offset by the framebuffer offset. - int scissorX1 = gstate.getScissorX1(); - int scissorY1 = gstate.getScissorY1(); - int scissorX2 = gstate.getScissorX2() + 1; - int scissorY2 = gstate.getScissorY2() + 1; - - if (scissorX2 < scissorX1 || scissorY2 < scissorY1) { - // Bad scissor, kill all drawing with a valid scissor setup. - out.scissorX = 0; - out.scissorY = 0; - out.scissorW = 0; - out.scissorH = 0; - } else { - out.scissorX = displayOffsetX + (scissorX1 + gstate_c.curRTOffsetX) * renderWidthFactor; - out.scissorY = displayOffsetY + (scissorY1 + gstate_c.curRTOffsetY) * renderHeightFactor; - out.scissorW = (scissorX2 - scissorX1) * renderWidthFactor; - out.scissorH = (scissorY2 - scissorY1) * renderHeightFactor; - } - - out.viewportX = displayOffsetX; - out.viewportY = displayOffsetY; - out.viewportW = gstate_c.curRTWidth * renderWidthFactor; - out.viewportH = gstate_c.curRTHeight * renderHeightFactor; } static const BlendFactor genericALookup[11] = { From c3206390164c0ca76218c81e1b314d1c5ff1d87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 13 Jul 2026 11:50:47 +0200 Subject: [PATCH 3/4] Simplify the math. --- GPU/Common/GPUStateUtils.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/GPU/Common/GPUStateUtils.cpp b/GPU/Common/GPUStateUtils.cpp index e07665b8be..ddedde12d5 100644 --- a/GPU/Common/GPUStateUtils.cpp +++ b/GPU/Common/GPUStateUtils.cpp @@ -522,14 +522,14 @@ void ConvertViewportAndScissor(const DisplayLayoutConfig &config, bool useBuffer // Scissor. The scissor needs to be offset by the framebuffer offset. const int scissorX1 = gstate.getScissorX1(); const int scissorY1 = gstate.getScissorY1(); - const int scissorX2 = gstate.getScissorX2() + 1; - const int scissorY2 = gstate.getScissorY2() + 1; + const int scissorW = gstate.getScissorX2() + 1 - scissorX1; + const int scissorH = gstate.getScissorY2() + 1 - scissorY1; if (useBufferedRendering) { const float renderWidthFactor = (float)renderWidth / (float)bufferWidth; const float renderHeightFactor = (float)renderHeight / (float)bufferHeight; - if (scissorX2 < scissorX1 || scissorY2 < scissorY1) { + if (scissorW < 0 || scissorH < 0) { // Bad scissor, kill all drawing with a valid scissor setup. out.scissorX = 0; out.scissorY = 0; @@ -538,8 +538,8 @@ void ConvertViewportAndScissor(const DisplayLayoutConfig &config, bool useBuffer } else { out.scissorX = (scissorX1 + gstate_c.curRTOffsetX) * renderWidthFactor; out.scissorY = (scissorY1 + gstate_c.curRTOffsetY) * renderHeightFactor; - out.scissorW = (scissorX2 - scissorX1) * renderWidthFactor; - out.scissorH = (scissorY2 - scissorY1) * renderHeightFactor; + out.scissorW = scissorW * renderWidthFactor; + out.scissorH = scissorH * renderHeightFactor; } out.viewportX = 0.0f; out.viewportY = 0.0f; @@ -558,7 +558,7 @@ void ConvertViewportAndScissor(const DisplayLayoutConfig &config, bool useBuffer renderHeight = rc.h; const float renderWidthFactor = renderWidth / 480.0f; const float renderHeightFactor = renderHeight / 272.0f; - if (scissorX2 < scissorX1 || scissorY2 < scissorY1) { + if (scissorW < 0 || scissorH < 0) { // Bad scissor, kill all drawing with a valid scissor setup. out.scissorX = 0; out.scissorY = 0; @@ -567,8 +567,8 @@ void ConvertViewportAndScissor(const DisplayLayoutConfig &config, bool useBuffer } else { out.scissorX = displayOffsetX + (scissorX1 + gstate_c.curRTOffsetX) * renderWidthFactor; out.scissorY = displayOffsetY + (scissorY1 + gstate_c.curRTOffsetY) * renderHeightFactor; - out.scissorW = (scissorX2 - scissorX1) * renderWidthFactor; - out.scissorH = (scissorY2 - scissorY1) * renderHeightFactor; + out.scissorW = scissorW * renderWidthFactor; + out.scissorH = scissorH * renderHeightFactor; } out.viewportX = displayOffsetX; From 38b0b0b126de23f4c006d1054df4b44e43d5a3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 13 Jul 2026 12:11:31 +0200 Subject: [PATCH 4/4] Eliminate two divisions and multiplication from all vertex shaders --- GPU/Common/ShaderUniforms.cpp | 4 ++-- GPU/Common/VertexShaderGenerator.cpp | 4 ++-- GPU/GLES/ShaderManagerGLES.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/GPU/Common/ShaderUniforms.cpp b/GPU/Common/ShaderUniforms.cpp index 27d4bf8b1f..7f07262c4d 100644 --- a/GPU/Common/ShaderUniforms.cpp +++ b/GPU/Common/ShaderUniforms.cpp @@ -79,8 +79,8 @@ void BaseUpdateUniforms(UB_VS_FS_Base *ub, uint64_t dirtyUniforms, bool useBuffe if (dirtyUniforms & DIRTY_FRAMEBUFFER_DIM) { ub->xywh[0] = (float)gstate_c.curRTOffsetX; ub->xywh[1] = (float)gstate_c.curRTOffsetY; - ub->xywh[2] = (float)gstate_c.curRTWidth; - ub->xywh[3] = (float)gstate_c.curRTHeight; + ub->xywh[2] = (float)(2.0 / gstate_c.curRTWidth); // intentionally do double precision here. + ub->xywh[3] = (float)(2.0 / gstate_c.curRTHeight); ub->rotation = useBufferedRendering ? 0 : (float)g_display.rotation; } diff --git a/GPU/Common/VertexShaderGenerator.cpp b/GPU/Common/VertexShaderGenerator.cpp index 2f9831b145..731ebc5ce5 100644 --- a/GPU/Common/VertexShaderGenerator.cpp +++ b/GPU/Common/VertexShaderGenerator.cpp @@ -1041,8 +1041,8 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag WRITE(p, " %sgl_ClipDistance%s = u_minZmaxZ.y < 65535.0 ? (u_minZmaxZ.y - clipZFar) * outPos.w : 1.0;\n", compat.vsOutPrefix, maxZClipPlaneSuffix); } - // Convert to NDC space, using the framebuffer offset and size stored in u_xywh. - WRITE(p, " outPos.xy = ((outPos.xy + u_xywh.xy) / u_xywh.zw) * 2.0 - 1.0;\n"); + // Convert to NDC space, using the framebuffer offset and (inverse size * 2) stored in u_xywh. + WRITE(p, " outPos.xy = ((outPos.xy + u_xywh.xy) * u_xywh.zw) - vec2(1.0, 1.0);\n"); if (gstate_c.Use(GPU_ROUND_DEPTH_TO_16BIT)) { // Actually 15-bit. Truncate here fixes Afterburner (similarly to the min/max clipping above). diff --git a/GPU/GLES/ShaderManagerGLES.cpp b/GPU/GLES/ShaderManagerGLES.cpp index ce6cd5ec6d..ac3b72b8df 100644 --- a/GPU/GLES/ShaderManagerGLES.cpp +++ b/GPU/GLES/ShaderManagerGLES.cpp @@ -410,8 +410,8 @@ void LinkedShader::UpdateUniforms(const ShaderID &vsid, const ShaderLanguageDesc float xywh[4]; xywh[0] = (float)gstate_c.curRTOffsetX; xywh[1] = (float)gstate_c.curRTOffsetY; - xywh[2] = (float)gstate_c.curRTWidth; - xywh[3] = (float)gstate_c.curRTHeight; + xywh[2] = (float)(2.0 / gstate_c.curRTWidth); // intentionally double precision here + xywh[3] = (float)(2.0 / gstate_c.curRTHeight); SetFloatUniform4(render_, &u_xywh, xywh); float nan = std::numeric_limits::quiet_NaN(); render_->SetUniformF1(&u_NaN, nan);