From 5987d8114a7a743eb4f4394ce6b8184d2ef39e7f Mon Sep 17 00:00:00 2001 From: kaizhang Date: Fri, 5 Jun 2026 13:10:19 -0500 Subject: [PATCH 01/21] Add ray tracing API structural dispatch proposal --- proposals/000-ray-tracing-api.md | 754 ++++++++++++++++++ .../figures/ray-tracing-api/api-overview.svg | 49 ++ .../ray-tracing-api/context-reachability.svg | 44 + .../ray-tracing-api/dispatch-model-gap.svg | 140 ++++ .../ray-tracing-api/tag-list-reachability.svg | 40 + 5 files changed, 1027 insertions(+) create mode 100644 proposals/000-ray-tracing-api.md create mode 100644 proposals/figures/ray-tracing-api/api-overview.svg create mode 100644 proposals/figures/ray-tracing-api/context-reachability.svg create mode 100644 proposals/figures/ray-tracing-api/dispatch-model-gap.svg create mode 100644 proposals/figures/ray-tracing-api/tag-list-reachability.svg diff --git a/proposals/000-ray-tracing-api.md b/proposals/000-ray-tracing-api.md new file mode 100644 index 0000000..41c34c1 --- /dev/null +++ b/proposals/000-ray-tracing-api.md @@ -0,0 +1,754 @@ +SP #000: Pipeline Ray Tracing API Structural Dispatch +===================================================== + +This proposal sketches a new Slang ray tracing API that treats Metal as a first-class target +while preserving the D3D and Vulkan pipeline model. The central idea is to make the shader source +declare a conceptual shader binding table, or SBT, as structured Slang types. D3D and Vulkan can +continue to use the native host-created SBT. Metal can use the same structure to synthesize the +post-trace closest-hit and miss dispatch logic that Metal programmers normally write by hand. + +Status +------ + +Status: Draft Proposal. + +Author: + +Reviewer: + +Scope +----- + +Scope: pipeline ray tracing only. Inline ray tracing and ray queries are intentionally out of +scope for this first design. + +## 1. Challenges Extending Current Slang Ray Tracing To Metal + +### 1.1 Dispatch Model Gap + +D3D and Vulkan expose ray tracing as a pipeline-stage model. A ray-generation shader calls +`TraceRay` or `OpTraceRayKHR`, and the driver or hardware uses SBT parameters to select miss, +any-hit, intersection, and closest-hit shaders. + +In simplified D3D/Vulkan-shaped Slang: + +```slang +[shader("raygeneration")] +void rayGen() +{ + RadiancePayload payload; + + TraceRay( + scene, + rayFlags, + instanceMask, + rayContributionToHitGroupIndex, + multiplierForGeometryContributionToHitGroupIndex, + missShaderIndex, + ray, + payload); +} + +[shader("closesthit")] +void closestHit(inout RadiancePayload payload, BuiltInTriangleIntersectionAttributes attr) +{ + payload.color = shadeTriangle(attr); +} +``` + +The closest-hit function is not called directly by `rayGen`. The host builds an SBT, and the +trace call supplies the parameters that are used to select a record: + +```text +hitGroupSlot = + instanceContribution + + geometryContribution * sbtStride + + sbtOffset +``` + +Metal exposes a different programming model. A ray is traced by calling `intersector.intersect`, +which returns an `intersection_result`. The user then writes ordinary shader code to decide what +to do with that result. + +In simplified Metal-shaped code: + +```metal +kernel void rayGen(...) +{ + intersector tracer; + + intersection_result result = + tracer.intersect(ray, scene, intersectionFunctions, payload); + + if (result.type == intersection_type::none) + { + miss(payload); + } + else + { + closestHit(payload, result); + } +} +``` + +The practical problem is that current Slang ray tracing assumes the native D3D/Vulkan dispatch +model. Metal needs generated ordinary control flow after `intersect`, but the current shader +source does not provide enough structured information to synthesize that control flow in a robust +way. + +![D3D and Vulkan SBT dispatch compared with Metal user-specified post-trace miss and closest-hit dispatch](figures/ray-tracing-api/dispatch-model-gap.svg) + +### 1.2 Metal Tag List And Reachability + +Metal also has a tag-list requirement that current Slang cannot express directly. The Metal +`intersector` type is specialized by semantic tags, and custom intersection functions reachable +from that intersector must be declared with compatible tags. + +For example, a Metal program may have a tracer with one tag set: + +```metal +intersector primaryTracer; +``` + +The reachable custom intersection functions need matching semantic declarations: + +```metal +[[intersection(bounding_box, instancing, triangle_data)]] +bool primaryIntersection(...) +{ + ... +} +``` + +The issue in the existing Slang model is that the trace call and the intersection shader are +independent entry points. The trace call is written in ray-generation code. The intersection +shader is selected through host-side SBT state. The compiler can see both declarations, but the +source language does not state which trace call can reach which intersection shader. + +Problem-shaped Slang: + +```slang +[shader("raygeneration")] +void rayGen() +{ + // This trace needs tags equivalent to instancing + triangle_data. + TraceRay(scene, flags, mask, offset, stride, missIndex, ray, payload); +} + +[shader("intersection")] +void intersectionA() +{ + // This shader body might require curve_data instead. +} +``` + +The host may bind `intersectionA` into the hit group reached by the trace call. That can create a +Metal tag mismatch, but the old source shape has no type-level relationship that lets Slang +validate the mismatch early or generate the correct Metal decoration with confidence. + +![Tag list reachability problem](figures/ray-tracing-api/tag-list-reachability.svg) + +### 1.3 Reserved Challenges + +Other details remain important, but they are not the main shape of this proposal: + +- Metal has limitations on custom attributes returned from custom intersection functions. +- Metal has multiple `intersect(...)` overload families, including no-dispatch, function-table, + and function-buffer forms. +- Device user data should be modeled in a way that maps to non-pointer targets. + +Those issues can be handled after the dispatch and tag-reachability model is settled. + +## 2. Proposed API Sketch + +### 2.1 Overview + +The proposed API asks shader authors to describe a trace program in source: + +1. Define payload and context types. +2. Write miss, closest-hit, any-hit, and intersection logic as structs that implement Slang + interfaces. +3. Group those stage structs into slot-indexed hit and miss groups. +4. Use `rt::RayTracer` and `rt::RayDispatch` from ray-generation code. + +The group declarations are the source-level conceptual SBT. They are visible to the compiler and +to reflection. D3D and Vulkan use that information to build or validate native SBT records. Metal +uses it to generate the post-trace dispatch switch. + +![API overview](figures/ray-tracing-api/api-overview.svg) + +### 2.2 Detailed Component Descriptions + +#### 2.2.1 Resolving The Dispatch Model Gap With A Conceptual SBT + +The dispatch solution is to declare the SBT structure in shader source. At this layer, the key +concepts are only slots, groups, group lists, a trace program, and dispatch parameters. + +Simplified API shape: + +```slang +namespace rt +{ + public struct RayDispatch + { + public uint sbtOffset; + public uint sbtStride; + public uint missIndex; + } + + public interface IHitGroupSlot { static const int index; } + public struct HitGroupSlot : IHitGroupSlot + { + public static const int index = indexValue; + } + + public interface IMissSlot { static const int index; } + public struct MissSlot : IMissSlot + { + public static const int index = indexValue; + } + + public struct HitGroup { ... } + public struct MissGroup { ... } + + public struct HitGroupList { ... } + public struct MissGroupList { ... } + + public interface TraceProgram + { + associatedtype MissGroups; + associatedtype HitGroups; + } +} +``` + +The real draft API includes context constraints on these types. This section omits them because +the dispatch mechanism itself is about how slots are declared and reflected. + +The canonical slot calculation remains the D3D/Vulkan SBT calculation: + +```slang +public uint getHitGroupSlot( + RayDispatch dispatch, + uint geometryContribution, + uint instanceContribution) +{ + return instanceContribution + geometryContribution * dispatch.sbtStride + + dispatch.sbtOffset; +} +``` + +For D3D and Vulkan, this API is mostly descriptive: + +- `RayDispatch.sbtOffset` maps to D3D `RayContributionToHitGroupIndex` and Vulkan + `sbtRecordOffset`. +- `RayDispatch.sbtStride` maps to D3D `MultiplierForGeometryContributionToHitGroupIndex` and + Vulkan `sbtRecordStride`. +- `RayDispatch.missIndex` maps to the native miss shader index. +- The native driver or hardware still performs the SBT lookup. + +For Metal, this API becomes executable structure: + +- Slang lowers `RayTracer.trace(...)` to `intersector.intersect(...)`. +- Slang uses `RayDispatch.missIndex` to synthesize miss dispatch. +- Slang uses `getHitGroupSlot(...)` to synthesize closest-hit dispatch. +- Slang uses the same hit-group slots to validate and emit Metal function-table or + function-buffer entries for any-hit and custom intersection functions. + +Conceptual Metal lowering: + +```slang +let result = metalIntersector.intersect(desc.ray, scene, functionBuffer, payload); + +if (!result.isNone) +{ + uint geometryContribution = __rtGetGeometryContribution(result); + uint instanceContribution = __rtGetInstanceContribution(result); + uint slot = rt::getHitGroupSlot(dispatch, geometryContribution, instanceContribution); + + switch (slot) + { + case 0: + PrimaryTriangleClosestHit()(makeClosestHitInput(...)); + break; + case 1: + PrimaryCurveClosestHit()(makeClosestHitInput(...)); + break; + case 2: + PrimarySphereClosestHit()(makeClosestHitInput(...)); + break; + } +} +``` + +The important property is that Metal closest-hit dispatch is not invented independently. It is +generated from the same conceptual SBT slots that the host uses for D3D and Vulkan. + +Alternative considered: require users to write a structural Metal-like dispatch function. + +```slang +struct PrimaryClosestHitDispatcher +{ + void operator()(rt::ClosestHitInput input) + { + switch (input.geometryIndex) + { + case 0: shadeOpaqueTriangle(input); break; + case 1: shadeCurve(input); break; + case 2: shadeSphere(input); break; + } + } +} +``` + +Slang could analyze this user-written dispatch logic and reflect enough metadata for D3D/Vulkan +host code to reconstruct the SBT. This is closer to the normal Metal mental model, but it is a +harder compiler problem: + +- The compiler must recognize and preserve the user's dispatch structure. +- Reflection depends on successful control-flow analysis. +- Dynamic dispatch tables, buffer lookups, and helper functions complicate extraction. +- Small shader-code changes could affect host reflection in surprising ways. + +The proposed design chooses the reverse direction: users declare the SBT structure directly, then +Slang synthesizes Metal dispatch. This is simpler to validate and easier to reflect. + +#### 2.2.2 Resolving The Metal Tag-List Issue With Context Types + +The dispatch table solves the question: "which slot contains which shaders?" The tag-list issue +also needs a way to answer: "which trace object, hit shader, and intersection function share the +same semantic requirements?" + +The proposed answer is a context type. A trace context defines the properties of a trace family: + +```slang +interface ITraceContext +{ + associatedtype Payload; + associatedtype ASKind; + associatedtype Motion; + static const RayDataTags dataTags; + static const int maxLevels; +} +``` + +A hit context can specialize the trace context with a primitive kind: + +```slang +interface IHitContextFor +{ + associatedtype TraceContext; + associatedtype Primitive; +} +``` + +A trace program connects the ray tracer and every grouped shader through the same trace context: + +```slang +struct PrimaryTraceContext : rt::ITraceContext +{ + typealias Payload = RadiancePayload; + typealias ASKind = rt::InstanceAS; + typealias Motion = rt::NoMotion; + static const rt::RayDataTags dataTags = + rt::RayDataTags::Instancing | rt::RayDataTags::TriangleData; + static const int maxLevels = 0; +} + +struct PrimaryTriangleContext : rt::IHitContextFor +{ + typealias TraceContext = PrimaryTraceContext; + typealias Primitive = rt::TrianglePrimitive; +} + +struct PrimaryProgram : rt::TraceProgram +{ + typealias TraceContext = PrimaryTraceContext; + + typealias HitGroups = rt::HitGroupList< + TraceContext, + rt::HitGroup< + TraceContext, + rt::HitGroupSlot<0>, + PrimaryTriangleContext, + PrimaryTriangleClosestHit, + PrimaryTriangleAnyHit, + rt::NoAttributes, + rt::NoIntersection>>; +} + +[shader("raygeneration")] +void rayGen() +{ + RadiancePayload payload; + rt::RayTracer tracer; + tracer.trace(desc, scene, dispatch, payload); +} +``` + +This gives the compiler a source-level relationship: + +- `RayTracer` identifies one `TraceProgram`. +- `PrimaryProgram.TraceContext` defines the trace-wide Metal tags. +- Every `HitGroup` in `PrimaryProgram.HitGroups` is constrained to that trace context. +- Any-hit and intersection stage structs receive input types derived from the same context. + +![Context connects ray tracer and hit shaders](figures/ray-tracing-api/context-reachability.svg) + +This does not prove that arbitrary host data is correct. If the host builds an SBT or Metal +function table that violates the reflected `TraceProgram`, the program can still be wrong. The +goal is to make the shader-side contract explicit enough that: + +- Slang can generate Metal tags for reachable custom intersection functions. +- Slang can reject shader declarations that are inconsistent inside the `TraceProgram`. +- Reflection can expose the expected table to host code. +- Validation layers or Slang runtime helpers can compare host records against the reflected + contract. + +### 2.3 Writing Stages As Interface-Conforming Types + +In the new model, miss, closest-hit, any-hit, and intersection logic are not written as independent +entry points. They are written as ordinary structs that conform to built-in stage interfaces. + +Example: + +```slang +struct PrimaryTriangleClosestHit + : rt::IClosestHitShader +{ + void operator()(rt::ClosestHitInput input) + { + input.payload.color = float4(input.distance, 0.0, 0.0, 1.0); + } +} + +struct PrimaryTriangleAnyHit + : rt::IAnyHitShader +{ + void operator()(rt::AnyHitInput input) + { + if (isTransparent(input.triangle)) + input.ignoreHit(); + } +} + +struct PrimarySphereIntersection + : rt::IIntersectionShader +{ + rt::IntersectionReturn + operator()(rt::IntersectionInput input) + { + SphereHitAttributes attr; + float t = intersectSphere(input, attr); + return rt::IntersectionReturn::accept(t, attr); + } +} +``` + +The compiler is responsible for lowering these structs to the target form: + +- D3D and Vulkan: generated native entry points and hit groups, connected to SBT records. +- Metal: generated intersection functions for any-hit/custom-intersection behavior, plus a + generated post-trace closest-hit dispatch switch. + +The user writes one source-level model. The target backend chooses the appropriate pipeline shape. + +## 3. Migration Examples + +### 3.1 Migrating Existing Metal Code To The New API + +Existing Metal users often write post-trace logic directly: + +```metal +kernel void rayGen(...) +{ + intersector tracer; + tracer.assume_geometry_type(geometry_type::triangle); + + auto result = tracer.intersect(ray, scene, intersectionFunctionBuffer, payload); + + if (result.type == intersection_type::none) + { + miss(payload); + } + else + { + uint slot = result.geometry_id; + + switch (slot) + { + case 0: shadeOpaqueTriangle(payload, result); break; + case 1: shadeAlphaTriangle(payload, result); break; + case 2: shadeProceduralSphere(payload, result); break; + } + } +} +``` + +With the proposed API, the user moves the manually dispatched operations into stage structs and +declares the dispatch table structurally: + +```slang +struct PrimaryProgram : rt::TraceProgram +{ + typealias TraceContext = PrimaryTraceContext; + + typealias MissGroups = rt::MissGroupList< + TraceContext, + rt::MissGroup, PrimaryMiss>>; + + typealias HitGroups = rt::HitGroupList< + TraceContext, + rt::HitGroup< + TraceContext, + rt::HitGroupSlot<0>, + PrimaryTriangleContext, + PrimaryOpaqueTriangleClosestHit, + rt::NoAnyHit, + rt::NoAttributes, + rt::NoIntersection>, + rt::HitGroup< + TraceContext, + rt::HitGroupSlot<1>, + PrimaryTriangleContext, + PrimaryAlphaTriangleClosestHit, + PrimaryAlphaTriangleAnyHit, + rt::NoAttributes, + rt::NoIntersection>, + rt::HitGroup< + TraceContext, + rt::HitGroupSlot<2>, + PrimarySphereContext, + PrimarySphereClosestHit, + rt::NoAnyHit, + SphereHitAttributes, + PrimarySphereIntersection>>; +} +``` + +Ray-generation code becomes: + +```slang +[shader("raygeneration")] +void rayGen() +{ + RadiancePayload payload; + + rt::RayDispatch dispatch; + dispatch.sbtOffset = 0; + dispatch.sbtStride = 3; + dispatch.missIndex = 0; + + rt::RayTracer tracer; + tracer.trace(desc, scene, dispatch, payload); +} +``` + +For Metal, Slang generates code that is equivalent to the user's old post-trace dispatch, but the +source of truth is now `PrimaryProgram.HitGroups` and `PrimaryProgram.MissGroups`. + +Metal host migration: + +1. Query `PrimaryProgram` through Slang reflection. +2. For each hit group slot, discover its any-hit and intersection stage structs. +3. Build the Metal intersection function buffer or function table using the reflected slot + mapping. +4. For function-buffer dispatch, configure Metal's index calculation consistently with + `RayDispatch`: + +```metal +intersector.set_geometry_multiplier(dispatch.sbtStride); +intersector.set_base_id(dispatch.sbtOffset); +``` + +This keeps Metal's any-hit and custom-intersection dispatch aligned with Slang's generated +closest-hit dispatch. + +### 3.2 Migrating Existing Slang D3D/Vulkan Ray Tracing Code + +Existing Slang code usually has independent pipeline entry points: + +```slang +[shader("raygeneration")] +void rayGen() +{ + RadiancePayload payload; + + TraceRay( + scene, + flags, + instanceMask, + rayContributionToHitGroupIndex, + multiplierForGeometryContributionToHitGroupIndex, + missShaderIndex, + ray, + payload); +} + +[shader("miss")] +void miss(inout RadiancePayload payload) +{ + payload.color = backgroundColor; +} + +[shader("closesthit")] +void closestHit(inout RadiancePayload payload, BuiltInTriangleIntersectionAttributes attr) +{ + payload.color = shadeTriangle(attr); +} +``` + +The migrated shader keeps the same conceptual data, but moves stage bodies into typed structs: + +```slang +struct PrimaryMiss : rt::IMissShader +{ + void operator()(rt::MissInput input) + { + input.payload.color = backgroundColor; + } +} + +struct PrimaryTriangleClosestHit + : rt::IClosestHitShader +{ + void operator()(rt::ClosestHitInput input) + { + input.payload.color = shadeTriangle(input.triangle); + } +} +``` + +The old trace parameters map directly to `RayDispatch`: + +```slang +rt::RayDispatch dispatch; +dispatch.sbtOffset = rayContributionToHitGroupIndex; +dispatch.sbtStride = multiplierForGeometryContributionToHitGroupIndex; +dispatch.missIndex = missShaderIndex; + +rt::RayTracer tracer; +tracer.trace(desc, scene, dispatch, payload); +``` + +D3D/Vulkan host migration: + +1. Query `PrimaryProgram` through Slang reflection. +2. For each reflected miss group, add a miss record at `MissSlot.index`. +3. For each reflected hit group, add a hit group record at `HitGroupSlot.index`. +4. Use the same application data that previously produced `rayContributionToHitGroupIndex`, + `multiplierForGeometryContributionToHitGroupIndex`, and `missShaderIndex`. + +The native SBT model is not replaced. The new shader declarations make the intended SBT layout +visible to Slang, which enables Metal lowering and gives host code a single reflected contract. + +### 3.3 Host Reflection Patterns + +The reflection API shape is not finalized. The expected information is: + +```cpp +struct ReflectedTraceProgram +{ + TypeReflection* traceContextType; + List missGroups; + List hitGroups; +}; + +struct ReflectedMissGroup +{ + int slot; + EntryPointReflection* generatedMissEntryPoint; +}; + +struct ReflectedHitGroup +{ + int slot; + TypeReflection* hitContextType; + EntryPointReflection* generatedClosestHitEntryPoint; + EntryPointReflection* generatedAnyHitEntryPoint; + EntryPointReflection* generatedIntersectionEntryPoint; +}; +``` + +Pattern A: D3D/Vulkan native SBT. + +```cpp +auto program = reflection->findTraceProgram("PrimaryProgram"); + +for (auto miss : program.missGroups) +{ + sbt.setMissRecord(miss.slot, miss.generatedMissEntryPoint); +} + +for (auto hit : program.hitGroups) +{ + sbt.setHitGroup( + hit.slot, + hit.generatedClosestHitEntryPoint, + hit.generatedAnyHitEntryPoint, + hit.generatedIntersectionEntryPoint); +} +``` + +The application still controls geometry contribution, instance contribution, stride, and offset. +The reflected slots tell the application which shader group belongs at each slot. + +Pattern B: Metal intersection function buffer. + +```cpp +auto program = reflection->findTraceProgram("PrimaryProgram"); + +for (auto hit : program.hitGroups) +{ + if (hit.generatedAnyHitEntryPoint || hit.generatedIntersectionEntryPoint) + { + functionBuffer.setFunction( + hit.slot, + hit.generatedIntersectionOrAnyHitFunction); + } +} +``` + +The generated Metal ray-generation code uses the same slot to dispatch closest-hit after +`intersect(...)`. The host does not need to provide a separate closest-hit dispatch table for +Metal because closest-hit dispatch is synthesized by Slang. + +Pattern C: Metal intersection function table. + +Metal's ordinary function table path is less flexible than the function-buffer path because +shader code cannot set the same base-id and geometry-multiplier values in the same way. It can +still be supported for restricted layouts, for example: + +- the acceleration structure already contains the expected function-table offsets, +- the shader uses a fixed `RayDispatch` layout, +- or the backend can prove that the table index matches the reflected hit-group slot. + +Function-buffer lowering is the preferred general path for matching D3D/Vulkan SBT index +calculation. + +Pattern D: Manual host construction without reflection. + +A developer can still build the table by reading the shader source if the project chooses a fixed +layout convention: + +```slang +typealias HitGroups = rt::HitGroupList< + TraceContext, + rt::HitGroup, ...>, + rt::HitGroup, ...>, + rt::HitGroup, ...>>; +``` + +Reflection is strongly preferred because it removes duplicated source-of-truth in host code and +enables validation, but the layout is intentionally visible and reviewable in shader source. + +## 4. Open Design Questions + +- Exact reflection API names and ownership model. +- Exact generated entry-point naming rules for D3D and Vulkan. +- Whether Metal ordinary function-table lowering should be a restricted feature or a fully + supported path. +- How to represent custom intersection attributes on Metal when native return-value limits are + insufficient. +- How much runtime validation Slang should provide between reflected `TraceProgram` data and + host-created SBT or Metal function-buffer state. diff --git a/proposals/figures/ray-tracing-api/api-overview.svg b/proposals/figures/ray-tracing-api/api-overview.svg new file mode 100644 index 0000000..0edbfda --- /dev/null +++ b/proposals/figures/ray-tracing-api/api-overview.svg @@ -0,0 +1,49 @@ + + + + + + + + + Proposed Slang source structure + + + Trace context + payload, AS kind, tags + + + Stage structs + operator() implements logic + + + TraceProgram + MissGroupList<...> + HitGroupList<Slot0, Slot1, ...> + reflection-friendly conceptual SBT + + + D3D / Vulkan + native SBT and pipeline stages + + + Metal + generated post-trace dispatch + + + RayTracer<TraceProgram>.trace + + + + + + + diff --git a/proposals/figures/ray-tracing-api/context-reachability.svg b/proposals/figures/ray-tracing-api/context-reachability.svg new file mode 100644 index 0000000..5747cec --- /dev/null +++ b/proposals/figures/ray-tracing-api/context-reachability.svg @@ -0,0 +1,44 @@ + + + + + + + + + Context makes reachability explicit + + + RayTracer + RayTracer<PrimaryProgram> + + + PrimaryProgram + TraceContext = PrimaryTraceContext + HitGroups = slot-indexed list + + + ClosestHitInput + PrimaryTriangleContext + + + AnyHitInput + PrimaryTriangleContext + + + IntersectionInput + PrimaryTriangleContext + + + + + + diff --git a/proposals/figures/ray-tracing-api/dispatch-model-gap.svg b/proposals/figures/ray-tracing-api/dispatch-model-gap.svg new file mode 100644 index 0000000..7fe78dd --- /dev/null +++ b/proposals/figures/ray-tracing-api/dispatch-model-gap.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + Dispatch model gap + D3D/Vulkan have native SBT dispatch for pipeline stages. Metal has traversal dispatch for custom intersection functions, + but miss and closest-hit are ordinary post-trace shader logic. + + D3D / Vulkan: driver or hardware dispatches through SBT records + + + Ray-generation shader + TraceRay / OpTraceRayKHR + missIndex = 1 + sbtOffset = 2 + sbtStride = 3 + + + Native index calculation + hitSlot = instanceContribution + + geometryContribution * sbtStride + + sbtOffset + example: 0 + 1 * 3 + 2 = hit slot 5 + + + + + Shader Binding Table, host-created + + + Miss table + + Miss[0] background + + Miss[1] shadow miss + + Miss[2] debug miss + + + Hit-group table + + Slot 0: CH triangle matte + + Slot 1: CH triangle alpha + AH + + Slot 2: CH curve + + Slot 3: CH material A + + Slot 4: CH material B + + Slot 5: CH sphere + + AH + Intersection + + + + + + Selected native stages + Miss[1] if no hit. HitGroup[5] dispatches intersection, any-hit, + and closest-hit as native pipeline stages. + + + + + gap: closest-hit dispatch changes ownership + D3D/Vulkan: host SBT records. Metal: user-specified shader code. + + + Metal: users write miss and closest-hit dispatch after intersect() + + + Ray-generation shader + intersector.intersect(...) + returns intersection_result + no native miss / closest-hit call + + + intersection_result + hit or none + distance, ids, tags + used by user code + + + User-specified post-trace dispatch + if result.none: + miss(payload) + else: + switch geometry/material/ray type + + + Metal function table/buffer can dispatch AnyHit and Intersection during traversal, + but not Miss or ClosestHit. + + + + + diff --git a/proposals/figures/ray-tracing-api/tag-list-reachability.svg b/proposals/figures/ray-tracing-api/tag-list-reachability.svg new file mode 100644 index 0000000..10aec05 --- /dev/null +++ b/proposals/figures/ray-tracing-api/tag-list-reachability.svg @@ -0,0 +1,40 @@ + + + + + + + + + Existing model: trace call and intersection shader are not connected in source + + RayGen TraceRay + needs Metal tags: instancing, triangle_data + + + Host SBT binding + decides reachable hit group + + + intersectionA + uses triangle_data + + + intersectionB + uses curve_data + + + + + The compiler needs a source-level contract for this reachability. + From d25d2ffdca56a6d4f02d39ab185a91d423df0ed4 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Fri, 5 Jun 2026 13:14:35 -0500 Subject: [PATCH 02/21] Number ray tracing API proposal as 041 --- .../{000-ray-tracing-api.md => 041-ray-tracing-api.md} | 10 +++++----- .../api-overview.svg | 0 .../context-reachability.svg | 0 .../dispatch-model-gap.svg | 0 .../tag-list-reachability.svg | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename proposals/{000-ray-tracing-api.md => 041-ray-tracing-api.md} (98%) rename proposals/figures/{ray-tracing-api => 041-ray-tracing-api}/api-overview.svg (100%) rename proposals/figures/{ray-tracing-api => 041-ray-tracing-api}/context-reachability.svg (100%) rename proposals/figures/{ray-tracing-api => 041-ray-tracing-api}/dispatch-model-gap.svg (100%) rename proposals/figures/{ray-tracing-api => 041-ray-tracing-api}/tag-list-reachability.svg (100%) diff --git a/proposals/000-ray-tracing-api.md b/proposals/041-ray-tracing-api.md similarity index 98% rename from proposals/000-ray-tracing-api.md rename to proposals/041-ray-tracing-api.md index 41c34c1..850f205 100644 --- a/proposals/000-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -1,4 +1,4 @@ -SP #000: Pipeline Ray Tracing API Structural Dispatch +SP #041: Pipeline Ray Tracing API Structural Dispatch ===================================================== This proposal sketches a new Slang ray tracing API that treats Metal as a first-class target @@ -96,7 +96,7 @@ model. Metal needs generated ordinary control flow after `intersect`, but the cu source does not provide enough structured information to synthesize that control flow in a robust way. -![D3D and Vulkan SBT dispatch compared with Metal user-specified post-trace miss and closest-hit dispatch](figures/ray-tracing-api/dispatch-model-gap.svg) +![D3D and Vulkan SBT dispatch compared with Metal user-specified post-trace miss and closest-hit dispatch](figures/041-ray-tracing-api/dispatch-model-gap.svg) ### 1.2 Metal Tag List And Reachability @@ -146,7 +146,7 @@ The host may bind `intersectionA` into the hit group reached by the trace call. Metal tag mismatch, but the old source shape has no type-level relationship that lets Slang validate the mismatch early or generate the correct Metal decoration with confidence. -![Tag list reachability problem](figures/ray-tracing-api/tag-list-reachability.svg) +![Tag list reachability problem](figures/041-ray-tracing-api/tag-list-reachability.svg) ### 1.3 Reserved Challenges @@ -175,7 +175,7 @@ The group declarations are the source-level conceptual SBT. They are visible to to reflection. D3D and Vulkan use that information to build or validate native SBT records. Metal uses it to generate the post-trace dispatch switch. -![API overview](figures/ray-tracing-api/api-overview.svg) +![API overview](figures/041-ray-tracing-api/api-overview.svg) ### 2.2 Detailed Component Descriptions @@ -393,7 +393,7 @@ This gives the compiler a source-level relationship: - Every `HitGroup` in `PrimaryProgram.HitGroups` is constrained to that trace context. - Any-hit and intersection stage structs receive input types derived from the same context. -![Context connects ray tracer and hit shaders](figures/ray-tracing-api/context-reachability.svg) +![Context connects ray tracer and hit shaders](figures/041-ray-tracing-api/context-reachability.svg) This does not prove that arbitrary host data is correct. If the host builds an SBT or Metal function table that violates the reflected `TraceProgram`, the program can still be wrong. The diff --git a/proposals/figures/ray-tracing-api/api-overview.svg b/proposals/figures/041-ray-tracing-api/api-overview.svg similarity index 100% rename from proposals/figures/ray-tracing-api/api-overview.svg rename to proposals/figures/041-ray-tracing-api/api-overview.svg diff --git a/proposals/figures/ray-tracing-api/context-reachability.svg b/proposals/figures/041-ray-tracing-api/context-reachability.svg similarity index 100% rename from proposals/figures/ray-tracing-api/context-reachability.svg rename to proposals/figures/041-ray-tracing-api/context-reachability.svg diff --git a/proposals/figures/ray-tracing-api/dispatch-model-gap.svg b/proposals/figures/041-ray-tracing-api/dispatch-model-gap.svg similarity index 100% rename from proposals/figures/ray-tracing-api/dispatch-model-gap.svg rename to proposals/figures/041-ray-tracing-api/dispatch-model-gap.svg diff --git a/proposals/figures/ray-tracing-api/tag-list-reachability.svg b/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg similarity index 100% rename from proposals/figures/ray-tracing-api/tag-list-reachability.svg rename to proposals/figures/041-ray-tracing-api/tag-list-reachability.svg From e8d8ab84828b11a38f0b799293ffba29af899567 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 12:10:40 -0500 Subject: [PATCH 03/21] Clarify Metal tag reachability challenge --- proposals/041-ray-tracing-api.md | 55 ++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md index 850f205..ed12d7c 100644 --- a/proposals/041-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -120,10 +120,50 @@ bool primaryIntersection(...) } ``` -The issue in the existing Slang model is that the trace call and the intersection shader are -independent entry points. The trace call is written in ray-generation code. The intersection -shader is selected through host-side SBT state. The compiler can see both declarations, but the -source language does not state which trace call can reach which intersection shader. +If a Metal program binds an incompatible custom intersection function to an intersector, the +mismatch is still detectable. However, it can only be detected when the pipeline is built, because +that is the point where all shader functions, intersector tag requirements, and binding +information are available together. This is acceptable for native Metal because the user already +writes the tag list on each `[[intersection(...)]]` function. The Metal compiler or pipeline +builder has concrete tags to validate. + +Slang has a harder problem. The existing Slang ray tracing API does not expose a Metal-style tag +system in user source. To lower AnyHit or Intersection entry points to Metal, Slang would need to +synthesize the `[[intersection(...)]]` tag list for each generated Metal function. + +Consider a Slang program with several candidate hit shaders: + +```slang +[shader("anyhit")] void AnyHit1() { ... } +[shader("anyhit")] void AnyHit2() { ... } +[shader("anyhit")] void AnyHit3() { ... } +[shader("intersection")] void Intersection1() { ... } +[shader("intersection")] void Intersection2() { ... } +[shader("intersection")] void Intersection3() { ... } +``` + +and two trace sites that must lower to different Metal intersector tag sets: + +```slang +void rayGen() +{ + // Lowers to a Metal intersector with tag set A. + intersector1.intersect(...); + + // Lowers to a Metal intersector with tag set B. + intersector2.intersect(...); +} +``` + +The compiler needs to decide whether `AnyHit1`, `AnyHit2`, `Intersection1`, etc. should receive +tag set A, tag set B, or some other tag set. In the existing pipeline model, that reachability is +not stated in shader source. The trace call is written in ray-generation code, while AnyHit and +Intersection shaders are selected through host-side SBT or function-table state. + +The host may bind `Intersection1` to the hit group reached by `intersector1`, and bind +`Intersection2` to the hit group reached by `intersector2`. That information is only available to +host code. Slang does not see it when compiling the shader module, so it cannot reliably +synthesize the required Metal tags for each generated custom intersection function. Problem-shaped Slang: @@ -142,9 +182,10 @@ void intersectionA() } ``` -The host may bind `intersectionA` into the hit group reached by the trace call. That can create a -Metal tag mismatch, but the old source shape has no type-level relationship that lets Slang -validate the mismatch early or generate the correct Metal decoration with confidence. +The host may bind `intersectionA` into the hit group reached by the trace call. If Slang emits no +Metal tags, or emits tags inferred from the wrong trace site, the generated Metal code can fail at +pipeline build. The old source shape has no type-level relationship that lets Slang determine +which trace call can reach which AnyHit or Intersection shader. ![Tag list reachability problem](figures/041-ray-tracing-api/tag-list-reachability.svg) From bd32bdea6a9f3c245b14479c949034e40e0b538f Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 12:17:11 -0500 Subject: [PATCH 04/21] Redesign Metal tag reachability diagram --- proposals/041-ray-tracing-api.md | 2 +- .../tag-list-reachability.svg | 134 ++++++++++++++---- 2 files changed, 110 insertions(+), 26 deletions(-) diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md index ed12d7c..96958bd 100644 --- a/proposals/041-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -187,7 +187,7 @@ Metal tags, or emits tags inferred from the wrong trace site, the generated Meta pipeline build. The old source shape has no type-level relationship that lets Slang determine which trace call can reach which AnyHit or Intersection shader. -![Tag list reachability problem](figures/041-ray-tracing-api/tag-list-reachability.svg) +![Metal tag-list reachability problem for Slang AnyHit and Intersection lowering](figures/041-ray-tracing-api/tag-list-reachability.svg) ### 1.3 Reserved Challenges diff --git a/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg b/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg index 10aec05..feb23bf 100644 --- a/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg +++ b/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg @@ -1,40 +1,124 @@ - + - + + + + + + + + + + - Existing model: trace call and intersection shader are not connected in source - - RayGen TraceRay - needs Metal tags: instancing, triangle_data + Metal tag-list reachability problem + Metal custom intersection functions are tagged in source. Slang must synthesize those tags, + but old-style pipeline shaders do not say which trace can reach which AnyHit or Intersection entry point. + + + Native Metal: tags are explicit, so pipeline-build validation has enough information + + + Metal shader source + intersector<TagA> tracerA; + intersector<TagB> tracerB; + + + Tagged intersection functions + [[intersection(TagA)]] fA() + [[intersection(TagB)]] fB() + [[intersection(TagB)]] fC() + + + Host bindings + tracerA slot 0 -> fA + tracerA slot 1 -> fB + mismatch + + + Pipeline build + sees tracer tags, + function tags, + and bindings + + + + + + + Mismatch is late but detectable: + pipeline build can reject tracerA -> fB because both sides already carry concrete Metal tags. + + + Existing Slang model: the compiler must emit Metal tags before host bindings are known + + + Ray-generation code + intersector1.intersect(...) + needs Metal tag set A + intersector2.intersect(...) + needs Metal tag set B + + + Independent stage entries + AnyHit1() + AnyHit2() + AnyHit3() + Intersection1() + Intersection2() + + + Slang Metal codegen + must emit: + [[intersection(?)]] + generatedAnyHit() + Which tag set? - - Host SBT binding - decides reachable hit group + + Host bindings + slot tables decide: + A -> Intersection2 + B -> AnyHit1 + Available later - - intersectionA - uses triangle_data + + + + too late - - intersectionB - uses curve_data + + Core problem: + Slang sees trace sites and stage entry points, but not the host binding edges that connect them. + Without a source-level reachability contract, it cannot know whether each generated + AnyHit/Intersection function needs tag set A, tag set B, or another tag set. - - - - The compiler needs a source-level contract for this reachability. + + + From 1f70e2d28077539ee74e7eaf13b72d4020332696 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 12:20:03 -0500 Subject: [PATCH 05/21] Adjust tag reachability diagram layout --- .../tag-list-reachability.svg | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg b/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg index feb23bf..3bad69f 100644 --- a/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg +++ b/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg @@ -100,16 +100,16 @@ generatedAnyHit() Which tag set? - - Host bindings - slot tables decide: - A -> Intersection2 - B -> AnyHit1 - Available later + + Host bindings + slot tables decide: + A -> Intersection2 + B -> AnyHit1 + Available later - + too late @@ -120,5 +120,5 @@ - + From cdea68d7d8f9a6a31b5e6d4abd6ed651de04b3ca Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 12:25:55 -0500 Subject: [PATCH 06/21] Introduce ray tracing reachability term --- proposals/041-ray-tracing-api.md | 8 ++ .../reachability-definition.svg | 97 +++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 proposals/figures/041-ray-tracing-api/reachability-definition.svg diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md index 96958bd..fbaafab 100644 --- a/proposals/041-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -100,6 +100,14 @@ way. ### 1.2 Metal Tag List And Reachability +This proposal uses the term **reachability** to describe the shader binding table entries that a +single trace call can access. A trace call does not directly call an AnyHit, Intersection, +ClosestHit, or Miss shader in source. Instead, the trace call supplies dispatch parameters, the +runtime traversal finds geometry and instance contributions, and the target selects one of the SBT +records. Those selectable records are the entries reachable from that trace call. + +![Reachability is the set of SBT entries that one trace call can select](figures/041-ray-tracing-api/reachability-definition.svg) + Metal also has a tag-list requirement that current Slang cannot express directly. The Metal `intersector` type is specialized by semantic tags, and custom intersection functions reachable from that intersector must be declared with compatible tags. diff --git a/proposals/figures/041-ray-tracing-api/reachability-definition.svg b/proposals/figures/041-ray-tracing-api/reachability-definition.svg new file mode 100644 index 0000000..37b8702 --- /dev/null +++ b/proposals/figures/041-ray-tracing-api/reachability-definition.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + Reachability: which SBT entries can one trace call access? + + + One trace call + TraceRay(...) + sbtOffset = 2 + sbtStride = 3 + missIndex = 0 + + + Runtime SBT index + slot = instanceContribution + + geometryContribution * stride + + offset + over all possible hits from this ray + + + + + Host SBT + + + slot 0: other ray type + + slot 1: other ray type + + slot 2: HitGroup A + AnyHit1, Intersection1 + + slot 3: other ray type + + slot 4: other ray type + + slot 5: HitGroup B + AnyHit2, Intersection2 + + slot 6: other ray type + + slot 8: HitGroup C + ClosestHit only + + + + + + + Reachable entries + the set this trace call + may select at runtime + + + Definition used by this proposal + Reachability(trace call) = the SBT hit or miss records + that can be selected by that trace call. + Those records determine which shader logic is reachable. + + + Why this matters for Metal + Reachable AnyHit and + Intersection functions need + the trace call's tag set. + + + From 0bef6974574e5551c04f7b8ea59e32acc700d108 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 12:34:08 -0500 Subject: [PATCH 07/21] Clarify host-defined ray tracing reachability --- proposals/041-ray-tracing-api.md | 5 +++++ .../041-ray-tracing-api/reachability-definition.svg | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md index fbaafab..e639426 100644 --- a/proposals/041-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -106,6 +106,11 @@ ClosestHit, or Miss shader in source. Instead, the trace call supplies dispatch runtime traversal finds geometry and instance contributions, and the target selects one of the SBT records. Those selectable records are the entries reachable from that trace call. +In existing D3D/Vulkan-style ray tracing models, this reachability is determined by host-created +binding data. The shader source contains the trace call, but the SBT records and the binding edges +from those records to AnyHit, Intersection, ClosestHit, and Miss shaders are provided by host code. +Therefore, the complete reachability set is not known from shader source at ordinary compile time. + ![Reachability is the set of SBT entries that one trace call can select](figures/041-ray-tracing-api/reachability-definition.svg) Metal also has a tag-list requirement that current Slang cannot express directly. The Metal diff --git a/proposals/figures/041-ray-tracing-api/reachability-definition.svg b/proposals/figures/041-ray-tracing-api/reachability-definition.svg index 37b8702..1424bb3 100644 --- a/proposals/figures/041-ray-tracing-api/reachability-definition.svg +++ b/proposals/figures/041-ray-tracing-api/reachability-definition.svg @@ -1,4 +1,4 @@ - + From 4f8d7d8f2e567f258b4debe5ea54f05a4767c201 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 12:35:34 -0500 Subject: [PATCH 08/21] Add captions for ray tracing proposal figures --- proposals/041-ray-tracing-api.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md index e639426..c7b232f 100644 --- a/proposals/041-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -94,10 +94,13 @@ kernel void rayGen(...) The practical problem is that current Slang ray tracing assumes the native D3D/Vulkan dispatch model. Metal needs generated ordinary control flow after `intersect`, but the current shader source does not provide enough structured information to synthesize that control flow in a robust -way. +way. Figure 1 summarizes the dispatch ownership gap. + ![D3D and Vulkan SBT dispatch compared with Metal user-specified post-trace miss and closest-hit dispatch](figures/041-ray-tracing-api/dispatch-model-gap.svg) +*Figure 1. Dispatch model gap: D3D and Vulkan select pipeline stages through host-created SBT records, while Metal requires user-written post-trace dispatch for Miss and ClosestHit.* + ### 1.2 Metal Tag List And Reachability This proposal uses the term **reachability** to describe the shader binding table entries that a @@ -110,9 +113,13 @@ In existing D3D/Vulkan-style ray tracing models, this reachability is determined binding data. The shader source contains the trace call, but the SBT records and the binding edges from those records to AnyHit, Intersection, ClosestHit, and Miss shaders are provided by host code. Therefore, the complete reachability set is not known from shader source at ordinary compile time. +Figure 2 defines this term graphically. + ![Reachability is the set of SBT entries that one trace call can select](figures/041-ray-tracing-api/reachability-definition.svg) +*Figure 2. Reachability definition: the reachable entries are the SBT records one trace call can select at runtime, but in the existing model that set is determined by host-created binding data.* + Metal also has a tag-list requirement that current Slang cannot express directly. The Metal `intersector` type is specialized by semantic tags, and custom intersection functions reachable from that intersector must be declared with compatible tags. @@ -198,10 +205,14 @@ void intersectionA() The host may bind `intersectionA` into the hit group reached by the trace call. If Slang emits no Metal tags, or emits tags inferred from the wrong trace site, the generated Metal code can fail at pipeline build. The old source shape has no type-level relationship that lets Slang determine -which trace call can reach which AnyHit or Intersection shader. +which trace call can reach which AnyHit or Intersection shader. Figure 3 shows this information +flow problem. + ![Metal tag-list reachability problem for Slang AnyHit and Intersection lowering](figures/041-ray-tracing-api/tag-list-reachability.svg) +*Figure 3. Metal tag-list reachability problem: native Metal can validate explicit tags at pipeline build time, but Slang must synthesize those tags before host binding data reveals which AnyHit or Intersection entries are reachable.* + ### 1.3 Reserved Challenges Other details remain important, but they are not the main shape of this proposal: @@ -227,10 +238,14 @@ The proposed API asks shader authors to describe a trace program in source: The group declarations are the source-level conceptual SBT. They are visible to the compiler and to reflection. D3D and Vulkan use that information to build or validate native SBT records. Metal -uses it to generate the post-trace dispatch switch. +uses it to generate the post-trace dispatch switch. Figure 4 gives a high-level view of the API +shape. + ![API overview](figures/041-ray-tracing-api/api-overview.svg) +*Figure 4. Proposed API overview: users define contexts, stage structs, and grouped dispatch metadata, and `RayTracer` lowers to target-specific dispatch mechanisms.* + ### 2.2 Detailed Component Descriptions #### 2.2.1 Resolving The Dispatch Model Gap With A Conceptual SBT @@ -447,8 +462,14 @@ This gives the compiler a source-level relationship: - Every `HitGroup` in `PrimaryProgram.HitGroups` is constrained to that trace context. - Any-hit and intersection stage structs receive input types derived from the same context. +Figure 5 shows how the `TraceProgram` context connects the trace call to the grouped stage +structs. + + ![Context connects ray tracer and hit shaders](figures/041-ray-tracing-api/context-reachability.svg) +*Figure 5. Context reachability contract: the `TraceProgram` connects `RayTracer`, the trace-wide context, hit groups, and stage input types so the compiler has a source-visible relationship.* + This does not prove that arbitrary host data is correct. If the host builds an SBT or Metal function table that violates the reflected `TraceProgram`, the program can still be wrong. The goal is to make the shader-side contract explicit enough that: From dd02bc6d462fa4a177aefcedf6aac5d4dfc6a2a9 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 12:37:45 -0500 Subject: [PATCH 09/21] Simplify ray tracing problem description --- proposals/041-ray-tracing-api.md | 192 ++++++------------------------- 1 file changed, 32 insertions(+), 160 deletions(-) diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md index c7b232f..68f5ecd 100644 --- a/proposals/041-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -26,75 +26,19 @@ scope for this first design. ### 1.1 Dispatch Model Gap -D3D and Vulkan expose ray tracing as a pipeline-stage model. A ray-generation shader calls -`TraceRay` or `OpTraceRayKHR`, and the driver or hardware uses SBT parameters to select miss, -any-hit, intersection, and closest-hit shaders. +D3D and Vulkan expose ray tracing as a pipeline-stage model. A trace call enters traversal, and +the driver or hardware uses host-created SBT records to select miss, any-hit, intersection, and +closest-hit shaders. The closest-hit function is not directly called from ray-generation source. -In simplified D3D/Vulkan-shaped Slang: +Metal exposes a different model. `intersector.intersect(...)` returns an `intersection_result`. +AnyHit and custom Intersection behavior can still be dispatched during traversal through Metal's +function table or function buffer, but Miss and ClosestHit are ordinary post-trace shader logic +written by the user. -```slang -[shader("raygeneration")] -void rayGen() -{ - RadiancePayload payload; - - TraceRay( - scene, - rayFlags, - instanceMask, - rayContributionToHitGroupIndex, - multiplierForGeometryContributionToHitGroupIndex, - missShaderIndex, - ray, - payload); -} - -[shader("closesthit")] -void closestHit(inout RadiancePayload payload, BuiltInTriangleIntersectionAttributes attr) -{ - payload.color = shadeTriangle(attr); -} -``` - -The closest-hit function is not called directly by `rayGen`. The host builds an SBT, and the -trace call supplies the parameters that are used to select a record: - -```text -hitGroupSlot = - instanceContribution + - geometryContribution * sbtStride + - sbtOffset -``` - -Metal exposes a different programming model. A ray is traced by calling `intersector.intersect`, -which returns an `intersection_result`. The user then writes ordinary shader code to decide what -to do with that result. - -In simplified Metal-shaped code: - -```metal -kernel void rayGen(...) -{ - intersector tracer; - - intersection_result result = - tracer.intersect(ray, scene, intersectionFunctions, payload); - - if (result.type == intersection_type::none) - { - miss(payload); - } - else - { - closestHit(payload, result); - } -} -``` - -The practical problem is that current Slang ray tracing assumes the native D3D/Vulkan dispatch -model. Metal needs generated ordinary control flow after `intersect`, but the current shader -source does not provide enough structured information to synthesize that control flow in a robust -way. Figure 1 summarizes the dispatch ownership gap. +Figure 1 shows the key mismatch: D3D/Vulkan assign stage dispatch to the host SBT and the +driver/hardware, while Metal assigns Miss and ClosestHit dispatch to shader code after +`intersect(...)` returns. A portable Slang API needs enough structure to synthesize that Metal +post-trace dispatch without changing the native D3D/Vulkan model. ![D3D and Vulkan SBT dispatch compared with Metal user-specified post-trace miss and closest-hit dispatch](figures/041-ray-tracing-api/dispatch-model-gap.svg) @@ -104,109 +48,37 @@ way. Figure 1 summarizes the dispatch ownership gap. ### 1.2 Metal Tag List And Reachability This proposal uses the term **reachability** to describe the shader binding table entries that a -single trace call can access. A trace call does not directly call an AnyHit, Intersection, -ClosestHit, or Miss shader in source. Instead, the trace call supplies dispatch parameters, the -runtime traversal finds geometry and instance contributions, and the target selects one of the SBT -records. Those selectable records are the entries reachable from that trace call. +single trace call can access. The trace call supplies dispatch parameters, traversal contributes +geometry and instance information, and the target selects one of the SBT records. Those selectable +records are the entries reachable from that trace call. In existing D3D/Vulkan-style ray tracing models, this reachability is determined by host-created binding data. The shader source contains the trace call, but the SBT records and the binding edges from those records to AnyHit, Intersection, ClosestHit, and Miss shaders are provided by host code. -Therefore, the complete reachability set is not known from shader source at ordinary compile time. -Figure 2 defines this term graphically. +Therefore, as shown in Figure 2, the complete reachability set is not known from shader source at +ordinary compile time. ![Reachability is the set of SBT entries that one trace call can select](figures/041-ray-tracing-api/reachability-definition.svg) *Figure 2. Reachability definition: the reachable entries are the SBT records one trace call can select at runtime, but in the existing model that set is determined by host-created binding data.* -Metal also has a tag-list requirement that current Slang cannot express directly. The Metal -`intersector` type is specialized by semantic tags, and custom intersection functions reachable -from that intersector must be declared with compatible tags. - -For example, a Metal program may have a tracer with one tag set: - -```metal -intersector primaryTracer; -``` - -The reachable custom intersection functions need matching semantic declarations: - -```metal -[[intersection(bounding_box, instancing, triangle_data)]] -bool primaryIntersection(...) -{ - ... -} -``` - -If a Metal program binds an incompatible custom intersection function to an intersector, the -mismatch is still detectable. However, it can only be detected when the pipeline is built, because -that is the point where all shader functions, intersector tag requirements, and binding -information are available together. This is acceptable for native Metal because the user already -writes the tag list on each `[[intersection(...)]]` function. The Metal compiler or pipeline -builder has concrete tags to validate. - -Slang has a harder problem. The existing Slang ray tracing API does not expose a Metal-style tag -system in user source. To lower AnyHit or Intersection entry points to Metal, Slang would need to -synthesize the `[[intersection(...)]]` tag list for each generated Metal function. - -Consider a Slang program with several candidate hit shaders: - -```slang -[shader("anyhit")] void AnyHit1() { ... } -[shader("anyhit")] void AnyHit2() { ... } -[shader("anyhit")] void AnyHit3() { ... } -[shader("intersection")] void Intersection1() { ... } -[shader("intersection")] void Intersection2() { ... } -[shader("intersection")] void Intersection3() { ... } -``` - -and two trace sites that must lower to different Metal intersector tag sets: - -```slang -void rayGen() -{ - // Lowers to a Metal intersector with tag set A. - intersector1.intersect(...); - - // Lowers to a Metal intersector with tag set B. - intersector2.intersect(...); -} -``` - -The compiler needs to decide whether `AnyHit1`, `AnyHit2`, `Intersection1`, etc. should receive -tag set A, tag set B, or some other tag set. In the existing pipeline model, that reachability is -not stated in shader source. The trace call is written in ray-generation code, while AnyHit and -Intersection shaders are selected through host-side SBT or function-table state. - -The host may bind `Intersection1` to the hit group reached by `intersector1`, and bind -`Intersection2` to the hit group reached by `intersector2`. That information is only available to -host code. Slang does not see it when compiling the shader module, so it cannot reliably -synthesize the required Metal tags for each generated custom intersection function. - -Problem-shaped Slang: - -```slang -[shader("raygeneration")] -void rayGen() -{ - // This trace needs tags equivalent to instancing + triangle_data. - TraceRay(scene, flags, mask, offset, stride, missIndex, ray, payload); -} - -[shader("intersection")] -void intersectionA() -{ - // This shader body might require curve_data instead. -} -``` - -The host may bind `intersectionA` into the hit group reached by the trace call. If Slang emits no -Metal tags, or emits tags inferred from the wrong trace site, the generated Metal code can fail at -pipeline build. The old source shape has no type-level relationship that lets Slang determine -which trace call can reach which AnyHit or Intersection shader. Figure 3 shows this information -flow problem. +Metal adds a second constraint: each custom intersection function reachable from an intersector +must have a compatible `[[intersection(...)]]` tag list. Native Metal can validate a mismatch at +pipeline build time because the user writes both the intersector tags and the function tags in +source. + +Slang does not currently expose that Metal tag system. When lowering AnyHit or Intersection entry +points to Metal, Slang must synthesize `[[intersection(...)]]` tags for the generated Metal +functions. The compiler can see trace sites and stage entry points, but in the existing model it +cannot see the host binding edges that determine which stage entries are reachable from each trace +site. + +Figure 3 shows the information-flow problem. If two trace sites lower to different Metal tag +sets, and several AnyHit or Intersection entries may be bound by the host, the compiler cannot +know whether a generated function needs tag set A, tag set B, or another tag set. Emitting no tag, +or emitting a tag inferred from the wrong trace site, can make the generated Metal pipeline fail +to build. ![Metal tag-list reachability problem for Slang AnyHit and Intersection lowering](figures/041-ray-tracing-api/tag-list-reachability.svg) From 35e69b5cde96913808fb2f8fd0aba41c44861154 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 12:42:11 -0500 Subject: [PATCH 10/21] Split Metal tag reachability figures --- proposals/041-ray-tracing-api.md | 24 ++-- .../native-metal-tag-validation.svg | 64 +++++++++ .../slang-tag-synthesis-gap.svg | 76 +++++++++++ .../tag-list-reachability.svg | 124 ------------------ 4 files changed, 155 insertions(+), 133 deletions(-) create mode 100644 proposals/figures/041-ray-tracing-api/native-metal-tag-validation.svg create mode 100644 proposals/figures/041-ray-tracing-api/slang-tag-synthesis-gap.svg delete mode 100644 proposals/figures/041-ray-tracing-api/tag-list-reachability.svg diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md index 68f5ecd..2e11e89 100644 --- a/proposals/041-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -66,7 +66,13 @@ ordinary compile time. Metal adds a second constraint: each custom intersection function reachable from an intersector must have a compatible `[[intersection(...)]]` tag list. Native Metal can validate a mismatch at pipeline build time because the user writes both the intersector tags and the function tags in -source. +source. Figure 3 shows why this works: pipeline build sees the intersector tags, function tags, +and host bindings together. + + +![Native Metal can validate explicit intersector and custom intersection function tags at pipeline build time](figures/041-ray-tracing-api/native-metal-tag-validation.svg) + +*Figure 3. Native Metal tag validation: the user-authored tag lists give pipeline build enough information to reject incompatible host bindings.* Slang does not currently expose that Metal tag system. When lowering AnyHit or Intersection entry points to Metal, Slang must synthesize `[[intersection(...)]]` tags for the generated Metal @@ -74,16 +80,16 @@ functions. The compiler can see trace sites and stage entry points, but in the e cannot see the host binding edges that determine which stage entries are reachable from each trace site. -Figure 3 shows the information-flow problem. If two trace sites lower to different Metal tag +Figure 4 shows the information-flow problem. If two trace sites lower to different Metal tag sets, and several AnyHit or Intersection entries may be bound by the host, the compiler cannot know whether a generated function needs tag set A, tag set B, or another tag set. Emitting no tag, or emitting a tag inferred from the wrong trace site, can make the generated Metal pipeline fail to build. - -![Metal tag-list reachability problem for Slang AnyHit and Intersection lowering](figures/041-ray-tracing-api/tag-list-reachability.svg) + +![Slang cannot synthesize Metal intersection tags when host binding data owns reachability](figures/041-ray-tracing-api/slang-tag-synthesis-gap.svg) -*Figure 3. Metal tag-list reachability problem: native Metal can validate explicit tags at pipeline build time, but Slang must synthesize those tags before host binding data reveals which AnyHit or Intersection entries are reachable.* +*Figure 4. Slang tag synthesis gap: Slang must emit Metal `[[intersection(...)]]` tags before host binding data reveals which AnyHit or Intersection entries are reachable from each trace site.* ### 1.3 Reserved Challenges @@ -110,13 +116,13 @@ The proposed API asks shader authors to describe a trace program in source: The group declarations are the source-level conceptual SBT. They are visible to the compiler and to reflection. D3D and Vulkan use that information to build or validate native SBT records. Metal -uses it to generate the post-trace dispatch switch. Figure 4 gives a high-level view of the API +uses it to generate the post-trace dispatch switch. Figure 5 gives a high-level view of the API shape. ![API overview](figures/041-ray-tracing-api/api-overview.svg) -*Figure 4. Proposed API overview: users define contexts, stage structs, and grouped dispatch metadata, and `RayTracer` lowers to target-specific dispatch mechanisms.* +*Figure 5. Proposed API overview: users define contexts, stage structs, and grouped dispatch metadata, and `RayTracer` lowers to target-specific dispatch mechanisms.* ### 2.2 Detailed Component Descriptions @@ -334,13 +340,13 @@ This gives the compiler a source-level relationship: - Every `HitGroup` in `PrimaryProgram.HitGroups` is constrained to that trace context. - Any-hit and intersection stage structs receive input types derived from the same context. -Figure 5 shows how the `TraceProgram` context connects the trace call to the grouped stage +Figure 6 shows how the `TraceProgram` context connects the trace call to the grouped stage structs. ![Context connects ray tracer and hit shaders](figures/041-ray-tracing-api/context-reachability.svg) -*Figure 5. Context reachability contract: the `TraceProgram` connects `RayTracer`, the trace-wide context, hit groups, and stage input types so the compiler has a source-visible relationship.* +*Figure 6. Context reachability contract: the `TraceProgram` connects `RayTracer`, the trace-wide context, hit groups, and stage input types so the compiler has a source-visible relationship.* This does not prove that arbitrary host data is correct. If the host builds an SBT or Metal function table that violates the reflected `TraceProgram`, the program can still be wrong. The diff --git a/proposals/figures/041-ray-tracing-api/native-metal-tag-validation.svg b/proposals/figures/041-ray-tracing-api/native-metal-tag-validation.svg new file mode 100644 index 0000000..dccbeb3 --- /dev/null +++ b/proposals/figures/041-ray-tracing-api/native-metal-tag-validation.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + Native Metal: explicit tags make pipeline validation possible + The shader author writes tags on both intersectors and custom intersection functions. Host bindings are validated when the pipeline is built. + + + Metal shader source + intersector<TagA> tracerA; + intersector<TagB> tracerB; + + + Tagged intersection functions + [[intersection(TagA)]] fA() + [[intersection(TagB)]] fB() + [[intersection(TagB)]] fC() + + + Host bindings + tracerA slot 0 -> fA + tracerA slot 1 -> fB + slot 1 is a mismatch + + + Pipeline build + sees tracer tags, + function tags, + and bindings + + + + + + + Late but detectable: + pipeline build can reject tracerA -> fB because both sides already carry concrete Metal tags. + + diff --git a/proposals/figures/041-ray-tracing-api/slang-tag-synthesis-gap.svg b/proposals/figures/041-ray-tracing-api/slang-tag-synthesis-gap.svg new file mode 100644 index 0000000..23bd524 --- /dev/null +++ b/proposals/figures/041-ray-tracing-api/slang-tag-synthesis-gap.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + Slang: host-owned reachability leaves tag synthesis underdetermined + Slang can see trace sites and stage entry points, but existing shader source does not contain the host binding edges that connect them. + + + Ray-generation code + intersector1.intersect(...) + needs Metal tag set A + intersector2.intersect(...) + needs Metal tag set B + + + Independent stage entries + AnyHit1() + AnyHit2() + AnyHit3() + Intersection1() + Intersection2() + + + Slang Metal codegen + must emit: + [[intersection(?)]] + generatedAnyHit() + Which tag set? + + + Host bindings + slot tables decide: + A -> Intersection2 + B -> AnyHit1 + Available later + + + + + too late + + + Core problem: + Slang must synthesize Metal tags before host binding data reveals reachability. + Without a source-level reachability contract, it cannot know whether each generated + AnyHit/Intersection function needs tag set A, tag set B, or another tag set. + + + + + diff --git a/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg b/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg deleted file mode 100644 index 3bad69f..0000000 --- a/proposals/figures/041-ray-tracing-api/tag-list-reachability.svg +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - Metal tag-list reachability problem - Metal custom intersection functions are tagged in source. Slang must synthesize those tags, - but old-style pipeline shaders do not say which trace can reach which AnyHit or Intersection entry point. - - - Native Metal: tags are explicit, so pipeline-build validation has enough information - - - Metal shader source - intersector<TagA> tracerA; - intersector<TagB> tracerB; - - - Tagged intersection functions - [[intersection(TagA)]] fA() - [[intersection(TagB)]] fB() - [[intersection(TagB)]] fC() - - - Host bindings - tracerA slot 0 -> fA - tracerA slot 1 -> fB - mismatch - - - Pipeline build - sees tracer tags, - function tags, - and bindings - - - - - - - Mismatch is late but detectable: - pipeline build can reject tracerA -> fB because both sides already carry concrete Metal tags. - - - Existing Slang model: the compiler must emit Metal tags before host bindings are known - - - Ray-generation code - intersector1.intersect(...) - needs Metal tag set A - intersector2.intersect(...) - needs Metal tag set B - - - Independent stage entries - AnyHit1() - AnyHit2() - AnyHit3() - Intersection1() - Intersection2() - - - Slang Metal codegen - must emit: - [[intersection(?)]] - generatedAnyHit() - Which tag set? - - - Host bindings - slot tables decide: - A -> Intersection2 - B -> AnyHit1 - Available later - - - - - too late - - - Core problem: - Slang sees trace sites and stage entry points, but not the host binding edges that connect them. - Without a source-level reachability contract, it cannot know whether each generated - AnyHit/Intersection function needs tag set A, tag set B, or another tag set. - - - - - From bce24597b52a6064e70adfe679381cd8e06153eb Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 13:00:40 -0500 Subject: [PATCH 11/21] Show TraceProgram reflection in API overview --- proposals/041-ray-tracing-api.md | 7 +- .../041-ray-tracing-api/api-overview.svg | 116 +++++++++++++----- 2 files changed, 87 insertions(+), 36 deletions(-) diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md index 2e11e89..03006dc 100644 --- a/proposals/041-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -115,14 +115,15 @@ The proposed API asks shader authors to describe a trace program in source: 4. Use `rt::RayTracer` and `rt::RayDispatch` from ray-generation code. The group declarations are the source-level conceptual SBT. They are visible to the compiler and -to reflection. D3D and Vulkan use that information to build or validate native SBT records. Metal -uses it to generate the post-trace dispatch switch. Figure 5 gives a high-level view of the API +to reflection. Host code can query `TraceProgram` reflection to build D3D/Vulkan SBT records or +Metal function tables/function buffers from the same grouping contract, so the grouping logic does +not need to be duplicated outside shader source. Figure 5 gives a high-level view of this API shape. ![API overview](figures/041-ray-tracing-api/api-overview.svg) -*Figure 5. Proposed API overview: users define contexts, stage structs, and grouped dispatch metadata, and `RayTracer` lowers to target-specific dispatch mechanisms.* +*Figure 5. Proposed API overview: users define contexts, stage structs, and grouped dispatch metadata in `TraceProgram`; host code uses reflection of that same `TraceProgram` to build D3D/Vulkan SBT records and Metal function tables/function buffers.* ### 2.2 Detailed Component Descriptions diff --git a/proposals/figures/041-ray-tracing-api/api-overview.svg b/proposals/figures/041-ray-tracing-api/api-overview.svg index 0edbfda..14bbe44 100644 --- a/proposals/figures/041-ray-tracing-api/api-overview.svg +++ b/proposals/figures/041-ray-tracing-api/api-overview.svg @@ -1,49 +1,99 @@ - + - + + + + + + + + + + - Proposed Slang source structure + Proposed API overview: grouping logic moves into shader source + + + Trace context + payload, AS kind, tags + + + Stage structs + Miss / ClosestHit + AnyHit / Intersection + + + TraceProgram + TraceContext + MissGroupList<slots...> + HitGroupList<slots...> + source-level grouping contract + + + + + + Slang reflection + groups, slots, contexts + stage entry symbols + target-specific metadata + + + query once + from shader source - - Trace context - payload, AS kind, tags + + D3D / Vulkan host + build native SBT + miss records + hit groups + from reflected slots - - Stage structs - operator() implements logic + + Metal host + build function table + or function buffer + from reflected slots - - TraceProgram - MissGroupList<...> - HitGroupList<Slot0, Slot1, ...> - reflection-friendly conceptual SBT + + reflection data + + reflection data - - D3D / Vulkan - native SBT and pipeline stages + + RayTracer<TraceProgram>.trace - - Metal - generated post-trace dispatch + + Target lowering + D3D/Vulkan: native trace + Metal: post-trace dispatch - - RayTracer<TraceProgram>.trace + + - - - - - + + No duplicated grouping logic + Change the shader-level + TraceProgram; host reads it. From bdc10e63479e2df9423c8f7e9cd54471a18dd9a4 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 13:06:03 -0500 Subject: [PATCH 12/21] Adjust API overview layout --- .../041-ray-tracing-api/api-overview.svg | 101 +++++++++--------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/proposals/figures/041-ray-tracing-api/api-overview.svg b/proposals/figures/041-ray-tracing-api/api-overview.svg index 14bbe44..5a7420d 100644 --- a/proposals/figures/041-ray-tracing-api/api-overview.svg +++ b/proposals/figures/041-ray-tracing-api/api-overview.svg @@ -35,65 +35,68 @@ Proposed API overview: grouping logic moves into shader source - - Trace context - payload, AS kind, tags + + Trace context + payload, AS kind, tags - - Stage structs - Miss / ClosestHit - AnyHit / Intersection + + Stage structs + Miss / ClosestHit + AnyHit / Intersection - - TraceProgram - TraceContext - MissGroupList<slots...> - HitGroupList<slots...> - source-level grouping contract + + TraceProgram + TraceContext + MissGroupList<slots...> + HitGroupList<slots...> + source-level grouping contract - - + + - - Slang reflection - groups, slots, contexts - stage entry symbols - target-specific metadata + + Slang reflection + groups, slots, contexts + stage entry symbols + target-specific metadata - - query once - from shader source + + query TraceProgram + reflection - - D3D / Vulkan host - build native SBT - miss records + hit groups - from reflected slots + + D3D / Vulkan host + build native SBT + miss records + hit groups + from reflected slots - - Metal host - build function table - or function buffer - from reflected slots + + Metal host + build function table + or function buffer + from reflected slots - - reflection data - - reflection data + + reflected + groups + + reflected + groups - - RayTracer<TraceProgram>.trace + + RayTracer<TraceProgram>.trace - - Target lowering - D3D/Vulkan: native trace - Metal: post-trace dispatch + + Target lowering + D3D/Vulkan: native trace + Metal: post-trace dispatch - - + + - - No duplicated grouping logic - Change the shader-level - TraceProgram; host reads it. + + No duplicated + grouping logic + Change TraceProgram; + host reads it. From 2de75dc3a1e0494d73fe69396b5c7d0860a0977e Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 13:12:23 -0500 Subject: [PATCH 13/21] Refine API overview figure spacing --- .../041-ray-tracing-api/api-overview.svg | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/proposals/figures/041-ray-tracing-api/api-overview.svg b/proposals/figures/041-ray-tracing-api/api-overview.svg index 5a7420d..a0d08fa 100644 --- a/proposals/figures/041-ray-tracing-api/api-overview.svg +++ b/proposals/figures/041-ray-tracing-api/api-overview.svg @@ -54,45 +54,45 @@ - - Slang reflection - groups, slots, contexts - stage entry symbols - target-specific metadata + + Slang reflection + groups, slots, contexts + stage entry symbols + target-specific metadata - - query TraceProgram - reflection + + query TraceProgram + reflection - - D3D / Vulkan host - build native SBT - miss records + hit groups - from reflected slots + + D3D / Vulkan host + build native SBT + miss records + hit groups + from reflected slots - - Metal host - build function table - or function buffer - from reflected slots + + Metal host + build function table + or function buffer + from reflected slots - - reflected - groups - - reflected - groups + + reflected + groups + + reflected + groups - RayTracer<TraceProgram>.trace + RayTracer<TraceProgram>.trace - - Target lowering - D3D/Vulkan: native trace - Metal: post-trace dispatch + + Target lowering + D3D/Vulkan: native trace + Metal: post-trace dispatch - + No duplicated From 2775858c4823479f03b90351dc2454ef99ef1875 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 8 Jun 2026 13:14:40 -0500 Subject: [PATCH 14/21] Move reflected groups labels above arrows --- proposals/figures/041-ray-tracing-api/api-overview.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/figures/041-ray-tracing-api/api-overview.svg b/proposals/figures/041-ray-tracing-api/api-overview.svg index a0d08fa..b6d00ab 100644 --- a/proposals/figures/041-ray-tracing-api/api-overview.svg +++ b/proposals/figures/041-ray-tracing-api/api-overview.svg @@ -77,11 +77,11 @@ from reflected slots - reflected - groups + reflected + groups - reflected - groups + reflected + groups RayTracer<TraceProgram>.trace From 876bc5e96567099c73a3878df45fc05a52852e5d Mon Sep 17 00:00:00 2001 From: kaizhang Date: Tue, 9 Jun 2026 12:11:23 -0500 Subject: [PATCH 15/21] Add proposal catalog --- proposals/041-ray-tracing-api.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md index 03006dc..5847c51 100644 --- a/proposals/041-ray-tracing-api.md +++ b/proposals/041-ray-tracing-api.md @@ -22,6 +22,23 @@ Scope Scope: pipeline ray tracing only. Inline ray tracing and ray queries are intentionally out of scope for this first design. +Catalog +------- + +- [1. Challenges Extending Current Slang Ray Tracing To Metal](#1-challenges-extending-current-slang-ray-tracing-to-metal) + - [1.1 Dispatch Model Gap](#11-dispatch-model-gap) + - [1.2 Metal Tag List And Reachability](#12-metal-tag-list-and-reachability) + - [1.3 Reserved Challenges](#13-reserved-challenges) +- [2. Proposed API Sketch](#2-proposed-api-sketch) + - [2.1 Overview](#21-overview) + - [2.2 Detailed Component Descriptions](#22-detailed-component-descriptions) + - [2.3 Writing Stages As Interface-Conforming Types](#23-writing-stages-as-interface-conforming-types) +- [3. Migration Examples](#3-migration-examples) + - [3.1 Migrating Existing Metal Code To The New API](#31-migrating-existing-metal-code-to-the-new-api) + - [3.2 Migrating Existing Slang D3D/Vulkan Ray Tracing Code](#32-migrating-existing-slang-d3dvulkan-ray-tracing-code) + - [3.3 Host Reflection Patterns](#33-host-reflection-patterns) +- [4. Open Design Questions](#4-open-design-questions) + ## 1. Challenges Extending Current Slang Ray Tracing To Metal ### 1.1 Dispatch Model Gap From 0159262de62099901671f068565c8a01b41c1871 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 15 Jun 2026 15:11:38 -0500 Subject: [PATCH 16/21] Update ray tracing API proposal --- proposals/041-ray-tracing-api.md | 725 -------------- proposals/042-ray-tracing-api.md | 901 ++++++++++++++++++ .../api-overview.svg | 14 +- .../binding-resource-comparison.svg | 168 ++++ .../context-reachability.svg | 6 +- .../dispatch-model-gap.svg | 0 .../native-metal-tag-validation.svg | 0 .../reachability-definition.svg | 0 .../slang-tag-synthesis-gap.svg | 0 9 files changed, 1079 insertions(+), 735 deletions(-) delete mode 100644 proposals/041-ray-tracing-api.md create mode 100644 proposals/042-ray-tracing-api.md rename proposals/figures/{041-ray-tracing-api => 042-ray-tracing-api}/api-overview.svg (91%) create mode 100644 proposals/figures/042-ray-tracing-api/binding-resource-comparison.svg rename proposals/figures/{041-ray-tracing-api => 042-ray-tracing-api}/context-reachability.svg (89%) rename proposals/figures/{041-ray-tracing-api => 042-ray-tracing-api}/dispatch-model-gap.svg (100%) rename proposals/figures/{041-ray-tracing-api => 042-ray-tracing-api}/native-metal-tag-validation.svg (100%) rename proposals/figures/{041-ray-tracing-api => 042-ray-tracing-api}/reachability-definition.svg (100%) rename proposals/figures/{041-ray-tracing-api => 042-ray-tracing-api}/slang-tag-synthesis-gap.svg (100%) diff --git a/proposals/041-ray-tracing-api.md b/proposals/041-ray-tracing-api.md deleted file mode 100644 index 5847c51..0000000 --- a/proposals/041-ray-tracing-api.md +++ /dev/null @@ -1,725 +0,0 @@ -SP #041: Pipeline Ray Tracing API Structural Dispatch -===================================================== - -This proposal sketches a new Slang ray tracing API that treats Metal as a first-class target -while preserving the D3D and Vulkan pipeline model. The central idea is to make the shader source -declare a conceptual shader binding table, or SBT, as structured Slang types. D3D and Vulkan can -continue to use the native host-created SBT. Metal can use the same structure to synthesize the -post-trace closest-hit and miss dispatch logic that Metal programmers normally write by hand. - -Status ------- - -Status: Draft Proposal. - -Author: - -Reviewer: - -Scope ------ - -Scope: pipeline ray tracing only. Inline ray tracing and ray queries are intentionally out of -scope for this first design. - -Catalog -------- - -- [1. Challenges Extending Current Slang Ray Tracing To Metal](#1-challenges-extending-current-slang-ray-tracing-to-metal) - - [1.1 Dispatch Model Gap](#11-dispatch-model-gap) - - [1.2 Metal Tag List And Reachability](#12-metal-tag-list-and-reachability) - - [1.3 Reserved Challenges](#13-reserved-challenges) -- [2. Proposed API Sketch](#2-proposed-api-sketch) - - [2.1 Overview](#21-overview) - - [2.2 Detailed Component Descriptions](#22-detailed-component-descriptions) - - [2.3 Writing Stages As Interface-Conforming Types](#23-writing-stages-as-interface-conforming-types) -- [3. Migration Examples](#3-migration-examples) - - [3.1 Migrating Existing Metal Code To The New API](#31-migrating-existing-metal-code-to-the-new-api) - - [3.2 Migrating Existing Slang D3D/Vulkan Ray Tracing Code](#32-migrating-existing-slang-d3dvulkan-ray-tracing-code) - - [3.3 Host Reflection Patterns](#33-host-reflection-patterns) -- [4. Open Design Questions](#4-open-design-questions) - -## 1. Challenges Extending Current Slang Ray Tracing To Metal - -### 1.1 Dispatch Model Gap - -D3D and Vulkan expose ray tracing as a pipeline-stage model. A trace call enters traversal, and -the driver or hardware uses host-created SBT records to select miss, any-hit, intersection, and -closest-hit shaders. The closest-hit function is not directly called from ray-generation source. - -Metal exposes a different model. `intersector.intersect(...)` returns an `intersection_result`. -AnyHit and custom Intersection behavior can still be dispatched during traversal through Metal's -function table or function buffer, but Miss and ClosestHit are ordinary post-trace shader logic -written by the user. - -Figure 1 shows the key mismatch: D3D/Vulkan assign stage dispatch to the host SBT and the -driver/hardware, while Metal assigns Miss and ClosestHit dispatch to shader code after -`intersect(...)` returns. A portable Slang API needs enough structure to synthesize that Metal -post-trace dispatch without changing the native D3D/Vulkan model. - - -![D3D and Vulkan SBT dispatch compared with Metal user-specified post-trace miss and closest-hit dispatch](figures/041-ray-tracing-api/dispatch-model-gap.svg) - -*Figure 1. Dispatch model gap: D3D and Vulkan select pipeline stages through host-created SBT records, while Metal requires user-written post-trace dispatch for Miss and ClosestHit.* - -### 1.2 Metal Tag List And Reachability - -This proposal uses the term **reachability** to describe the shader binding table entries that a -single trace call can access. The trace call supplies dispatch parameters, traversal contributes -geometry and instance information, and the target selects one of the SBT records. Those selectable -records are the entries reachable from that trace call. - -In existing D3D/Vulkan-style ray tracing models, this reachability is determined by host-created -binding data. The shader source contains the trace call, but the SBT records and the binding edges -from those records to AnyHit, Intersection, ClosestHit, and Miss shaders are provided by host code. -Therefore, as shown in Figure 2, the complete reachability set is not known from shader source at -ordinary compile time. - - -![Reachability is the set of SBT entries that one trace call can select](figures/041-ray-tracing-api/reachability-definition.svg) - -*Figure 2. Reachability definition: the reachable entries are the SBT records one trace call can select at runtime, but in the existing model that set is determined by host-created binding data.* - -Metal adds a second constraint: each custom intersection function reachable from an intersector -must have a compatible `[[intersection(...)]]` tag list. Native Metal can validate a mismatch at -pipeline build time because the user writes both the intersector tags and the function tags in -source. Figure 3 shows why this works: pipeline build sees the intersector tags, function tags, -and host bindings together. - - -![Native Metal can validate explicit intersector and custom intersection function tags at pipeline build time](figures/041-ray-tracing-api/native-metal-tag-validation.svg) - -*Figure 3. Native Metal tag validation: the user-authored tag lists give pipeline build enough information to reject incompatible host bindings.* - -Slang does not currently expose that Metal tag system. When lowering AnyHit or Intersection entry -points to Metal, Slang must synthesize `[[intersection(...)]]` tags for the generated Metal -functions. The compiler can see trace sites and stage entry points, but in the existing model it -cannot see the host binding edges that determine which stage entries are reachable from each trace -site. - -Figure 4 shows the information-flow problem. If two trace sites lower to different Metal tag -sets, and several AnyHit or Intersection entries may be bound by the host, the compiler cannot -know whether a generated function needs tag set A, tag set B, or another tag set. Emitting no tag, -or emitting a tag inferred from the wrong trace site, can make the generated Metal pipeline fail -to build. - - -![Slang cannot synthesize Metal intersection tags when host binding data owns reachability](figures/041-ray-tracing-api/slang-tag-synthesis-gap.svg) - -*Figure 4. Slang tag synthesis gap: Slang must emit Metal `[[intersection(...)]]` tags before host binding data reveals which AnyHit or Intersection entries are reachable from each trace site.* - -### 1.3 Reserved Challenges - -Other details remain important, but they are not the main shape of this proposal: - -- Metal has limitations on custom attributes returned from custom intersection functions. -- Metal has multiple `intersect(...)` overload families, including no-dispatch, function-table, - and function-buffer forms. -- Device user data should be modeled in a way that maps to non-pointer targets. - -Those issues can be handled after the dispatch and tag-reachability model is settled. - -## 2. Proposed API Sketch - -### 2.1 Overview - -The proposed API asks shader authors to describe a trace program in source: - -1. Define payload and context types. -2. Write miss, closest-hit, any-hit, and intersection logic as structs that implement Slang - interfaces. -3. Group those stage structs into slot-indexed hit and miss groups. -4. Use `rt::RayTracer` and `rt::RayDispatch` from ray-generation code. - -The group declarations are the source-level conceptual SBT. They are visible to the compiler and -to reflection. Host code can query `TraceProgram` reflection to build D3D/Vulkan SBT records or -Metal function tables/function buffers from the same grouping contract, so the grouping logic does -not need to be duplicated outside shader source. Figure 5 gives a high-level view of this API -shape. - - -![API overview](figures/041-ray-tracing-api/api-overview.svg) - -*Figure 5. Proposed API overview: users define contexts, stage structs, and grouped dispatch metadata in `TraceProgram`; host code uses reflection of that same `TraceProgram` to build D3D/Vulkan SBT records and Metal function tables/function buffers.* - -### 2.2 Detailed Component Descriptions - -#### 2.2.1 Resolving The Dispatch Model Gap With A Conceptual SBT - -The dispatch solution is to declare the SBT structure in shader source. At this layer, the key -concepts are only slots, groups, group lists, a trace program, and dispatch parameters. - -Simplified API shape: - -```slang -namespace rt -{ - public struct RayDispatch - { - public uint sbtOffset; - public uint sbtStride; - public uint missIndex; - } - - public interface IHitGroupSlot { static const int index; } - public struct HitGroupSlot : IHitGroupSlot - { - public static const int index = indexValue; - } - - public interface IMissSlot { static const int index; } - public struct MissSlot : IMissSlot - { - public static const int index = indexValue; - } - - public struct HitGroup { ... } - public struct MissGroup { ... } - - public struct HitGroupList { ... } - public struct MissGroupList { ... } - - public interface TraceProgram - { - associatedtype MissGroups; - associatedtype HitGroups; - } -} -``` - -The real draft API includes context constraints on these types. This section omits them because -the dispatch mechanism itself is about how slots are declared and reflected. - -The canonical slot calculation remains the D3D/Vulkan SBT calculation: - -```slang -public uint getHitGroupSlot( - RayDispatch dispatch, - uint geometryContribution, - uint instanceContribution) -{ - return instanceContribution + geometryContribution * dispatch.sbtStride + - dispatch.sbtOffset; -} -``` - -For D3D and Vulkan, this API is mostly descriptive: - -- `RayDispatch.sbtOffset` maps to D3D `RayContributionToHitGroupIndex` and Vulkan - `sbtRecordOffset`. -- `RayDispatch.sbtStride` maps to D3D `MultiplierForGeometryContributionToHitGroupIndex` and - Vulkan `sbtRecordStride`. -- `RayDispatch.missIndex` maps to the native miss shader index. -- The native driver or hardware still performs the SBT lookup. - -For Metal, this API becomes executable structure: - -- Slang lowers `RayTracer.trace(...)` to `intersector.intersect(...)`. -- Slang uses `RayDispatch.missIndex` to synthesize miss dispatch. -- Slang uses `getHitGroupSlot(...)` to synthesize closest-hit dispatch. -- Slang uses the same hit-group slots to validate and emit Metal function-table or - function-buffer entries for any-hit and custom intersection functions. - -Conceptual Metal lowering: - -```slang -let result = metalIntersector.intersect(desc.ray, scene, functionBuffer, payload); - -if (!result.isNone) -{ - uint geometryContribution = __rtGetGeometryContribution(result); - uint instanceContribution = __rtGetInstanceContribution(result); - uint slot = rt::getHitGroupSlot(dispatch, geometryContribution, instanceContribution); - - switch (slot) - { - case 0: - PrimaryTriangleClosestHit()(makeClosestHitInput(...)); - break; - case 1: - PrimaryCurveClosestHit()(makeClosestHitInput(...)); - break; - case 2: - PrimarySphereClosestHit()(makeClosestHitInput(...)); - break; - } -} -``` - -The important property is that Metal closest-hit dispatch is not invented independently. It is -generated from the same conceptual SBT slots that the host uses for D3D and Vulkan. - -Alternative considered: require users to write a structural Metal-like dispatch function. - -```slang -struct PrimaryClosestHitDispatcher -{ - void operator()(rt::ClosestHitInput input) - { - switch (input.geometryIndex) - { - case 0: shadeOpaqueTriangle(input); break; - case 1: shadeCurve(input); break; - case 2: shadeSphere(input); break; - } - } -} -``` - -Slang could analyze this user-written dispatch logic and reflect enough metadata for D3D/Vulkan -host code to reconstruct the SBT. This is closer to the normal Metal mental model, but it is a -harder compiler problem: - -- The compiler must recognize and preserve the user's dispatch structure. -- Reflection depends on successful control-flow analysis. -- Dynamic dispatch tables, buffer lookups, and helper functions complicate extraction. -- Small shader-code changes could affect host reflection in surprising ways. - -The proposed design chooses the reverse direction: users declare the SBT structure directly, then -Slang synthesizes Metal dispatch. This is simpler to validate and easier to reflect. - -#### 2.2.2 Resolving The Metal Tag-List Issue With Context Types - -The dispatch table solves the question: "which slot contains which shaders?" The tag-list issue -also needs a way to answer: "which trace object, hit shader, and intersection function share the -same semantic requirements?" - -The proposed answer is a context type. A trace context defines the properties of a trace family: - -```slang -interface ITraceContext -{ - associatedtype Payload; - associatedtype ASKind; - associatedtype Motion; - static const RayDataTags dataTags; - static const int maxLevels; -} -``` - -A hit context can specialize the trace context with a primitive kind: - -```slang -interface IHitContextFor -{ - associatedtype TraceContext; - associatedtype Primitive; -} -``` - -A trace program connects the ray tracer and every grouped shader through the same trace context: - -```slang -struct PrimaryTraceContext : rt::ITraceContext -{ - typealias Payload = RadiancePayload; - typealias ASKind = rt::InstanceAS; - typealias Motion = rt::NoMotion; - static const rt::RayDataTags dataTags = - rt::RayDataTags::Instancing | rt::RayDataTags::TriangleData; - static const int maxLevels = 0; -} - -struct PrimaryTriangleContext : rt::IHitContextFor -{ - typealias TraceContext = PrimaryTraceContext; - typealias Primitive = rt::TrianglePrimitive; -} - -struct PrimaryProgram : rt::TraceProgram -{ - typealias TraceContext = PrimaryTraceContext; - - typealias HitGroups = rt::HitGroupList< - TraceContext, - rt::HitGroup< - TraceContext, - rt::HitGroupSlot<0>, - PrimaryTriangleContext, - PrimaryTriangleClosestHit, - PrimaryTriangleAnyHit, - rt::NoAttributes, - rt::NoIntersection>>; -} - -[shader("raygeneration")] -void rayGen() -{ - RadiancePayload payload; - rt::RayTracer tracer; - tracer.trace(desc, scene, dispatch, payload); -} -``` - -This gives the compiler a source-level relationship: - -- `RayTracer` identifies one `TraceProgram`. -- `PrimaryProgram.TraceContext` defines the trace-wide Metal tags. -- Every `HitGroup` in `PrimaryProgram.HitGroups` is constrained to that trace context. -- Any-hit and intersection stage structs receive input types derived from the same context. - -Figure 6 shows how the `TraceProgram` context connects the trace call to the grouped stage -structs. - - -![Context connects ray tracer and hit shaders](figures/041-ray-tracing-api/context-reachability.svg) - -*Figure 6. Context reachability contract: the `TraceProgram` connects `RayTracer`, the trace-wide context, hit groups, and stage input types so the compiler has a source-visible relationship.* - -This does not prove that arbitrary host data is correct. If the host builds an SBT or Metal -function table that violates the reflected `TraceProgram`, the program can still be wrong. The -goal is to make the shader-side contract explicit enough that: - -- Slang can generate Metal tags for reachable custom intersection functions. -- Slang can reject shader declarations that are inconsistent inside the `TraceProgram`. -- Reflection can expose the expected table to host code. -- Validation layers or Slang runtime helpers can compare host records against the reflected - contract. - -### 2.3 Writing Stages As Interface-Conforming Types - -In the new model, miss, closest-hit, any-hit, and intersection logic are not written as independent -entry points. They are written as ordinary structs that conform to built-in stage interfaces. - -Example: - -```slang -struct PrimaryTriangleClosestHit - : rt::IClosestHitShader -{ - void operator()(rt::ClosestHitInput input) - { - input.payload.color = float4(input.distance, 0.0, 0.0, 1.0); - } -} - -struct PrimaryTriangleAnyHit - : rt::IAnyHitShader -{ - void operator()(rt::AnyHitInput input) - { - if (isTransparent(input.triangle)) - input.ignoreHit(); - } -} - -struct PrimarySphereIntersection - : rt::IIntersectionShader -{ - rt::IntersectionReturn - operator()(rt::IntersectionInput input) - { - SphereHitAttributes attr; - float t = intersectSphere(input, attr); - return rt::IntersectionReturn::accept(t, attr); - } -} -``` - -The compiler is responsible for lowering these structs to the target form: - -- D3D and Vulkan: generated native entry points and hit groups, connected to SBT records. -- Metal: generated intersection functions for any-hit/custom-intersection behavior, plus a - generated post-trace closest-hit dispatch switch. - -The user writes one source-level model. The target backend chooses the appropriate pipeline shape. - -## 3. Migration Examples - -### 3.1 Migrating Existing Metal Code To The New API - -Existing Metal users often write post-trace logic directly: - -```metal -kernel void rayGen(...) -{ - intersector tracer; - tracer.assume_geometry_type(geometry_type::triangle); - - auto result = tracer.intersect(ray, scene, intersectionFunctionBuffer, payload); - - if (result.type == intersection_type::none) - { - miss(payload); - } - else - { - uint slot = result.geometry_id; - - switch (slot) - { - case 0: shadeOpaqueTriangle(payload, result); break; - case 1: shadeAlphaTriangle(payload, result); break; - case 2: shadeProceduralSphere(payload, result); break; - } - } -} -``` - -With the proposed API, the user moves the manually dispatched operations into stage structs and -declares the dispatch table structurally: - -```slang -struct PrimaryProgram : rt::TraceProgram -{ - typealias TraceContext = PrimaryTraceContext; - - typealias MissGroups = rt::MissGroupList< - TraceContext, - rt::MissGroup, PrimaryMiss>>; - - typealias HitGroups = rt::HitGroupList< - TraceContext, - rt::HitGroup< - TraceContext, - rt::HitGroupSlot<0>, - PrimaryTriangleContext, - PrimaryOpaqueTriangleClosestHit, - rt::NoAnyHit, - rt::NoAttributes, - rt::NoIntersection>, - rt::HitGroup< - TraceContext, - rt::HitGroupSlot<1>, - PrimaryTriangleContext, - PrimaryAlphaTriangleClosestHit, - PrimaryAlphaTriangleAnyHit, - rt::NoAttributes, - rt::NoIntersection>, - rt::HitGroup< - TraceContext, - rt::HitGroupSlot<2>, - PrimarySphereContext, - PrimarySphereClosestHit, - rt::NoAnyHit, - SphereHitAttributes, - PrimarySphereIntersection>>; -} -``` - -Ray-generation code becomes: - -```slang -[shader("raygeneration")] -void rayGen() -{ - RadiancePayload payload; - - rt::RayDispatch dispatch; - dispatch.sbtOffset = 0; - dispatch.sbtStride = 3; - dispatch.missIndex = 0; - - rt::RayTracer tracer; - tracer.trace(desc, scene, dispatch, payload); -} -``` - -For Metal, Slang generates code that is equivalent to the user's old post-trace dispatch, but the -source of truth is now `PrimaryProgram.HitGroups` and `PrimaryProgram.MissGroups`. - -Metal host migration: - -1. Query `PrimaryProgram` through Slang reflection. -2. For each hit group slot, discover its any-hit and intersection stage structs. -3. Build the Metal intersection function buffer or function table using the reflected slot - mapping. -4. For function-buffer dispatch, configure Metal's index calculation consistently with - `RayDispatch`: - -```metal -intersector.set_geometry_multiplier(dispatch.sbtStride); -intersector.set_base_id(dispatch.sbtOffset); -``` - -This keeps Metal's any-hit and custom-intersection dispatch aligned with Slang's generated -closest-hit dispatch. - -### 3.2 Migrating Existing Slang D3D/Vulkan Ray Tracing Code - -Existing Slang code usually has independent pipeline entry points: - -```slang -[shader("raygeneration")] -void rayGen() -{ - RadiancePayload payload; - - TraceRay( - scene, - flags, - instanceMask, - rayContributionToHitGroupIndex, - multiplierForGeometryContributionToHitGroupIndex, - missShaderIndex, - ray, - payload); -} - -[shader("miss")] -void miss(inout RadiancePayload payload) -{ - payload.color = backgroundColor; -} - -[shader("closesthit")] -void closestHit(inout RadiancePayload payload, BuiltInTriangleIntersectionAttributes attr) -{ - payload.color = shadeTriangle(attr); -} -``` - -The migrated shader keeps the same conceptual data, but moves stage bodies into typed structs: - -```slang -struct PrimaryMiss : rt::IMissShader -{ - void operator()(rt::MissInput input) - { - input.payload.color = backgroundColor; - } -} - -struct PrimaryTriangleClosestHit - : rt::IClosestHitShader -{ - void operator()(rt::ClosestHitInput input) - { - input.payload.color = shadeTriangle(input.triangle); - } -} -``` - -The old trace parameters map directly to `RayDispatch`: - -```slang -rt::RayDispatch dispatch; -dispatch.sbtOffset = rayContributionToHitGroupIndex; -dispatch.sbtStride = multiplierForGeometryContributionToHitGroupIndex; -dispatch.missIndex = missShaderIndex; - -rt::RayTracer tracer; -tracer.trace(desc, scene, dispatch, payload); -``` - -D3D/Vulkan host migration: - -1. Query `PrimaryProgram` through Slang reflection. -2. For each reflected miss group, add a miss record at `MissSlot.index`. -3. For each reflected hit group, add a hit group record at `HitGroupSlot.index`. -4. Use the same application data that previously produced `rayContributionToHitGroupIndex`, - `multiplierForGeometryContributionToHitGroupIndex`, and `missShaderIndex`. - -The native SBT model is not replaced. The new shader declarations make the intended SBT layout -visible to Slang, which enables Metal lowering and gives host code a single reflected contract. - -### 3.3 Host Reflection Patterns - -The reflection API shape is not finalized. The expected information is: - -```cpp -struct ReflectedTraceProgram -{ - TypeReflection* traceContextType; - List missGroups; - List hitGroups; -}; - -struct ReflectedMissGroup -{ - int slot; - EntryPointReflection* generatedMissEntryPoint; -}; - -struct ReflectedHitGroup -{ - int slot; - TypeReflection* hitContextType; - EntryPointReflection* generatedClosestHitEntryPoint; - EntryPointReflection* generatedAnyHitEntryPoint; - EntryPointReflection* generatedIntersectionEntryPoint; -}; -``` - -Pattern A: D3D/Vulkan native SBT. - -```cpp -auto program = reflection->findTraceProgram("PrimaryProgram"); - -for (auto miss : program.missGroups) -{ - sbt.setMissRecord(miss.slot, miss.generatedMissEntryPoint); -} - -for (auto hit : program.hitGroups) -{ - sbt.setHitGroup( - hit.slot, - hit.generatedClosestHitEntryPoint, - hit.generatedAnyHitEntryPoint, - hit.generatedIntersectionEntryPoint); -} -``` - -The application still controls geometry contribution, instance contribution, stride, and offset. -The reflected slots tell the application which shader group belongs at each slot. - -Pattern B: Metal intersection function buffer. - -```cpp -auto program = reflection->findTraceProgram("PrimaryProgram"); - -for (auto hit : program.hitGroups) -{ - if (hit.generatedAnyHitEntryPoint || hit.generatedIntersectionEntryPoint) - { - functionBuffer.setFunction( - hit.slot, - hit.generatedIntersectionOrAnyHitFunction); - } -} -``` - -The generated Metal ray-generation code uses the same slot to dispatch closest-hit after -`intersect(...)`. The host does not need to provide a separate closest-hit dispatch table for -Metal because closest-hit dispatch is synthesized by Slang. - -Pattern C: Metal intersection function table. - -Metal's ordinary function table path is less flexible than the function-buffer path because -shader code cannot set the same base-id and geometry-multiplier values in the same way. It can -still be supported for restricted layouts, for example: - -- the acceleration structure already contains the expected function-table offsets, -- the shader uses a fixed `RayDispatch` layout, -- or the backend can prove that the table index matches the reflected hit-group slot. - -Function-buffer lowering is the preferred general path for matching D3D/Vulkan SBT index -calculation. - -Pattern D: Manual host construction without reflection. - -A developer can still build the table by reading the shader source if the project chooses a fixed -layout convention: - -```slang -typealias HitGroups = rt::HitGroupList< - TraceContext, - rt::HitGroup, ...>, - rt::HitGroup, ...>, - rt::HitGroup, ...>>; -``` - -Reflection is strongly preferred because it removes duplicated source-of-truth in host code and -enables validation, but the layout is intentionally visible and reviewable in shader source. - -## 4. Open Design Questions - -- Exact reflection API names and ownership model. -- Exact generated entry-point naming rules for D3D and Vulkan. -- Whether Metal ordinary function-table lowering should be a restricted feature or a fully - supported path. -- How to represent custom intersection attributes on Metal when native return-value limits are - insufficient. -- How much runtime validation Slang should provide between reflected `TraceProgram` data and - host-created SBT or Metal function-buffer state. diff --git a/proposals/042-ray-tracing-api.md b/proposals/042-ray-tracing-api.md new file mode 100644 index 0000000..b6bdfa7 --- /dev/null +++ b/proposals/042-ray-tracing-api.md @@ -0,0 +1,901 @@ +SP #042: Pipeline Ray Tracing API Structural Dispatch +===================================================== + +Status +------ + +Status: Draft Proposal. + +Author: + +Reviewer: + +Scope +----- + +Scope: pipeline ray tracing only. Inline ray tracing and ray queries are intentionally out of +scope for this first design. + +This proposal sketches a new Slang ray tracing API that treats Metal as a first-class target +while preserving the D3D and Vulkan pipeline model. The central idea is to make the shader source +declare a conceptual shader binding table, or SBT, as structured Slang types. D3D and Vulkan can +continue to use the native host-created SBT. Metal can use the same structure to synthesize the +post-trace closest-hit and miss dispatch logic that Metal programmers normally write by hand. + +The prototype API sketch is being iterated in the Slang implementation repository under: + +- `docs/design/rt-api-workspace/design-static-dispatch-table/rt_static_dispatch_extend.slang` +- `docs/design/rt-api-workspace/design-static-dispatch-table/rt_static_dispatch_table_extend.slang` +- `docs/design/rt-api-workspace/design-static-dispatch-table/rt_static_dispathc_table_example_extend.slang` + +Catalog +------- + +- [1. Challenges Extending Current Slang Ray Tracing To Metal](#1-challenges-extending-current-slang-ray-tracing-to-metal) + - [1.1 Dispatch Model Gap](#11-dispatch-model-gap) + - [1.2 Metal Function Table And Function Buffer Resource Mismatch](#12-metal-function-table-and-function-buffer-resource-mismatch) + - [1.3 Metal Tag List And Reachability](#13-metal-tag-list-and-reachability) + - [1.4 Reserved Challenges](#14-reserved-challenges) +- [2. Proposed API Sketch](#2-proposed-api-sketch) + - [2.1 Overview](#21-overview) + - [2.2 Detailed Component Descriptions](#22-detailed-component-descriptions) + - [2.2.1 Resolving The Dispatch Model Gap With A Conceptual SBT](#221-resolving-the-dispatch-model-gap-with-a-conceptual-sbt) + - [2.2.2 Resolving The Metal Function Table And Function Buffer Resource Mismatch With TraceProgramDescriptor](#222-resolving-the-metal-function-table-and-function-buffer-resource-mismatch-with-traceprogramdescriptor) + - [2.2.3 Resolving The Metal Tag-List Issue With Context Types](#223-resolving-the-metal-tag-list-issue-with-context-types) + - [2.3 Writing Stages As Interface-Conforming Types](#23-writing-stages-as-interface-conforming-types) +- [3. Migration Examples](#3-migration-examples) + - [3.1 Migrating Existing Metal Code To The New API](#31-migrating-existing-metal-code-to-the-new-api) + - [3.2 Migrating Existing Slang D3D/Vulkan Ray Tracing Code](#32-migrating-existing-slang-d3dvulkan-ray-tracing-code) + - [3.3 Host Reflection Patterns](#33-host-reflection-patterns) +- [4. Open Design Questions](#4-open-design-questions) + +## 1. Challenges Extending Current Slang Ray Tracing To Metal + +### 1.1 Dispatch Model Gap + +D3D and Vulkan expose ray tracing as a pipeline-stage model. A trace call enters traversal, and +the driver or hardware uses host-created SBT records to select miss, any-hit, intersection, and +closest-hit shaders. The closest-hit function is not directly called from ray-generation source. + +Metal exposes a different model. `intersector.intersect(...)` returns an `intersection_result`. +AnyHit and custom Intersection behavior can still be dispatched during traversal through Metal's +function table or function buffer, but Miss and ClosestHit are ordinary post-trace shader logic +written by the user. + +Figure 1 shows the key mismatch: D3D/Vulkan assign stage dispatch to the host SBT and the +driver/hardware, while Metal assigns Miss and ClosestHit dispatch to shader code after +`intersect(...)` returns. A portable Slang API needs enough structure to synthesize that Metal +post-trace dispatch without changing the native D3D/Vulkan model. + + +![D3D and Vulkan SBT dispatch compared with Metal user-specified post-trace miss and closest-hit dispatch](figures/042-ray-tracing-api/dispatch-model-gap.svg) + +*Figure 1. Dispatch model gap: D3D and Vulkan select pipeline stages through host-created SBT records, while Metal requires user-written post-trace dispatch for Miss and ClosestHit.* + +### 1.2 Metal Function Table And Function Buffer Resource Mismatch + +Metal introduces `intersection_function_table` and `intersection_function_buffer` resource types +that are visible to shader code and must be bound from host code when traversal needs custom +intersection behavior. This is different from the D3D/Vulkan SBT model. The SBT is built by host +code, but it is not a shader-visible resource and shader code does not declare a binding point for +it. + +This creates an asymmetric programming model. Metal shader code may need a parameter that +represents a function table or function buffer. D3D/Vulkan shader code has no corresponding +parameter, even though host code still needs to build SBT records. A portable Slang API therefore +needs a way to describe this logical binding without forcing D3D/Vulkan targets to expose a fake +shader resource. + +### 1.3 Metal Tag List And Reachability + +This proposal uses the term **reachability** to describe the shader binding table entries that a +single trace call can access. The trace call supplies dispatch parameters, traversal contributes +geometry and instance information, and the target selects one of the SBT records. Those selectable +records are the entries reachable from that trace call. + +In existing D3D/Vulkan-style ray tracing models, this reachability is determined by host-created +binding data. The shader source contains the trace call, but the SBT records and the binding edges +from those records to AnyHit, Intersection, ClosestHit, and Miss shaders are provided by host code. +Therefore, as shown in Figure 2, the complete reachability set is not known from shader source at +ordinary compile time. + + +![Reachability is the set of SBT entries that one trace call can select](figures/042-ray-tracing-api/reachability-definition.svg) + +*Figure 2. Reachability definition: the reachable entries are the SBT records one trace call can select at runtime, but in the existing model that set is determined by host-created binding data.* + +Metal adds a second constraint: each custom intersection function reachable from an intersector +must have a compatible `[[intersection(...)]]` tag list. Native Metal can validate a mismatch at +pipeline build time because the user writes both the intersector tags and the function tags in +source. Figure 3 shows why this works: pipeline build sees the intersector tags, function tags, +and host bindings together. + + +![Native Metal can validate explicit intersector and custom intersection function tags at pipeline build time](figures/042-ray-tracing-api/native-metal-tag-validation.svg) + +*Figure 3. Native Metal tag validation: the user-authored tag lists give pipeline build enough information to reject incompatible host bindings.* + +Slang does not currently expose that Metal tag system. When lowering AnyHit or Intersection entry +points to Metal, Slang must synthesize `[[intersection(...)]]` tags for the generated Metal +functions. The compiler can see trace sites and stage entry points, but in the existing model it +cannot see the host binding edges that determine which stage entries are reachable from each trace +site. + +Figure 4 shows the information-flow problem. If two trace sites lower to different Metal tag +sets, and several AnyHit or Intersection entries may be bound by the host, the compiler cannot +know whether a generated function needs tag set A, tag set B, or another tag set. Emitting no tag, +or emitting a tag inferred from the wrong trace site, can make the generated Metal pipeline fail +to build. + + +![Slang cannot synthesize Metal intersection tags when host binding data owns reachability](figures/042-ray-tracing-api/slang-tag-synthesis-gap.svg) + +*Figure 4. Slang tag synthesis gap: Slang must emit Metal `[[intersection(...)]]` tags before host binding data reveals which AnyHit or Intersection entries are reachable from each trace site.* + +### 1.4 Reserved Challenges + +Other details remain important, but they are not the main shape of this proposal: + +- Metal has limitations on custom attributes returned from custom intersection functions. +- Metal has multiple `intersect(...)` overload families, including no-dispatch, function-table, + and function-buffer forms. +- Device user data should be modeled in a way that maps to non-pointer targets. + +Those issues can be handled after the dispatch and tag-reachability model is settled. + +## 2. Proposed API Sketch + +### 2.1 Overview + +The proposed API asks shader authors to describe a trace program in source: + +1. Define payload and context types. +2. Write miss, closest-hit, any-hit, and intersection logic as structs that implement Slang + interfaces. +3. Group those stage structs into ordered hit, miss, and callable group lists. +4. Use `rt::RayTracer`, `rt::RayTraversalDesc`, and + `rt::TraceProgramDescriptor` from ray-generation code. + +The group declarations are the source-level conceptual SBT. They are visible to the compiler and +to reflection. Host code can query `ITraceProgramLayout` reflection to build D3D/Vulkan SBT +records or Metal function tables/function buffers from the same grouping contract, so the grouping +logic does not need to be duplicated outside shader source. Figure 5 gives a high-level view of +this API shape. + + +![API overview](figures/042-ray-tracing-api/api-overview.svg) + +*Figure 5. Proposed API overview: users define contexts, stage structs, and grouped dispatch metadata in an `ITraceProgramLayout`; host code uses reflection of that same program layout to build D3D/Vulkan SBT records and Metal function tables/function buffers.* + +The running example uses the same prototype files listed above. The older structural-dispatch +sketches in this directory are retained as design history and are not the source shape described +by this proposal. + +### 2.2 Detailed Component Descriptions + +#### 2.2.1 Resolving The Dispatch Model Gap With A Conceptual SBT + +The dispatch solution is to declare the SBT structure in shader source. At this layer, the key +concepts are ordered groups, group lists, a trace program layout, a descriptor, and traversal +parameters. A slot is the zero-based position of a group in its corresponding list. + +Simplified API shape: + +```slang +namespace rt +{ + public struct RayTraversalDesc + { + public RayDesc ray; + public uint instanceMask; + public uint sbtOffset; + public uint sbtStride; + public uint missIndex; + } + + public interface IHitGroup + { + associatedtype Context : IHitGroupContext; + associatedtype ClosestHit : IClosestHitShader; + associatedtype AnyHit : IAnyHitShader; + associatedtype IntersectionAttributes; + associatedtype Intersection : IIntersectionShader; + } + + public interface IMissGroup + { + associatedtype Context : IMissGroupContext; + associatedtype Miss : IMissShader; + } + + public interface ICallableGroup { ... } + + public struct HitGroupList { ... } + public struct MissGroupList { ... } + public struct CallableGroupList { ... } + + public interface ITraceProgramLayout + { + associatedtype TraceContext : ITraceContext; + associatedtype MissGroups : IMissGroupList; + associatedtype HitGroups : IHitGroupList; + associatedtype CallableGroups : ICallableGroupList; + } + + public struct TraceProgramDescriptor + where ProgramLayout : ITraceProgramLayout + {} + + public struct RayTracer + where ProgramLayout : ITraceProgramLayout + { + public void trace( + RayTraversalDesc desc, + AccelerationStructure< + ProgramLayout.TraceContext.ASKind, + ProgramLayout.TraceContext.Motion> scene, + TraceProgramDescriptor descriptor, + inout ProgramLayout.TraceContext.Payload payload); + } +} +``` + +This simplified shape omits some details, but keeps the current naming and ownership model: +`ITraceProgramLayout` is the schema, `TraceProgramDescriptor` is the value-level +descriptor, and `RayTraversalDesc` carries per-trace SBT-style indices. + +The compiler-synthesized closest-hit slot calculation still follows the D3D/Vulkan SBT +calculation. This is not a user-facing API; it is shown only as the conceptual formula Slang will +generate for Metal: + +```slang +slot = instanceContribution + geometryContribution * desc.sbtStride + desc.sbtOffset; +``` + +For D3D and Vulkan, these fields are mostly descriptive: + +- `RayTraversalDesc.sbtOffset` maps to D3D `RayContributionToHitGroupIndex` and Vulkan + `sbtRecordOffset`. +- `RayTraversalDesc.sbtStride` maps to D3D `MultiplierForGeometryContributionToHitGroupIndex` and + Vulkan `sbtRecordStride`. +- `RayTraversalDesc.missIndex` maps to the native miss shader index. +- The native driver or hardware still performs the SBT lookup. + +For Metal, this API becomes executable structure: + +- Slang lowers `RayTracer.trace(...)` to `intersector.intersect(...)`. +- Slang uses `RayTraversalDesc.missIndex` to synthesize miss dispatch. +- Slang synthesizes the internal hit-group slot calculation to dispatch closest-hit. +- Slang uses the same hit-group list slots to validate and emit Metal function-table or + function-buffer entries for any-hit and custom intersection functions. + +Conceptual Metal lowering: + +```slang +let result = metalIntersector.intersect(desc.ray, scene, descriptor, payload); + +if (!result.isNone) +{ + uint geometryContribution = __rtGetGeometryContribution(result); + uint instanceContribution = __rtGetInstanceContribution(result); + uint slot = instanceContribution + geometryContribution * desc.sbtStride + desc.sbtOffset; + + switch (slot) + { + case 0: + PrimaryTriangleClosestHit().invoke(makeClosestHitInput(...)); + break; + case 1: + PrimaryCurveClosestHit().invoke(makeClosestHitInput(...)); + break; + case 2: + PrimarySphereClosestHit().invoke(makeClosestHitInput(...)); + break; + } +} +``` + +The important property is that Metal closest-hit dispatch is not invented independently. It is +generated from the same conceptual SBT slots that the host uses for D3D and Vulkan. + +Alternative considered: require users to write a structural Metal-like dispatch function. + +```slang +struct PrimaryClosestHitDispatcher +{ + void invoke(rt::ClosestHitInput input) + { + switch (input.geometryIndex) + { + case 0: shadeOpaqueTriangle(input); break; + case 1: shadeCurve(input); break; + case 2: shadeSphere(input); break; + } + } +} +``` + +Slang could analyze this user-written dispatch logic and reflect enough metadata for D3D/Vulkan +host code to reconstruct the SBT. This is closer to the normal Metal mental model, but it is a +harder compiler problem: + +- The compiler must recognize and preserve the user's dispatch structure. +- Reflection depends on successful control-flow analysis. +- Dynamic dispatch tables, buffer lookups, and helper functions complicate extraction. +- Small shader-code changes could affect host reflection in surprising ways. + +The proposed design chooses the reverse direction: users declare the SBT structure directly, then +Slang synthesizes Metal dispatch. This is simpler to validate and easier to reflect. + +#### 2.2.2 Resolving The Metal Function Table And Function Buffer Resource Mismatch With TraceProgramDescriptor + +Metal introduces `intersection_function_table` and `intersection_function_buffer` as resource +types that can be visible to shader code and bound from host code. D3D and Vulkan instead use an +SBT. The SBT is also built by host code, but it is not a shader-visible resource, so shader code +does not declare an SBT binding point. + +Before defining the Slang abstraction, it is useful to review the structure of these target +objects. Metal's `intersection_function_table` is a host-bound resource-object table containing +custom intersection function entries and resource bindings visible to those functions. Metal's +`intersection_function_buffer` is the buffer form of the same candidate-hit dispatch idea. In the +function-buffer path, the useful mental model is a pair of physical inputs: + +- a function-buffer table indexed by traversal, where each selected entry names one custom + intersection function; +- a user-data buffer that can carry records, generated slot maps, bindless resources, and callable + visible-function-table values. + +This section focuses on the `intersection_function_buffer_arguments` path because it is the more +general form for the proposed portable abstraction. Metal's `intersection_function_table` is a +special-case resource type relative to `intersection_function_buffer_arguments`; it follows the +same candidate-hit dispatch idea, and support for it can be added as a restricted lowering path +without changing the core `TraceProgramDescriptor` model. + +D3D and Vulkan do not expose an equivalent shader-visible function table or function buffer. +Their comparable structure is the host-created SBT. It is useful to view the SBT as one +host-side database with separate hit-group, miss, and callable sections. A hit-group record can +name ClosestHit, AnyHit, and Intersection shaders together, while the miss and callable sections +are one-dimensional lists. + +Figure 6 compares the two binding models. The Metal panel shows a shader-visible function buffer +and user-data buffer. The D3D/Vulkan panel shows the SBT as one host-side object with hit-group, +miss, and callable sections. + + +![Metal function buffer compared with D3D and Vulkan shader binding table](figures/042-ray-tracing-api/binding-resource-comparison.svg) + +*Figure 6. Binding resource comparison: (a) Metal uses a shader-visible function buffer plus user data; (b) D3D/Vulkan use one host-side SBT object with hit-group, miss, and callable sections, and no shader binding point.* + +The previous subsection already makes the two sides share the same conceptual layout: the shader +source declares a `ProgramLayout`, and host code can reflect that layout to build the target-side +records. The remaining mismatch is purely about binding. Metal needs a shader-visible resource +for the function table or function buffer path, while D3D/Vulkan need no corresponding shader +parameter. + +The proposed answer is to introduce an opaque resource type: + +```slang +struct TraceProgramDescriptor + where ProgramLayout : ITraceProgramLayout +{ +} +``` + +`TraceProgramDescriptor` is described by `ProgramLayout`, but it does not itself +declare the trace context. Its job is to represent the target-specific descriptor object derived from +the reflected program layout. + +On Metal, `TraceProgramDescriptor` lowers to the physical resources needed by the +selected Metal traversal path. For the general function-buffer path, that means a function buffer +plus generated user data, matching the Metal panel in Figure 6. Host code binds those physical Metal +resources to the opaque Slang descriptor and uses reflection of `ProgramLayout` to populate the +custom intersection functions, record data, generated slot maps, bindless resources, and callable +visible-function-table values. + +On D3D and Vulkan, `TraceProgramDescriptor` does not need to lower to a shader-visible +resource. The native SBT remains a host-side object, as shown in the D3D/Vulkan panel in Figure 6. +Host code still uses the same `ProgramLayout` reflection to build hit-group, miss, and callable +SBT records, but shader code does not receive a Metal-style function-table or function-buffer +object. + +#### 2.2.3 Resolving The Metal Tag-List Issue With Context Types + +With the descriptor abstraction separated, the tag-list issue still needs a way to answer: "which +trace object, hit shader, and intersection function share the same semantic requirements?" The +proposed answer is a context type. A trace context defines the properties of a trace family: + +```slang +interface ITraceContext +{ + associatedtype Payload; + associatedtype ASKind; + associatedtype Motion; + static const RayDataTags dataTags; + static const int maxLevels; +} +``` + +A hit group context can specialize the trace context with a primitive kind and a record type: + +```slang +interface IHitGroupContext +{ + associatedtype TraceContext : ITraceContext; + associatedtype Primitive : IIntersectionPrimitive; + associatedtype Record; +} +``` + +A trace program layout connects the ray tracer and every grouped shader through the same trace +context: + +```slang +struct PrimaryTraceContext : rt::ITraceContext +{ + typealias Payload = RadiancePayload; + typealias ASKind = rt::InstanceAS; + typealias Motion = rt::NoMotion; + static const rt::RayDataTags dataTags = rt::RayDataTags::Triangle; + static const int maxLevels = 0; +} + +struct PrimaryTriangleContext : rt::IHitGroupContext +{ + typealias TraceContext = PrimaryTraceContext; + typealias Primitive = rt::TrianglePrimitive; + typealias Record = PrimaryHitRecord; +} + +struct PrimaryMissContext : rt::IMissGroupContext +{ + typealias TraceContext = PrimaryTraceContext; + typealias Record = PrimaryMissRecord; +} + +struct PrimaryMissGroup : rt::IMissGroup +{ + typealias Context = PrimaryMissContext; + typealias Miss = PrimaryMiss; +} + +struct PrimaryTriangleGroup : rt::IHitGroup +{ + typealias Context = PrimaryTriangleContext; + typealias ClosestHit = PrimaryTriangleClosestHit; + typealias AnyHit = PrimaryTriangleAnyHit; + typealias IntersectionAttributes = rt::NoAttributes; + typealias Intersection = rt::NoIntersection; +} + +struct PrimaryTraceProgramLayout : rt::ITraceProgramLayout +{ + typealias TraceContext = PrimaryTraceContext; + + typealias MissGroups = rt::MissGroupList< + TraceContext, + PrimaryMissGroup>; + + typealias HitGroups = rt::HitGroupList< + TraceContext, + PrimaryTriangleGroup>; + + typealias CallableGroups = rt::NoCallableGroups; +} + +rt::TraceProgramDescriptor gPrimaryDescriptor; + +[shader("raygeneration")] +void rayGen() +{ + RadiancePayload payload; + rt::RayTracer tracer; + tracer.trace(desc, scene, gPrimaryDescriptor, payload); +} +``` + +This gives the compiler a source-level relationship: + +- `RayTracer` identifies one `ITraceProgramLayout`. +- `PrimaryTraceProgramLayout.TraceContext` defines the trace-wide Metal tags. +- Every group in `PrimaryTraceProgramLayout.HitGroups` is constrained to that trace context. +- Any-hit and intersection stage structs receive input types derived from the same context. + +Figure 7 shows how the trace program layout connects the trace call to the grouped stage structs. + + +![Context connects ray tracer and hit shaders](figures/042-ray-tracing-api/context-reachability.svg) + +*Figure 7. Context reachability contract: the trace program layout connects `RayTracer`, the trace-wide context, hit groups, and stage input types so the compiler has a source-visible relationship.* + +This does not prove that arbitrary host data is correct. If the host builds an SBT or Metal +function table that violates the reflected program layout, the program can still be wrong. The +goal is to make the shader-side contract explicit enough that: + +- Slang can generate Metal tags for reachable custom intersection functions. +- Slang can reject shader declarations that are inconsistent inside the program layout. +- Reflection can expose the expected table to host code. +- Validation layers or Slang runtime helpers can compare host records against the reflected + contract. + +### 2.3 Writing Stages As Interface-Conforming Types + +In the new model, miss, closest-hit, any-hit, and intersection logic are not written as independent +entry points. They are written as ordinary structs that conform to built-in stage interfaces. + +Example: + +```slang +struct PrimaryTriangleClosestHit + : rt::IClosestHitShader +{ + void invoke(rt::ClosestHitInput input) + { + input.payload.color = float4(input.distance, 0.0, 0.0, 1.0); + } +} + +struct PrimaryTriangleAnyHit + : rt::IAnyHitShader +{ + void invoke(rt::AnyHitInput input) + { + if (isTransparent(input.triangle)) + input.ignoreHit(); + } +} + +struct PrimarySphereIntersection + : rt::IIntersectionShader +{ + rt::IntersectionReturn + invoke(rt::IntersectionInput input) + { + SphereHitAttributes attr; + float t = intersectSphere(input, attr); + return rt::IntersectionReturn::accept(t, attr); + } +} +``` + +The compiler is responsible for lowering these structs to the target form: + +- D3D and Vulkan: generated native entry points and hit groups, connected to SBT records. +- Metal: generated intersection functions for any-hit/custom-intersection behavior, plus a + generated post-trace closest-hit dispatch switch. + +The user writes one source-level model. The target backend chooses the appropriate pipeline shape. + +## 3. Migration Examples + +### 3.1 Migrating Existing Metal Code To The New API + +Existing Metal users often write post-trace logic directly: + +```metal +kernel void rayGen(...) +{ + intersector tracer; + tracer.assume_geometry_type(geometry_type::triangle); + + auto result = tracer.intersect(ray, scene, intersectionFunctionBuffer, payload); + + if (result.type == intersection_type::none) + { + miss(payload); + } + else + { + uint slot = result.geometry_id; + + switch (slot) + { + case 0: shadeOpaqueTriangle(payload, result); break; + case 1: shadeAlphaTriangle(payload, result); break; + case 2: shadeProceduralSphere(payload, result); break; + } + } +} +``` + +With the proposed API, the user moves the manually dispatched operations into stage structs and +declares the trace program layout structurally. The list order defines the slots: + +```slang +struct PrimaryMissGroup : rt::IMissGroup +{ + typealias Context = PrimaryMissContext; + typealias Miss = PrimaryMiss; +} + +struct PrimaryOpaqueTriangleGroup : rt::IHitGroup +{ + typealias Context = PrimaryTriangleContext; + typealias ClosestHit = PrimaryOpaqueTriangleClosestHit; + typealias AnyHit = rt::NoAnyHit; + typealias IntersectionAttributes = rt::NoAttributes; + typealias Intersection = rt::NoIntersection; +} + +struct PrimaryAlphaTriangleGroup : rt::IHitGroup +{ + typealias Context = PrimaryTriangleContext; + typealias ClosestHit = PrimaryAlphaTriangleClosestHit; + typealias AnyHit = PrimaryAlphaTriangleAnyHit; + typealias IntersectionAttributes = rt::NoAttributes; + typealias Intersection = rt::NoIntersection; +} + +struct PrimarySphereGroup : rt::IHitGroup +{ + typealias Context = PrimarySphereContext; + typealias ClosestHit = PrimarySphereClosestHit; + typealias AnyHit = rt::NoAnyHit; + typealias IntersectionAttributes = SphereHitAttributes; + typealias Intersection = PrimarySphereIntersection; +} + +struct PrimaryTraceProgramLayout : rt::ITraceProgramLayout +{ + typealias TraceContext = PrimaryTraceContext; + + typealias MissGroups = rt::MissGroupList< + TraceContext, + PrimaryMissGroup>; // miss[0] + + typealias HitGroups = rt::HitGroupList< + TraceContext, + PrimaryOpaqueTriangleGroup, // hitGroup[0] + PrimaryAlphaTriangleGroup, // hitGroup[1] + PrimarySphereGroup>; // hitGroup[2] + + typealias CallableGroups = rt::NoCallableGroups; +} + +rt::TraceProgramDescriptor gPrimaryDescriptor; +``` + +Ray-generation code becomes: + +```slang +[shader("raygeneration")] +void rayGen() +{ + RadiancePayload payload; + + rt::RayTraversalDesc desc; + desc.ray = makeRay(); + desc.instanceMask = 0xff; + desc.sbtOffset = 0; + desc.sbtStride = 3; + desc.missIndex = 0; + + rt::RayTracer tracer; + tracer.trace(desc, scene, gPrimaryDescriptor, payload); +} +``` + +For Metal, Slang generates code that is equivalent to the user's old post-trace dispatch, but the +source of truth is now `PrimaryTraceProgramLayout.HitGroups` and +`PrimaryTraceProgramLayout.MissGroups`. + +Metal host migration: + +1. Query `PrimaryTraceProgramLayout` through Slang reflection. +2. For each hit group list position, discover its any-hit and intersection stage structs. +3. Build the Metal intersection function buffer or function table using the reflected slot + mapping. +4. For function-buffer dispatch, configure Metal's index calculation consistently with + `RayTraversalDesc`: + +```metal +intersector.set_geometry_multiplier(desc.sbtStride); +intersector.set_base_id(desc.sbtOffset); +``` + +This keeps Metal's any-hit and custom-intersection dispatch aligned with Slang's generated +closest-hit dispatch. + +### 3.2 Migrating Existing Slang D3D/Vulkan Ray Tracing Code + +Existing Slang code usually has independent pipeline entry points: + +```slang +[shader("raygeneration")] +void rayGen() +{ + RadiancePayload payload; + + TraceRay( + scene, + flags, + instanceMask, + rayContributionToHitGroupIndex, + multiplierForGeometryContributionToHitGroupIndex, + missShaderIndex, + ray, + payload); +} + +[shader("miss")] +void miss(inout RadiancePayload payload) +{ + payload.color = backgroundColor; +} + +[shader("closesthit")] +void closestHit(inout RadiancePayload payload, BuiltInTriangleIntersectionAttributes attr) +{ + payload.color = shadeTriangle(attr); +} +``` + +The migrated shader keeps the same conceptual data, but moves stage bodies into typed structs: + +```slang +struct PrimaryMissContext : rt::IMissGroupContext +{ + typealias TraceContext = PrimaryTraceContext; + typealias Record = PrimaryMissRecord; +} + +struct PrimaryMiss : rt::IMissShader +{ + void invoke(rt::MissInput input) + { + input.payload.color = backgroundColor; + } +} + +struct PrimaryTriangleClosestHit + : rt::IClosestHitShader +{ + void invoke(rt::ClosestHitInput input) + { + input.payload.color = shadeTriangle(input.triangle); + } +} +``` + +The old trace parameters map directly to fields in `RayTraversalDesc`: + +```slang +rt::RayTraversalDesc desc; +desc.ray = ray; +desc.instanceMask = instanceMask; +desc.sbtOffset = rayContributionToHitGroupIndex; +desc.sbtStride = multiplierForGeometryContributionToHitGroupIndex; +desc.missIndex = missShaderIndex; + +rt::RayTracer tracer; +tracer.trace(desc, scene, gPrimaryDescriptor, payload); +``` + +D3D/Vulkan host migration: + +1. Query `PrimaryTraceProgramLayout` through Slang reflection. +2. For each reflected miss group, add a miss record at its list position. +3. For each reflected hit group, add a hit group record at its list position. +4. Use the same application data that previously produced `rayContributionToHitGroupIndex`, + `multiplierForGeometryContributionToHitGroupIndex`, and `missShaderIndex`. + +The native SBT model is not replaced. The new shader declarations make the intended SBT layout +visible to Slang, which enables Metal lowering and gives host code a single reflected contract. + +### 3.3 Host Reflection Patterns + +The reflection API shape is not finalized. The expected information is: + +```cpp +struct ReflectedTraceProgramLayout +{ + TypeReflection* traceContextType; + List missGroups; + List hitGroups; + List callableGroups; +}; + +struct ReflectedMissGroup +{ + int slot; + EntryPointReflection* generatedMissEntryPoint; +}; + +struct ReflectedHitGroup +{ + int slot; + TypeReflection* hitContextType; + EntryPointReflection* generatedClosestHitEntryPoint; + EntryPointReflection* generatedAnyHitEntryPoint; + EntryPointReflection* generatedIntersectionEntryPoint; +}; + +struct ReflectedCallableGroup +{ + int slot; + TypeReflection* callableContextType; + EntryPointReflection* generatedCallableEntryPoint; +}; +``` + +Pattern A: D3D/Vulkan native SBT. + +```cpp +auto programLayout = reflection->findTraceProgramLayout("PrimaryTraceProgramLayout"); + +for (auto miss : programLayout.missGroups) +{ + sbt.setMissRecord(miss.slot, miss.generatedMissEntryPoint); +} + +for (auto hit : programLayout.hitGroups) +{ + sbt.setHitGroup( + hit.slot, + hit.generatedClosestHitEntryPoint, + hit.generatedAnyHitEntryPoint, + hit.generatedIntersectionEntryPoint); +} +``` + +The application still controls geometry contribution, instance contribution, stride, and offset. +The reflected slots tell the application which shader group belongs at each slot. + +Pattern B: Metal intersection function buffer. + +```cpp +auto programLayout = reflection->findTraceProgramLayout("PrimaryTraceProgramLayout"); + +for (auto hit : programLayout.hitGroups) +{ + if (hit.generatedAnyHitEntryPoint || hit.generatedIntersectionEntryPoint) + { + functionBuffer.setFunction( + hit.slot, + hit.generatedIntersectionOrAnyHitFunction); + } +} +``` + +The generated Metal ray-generation code uses the same slot to dispatch closest-hit after +`intersect(...)`. The host does not need to provide a separate closest-hit dispatch table for +Metal because closest-hit dispatch is synthesized by Slang. + +Pattern C: Metal intersection function table. + +Metal's ordinary function table path is less flexible than the function-buffer path because +shader code cannot set the same base-id and geometry-multiplier values in the same way. It can +still be supported for restricted layouts, for example: + +- the acceleration structure already contains the expected function-table offsets, +- the shader uses a fixed `RayTraversalDesc` layout, +- or the backend can prove that the table index matches the reflected hit-group slot. + +Function-buffer lowering is the preferred general path for matching D3D/Vulkan SBT index +calculation. + +Pattern D: Manual host construction without reflection. + +A developer can still build the table by reading the shader source if the project chooses a fixed +layout convention: + +```slang +typealias HitGroups = rt::HitGroupList< + TraceContext, + PrimaryOpaqueTriangleGroup, // hitGroup[0] + PrimaryAlphaTriangleGroup, // hitGroup[1] + PrimarySphereGroup>; // hitGroup[2] +``` + +Reflection is strongly preferred because it removes duplicated source-of-truth in host code and +enables validation, but the layout is intentionally visible and reviewable in shader source. + +## 4. Open Design Questions + +- Exact reflection API names and ownership model. +- Exact generated entry-point naming rules for D3D and Vulkan. +- Whether Metal ordinary function-table lowering should be a restricted feature or a fully + supported path. +- How to represent custom intersection attributes on Metal when native return-value limits are + insufficient. +- How much runtime validation Slang should provide between reflected `ITraceProgramLayout` data and + host-created SBT or Metal function-buffer state. diff --git a/proposals/figures/041-ray-tracing-api/api-overview.svg b/proposals/figures/042-ray-tracing-api/api-overview.svg similarity index 91% rename from proposals/figures/041-ray-tracing-api/api-overview.svg rename to proposals/figures/042-ray-tracing-api/api-overview.svg index b6d00ab..48dda4d 100644 --- a/proposals/figures/041-ray-tracing-api/api-overview.svg +++ b/proposals/figures/042-ray-tracing-api/api-overview.svg @@ -1,4 +1,4 @@ - + diff --git a/proposals/figures/042-ray-tracing-api/binding-resource-comparison.svg b/proposals/figures/042-ray-tracing-api/binding-resource-comparison.svg new file mode 100644 index 0000000..fd330b3 --- /dev/null +++ b/proposals/figures/042-ray-tracing-api/binding-resource-comparison.svg @@ -0,0 +1,168 @@ + + + + + + + + + Function-buffer resource vs. native SBT layout + + + Metal path + Shader-visible function buffer plus user data. + + + function-buffer table: m x n + + + row/col + + slot 0 + + slot 1 + + slot 2 + + ... + + + row 0 + + Intersection + + Intersection + + Intersection + + ... + + + row 1 + + Intersection + + Intersection + + Intersection + + ... + + + ... + + ... + + ... + + ... + + ... + + + + + user_data + records + bindless buffers + callable tables + slot maps + + + Not in this table + ClosestHit and Miss are post-trace shader logic on Metal. + + (a) Metal: function buffer entries contain one custom intersection function. + + + D3D / Vulkan path + Host-side SBT object, not a shader-visible resource. + + + Shader Binding Table + no shader binding point + + + hit-group section: m x n + + + row/col + + slot 0 + + slot 1 + + slot 2 + + ... + + + row 0 + + ClosestHit + AnyHit + Intersection + + ClosestHit + AnyHit + Intersection + + ClosestHit + AnyHit + Intersection + + ... + + + row 1 + + ClosestHit + AnyHit + Intersection + + ClosestHit + AnyHit + Intersection + + ClosestHit + AnyHit + Intersection + + ... + + + miss section + + Miss0 + + ... + + + callable section + + Call0 + + ... + + (b) D3D/Vulkan: SBT records group hit shaders and live only on the host side. + diff --git a/proposals/figures/041-ray-tracing-api/context-reachability.svg b/proposals/figures/042-ray-tracing-api/context-reachability.svg similarity index 89% rename from proposals/figures/041-ray-tracing-api/context-reachability.svg rename to proposals/figures/042-ray-tracing-api/context-reachability.svg index 5747cec..450da1e 100644 --- a/proposals/figures/041-ray-tracing-api/context-reachability.svg +++ b/proposals/figures/042-ray-tracing-api/context-reachability.svg @@ -18,12 +18,12 @@ RayTracer - RayTracer<PrimaryProgram> + RayTracer<ProgramLayout> - PrimaryProgram + PrimaryTraceProgramLayout TraceContext = PrimaryTraceContext - HitGroups = slot-indexed list + HitGroups = ordered group list ClosestHitInput diff --git a/proposals/figures/041-ray-tracing-api/dispatch-model-gap.svg b/proposals/figures/042-ray-tracing-api/dispatch-model-gap.svg similarity index 100% rename from proposals/figures/041-ray-tracing-api/dispatch-model-gap.svg rename to proposals/figures/042-ray-tracing-api/dispatch-model-gap.svg diff --git a/proposals/figures/041-ray-tracing-api/native-metal-tag-validation.svg b/proposals/figures/042-ray-tracing-api/native-metal-tag-validation.svg similarity index 100% rename from proposals/figures/041-ray-tracing-api/native-metal-tag-validation.svg rename to proposals/figures/042-ray-tracing-api/native-metal-tag-validation.svg diff --git a/proposals/figures/041-ray-tracing-api/reachability-definition.svg b/proposals/figures/042-ray-tracing-api/reachability-definition.svg similarity index 100% rename from proposals/figures/041-ray-tracing-api/reachability-definition.svg rename to proposals/figures/042-ray-tracing-api/reachability-definition.svg diff --git a/proposals/figures/041-ray-tracing-api/slang-tag-synthesis-gap.svg b/proposals/figures/042-ray-tracing-api/slang-tag-synthesis-gap.svg similarity index 100% rename from proposals/figures/041-ray-tracing-api/slang-tag-synthesis-gap.svg rename to proposals/figures/042-ray-tracing-api/slang-tag-synthesis-gap.svg From 123cbb03c45389bcdc07cb72c10e9f72e4dacdec Mon Sep 17 00:00:00 2001 From: kaizhang Date: Mon, 15 Jun 2026 15:33:14 -0500 Subject: [PATCH 17/21] Remove prototype links from ray tracing proposal --- proposals/042-ray-tracing-api.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/proposals/042-ray-tracing-api.md b/proposals/042-ray-tracing-api.md index b6bdfa7..747f584 100644 --- a/proposals/042-ray-tracing-api.md +++ b/proposals/042-ray-tracing-api.md @@ -22,12 +22,6 @@ declare a conceptual shader binding table, or SBT, as structured Slang types. D3 continue to use the native host-created SBT. Metal can use the same structure to synthesize the post-trace closest-hit and miss dispatch logic that Metal programmers normally write by hand. -The prototype API sketch is being iterated in the Slang implementation repository under: - -- `docs/design/rt-api-workspace/design-static-dispatch-table/rt_static_dispatch_extend.slang` -- `docs/design/rt-api-workspace/design-static-dispatch-table/rt_static_dispatch_table_extend.slang` -- `docs/design/rt-api-workspace/design-static-dispatch-table/rt_static_dispathc_table_example_extend.slang` - Catalog ------- From 6d7a31376d85fc4c01c8da3f3e2a83128d89cd0b Mon Sep 17 00:00:00 2001 From: kaizhang Date: Tue, 23 Jun 2026 14:15:47 -0500 Subject: [PATCH 18/21] Prefer visible functions for Metal hit dispatch --- proposals/042-ray-tracing-api.md | 89 ++++++++++++------- .../binding-resource-comparison.svg | 8 +- 2 files changed, 61 insertions(+), 36 deletions(-) diff --git a/proposals/042-ray-tracing-api.md b/proposals/042-ray-tracing-api.md index 747f584..287777b 100644 --- a/proposals/042-ray-tracing-api.md +++ b/proposals/042-ray-tracing-api.md @@ -258,40 +258,47 @@ For D3D and Vulkan, these fields are mostly descriptive: For Metal, this API becomes executable structure: - Slang lowers `RayTracer.trace(...)` to `intersector.intersect(...)`. -- Slang uses `RayTraversalDesc.missIndex` to synthesize miss dispatch. -- Slang synthesizes the internal hit-group slot calculation to dispatch closest-hit. +- Slang uses `RayTraversalDesc.missIndex` to index generated Miss visible-function dispatch. +- Slang synthesizes the internal hit-group slot calculation to index generated ClosestHit + visible-function dispatch. - Slang uses the same hit-group list slots to validate and emit Metal function-table or function-buffer entries for any-hit and custom intersection functions. Conceptual Metal lowering: ```slang +// Internal lowering pseudocode, not user-visible rt API. let result = metalIntersector.intersect(desc.ray, scene, descriptor, payload); -if (!result.isNone) +if (result.isNone) +{ + let missFn = descriptor.__generatedMissVisibleFunctions[desc.missIndex]; + missFn(makeMissInput(...)); +} +else { uint geometryContribution = __rtGetGeometryContribution(result); uint instanceContribution = __rtGetInstanceContribution(result); uint slot = instanceContribution + geometryContribution * desc.sbtStride + desc.sbtOffset; - switch (slot) - { - case 0: - PrimaryTriangleClosestHit().invoke(makeClosestHitInput(...)); - break; - case 1: - PrimaryCurveClosestHit().invoke(makeClosestHitInput(...)); - break; - case 2: - PrimarySphereClosestHit().invoke(makeClosestHitInput(...)); - break; - } + let closestHitFn = descriptor.__generatedClosestHitVisibleFunctions[slot]; + closestHitFn(makeClosestHitInput(...)); } ``` The important property is that Metal closest-hit dispatch is not invented independently. It is generated from the same conceptual SBT slots that the host uses for D3D and Vulkan. +The generated Metal dispatch should not normally be emitted as a literal `switch` over every +reachable Miss or ClosestHit body. A switch keeps all selected stage bodies in the same Metal +shader compilation unit, so an expensive ClosestHit path that is unreachable for a given +instance can still increase register pressure for a lightweight ClosestHit path compiled in the +same function. The preferred Metal lowering is therefore to use generated visible-function tables +for Miss and ClosestHit dispatch. This keeps the source model close to SBT dispatch while giving +Metal a separate callable target for each reflected stage body. A switch remains useful as +semantic pseudocode or as a restricted fallback for very small programs or targets where +visible-function dispatch is not available. + Alternative considered: require users to write a structural Metal-like dispatch function. ```slang @@ -317,6 +324,8 @@ harder compiler problem: - Reflection depends on successful control-flow analysis. - Dynamic dispatch tables, buffer lookups, and helper functions complicate extraction. - Small shader-code changes could affect host reflection in surprising ways. +- If lowered as a literal switch, heavy and light stage bodies can be forced into the same Metal + shader compilation unit and interfere through register allocation. The proposed design chooses the reverse direction: users declare the SBT structure directly, then Slang synthesizes Metal dispatch. This is simpler to validate and easier to reflect. @@ -336,8 +345,8 @@ function-buffer path, the useful mental model is a pair of physical inputs: - a function-buffer table indexed by traversal, where each selected entry names one custom intersection function; -- a user-data buffer that can carry records, generated slot maps, bindless resources, and callable - visible-function-table values. +- generated resources that carry records, generated slot maps, bindless resources, and + visible-function tables used for Miss, ClosestHit, and callable dispatch. This section focuses on the `intersection_function_buffer_arguments` path because it is the more general form for the proposed portable abstraction. Metal's `intersection_function_table` is a @@ -351,14 +360,14 @@ host-side database with separate hit-group, miss, and callable sections. A hit-g name ClosestHit, AnyHit, and Intersection shaders together, while the miss and callable sections are one-dimensional lists. -Figure 6 compares the two binding models. The Metal panel shows a shader-visible function buffer -and user-data buffer. The D3D/Vulkan panel shows the SBT as one host-side object with hit-group, -miss, and callable sections. +Figure 6 compares the two binding models. The Metal panel shows a shader-visible function buffer, +descriptor-side data, and generated visible-function dispatch resources. The D3D/Vulkan panel +shows the SBT as one host-side object with hit-group, miss, and callable sections. ![Metal function buffer compared with D3D and Vulkan shader binding table](figures/042-ray-tracing-api/binding-resource-comparison.svg) -*Figure 6. Binding resource comparison: (a) Metal uses a shader-visible function buffer plus user data; (b) D3D/Vulkan use one host-side SBT object with hit-group, miss, and callable sections, and no shader binding point.* +*Figure 6. Binding resource comparison: (a) Metal uses a shader-visible function buffer plus descriptor-side data and visible-function dispatch resources; (b) D3D/Vulkan use one host-side SBT object with hit-group, miss, and callable sections, and no shader binding point.* The previous subsection already makes the two sides share the same conceptual layout: the shader source declares a `ProgramLayout`, and host code can reflect that layout to build the target-side @@ -381,9 +390,10 @@ the reflected program layout. On Metal, `TraceProgramDescriptor` lowers to the physical resources needed by the selected Metal traversal path. For the general function-buffer path, that means a function buffer -plus generated user data, matching the Metal panel in Figure 6. Host code binds those physical Metal -resources to the opaque Slang descriptor and uses reflection of `ProgramLayout` to populate the -custom intersection functions, record data, generated slot maps, bindless resources, and callable +plus generated descriptor-side data and visible-function tables, matching the Metal panel in +Figure 6. Host code binds those physical Metal resources to the opaque Slang descriptor and uses +reflection of `ProgramLayout` to populate the custom intersection functions, record data, +generated slot maps, bindless resources, Miss and ClosestHit visible-function entries, and callable visible-function-table values. On D3D and Vulkan, `TraceProgramDescriptor` does not need to lower to a shader-visible @@ -554,8 +564,8 @@ struct PrimarySphereIntersection The compiler is responsible for lowering these structs to the target form: - D3D and Vulkan: generated native entry points and hit groups, connected to SBT records. -- Metal: generated intersection functions for any-hit/custom-intersection behavior, plus a - generated post-trace closest-hit dispatch switch. +- Metal: generated intersection functions for any-hit/custom-intersection behavior, plus generated + post-trace Miss and ClosestHit visible-function dispatch. The user writes one source-level model. The target backend chooses the appropriate pipeline shape. @@ -670,7 +680,8 @@ void rayGen() For Metal, Slang generates code that is equivalent to the user's old post-trace dispatch, but the source of truth is now `PrimaryTraceProgramLayout.HitGroups` and -`PrimaryTraceProgramLayout.MissGroups`. +`PrimaryTraceProgramLayout.MissGroups`. The generated dispatch should use Metal visible functions +for Miss and ClosestHit rather than emitting one large switch containing every stage body. Metal host migration: @@ -678,7 +689,9 @@ Metal host migration: 2. For each hit group list position, discover its any-hit and intersection stage structs. 3. Build the Metal intersection function buffer or function table using the reflected slot mapping. -4. For function-buffer dispatch, configure Metal's index calculation consistently with +4. Populate the generated Miss and ClosestHit visible-function tables inside the + `TraceProgramDescriptor` lowering using the reflected miss and hit-group list positions. +5. For function-buffer dispatch, configure Metal's index calculation consistently with `RayTraversalDesc`: ```metal @@ -687,7 +700,7 @@ intersector.set_base_id(desc.sbtOffset); ``` This keeps Metal's any-hit and custom-intersection dispatch aligned with Slang's generated -closest-hit dispatch. +visible-function ClosestHit dispatch. ### 3.2 Migrating Existing Slang D3D/Vulkan Ray Tracing Code @@ -839,8 +852,19 @@ Pattern B: Metal intersection function buffer. ```cpp auto programLayout = reflection->findTraceProgramLayout("PrimaryTraceProgramLayout"); +for (auto miss : programLayout.missGroups) +{ + descriptor.setGeneratedMissVisibleFunction( + miss.slot, + miss.generatedMissEntryPoint); +} + for (auto hit : programLayout.hitGroups) { + descriptor.setGeneratedClosestHitVisibleFunction( + hit.slot, + hit.generatedClosestHitEntryPoint); + if (hit.generatedAnyHitEntryPoint || hit.generatedIntersectionEntryPoint) { functionBuffer.setFunction( @@ -850,9 +874,10 @@ for (auto hit : programLayout.hitGroups) } ``` -The generated Metal ray-generation code uses the same slot to dispatch closest-hit after -`intersect(...)`. The host does not need to provide a separate closest-hit dispatch table for -Metal because closest-hit dispatch is synthesized by Slang. +The generated Metal ray-generation code uses the same slot to select a generated ClosestHit +visible function after `intersect(...)`. The host does not author custom post-trace dispatch logic +for Metal, but the `TraceProgramDescriptor` lowering may expose generated visible-function table +resources that the host or Slang runtime populates from the reflected program layout. Pattern C: Metal intersection function table. diff --git a/proposals/figures/042-ray-tracing-api/binding-resource-comparison.svg b/proposals/figures/042-ray-tracing-api/binding-resource-comparison.svg index fd330b3..4edb5e8 100644 --- a/proposals/figures/042-ray-tracing-api/binding-resource-comparison.svg +++ b/proposals/figures/042-ray-tracing-api/binding-resource-comparison.svg @@ -30,7 +30,7 @@ Metal path - Shader-visible function buffer plus user data. + Function buffer plus descriptor-side dispatch resources. function-buffer table: m x n @@ -85,12 +85,12 @@ user_data records bindless buffers - callable tables + visible funcs slot maps - Not in this table - ClosestHit and Miss are post-trace shader logic on Metal. + Post-trace stage dispatch + Miss and ClosestHit use generated visible-function dispatch. (a) Metal: function buffer entries contain one custom intersection function. From c64b8a469989fa88ebe1eddaf802da8cb2e74823 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Thu, 25 Jun 2026 10:43:19 -0500 Subject: [PATCH 19/21] Revise ray tracing structural dispatch proposal --- proposals/042-ray-tracing-api.md | 408 +++++++++++++++--- .../d3d-vulkan-sbt-layout.svg | 89 ++++ .../intersection-function-buffer-layout.svg | 101 +++++ .../intersection-function-table-layout.svg | 162 +++++++ 4 files changed, 709 insertions(+), 51 deletions(-) create mode 100644 proposals/figures/042-ray-tracing-api/d3d-vulkan-sbt-layout.svg create mode 100644 proposals/figures/042-ray-tracing-api/intersection-function-buffer-layout.svg create mode 100644 proposals/figures/042-ray-tracing-api/intersection-function-table-layout.svg diff --git a/proposals/042-ray-tracing-api.md b/proposals/042-ray-tracing-api.md index 287777b..86cb3d9 100644 --- a/proposals/042-ray-tracing-api.md +++ b/proposals/042-ray-tracing-api.md @@ -35,6 +35,8 @@ Catalog - [2.2 Detailed Component Descriptions](#22-detailed-component-descriptions) - [2.2.1 Resolving The Dispatch Model Gap With A Conceptual SBT](#221-resolving-the-dispatch-model-gap-with-a-conceptual-sbt) - [2.2.2 Resolving The Metal Function Table And Function Buffer Resource Mismatch With TraceProgramDescriptor](#222-resolving-the-metal-function-table-and-function-buffer-resource-mismatch-with-traceprogramdescriptor) + - [2.2.2.1 Lowering To `intersection_function_table`](#2221-lowering-to-intersection_function_table) + - [2.2.2.2 Lowering To `intersection_function_buffer_arguments`](#2222-lowering-to-intersection_function_buffer_arguments) - [2.2.3 Resolving The Metal Tag-List Issue With Context Types](#223-resolving-the-metal-tag-list-issue-with-context-types) - [2.3 Writing Stages As Interface-Conforming Types](#23-writing-stages-as-interface-conforming-types) - [3. Migration Examples](#3-migration-examples) @@ -68,11 +70,11 @@ post-trace dispatch without changing the native D3D/Vulkan model. ### 1.2 Metal Function Table And Function Buffer Resource Mismatch -Metal introduces `intersection_function_table` and `intersection_function_buffer` resource types -that are visible to shader code and must be bound from host code when traversal needs custom -intersection behavior. This is different from the D3D/Vulkan SBT model. The SBT is built by host -code, but it is not a shader-visible resource and shader code does not declare a binding point for -it. +Metal introduces `intersection_function_table` resource objects and +`intersection_function_buffer_arguments` values that are visible to shader code and must be bound +from host code when traversal needs custom intersection behavior. This is different from the +D3D/Vulkan SBT model. The SBT is built by host code, but it is not a shader-visible resource and +shader code does not declare a binding point for it. This creates an asymmetric programming model. Metal shader code may need a parameter that represents a function table or function buffer. D3D/Vulkan shader code has no corresponding @@ -332,27 +334,10 @@ Slang synthesizes Metal dispatch. This is simpler to validate and easier to refl #### 2.2.2 Resolving The Metal Function Table And Function Buffer Resource Mismatch With TraceProgramDescriptor -Metal introduces `intersection_function_table` and `intersection_function_buffer` as resource -types that can be visible to shader code and bound from host code. D3D and Vulkan instead use an -SBT. The SBT is also built by host code, but it is not a shader-visible resource, so shader code -does not declare an SBT binding point. - -Before defining the Slang abstraction, it is useful to review the structure of these target -objects. Metal's `intersection_function_table` is a host-bound resource-object table containing -custom intersection function entries and resource bindings visible to those functions. Metal's -`intersection_function_buffer` is the buffer form of the same candidate-hit dispatch idea. In the -function-buffer path, the useful mental model is a pair of physical inputs: - -- a function-buffer table indexed by traversal, where each selected entry names one custom - intersection function; -- generated resources that carry records, generated slot maps, bindless resources, and - visible-function tables used for Miss, ClosestHit, and callable dispatch. - -This section focuses on the `intersection_function_buffer_arguments` path because it is the more -general form for the proposed portable abstraction. Metal's `intersection_function_table` is a -special-case resource type relative to `intersection_function_buffer_arguments`; it follows the -same candidate-hit dispatch idea, and support for it can be added as a restricted lowering path -without changing the core `TraceProgramDescriptor` model. +Metal introduces `intersection_function_table` resource objects and +`intersection_function_buffer_arguments` values that can be visible to shader code and bound from +host code. D3D and Vulkan instead use an SBT. The SBT is also built by host code, but it is not a +shader-visible resource, so shader code does not declare an SBT binding point. D3D and Vulkan do not expose an equivalent shader-visible function table or function buffer. Their comparable structure is the host-created SBT. It is useful to view the SBT as one @@ -360,14 +345,13 @@ host-side database with separate hit-group, miss, and callable sections. A hit-g name ClosestHit, AnyHit, and Intersection shaders together, while the miss and callable sections are one-dimensional lists. -Figure 6 compares the two binding models. The Metal panel shows a shader-visible function buffer, -descriptor-side data, and generated visible-function dispatch resources. The D3D/Vulkan panel -shows the SBT as one host-side object with hit-group, miss, and callable sections. +Figure 6 shows the SBT baseline that the portable layout is trying to preserve. The native +D3D/Vulkan SBT is one host-side object with hit-group, miss, and callable sections. - -![Metal function buffer compared with D3D and Vulkan shader binding table](figures/042-ray-tracing-api/binding-resource-comparison.svg) + +![D3D and Vulkan shader binding table layout](figures/042-ray-tracing-api/d3d-vulkan-sbt-layout.svg) -*Figure 6. Binding resource comparison: (a) Metal uses a shader-visible function buffer plus descriptor-side data and visible-function dispatch resources; (b) D3D/Vulkan use one host-side SBT object with hit-group, miss, and callable sections, and no shader binding point.* +*Figure 6. D3D/Vulkan SBT layout: one host-side object contains hit-group, miss, and callable sections, and shader code has no SBT binding point.* The previous subsection already makes the two sides share the same conceptual layout: the shader source declares a `ProgramLayout`, and host code can reflect that layout to build the target-side @@ -388,20 +372,315 @@ struct TraceProgramDescriptor declare the trace context. Its job is to represent the target-specific descriptor object derived from the reflected program layout. -On Metal, `TraceProgramDescriptor` lowers to the physical resources needed by the -selected Metal traversal path. For the general function-buffer path, that means a function buffer -plus generated descriptor-side data and visible-function tables, matching the Metal panel in -Figure 6. Host code binds those physical Metal resources to the opaque Slang descriptor and uses -reflection of `ProgramLayout` to populate the custom intersection functions, record data, -generated slot maps, bindless resources, Miss and ClosestHit visible-function entries, and callable -visible-function-table values. - On D3D and Vulkan, `TraceProgramDescriptor` does not need to lower to a shader-visible -resource. The native SBT remains a host-side object, as shown in the D3D/Vulkan panel in Figure 6. +resource. The native SBT remains a host-side object, as shown in Figure 6. Host code still uses the same `ProgramLayout` reflection to build hit-group, miss, and callable SBT records, but shader code does not receive a Metal-style function-table or function-buffer object. +On Metal, `TraceProgramDescriptor` lowers to the physical resources needed by the +selected Metal traversal path. This proposal does not yet expose an API for users to choose the +Metal lowering form. For now, assume the Slang compiler and runtime can lower the same opaque +descriptor to either an ordinary `intersection_function_table` shape or an +`intersection_function_buffer_arguments` shape. The two shapes have the same source-level contract +but different target-side binding structure. + +##### 2.2.2.1 Lowering To `intersection_function_table` + +An ordinary Metal `intersection_function_table` is a shader-visible resource-object table. Figure +7 shows both the resource layout and the dispatch relationship that Slang needs to synthesize. + + +![Metal intersection function table resource layout with closest-hit dispatch zoom](figures/042-ray-tracing-api/intersection-function-table-layout.svg) + +*Figure 7. Ordinary intersection function table lowering: the left side shows the whole IFT resource, including function entries, buffer bindings, and three visible-function tables; the right side zooms into the paired dispatch between the ClosestHit visible-function table and IFT entries. A 1:1 mapping connects native IFT entries to logical hit slots. Miss and Callable visible-function tables use independent indices.* + +**Native Layout** + +The ordinary function-table layout has two conceptually separate parts: + +```text +intersection_function_table + function entries: + entry metalIFTIndex -> generated candidate-hit function + // AnyHit / custom Intersection behavior only + // mapped 1:1 to a logicalHitSlot + + resource bindings: + buffer slot 0 -> generated descriptor data + // records, slot maps, bindless resource handles + + visible-function table slot 0 -> generated Miss functions + visible-function table slot 1 -> generated ClosestHit functions + // indexed by logicalHitSlot + visible-function table slot 2 -> generated Callable functions +``` + +There are at most three generated visible-function tables associated with the function table: +`visible_function_table_0` is the Miss table, `visible_function_table_1` is the ClosestHit table, +and `visible_function_table_2` is the Callable table. These visible-function tables are resource +bindings of the ordinary Metal function table; they are not the native IFT entries themselves. + +**Lowering Strategy** + +When the compiler lowers `TraceProgramDescriptor` to this ordinary function-table +path, it can be thought of as: + +```text +TraceProgramDescriptor + intersection_function_table: + function entries -> generated candidate-hit functions + buffer slot 0 -> generated descriptor data + visible-function table slot 0 -> generated Miss functions + visible-function table slot 1 -> generated ClosestHit functions + visible-function table slot 2 -> generated Callable functions +``` + +The generated Metal-side use can be thought of as: + +```slang +// Internal Metal-shaped pseudocode. +let table = descriptor.__intersectionFunctionTable; +let descriptorData = table.get_buffer<__RTDescriptorData*>(0); +let missFns = table.get_visible_function_table<__RTMissFn>(0); +let closestHitFns = table.get_visible_function_table<__RTClosestHitFn>(1); + +let result = metalIntersector.intersect(desc.ray, scene, table, payload); + +if (result.isNone) +{ + missFns[desc.missIndex](payload, descriptorData, desc.missIndex); +} +else +{ + uint logicalHitSlot = + instanceOffset + geometryId * desc.sbtStride + desc.sbtOffset; + + closestHitFns[logicalHitSlot](payload, descriptorData, logicalHitSlot, result); +} +``` + +**Gaps, Fixes, And Constraints** + +- **Gap 1: Native function-table entries do not dispatch every ray-tracing stage.** The native + entries dispatch only candidate-hit behavior: AnyHit filtering and custom Intersection logic. + They do not dispatch Miss or ClosestHit. Slang fixes this by lowering Miss and ClosestHit to + generated visible-function tables stored as function-table resource bindings. + + **Constraint:** the host or Slang runtime must populate those generated visible-function tables + as part of the `TraceProgramDescriptor` lowering. The Miss, ClosestHit, and Callable entries can + be queried from the `ProgramLayout` reflection data described in the previous section. Miss uses + `desc.missIndex`, ClosestHit uses `logicalHitSlot`, and Callable uses its own callable index. + +- **Gap 2: Native function-table indexing is not the portable hit-slot formula.** Ordinary + `intersection_function_table` traversal does not use `RayTraversalDesc.sbtOffset` or + `RayTraversalDesc.sbtStride` when selecting candidate-hit functions. Metal selects an IFT entry + from acceleration-structure offsets: + + ```text + metalIFTIndex = + geometryIntersectionFunctionTableOffset + + instanceIntersectionFunctionTableOffset + ``` + + Slang treats `logicalHitSlot` as the portable identity of the hit group: + + ```text + logicalHitSlot = instanceOffset + geometryId * desc.sbtStride + desc.sbtOffset + ``` + + **Constraint:** the host must construct acceleration-structure function-table offsets and function + table contents so every `metalIFTIndex` selected by traversal maps to exactly one + `logicalHitSlot`. The numbers do not need to be equal. What matters is that the selected + candidate-hit function and the generated ClosestHit visible function represent the same logical + hit group: + + ```text + intersection_function_table[metalIFTIndex] + -> generated AnyHit / custom Intersection candidate function + -> maps to logicalHitSlot + + visible_function_table_1[logicalHitSlot] + -> generated ClosestHit function + ``` + + Separate `TraceProgramDescriptor` values per ray type are also a natural way to keep this + mapping simple. + +**Concrete Example** + +Suppose one trace call uses `desc.sbtStride = 2`, `desc.sbtOffset = 1`, and logical +`instanceOffset = 0`. The portable logical slots are: + +```text +logicalHitSlot(geometry 0) = 0 + geometryId 0 * 2 + 1 = 1 +logicalHitSlot(geometry 1) = 0 + geometryId 1 * 2 + 1 = 3 +``` + +The Metal IFT indices do not need to be `1` and `3`. They only need a 1:1 mapping back to logical +slots `1` and `3`: + +```text +instance: + instanceIntersectionFunctionTableOffset = 8 + +geometry 0: + geometryIntersectionFunctionTableOffset = 0 + metalIFTIndex = 0 + 8 = 8 + maps to logicalHitSlot 1 + +geometry 1: + geometryIntersectionFunctionTableOffset = 4 + metalIFTIndex = 4 + 8 = 12 + maps to logicalHitSlot 3 + +intersection_function_table[8] -> candidate function for logicalHitSlot 1 +intersection_function_table[12] -> candidate function for logicalHitSlot 3 + +visible_function_table_1[1] -> ClosestHit for logicalHitSlot 1 +visible_function_table_1[3] -> ClosestHit for logicalHitSlot 3 +``` + +##### 2.2.2.2 Lowering To `intersection_function_buffer_arguments` + +The Metal 4 function-buffer path represents candidate-hit dispatch with an +`intersection_function_buffer_arguments` value rather than an ordinary resource-object table. +Figure 8 shows both the function-buffer layout and the descriptor-side data needed for generated +Miss, ClosestHit, and Callable dispatch. + + +![Metal intersection function buffer arguments layout](figures/042-ray-tracing-api/intersection-function-buffer-layout.svg) + +*Figure 8. Metal function-buffer lowering: `intersection_function_buffer_arguments` carries the candidate-hit table, while descriptor-side data carries records and visible-function dispatch resources for Miss, ClosestHit, and Callable dispatch.* + +**Native Layout** + +The native function-buffer argument describes the traversal-time candidate-hit table: + +```text +intersection_function_buffer_arguments: + intersection_function_buffer -> raw table of generated candidate-hit functions + intersection_function_buffer_size -> byte size of that table + intersection_function_stride -> byte stride between table entries +``` + +The function-buffer table still only contains candidate-hit behavior: AnyHit filtering and custom +Intersection logic. Descriptor-side data carries the rest of the portable trace program state: +records, slot maps, bindless resources, and generated visible-function tables for Miss, +ClosestHit, and Callable dispatch. + +**Lowering Strategy** + +When the compiler lowers `TraceProgramDescriptor` to this function-buffer path, it +can be thought of as: + +```text +TraceProgramDescriptor + intersection_function_buffer_arguments: + intersection_function_buffer -> table of generated candidate-hit functions + intersection_function_buffer_size -> byte size of the table + intersection_function_stride -> byte stride between entries + + generated descriptor-side data: + records + slot maps + bindless resource handles + visible-function table for Miss + visible-function table for ClosestHit + visible-function table for Callable +``` + +The generated Metal-side use can be thought of as: + +```slang +// Internal Metal-shaped pseudocode. +let ifbArgs = descriptor.__intersectionFunctionBufferArguments; +let descriptorData = descriptor.__generatedDescriptorData; +let missFns = descriptorData.missVisibleFunctions; +let closestHitFns = descriptorData.closestHitVisibleFunctions; + +metalIntersector.set_base_id(desc.sbtOffset); +metalIntersector.set_geometry_multiplier(desc.sbtStride); + +let result = metalIntersector.intersect(desc.ray, scene, ifbArgs, descriptorData, payload); + +if (result.isNone) +{ + missFns[desc.missIndex](payload, descriptorData, desc.missIndex); +} +else +{ + uint logicalHitSlot = + instanceOffset + geometryId * desc.sbtStride + desc.sbtOffset; + + closestHitFns[logicalHitSlot](payload, descriptorData, logicalHitSlot, result); +} +``` + +**Gaps, Fixes, And Constraints** + +- **Gap 1: The function buffer still does not dispatch every ray-tracing stage.** Like ordinary + `intersection_function_table`, the function-buffer table dispatches candidate-hit behavior, not + Miss or ClosestHit. Slang fixes this by keeping candidate-hit functions in the function buffer + and lowering Miss, ClosestHit, and Callable to generated visible-function tables in + descriptor-side data. + + **Constraint:** the host or Slang runtime must populate the generated visible-function tables + from the `ProgramLayout` reflection data described in the previous section. Miss uses + `desc.missIndex`, ClosestHit uses `logicalHitSlot`, and Callable uses its own callable index. + +- **Gap 2: The host must still build a target-side candidate-hit table.** The function-buffer form + is closer to the D3D/Vulkan SBT model than ordinary `intersection_function_table`, because the + table representation includes an explicit stride. Conceptually, this lets the backend model the + portable hit slot directly: + + ```text + logicalHitSlot = instanceOffset + geometryId * desc.sbtStride + desc.sbtOffset + ``` + + **Constraint:** the host or Slang runtime must populate the function buffer consistently with the + reflected `ProgramLayout` hit groups and with the exact Metal IFB indexing rules. The same + logical slot should select both the candidate-hit function and the generated ClosestHit visible + function: + + ```text + functionBuffer[logicalHitSlot] + -> generated AnyHit / custom Intersection candidate function + + visible_function_table_1[logicalHitSlot] + -> generated ClosestHit function + ``` + +**Concrete Example** + +Suppose one trace call uses `desc.sbtStride = 2`, `desc.sbtOffset = 1`, and logical +`instanceOffset = 0`. The portable logical slots are the same as in the ordinary function-table +example: + +```text +logicalHitSlot(geometry 0) = 0 + geometryId 0 * 2 + 1 = 1 +logicalHitSlot(geometry 1) = 0 + geometryId 1 * 2 + 1 = 3 +``` + +For the function-buffer lowering, the host or Slang runtime can lay out the candidate-hit table by +logical hit slot: + +```text +functionBuffer[0] -> unused or default candidate function +functionBuffer[1] -> candidate function for logicalHitSlot 1 +functionBuffer[2] -> unused or default candidate function +functionBuffer[3] -> candidate function for logicalHitSlot 3 + +visible_function_table_1[1] -> ClosestHit for logicalHitSlot 1 +visible_function_table_1[3] -> ClosestHit for logicalHitSlot 3 +``` + +The physical byte address of each function-buffer entry is derived from +`intersection_function_buffer` plus `logicalHitSlot * intersection_function_stride`, subject to the +exact Metal IFB traversal rules. The useful difference from ordinary function-table lowering is +that the function-buffer table can be organized directly around the portable slot calculation, +instead of requiring a separate `metalIFTIndex` to `logicalHitSlot` mapping. + #### 2.2.3 Resolving The Metal Tag-List Issue With Context Types With the descriptor abstraction separated, the tag-list issue still needs a way to answer: "which @@ -504,12 +783,12 @@ This gives the compiler a source-level relationship: - Every group in `PrimaryTraceProgramLayout.HitGroups` is constrained to that trace context. - Any-hit and intersection stage structs receive input types derived from the same context. -Figure 7 shows how the trace program layout connects the trace call to the grouped stage structs. +Figure 9 shows how the trace program layout connects the trace call to the grouped stage structs. ![Context connects ray tracer and hit shaders](figures/042-ray-tracing-api/context-reachability.svg) -*Figure 7. Context reachability contract: the trace program layout connects `RayTracer`, the trace-wide context, hit groups, and stage input types so the compiler has a source-visible relationship.* +*Figure 9. Context reachability contract: the trace program layout connects `RayTracer`, the trace-wide context, hit groups, and stage input types so the compiler has a source-visible relationship.* This does not prove that arbitrary host data is correct. If the host builds an SBT or Metal function table that violates the reflected program layout, the program can still be wrong. The @@ -881,16 +1160,43 @@ resources that the host or Slang runtime populates from the reflected program la Pattern C: Metal intersection function table. -Metal's ordinary function table path is less flexible than the function-buffer path because -shader code cannot set the same base-id and geometry-multiplier values in the same way. It can -still be supported for restricted layouts, for example: +Metal's ordinary function table path uses the same reflected `ProgramLayout`, but candidate-hit +selection is driven by acceleration-structure function-table offsets instead of directly by +`RayTraversalDesc.sbtOffset` and `RayTraversalDesc.sbtStride`. Host setup must therefore align the +ordinary function-table entries with the logical hit-group slots used by generated ClosestHit +visible-function dispatch. + +```cpp +auto programLayout = reflection->findTraceProgramLayout("PrimaryTraceProgramLayout"); + +for (auto miss : programLayout.missGroups) +{ + descriptor.setGeneratedMissVisibleFunction( + miss.slot, + miss.generatedMissEntryPoint); +} + +for (auto hit : programLayout.hitGroups) +{ + descriptor.setGeneratedClosestHitVisibleFunction( + hit.slot, + hit.generatedClosestHitEntryPoint); -- the acceleration structure already contains the expected function-table offsets, -- the shader uses a fixed `RayTraversalDesc` layout, -- or the backend can prove that the table index matches the reflected hit-group slot. + if (hit.generatedAnyHitEntryPoint || hit.generatedIntersectionEntryPoint) + { + uint metalIFTIndex = engineLayout.getMetalFunctionTableIndexForHitSlot(hit.slot); + functionTable.setFunction( + metalIFTIndex, + hit.generatedIntersectionOrAnyHitFunction); + } +} +``` -Function-buffer lowering is the preferred general path for matching D3D/Vulkan SBT index -calculation. +This is valid when the engine also builds geometry and instance acceleration-structure metadata +so traversal selects the same `metalIFTIndex` for primitives that post-trace dispatch will map to +`hit.slot`. The mapping from `metalIFTIndex` to `hit.slot` must be 1:1, but the numbers do not +need to be equal. The proposal does not yet define an API for choosing ordinary function-table +lowering versus function-buffer lowering; that is left as a backend/runtime policy to revisit. Pattern D: Manual host construction without reflection. diff --git a/proposals/figures/042-ray-tracing-api/d3d-vulkan-sbt-layout.svg b/proposals/figures/042-ray-tracing-api/d3d-vulkan-sbt-layout.svg new file mode 100644 index 0000000..6ecdb0c --- /dev/null +++ b/proposals/figures/042-ray-tracing-api/d3d-vulkan-sbt-layout.svg @@ -0,0 +1,89 @@ + + + + D3D / Vulkan Shader Binding Table + One host-side object with hit-group, miss, and callable sections. + + + Shader Binding Table + no shader binding point + + + hit-group section: m x n + + + row/col + + slot 0 + + slot 1 + + slot 2 + + ... + + + row 0 + + ClosestHit + AnyHit + Intersection + + ClosestHit + AnyHit + Intersection + + ClosestHit + AnyHit + Intersection + + ... + + + row 1 + + ClosestHit + AnyHit + Intersection + + ClosestHit + AnyHit + Intersection + + ClosestHit + AnyHit + Intersection + + ... + + + miss section + + Miss0 + + ... + + + callable section + + Call0 + + ... + + + Hit-group records can name ClosestHit, AnyHit, and Intersection together. + diff --git a/proposals/figures/042-ray-tracing-api/intersection-function-buffer-layout.svg b/proposals/figures/042-ray-tracing-api/intersection-function-buffer-layout.svg new file mode 100644 index 0000000..240e2d2 --- /dev/null +++ b/proposals/figures/042-ray-tracing-api/intersection-function-buffer-layout.svg @@ -0,0 +1,101 @@ + + + + + + + + + Metal intersection_function_buffer_arguments Layout + Candidate-hit dispatch is table-shaped; descriptor-side data carries records and visible-function dispatch resources. + + + + + function-buffer table: m x n + one generated candidate-hit function per entry + + + row/col + + slot 0 + + slot 1 + + slot 2 + + ... + + + row 0 + + IntersectionFn + + IntersectionFn + + IntersectionFn + + ... + + + row 1 + + IntersectionFn + + IntersectionFn + + IntersectionFn + + ... + + + ... + + ... + + ... + + ... + + ... + + + + + + descriptor data + hit records + miss records + call records + slot maps + bindless data + + + Miss + visible funcs + + + ClosestHit + visible funcs + + + Callable + visible funcs + + + Miss dispatch uses missIndex; ClosestHit dispatch uses the same logical hit slot as the candidate-hit table. + diff --git a/proposals/figures/042-ray-tracing-api/intersection-function-table-layout.svg b/proposals/figures/042-ray-tracing-api/intersection-function-table-layout.svg new file mode 100644 index 0000000..59fa518 --- /dev/null +++ b/proposals/figures/042-ray-tracing-api/intersection-function-table-layout.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + Ordinary intersection_function_table lowering + + + intersection_function_table resource + One shader-visible object with function entries and resource bindings. + + + IFT entries + native Metal traversal table + + index + + candidate-hit function + + 0 + + CandidateHit1 + + + buffer slot 0 + generated descriptor data + records, slot maps, bindless handles + + + VFT 0 + Miss + missIndex + + + VFT 1 + ClosestHit + logicalHitSlot + + + VFT 2 + Callable + callable index + + + IFT entries do not dispatch Miss or ClosestHit. + Those are generated visible-function dispatches. + + + + zoom in + aligned by mapping + + + Dispatch zoom: ClosestHit VFT and IFT entries + + + logicalHitSlot + = instanceOffset + geometryId * desc.sbtStride + desc.sbtOffset + Portable identity of the hit group used by reflection and ClosestHit dispatch. + + + visible_function_table_1 + indexed by logicalHitSlot + + + slot + + ClosestHit + + + 0 + + ClosestHit0 + + + 1 + + ClosestHit1 + + + 2 + + ClosestHit2 + + + IFT entries + host maps each slot to a native IFT index + + + slot + + IFT index + + function + + + 0 + + idxA + + CandFn0 + + + 1 + + idxB + + CandFn1 + + + 2 + + idxC + + CandFn2 + + + + 1:1 mapping, not equality + + + Metal native index = geometryIntersectionFunctionTableOffset + + instanceIntersectionFunctionTableOffset. + idxA/B/C are native IFT indices. + Host setup maps each native IFT entry to one logical hit slot; + the selected candidate and ClosestHit describe the same hit group. + + The whole IFT resource carries function entries plus buffer and visible-function bindings; the zoom shows the only paired dispatch: candidate hit and ClosestHit. + From 61376789b3cfdb15fef9819d07324fbb098f8f89 Mon Sep 17 00:00:00 2001 From: kaizhang Date: Thu, 25 Jun 2026 10:49:51 -0500 Subject: [PATCH 20/21] Update ray tracing migration examples --- proposals/042-ray-tracing-api.md | 141 +++++++++++++++++++++++-------- 1 file changed, 105 insertions(+), 36 deletions(-) diff --git a/proposals/042-ray-tracing-api.md b/proposals/042-ray-tracing-api.md index 86cb3d9..2d3f960 100644 --- a/proposals/042-ray-tracing-api.md +++ b/proposals/042-ray-tracing-api.md @@ -881,7 +881,8 @@ kernel void rayGen(...) ``` With the proposed API, the user moves the manually dispatched operations into stage structs and -declares the trace program layout structurally. The list order defines the slots: +declares the trace program layout structurally. The list order defines the portable logical hit +slots: ```slang struct PrimaryMissGroup : rt::IMissGroup @@ -949,7 +950,7 @@ void rayGen() desc.ray = makeRay(); desc.instanceMask = 0xff; desc.sbtOffset = 0; - desc.sbtStride = 3; + desc.sbtStride = 1; desc.missIndex = 0; rt::RayTracer tracer; @@ -959,27 +960,28 @@ void rayGen() For Metal, Slang generates code that is equivalent to the user's old post-trace dispatch, but the source of truth is now `PrimaryTraceProgramLayout.HitGroups` and -`PrimaryTraceProgramLayout.MissGroups`. The generated dispatch should use Metal visible functions -for Miss and ClosestHit rather than emitting one large switch containing every stage body. +`PrimaryTraceProgramLayout.MissGroups`. The generated dispatch uses Metal visible functions for +Miss and ClosestHit rather than emitting one large switch containing every stage body. Metal host migration: 1. Query `PrimaryTraceProgramLayout` through Slang reflection. -2. For each hit group list position, discover its any-hit and intersection stage structs. -3. Build the Metal intersection function buffer or function table using the reflected slot - mapping. -4. Populate the generated Miss and ClosestHit visible-function tables inside the - `TraceProgramDescriptor` lowering using the reflected miss and hit-group list positions. -5. For function-buffer dispatch, configure Metal's index calculation consistently with - `RayTraversalDesc`: - -```metal -intersector.set_geometry_multiplier(desc.sbtStride); -intersector.set_base_id(desc.sbtOffset); -``` - -This keeps Metal's any-hit and custom-intersection dispatch aligned with Slang's generated -visible-function ClosestHit dispatch. +2. Populate the generated Miss and ClosestHit visible-function tables in the + `TraceProgramDescriptor` lowering from the reflected miss and hit-group slots. +3. Populate the candidate-hit dispatch resource from the reflected hit groups: + AnyHit and custom Intersection stages go into either an + `intersection_function_buffer_arguments` lowering or an ordinary `intersection_function_table` + lowering. +4. If the lowering uses `intersection_function_buffer_arguments`, lay out the candidate-hit table + by the same logical hit slots that `RayTraversalDesc` computes. In this example, + `geometry_id` values `0`, `1`, and `2` map directly to logical hit slots `0`, `1`, and `2`. +5. If the lowering uses ordinary `intersection_function_table`, choose native Metal IFT indices + for each reachable logical hit slot and build acceleration-structure function-table offsets so + traversal selects the corresponding native index. The native IFT index and logical hit slot do + not need to be numerically equal, but the mapping must be 1:1. + +This keeps Metal's AnyHit/custom-Intersection dispatch aligned with Slang's generated visible +function dispatch for Miss and ClosestHit. ### 3.2 Migrating Existing Slang D3D/Vulkan Ray Tracing Code @@ -1045,6 +1047,8 @@ struct PrimaryTriangleClosestHit The old trace parameters map directly to fields in `RayTraversalDesc`: ```slang +rt::TraceProgramDescriptor gPrimaryDescriptor; + rt::RayTraversalDesc desc; desc.ray = ray; desc.instanceMask = instanceMask; @@ -1061,11 +1065,15 @@ D3D/Vulkan host migration: 1. Query `PrimaryTraceProgramLayout` through Slang reflection. 2. For each reflected miss group, add a miss record at its list position. 3. For each reflected hit group, add a hit group record at its list position. -4. Use the same application data that previously produced `rayContributionToHitGroupIndex`, +4. Populate any reflected shader-record or local-root data associated with the miss, hit, and + callable groups. +5. Use the same application data that previously produced `rayContributionToHitGroupIndex`, `multiplierForGeometryContributionToHitGroupIndex`, and `missShaderIndex`. -The native SBT model is not replaced. The new shader declarations make the intended SBT layout -visible to Slang, which enables Metal lowering and gives host code a single reflected contract. +The native SBT model is not replaced, and `TraceProgramDescriptor` does +not need to become a shader-visible resource on D3D/Vulkan. The new shader declarations make the +intended SBT layout visible to Slang, which enables Metal lowering and gives host code a single +reflected contract. ### 3.3 Host Reflection Patterns @@ -1083,13 +1091,17 @@ struct ReflectedTraceProgramLayout struct ReflectedMissGroup { int slot; + TypeReflection* contextType; + TypeReflection* recordType; EntryPointReflection* generatedMissEntryPoint; }; struct ReflectedHitGroup { int slot; - TypeReflection* hitContextType; + TypeReflection* contextType; + TypeReflection* recordType; + TypeReflection* intersectionAttributesType; EntryPointReflection* generatedClosestHitEntryPoint; EntryPointReflection* generatedAnyHitEntryPoint; EntryPointReflection* generatedIntersectionEntryPoint; @@ -1098,11 +1110,15 @@ struct ReflectedHitGroup struct ReflectedCallableGroup { int slot; - TypeReflection* callableContextType; + TypeReflection* contextType; + TypeReflection* recordType; EntryPointReflection* generatedCallableEntryPoint; }; ``` +The helper names in the following examples are illustrative; the reflection API shape and runtime +ownership model are still open design questions. + Pattern A: D3D/Vulkan native SBT. ```cpp @@ -1110,7 +1126,10 @@ auto programLayout = reflection->findTraceProgramLayout("PrimaryTraceProgramLayo for (auto miss : programLayout.missGroups) { - sbt.setMissRecord(miss.slot, miss.generatedMissEntryPoint); + sbt.setMissRecord( + miss.slot, + miss.generatedMissEntryPoint, + buildShaderRecordData(miss.recordType)); } for (auto hit : programLayout.hitGroups) @@ -1119,12 +1138,22 @@ for (auto hit : programLayout.hitGroups) hit.slot, hit.generatedClosestHitEntryPoint, hit.generatedAnyHitEntryPoint, - hit.generatedIntersectionEntryPoint); + hit.generatedIntersectionEntryPoint, + buildShaderRecordData(hit.recordType)); +} + +for (auto callable : programLayout.callableGroups) +{ + sbt.setCallableRecord( + callable.slot, + callable.generatedCallableEntryPoint, + buildShaderRecordData(callable.recordType)); } ``` The application still controls geometry contribution, instance contribution, stride, and offset. -The reflected slots tell the application which shader group belongs at each slot. +The reflected slots tell the application which shader group belongs at each SBT slot, while the +reflected record types tell it what local-root/shader-record data each record expects. Pattern B: Metal intersection function buffer. @@ -1136,6 +1165,9 @@ for (auto miss : programLayout.missGroups) descriptor.setGeneratedMissVisibleFunction( miss.slot, miss.generatedMissEntryPoint); + descriptor.setMissRecordData( + miss.slot, + buildShaderRecordData(miss.recordType)); } for (auto hit : programLayout.hitGroups) @@ -1148,15 +1180,29 @@ for (auto hit : programLayout.hitGroups) { functionBuffer.setFunction( hit.slot, - hit.generatedIntersectionOrAnyHitFunction); + buildGeneratedCandidateHitFunction(hit)); } + + descriptor.setHitRecordData( + hit.slot, + buildShaderRecordData(hit.recordType)); +} + +for (auto callable : programLayout.callableGroups) +{ + descriptor.setGeneratedCallableVisibleFunction( + callable.slot, + callable.generatedCallableEntryPoint); + descriptor.setCallableRecordData( + callable.slot, + buildShaderRecordData(callable.recordType)); } ``` -The generated Metal ray-generation code uses the same slot to select a generated ClosestHit -visible function after `intersect(...)`. The host does not author custom post-trace dispatch logic -for Metal, but the `TraceProgramDescriptor` lowering may expose generated visible-function table -resources that the host or Slang runtime populates from the reflected program layout. +For function-buffer lowering, the candidate-hit table is organized by the same logical slots used +by generated ClosestHit dispatch. The host does not author custom post-trace dispatch logic for +Metal, but the `TraceProgramDescriptor` lowering may expose generated visible-function table and +record resources that the host or Slang runtime populates from the reflected program layout. Pattern C: Metal intersection function table. @@ -1174,6 +1220,9 @@ for (auto miss : programLayout.missGroups) descriptor.setGeneratedMissVisibleFunction( miss.slot, miss.generatedMissEntryPoint); + descriptor.setMissRecordData( + miss.slot, + buildShaderRecordData(miss.recordType)); } for (auto hit : programLayout.hitGroups) @@ -1184,19 +1233,39 @@ for (auto hit : programLayout.hitGroups) if (hit.generatedAnyHitEntryPoint || hit.generatedIntersectionEntryPoint) { - uint metalIFTIndex = engineLayout.getMetalFunctionTableIndexForHitSlot(hit.slot); + uint metalIFTIndex = engineLayout.chooseMetalFunctionTableIndex(hit.slot); functionTable.setFunction( metalIFTIndex, - hit.generatedIntersectionOrAnyHitFunction); + buildGeneratedCandidateHitFunction(hit)); + + engineLayout.recordMetalFunctionTableMapping( + hit.slot, + metalIFTIndex); } + + descriptor.setHitRecordData( + hit.slot, + buildShaderRecordData(hit.recordType)); +} + +for (auto callable : programLayout.callableGroups) +{ + descriptor.setGeneratedCallableVisibleFunction( + callable.slot, + callable.generatedCallableEntryPoint); + descriptor.setCallableRecordData( + callable.slot, + buildShaderRecordData(callable.recordType)); } ``` This is valid when the engine also builds geometry and instance acceleration-structure metadata so traversal selects the same `metalIFTIndex` for primitives that post-trace dispatch will map to `hit.slot`. The mapping from `metalIFTIndex` to `hit.slot` must be 1:1, but the numbers do not -need to be equal. The proposal does not yet define an API for choosing ordinary function-table -lowering versus function-buffer lowering; that is left as a backend/runtime policy to revisit. +need to be equal. Callable and Miss visible-function tables do not use this hit-slot mapping: +Miss is indexed by `missIndex`, and Callable is indexed by the callable index. The proposal does +not yet define an API for choosing ordinary function-table lowering versus function-buffer +lowering; that is left as a backend/runtime policy to revisit. Pattern D: Manual host construction without reflection. From 9ed66904aac06eb8f0a3dcd7fbcadd311c07c3ca Mon Sep 17 00:00:00 2001 From: kaizhang Date: Fri, 26 Jun 2026 13:10:51 -0500 Subject: [PATCH 21/21] Clarify ray tracing context reachability figure --- proposals/042-ray-tracing-api.md | 5 +- .../context-reachability.svg | 94 +++++++++++++------ 2 files changed, 69 insertions(+), 30 deletions(-) diff --git a/proposals/042-ray-tracing-api.md b/proposals/042-ray-tracing-api.md index 2d3f960..88355e1 100644 --- a/proposals/042-ray-tracing-api.md +++ b/proposals/042-ray-tracing-api.md @@ -783,12 +783,13 @@ This gives the compiler a source-level relationship: - Every group in `PrimaryTraceProgramLayout.HitGroups` is constrained to that trace context. - Any-hit and intersection stage structs receive input types derived from the same context. -Figure 9 shows how the trace program layout connects the trace call to the grouped stage structs. +Figure 9 zooms into the handwritten code lines that carry the contract: the trace call names the +program layout, and each stage `invoke(...)` method names the input context it accepts. ![Context connects ray tracer and hit shaders](figures/042-ray-tracing-api/context-reachability.svg) -*Figure 9. Context reachability contract: the trace program layout connects `RayTracer`, the trace-wide context, hit groups, and stage input types so the compiler has a source-visible relationship.* +*Figure 9. Context reachability contract: the user-written trace call and stage `invoke(...)` signatures give the compiler a source-visible relationship between `RayTracer`, the trace-wide context, hit groups, and stage input types.* This does not prove that arbitrary host data is correct. If the host builds an SBT or Metal function table that violates the reflected program layout, the program can still be wrong. The diff --git a/proposals/figures/042-ray-tracing-api/context-reachability.svg b/proposals/figures/042-ray-tracing-api/context-reachability.svg index 450da1e..247e972 100644 --- a/proposals/figures/042-ray-tracing-api/context-reachability.svg +++ b/proposals/figures/042-ray-tracing-api/context-reachability.svg @@ -1,44 +1,82 @@ - + - + + + + + + + - Context makes reachability explicit + Context reachability is visible in the code users write + + + PrimaryTraceProgramLayout + declares one trace context + and ordered hit/miss groups + source-level grouping contract + + + Do not infer this from host SBT + The shader source names the layout + at the trace call and stage inputs. + + + zoom + + + Handwritten code lines that carry the contract - - RayTracer - RayTracer<ProgramLayout> + + Ray-generation code + rt::RayTracer< + PrimaryTraceProgramLayout> tracer; + tracer.trace(desc, scene, + gPrimaryDescriptor, payload); - - PrimaryTraceProgramLayout - TraceContext = PrimaryTraceContext - HitGroups = ordered group list + + Closest-hit code + void invoke(rt::ClosestHitInput< + PrimaryTriangleContext> input) + { shadeTriangle(input); } - - ClosestHitInput - PrimaryTriangleContext + + Any-hit code + void invoke(rt::AnyHitInput< + PrimaryTriangleContext> input) + { alphaTest(input); } - - AnyHitInput - PrimaryTriangleContext + + Intersection code + void invoke(rt::IntersectionInput< + PrimarySphereContext> input) + { return intersectSphere(input); } - - IntersectionInput - PrimaryTriangleContext + + + - - - - + These names are what Slang links: + ProgramLayout -> group -> stage input context.