Skip to content

Emulate reversed-depth viewports on affected AMD Mac GPUs#2760

Draft
vkedwardli wants to merge 1 commit into
KhronosGroup:mainfrom
vkedwardli:fix/Mac2-AMD
Draft

Emulate reversed-depth viewports on affected AMD Mac GPUs#2760
vkedwardli wants to merge 1 commit into
KhronosGroup:mainfrom
vkedwardli:fix/Mac2-AMD

Conversation

@vkedwardli

@vkedwardli vkedwardli commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

This PR works around a Metal depth viewport issue seen on AMD Mac2 GPUs, such as Radeon Pro 560

output

flyinghead/flycast#1172
#955

A working PoC for using non reversed-Z viewport
flyinghead/flycast#2298

On these GPUs, passing a reversed-depth Metal viewport (znear > zfar) can cause depth testing/writes to behave incorrectly. Instead of passing the reversed depth range directly to Metal, MoltenVK now detects the affected hardware path and emulates reversed-depth viewports dynamically.

I acknowledge this appears to be a Metal 2 driver/API issue rather than MoltenVK’s responsibility. I am opening this PR to seek maintainer insight on whether this kind of workaround belongs in MoltenVK, and I tried to keep the implementation as narrowly scoped and least invasive as possible.

  • Detect affected AMD Mac2 non-Metal3 GPUs.
  • Normalize reversed Metal viewports to znear = 0, zfar = 1.
  • Add a hidden per-draw implicit flag for reversed-depth viewport emulation.
  • Patch generated vertex/tessellation-evaluation MSL to invert clip-space Z when that flag is enabled.
  • Include the new conversion options in shader cache matching/serialization.
  • Disable discarded fragment store checking on the affected path to avoid helper-thread behavior that breaks OIT rendering.

The current SPIR-V/MSL conversion changes are kept local to this PR for discussion. Once the direction is clear, those changes can and probably should be moved into SPIRV-Cross, where the transformation can be implemented with shader semantic context instead of MoltenVK-side MSL text patching.

This is intentionally scoped to the affected hardware path and does not add a public config option.

Known limitation:
Mixed multi-viewport draws where some viewports use reversed depth and others do not, are not fully emulated by this MoltenVK-side patch. Full per-viewport emulation would be better implemented in SPIRV-Cross, where viewport-index output semantics are available during MSL generation.

@vkedwardli

Copy link
Copy Markdown
Contributor Author

Here's a repro used to isolate the issue outside Flycast reversed-depth-repro.zip

The test renders overlapping quads using reversed-depth viewport state:

  • Vulkan viewport: minDepth = 1, maxDepth = 0
  • Depth clear: 0
  • Depth compare: GREATER
  • Expected result: the later/nearer green quad should pass depth and appear at the sampled pixel

On Radeon Pro 560 through MoltenVK/Metal 2, the failing build shows that reversed Metal viewport depth (MTLViewport znear > zfar) does not behave correctly. Several reversed-depth cases leave the sampled pixel black and the depth value unchanged, while equivalent non-reversed/emulated paths pass.

Original failing result on Radeon Pro 560
[mvk-info] Created VkInstance for Vulkan version 1.1.350, as requested by app, with the following 0 Vulkan extensions enabled:
device: AMD Radeon Pro 560 vendorID=0x1002 deviceID=0x67ef
selected: AMD Radeon Pro 560
[mvk-info] Vulkan semaphores using MTLEvent.
[mvk-info] Descriptor sets binding resources using Metal argument buffers.
[mvk-info] Created VkDevice to run on GPU AMD Radeon Pro 560 with the following 1 Vulkan extensions enabled:
	VK_KHR_portability_subset v1
