From 0ad07d15609985813dbe5d19c57315af5cc4b801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 2 Nov 2026 14:09:07 +0100 Subject: [PATCH] Reinterpret: Write the stencil from the source, in the same render pass Rebuilding the stencil after a reinterpret (89fabe21fb) copied the whole destination to a temp buffer and redrew the stencil from it in a pass of its own. Tantalus games reinterpret about 70 times a frame, and on an M1 with Vulkan that made MX vs ATV Untamed 44% slower (GPU bound). Now a stencil variant of the reinterpret shader reads the source the same way the color reinterpret does and draws the stencil from its alpha, into the same rectangle right after it, so it lands in the same render pass and needs no copy. One draw with stencil export, otherwise a clear and one per alpha bit. The 1141 dumps in ppsspp-framedumps render identically before and after, on Vulkan (export) and OpenGL (per bit) on macOS. Split Second, OutRun and Ultimate Ghosts 'n Goblins dumps go through this path. Co-Authored-By: Claude Opus 5.5 --- GPU/Common/Draw2D.cpp | 32 ++++++++++----- GPU/Common/Draw2D.h | 10 +++-- GPU/Common/FramebufferManagerCommon.cpp | 25 ++++++++++-- GPU/Common/FramebufferManagerCommon.h | 4 +- GPU/Common/ReinterpretFramebuffer.cpp | 38 ++++++++++++++++-- GPU/Common/ReinterpretFramebuffer.h | 14 ++++++- GPU/Common/StencilCommon.cpp | 52 ++++++++++++++++--------- unittest/TestShaderGenerators.cpp | 39 +++++++++++-------- 8 files changed, 157 insertions(+), 57 deletions(-) diff --git a/GPU/Common/Draw2D.cpp b/GPU/Common/Draw2D.cpp index 80f3f2a68e..a451fa6e71 100644 --- a/GPU/Common/Draw2D.cpp +++ b/GPU/Common/Draw2D.cpp @@ -42,11 +42,17 @@ const UniformDef g_draw2Duniforms[2] = { { "float", "scaleFactor", 1}, }; +const UniformDef g_draw2DStencilUniforms[3] = { + { "vec2", "texSize", 0 }, + { "float", "scaleFactor", 1}, + { "float", "stencilValue", 2}, +}; + struct Draw2DUB { float texSizeX; float texSizeY; float scaleFactor; - float zScale; + float stencilValue; float zScaleInv; float zOffset; }; @@ -54,6 +60,7 @@ struct Draw2DUB { const UniformBufferDesc draw2DUBDesc{ sizeof(Draw2DUB), { { "texSize", -1, 0, UniformType::FLOAT2, 0 }, { "scaleFactor", -1, 1, UniformType::FLOAT1, 8 }, + { "stencilValue", -1, 2, UniformType::FLOAT1, 12 }, } }; Draw2DPipelineInfo GenerateDraw2DCopyColorFs(ShaderWriter &writer) { @@ -253,7 +260,7 @@ void Draw2D::Ensure2DResources() { } } -Draw2DPipeline *Draw2D::Create2DPipeline(std::function generate) { +Draw2DPipeline *Draw2D::Create2DPipeline(std::function generate, Slice extensions) { Ensure2DResources(); using namespace Draw; @@ -264,7 +271,7 @@ Draw2DPipeline *Draw2D::Create2DPipeline(std::function::empty(), flags); + ShaderWriter writer(fsCode, shaderLanguageDesc, ShaderStage::Fragment, extensions, flags); Draw2DPipelineInfo info = generate(writer); _assert_msg_(strlen(fsCode) < 8192, "Draw2D FS length error: %d", (int)strlen(fsCode)); @@ -282,10 +289,16 @@ Draw2DPipeline *Draw2D::Create2DPipeline(std::functionCreateInputLayout(desc); - BlendState *blend = draw_->CreateBlendState({ false, info.writeChannel == RASTER_COLOR ? 0xF : 0x0 }); + BlendState *blend = draw_->CreateBlendState({ false, (info.writeChannel == RASTER_COLOR && !info.writeStencil) ? 0xF : 0x0 }); DepthStencilStateDesc dsDesc{}; - if (info.writeChannel == RASTER_DEPTH) { + if (info.writeStencil) { + dsDesc.stencilEnabled = true; + dsDesc.stencil.compareOp = Comparison::ALWAYS; + dsDesc.stencil.failOp = StencilOp::REPLACE; + dsDesc.stencil.passOp = StencilOp::REPLACE; + dsDesc.stencil.depthFailOp = StencilOp::REPLACE; + } else if (info.writeChannel == RASTER_DEPTH) { dsDesc.depthTestEnabled = true; dsDesc.depthWriteEnabled = true; dsDesc.depthCompare = Draw::Comparison::ALWAYS; @@ -321,7 +334,7 @@ Draw2DPipeline *Draw2D::Create2DPipeline(std::functionWidth() : texW; ub.texSizeY = tex ? tex->Height() : texH; ub.scaleFactor = (float)scaleFactor; + ub.stencilValue = stencilValue; draw_->BindPipeline(pipeline->pipeline); draw_->UpdateDynamicUniformBuffer(&ub, sizeof(ub)); diff --git a/GPU/Common/Draw2D.h b/GPU/Common/Draw2D.h index a311cf06db..0bf1539020 100644 --- a/GPU/Common/Draw2D.h +++ b/GPU/Common/Draw2D.h @@ -42,9 +42,13 @@ struct Draw2DPipelineInfo { RasterChannel writeChannel; Slice samplers; bool readSamples = false; // Binds the source with BindFramebufferSamplesAsTexture. + // Writes only the stencil, with REPLACE: the reference from SetStencilParams, or exported by the shader. + bool writeStencil = false; }; extern const UniformDef g_draw2Duniforms[2]; +// g_draw2Duniforms plus stencilValue, for writeStencil pipelines. +extern const UniformDef g_draw2DStencilUniforms[3]; struct Draw2DPipeline { Draw::Pipeline *pipeline; @@ -67,11 +71,11 @@ public: void DeviceLost(); void DeviceRestore(Draw::DrawContext *draw); - Draw2DPipeline *Create2DPipeline(std::function generate); + Draw2DPipeline *Create2DPipeline(std::function generate, Slice extensions = Slice::empty()); - void DrawStrip2D(Draw::Texture *tex, const Draw2DVertex *verts, int vertexCount, bool linearFilter, Draw2DPipeline *pipeline, float texW = 0.0f, float texH = 0.0f, int scaleFactor = 0); + void DrawStrip2D(Draw::Texture *tex, const Draw2DVertex *verts, int vertexCount, bool linearFilter, Draw2DPipeline *pipeline, float texW = 0.0f, float texH = 0.0f, int scaleFactor = 0, float stencilValue = 0.0f); - void Blit(Draw2DPipeline *pipeline, float srcX1, float srcY1, float srcX2, float srcY2, float dstX1, float dstY1, float dstX2, float dstY2, float srcWidth, float srcHeight, float dstWidth, float dstHeight, bool linear, int scaleFactor); + void Blit(Draw2DPipeline *pipeline, float srcX1, float srcY1, float srcX2, float srcY2, float dstX1, float dstY1, float dstX2, float dstY2, float srcWidth, float srcHeight, float dstWidth, float dstHeight, bool linear, int scaleFactor, float stencilValue = 0.0f); void Ensure2DResources(); private: diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp index 62ad201f37..184b241fae 100644 --- a/GPU/Common/FramebufferManagerCommon.cpp +++ b/GPU/Common/FramebufferManagerCommon.cpp @@ -942,16 +942,16 @@ void FramebufferManagerCommon::CopyToColorFromOverlappingFramebuffers(VirtualFra // OK we have the pipeline, now just do the blit. BlitUsingRaster(src->fbo, 0.0f, 0.0f, srcWidth, srcHeight, dst->fbo, dstX1, dstY1, dstX2, dstY2, false, dst->renderScaleFactor, pipeline, pass_name); + if (src->fb_format != dst->fb_format) { + // In the same render pass. + WriteStencilFromReinterpret(src, dst, srcWidth, srcHeight, dstX1, dstY1, dstX2, dstY2); + } } if (scaleFactorX == 1.0f && dst->z_address == src->z_address && dst->z_stride == src->z_stride) { // We should also copy the depth buffer in this case! BlitFramebufferDepth(src, dst, true); } - - if (pipeline && src->fb_format != dst->fb_format) { - WriteStencilFromFramebufferAlpha(dst); - } } } @@ -993,6 +993,22 @@ Draw2DPipeline *FramebufferManagerCommon::GetReinterpretPipeline(GEBufferFormat return pipeline; } +Draw2DPipeline *FramebufferManagerCommon::GetReinterpretStencilPipeline(GEBufferFormat from, GEBufferFormat to, bool useExport) { + // Which variant is decided by the device, so it doesn't change. + Draw2DPipeline *pipeline = reinterpretStencilFromTo_[(int)from][(int)to]; + if (!pipeline) { + const Draw::Bugs &bugs = draw_->GetBugs(); + const bool discardNeedsDepthWrite = bugs.Has(Draw::Bugs::NO_DEPTH_CANNOT_DISCARD_STENCIL_MALI) || bugs.Has(Draw::Bugs::NO_DEPTH_CANNOT_DISCARD_STENCIL_ADRENO); + const ReinterpretOutput output = useExport ? ReinterpretOutput::STENCIL_EXPORT : ReinterpretOutput::STENCIL_BITS; + static const char *const exportExtensions[1] = { g_stencilExportExtension }; + pipeline = draw2D_.Create2DPipeline([from, to, output, discardNeedsDepthWrite](ShaderWriter &shaderWriter) -> Draw2DPipelineInfo { + return GenerateReinterpretFragmentShader(shaderWriter, from, to, output, discardNeedsDepthWrite); + }, useExport ? Slice(exportExtensions) : Slice::empty()); + reinterpretStencilFromTo_[(int)from][(int)to] = pipeline; + } + return pipeline; +} + void FramebufferManagerCommon::DestroyFramebuf(VirtualFramebuffer *v) { // Notify the texture cache of both the color and depth buffers. textureCache_->NotifyFramebuffer(v, NOTIFY_FB_DESTROYED); @@ -3785,6 +3801,7 @@ void FramebufferManagerCommon::ReleasePipelines() { for (int i = 0; i < ARRAY_SIZE(reinterpretFromTo_); i++) { for (int j = 0; j < ARRAY_SIZE(reinterpretFromTo_); j++) { DoRelease(reinterpretFromTo_[i][j]); + DoRelease(reinterpretStencilFromTo_[i][j]); } } DoRelease(stencilWriteSampler_); diff --git a/GPU/Common/FramebufferManagerCommon.h b/GPU/Common/FramebufferManagerCommon.h index 6b7b53148d..f60b6b7f14 100644 --- a/GPU/Common/FramebufferManagerCommon.h +++ b/GPU/Common/FramebufferManagerCommon.h @@ -362,7 +362,7 @@ public: void ApplyClearToMemory(int x1, int y1, int x2, int y2, u32 clearColor); bool PerformWriteStencilFromMemory(u32 addr, int size, WriteStencil flags); bool ReadbackStencilToMemory(u32 addr, int size); - void WriteStencilFromFramebufferAlpha(VirtualFramebuffer *dst); + void WriteStencilFromReinterpret(VirtualFramebuffer *src, VirtualFramebuffer *dst, float srcX2, float srcY2, float dstX1, float dstY1, float dstX2, float dstY2); // We changed our depth mode, gotta start over. // Ideally, we should convert depth buffers here, not just clear them. @@ -504,6 +504,7 @@ public: // If from==to, returns a copy pipeline. Draw2DPipeline *GetReinterpretPipeline(GEBufferFormat from, GEBufferFormat to, float *scaleFactorX); + Draw2DPipeline *GetReinterpretStencilPipeline(GEBufferFormat from, GEBufferFormat to, bool useExport); // Public to be used from the texture cache's depal shenanigans. void BlitUsingRaster( @@ -688,6 +689,7 @@ protected: // Safe, not optimal - there might be input attachment tricks, etc, but we can't use them // since we don't want N different implementations. Draw2DPipeline *reinterpretFromTo_[4][4]{}; + Draw2DPipeline *reinterpretStencilFromTo_[4][4]{}; // Common implementation of stencil buffer upload. Also not 100% optimal, but not performance // critical either. diff --git a/GPU/Common/ReinterpretFramebuffer.cpp b/GPU/Common/ReinterpretFramebuffer.cpp index a5e89c8eee..5749a94cf6 100644 --- a/GPU/Common/ReinterpretFramebuffer.cpp +++ b/GPU/Common/ReinterpretFramebuffer.cpp @@ -14,12 +14,20 @@ static const SamplerDef samplers[1] = { { 0, "tex", SamplerFlags::ARRAY_ON_VULKAN } }; +const char *const g_stencilExportExtension = "#extension GL_ARB_shader_stencil_export : require"; + // Requires full size integer math. It would be possible to make a floating point-only version with lots of // modulo and stuff, might do it one day. -Draw2DPipelineInfo GenerateReinterpretFragmentShader(ShaderWriter &writer, GEBufferFormat from, GEBufferFormat to) { +Draw2DPipelineInfo GenerateReinterpretFragmentShader(ShaderWriter &writer, GEBufferFormat from, GEBufferFormat to, ReinterpretOutput output, bool discardNeedsDepthWrite) { + const bool stencil = output != ReinterpretOutput::COLOR; + writer.HighPrecisionFloat(); writer.DeclareSamplers(samplers); + if (output == ReinterpretOutput::STENCIL_BITS && discardNeedsDepthWrite) { + writer.C("layout (depth_unchanged) out float gl_FragDepth;\n"); + } + if (writer.Lang().bitwiseOps) { switch (from) { case GE_FORMAT_4444: @@ -157,7 +165,11 @@ Draw2DPipelineInfo GenerateReinterpretFragmentShader(ShaderWriter &writer, GEBuf } } - writer.BeginFSMain(g_draw2Duniforms, varyings); + if (stencil) { + writer.BeginFSMain(g_draw2DStencilUniforms, varyings); + } else { + writer.BeginFSMain(g_draw2Duniforms, varyings); + } if (IsBufferFormat16Bit(from) && IsBufferFormat16Bit(to)) { writer.C(" vec4 val = ").SampleTexture2D("tex", "v_texcoord.xy").C(";\n"); @@ -189,12 +201,30 @@ Draw2DPipelineInfo GenerateReinterpretFragmentShader(ShaderWriter &writer, GEBuf writer.C(" vec4 outColor = unpackColor(u == 0.0 ? packColor(val.rg) : packColor(val.ba));\n"); } + if (stencil) { + // The same rounding as the stencil upload shader. + writer.C(" float alpha = floor(outColor.a * 255.99);\n"); + if (output == ReinterpretOutput::STENCIL_EXPORT) { + writer.C(" gl_FragStencilRefARB = int(alpha);\n"); + } else { + writer.C(" if (stencilValue > 0.0) {\n"); + writer.C(" float shifted = alpha / floor(stencilValue * 255.99);\n"); + writer.C(" if (mod(floor(shifted), 2.0) < 0.99) DISCARD;\n"); + writer.C(" }\n"); + if (discardNeedsDepthWrite) { + writer.C(" gl_FragDepth = gl_FragCoord.z;\n"); + } + } + } + writer.EndFSMain("outColor"); - return Draw2DPipelineInfo{ - "reinterpret", + Draw2DPipelineInfo info{ + stencil ? "reinterpret_stencil" : "reinterpret", RASTER_COLOR, RASTER_COLOR, }; + info.writeStencil = stencil; + return info; } diff --git a/GPU/Common/ReinterpretFramebuffer.h b/GPU/Common/ReinterpretFramebuffer.h index f1a78a9b88..bb2d468cd1 100644 --- a/GPU/Common/ReinterpretFramebuffer.h +++ b/GPU/Common/ReinterpretFramebuffer.h @@ -4,4 +4,16 @@ #include "GPU/ge_constants.h" #include "GPU/Common/Draw2D.h" -Draw2DPipelineInfo GenerateReinterpretFragmentShader(ShaderWriter &writer, GEBufferFormat from, GEBufferFormat to); +enum class ReinterpretOutput { + COLOR, + // The stencil from the reinterpreted alpha (the PSP's stencil is the alpha bits). The bit to write is in + // stencilValue (as in the stencil upload shader), and 0 writes everywhere, to clear. + STENCIL_BITS, + // The whole stencil in one draw, through GL_ARB_shader_stencil_export. + STENCIL_EXPORT, +}; + +// The extension the writer needs for STENCIL_EXPORT. +extern const char *const g_stencilExportExtension; + +Draw2DPipelineInfo GenerateReinterpretFragmentShader(ShaderWriter &writer, GEBufferFormat from, GEBufferFormat to, ReinterpretOutput output = ReinterpretOutput::COLOR, bool discardNeedsDepthWrite = false); diff --git a/GPU/Common/StencilCommon.cpp b/GPU/Common/StencilCommon.cpp index 52f47f81a7..5ec519b06e 100644 --- a/GPU/Common/StencilCommon.cpp +++ b/GPU/Common/StencilCommon.cpp @@ -264,8 +264,10 @@ void FramebufferManagerCommon::DrawStencilWritePasses(GEBufferFormat format, int // On the PSP the stencil is the alpha bits, so after reinterpreting the colors of another format into a buffer, its // stencil has to follow the new alpha (Ultimate Ghosts 'n Goblins sets 5551 alpha through a masked 565 blue write, -// then stencil tests on it). -void FramebufferManagerCommon::WriteStencilFromFramebufferAlpha(VirtualFramebuffer *dst) { +// then stencil tests on it). Called right after the color reinterpret, with the same rectangles, so dst is still the +// render target and the draws land in the same render pass. They read the alpha the same way from src, so dst needs +// no copy. +void FramebufferManagerCommon::WriteStencilFromReinterpret(VirtualFramebuffer *src, VirtualFramebuffer *dst, float srcX2, float srcY2, float dstX1, float dstY1, float dstX2, float dstY2) { using namespace Draw; int values = 0; @@ -275,30 +277,42 @@ void FramebufferManagerCommon::WriteStencilFromFramebufferAlpha(VirtualFramebuff case GE_FORMAT_8888: values = 256; break; default: return; } - if (!dst->fbo) { + if (!src->fbo || !dst->fbo) { return; } - const bool useExportShader = draw_->GetDeviceCaps().fragmentShaderStencilWriteSupported; - EnsureStencilWritePipeline(useExportShader); + const bool useExport = draw_->GetDeviceCaps().fragmentShaderStencilWriteSupported; + Draw2DPipeline *pipeline = GetReinterpretStencilPipeline(src->fb_format, dst->fb_format, useExport); - // The source can't be the target, so read the alpha from a copy. - const int w = dst->renderWidth; - const int h = dst->renderHeight; - Framebuffer *copy = GetTempFBO(TempFBO::COPY, w, h); - BlitUsingRaster(dst->fbo, 0.0f, 0.0f, (float)w, (float)h, copy, 0.0f, 0.0f, (float)w, (float)h, false, dst->renderScaleFactor, Get2DPipeline(DRAW2D_COPY_COLOR), "StencilFromAlpha_Copy"); + int srcW, srcH, dstW, dstH; + draw_->GetFramebufferDimensions(src->fbo, &srcW, &srcH); + draw_->GetFramebufferDimensions(dst->fbo, &dstW, &dstH); + draw_->BindFramebufferAsTexture(src->fbo, 0, Aspect::COLOR_BIT, ALL_LAYERS); - draw_->BindFramebufferAsRenderTarget(dst->fbo, { RPAction::KEEP, RPAction::KEEP, RPAction::CLEAR }, "StencilFromAlpha"); - Viewport viewport = { 0.0f, 0.0f, (float)w, (float)h, 0.0f, 1.0f }; - draw_->SetViewport(viewport); - draw_->SetScissorRect(0, 0, w, h); - draw_->BindFramebufferAsTexture(copy, TEX_SLOT_PSP_TEXTURE, Aspect::COLOR_BIT, 0); - draw_->BindSamplerStates(TEX_SLOT_PSP_TEXTURE, 1, &stencilWriteSampler_); - draw_->BindPipeline(stencilWritePipeline_); - DrawStencilWritePasses(dst->fb_format, values, 0xFF, useExportShader); + auto drawStencil = [&](uint8_t ref, uint8_t writeMask, float stencilValue) { + // Vulkan only applies the stencil params to a bound pipeline that uses stencil. + draw_->BindPipeline(pipeline->pipeline); + draw_->SetStencilParams(ref, writeMask, 0xFF); + draw2D_.Blit(pipeline, 0.0f, 0.0f, srcX2, srcY2, dstX1, dstY1, dstX2, dstY2, (float)srcW, (float)srcH, (float)dstW, (float)dstH, false, dst->renderScaleFactor, stencilValue); + }; + + if (useExport) { + drawStencil(0xFF, 0xFF, 0.0f); + } else { + // A draw can only set one bit, so clear first, then set each bit where the alpha has it. + drawStencil(0, 0xFF, 0.0f); + for (int i = 1; i < values; i += i) { + if (dst->fb_format == GE_FORMAT_4444) { + drawStencil(0xFF, (i << 4) | i, i * (16.0f / 255.0f)); + } else if (dst->fb_format == GE_FORMAT_5551) { + drawStencil(0xFF, 0xFF, i * (128.0f / 255.0f)); + } else { + drawStencil(0xFF, i, i * (1.0f / 255.0f)); + } + } + } textureCache_->ForgetLastTexture(); - draw_->Invalidate(InvalidationFlags::CACHED_RENDER_STATE); gstate_c.Dirty(DIRTY_ALL_RENDER_STATE); } diff --git a/unittest/TestShaderGenerators.cpp b/unittest/TestShaderGenerators.cpp index 70358e5806..7af8c4dc31 100644 --- a/unittest/TestShaderGenerators.cpp +++ b/unittest/TestShaderGenerators.cpp @@ -199,22 +199,29 @@ bool TestReinterpretShaders() { std::string errorMessage; - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - if (i == j) - continue; // useless shader! - ShaderWriter writer(buffer, desc, ShaderStage::Fragment); - GenerateReinterpretFragmentShader(writer, fmts[i], fmts[j]); - if (strlen(buffer) >= 8192) { - printf("Reinterpret fragment shader %d exceeded buffer:\n\n%s\n", (int)j, LineNumberString(buffer).c_str()); - failed = true; - } - if (!TestCompileShader(buffer, languages[k], ShaderStage::Fragment, &errorMessage)) { - printf("Error compiling reinterpret fragment shader %d:\n\n%s\n\n%s\n", (int)j, LineNumberString(buffer).c_str(), errorMessage.c_str()); - failed = true; - } else { - if (g_testLog) { - printf("===\n%s\n===\n", buffer); + // Stencil export only on Vulkan, as GLSL_3xx is ES, which doesn't support it. + const int numOutputs = languages[k] == ShaderLanguage::GLSL_VULKAN ? 3 : 2; + for (int o = 0; o < numOutputs; o++) { + const ReinterpretOutput output = (ReinterpretOutput)o; + const char *extensions[1] = { g_stencilExportExtension }; + const Slice writerExtensions = output == ReinterpretOutput::STENCIL_EXPORT ? Slice(extensions) : Slice::empty(); + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (i == j) + continue; // useless shader! + ShaderWriter writer(buffer, desc, ShaderStage::Fragment, writerExtensions); + GenerateReinterpretFragmentShader(writer, fmts[i], fmts[j], output); + if (strlen(buffer) >= 8192) { + printf("Reinterpret fragment shader %d (output %d) exceeded buffer:\n\n%s\n", (int)j, o, LineNumberString(buffer).c_str()); + failed = true; + } + if (!TestCompileShader(buffer, languages[k], ShaderStage::Fragment, &errorMessage)) { + printf("Error compiling reinterpret fragment shader %d (output %d):\n\n%s\n\n%s\n", (int)j, o, LineNumberString(buffer).c_str(), errorMessage.c_str()); + failed = true; + } else { + if (g_testLog) { + printf("===\n%s\n===\n", buffer); + } } } }