Fix #12637: lower portable HLSL ReportHit to OptiX/CUDA - #12783
Fix #12637: lower portable HLSL ReportHit to OptiX/CUDA#12783nv-slang-bot[bot] wants to merge 1 commit into
Conversation
Add a cuda case to the standard ReportHit<A> that flattens the aggregate attributes field-wise into optixReportIntersection attribute registers, mirroring the read side (emitOptiXAttributeFetch): one 32-bit register per scalar leaf, float leaves bit-reinterpreted via __float_as_uint. Introduces the reportOptiXIntersection IR op, a CUDA flatten pass run after post-inline DCE and before empty-type legalization, and user-facing diagnostics for unsupported attribute types (E55215) and the 8-register limit (E55216).
|
Automated notice (PR board sync) — do not reply to this comment. Auto-assigned @jkwak-work as shepherd for this Bot PR. FYI for maintainers: committer signal on the changed files is higher for szihs than for the auto-requested reviewer (jkwak-work). They were not auto-requested; a human may optionally add them as a reviewer. |
1 similar comment
|
Automated notice (PR board sync) — do not reply to this comment. Auto-assigned @jkwak-work as shepherd for this Bot PR. FYI for maintainers: committer signal on the changed files is higher for szihs than for the auto-requested reviewer (jkwak-work). They were not auto-requested; a human may optionally add them as a reviewer. |
jkwak-work
left a comment
There was a problem hiding this comment.
I will need to review it more carefully.
But I don't think we need an explicit legalization pass for this.
I think that adding a new inst and emit the cuda for the new inst should be enough.
@nv-slang-bot please explain to me why the legalization is needed if it is needed.
|
@nv-slang-bot can you review my previous comment? I am waiting for your input. |
Motivation
The portable, standard HLSL intersection-shader hit report fails to compile for CUDA/OptiX:
The core-module
ReportHit<A>(source/slang/hlsl.meta.slang) was declared[require(glsl_hlsl_spirv, raytracing_intersection)]and its__target_switchhad onlyhlsl/glsl/spirvcases — CUDA was excluded, so the capability check rejected it. Priorwork (#4948 / PR #4949) added the target-specific
ReportHitOptix, which takes attributes as analready-flattened variadic pack of integer scalars; portable shaders passing an aggregate
attributes(the same code that compiles for D3D/Vulkan) still could not target CUDA. Closes #12637.Proposed solution
Give the standard
ReportHit<A>acudacase that flattens the aggregateattributesfield-wise into OptiX attribute registers and emits a single
optixReportIntersectioncall.This is the principled contract because the read side already defines it. When a hit shader
reads its
attributesparameter back,emitOptiXAttributeFetch(
slang-ir-legalize-varying-params.cpp) recurses the aggregate and consumes one attributeregister per scalar leaf (struct fields in order, then array/vector/matrix elements in index
order), rendering
optixGetAttribute_N()for each — float leaves via__int_as_float(...)topreserve their bit pattern. The write side is made the exact inverse of that, so a value reported by
ReportHitround-trips through the fixed-function registers. A whole-struct byte-packed reinterpret(e.g.
bit_cast<uint[N]>) would in general not round-trip: whenever padding or sub-dword fieldsmake the byte layout's register boundaries differ from the reader's per-leaf boundaries, the two
disagree.
Two OptiX/CUDA facts shape the mechanism:
optixSetPayload_N), hitattributes are written only through the single call
optixReportIntersection(float, unsigned int [, unsigned int a0 .. a7])(fixed 0–8 register overloads,external/optix-dev/.../optix_device.h).So the write side gathers all leaves into one call.
IRBitCastas a numeric C cast, not a bit cast (slang-emit-c-like.cpp). So afloat leaf cannot rely on an IR
BitCastto reach the register bit-for-bit; the emitter writes__float_as_uint(x)explicitly, so the register holds the same 32-bit pattern the readerrecovers with
__int_as_float(optixGetAttribute_N()).Change summary
source/slang/hlsl.meta.slangcudatoReportHit<A>'s[require]set and acase cuda:forwarding to a new hidden marker__reportOptiXIntersection.source/slang/slang-ir-insts.luareportOptiXIntersection.source/slang/slang-ir-insts-stable-names.luasource/slang/slang-ir-legalize-varying-params.cpp/.hflattenOptiXHitAttributes(write-side mirror ofemitOptiXAttributeFetch, restricting leaves to types that survive a single 32-bit register),legalizeOptiXReportIntersections(rewrites the marker to carry flattened leaves, enforcing the shared 8-register cap), and alegalizeOptiXReportIntersectionsForCUDAentry point.source/slang/slang-emit.cppsource/slang/slang-emit-cuda.cppcase kIROp_ReportOptiXIntersection:asoptixReportIntersection(tHit, hitKind, a0..aN), wrapping float leaves in__float_as_uint.source/slang/slang-diagnostics.luatests/cuda/report-hit-portable.slang,tests/cuda/report-hit-portable-diagnostic.slangConcepts and vocabulary
(
a0..a7), written once viaoptixReportIntersectionand read individually viaoptixGetAttribute_N. Distinct from ray payload registers, which do have per-registersetters (
optixSetPayload_N) — the two are easy to conflate, but only payloads are per-registerwritable.
emitOptiXAttributeFetch— the existing read-side recursion that reconstructs a hit-attributeaggregate from the registers, one register per scalar leaf. This PR's write side mirrors it.
reportOptiXIntersectioncarries the un-flattened aggregate from the coremodule through inlining to the CUDA flatten pass, keeping the emitter trivial (no aggregate
walking at emit time), matching how the read side splits work between the pass and the emitter.
Process report
hlsl.meta.slang— capability +cudacase. The root cause of E36107 is purely that CUDA wasabsent from
ReportHit<A>'s capability set and switch. Addingcudato[require(...)](reusingthe existing
cuda_glsl_hlsl_spirvaliasReportHitOptixalready uses) and acase cuda: return __reportOptiXIntersection(tHit, hitKind, attributes);is the minimal front-endchange.
__reportOptiXIntersection<A>is a hidden__intrinsic_opdeclaration (modeled on theadjacent
__reportIntersectionand generic__intrinsic_ophelpers likeselect), so theaggregate is preserved as a single IR operand rather than decomposed at the language layer.
Why an IR op + pass rather than emit-time flattening. The aggregate must be broken into scalar
leaves to fill the fixed
a0..a7registers. Doing that in the emitter would require the backend tosynthesize field/element access and re-derive the aggregate's shape — the "consumer-side patching /
semantic-to-syntax reconstruction" the methodology warns against. The read side already puts this in
the IR pass (
emitOptiXAttributeFetch) and hands the emitter trivially-renderable per-register ops;the write side mirrors that split.
flattenOptiXHitAttributesis a near-structural copy of thereader's recursion, collecting extracted leaf values instead of fetching registers, guaranteeing
identical leaf order so the two paths round-trip.
Pass placement.
ReportHitis[ForceInline], so itsreportOptiXIntersectionmarker appearsat each call site after
performForceInlining(the pass scans every function). The flatten pass runs after thepost-inline DCE (so the dead specialized core-module
ReportHitbody is gone and each marker isdiagnosed only once) and before the generic empty-type / resource legalization passes, which do
not understand the marker's aggregate operand. Placing it after inlining but before those passes is
what makes an empty attribute struct (0 leaves) reach the valid zero-attribute overload
optixReportIntersection(tHit, hitKind)instead of aborting in empty-type legalization.Input-shape check. The pass handles
kIROp_ReportOptiXIntersectionwhose operand 2 is theaggregate the core module passed — a canonical, intentionally-produced shape, not a malformed one
being repaired.
flattenOptiXHitAttributesaccepts only leaves that survive a single 32-bitregister:
float(bit-reinterpreted), and integer/bool/chartypes of 32 bits or fewer (whichsign/zero-extend on write and truncate back on read, symmetrically with the reader). It rejects
doubleand 64-bit scalars (they exceed one register) andhalf(the reader has no bit-preservinghalf read — it would assign the register's integer value to the half numerically), as well as
pointer-typed attributes (a scalar register cannot round-trip a pointer, and dereferencing would
change which value is reported) — with the user-facing E55216 rather than a silent miscompile.
Aggregates exceeding 8 registers get E55217 (the shared
kMaxHitAttributeRegisterscap, now used by both the read and write paths).Emitter.
case kIROp_ReportOptiXIntersectionrenders oneoptixReportIntersection(...)call;float leaves get
__float_as_uint(...), the exact inverse of the reader's__int_as_float, so afloat attribute reads back bit-identical. Non-float leaves pass through as the same
unsigned intregister value the reader returns and assigns to the leaf type.
Testing.
tests/cuda/report-hit-portable.slangchecks the emitted flattened call for a structwith signed/narrow-integer/float/vector fields, an empty-attribute entry point, and a PTX/NVRTC
compile against the real OptiX header.
tests/cuda/report-hit-portable-diagnostic.slangcovers theE55216 (double, pointer) and E55217 (>8 registers) rejection paths. Locally I verified via direct
slangcemission plus a PTX/NVRTC compile (the FileCheck-basedslang-testharness needsslang-llvm, unavailable in my environment, so CI runs the harness); the pre-existingreport-hit.slang,optix-hit-attributes.slang, andoptix-get-attributes-mixed.slangremain asregression coverage and emitted unchanged in my checks.