Keep the GE-exact FIXA/FIXB blend within fp16's range

Compute the product as (c+0.5) * (2f+1)/512, which is still exact in fp32, and
use mediump where highp isn't available instead of falling back to the old
blend. Simulated over all inputs, about 3% of fp16 results come out a level
too bright, against nearly half with the plain blend factors. With mediump
forced on NVIDIA's Vulkan driver (which runs it as fp16), the Split/Second
bloom dumps land within 2% of the highp result.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-09 18:50:22 +02:00
1 parent b882155d23
commit ce310e69a6
2 files changed
+9 -8

No files matched your search

+8 -6
View File
@@ -1018,14 +1018,16 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu
// 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.
// This needs highp: the product reaches 511*511, past fp16's range, and the default precision is lowp,
// which NVIDIA's Vulkan driver runs as fp16.
WRITE(p, " highp vec3 fixA2 = u_blendFixA;\n");
WRITE(p, " highp vec3 fixB2 = u_blendFixB;\n");
WRITE(p, " highp vec3 srcTerm = v.rgb;\n");
// The product is computed as (c+0.5) * (2f+1)/512 to stay within fp16's range. That's exact in fp32,
// while in fp16 about 3% of results come out a level too bright, so use highp where we have it.
// The default precision is lowp, which can be fixed point, and NVIDIA's Vulkan driver runs it as fp16.
const char *prec = gstate_c.Use(GPU_USE_FULL_PRECISION_IN_FRAGMENT) ? "highp" : "mediump";
WRITE(p, " %s vec3 fixA2 = u_blendFixA;\n", prec);
WRITE(p, " %s vec3 fixB2 = u_blendFixB;\n", prec);
WRITE(p, " %s vec3 srcTerm = v.rgb;\n", prec);
WRITE(p, " fixA2 = floor(fixA2 * 510.0 + 0.5) + 1.0;\n");
WRITE(p, " fixB2 = floor(fixB2 * 510.0 + 0.5) + 1.0;\n");
WRITE(p, " srcTerm = floor((floor(srcTerm * 255.0 + 0.01) * 2.0 + 1.0) * fixA2 / 1024.0);\n");
WRITE(p, " srcTerm = floor((floor(srcTerm * 255.0 + 0.01) + 0.5) * (fixA2 / 512.0));\n");
WRITE(p, " v.rgb = (srcTerm - 0.5 + fixB2 / 1024.0) / 255.0;\n");
} else {
WRITE(p, " v.rgb = v.rgb * %s;\n", srcFactor);
+1 -2
View File
@@ -69,9 +69,8 @@ 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.
// The math needs highp floats in the fragment shader, which some old GLES devices lack.
inline bool IsGEExactFixedBlend(GEBlendDstFactor funcB, GEBlendMode eq) {
return funcB == GE_DSTBLEND_FIXB && eq == GE_BLENDMODE_MUL_AND_ADD && gstate_c.Use(GPU_USE_FULL_PRECISION_IN_FRAGMENT);
return funcB == GE_DSTBLEND_FIXB && eq == GE_BLENDMODE_MUL_AND_ADD;
}
// This is for the fallback path if real logic ops are not available.