D16_UNORM depth attachment support: 1
D24_UNORM_S8_UINT depth attachment support: 1
D32_SFLOAT depth attachment support: 1
D32_SFLOAT_S8_UINT depth attachment support: 1
render state: color=R8G8B8A8_UNORM depth=D32_SFLOAT_S8_UINT default clearDepth=0 compare=GREATER viewportDepth=1->0
diagnostic-reversed-no-depth-test: reversed viewport with depth test disabled; second draw should simply overwrite first
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.00000000 raw=00 00 00 00
diagnostic-reversed-depth-always: reversed viewport with depth compare Always and depth writes enabled
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.00000000 raw=00 00 00 00
diagnostic-normal-less-baseline: normal viewport minDepth=0 maxDepth=1 with clearDepth=1 and compare Less
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=1.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.25000000 raw=00 00 80 3e
diagnostic-reversed-less: reversed viewport with clearDepth=1 and compare Less; correct reversed-depth result is red
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=1.00 center RGBA: 255 0 0 255 expected 255 0 0 255 -> PASS
  center depth: 0.00000000 raw=00 00 00 00
diagnostic-reversed-greater-no-write: reversed viewport with compare Greater but depth writes disabled; both draws compare against clearDepth=0
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.00000000 raw=00 00 00 00
diagnostic-reversed-greater-equal-clear-zero: reversed viewport with compare GreaterEqual and clearDepth=0
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.00000000 raw=00 00 00 00
diagnostic-reversed-greater-equal-clear-half: reversed viewport with compare GreaterEqual and clearDepth=0.5
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.50000000 raw=00 00 00 3f
diagnostic-reversed-near-epsilon-greater-clear-zero: viewport minDepth=0.99999 maxDepth=0 with compare Greater and clearDepth=0
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.00000000 raw=00 00 00 00
diagnostic-reversed-near-epsilon-greater-clear-half: viewport minDepth=0.99999 maxDepth=0 with compare Greater and clearDepth=0.5
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.50000000 raw=00 00 00 3f
diagnostic-reversed-near-epsilon-greater-equal-clear-zero: viewport minDepth=0.99999 maxDepth=0 with compare GreaterEqual and clearDepth=0
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.00000000 raw=00 00 00 00
diagnostic-reversed-near-epsilon-greater-equal-clear-half: viewport minDepth=0.99999 maxDepth=0 with compare GreaterEqual and clearDepth=0.5
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.50000000 raw=00 00 00 3f
diagnostic-reversed-greater-clear-epsilon: reversed viewport with compare Greater and clearDepth just above zero
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.00000100 raw=bd 37 86 35
diagnostic-reversed-greater-clear-half: reversed viewport with compare Greater and clearDepth=0.5; red should fail and green should pass
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.50000000 raw=00 00 00 3f
fixed-function-reversed-viewport: viewport minDepth=1 maxDepth=0 maps red z=0.75 to depth 0.25 and green z=0.25 to depth 0.75
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.00000000 raw=00 00 00 00
frag-depth-from-fragcoord: fragment shader writes gl_FragDepth=gl_FragCoord.z under the same reversed viewport
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.00000000 raw=00 00 00 00
frag-depth-constant: fragment shader writes explicit reversed depth values 0.25 then 0.75
  first draw red:   vertexZ=0.50 fragDepth=0.25
  second draw green: vertexZ=0.50 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 0 0 255 expected 0 255 0 255 -> FAIL
  center depth: 0.00000000 raw=00 00 00 00
emulated-fixed-function-clear-zero: normal viewport with vertex shader z = 1 - z and compare Greater
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
emulated-fixed-function-clear-half: normal viewport with vertex shader z = 1 - z, compare Greater, and clearDepth=0.5
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
emulated-frag-depth-from-fragcoord: normal viewport with vertex z inversion and gl_FragDepth=gl_FragCoord.z
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
emulated-frag-depth-constant: normal viewport with vertex z inversion and explicit fragment depth constants
  first draw red:   vertexZ=0.50 fragDepth=0.25
  second draw green: vertexZ=0.50 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
znear sweep: viewport minDepth -> maxDepth=0, compare Greater, clearDepth=0
  znear=1.000000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.999999 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.999990 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.999900 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.999000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.990000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.950000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.900000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.750000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.500000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.250000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.100000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.010000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.001000 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.000100 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.000010 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
  znear=0.000001 rgba=0 0 0 255 depth=0.00000000 raw=00 00 00 00 -> FAIL
