From d0baab56149a79e594494e3595c7ce63d634acea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 8 Oct 2026 18:44:23 +0200 Subject: [PATCH 1/5] Compute FIXA/FIXB blends like the GE The GE's blend terms are floor((2c+1)(2f+1)/1024), which truncates, while the hardware multiplies by f/255 and rounds, so every pass of a fixed color blend chain came out up to a level brighter. God Eater Burst's bloom is many such passes and ended up far too bright. For additive FIXA/FIXB blends that aren't just 0 and 0xFFFFFF, the shader now computes the src term exactly and lowers it by 0.5 - (2B+1)/1024, and the blend constant is the GE's dst factor (2B+1)/512, so the hardware's rounding of the sum lands on the GE's floor (except where the src term would go below zero). Blend constants are passed as floats to make that possible. Co-Authored-By: Claude Opus 5.5 --- Common/GPU/Vulkan/VulkanQueueRunner.cpp | 8 ++----- Common/GPU/Vulkan/VulkanQueueRunner.h | 2 +- Common/GPU/Vulkan/VulkanRenderManager.h | 4 ++-- Common/GPU/Vulkan/thin3d_vulkan.cpp | 3 +-- GPU/Common/FragmentShaderGenerator.cpp | 12 +++++++++- GPU/Common/GPUStateUtils.cpp | 30 ++++++++++++++++++++++++- GPU/Common/GPUStateUtils.h | 16 +++++++++++-- GPU/D3D11/StateMappingD3D11.cpp | 5 ++--- GPU/D3D11/StateMappingD3D11.h | 2 +- GPU/GLES/StateMappingGLES.cpp | 3 +-- GPU/Vulkan/StateMappingVulkan.cpp | 2 +- GPU/Vulkan/StateMappingVulkan.h | 2 +- 12 files changed, 66 insertions(+), 23 deletions(-) diff --git a/Common/GPU/Vulkan/VulkanQueueRunner.cpp b/Common/GPU/Vulkan/VulkanQueueRunner.cpp index 7ea72f7bd4..d51997dbb6 100644 --- a/Common/GPU/Vulkan/VulkanQueueRunner.cpp +++ b/Common/GPU/Vulkan/VulkanQueueRunner.cpp @@ -933,7 +933,7 @@ void VulkanQueueRunner::LogRenderPass(const VKRStep &pass, bool verbose) { INFO_LOG(Log::G3D, " BindGraphicsPipeline(%x)", (int)(intptr_t)cmd.graphics_pipeline.pipeline); break; case VKRRenderCommand::BLEND: - INFO_LOG(Log::G3D, " BlendColor(%08x)", cmd.blendColor.color); + INFO_LOG(Log::G3D, " BlendColor(%f, %f, %f, %f)", cmd.blendColor.color[0], cmd.blendColor.color[1], cmd.blendColor.color[2], cmd.blendColor.color[3]); break; case VKRRenderCommand::CLEAR: INFO_LOG(Log::G3D, " Clear"); @@ -1179,12 +1179,8 @@ void VulkanQueueRunner::PerformRenderPass(const VKRStep &step, VkCommandBuffer c } case VKRRenderCommand::BLEND: - { - float bc[4]; - Uint8x4ToFloat4(bc, c.blendColor.color); - vkCmdSetBlendConstants(cmd, bc); + vkCmdSetBlendConstants(cmd, c.blendColor.color); break; - } case VKRRenderCommand::PUSH_CONSTANTS: if (pipelineOK) { diff --git a/Common/GPU/Vulkan/VulkanQueueRunner.h b/Common/GPU/Vulkan/VulkanQueueRunner.h index 6813b0a8da..eea23961cf 100644 --- a/Common/GPU/Vulkan/VulkanQueueRunner.h +++ b/Common/GPU/Vulkan/VulkanQueueRunner.h @@ -99,7 +99,7 @@ struct VkRenderData { uint8_t stencilRef; } stencil; struct { - uint32_t color; + float color[4]; } blendColor; struct { VkShaderStageFlags stages; diff --git a/Common/GPU/Vulkan/VulkanRenderManager.h b/Common/GPU/Vulkan/VulkanRenderManager.h index 0b19a4c2cb..d8df63f634 100644 --- a/Common/GPU/Vulkan/VulkanRenderManager.h +++ b/Common/GPU/Vulkan/VulkanRenderManager.h @@ -396,11 +396,11 @@ public: data.stencil.stencilRef = refValue; } - void SetBlendFactor(uint32_t color) { + void SetBlendFactor(const float color[4]) { _dbg_assert_(curRenderStep_ && curRenderStep_->stepType == VKRStepType::RENDER); VkRenderData &data = curRenderStep_->commands.push_uninitialized(); data.cmd = VKRRenderCommand::BLEND; - data.blendColor.color = color; + memcpy(data.blendColor.color, color, sizeof(data.blendColor.color)); } void PushConstants(VkShaderStageFlags stages, int offset, int size, void *constants) { diff --git a/Common/GPU/Vulkan/thin3d_vulkan.cpp b/Common/GPU/Vulkan/thin3d_vulkan.cpp index ddff19a817..9e28c5a92a 100644 --- a/Common/GPU/Vulkan/thin3d_vulkan.cpp +++ b/Common/GPU/Vulkan/thin3d_vulkan.cpp @@ -1370,8 +1370,7 @@ void VKContext::SetViewport(const Viewport &viewport) { } void VKContext::SetBlendFactor(float color[4]) { - uint32_t col = Float4ToUint8x4(color); - renderManager_.SetBlendFactor(col); + renderManager_.SetBlendFactor(color); } void VKContext::SetStencilParams(uint8_t refValue, uint8_t writeMask, uint8_t compareMask) { diff --git a/GPU/Common/FragmentShaderGenerator.cpp b/GPU/Common/FragmentShaderGenerator.cpp index 01bb2e85f1..bf5689c8b7 100644 --- a/GPU/Common/FragmentShaderGenerator.cpp +++ b/GPU/Common/FragmentShaderGenerator.cpp @@ -1008,7 +1008,17 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu return false; } - WRITE(p, " v.rgb = v.rgb * %s;\n", srcFactor); + if (replaceBlend == REPLACE_BLEND_PRE_SRC && replaceBlendFuncA == GE_SRCBLEND_FIXA && IsGEExactFixedBlend(replaceBlendFuncB, replaceBlendEq)) { + // The GE's product of a color and a factor is floor((2c+1)(2f+1)/1024), and the hardware blend multiplies + // the dst by (2B+1)/512 (see ConvertBlendState). Lowering the src term by 0.5 - (2B+1)/1024 turns the + // rounding of the sum into the GE's floor, except where that would go below zero. + WRITE(p, " vec3 fixA2 = floor(u_blendFixA * 510.0 + 0.5) + 1.0;\n"); + WRITE(p, " vec3 fixB2 = floor(u_blendFixB * 510.0 + 0.5) + 1.0;\n"); + WRITE(p, " v.rgb = floor((floor(v.rgb * 255.0 + 0.5) * 2.0 + 1.0) * fixA2 / 1024.0);\n"); + WRITE(p, " v.rgb = (v.rgb - 0.5 + fixB2 / 1024.0) / 255.0;\n"); + } else { + WRITE(p, " v.rgb = v.rgb * %s;\n", srcFactor); + } } if (replaceBlend == REPLACE_BLEND_READ_FRAMEBUFFER) { diff --git a/GPU/Common/GPUStateUtils.cpp b/GPU/Common/GPUStateUtils.cpp index 56ced6bd0e..4d49b81496 100644 --- a/GPU/Common/GPUStateUtils.cpp +++ b/GPU/Common/GPUStateUtils.cpp @@ -19,6 +19,7 @@ #include #include +#include "Common/Data/Convert/SmallDataConvert.h" #include "Core/ConfigValues.h" #include "Core/System.h" #include "Core/Config.h" @@ -262,6 +263,16 @@ StencilValueType ReplaceAlphaWithStencilType() { return STENCIL_VALUE_KEEP; } +void GenericBlendState::blendColorToFloat4(float out[4]) const { + Uint8x4ToFloat4(out, blendColor); + if (blendColorGE) { + for (int i = 0; i < 3; i++) { + const int c = (blendColor >> (i * 8)) & 0xFF; + out[i] = (2 * c + 1) / 512.0f; + } + } +} + ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) { if (gstate_c.blueToAlpha) { return REPLACE_BLEND_BLUE_TO_ALPHA; @@ -450,6 +461,11 @@ ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) { if (gstate.getFixA() == 0xFFFFFF && gstate.getFixB() == 0x000000) { // Some games specify this. Some GPUs may prefer blending off entirely. return REPLACE_BLEND_NO; + } else if (IsGEExactFixedBlend(funcB, gstate.getBlendEq())) { + // 0 and 0xFFFFFF are exact as blend factors. Other fixed colors need the GE's math, see ConvertBlendState. + const bool trivialA = gstate.getFixA() == 0xFFFFFF || gstate.getFixA() == 0x000000; + const bool trivialB = gstate.getFixB() == 0xFFFFFF || gstate.getFixB() == 0x000000; + return trivialA && trivialB ? REPLACE_BLEND_STANDARD : REPLACE_BLEND_PRE_SRC; } else if (gstate.getFixA() == 0xFFFFFF || gstate.getFixA() == 0x000000 || gstate.getFixB() == 0xFFFFFF || gstate.getFixB() == 0x000000) { // We can represent this with standard factors. return REPLACE_BLEND_STANDARD; @@ -1068,7 +1084,19 @@ static void ConvertBlendState(GenericBlendState &blendState, FBReadSetting useFB glBlendFuncB = toDualSource(glBlendFuncB); } - if (blendFuncA == GE_SRCBLEND_FIXA || blendFuncB == GE_DSTBLEND_FIXB) { + if (usePreSrc && replaceBlend == REPLACE_BLEND_PRE_SRC && blendFuncA == GE_SRCBLEND_FIXA && IsGEExactFixedBlend(blendFuncB, blendFuncEq)) { + // The shader computed the src term like the GE and lowered it so that the hardware's rounding of the + // sum lands on the GE's floor of it, given the dst factor (2B+1)/512 the GE multiplies with. + if (fixB == 0 || fixB == 0xFFFFFF) { + glBlendFuncB = fixB == 0 ? BlendFactor::ZERO : BlendFactor::ONE; + if (constantAlphaGL == BlendFactor::CONSTANT_ALPHA) { + blendState.defaultBlendColor(constantAlpha); + } + } else { + glBlendFuncB = BlendFactor::CONSTANT_COLOR; + blendState.setBlendColorGE(fixB, constantAlpha); + } + } else if (blendFuncA == GE_SRCBLEND_FIXA || blendFuncB == GE_DSTBLEND_FIXB) { if (glBlendFuncA == BlendFactor::INVALID && glBlendFuncB != BlendFactor::INVALID) { // Can use blendcolor trivially. blendState.setBlendColor(fixA, constantAlpha); diff --git a/GPU/Common/GPUStateUtils.h b/GPU/Common/GPUStateUtils.h index cc822c8b92..e855b69909 100644 --- a/GPU/Common/GPUStateUtils.h +++ b/GPU/Common/GPUStateUtils.h @@ -68,6 +68,11 @@ int ClearModeChannelMask(); ReplaceAlphaType ReplaceAlphaWithStencil(ReplaceBlendType replaceBlend); ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat); +// Fixed color blends (with FIXA as the src factor) that REPLACE_BLEND_PRE_SRC computes like the GE. +inline bool IsGEExactFixedBlend(GEBlendDstFactor funcB, GEBlendMode eq) { + return funcB == GE_DSTBLEND_FIXB && eq == GE_BLENDMODE_MUL_AND_ADD; +} + // This is for the fallback path if real logic ops are not available. SimulateLogicOpType SimulateLogicOpShaderTypeIfNeeded(); @@ -143,6 +148,8 @@ struct GenericBlendState { bool useBlendColor; u32 blendColor; + // The color channels are a GE fixed blend factor, applied as (2c+1)/512 like the GE does. + bool blendColorGE; void setFactors(BlendFactor srcC, BlendFactor dstC, BlendFactor srcA, BlendFactor dstA) { srcColor = srcC; @@ -157,11 +164,16 @@ struct GenericBlendState { void setBlendColor(uint32_t color, uint8_t alpha) { blendColor = color | ((uint32_t)alpha << 24); useBlendColor = true; + blendColorGE = false; + } + void setBlendColorGE(uint32_t color, uint8_t alpha) { + setBlendColor(color, alpha); + blendColorGE = true; } void defaultBlendColor(uint8_t alpha) { - blendColor = 0xFFFFFF | ((uint32_t)alpha << 24); - useBlendColor = true; + setBlendColor(0xFFFFFF, alpha); } + void blendColorToFloat4(float out[4]) const; void Log(); }; diff --git a/GPU/D3D11/StateMappingD3D11.cpp b/GPU/D3D11/StateMappingD3D11.cpp index 9d91719fc1..019037586f 100644 --- a/GPU/D3D11/StateMappingD3D11.cpp +++ b/GPU/D3D11/StateMappingD3D11.cpp @@ -192,7 +192,7 @@ void DrawEngineD3D11::ApplyDrawState(int prim) { } dynState_.useBlendColor = blendState.useBlendColor; if (blendState.useBlendColor) { - dynState_.blendColor = blendState.blendColor; + blendState.blendColorToFloat4(dynState_.blendColor); } } else { keys_.blend.blendEnable = false; @@ -437,8 +437,7 @@ void DrawEngineD3D11::ApplyDrawStateLate(bool applyStencilRef, uint8_t stencilRe } if (gstate_c.IsDirty(DIRTY_BLEND_STATE)) { // Need to do this AFTER ApplyTexture because the process of depalettization can ruin the blend state. - float blendColor[4]; - Uint8x4ToFloat4(blendColor, dynState_.blendColor); + const float *blendColor = dynState_.blendColor; if (device1_) { context1_->OMSetBlendState(blendState1_, blendColor, 0xFFFFFFFF); } else { diff --git a/GPU/D3D11/StateMappingD3D11.h b/GPU/D3D11/StateMappingD3D11.h index 0e484b211c..a6c93329d5 100644 --- a/GPU/D3D11/StateMappingD3D11.h +++ b/GPU/D3D11/StateMappingD3D11.h @@ -65,7 +65,7 @@ struct D3D11StateKeys { struct D3D11DynamicState { int topology; bool useBlendColor; - uint32_t blendColor; + float blendColor[4]; bool useStencil; uint8_t stencilRef; Draw::Viewport viewport; diff --git a/GPU/GLES/StateMappingGLES.cpp b/GPU/GLES/StateMappingGLES.cpp index e0a32f9f0e..30d1cb8c90 100644 --- a/GPU/GLES/StateMappingGLES.cpp +++ b/GPU/GLES/StateMappingGLES.cpp @@ -180,9 +180,8 @@ void DrawEngineGLES::ApplyDrawState(int prim) { gstate_c.Dirty(DIRTY_SHADERBLEND); } if (blendState.useBlendColor) { - uint32_t color = blendState.blendColor; float col[4]; - Uint8x4ToFloat4(col, color); + blendState.blendColorToFloat4(col); renderManager->SetBlendFactor(col); } } diff --git a/GPU/Vulkan/StateMappingVulkan.cpp b/GPU/Vulkan/StateMappingVulkan.cpp index 4b54ada10f..b6833bb8dc 100644 --- a/GPU/Vulkan/StateMappingVulkan.cpp +++ b/GPU/Vulkan/StateMappingVulkan.cpp @@ -173,7 +173,7 @@ void DrawEngineVulkan::ConvertStateToVulkanKey(FramebufferManagerVulkan &fbManag } dynState.useBlendColor = blendState.useBlendColor; if (blendState.useBlendColor) { - dynState.blendColor = blendState.blendColor; + blendState.blendColorToFloat4(dynState.blendColor); } } else { key.blendEnable = false; diff --git a/GPU/Vulkan/StateMappingVulkan.h b/GPU/Vulkan/StateMappingVulkan.h index 8f706bb9ae..d8ac015fd5 100644 --- a/GPU/Vulkan/StateMappingVulkan.h +++ b/GPU/Vulkan/StateMappingVulkan.h @@ -15,7 +15,7 @@ struct VulkanDynamicState { VkViewport viewport; ScissorRect scissor; bool useBlendColor; - uint32_t blendColor; + float blendColor[4]; bool useStencil; uint8_t stencilRef; uint8_t stencilWriteMask; From eef6e3b4e9f6f28e98ef662ad97dc42f8b6da005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 8 Oct 2026 23:39:14 +0200 Subject: [PATCH 2/5] Truncate the color in the GE-exact FIXA/FIXB blend, like the GE's filter The GE's bilinear filter truncates (4-bit weights, twice), and rounding the GPU's finer filtered value made each texel about a level brighter. God Eater Burst's bloom is faint and feeds back into itself pass after pass, so that roughly doubled it: its dumps go from 15-31k px off the PSP to 0.4-3k, the #17033 ones from 79k/107k to 25k/47k. Split/Second and Call of Duty: Road to Victory improve too. Co-Authored-By: Claude Opus 5.5 --- GPU/Common/FragmentShaderGenerator.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/GPU/Common/FragmentShaderGenerator.cpp b/GPU/Common/FragmentShaderGenerator.cpp index bf5689c8b7..65954fc2ec 100644 --- a/GPU/Common/FragmentShaderGenerator.cpp +++ b/GPU/Common/FragmentShaderGenerator.cpp @@ -1012,9 +1012,13 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu // The GE's product of a color and a factor is floor((2c+1)(2f+1)/1024), and the hardware blend multiplies // the dst by (2B+1)/512 (see ConvertBlendState). Lowering the src term by 0.5 - (2B+1)/1024 turns the // rounding of the sum into the GE's floor, except where that would go below zero. + // The color is truncated too: the GE's bilinear filter truncates (4-bit weights, see the software + // Sampler), and rounding the GPU's finer filtered value instead makes each texel about a level + // brighter. That doubles God Eater Burst's faint bloom, which feeds back into itself pass after pass. + // The 0.01 keeps an unfiltered texel's exact integer from flooring a level down on float error. WRITE(p, " vec3 fixA2 = floor(u_blendFixA * 510.0 + 0.5) + 1.0;\n"); WRITE(p, " vec3 fixB2 = floor(u_blendFixB * 510.0 + 0.5) + 1.0;\n"); - WRITE(p, " v.rgb = floor((floor(v.rgb * 255.0 + 0.5) * 2.0 + 1.0) * fixA2 / 1024.0);\n"); + WRITE(p, " v.rgb = floor((floor(v.rgb * 255.0 + 0.01) * 2.0 + 1.0) * fixA2 / 1024.0);\n"); WRITE(p, " v.rgb = (v.rgb - 0.5 + fixB2 / 1024.0) / 255.0;\n"); } else { WRITE(p, " v.rgb = v.rgb * %s;\n", srcFactor); From e05a3b29fb7495390c08b877fc58fc3752e7ae49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 8 Oct 2026 20:54:16 +0200 Subject: [PATCH 3/5] Read the framebuffer for DSTCOLOR + DOUBLESRCALPHA blends in through mode The result is dst * (src + 2a), up to 3x the dst, which fixed function blending can't reach: doubling alpha in the shader clamps it to 2x. MotorStorm Arctic Edge and Tomb Raider Anniversary brighten their whole frame this way and came out much too dark. Through mode draws are mostly full screen passes that don't overlap themselves, so the framebuffer copy is safe there; 3D draws keep the approximation (copies there caused overlap problems in Silent Hill). Co-Authored-By: Claude Opus 5.5 --- GPU/Common/GPUStateUtils.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/GPU/Common/GPUStateUtils.cpp b/GPU/Common/GPUStateUtils.cpp index 4d49b81496..23e9541871 100644 --- a/GPU/Common/GPUStateUtils.cpp +++ b/GPU/Common/GPUStateUtils.cpp @@ -504,8 +504,12 @@ ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) { // We will just hope that doubling alpha for the dst factor will not clamp too badly. if (gstate_c.Use(GPU_USE_FRAMEBUFFER_FETCH)) return REPLACE_BLEND_READ_FRAMEBUFFER; + // Through mode is mostly full screen passes that don't overlap themselves, so a copy is safe there. + // MotorStorm brightens its frame with DSTCOLOR + DOUBLESRCALPHA at alpha 0xFE, which is up to 3x the dst, + // and doubling alpha in the shader clamps that to 2x. + if (gstate.isModeThrough()) + return REPLACE_BLEND_READ_FRAMEBUFFER; // Hm, this is similar to the L.A. Rush case above. This will not be accurate. - // Wonder in which games we encounter this? One example is MotorStorm. return REPLACE_BLEND_2X_ALPHA; } From 74991f96d3d87284042cf4bc5db4f05db629e503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 8 Oct 2026 23:05:10 +0200 Subject: [PATCH 4/5] Read the framebuffer for DOUBLESRCALPHA + DOUBLESRCALPHA blends in through mode With alpha above 0.5 the dst factor goes past 1, which 2x alpha in the shader clamps: Super Stardust composites its bloom at alpha 0xFF, doubling the dst, and came out at 1x without the bloom brightening. Same through mode reasoning as for DSTCOLOR + DOUBLESRCALPHA. Also fixes Ford Bold Moves Street Racing. Co-Authored-By: Claude Opus 5.5 --- GPU/Common/GPUStateUtils.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/GPU/Common/GPUStateUtils.cpp b/GPU/Common/GPUStateUtils.cpp index 23e9541871..91f3793d9f 100644 --- a/GPU/Common/GPUStateUtils.cpp +++ b/GPU/Common/GPUStateUtils.cpp @@ -335,6 +335,10 @@ ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) { // Using a copy isn't accurate either, though, when there's overlap. if (gstate_c.Use(GPU_USE_FRAMEBUFFER_FETCH)) return REPLACE_BLEND_READ_FRAMEBUFFER; + // Through mode is mostly full screen passes that don't overlap themselves, so a copy is safe there. + // Super Stardust composites its bloom at alpha 0xFF, doubling the dst, which 2x alpha clamps to 1x. + if (gstate.isModeThrough()) + return REPLACE_BLEND_READ_FRAMEBUFFER; return REPLACE_BLEND_PRE_SRC_2X_ALPHA; case GE_DSTBLEND_DOUBLEINVSRCALPHA: From 0dc6381ba4d9d423f4b7a80b81eb469a916af1fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 9 Oct 2026 09:54:25 +0200 Subject: [PATCH 5/5] Headless: Add --dump-compat, to apply compat settings to frame dumps Headless leaves a game's compat.ini settings out when replaying a frame dump, so comparisons with the PSP see what the renderer itself does. This puts them back, for checking what a compat entry does to a dump (Puyo Puyo Fever 2's depth rounding). Co-Authored-By: Claude Opus 5.5 --- Core/CmdLine.cpp | 1 + Core/CmdLine.h | 2 ++ Core/CoreParameter.h | 2 ++ Core/System.cpp | 2 +- headless/Headless.cpp | 1 + 5 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index b796ec4a05..4c8fca4178 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -205,6 +205,7 @@ static const CommandLineParam g_autoParams[] = { {POFF(screenshotFilename), CmdParamType::String, "screenshot", '\0', "Compare rendered output against a reference screenshot FILE", CmdLineMode::Headless}, {POFF(screenshotFilenameSave), CmdParamType::String, "screenshot-save", '\0', "Save rendered screenshot to specified path (PNG if the path ends in .png, BMP otherwise)", CmdLineMode::Headless}, {POFF(replayEnd), CmdParamType::Int, "replay-end", '\0', "When replaying a GE frame dump, only draw primitives up to this one (1-based)", CmdLineMode::Headless}, + {POFF(dumpCompat), CmdParamType::Bool, "dump-compat", '\0', "Apply the game's compat.ini settings when replaying a GE frame dump (off by default)", CmdLineMode::Headless}, {POFF(depthFilenameSave), CmdParamType::String, "depth-save", '\0', "Save the depth buffer to FILE along with the screenshot (u32 width, u32 height, u16 depths; float depths if FILE ends in .f32)", CmdLineMode::Headless}, {POFF(screenshotRenderTarget), CmdParamType::Bool, "screenshot-render-target", '\0', "Take screenshots of the current render target instead of the displayed framebuffer", CmdLineMode::Headless}, {POFF(screenshotFilenameDiff), CmdParamType::String, "screenshot-diff", '\0', "Save a visual comparison image to FILE when comparing screenshots", CmdLineMode::Headless}, diff --git a/Core/CmdLine.h b/Core/CmdLine.h index 02f534ffdf..4df2726356 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -178,6 +178,8 @@ struct CommandLineOptions { std::optional screenshotFilenameSave; std::optional depthFilenameSave; std::optional replayEnd; + // Headless: apply the game's compatibility settings when replaying a GE frame dump. + std::optional dumpCompat; std::optional screenshotRenderTarget; std::optional screenshotFilenameDiff; // Headless: preserve the alpha channel when saving PNG screenshots. diff --git a/Core/CoreParameter.h b/Core/CoreParameter.h index 9558ba2cac..8e93d7eae5 100644 --- a/Core/CoreParameter.h +++ b/Core/CoreParameter.h @@ -75,6 +75,8 @@ struct CoreParameter { bool startBreak = false; std::string *collectDebugOutput = nullptr; bool headLess = false; // Try to avoid messageboxes etc + // Headless leaves out compat settings when replaying frame dumps, unless this is set. + bool dumpCompat = false; // Internal PSP rendering resolution and scale factor. int renderScaleFactor = 1; diff --git a/Core/System.cpp b/Core/System.cpp index 9aed799bcd..7c9567ade7 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -498,7 +498,7 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin // Homebrew get fake disc IDs assigned to the global paramSFO, so they shouldn't clash with real games. std::string discId = g_paramSFO.GetDiscID(); - if (type == IdentifiedFileType::PPSSPP_GE_DUMP && g_CoreParameter.headLess) { + if (type == IdentifiedFileType::PPSSPP_GE_DUMP && g_CoreParameter.headLess && !g_CoreParameter.dumpCompat) { // Headless replays frame dumps to compare them with references and with the PSP, which a game's // workarounds would skew: DarkStalkers' present hack replaces the game's final upscale. g_CoreParameter.compat.Clear(); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index a54269d8c0..9af80bfcc7 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -1070,6 +1070,7 @@ int main(int argc, const char* argv[]) { coreParameter.mountRoot = mountRoot.empty() ? Path() : Path(mountRoot); coreParameter.startBreak = false; coreParameter.headLess = true; + coreParameter.dumpCompat = cmdLineOptions.dumpCompat.value_or(false); coreParameter.loadGameConfigs = false; coreParameter.renderScaleFactor = cmdLineOptions.resolutionScale.value_or(1); coreParameter.renderWidth = 480 * coreParameter.renderScaleFactor;