Checks#
Radix has three sets of checks: core checks in Node (no browser), GPU checks in the browser on both backends, and a check of these docs. Performance is measured separately, by the benchmarks at the end.
| What | Command | Needs |
|---|---|---|
| core checks | npm run check:core | Node |
| GPU checks, both backends | npm run check:test (scripts/check-test.mjs) | npm run build:test, npm run serve and Chrome |
| GPU checks, one backend | open http://localhost:8000/build/test/gpu/index.html?gpu=webgpu or ?gpu=webgpu-gl | npm run build:test and npm run serve |
| example pages, both backends | npm run check:examples (scripts/check-examples.mjs) | npm run build, npm run serve and Chrome |
| docs | npm run check:docs | Node |
| types | npm run typecheck (tsconfig.lib.json, tsconfig.core.json, tsconfig.examples.json, tsconfig.test.json) | Node |
| lint | npm run lint (the types, then eslint .) | Node |
check:core: core checks in Node#
npm run check:core (scripts/check-core.ts) runs without a browser. It checks:
ArrayData: types, shared arrays, growth, copies, transfer,swap, andsetFromTransferablekeeping the existing objects.- RDX: round trips of whole and chunked files, chunks read out of order, range reads, and chunked URLs.
- Codecs: every field stays within its bit budget (rounded octahedral normals within 0.06 degrees, and the generic and fixed-size codecs agree), and covariance round trips.
- Splat access:
Splats.loadCentermatchesSplats.loadfor every layout.Splats.storeandSplats.storePbreach leave the other's bits unchanged, and together they produce the same words asSplats.encodeExt(likewise for the cov and packed layouts).Splats.get/Splats.findlook up the splat bundle by explicit name, then by the standard names, then by formats. - Layouts:
SplatFiles.decodedecodes every format intopackedSplatswith an encoding and intocovSplats. The packedencodingis optional: the defaults apply when it is left out, partial fields are filled from the defaults, and an RDX file keeps its own encoding.Splats.convertreturns data already in the layout unchanged, converts data in another layout, shares the other bundles, and renames a bundle without copying its arrays.SplatDatatyping is checked too. - Types:
Splats.loadPbris refused for packed and cov bundles and for untyped ones, and accepted afterSplats.isExt. - Files:
- PLY with and without PBR, point clouds, and SH degrees.
- Compressed PLY round trips in table and Morton order, and older SuperSplat files (without color ranges, with an
shelement). - SPZ versions 2, 3, and 4: PBR and the LoD tree round trip. SPZ 4 at SH degrees 0 and 4, with its records, unpacks to the same columns as version 3 and is detected by its
NGSPheader. Version 4 inside gzip reads as version 3 with SH degree 4. - SPZ extension records: safe orbit, coordinate system (with and without conversion), and unknown types skipped.
SpzFile.unpackof streams in chunks of 1, 7, and random sizes gives the same columns, rejects early ends, and withsh: falsestops before SPZ 4's SH stream.- Tampered SPZ 4 files are rejected: table of contents, stream counts and sizes, trailing bytes, point counts, truncated and corrupt streams.
- RAD with several chunks and a LoD tree, format detection, and
SplatFiles.encode/decodefor every format. - When
splats/spz4/holds SharedTestModels' SPZ 4 samples: each against its gzip version, and the backpack against its PLY.
- zstd: Radix's encoder is read by fzstd and by Node's zstd, and Node's frames by fzstd, on random, skewed, RLE, and mixed data across block edges. A wrong size is refused.
- Axes: all 256 pairs of the 16 coordinate systems round trip; SH matches when seen from the converted direction; quaternions and permuted scales match M Σ Mᵀ; flips change only SH signs.
Axes.converton ext, cov, and packed tables (per-splat SH and a palette) matches converting one splat at a time. - Spherical harmonics:
- Each codec round trips at every degree within its budget: RMS color error at most 0.2 / 255 (ext) and 0.4 / 255 (packed), on values with ±5 outliers. Reading degrees 1 to 3 of a degree-4 entry equals a lower-degree encode.
Sh.findidentifies each degree's channel set (extShA..F,packedShA..E) and rejects other names and partial sets.- Palettes use power-of-two strides by default, and convert and expand.
- Every writer round trips through its reader: PLY, RDX, and RAD
f32exactly; RADf16ands8, compressed PLY, and SPZ within their steps.sh: falseskips SH and records its degree. - Degree 4 round trips through PLY (72
f_rest), SPZ, and RDX; compressed PLY and RAD keep its first 3 bands. - Loading defaults to ext and takes a codec; RDX keeps its codec unless asked.
- RAD clustered SH written from a palette (
f32,f16, ands8codes; labels over several chunks) reads back as that palette, in theshcodec or skipped. The PLY and RDX writers expand it per splat. - A hand-built RAD in Spark's clustered layout (
s8codes in chunk 0,u16thenu32labels) reads as an ext palette of its codes; one with 70,000 entries expands to per-splat SH. Sh.evaluatealong the axes, and for bands 1 to 4 against real SH computed from associated Legendre polynomials.- When
splats/grace-interior-10m-sh3.plyand its SPZ copy exist: the SPZ reader against sampled records of the float PLY. - When
splats/sh4/holdssparkling_waterandscarf(.plyand gzip version-4.spz, CC0, from github.com/raymondyfei/SharedTestModels): each SPZ against its PLY at degree 4. Order and centers match, SH is within half of SPZ's 5-bit (band 1) or 4-bit step plus rounding, and no signs flip.
- Sort keys: keys are monotonic in distance and order splats back to front at 16, 24, and 32 bits, and non-finite distances give invalid keys.
SortKeys.sortis stable, puts invalid keys last, and gives the same order at every key width.ArrayData.INDICEShas the expected definition.
tsconfig.core.json also typechecks the script, so its @ts-expect-error lines test the types.
/build/test/gpu/index.html: GPU checks in the browser#
/build/test/gpu/index.html?gpu=webgpu and ?gpu=webgpu-gl run the checks in test/gpu/: tests.ts, then splatTests.ts, shTests.ts, wgpuTests.ts, renderTests.ts, sceneTests.ts, and controlsTests.ts, with helpers in renderUtils.ts. ?run=0 loads the page without running them. The checks:
- Upload and readback for every GPU format, packed and unpacked.
- Ranges that start in the middle of a row, GPU copies, and capacity growth.
GpuArrayReaderin a fragment shader, compared with the CPU data.- The splat functions that fill
THREE.Vector3,Quaternion, andColor. - Splat readers against CPU decoding for every layout and load function, bound by their default bundle names, and
GpuSplatReader.from(arrays)picking the layout. GpuSplats.store*encoding against the CPU's bits, through the fragment writer and, on WebGPU, through the compute writer. Splats whose code is one step off must still decode within the encoder's budgets.GpuArrayWriterranges across rows and layers, packing 1, 2, and 4,writeBatch, and the writer's limits.GpuArrays.swapwith a rebound reader.- Spherical harmonics (
shTests.ts):GpuShReaderdecodes ext and packed SH likeShon the CPU: per splat at degrees 1 to 4 of a degree-4 bundle, and through palettes of degree 3 (strides 4 and 3) and 4 (strides 8, and 6 or 5).- The projection adds each object's SH from its own view origin, matching the same splats with that color baked in. Two placements are tested, one turned and scaled, and degree 4 per splat.
objectParams.maxShDegreecaps the bands: 1 and 3 on degree-4 data, and 0 matches no SH.- Palettes of both codecs draw in one collection.
- A RAD with clustered SH is written on a worker (the palette's arrays come back).
loadSplatFileloads the same palette on a worker and on the page, andloadSplatsexpands it to the same per-splat SH on both. - A
GpuShReaderin a custom remapped generator matchesfromSplats.
- The projected word codecs: ewa's
depth20within 2^-16 of the depth (clamped to 2^-14 to 2^18) andangle12within π / 8192; the ray methods' center within 5e-6 rad of its direction (octahedron corners, axes, and both sides of the fold included) with its distance, whatever the last head word holds below bit 24. createRendererwithprofile,hdr, anddirectCanvas.loadSplats/writeSplatsgive the same results with everyworkerchoice. A pool makes no workers until it is used, and the data comes back from a failed write.loadSplatsintopackedSplatswith an encoding gives the same bits on a worker and on the current thread. Asignalaborted before the load, or on its first progress report, rejects with its reason on a worker and here, and the pool then loads again; a load queued behind a lease leaves the queue when aborted.requestreachesfetchhere and goes to a worker, aHeadersobject too.
WebGPU only (wgpuTests.ts). These are skipped on WebGL2, where storage: true must throw instead:
WgpuBufferupload, read, clear,copyFrom, growing withkeep, and an atomic counter.WgpuComputedispatched with 5M threads (more than 65535 workgroups), 2D workgroups, and an indirect buffer.WgpuComputeBatchin one pass and split.WgpuArrayWriterfor every storable format at packing 1, 2, and 4: a range across layers withfirst, a batch mixed with a fragment writer, and the same texels as the source and as the fragment writer.- The compute writer's limits, and its refusal of arrays made without
storage(also a type error, checked with@ts-expect-error). WgpuArrayStorewriting in reversed order, writing packed texels, and rebinding afterswap.- Storage arrays: upload, read,
copyFrom,swap, and readers in fragment and compute shaders.
Projection, sorting, and drawing (renderTests.ts; the WebGPU-only checks are skipped on WebGL2):
WgpuRadixSortagainstSortKeys.sortwith 16-, 24-, and 32-bit keys, both scan tiers, and counts of 0, 1, around one block, and 70000 (WebGPU).WgpuCompactwith sort keys read from an RGBA8 texture, followed bysortIndirect, againstSortKeys.sort(WebGPU).- Every projection method on every splat layout with 1 and 2 eyes. The compute and fragment writers give the same words and sort keys (WebGPU). In stereo, with the head placed at one eye, that eye gives its mono words (WebGL2 compilers may round raygs' float c² a few ulps apart).
- A TSL generator that gives only splats, or only covariances, projects like the same splats read from arrays. Two objects of one collection at different offsets equal one object that holds both.
- ewa bounds against a CPU EWA reference, and ewa, 3dgut, and htgs bounds agreeing for small, distant splats.
- Culling behind the camera, off screen, and under
minAlpha, with sort keys marked invalid only whencullKeysasks for it. - Sort keys against
SortKeys.computeat every sort key width, switched by the uniform alone. WgpuSplatSorteragainstSortKeys.sortof the read-back sort keys, with its indirect arguments (WebGPU).sortSplatKeyson a worker and inline. A view'ssortagainstSortKeys.sortof a full projection's sort keys.- Rendered images: one splat gives a centered, symmetric falloff for every method. A small, far splat gives its exact falloff. Two splats blend back to front, and blend the other way with a reversed order. GPU and CPU orders give the same image (WebGPU).
Hdrtone mapping leaves a frame below the knee unchanged. A negative color draws as black into a half-float target, never NaN. An opaque mesh in front of a splat hides it and one behind does not, with and withoutlogarithmicDepthBuffer. A renderer that drew nothing before its splats arrived draws them correctly. A two-eyeArrayCameramatches the mono render of each eye. objectParams.sortGuarddrops the sort keys of splats beyond the widened view and behind the head, and drops none at 0.
Collections, renderers, and views (sceneTests.ts):
isUniformScale: rotations times a uniform scale (and products of them) pass; shear, non-uniform scale, mirrors, and zero fail.- An object's world transform (its
matrixWorld: uniform, non-uniform, or mirrored) against the same splats pre-transformed by it on the CPU, for every method. - The static uniforms: one program dispatched twice in a frame with two matrices gives the same result as each dispatch alone. Two
SplatRenderers in one render each draw their own splats. - The program cache: one program per generator across frames and renderers. Two modifiers build 2 programs, then none. New data, a modifier space, or a moved frame build none. A disposed generator's programs are rebuilt.
SplatCollection.objectrefuses data that does not match its generator's (@ts-expect-error). - Form selection: a covariance-only generator is never diagonalized under ewa, and is diagonalized under 3dgut.
- Modifiers: the modifier that matches the form runs. A view-space depth modifier colors splats by depth. An identity modifier in each space projects like no modifier. A world height modifier colors objects by their world height.
SplatGenerator.fromSplats: an index list or a TSL remap gives the same result as reading those splats directly. New index lists compile nothing. In a subset drawn through an index list (such as a LoD subset),splat.indexstays the index of the source splat. Modifiers run in turn.- Views: a view's collection,
objectParams,nullmodifier, andsortBitsproject like a renderer with those settings; its unset settings follow the renderer;viewForreturns the same view for a key; a disposed view leaves its renderer. A render draws the renderer's settings, its camera's view, or the explicitview; in gpu mode a render makes no view.viewKey: renders from cameras made anew share the view of their key; by default each camera makes one (cpu mode). - The CPU sort: nothing is drawn before an order arrives.
applyskips older versions. A newer order shows its own collection while the renderer's collection has moved on. A later landed order supersedes an earlier one. A switch to gpu mode drops the orders and releases their objects, andapplyrefuses in gpu mode. - Object release: an object is released once, when no renderer's live collection holds it. Kept objects and other renderers still hold their objects. A view's own collection holds its objects until the view is disposed.
dispose()releases the renderer's objects. A release may set a renderer's collection. Shown and in-flight collections (and landed collections not yet applied) hold their objects. - Sort workers: cpu renderers and their views share
SplatRenderer.sortPool(4 workers), sorting in parallel. A gpu renderer refusessort(). A renderer withsortWorkersorts there instead. Both pools outlive their renderers. Disposing the renderer during a sort rejects the sort, keeps no sort key arrays or views, and leaves the shared pool working. A disposed renderer left in the scene draws nothing, makes no textures, and throws nothing,preparetoo, in gpu and cpu modes; itssortKeysand a disposed pool reject. SplatRendererrefuses a view that belongs to another renderer. A render that does not follow itsprepareprojects again (WebGPU).- A camera in the other clip-space convention projects like one in the renderer's convention, and is left unchanged.
autoSortre-sorts on a lens change when asortGuardis set, and does not without one.requestSort()makes it sort again; a request made while a sort is in flight starts one more sort after it finishes. WithautoSort: false, a view sorts only when asked.onInvalidateis called for a request, for an applied order, and once for every view on the renderer'srequestSort().
snorm8 is skipped on WebGL2, where it cannot be read back. npm run check:test runs the page headless on both backends and prints the failures and totals (it needs npm run build:test and npm run serve).
The docs check#
npm run docs (scripts/build-docs.mjs) writes the HTML version of every page to docs/ref/, which is gitignored; npm run build runs it too. npm run check:docs (--check) writes the pages and fails when:
- a relative link or
#anchorin any.mdpage or docs.htmlpage does not resolve (anchors follow GitHub's heading ids); - an export of a Radix
index.tsappears on no.mdpage, in inline code or in a code block.
Benchmarks#
Performance is measured by benchmarks rather than by the checks: npm run bench:core (core benchmarks in Node, on files in splats/ or given with --file) and npm run bench -- --matrix <name> (/build/test/bench/index.html in headless Chrome, on the hosted samples). A summary of the results is in Performance. Radix is compared with SparkJS and PlayCanvas on the same scenes and camera paths in bench.html, the summary, and bench-report.html, every result.
Source: docs/checks.md