[mvk-info] Destroyed VkDevice on GPU AMD Radeon Pro 560 with 1 Vulkan extensions enabled.
[mvk-info] Destroyed VkPhysicalDevice for GPU AMD Radeon Pro 560 with 8 MB of GPU memory still allocated.
[mvk-info] Destroyed VkPhysicalDevice for GPU Intel(R) HD Graphics 630 with 4 MB of GPU memory still allocated.
[mvk-info] Destroying VkInstance for Vulkan version 1.1.350 with 0 Vulkan extensions enabled.
RESULT: FAIL

After this PR, the same repro passes the reversed-depth cases by normalizing the Metal viewport depth range and emulating the reversed depth transform in the generated vertex/tessellation-evaluation MSL.

Passing result with this PR on Radeon Pro 560
[mvk-info] Created VkInstance for Vulkan version 1.1.350, as requested by app, with the following 0 Vulkan extensions enabled:
device: AMD Radeon Pro 560 vendorID=0x1002 deviceID=0x67ef
selected: AMD Radeon Pro 560
[mvk-info] Vulkan semaphores using MTLEvent.
[mvk-info] Descriptor sets binding resources using Metal argument buffers.
[mvk-info] Created VkDevice to run on GPU AMD Radeon Pro 560 with the following 1 Vulkan extensions enabled:
	VK_KHR_portability_subset v1
D16_UNORM depth attachment support: 1
D24_UNORM_S8_UINT depth attachment support: 1
D32_SFLOAT depth attachment support: 1
D32_SFLOAT_S8_UINT depth attachment support: 1
render state: color=R8G8B8A8_UNORM depth=D32_SFLOAT_S8_UINT default clearDepth=0 compare=GREATER viewportDepth=1->0
diagnostic-reversed-no-depth-test: reversed viewport with depth test disabled; second draw should simply overwrite first
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.00000000 raw=00 00 00 00
diagnostic-reversed-depth-always: reversed viewport with depth compare Always and depth writes enabled
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
diagnostic-normal-less-baseline: normal viewport minDepth=0 maxDepth=1 with clearDepth=1 and compare Less
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=1.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.25000000 raw=00 00 80 3e
diagnostic-reversed-less: reversed viewport with clearDepth=1 and compare Less; correct reversed-depth result is red
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=1.00 center RGBA: 255 0 0 255 expected 255 0 0 255 -> PASS
  center depth: 0.25000000 raw=00 00 80 3e
diagnostic-reversed-greater-no-write: reversed viewport with compare Greater but depth writes disabled; both draws compare against clearDepth=0
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.00000000 raw=00 00 00 00
diagnostic-reversed-greater-equal-clear-zero: reversed viewport with compare GreaterEqual and clearDepth=0
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
diagnostic-reversed-greater-equal-clear-half: reversed viewport with compare GreaterEqual and clearDepth=0.5
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
diagnostic-reversed-near-epsilon-greater-clear-zero: viewport minDepth=0.99999 maxDepth=0 with compare Greater and clearDepth=0
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
diagnostic-reversed-near-epsilon-greater-clear-half: viewport minDepth=0.99999 maxDepth=0 with compare Greater and clearDepth=0.5
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
diagnostic-reversed-near-epsilon-greater-equal-clear-zero: viewport minDepth=0.99999 maxDepth=0 with compare GreaterEqual and clearDepth=0
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
diagnostic-reversed-near-epsilon-greater-equal-clear-half: viewport minDepth=0.99999 maxDepth=0 with compare GreaterEqual and clearDepth=0.5
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
diagnostic-reversed-greater-clear-epsilon: reversed viewport with compare Greater and clearDepth just above zero
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
diagnostic-reversed-greater-clear-half: reversed viewport with compare Greater and clearDepth=0.5; red should fail and green should pass
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
fixed-function-reversed-viewport: viewport minDepth=1 maxDepth=0 maps red z=0.75 to depth 0.25 and green z=0.25 to depth 0.75
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
frag-depth-from-fragcoord: fragment shader writes gl_FragDepth=gl_FragCoord.z under the same reversed viewport
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
frag-depth-constant: fragment shader writes explicit reversed depth values 0.25 then 0.75
  first draw red:   vertexZ=0.50 fragDepth=0.25
  second draw green: vertexZ=0.50 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
