Skip to content

Let a consumer supply PagedSplats' ranged reads (closes #423) - #432

Open
xohm wants to merge 1 commit into
sparkjsdev:mainfrom
xohm:feat/paged-fetch-bytes
Open

xohm wants to merge 1 commit into
sparkjsdev:mainfrom
xohm:feat/paged-fetch-bytes

Conversation

@xohm

@xohm xohm commented Sep 16, 2026

Copy link
Copy Markdown

Why

Every RAD read in PagedSplats goes through fetchRange, which is module-private. Two open needs run into that wall from opposite sides:

  • Allow overriding fetchRange to support custom decryption of RAD chunks #423 — serving .rad files with an encryption layer, signed requests, or a custom CDN. The URL is still Spark's; what's needed is to sit in front of the fetch. The reporter is patching dist/spark.module.js by hand on every release.
  • No URL at all — a .rad inside a container at an offset, in OPFS or IndexedDB, or a local file the user picked, readable only through File.slice. On a page opened from file:// there is no server to range against either.

requestHeader and withCredentials cover neither case.

What

export interface FetchRangeRequest {
  url?: string;            // rootUrl, or a chunked RAD's sibling file;
                           // undefined when there is no rootUrl
  offset?: number;         // both undefined asks for the whole file
  bytes?: number;
  requestHeader?: Record<string, string>;
  withCredentials?: boolean;
  signal?: AbortSignal;    // the PagedSplats' own, aborted on dispose
}

export type FetchRange = (req: FetchRangeRequest) => Promise<Uint8Array>;
// #423: decorate the fetch of a URL Spark still owns
new PagedSplats({
  rootUrl: "https://example.com/scene.rad",
  fetchRange: async (req) => decrypt(await myFetch(req)),
});

// a source with no URL: a container member, a picked File, OPFS
new PagedSplats({
  fetchRange: ({ offset, bytes }) => container.read(member, offset, bytes),
  fileType: SplatFileType.RAD,
});
  • The request carries the requestHeader and withCredentials the PagedSplats was built with, so a replacement can honour them rather than reimplement them.
  • Returning fewer bytes than asked for means the file ended; the header probe uses that to stop backing off on a small file. Returning more is not allowed.
  • With no rootUrl, fileType must be given — there are no bytes to sniff before the first read — and the error says so.
  • RAD only. The rootUrl and fileBytes paths are untouched; nothing changes for anyone who does not pass the hook.

Every RAD read now goes through one routing helper, so the hook covers the header probe, single-file chunks and sibling-file chunks alike. One fix falls out of that: the built-in path now passes the abort signal on the header probe, which it did not before.

Verified

Built from this branch and driven in headless Chromium (SwiftShader) against a 106 MB archive holding a .rad member, with no rootUrl: the hook supplies the header and the chunk reads, the first chunk decodes to 48,196 splats, and opening the file costs 18 KB of reads. The same hook backs a file:// page with no server, through File.slice.

PagedSplats can page a RAD that is a plain ranged URL, or one already whole
in memory, and fetchRange is module-private, so there is no way to sit in
front of the reads. Two open needs run into that: decrypting or otherwise
customising the fetch of a URL that Spark still owns (sparkjsdev#423, where the
reporter patches dist by hand on every release), and paging a RAD that has
no URL at all - a member inside a container, a picked File, OPFS, a page
opened from file:// with no server to range against.

One hook serves both. fetchRange(req) is handed the file to read - rootUrl,
or a chunked RAD's sibling - the range, the requestHeader and
withCredentials PagedSplats was built with, and its own abort signal. The
url is undefined when there is no rootUrl, which is the case where the
consumer itself is the source. A short return means the file ended, which
lets the header probe stop backing off on a small file.

Every RAD read now goes through one routing helper, so the hook covers the
header probe, single-file chunks and sibling-file chunks alike, and the
built-in path picks up the abort signal on the header probe, which it did
not pass before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant