pub const LIGHT_CULL: &str = "// Clustered light-binning compute kernel. One thread per froxel cluster: builds\n// the cluster\'s world-space AABB from its screen tile + exponential depth slice,\n// tests every local light\'s bounding sphere against it, and writes the surviving\n// light indices into the per-cluster list the forward pass reads.\n//\n// Single source for every backend. Vulkan binds set 0 bindings 0-2; Metal and\n// DXIL assign the same slots from declaration order (params 0, lights 1,\n// list 2), matching the hand-assigned host bindings on all three.\n//\n// Layouts must match render_types.rs: ClusterParams (128 B) and GpuLight (64 B).\n// CLUSTER_LIGHT_LIST_STRIDE mirrors MAX_LIGHTS_PER_CLUSTER + 1.\n\nstatic const uint CLUSTER_LIGHT_LIST_STRIDE = 64u;\nstatic const uint MAX_LIGHTS_PER_CLUSTER = 63u;\n\n// Each (vec3, scalar) pair is spelled as one float4: MSL sizes a float3 at 16\n// bytes in a structured buffer as well as in a constant buffer, so a literal\n// transcription pushes every following field four bytes late on Metal alone.\nstruct GpuLight\n{\n // xyz = world-space position, w = range.\n float4 position_range;\n // xyz = linear RGB, w = intensity.\n float4 color_intensity;\n // xyz = direction, w = the LIGHT_KIND_* discriminant\'s bits.\n float4 direction_kind;\n float cos_inner;\n float cos_outer;\n int shadow_index;\n // Index into the AreaLightData table for an area light, else -1. Unused by\n // the cull (the bounding sphere covers every light kind) but kept so this\n // declaration matches the forward pass\'s byte for byte.\n int data_index;\n};\n\nstruct ClusterParams\n{\n float4x4 inv_view_proj;\n // xyz = camera position, w = z_near. float4 pairs rather than float3 +\n // scalar: MSL sizes a constant-buffer float3 at 16 bytes, so the packed\n // 128-byte CPU layout only survives on every target without vec3 fields.\n float4 cam_pos_znear;\n // xyz = view forward, w = z_far.\n float4 view_forward_zfar;\n uint grid_x;\n uint grid_y;\n uint grid_z;\n uint num_lights;\n float screen_w;\n float screen_h;\n uint use_clusters;\n uint _pad;\n};\n\n[[vk::binding(0, 0)]]\nConstantBuffer<ClusterParams> cluster;\n\n[[vk::binding(1, 0)]]\nStructuredBuffer<GpuLight> lights;\n\n[[vk::binding(2, 0)]]\nRWStructuredBuffer<uint> cluster_list;\n\n// Direction of the camera ray through a screen-NDC point. Unprojects the far\n// plane (z = 1) to world space, then normalises from the camera: for a\n// perspective projection every ray through a screen point passes through the\n// eye, so the far-plane unprojection gives the direction.\nfloat3 cluster_corner_ray(float2 ndc)\n{\n float4 clip = float4(ndc, 1.0, 1.0);\n float4 world = mul(cluster.inv_view_proj, clip);\n world /= world.w;\n return normalize(world.xyz - cluster.cam_pos_znear.xyz);\n}\n\n[shader(\"compute\")]\n[numthreads(64, 1, 1)]\nvoid light_cull_kernel(uint3 tid : SV_DispatchThreadID)\n{\n uint cid = tid.x;\n uint cluster_count = cluster.grid_x * cluster.grid_y * cluster.grid_z;\n if (cid >= cluster_count)\n {\n return;\n }\n\n uint cx = cid % cluster.grid_x;\n uint cy = (cid / cluster.grid_x) % cluster.grid_y;\n uint cz = cid / (cluster.grid_x * cluster.grid_y);\n\n // Screen-tile NDC bounds (y flipped: screen y-down to NDC y-up).\n float2 lo = float2(float(cx), float(cy)) / float2(float(cluster.grid_x), float(cluster.grid_y));\n float2 hi = float2(float(cx + 1u), float(cy + 1u)) / float2(float(cluster.grid_x), float(cluster.grid_y));\n float2 ndcs[4] = {\n float2(lo.x * 2.0 - 1.0, -(lo.y * 2.0 - 1.0)),\n float2(hi.x * 2.0 - 1.0, -(lo.y * 2.0 - 1.0)),\n float2(lo.x * 2.0 - 1.0, -(hi.y * 2.0 - 1.0)),\n float2(hi.x * 2.0 - 1.0, -(hi.y * 2.0 - 1.0)),\n };\n\n // Exponential depth slice: near/far view-space distances for this Z band.\n float ratio = cluster.view_forward_zfar.w / cluster.cam_pos_znear.w;\n float near_d = cluster.cam_pos_znear.w * pow(ratio, float(cz) / float(cluster.grid_z));\n float far_d = cluster.cam_pos_znear.w * pow(ratio, float(cz + 1u) / float(cluster.grid_z));\n\n // World-space AABB over the tile frustum clamped to [near_d, far_d].\n float3 aabb_min = float3(1e30);\n float3 aabb_max = float3(-1e30);\n for (uint i = 0u; i < 4u; ++i)\n {\n float3 ray = cluster_corner_ray(ndcs[i]);\n float fdot = max(dot(ray, cluster.view_forward_zfar.xyz), 1e-4);\n float3 p_near = cluster.cam_pos_znear.xyz + ray * (near_d / fdot);\n float3 p_far = cluster.cam_pos_znear.xyz + ray * (far_d / fdot);\n aabb_min = min(aabb_min, min(p_near, p_far));\n aabb_max = max(aabb_max, max(p_near, p_far));\n }\n\n uint base = cid * CLUSTER_LIGHT_LIST_STRIDE;\n uint count = 0u;\n for (uint li = 0u; li < cluster.num_lights; ++li)\n {\n float3 lp = lights[li].position_range.xyz;\n float r = lights[li].position_range.w;\n // Sphere vs AABB: distance from the light centre to the clamped point.\n float3 d = lp - clamp(lp, aabb_min, aabb_max);\n if (dot(d, d) <= r * r)\n {\n if (count < MAX_LIGHTS_PER_CLUSTER)\n {\n cluster_list[base + 1u + count] = li;\n count += 1u;\n }\n }\n }\n cluster_list[base] = count;\n}\n";Expand description
light_cull.slang.