Fix some issues with depth rounding, and depth clamp in D3D11

This commit is contained in:
Henrik Rydgård
2026-05-30 19:07:59 +02:00
parent 3f6582aa7b
commit f8b153ba2b
12 changed files with 48 additions and 48 deletions
+18 -28
View File
@@ -186,7 +186,7 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu
}
bool needFragCoord = readFramebufferTex || gstate_c.Use(GPU_ROUND_FRAGMENT_DEPTH_TO_16BIT);
bool writeDepth = (gstate_c.Use(GPU_ROUND_FRAGMENT_DEPTH_TO_16BIT) && !forceDepthWritesOff) || fsDepthClamp;
bool writeDepth = (gstate_c.Use(GPU_ROUND_FRAGMENT_DEPTH_TO_16BIT) || fsDepthClamp) && !forceDepthWritesOff;
// TODO: We could have a separate mechanism to support more ops using the shader blending mechanism,
// on hardware that can do proper bit math in fragment shaders.
@@ -1180,36 +1180,26 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu
if (fsMinmaxDiscard) {
// See the vertex shader generator for the explanation for this.
WRITE(p, " float clipZ = floor(projZ * 0.5 + 0.5) * 2.0;\n");
WRITE(p, " if (u_minZmaxZ.x > 0 && clipZ < u_minZmaxZ.x) DISCARD;\n");
WRITE(p, " if (u_minZmaxZ.y < 65535 && clipZ > u_minZmaxZ.y) DISCARD;\n");
WRITE(p, " if (u_minZmaxZ.x > 0.0 && clipZ < u_minZmaxZ.x) DISCARD;\n");
WRITE(p, " if (u_minZmaxZ.y < 65535.0 && clipZ > u_minZmaxZ.y) DISCARD;\n");
}
if (gstate_c.Use(GPU_ROUND_FRAGMENT_DEPTH_TO_16BIT)) {
DepthScaleFactors depthScale = GetDepthScaleFactors(gstate_c.UseFlags());
const double scale = depthScale.ScaleU16();
if (fsDepthClamp) {
WRITE(p, " highp float z = clamp(projZ, 0.0, 65536.0) / 65536.0;\n");
} else {
WRITE(p, " highp float z = gl_FragCoord.z;\n");
if (writeDepth) {
if (gstate_c.Use(GPU_ROUND_FRAGMENT_DEPTH_TO_16BIT)) {
if (fsDepthClamp) {
WRITE(p, " gl_FragDepth = clamp(floor(projZ), 0.0, 65536.0) / 65536.0;\n");
} else {
WRITE(p, " gl_FragDepth = floor(gl_FragCoord.z) / 65536.0;\n");
}
} else if (fsDepthClamp) {
WRITE(p, " gl_FragDepth = clamp(projZ, 0.0, 65536.0) / 65536.0;\n");
} else if (useDiscardStencilBugWorkaround) {
// Adreno and some Mali drivers apply early frag tests even with discard in the shader,
// when only stencil is used. The exact situation seems to vary by driver.
// Writing depth prevents the bug for both vendors, even with depth_unchanged specified.
// This doesn't make a ton of sense, but empirically does work.
WRITE(p, " gl_FragDepth = gl_FragCoord.z;\n");
}
// We center the depth with an offset, but only its fraction matters.
// When (DepthSliceFactor() - 1) is odd, it will be 0.5, otherwise 0.
if (((int)(depthScale.Scale() - 1.0f) & 1) == 1) {
WRITE(p, " z = (floor((z * %f) - (1.0 / 2.0)) + (1.0 / 2.0)) * (1.0 / %f);\n", scale, scale);
} else {
WRITE(p, " z = floor(z * %f) * (1.0 / %f);\n", scale, scale);
}
WRITE(p, " gl_FragDepth = z;\n");
} else if (fsDepthClamp) {
WRITE(p, " gl_FragDepth = clamp(projZ, 0.0, 65536.0) / 65536.0;\n");
} else if (useDiscardStencilBugWorkaround) {
// Adreno and some Mali drivers apply early frag tests even with discard in the shader,
// when only stencil is used. The exact situation seems to vary by driver.
// Writing depth prevents the bug for both vendors, even with depth_unchanged specified.
// This doesn't make a ton of sense, but empirically does work.
WRITE(p, " gl_FragDepth = gl_FragCoord.z;\n");
}
if (compat.shaderLanguage == HLSL_D3D11) {
+4 -4
View File
@@ -299,20 +299,20 @@ inline u32 SanitizeBlendMode(GEBlendMode mode) {
// Here we must take all the bits of the gstate that determine what the fragment shader will
// look like, and concatenate them together into an ID.
void ComputeFragmentShaderID(FShaderID *id_out, const ComputedPipelineState &pipelineState, const Draw::Bugs &bugs, bool useHwTransform, ClipInfoFlags clipInfoFlags) {
void ComputeFragmentShaderID(FShaderID *id_out, const ComputedPipelineState &pipelineState, const Draw::Bugs &bugs, ClipInfoFlags clipInfoFlags) {
FShaderID id;
bool isModeThrough = gstate.isModeThrough();
// We exclude hwTransform mode here, although we could absolutely do this in hwtransform as well, because we detect
// draws that needs this and use software transform as the fallback for them. That logic will have to change if we change that.
// NOTE: This check MUST be identical to the one in ComputeVertexShaderID, otherwise we might get mismatches between VS and FS and end up with no shader at all.
if (!useHwTransform && !isModeThrough) {
if (!isModeThrough) {
if (clipInfoFlags & ClipInfoFlags::DepthClampFragment) {
id.SetBit(FS_BIT_DEPTH_CLAMP);
}
if (clipInfoFlags & ClipInfoFlags::MinMaxZDiscard) {
id.SetBit(FS_BIT_MINMAX_DISCARD);
}
} else {
_dbg_assert_(0 == (clipInfoFlags & (ClipInfoFlags::DepthClampFragment | ClipInfoFlags::MinMaxZDiscard)));
}
if (gstate.isModeClear()) {
+2 -2
View File
@@ -18,7 +18,7 @@ inline bool needFragmentMinMaxClipping() {
inline bool needFragmentDepthClamp() {
// If gstate.isDepthClipEnabled is false, clamping does not happen, instead fragments are culled as normal.
return (gstate.getDepthRangeMin() == 0 || gstate.getDepthRangeMax() == 0xFFFF) && gstate.isDepthClipEnabled() && !gstate_c.Use(GPU_USE_DEPTH_CLAMP);
return (gstate.getDepthRangeMin() == 0 || gstate.getDepthRangeMax() == 0xFFFF) && gstate.isDepthClipEnabled();
}
// VS_BIT_LIGHT_UBERSHADER indicates that some groups of these will be
@@ -268,7 +268,7 @@ void ComputeVertexShaderID(VShaderID *id, u32 vertType, bool useHWTransform, boo
std::string VertexShaderDesc(const VShaderID &id);
struct ComputedPipelineState;
void ComputeFragmentShaderID(FShaderID *id, const ComputedPipelineState &pipelineState, const Draw::Bugs &bugs, bool useHwTransform, ClipInfoFlags clipInfoFlags);
void ComputeFragmentShaderID(FShaderID *id, const ComputedPipelineState &pipelineState, const Draw::Bugs &bugs, ClipInfoFlags clipInfoFlags);
std::string FragmentShaderDesc(const FShaderID &id);
// For sanity checking.
+4 -2
View File
@@ -294,8 +294,10 @@ void DrawEngineD3D11::Flush() {
// Always use software for flat shading to fix the provoking index.
bool tess = gstate_c.submitType == SubmitType::HW_BEZIER || gstate_c.submitType == SubmitType::HW_SPLINE;
bool useHWTransform = CanUseHardwareTransform(prim) && (tess || gstate.getShadeMode() != GE_SHADE_FLAT);
if (clipInfoFlags_ & ClipInfoFlags::SoftClipCull) {
useHWTransform = false;
if (clipInfoFlags_ & ClipInfoFlags::Valid) {
if (clipInfoFlags_ & ClipInfoFlags::SoftClipCull) {
useHWTransform = false;
}
}
if (useHWTransform != lastUseHwTransform_) {
+1 -1
View File
@@ -199,7 +199,7 @@ void ShaderManagerD3D11::GetShaders(int prim, u32 vertexType, D3D11VertexShader
if (gstate_c.IsDirty(DIRTY_FRAGMENTSHADER_STATE)) {
gstate_c.Clean(DIRTY_FRAGMENTSHADER_STATE);
ComputeFragmentShaderID(&FSID, pipelineState, draw_->GetBugs(), useHWTransform, clipInfoFlags);
ComputeFragmentShaderID(&FSID, pipelineState, draw_->GetBugs(), clipInfoFlags);
} else {
FSID = lastFSID_;
}
+2 -1
View File
@@ -230,7 +230,8 @@ void DrawEngineD3D11::ApplyDrawState(int prim) {
} else {
if (gstate.getDepthRangeMin() == 0 || gstate.getDepthRangeMax() == 65535) {
// We get some extra clamping behavior if clipping is enabled.
keys_.raster.depthClipEnable = !(gstate.isDepthClipEnabled() && !gstate_c.Use(GPU_USE_DEPTH_CLAMP));
const bool clamp = gstate.isDepthClipEnabled() && gstate_c.Use(GPU_USE_DEPTH_CLAMP);
keys_.raster.depthClipEnable = !clamp;
} else {
// We just want to clip in this case, the clamp would be clipped anyway.
keys_.raster.depthClipEnable = 1;
+6 -4
View File
@@ -254,8 +254,10 @@ void DrawEngineGLES::Flush() {
GEPrimitiveType prim = prevPrim_;
bool useHWTransform = CanUseHardwareTransform(prim);
if (clipInfoFlags_ & ClipInfoFlags::SoftClipCull) {
useHWTransform = false;
if (clipInfoFlags_ & ClipInfoFlags::Valid) {
if (clipInfoFlags_ & ClipInfoFlags::SoftClipCull) {
useHWTransform = false;
}
}
if (useHWTransform != lastUseHwTransform_) {
@@ -315,7 +317,7 @@ void DrawEngineGLES::Flush() {
ApplyDrawState(prim);
ApplyDrawStateLate(false, 0);
LinkedShader *program = shaderManager_->ApplyFragmentShader(vsid, vshader, pipelineState_, true, clipInfoFlags_);
LinkedShader *program = shaderManager_->ApplyFragmentShader(vsid, vshader, pipelineState_, clipInfoFlags_);
GLRInputLayout *inputLayout = SetupDecFmtForDraw(dec_->GetDecVtxFmt());
if (useElements) {
render_->DrawIndexed(inputLayout,
@@ -401,7 +403,7 @@ void DrawEngineGLES::Flush() {
ApplyDrawState(prim);
ApplyDrawStateLate(result.setStencil, result.stencilValue);
LinkedShader *linked = shaderManager_->ApplyFragmentShader(vsid, vshader, pipelineState_, false, clipInfoFlags_);
LinkedShader *linked = shaderManager_->ApplyFragmentShader(vsid, vshader, pipelineState_, clipInfoFlags_);
if (!linked) {
// Not much we can do here. Let's skip drawing.
goto bail;
+2 -2
View File
@@ -808,7 +808,7 @@ Shader *ShaderManagerGLES::ApplyVertexShader(bool useHWTransform, bool useHWTess
return vs;
}
LinkedShader *ShaderManagerGLES::ApplyFragmentShader(VShaderID VSID, Shader *vs, const ComputedPipelineState &pipelineState, bool useHwTransform, ClipInfoFlags clipInfoFlags) {
LinkedShader *ShaderManagerGLES::ApplyFragmentShader(VShaderID VSID, Shader *vs, const ComputedPipelineState &pipelineState, ClipInfoFlags clipInfoFlags) {
uint64_t dirty = gstate_c.GetDirtyUniforms();
if (dirty) {
if (lastShader_)
@@ -820,7 +820,7 @@ LinkedShader *ShaderManagerGLES::ApplyFragmentShader(VShaderID VSID, Shader *vs,
FShaderID FSID;
if (gstate_c.IsDirty(DIRTY_FRAGMENTSHADER_STATE)) {
gstate_c.Clean(DIRTY_FRAGMENTSHADER_STATE);
ComputeFragmentShaderID(&FSID, pipelineState, draw_->GetBugs(), useHwTransform, clipInfoFlags);
ComputeFragmentShaderID(&FSID, pipelineState, draw_->GetBugs(), clipInfoFlags);
} else {
FSID = lastFSID_;
}
+1 -1
View File
@@ -173,7 +173,7 @@ public:
// This is the old ApplyShader split into two parts, because of annoying information dependencies.
// If you call ApplyVertexShader, you MUST call ApplyFragmentShader soon afterwards.
Shader *ApplyVertexShader(bool useHWTransform, bool useHWTessellation, u32 vertexType, bool weightsAsFloat, bool useSkinInDecode, ClipInfoFlags clipInfoFlags, VShaderID *VSID);
LinkedShader *ApplyFragmentShader(VShaderID VSID, Shader *vs, const ComputedPipelineState &pipelineState, bool useHWTransform, ClipInfoFlags clipInfoFlags);
LinkedShader *ApplyFragmentShader(VShaderID VSID, Shader *vs, const ComputedPipelineState &pipelineState, ClipInfoFlags clipInfoFlags);
void DeviceLost() override;
void DeviceRestore(Draw::DrawContext *draw) override;
+4 -2
View File
@@ -241,8 +241,10 @@ void DrawEngineVulkan::Flush() {
provokingVertexOk = true;
}
bool useHWTransform = CanUseHardwareTransform(prim) && provokingVertexOk;
if (clipInfoFlags_ & ClipInfoFlags::SoftClipCull) {
useHWTransform = false;
if (clipInfoFlags_ & ClipInfoFlags::Valid) {
if (clipInfoFlags_ & ClipInfoFlags::SoftClipCull) {
useHWTransform = false;
}
}
// Is this still needed?
+1 -1
View File
@@ -284,7 +284,7 @@ void ShaderManagerVulkan::GetShaders(int prim, u32 vertexType, VulkanVertexShade
VulkanFragmentShader *fs = nullptr;
if (gstate_c.IsDirty(DIRTY_FRAGMENTSHADER_STATE)) {
gstate_c.Clean(DIRTY_FRAGMENTSHADER_STATE);
ComputeFragmentShaderID(&FSID, pipelineState, draw_->GetBugs(), useHWTransform, clipInfoFlags);
ComputeFragmentShaderID(&FSID, pipelineState, draw_->GetBugs(), clipInfoFlags);
recomputedFS = true;
if (FSID == lastFSID_) {
_dbg_assert_(lastFShader_ != nullptr);
+3
View File
@@ -135,6 +135,9 @@ NPJH50625 = true
ULJM08069 = true
NPJH50625 = true
# Kidou Keisatsu
ULJS00026 = true
[DepthRangeHack]
# Phantasy Star Portable 2 and Infinity as well as some FromSoftware titles both use viewport depth outside [0, 1].
# This gets clamped in our current implementation, but attempts to fix it run into