feat(webgpu-rt): emit intersection/any-hit dispatch + build AABB BVH

PipelineRTWebGPU emits a runIntersection mega-switch and the
RT_HAS_ANYHIT / RT_HAS_INTERSECTION consts (+ the @CRAFTER_RT_TRACE_USER
marker) that gate the library's new TRACE-stage user callbacks, so an
opaque triangle-only scene still const-folds them away. Mesh-WebGPU
builds a SAH BVH2 over AABB primitives and uploads them in primitive
order for the intersection shader to fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-02 22:09:20 +00:00
commit a91603c70b
2 changed files with 116 additions and 1 deletions

View file

@ -36,6 +36,8 @@ namespace {
"fn _crafter_default_anyhit(ray: RayDesc, hit: HitInfo, payload: ptr<function, Payload>) -> u32 { return RT_ANYHIT_ACCEPT; }";
constexpr std::string_view kPlaceholderMiss =
"fn _crafter_default_miss(ray: RayDesc, payload: ptr<function, Payload>) {}";
constexpr std::string_view kPlaceholderIntersection =
"fn _crafter_default_intersection(ray: RayDesc, aabbMin: vec3<f32>, aabbMax: vec3<f32>, primitiveId: u32) -> IntersectionResult { var r: IntersectionResult; r.hit = false; return r; }";
void AppendCase(std::string& out,
std::uint32_t hitGroupIndex,
@ -60,6 +62,17 @@ namespace {
out += entryFn;
out += "(ray, hit, payload); }\n";
}
// intersection has a return type — forwards the AABB args + the result.
void AppendIntersectionCase(std::string& out,
std::uint32_t hitGroupIndex,
std::string_view entryFn) {
out += " case ";
out += std::to_string(hitGroupIndex);
out += "u: { return ";
out += entryFn;
out += "(ray, aabbMin, aabbMax, primitiveId); }\n";
}
}
void PipelineRTWebGPU::Init(WebGPUCommandEncoderRef /*cmd*/,
@ -150,6 +163,46 @@ void PipelineRTWebGPU::Init(WebGPUCommandEncoderRef /*cmd*/,
wgsl += " }\n";
wgsl += "}\n";
// runIntersection — per-AABB procedural intersection dispatch. For a
// ProceduralHitGroup the intersection shader determines the hit; for
// triangle groups (or groups with no intersection shader) the default
// reports no hit, so the BLAS leaf falls back to the triangle path.
wgsl += "\nfn runIntersection(hg: u32, ray: RayDesc, aabbMin: vec3<f32>, aabbMax: vec3<f32>, primitiveId: u32) -> IntersectionResult {\n";
wgsl += " switch hg {\n";
bool anyIntersection = false;
for (std::uint32_t i = 0; i < hitGroups.size(); ++i) {
const auto& g = hitGroups[i];
if (g.intersectionShader == kRTShaderUnused) continue;
if (g.intersectionShader >= sbt.shaders.size()) continue;
const auto& fn = sbt.shaders[g.intersectionShader].entryFn;
AppendIntersectionCase(wgsl, i, fn);
anyIntersection = true;
}
if (!anyIntersection) wgsl += " // (no intersection shaders registered)\n";
wgsl += " default: { }\n";
wgsl += " }\n";
wgsl += " var none: IntersectionResult;\n";
wgsl += " none.hit = false;\n";
wgsl += " return none;\n";
wgsl += "}\n";
// Trace-time capability flags. The library traversal (injected at the
// marker below) gates its any-hit / intersection callbacks on these
// consts, so a triangle-only opaque scene dead-strips all user code out
// of TRACE and keeps its zero-user-code register footprint. When either
// is set the JS side also gives the TRACE pipeline the user bind-group
// layout (so any-hit / intersection shaders can sample @group(3+)
// resources) — it scans for the exact `@CRAFTER_RT_TRACE_USER` marker.
wgsl += "\nconst RT_HAS_ANYHIT: bool = ";
wgsl += (anyAnyhit ? "true" : "false");
wgsl += ";\n";
wgsl += "const RT_HAS_INTERSECTION: bool = ";
wgsl += (anyIntersection ? "true" : "false");
wgsl += ";\n";
if (anyAnyhit || anyIntersection) {
wgsl += "// @CRAFTER_RT_TRACE_USER = true\n";
}
// runResolve — RESOLVE-stage tonemap hook. The first registered
// Resolve shader wins; with none, identity passthrough (alpha forced
// to 1) so the wavefront output matches a megakernel that wrote raw