emulated-fixed-function-clear-zero: normal viewport with vertex shader z = 1 - z and compare Greater
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
emulated-fixed-function-clear-half: normal viewport with vertex shader z = 1 - z, compare Greater, and clearDepth=0.5
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.50 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
emulated-frag-depth-from-fragcoord: normal viewport with vertex z inversion and gl_FragDepth=gl_FragCoord.z
  first draw red:   vertexZ=0.75 fragDepth=0.25
  second draw green: vertexZ=0.25 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
emulated-frag-depth-constant: normal viewport with vertex z inversion and explicit fragment depth constants
  first draw red:   vertexZ=0.50 fragDepth=0.25
  second draw green: vertexZ=0.50 fragDepth=0.75
  clearDepth=0.00 center RGBA: 0 255 0 255 expected 0 255 0 255 -> PASS
  center depth: 0.75000000 raw=00 00 40 3f
znear sweep: viewport minDepth -> maxDepth=0, compare Greater, clearDepth=0
  znear=1.000000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.999999 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.999990 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.999900 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.999000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.990000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.950000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.900000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.750000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.500000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.250000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.100000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.010000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.001000 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.000100 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.000010 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
  znear=0.000001 rgba=0 255 0 255 depth=0.75000000 raw=00 00 40 3f -> PASS
[mvk-info] Destroyed VkDevice on GPU AMD Radeon Pro 560 with 1 Vulkan extensions enabled.
[mvk-info] Destroyed VkPhysicalDevice for GPU AMD Radeon Pro 560 with 8 MB of GPU memory still allocated.
[mvk-info] Destroyed VkPhysicalDevice for GPU Intel(R) HD Graphics 630 with 4 MB of GPU memory still allocated.
[mvk-info] Destroying VkInstance for Vulkan version 1.1.350 with 0 Vulkan extensions enabled.
RESULT: PASS

return !positionMemberName.empty();
}

static bool mvkPatchMSLEmulatedReversedDepthViewport(std::string& msl, uint32_t emulatedReversedDepthViewportBufferIndex) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally these kinds of things are implemented into SPIRV-Cross, which we then set the configuration for, rather than patching MSL manually, as it seems this could be rather dicey.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I do not think the MSL text patching is the ideal final shape either, I kept it local in this PR mainly to validate the workaround direction. If this Metal2 bug should not be handled by MoltenVK, then we could save time on creating + reviewing cross repo PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's indeed broken and there's no simpler workaround, shader handling would make sense I suppose. As far as I know this is a valid thing to do which should work.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, I will move the patch to SPIRV-Cross later.

The current minimal workaround is:

  • pass Metal a normal/forward viewport depth range by swapping znear/zfar
  • invert clip-space Z in vertex/tessellation-evaluation shaders
  • keep the rest of the reversed-Z state unchanged:
    • Vulkan viewport semantics from the app’s point of view
    • depth compare op, e.g. GREATER remains GREATER
    • depth clear value, e.g. reversed-Z clear 0 remains 0
    • depth test enable/write enable
    • depth attachment format
    • render pass/load/store behavior
    • depth attachment contents/final depth values, logically matching the original reversed viewport
    • fragment depth behavior, including gl_FragCoord.z / gl_FragDepth
    • renderer draw ordering and OIT logic

Comment on lines +1146 to +1147
mtlViewports[i].znear = shouldEmulateReversedDepthViewports && isReversedDepthViewport ? 0.0 : viewports[i].minDepth;
mtlViewports[i].zfar = shouldEmulateReversedDepthViewports && isReversedDepthViewport ? 1.0 : viewports[i].maxDepth;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be viewports[i].maxDepth : viewports[i].minDepth and viewports[i].minDepth : viewports[i].maxDepth in case you are wondering. but keeping 0.0 / 1.0 seems easier to read for a draft

Avoid passing reversed depth ranges directly to Metal on AMD Mac2 GPUs that do not support Metal 3.
Normalize reversed Metal viewports to 0..1, pass a hidden per-draw flag to vertex/tessellation-evaluation shaders, and patch generated MSL to invert clip-space Z dynamically.

Disable discarded fragment store checking on the affected path to avoid the helper-thread behavior that breaks OIT rendering.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants