RDX files#

RDX is Radix's own file format. It stores the bundles of an ArrayData, compressed, in chunks of rows. Any table can be saved, including tables that hold no splats. A file can be read one chunk at a time over HTTP range requests, so a large scene can start drawing before it has fully loaded.

Code: src/core/rdx.ts (RdxFile) and compress.ts (Compression), in @sparkjsdev/radix/core.

// One file of every serialized bundle, and the same table back
const file = await RdxFile.encode(data);
const loaded = await RdxFile.decode(file);

// Over HTTP range requests, into an empty table of the right shape
const rdx = await RdxFile.load("scene.rdx");
const target = RdxFile.makeData(rdx.header);
for (let c = 0; c < rdx.header.chunks.length; c++) {
  await rdx.readChunk(c, target);
}

The layout#

whole file:  [ header ][ chunk 0 ][ chunk 1 ] ...
chunked:     scene.rdx (the header), scene-0.rdx, scene-1.rdx, ...

a container (the header and every chunk):
  u32 magic | u32 meta bytes | u32 payload count | u32 0
    (magic: "RDXH" for the header, "RDXC" for a chunk)
  meta: JSON, padded to 16 bytes
  u32 size per payload, padded to 16 bytes
  payloads, each padded to 16 bytes

Saving#

MemberWhat it does
RdxFile.encode(data, { chunkSize?, progress? })one file: the header, then every chunk
RdxFile.encodeChunks(data, options?){ header, chunks }: the pieces as separate byte arrays
RdxFile.encodeFiles(data, "scene.rdx", options?)a chunked set by file name: { "scene.rdx": header, "scene-0.rdx": chunk 0, ... }
RdxFile.chunkName("scene.rdx", 3)"scene-3.rdx"; a query or hash after .rdx is kept

writeSplats(data, "rdx") writes a whole file on a worker (Splat files). SH bundles (extSh, packedSh, a palette's shLabel) are saved like any other bundle, in their codec, and load in that codec unless sh asks for another. An SH palette (ShPalette.data) is a separate table: save it as its own RDX file, or pass it to writeSplats as shPalette to write the SH per splat (Spherical harmonics).

Loading#

MemberWhat it does
RdxFile.decode(bytes, { progress? })a whole file to a table
RdxFile.load(url, { chunked?, init? })an RdxReader over HTTP: one request for the header and one per chunk. chunked: true reads a chunked set, one file per chunk. init is passed to fetch
RdxFile.open(readRange)an RdxReader over any (start, end) => Promise<bytes>
RdxFile.makeData(header)an empty table of a header's bundles, count, and meta
RdxFile.decodeHeader(bytes){ header, byteLength }: the header at the start of bytes; chunk 0 of a whole file starts at byteLength
RdxFile.decodeChunk(header, bytes, into)writes one chunk's rows into into

An RdxReader has header and readChunk(chunk, into). Chunks can be read in any order.

Streaming into the GPU#

// A file of packed splats
const rdx = await RdxFile.load(url);
const { count, chunkSize, chunks } = rdx.header;
const data = RdxFile.makeData(rdx.header);

const splats = new GpuArrays({
  renderer,
  bundles: { packedSplats: Splats.PACKED },
  capacity: count,
});

for (let c = 0; c < chunks.length; c++) {
  await rdx.readChunk(c, data);
  const start = c * chunkSize;
  splats.upload(data, {
    bundles: ["packedSplats"],
    srcStart: start,
    dstStart: start,
    count: Math.min(chunkSize, count - start),
  });

  // Draw the splats loaded so far
  const object = SplatCollection.arrays(splats);
  splatRenderer.collection = new SplatCollection([object]);
}

The arrays' count grows with each upload, and each new collection draws the splats uploaded so far. A packed bundle's encoding is in its meta, and upload copies it.

Gotchas#

Source: docs/rdx.md