From dabf723b3e87d626e83eada61860e5f8a2d4abbd Mon Sep 17 00:00:00 2001 From: kuba Date: Fri, 17 Jul 2026 01:29:53 +0200 Subject: [PATCH] [Vulkan] Honor guest depth clear state (#290) --- src/SharpEmu.Libs/Agc/AgcExports.cs | 24 ++--- src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs | 5 +- .../VideoOut/VulkanVideoPresenter.cs | 51 +++++++++-- .../VideoOut/VulkanDepthAttachmentTests.cs | 89 +++++++++++++++++++ 4 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/VideoOut/VulkanDepthAttachmentTests.cs diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 9a8f1b37..1a54e9f7 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -121,6 +121,7 @@ public static partial class AgcExports private const uint CbBlend0Control = 0x1E0; private const uint PaScModeCntl0 = 0x292; // GFX10 DB context registers (register byte address minus 0x28000, / 4). + private const uint DbRenderControl = 0x000; private const uint DbDepthView = 0x002; private const uint DbDepthSizeXy = 0x007; private const uint DbDepthClear = 0x00B; @@ -5291,7 +5292,7 @@ public static partial class AgcExports var hasDepthOnlyCandidate = hasExportShader && !hasPixelShader && depthTarget is not null && - (depthState.TestEnable || depthState.WriteEnable); + (depthState.TestEnable || depthState.WriteEnable || depthState.ClearEnable); if (hasDepthOnlyCandidate && TryCreateTranslatedDepthOnlyGuestDraw( ctx, @@ -6652,27 +6653,30 @@ public static partial class AgcExports // DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE // bit2, ZFUNC bits[6:4] (GCN compare, matches Vulkan CompareOp ordering). + // DB_RENDER_CONTROL (context register 0x000): DEPTH_CLEAR_ENABLE bit0. private const uint DbDepthControl = 0x200; - private static GuestDepthState DecodeDepthState( + internal static GuestDepthState DecodeDepthState( IReadOnlyDictionary registers) { - if (!registers.TryGetValue(DbDepthControl, out var control)) - { - return GuestDepthState.Default; - } - + var hasDepthControl = registers.TryGetValue(DbDepthControl, out var control); + registers.TryGetValue(DbRenderControl, out var renderControl); var testEnable = (control & 0x2u) != 0; var writeEnable = (control & 0x4u) != 0; - var compareOp = (control >> 4) & 0x7u; - return new GuestDepthState(testEnable, writeEnable, compareOp); + var compareOp = hasDepthControl + ? (control >> 4) & 0x7u + : GuestDepthState.Default.CompareOp; + var clearEnable = (renderControl & 0x1u) != 0; + return new GuestDepthState(testEnable, writeEnable, compareOp, clearEnable); } private static GuestDepthTarget? DecodeDepthTarget( IReadOnlyDictionary registers) { var depthState = DecodeDepthState(registers); - if (!depthState.TestEnable && !depthState.WriteEnable) + if (!depthState.TestEnable && + !depthState.WriteEnable && + !depthState.ClearEnable) { return null; } diff --git a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs index 99f90dc7..9c143a30 100644 --- a/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs +++ b/src/SharpEmu.Libs/Gpu/GuestGpuTypes.cs @@ -91,9 +91,10 @@ internal readonly record struct GuestRasterState( internal readonly record struct GuestDepthState( bool TestEnable, bool WriteEnable, - uint CompareOp) + uint CompareOp, + bool ClearEnable = false) { - public static GuestDepthState Default { get; } = new(false, false, 7); + public static GuestDepthState Default { get; } = new(false, false, 7, false); } /// Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 56cfe854..fffc9b5d 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -2213,6 +2213,12 @@ internal static unsafe class VulkanVideoPresenter return keys; } + internal static bool ShouldAttachGuestDepth( + GuestDepthTarget? target, + GuestDepthState state) => + target is not null && + (state.TestEnable || state.WriteEnable || state.ClearEnable); + private readonly record struct Presentation( byte[]? Pixels, uint Width, @@ -9440,6 +9446,7 @@ internal static unsafe class VulkanVideoPresenter { var extent = new Extent2D(firstTarget.Width, firstTarget.Height); var draw = work.Draw; + var clearDepthForDraw = draw.RenderState.Depth.ClearEnable; if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable) { draw = draw with @@ -9452,9 +9459,10 @@ internal static unsafe class VulkanVideoPresenter } GuestDepthResource? depth = null; DepthFramebufferResource? depthFramebuffer = null; - if (work.DepthTarget is { } depthTarget && - (draw.RenderState.Depth.TestEnable || - draw.RenderState.Depth.WriteEnable)) + if (ShouldAttachGuestDepth( + work.DepthTarget, + draw.RenderState.Depth) && + work.DepthTarget is { } depthTarget) { var effectiveDepthTarget = depthTarget; if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height) @@ -9501,21 +9509,45 @@ internal static unsafe class VulkanVideoPresenter depth = GetOrCreateGuestDepth(effectiveDepthTarget); PrepareFirstUseDepth(depth, draw.RenderState.Depth); + if (clearDepthForDraw) + { + depth.GuestClearDepth = effectiveDepthTarget.ClearDepth; + depth.ClearDepth = effectiveDepthTarget.ClearDepth; + } if (targets.Length == 1) { depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth); } } + if (clearDepthForDraw) + { + // DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB + // clear operation. The draw still produces color, but its + // interpolated vertex Z is not the guest clear value. + draw = draw with + { + RenderState = draw.RenderState with + { + Depth = draw.RenderState.Depth with + { + TestEnable = false, + WriteEnable = false, + ClearEnable = false, + }, + }, + }; + } + var renderPass = depthFramebuffer is null ? firstTarget.Initialized ? firstTarget.RenderPass : firstTarget.InitialRenderPass : firstTarget.Initialized - ? depth!.Initialized + ? depth!.Initialized && !clearDepthForDraw ? depthFramebuffer.LoadRenderPass : depthFramebuffer.DepthClearRenderPass - : depth!.Initialized + : depth!.Initialized && !clearDepthForDraw ? depthFramebuffer.ColorClearRenderPass : depthFramebuffer.BothClearRenderPass; var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer; @@ -9531,7 +9563,7 @@ internal static unsafe class VulkanVideoPresenter targets.Select(target => target.Initialized || target.InitialUploadPending).ToArray(), depth, - depth?.Initialized == true); + depth?.Initialized == true && !clearDepthForDraw); transientRenderPass = renderPass; transientFramebuffer = framebuffer; } @@ -9719,7 +9751,12 @@ internal static unsafe class VulkanVideoPresenter if (depth is not null) { depth.Initialized = true; - if (draw.RenderState.Depth.WriteEnable) + depth.Layout = ImageLayout.DepthStencilAttachmentOptimal; + if (clearDepthForDraw) + { + depth.InitializationSource = "guest-depth-clear"; + } + else if (draw.RenderState.Depth.WriteEnable) { depth.InitializationSource = "translated-depth-write"; } diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VulkanDepthAttachmentTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VulkanDepthAttachmentTests.cs new file mode 100644 index 00000000..cfe9e9bd --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VulkanDepthAttachmentTests.cs @@ -0,0 +1,89 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.Agc; +using SharpEmu.Libs.Gpu; +using SharpEmu.Libs.VideoOut; +using Xunit; + +namespace SharpEmu.Libs.Tests.VideoOut; + +public sealed class VulkanDepthAttachmentTests +{ + private static readonly GuestDepthTarget Target = new( + ReadAddress: 0x1000, + WriteAddress: 0x1000, + Width: 1920, + Height: 1080, + GuestFormat: 1, + SwizzleMode: 0, + ClearDepth: 1f, + ReadOnly: false); + + [Fact] + public void GuestDepthTarget_AttachesForDepthWork() + { + var state = new GuestDepthState( + TestEnable: true, + WriteEnable: true, + CompareOp: 3); + + Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state)); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void GuestDepthTarget_AttachesForEitherDepthOperation( + bool testEnable, + bool writeEnable) + { + var state = new GuestDepthState(testEnable, writeEnable, CompareOp: 3); + + Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state)); + } + + [Fact] + public void GuestDepthTarget_RequiresTargetAndDepthWork() + { + var state = GuestDepthState.Default; + + Assert.False(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state)); + Assert.False(VulkanVideoPresenter.ShouldAttachGuestDepth( + target: null, + new GuestDepthState(true, false, CompareOp: 3))); + } + + [Fact] + public void GuestDepthTarget_AttachesForDepthClear() + { + var state = new GuestDepthState( + TestEnable: false, + WriteEnable: false, + CompareOp: 7, + ClearEnable: true); + + Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state)); + } + + [Theory] + [InlineData(0x41u, true)] + [InlineData(0x40u, false)] + public void DepthState_DecodesRenderControlClearBit( + uint renderControl, + bool clearEnable) + { + var registers = new Dictionary + { + [0x000] = renderControl, + [0x200] = 0x776, + }; + + var state = AgcExports.DecodeDepthState(registers); + + Assert.True(state.TestEnable); + Assert.True(state.WriteEnable); + Assert.Equal(7u, state.CompareOp); + Assert.Equal(clearEnable, state.ClearEnable); + } +}