Merge pull request #22465 from hrydgard/hw-ge-blends

HW backends: Improve accuracy of blending
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-09 11:05:28 +02:00
commit bd8f37bf0f
17 files changed
+86 -25

No files matched your search

+2 -6
View File
@@ -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) {
+1 -1
View File
@@ -99,7 +99,7 @@ struct VkRenderData {
uint8_t stencilRef;
} stencil;
struct {
uint32_t color;
float color[4];
} blendColor;
struct {
VkShaderStageFlags stages;
+2 -2
View File
@@ -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) {
+1 -2
View File
@@ -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) {
+1
View File
@@ -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},
+2
View File
@@ -178,6 +178,8 @@ struct CommandLineOptions {
std::optional<std::string> screenshotFilenameSave;
std::optional<std::string> depthFilenameSave;
std::optional<int> replayEnd;
// Headless: apply the game's compatibility settings when replaying a GE frame dump.
std::optional<bool> dumpCompat;
std::optional<bool> screenshotRenderTarget;
std::optional<std::string> screenshotFilenameDiff;
// Headless: preserve the alpha channel when saving PNG screenshots.
+2
View File
@@ -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;
+1 -1
View File
@@ -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();
+15 -1
View File
@@ -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) {
+38 -2
View File
@@ -19,6 +19,7 @@
#include <algorithm>
#include <limits>
#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);
+14 -2
View File
@@ -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();
};
+2 -3
View File
@@ -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 {
+1 -1
View File
@@ -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;
+1 -2
View File
@@ -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);
}
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1
View File
@@ -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;