The WebGPU backend rejects primitive bounds outside each pass's frustum before submitting instance batches. Camera and shadow visibility are independent: an object outside the camera can still cast a shadow onto a visible surface. Place membership remains the presentation producer's responsibility.
#Contract
PrimitiveFrustum uses the WebGPU clip volume -w <= x,y <= w, 0 <= z <= w. Every current primitive vertex lies within the local cube [-0.5, 0.5]^3. For each plane, the transformed cube's support radius is half the sum of the absolute dot products with the three model basis columns. This includes rotated, nonuniformly scaled and sheared models without approximating them as unscaled spheres.
The renderer tests the exact float32 model that it uploads, after traveler interpolation/yaw and procedural float/flicker animation. It retains touching and intersecting bounds. A conservative margin accounts for float32 model/view multiplication and cancellation, retaining the original projection-row magnitudes even when extracting a nearly cancelling far plane. The test may retain invisible objects; it must not remove visible geometry. Degenerate plane arithmetic fails open.
Each frame independently packs visible and shadow instances, preserving relative primitive order. Both lists share one GPU storage buffer bounded to 8,192 records (786,432 bytes); each pass has at most 4,096. CPU staging owns a 4,096-record transformed source array and the 8,192-record packed array. Duplicate records trade extra bounded storage/uploads for simple contiguous instanced draws. Empty primitive batches submit no draw; an entirely empty scene still executes the background/postprocess pass. The one-submission-in-flight policy is unchanged.
RenderStats.instances and triangles still describe resident scene content. visibleInstances, shadowInstances, visibleTriangles and shadowTriangles describe the last submitted pass lists. drawCalls counts actual nonempty batches plus the postprocess draw. F2 shows the instance counts, and playtest exports include all counts. Buffer counts alone do not report allocation size or driver memory.
WebGPURenderer.setCulling(false) is a diagnostic reference switch. The player defaults to culling enabled. No presentation/save protocol or content fingerprint changes are required.
#Acceptance
tests/visibility.test.ts checks all primitive vertices, six touching/outside clip boundaries, WebGPU near depth, camera-straddling bounds, large-coordinate cancellation and 10,000 deterministic affine cases against an independent eight-corner clip-space oracle.
Engine Observatory's third worker cycle compares PNG bytes with culling disabled/enabled for eighteen cases:
- An off-camera shadow caster, with a removal control proving its shadow affects visible pixels.
- Mixed primitives, hierarchical shear, animated decorations, a traveler, a narrow viewport, a reversed camera and perspective point-light shadows.
- Float and flicker animations crossing the clip boundary, checking both pixels and submitted counts.
- An empty scene and the 4,096-instance capacity, including the full 8,192-record reference upload.
- An identical traveler motion/interpolation sequence replayed in each mode.
- Entry and reverse views of all three Darkwind places.
The probe uses its own canvas/renderer and disposes them, without joining or changing the active room. Its report records resident and submitted instance/triangle counts per case. Browser acceptance also exercises the normal player, multiplayer, persistence and the existing graphics lifecycle checks.
The initial software-WebGPU entry-view comparisons submit 514 of 674 house instances, 338 of 578 garden instances and 124 of 225 sanctuary instances to the camera pass. House shadow submissions fall to 378; the other two places retain all shadow casters. These counts are for the documented probe cameras, not arbitrary player views or a frame-rate measurement.
Source 07d3c4a is recorded in local acceptance, with the final browser capability report. All 156 headless checks and 23 browser scenarios passed; two final browser checks cover the strengthened traveler exit and panel scrolling. Runs preceded the source commit, as recorded in their build metadata. Source CI is tracked separately.
#Remaining work
This is CPU frustum rejection for current unit primitives, not occlusion culling, imported-mesh bounds, LOD, a spatial acceleration structure, GPU indirect draws or visibility streaming. It does not close the complete renderer milestone. Submission reductions do not establish a hardware-GPU frame-rate improvement; physical-device profiling and CPU/upload tradeoff measurements remain necessary. Imported meshes, skinning and vertex displacement must supply appropriate conservative bounds before using this path.