From c44296a90c8d5d287ac0ce0ef54269b3749ce86b Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Wed, 3 Jun 2026 12:55:24 +0200 Subject: [PATCH] Cover PSO-only RT surface: recursion, skip-CH flag, callable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three tests stacked on #1275 covering features that inline RT (RayQuery in compute) physically can't express — they're only reachable through a DispatchRays-driven RT pipeline. - `trace-ray-recursion.test` — closest-hit fires a secondary TraceRay from an above-the-triangle origin. First-level CH sees payload=0 → bumps to 0x1 → calls TraceRay. Second-level CH sees payload!=0 → writes 0x10. Unwinds: first-level OR's in 0x100. Final payload 0x110 (272 decimal). `RayTracingPipelineConfig.MaxTraceRecursion- Depth: 2` so both TraceRay calls are within budget. - `ray-flag-skip-closest-hit.test` — two lanes fire identical rays at the same triangle. Lane 0 uses RAY_FLAG_NONE so CH runs and writes 0xBEEF. Lane 1 uses RAY_FLAG_SKIP_CLOSEST_HIT_SHADER (PSO-only — inline RT has no equivalent) so CH is skipped and payload keeps its initial 0xAAAA. Output [0xBEEF, 0xAAAA]. - `callable-shader.test` — two callable shaders writing distinct sentinels (0xAAAA / 0xBBBB). Each lane calls `CallShader(Idx, ...)` so the SBT callable region's per-record routing is exercised independently of the hit-group / miss routing already covered in #1277. Callable shaders themselves don't exist in inline RT. Only `# XFAIL: Clang` — the clang HLSL path doesn't implement the RT pipeline yet (#1268). On Vulkan the current DXC `-spirv` output is valid: each callable's IncomingCallableDataKHR is listed only in its own entry point's interface and `spirv-val` is clean, so the module loads and the test passes (confirmed XPASS on the Intel Vulkan CI runner). An earlier DXC over-listed those interface variables in every callable entry point — that's since been fixed upstream, so no Vulkan XFAIL or `spirv-opt` cleanup is needed anymore. All three pass on Metal once the bring-up PR ahead of this commit sets the raygen pipeline's `setMaxCallStackDepth(MaxTraceRecursionDepth)` so nested TraceRay actually unwinds (with the default of 1, the second TraceRay was silently dropped and the recursion test produced 0x1 instead of 0x110). Locally verified on the user's Linux box: - Vulkan via the native offloader: recursion + skip-CH PASS. Callable passes on Vulkan too — current DXC emits valid SPIR-V (confirmed via XPASS on the Intel Vulkan CI runner). - D3D12 via Wine + vkd3d-proton + cross-compiled offloader.exe: all three PASS. And on macOS 15 / metal-irconverter 3.1.1 via the native offloader: - All three PASS (recursion + skip-CH + callable). Co-Authored-By: Claude Opus 4.8 (1M context) --- test/Feature/RT/callable-shader.test | 87 ++++++++++++ .../Feature/RT/ray-flag-skip-closest-hit.test | 127 +++++++++++++++++ test/Feature/RT/trace-ray-recursion.test | 134 ++++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 test/Feature/RT/callable-shader.test create mode 100644 test/Feature/RT/ray-flag-skip-closest-hit.test create mode 100644 test/Feature/RT/trace-ray-recursion.test diff --git a/test/Feature/RT/callable-shader.test b/test/Feature/RT/callable-shader.test new file mode 100644 index 000000000..91b289576 --- /dev/null +++ b/test/Feature/RT/callable-shader.test @@ -0,0 +1,87 @@ +#--- source.hlsl + +// Callable shaders receive an inout user-defined parameter — Vulkan / DXR +// don't have a Built-in convention beyond "size matches what the caller +// declared". One uint of payload is enough to confirm routing. +struct CallableParams { + uint Value; +}; + +[[vk::binding(0, 0)]] RWStructuredBuffer Output : register(u0); + +[shader("raygeneration")] +void RayGen() { + // Each lane calls a different callable shader index. CallShader picks + // the callable record at the given index in the SBT's Callable region + // — inline RT has no equivalent of this stage. + const uint Idx = DispatchRaysIndex().x; + CallableParams P; + P.Value = 0; + CallShader(Idx, P); + Output[Idx] = P.Value; +} + +[shader("callable")] +void CallableA(inout CallableParams P) { + P.Value = 0xAAAA; +} + +[shader("callable")] +void CallableB(inout CallableParams P) { + P.Value = 0xBBBB; +} +//--- pipeline.yaml +--- +Shaders: + - Stage: RayGeneration + Entry: RayGen + - Stage: Callable + Entry: CallableA + - Stage: Callable + Entry: CallableB +Buffers: + - Name: Output + Format: UInt32 + Stride: 4 + FillSize: 8 + - Name: Expected + Format: UInt32 + Stride: 4 + Data: [ 0xAAAA, 0xBBBB ] +RayTracingPipelineConfig: + # No TraceRay calls — depth 1 is sufficient (CallShader doesn't count + # against ray recursion). + MaxTraceRecursionDepth: 1 + MaxPayloadSizeInBytes: 4 +ShaderBindingTable: + RayGen: + ShaderName: RayGen + Callable: + - ShaderName: CallableA + - ShaderName: CallableB +DescriptorSets: + - Resources: + - Name: Output + Kind: RWStructuredBuffer + DirectXBinding: + Register: 0 + Space: 0 + VulkanBinding: + Binding: 0 +DispatchParameters: + DispatchGroupCount: [ 2, 1, 1 ] +Results: + - Result: CallableRouting + Rule: BufferExact + Actual: Output + Expected: Expected +... +#--- end + +# REQUIRES: raytracing-pipeline +# Unimplemented https://github.com/llvm/offload-test-suite/issues/1268 +# XFAIL: Clang + +# RUN: split-file %s %t +# RUN: %dxc_target_lib -T lib_6_5 -Fo %t.o %t/source.hlsl +# RUN: %offloader %t/pipeline.yaml %t.o diff --git a/test/Feature/RT/ray-flag-skip-closest-hit.test b/test/Feature/RT/ray-flag-skip-closest-hit.test new file mode 100644 index 000000000..db7ec4683 --- /dev/null +++ b/test/Feature/RT/ray-flag-skip-closest-hit.test @@ -0,0 +1,127 @@ +#--- source.hlsl + +struct Payload { + uint Value; +}; + +[[vk::binding(0, 0)]] RaytracingAccelerationStructure Scene : register(t0); +[[vk::binding(1, 0)]] RWStructuredBuffer Output : register(u0); + +[shader("raygeneration")] +void RayGen() { + // Both lanes fire identical rays that hit the same triangle. Lane 0 + // uses RAY_FLAG_NONE so the closest-hit shader runs and writes + // 0xBEEF into the payload. Lane 1 sets + // RAY_FLAG_SKIP_CLOSEST_HIT_SHADER (a PSO-only flag — inline RT can't + // express this) so the traversal records a hit but the closest-hit + // shader is never invoked. With initial payload = 0xAAAA the output + // is the sentinel for lane 1 and 0xBEEF for lane 0. + const uint Idx = DispatchRaysIndex().x; + const uint Flags = + Idx == 0 ? RAY_FLAG_NONE : RAY_FLAG_SKIP_CLOSEST_HIT_SHADER; + + Payload P; + P.Value = 0xAAAA; + RayDesc Ray; + Ray.Origin = float3(0, 0, 1); + Ray.Direction = float3(0, 0, -1); + Ray.TMin = 0.0; + Ray.TMax = 100.0; + TraceRay(Scene, Flags, 0xFF, 0, 1, 0, Ray, P); + Output[Idx] = P.Value; +} + +[shader("miss")] +void MissMain(inout Payload P) { + // Sentinel — every ray hits the triangle, so this never runs. + P.Value = 0xDEAD; +} + +[shader("closesthit")] +void ClosestHitMain(inout Payload P, + in BuiltInTriangleIntersectionAttributes Attr) { + P.Value = 0xBEEF; +} +//--- pipeline.yaml +--- +Shaders: + - Stage: RayGeneration + Entry: RayGen + - Stage: Miss + Entry: MissMain + - Stage: ClosestHit + Entry: ClosestHitMain +Buffers: + - Name: Vertices + Format: Float32 + Stride: 12 + Data: [ 0.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0 ] + - Name: Output + Format: UInt32 + Stride: 4 + FillSize: 8 + - Name: Expected + Format: UInt32 + Stride: 4 + # Lane 0: closest-hit runs, writes 0xBEEF. + # Lane 1: closest-hit skipped, payload stays at the initial 0xAAAA. + Data: [ 0xBEEF, 0xAAAA ] +AccelerationStructures: + BLAS: + - Name: TriangleBLAS + Triangles: + - VertexBuffer: Vertices + VertexFormat: RGB32Float + VertexStride: 12 + VertexCount: 3 + TLAS: + - Name: Scene + Instances: + - BLAS: TriangleBLAS +RayTracingPipelineConfig: + MaxTraceRecursionDepth: 1 + MaxPayloadSizeInBytes: 4 +HitGroups: + - Name: TriangleHitGroup + Type: Triangles + ClosestHit: ClosestHitMain +ShaderBindingTable: + RayGen: + ShaderName: RayGen + Miss: + - ShaderName: MissMain + HitGroup: + - ShaderName: TriangleHitGroup +DescriptorSets: + - Resources: + - Name: Scene + Kind: AccelerationStructure + DirectXBinding: + Register: 0 + Space: 0 + VulkanBinding: + Binding: 0 + - Name: Output + Kind: RWStructuredBuffer + DirectXBinding: + Register: 0 + Space: 0 + VulkanBinding: + Binding: 1 +DispatchParameters: + DispatchGroupCount: [ 2, 1, 1 ] +Results: + - Result: RayFlagSkipClosestHit + Rule: BufferExact + Actual: Output + Expected: Expected +... +#--- end + +# REQUIRES: raytracing-pipeline +# Unimplemented https://github.com/llvm/offload-test-suite/issues/1268 +# XFAIL: Clang + +# RUN: split-file %s %t +# RUN: %dxc_target_lib -T lib_6_5 -Fo %t.o %t/source.hlsl +# RUN: %offloader %t/pipeline.yaml %t.o diff --git a/test/Feature/RT/trace-ray-recursion.test b/test/Feature/RT/trace-ray-recursion.test new file mode 100644 index 000000000..98d509ba3 --- /dev/null +++ b/test/Feature/RT/trace-ray-recursion.test @@ -0,0 +1,134 @@ +#--- source.hlsl + +struct Payload { + uint Value; +}; + +[[vk::binding(0, 0)]] RaytracingAccelerationStructure Scene : register(t0); +[[vk::binding(1, 0)]] RWStructuredBuffer Output : register(u0); + +[shader("raygeneration")] +void RayGen() { + Payload P; + P.Value = 0; + RayDesc Ray; + Ray.Origin = float3(0, 0, 1); + Ray.Direction = float3(0, 0, -1); + Ray.TMin = 0.0; + Ray.TMax = 100.0; + TraceRay(Scene, RAY_FLAG_NONE, 0xFF, 0, 1, 0, Ray, P); + Output[0] = P.Value; +} + +[shader("miss")] +void MissMain(inout Payload P) { + // Sentinel — never observed (every ray hits the triangle). + P.Value = 0xDEAD; +} + +[shader("closesthit")] +void RecursiveCH(inout Payload P, in BuiltInTriangleIntersectionAttributes Attr) { + // First entry into the hit shader: payload still carries raygen's 0. + // Bump it to a marker, fire a secondary ray from a fresh origin above the + // triangle so we hit the same triangle again, then OR in the high marker + // when the secondary chain unwinds. The secondary invocation sees a + // non-zero payload and goes down the second-level branch. + if (P.Value == 0) { + P.Value = 0x1; + RayDesc Ray; + Ray.Origin = float3(0, 0, 2); + Ray.Direction = float3(0, 0, -1); + Ray.TMin = 0.0; + Ray.TMax = 100.0; + TraceRay(Scene, RAY_FLAG_NONE, 0xFF, 0, 1, 0, Ray, P); + P.Value |= 0x100; + } else { + // Second-level hit: write the low marker and don't recurse further. + P.Value = 0x10; + } +} +//--- pipeline.yaml +--- +Shaders: + - Stage: RayGeneration + Entry: RayGen + - Stage: Miss + Entry: MissMain + - Stage: ClosestHit + Entry: RecursiveCH +Buffers: + - Name: Vertices + Format: Float32 + Stride: 12 + Data: [ 0.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0 ] + - Name: Output + Format: UInt32 + Stride: 4 + FillSize: 4 + - Name: Expected + Format: UInt32 + Stride: 4 + # First-level CH sets Value=0x1 (overwritten by secondary CH to 0x10), then + # OR's in 0x100 after the secondary chain unwinds — final = 0x110. + Data: [ 0x110 ] +AccelerationStructures: + BLAS: + - Name: TriangleBLAS + Triangles: + - VertexBuffer: Vertices + VertexFormat: RGB32Float + VertexStride: 12 + VertexCount: 3 + TLAS: + - Name: Scene + Instances: + - BLAS: TriangleBLAS +RayTracingPipelineConfig: + # Raygen invokes TraceRay (depth 1) and the first-level CH invokes + # TraceRay again (depth 2). Both need to be allowed. + MaxTraceRecursionDepth: 2 + MaxPayloadSizeInBytes: 4 +HitGroups: + - Name: TriangleHitGroup + Type: Triangles + ClosestHit: RecursiveCH +ShaderBindingTable: + RayGen: + ShaderName: RayGen + Miss: + - ShaderName: MissMain + HitGroup: + - ShaderName: TriangleHitGroup +DescriptorSets: + - Resources: + - Name: Scene + Kind: AccelerationStructure + DirectXBinding: + Register: 0 + Space: 0 + VulkanBinding: + Binding: 0 + - Name: Output + Kind: RWStructuredBuffer + DirectXBinding: + Register: 0 + Space: 0 + VulkanBinding: + Binding: 1 +DispatchParameters: + DispatchGroupCount: [ 1, 1, 1 ] +Results: + - Result: TraceRayRecursion + Rule: BufferExact + Actual: Output + Expected: Expected +... +#--- end + +# REQUIRES: raytracing-pipeline +# Unimplemented https://github.com/llvm/offload-test-suite/issues/1268 +# XFAIL: Clang + +# RUN: split-file %s %t +# RUN: %dxc_target_lib -T lib_6_5 -Fo %t.o %t/source.hlsl +# RUN: %offloader %t/pipeline.yaml %t.o