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/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/GPU/Common/FragmentShaderGenerator.cpp b/GPU/Common/FragmentShaderGenerator.cpp index 01bb2e85f1..65954fc2ec 100644 --- a/GPU/Common/FragmentShaderGenerator.cpp +++ b/GPU/Common/FragmentShaderGenerator.cpp @@ -1008,7 +1008,21 @@ 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. + // 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.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); + } } if (replaceBlend == REPLACE_BLEND_READ_FRAMEBUFFER) { diff --git a/GPU/Common/GPUStateUtils.cpp b/GPU/Common/GPUStateUtils.cpp index 56ced6bd0e..91f3793d9f 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; @@ -324,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: @@ -450,6 +465,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; @@ -488,8 +508,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; } @@ -1068,7 +1092,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; 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;