From ab9d31376eae2d606ff5b60fc528e2b077b5c8f6 Mon Sep 17 00:00:00 2001
From: "nv-slang-bot[bot]"
<274397474+nv-slang-bot[bot]@users.noreply.github.com>
Date: Fri, 21 Aug 2026 01:05:53 +0000
Subject: [PATCH] Fix shader-slang/slang#12637: lower portable ReportHit to
OptiX/CUDA
Add a cuda case to the standard ReportHit 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).
---
source/slang/hlsl.meta.slang | 14 +-
source/slang/slang-diagnostics.lua | 14 ++
source/slang/slang-emit-cuda.cpp | 29 +++
source/slang/slang-emit.cpp | 11 +
source/slang/slang-ir-insts-stable-names.lua | 3 +-
source/slang/slang-ir-insts.lua | 7 +
.../slang-ir-legalize-varying-params.cpp | 196 +++++++++++++++++-
.../slang/slang-ir-legalize-varying-params.h | 6 +
.../cuda/report-hit-portable-diagnostic.slang | 68 ++++++
tests/cuda/report-hit-portable.slang | 57 +++++
10 files changed, 402 insertions(+), 3 deletions(-)
create mode 100644 tests/cuda/report-hit-portable-diagnostic.slang
create mode 100644 tests/cuda/report-hit-portable.slang
diff --git a/source/slang/hlsl.meta.slang b/source/slang/hlsl.meta.slang
index 764cafd87a2..46e1c02cdc7 100644
--- a/source/slang/hlsl.meta.slang
+++ b/source/slang/hlsl.meta.slang
@@ -20103,6 +20103,16 @@ bool __reportIntersection(float tHit, uint hitKind)
}
}
+//@hidden:
+// Marker for the OptiX write side of a portable `ReportHit`. The `attributes` aggregate is
+// passed as a single operand; the CUDA varying-param legalization pass flattens it field-wise
+// into scalar attribute leaves before emission (see `legalizeOptiXReportIntersections`).
+__generic
+__intrinsic_op($(kIROp_ReportOptiXIntersection))
+[require(cuda, raytracing_intersection)]
+bool __reportOptiXIntersection(float tHit, uint hitKind, A attributes);
+//@public:
+
/// Reports a hit from an intersection shader.
/// @param tHit Distance along the ray where the intersection occurred
/// @param hitKind User-defined value identifying the type of hit
@@ -20112,7 +20122,7 @@ bool __reportIntersection(float tHit, uint hitKind)
/// @category raytracing
__generic
[ForceInline]
-[require(glsl_hlsl_spirv, raytracing_intersection)]
+[require(cuda_glsl_hlsl_spirv, raytracing_intersection)]
bool ReportHit(float tHit, uint hitKind, A attributes)
{
__target_switch
@@ -20125,6 +20135,8 @@ bool ReportHit(float tHit, uint hitKind, A attributes)
static A a;
a = attributes;
return __reportIntersection(tHit, hitKind);
+ case cuda:
+ return __reportOptiXIntersection(tHit, hitKind, attributes);
}
}
diff --git a/source/slang/slang-diagnostics.lua b/source/slang/slang-diagnostics.lua
index 1a9ff9f8189..acc03bb04d8 100644
--- a/source/slang/slang-diagnostics.lua
+++ b/source/slang/slang-diagnostics.lua
@@ -5706,6 +5706,20 @@ err(
span { loc = "location", message = "a shader-terminating intrinsic ('IgnoreHit' or 'AcceptHitAndEndSearch') is reachable from this ray entry point only through a call that could not be inlined (for example, recursion); mark the intervening function(s) '[ForceInline]' or call the intrinsic directly in the entry point so the ray payload is written back before the ray terminates." }
)
+err(
+ "optix-hit-attribute-type-not-supported",
+ 55216,
+ "unsupported hit attribute type for ReportHit on OptiX",
+ span { loc = "location", message = "the hit attribute type passed to 'ReportHit' cannot be lowered to OptiX attribute registers; each scalar field must fit in one 32-bit register ('float', 'bool', and 8/16/32-bit signed or unsigned integers, and vectors/arrays/matrices of those) — 'double', 'half', and 64-bit types are not supported." }
+)
+
+err(
+ "optix-hit-attribute-too-large",
+ 55217,
+ "hit attribute exceeds the OptiX attribute register limit",
+ span { loc = "location", message = "the hit attribute passed to 'ReportHit' requires ~registerCount:int 32-bit attribute registers, but OptiX supports at most 8 (32 bytes)." }
+)
+
err(
"unable-to-auto-map-cuda-type-to-host-type",
56001,
diff --git a/source/slang/slang-emit-cuda.cpp b/source/slang/slang-emit-cuda.cpp
index 23bfc76d58f..3a9d5682a9b 100644
--- a/source/slang/slang-emit-cuda.cpp
+++ b/source/slang/slang-emit-cuda.cpp
@@ -1378,6 +1378,35 @@ bool CUDASourceEmitter::tryEmitInstExprImpl(IRInst* inst, const EmitOpInfo& inOu
}
return true;
}
+ case kIROp_ReportOptiXIntersection:
+ {
+ // optixReportIntersection(tHit, hitKind, a0..aN). The CUDA legalization pass has
+ // already flattened the aggregate into scalar leaves (operands 2..N), one per attribute
+ // register. A float leaf is bit-reinterpreted with `__float_as_uint`, the exact inverse
+ // of the reader's `__int_as_float(optixGetAttribute_N())`, because CUDA lowers
+ // `IRBitCast` as a numeric C cast, not a bit cast.
+ m_writer->emit("optixReportIntersection(");
+ emitOperand(inst->getOperand(0), getInfo(EmitOp::General));
+ m_writer->emit(", ");
+ emitOperand(inst->getOperand(1), getInfo(EmitOp::General));
+ for (UInt i = 2; i < inst->getOperandCount(); ++i)
+ {
+ m_writer->emit(", ");
+ auto leaf = inst->getOperand(i);
+ if (leaf->getDataType()->getOp() == kIROp_FloatType)
+ {
+ m_writer->emit("__float_as_uint(");
+ emitOperand(leaf, getInfo(EmitOp::General));
+ m_writer->emit(")");
+ }
+ else
+ {
+ emitOperand(leaf, getInfo(EmitOp::General));
+ }
+ }
+ m_writer->emit(")");
+ return true;
+ }
case kIROp_GetOptiXSbtDataPtr:
{
m_writer->emit("((");
diff --git a/source/slang/slang-emit.cpp b/source/slang/slang-emit.cpp
index 8de0f15d76f..36d05ff22e6 100644
--- a/source/slang/slang-emit.cpp
+++ b/source/slang/slang-emit.cpp
@@ -1818,6 +1818,17 @@ Result linkAndOptimizeIR(
SLANG_PASS(simplifyIR, targetProgram, defaultIRSimplificationOptions, sink);
}
+ // `ReportHit` is [ForceInline], so its `kIROp_ReportOptiXIntersection` marker now appears at
+ // each call site after `performForceInlining`. Flatten its aggregate attribute operand into
+ // scalar OptiX attribute-register leaves after the post-inline DCE above (so the dead
+ // specialized core-module `ReportHit` body is gone and each marker is diagnosed only once) but
+ // before the generic empty-type / resource legalization passes below, which do not understand
+ // the aggregate operand.
+ if (target == CodeGenTarget::CUDASource || target == CodeGenTarget::CUDAHeader)
+ {
+ SLANG_PASS(legalizeOptiXReportIntersectionsForCUDA, sink);
+ }
+
// Report checkpointing information.
if (codeGenContext->shouldReportCheckpointIntermediates())
{
diff --git a/source/slang/slang-ir-insts-stable-names.lua b/source/slang/slang-ir-insts-stable-names.lua
index 5748ded1c9f..c44d1f11034 100644
--- a/source/slang/slang-ir-insts-stable-names.lua
+++ b/source/slang/slang-ir-insts-stable-names.lua
@@ -874,5 +874,6 @@ return {
["imageGatherOffset"] = 898,
["getNaturalAlignment"] = 899,
["Type.PtrTypeBase.SPIRVUntypedPtr"] = 900,
- ["Attr.TypeAlignment"] = 901
+ ["Attr.TypeAlignment"] = 901,
+ ["reportOptiXIntersection"] = 902
}
diff --git a/source/slang/slang-ir-insts.lua b/source/slang/slang-ir-insts.lua
index 4ee3e6baada..db3679aaed0 100644
--- a/source/slang/slang-ir-insts.lua
+++ b/source/slang/slang-ir-insts.lua
@@ -1657,6 +1657,13 @@ local insts = {
-- Operand 0: register index (int literal)
-- Operand 1: value to write (uint32)
{ setOptiXPayloadRegister = { min_operands = 2 } },
+ -- Write side of a portable `ReportHit(tHit, hitKind, attributes)` call for OptiX.
+ -- Operand 0: tHit (float). Operand 1: hitKind (uint). The remaining operands are the
+ -- aggregate's scalar attribute leaves, produced by the CUDA varying-param legalization
+ -- pass, which flattens `attributes` field-wise (one operand per OptiX attribute register)
+ -- mirroring the read side (`emitOptiXAttributeFetch`). The CUDA emitter renders this as a
+ -- single `optixReportIntersection(tHit, hitKind, a0..aN)`.
+ { reportOptiXIntersection = { min_operands = 2 } },
{ GetVulkanRayTracingPayloadLocation = { min_operands = 1 } },
{ GetLegalizedSPIRVGlobalParamAddr = { min_operands = 1 } },
{
diff --git a/source/slang/slang-ir-legalize-varying-params.cpp b/source/slang/slang-ir-legalize-varying-params.cpp
index abf11d12a16..3d81be95b87 100644
--- a/source/slang/slang-ir-legalize-varying-params.cpp
+++ b/source/slang/slang-ir-legalize-varying-params.cpp
@@ -1126,6 +1126,10 @@ struct CUDAEntryPointVaryingParamLegalizeContext : EntryPointVaryingParamLegaliz
// Maximum number of payload registers (32 registers = 128 bytes)
static const int kMaxPayloadRegisters = 32;
+ // Maximum number of OptiX hit attribute registers (8 registers = 32 bytes). Shared by the read
+ // path (`getLegalizedVaryingVal`) and the `ReportHit` write path.
+ static const int kMaxHitAttributeRegisters = 8;
+
// Track payload write-back info for inout parameters
struct PayloadWritebackInfo
{
@@ -2155,6 +2159,190 @@ struct CUDAEntryPointVaryingParamLegalizeContext : EntryPointVaryingParamLegaliz
return nullptr;
}
+ // An OptiX hit attribute register holds exactly 32 bits, and both this write path and the read
+ // path (`emitOptiXAttributeFetch` / the `kIROp_GetOptiXHitAttribute` emit) map one scalar leaf
+ // to one register with no sub-word packing or multi-register splitting. A leaf is supported
+ // when it survives that single-register round-trip: `float` is bit-reinterpreted
+ // (`__float_as_uint` on write, `__int_as_float` on read); an integer/bool type of 32 bits or
+ // fewer passes through the register's `unsigned int` value (a narrow type sign/zero-extends on
+ // write and truncates back to its low bits on read, symmetrically). Wider scalars (`double`,
+ // `int64_t`/`uint64_t`, `intptr`/`uintptr`) do not fit one register, and `half` has no
+ // bit-preserving read counterpart (the reader would assign the register's integer value
+ // numerically), so both are rejected rather than silently miscompiled.
+ static bool isSupportedOptiXHitAttributeLeaf(IRBasicType* basicType)
+ {
+ switch (basicType->getBaseType())
+ {
+ case BaseType::Float:
+ case BaseType::Bool:
+ case BaseType::Int8:
+ case BaseType::Int16:
+ case BaseType::Int:
+ case BaseType::UInt8:
+ case BaseType::UInt16:
+ case BaseType::UInt:
+ case BaseType::Char:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ // Flatten `value` (of `type`) into its scalar leaves, appended to `outLeaves` in the exact
+ // order the read side (`emitOptiXAttributeFetch`) consumes attribute registers: struct fields
+ // in declaration order, then array/vector/matrix elements in index order, down to each scalar
+ // leaf. This is the write-side mirror of the reader, so a value reported by `ReportHit` on
+ // OptiX round-trips through the fixed-function attribute registers when a hit shader reads it
+ // back. Returns false (so the caller can diagnose) for a type that cannot be lowered to
+ // attribute registers: an unsized array, or a scalar leaf that is not a supported 32-bit
+ // register type. Consider `struct Attributes { uint id; float weight; }`: the leaves are
+ // `{ value.id, value.weight }`, occupying attribute registers 0 and 1.
+ bool flattenOptiXHitAttributes(
+ IRInst* value,
+ IRType* type,
+ IRBuilder* builder,
+ List& outLeaves)
+ {
+ // Attributes are reported by value, so a scalar attribute register never round-trips a
+ // pointer: extracting a field from a pointer value would emit `(&a).x`, and dereferencing
+ // would silently change which value is reported. Reject any pointer type rather than
+ // miscompile it.
+ if (tryGetPointedToType(builder, type))
+ return false;
+
+ if (auto structType = as(type))
+ {
+ for (auto field : structType->getFields())
+ {
+ auto fieldType = field->getFieldType();
+ auto fieldVal = builder->emitFieldExtract(fieldType, value, field->getKey());
+ if (!flattenOptiXHitAttributes(fieldVal, fieldType, builder, outLeaves))
+ return false;
+ }
+ return true;
+ }
+ else if (auto arrayType = as(type))
+ {
+ auto elementCountInst = as(arrayType->getElementCount());
+ if (!elementCountInst)
+ return false;
+ auto elementType = arrayType->getElementType();
+ for (IRIntegerValue ii = 0; ii < elementCountInst->getValue(); ++ii)
+ {
+ auto idx = builder->getIntValue(builder->getIntType(), ii);
+ auto elementVal = builder->emitElementExtract(elementType, value, idx);
+ if (!flattenOptiXHitAttributes(elementVal, elementType, builder, outLeaves))
+ return false;
+ }
+ return true;
+ }
+ else if (auto matType = as(type))
+ {
+ auto rowCountInst = as(matType->getRowCount());
+ auto colCountInst = as(matType->getColumnCount());
+ if (!rowCountInst || !colCountInst)
+ return false;
+ auto elementType = matType->getElementType();
+ auto rowType = builder->getVectorType(elementType, matType->getColumnCount());
+ for (IRIntegerValue row = 0; row < rowCountInst->getValue(); ++row)
+ {
+ auto rowIdx = builder->getIntValue(builder->getIntType(), row);
+ auto rowVal = builder->emitElementExtract(rowType, value, rowIdx);
+ for (IRIntegerValue col = 0; col < colCountInst->getValue(); ++col)
+ {
+ auto colIdx = builder->getIntValue(builder->getIntType(), col);
+ auto elementVal = builder->emitElementExtract(elementType, rowVal, colIdx);
+ if (!flattenOptiXHitAttributes(elementVal, elementType, builder, outLeaves))
+ return false;
+ }
+ }
+ return true;
+ }
+ else if (auto vecType = as(type))
+ {
+ auto elementCountInst = as(vecType->getElementCount());
+ if (!elementCountInst)
+ return false;
+ auto elementType = vecType->getElementType();
+ for (IRIntegerValue ii = 0; ii < elementCountInst->getValue(); ++ii)
+ {
+ auto idx = builder->getIntValue(builder->getIntType(), ii);
+ auto elementVal = builder->emitElementExtract(elementType, value, idx);
+ if (!flattenOptiXHitAttributes(elementVal, elementType, builder, outLeaves))
+ return false;
+ }
+ return true;
+ }
+ else if (auto basicType = as(type))
+ {
+ if (!isSupportedOptiXHitAttributeLeaf(basicType))
+ return false;
+ outLeaves.add(value);
+ return true;
+ }
+
+ return false;
+ }
+
+ // Rewrite each `ReportOptiXIntersection(tHit, hitKind, attributes)` produced by the core-module
+ // `ReportHit` into a call carrying the aggregate's flattened scalar leaves, so the CUDA emitter
+ // can render a single `optixReportIntersection(tHit, hitKind, a0..aN)`. Collect the marker
+ // insts first, then rewrite: the rewrite creates a new (already-flattened) inst that must not
+ // be re-collected, and the pass runs exactly once per module.
+ void legalizeOptiXReportIntersections(IRModule* module, DiagnosticSink* sink)
+ {
+ List workList;
+ for (auto globalInst : module->getGlobalInsts())
+ {
+ auto func = as(globalInst);
+ if (!func)
+ continue;
+ for (auto block : func->getBlocks())
+ for (auto inst : block->getChildren())
+ if (inst->getOp() == kIROp_ReportOptiXIntersection &&
+ inst->getOperandCount() == 3)
+ workList.add(inst);
+ }
+
+ for (auto inst : workList)
+ {
+ IRBuilder builder(module);
+ builder.setInsertBefore(inst);
+
+ auto tHit = inst->getOperand(0);
+ auto hitKind = inst->getOperand(1);
+ auto attrs = inst->getOperand(2);
+
+ List leaves;
+ if (!flattenOptiXHitAttributes(attrs, attrs->getDataType(), &builder, leaves))
+ {
+ sink->diagnose(
+ Diagnostics::OptixHitAttributeTypeNotSupported{.location = inst->sourceLoc});
+ continue;
+ }
+ if (leaves.getCount() > kMaxHitAttributeRegisters)
+ {
+ sink->diagnose(Diagnostics::OptixHitAttributeTooLarge{
+ .registerCount = int(leaves.getCount()),
+ .location = inst->sourceLoc});
+ continue;
+ }
+
+ List args;
+ args.add(tHit);
+ args.add(hitKind);
+ args.addRange(leaves);
+ auto newInst = builder.emitIntrinsicInst(
+ inst->getFullType(),
+ kIROp_ReportOptiXIntersection,
+ args.getCount(),
+ args.getBuffer());
+ newInst->sourceLoc = inst->sourceLoc;
+ inst->replaceUsesWith(newInst);
+ inst->removeAndDeallocate();
+ }
+ }
+
void beginModuleImpl() SLANG_OVERRIDE
{
// Because many of the varying parameters are defined
@@ -2376,7 +2564,7 @@ struct CUDAEntryPointVaryingParamLegalizeContext : EntryPointVaryingParamLegaliz
/*ioBaseAttributeIndex*/ ioBaseAttributeIndex,
/* type to fetch */ info.type,
/*the builder in use*/ &builder);
- if (ioBaseAttributeIndex > 8)
+ if (ioBaseAttributeIndex > kMaxHitAttributeRegisters)
{
// A hit attribute is always a parameter, never a result, so
// `m_param` is set here; guard the deref in release too.
@@ -2577,6 +2765,12 @@ void legalizeEntryPointVaryingParamsForCUDA(IRModule* module, DiagnosticSink* si
context.processModule(module, sink);
}
+void legalizeOptiXReportIntersectionsForCUDA(IRModule* module, DiagnosticSink* sink)
+{
+ CUDAEntryPointVaryingParamLegalizeContext context;
+ context.legalizeOptiXReportIntersections(module, sink);
+}
+
void depointerizeInputParams(IRFunc* entryPointFunc)
{
List workList;
diff --git a/source/slang/slang-ir-legalize-varying-params.h b/source/slang/slang-ir-legalize-varying-params.h
index 5c4d88de3c7..4bef0372226 100644
--- a/source/slang/slang-ir-legalize-varying-params.h
+++ b/source/slang/slang-ir-legalize-varying-params.h
@@ -27,6 +27,12 @@ void legalizeEntryPointVaryingParamsForCPU(
void legalizeEntryPointVaryingParamsForCUDA(IRModule* module, DiagnosticSink* sink);
+// Flatten the aggregate attribute operand of each portable `ReportHit` (lowered to a
+// `kIROp_ReportOptiXIntersection` marker) into scalar OptiX attribute-register leaves. Must run
+// before the generic empty-type / varying-parameter legalization passes, which do not understand
+// the aggregate operand of the marker op.
+void legalizeOptiXReportIntersectionsForCUDA(IRModule* module, DiagnosticSink* sink);
+
void legalizeEntryPointVaryingParamsForMetal(
IRModule* module,
DiagnosticSink* sink,
diff --git a/tests/cuda/report-hit-portable-diagnostic.slang b/tests/cuda/report-hit-portable-diagnostic.slang
new file mode 100644
index 00000000000..1d07ef5d50a
--- /dev/null
+++ b/tests/cuda/report-hit-portable-diagnostic.slang
@@ -0,0 +1,68 @@
+// Portable `ReportHit(tHit, hitKind, attributes)` on CUDA/OptiX flattens the aggregate field-wise
+// into `optixReportIntersection` attribute registers. Two cases are rejected rather than
+// miscompiled: an attribute that needs more than 8 32-bit registers (OptiX's 32-byte limit), and a
+// scalar leaf that is not a supported single-register type (here `double`, which cannot fit one
+// 32-bit register).
+
+//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK): -target cuda -entry tooLarge -stage intersection
+//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK-TYPE): -target cuda -entry badType -stage intersection
+//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK-HALF): -target cuda -entry badHalf -stage intersection
+//DIAGNOSTIC_TEST:SIMPLE(diag=CHECK-PTR): -target cuda -entry badPtr -stage intersection
+
+struct NineFloats
+{
+ float a0, a1, a2, a3, a4, a5, a6, a7, a8;
+};
+
+[shader("intersection")]
+void tooLarge()
+{
+ NineFloats attributes = {0, 1, 2, 3, 4, 5, 6, 7, 8};
+ ReportHit(1.0f, 1u, attributes);
+ //CHECK: E55217
+ //CHECK: requires 9 32-bit attribute registers, but OptiX supports at most 8
+}
+
+struct HasDouble
+{
+ double value;
+};
+
+[shader("intersection")]
+void badType()
+{
+ HasDouble attributes = {1.5};
+ ReportHit(1.0f, 1u, attributes);
+ //CHECK-TYPE: E55216
+ //CHECK-TYPE: each scalar field must fit in one 32-bit register
+}
+
+struct HasHalf
+{
+ half value;
+};
+
+[shader("intersection")]
+void badHalf()
+{
+ HasHalf attributes = {half(1.5)};
+ // `half` is 16 bits but has no bit-preserving read counterpart, so it is rejected too.
+ ReportHit(1.0f, 1u, attributes);
+ //CHECK-HALF: E55216
+}
+
+struct PointerAttr
+{
+ float x;
+};
+
+[shader("intersection")]
+void badPtr()
+{
+ PointerAttr attr = {0.5f};
+ PointerAttr* p = &attr;
+ // A pointer cannot round-trip through a scalar attribute register; reject rather than
+ // dereference (which would change which value is reported).
+ ReportHit(1.0f, 1u, p);
+ //CHECK-PTR: E55216
+}
diff --git a/tests/cuda/report-hit-portable.slang b/tests/cuda/report-hit-portable.slang
new file mode 100644
index 00000000000..5bc99db0194
--- /dev/null
+++ b/tests/cuda/report-hit-portable.slang
@@ -0,0 +1,57 @@
+//TEST:SIMPLE(filecheck=CHECK): -target cuda -entry main -stage intersection
+//TEST:SIMPLE(filecheck=CHECK-PTX): -target ptx -Xnvrtc -I"./external/optix-dev/include/" -entry main -stage intersection
+//TEST:SIMPLE(filecheck=CHECK-EMPTY): -target cuda -entry empty -stage intersection
+
+// Portable HLSL `ReportHit(tHit, hitKind, attributes)` compiles for CUDA/OptiX by flattening the
+// aggregate field-wise into `optixReportIntersection`, one attribute register per scalar leaf, in
+// the same order the reader (`optixGetAttribute_N`) consumes them: struct fields in order, then
+// vector components in index order. An integer leaf passes through; a float leaf is
+// bit-reinterpreted with `__float_as_uint` (the inverse of the reader's `__int_as_float`).
+
+// The narrow integer fields (`int16_t`, `uint8_t`) exercise that any integer type of 32 bits or
+// fewer passes through a single attribute register unchanged; only `float` fields are wrapped in
+// `__float_as_uint`.
+struct Attributes
+{
+ int id;
+ int16_t code;
+ uint8_t tag;
+ bool flag;
+ float weight;
+ float2 bary;
+};
+
+[shader("intersection")]
+void main()
+{
+ Attributes attributes = {-3, int16_t(-2), uint8_t(200), true, 0.5f, float2(0.25f, 0.75f)};
+ // CHECK: optixReportIntersection(1.0f, 1U,
+ // CHECK-SAME: id
+ // CHECK-SAME: code
+ // CHECK-SAME: tag
+ // CHECK-SAME: flag
+ // CHECK-SAME: __float_as_uint(
+ // CHECK-SAME: weight
+ // CHECK-SAME: __float_as_uint(
+ // CHECK-SAME: bary
+ // CHECK-SAME: .x
+ // CHECK-SAME: __float_as_uint(
+ // CHECK-SAME: bary
+ // CHECK-SAME: .y
+ // CHECK-PTX: _optix_report_intersection
+ ReportHit(1.0f, 1u, attributes);
+}
+
+// An empty attribute struct flattens to zero leaves and uses OptiX's zero-attribute
+// `optixReportIntersection(tHit, hitKind)` overload.
+struct EmptyAttributes
+{
+};
+
+[shader("intersection")]
+void empty()
+{
+ EmptyAttributes attributes = {};
+ // CHECK-EMPTY: optixReportIntersection(1.0f, 1U)
+ ReportHit(1.0f, 1u, attributes);
+}