Repository navigation
Conversation
The decoder kept the zip buffer, the compressed file cache, all decoded RGBA images and full-size f32 arrays alive at the same time, which exceeds the 4 GiB wasm32 limit on large files (~40M splats). - finish() moves the zip buffer into decode_sogs, dropped after preload - cache entries are removed on their last use - compressed bytes are dropped right after each image decode - splats are emitted in MAX_SPLAT_CHUNK blocks via set_batch, with reused buffers; shN centroids/labels are loaded once and decoded per block - tests: 70k-splat v1/v2 SOG zips with 3 SH bands, compared against a per-splat reference Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| let mut file_cache: HashMap<String, Vec<u8>> = HashMap::new(); | ||
| for f in meta_files(&meta) { | ||
| preload_file(&mut zip, &prefix, &mut file_cache, f)?; | ||
| } |
There was a problem hiding this comment.
The files in file_cache will exclusively be images. Changing it to a HashMap<String, ImageData> and moving the image decoding here could simplify things a bit. The raw file/image buffer can be dropped after decoding, eliminating the need to keep track of uses and mutating the cache.
| } | ||
| } | ||
|
|
||
| fn meta_files(meta: &PcSogsRoot) -> Vec<&String> { |
There was a problem hiding this comment.
Nitpick: naming could be improved as the returned file names are the image files referenced in the meta file, not a list of "meta files".
| sh1: sh(1, 9), | ||
| sh2: sh(2, 15), | ||
| sh3: sh(3, 21), |
There was a problem hiding this comment.
Nitpick: I think it's fine to inline and repeat the sh function here.
| }; | ||
| let mut base = 0usize; | ||
| while base < num_splats { | ||
| let cnt = (num_splats - base).min(MAX_SPLAT_CHUNK); |
| let i4 = i * 4; | ||
| ) { | ||
| for i in 0..out_center.len() / 3 { | ||
| let i4 = (base + i) * 4; |
There was a problem hiding this comment.
Not too fond of i4 and i3 being "out of sync" here and in the other decode methods. Ideally it remains a simple shorthand for i * 4. Perhaps using a slice of img0/1.rgba or computing a "base offset" base * 4 and using that alongside i4?
| let lz = build_lookup(mins[2], maxs[2]); | ||
| for i in 0..num_splats { | ||
| let i4 = i * 4; | ||
| /// v2: same codebook on all 3 axes; v1: per-axis min/max lookup |
There was a problem hiding this comment.
No need for this comment IMHO, it makes more sense at the call-site, though the fact that v2 has a single codebook for all three axes is pretty self-evident at that point in the code.
|
Changes look good for the most part, left some comments. There is also #389, which hasn't been merged, but would change the memory characteristics of SOGs loading as well. Effectively moving the unzipping and image decoding to the JS side, which likewise would keep the wasm memory usage lower. Although it doesn't have the chunked processing, meaning peak memory usage would still spike somewhat. Regardless it's a good idea to no keep the zip buffer in-memory when decompression has finished (or the raw image buffers after image decoding). Even with the above linked PR this would still be the case for non-web contexts like |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks a lot for the careful review, and for the time you put into Spark in general, it's been a great base to build on. All points are addressed:
Re #389: I see it as complementary. Moving unzip/decode to JS helps the web path, but chunked processing still bounds peak memory there, and Thanks again, the suggestions made the code noticeably simpler. |
Fixes #490
Cause
SogsDecoderkept everything alive at the same time: the zip buffer, the cache of compressed files, the five decoded RGBA images, full-size f32 arrays for every property, plus the receivingPackedSplats. On a 39.5M-splat SOG this adds up to about 4.5 GB, over the 4 GiB wasm32 address space. The allocation failure aborts, which surfaces asunreachablein the worker.Changes (
rust/spark-lib/src/sogs.rsonly)finish()takes the buffer withstd::mem::takeand passes it by value; the zip bytes are dropped right after the files are preloaded.get_fileremoves an entry from the cache on its last use (use count per name taken frommeta.json).MAX_SPLAT_CHUNKblocks with reused buffers, oneset_batchper block.Results (Chrome, SOG v2, 39,548,487 splats, 415 MB, no shN)
unreachable)No regression on a 9.3M-splat SOG and on a streamed SOG.
Tests
Two new tests build 70,000-splat SOG zips in memory (more than one chunk, last one partial), v1 and v2, with 3 shN bands, feed them to
SogsDecoderin several pushes and compare every value against a per-splat reference (relative tolerance 1e-6).The memory64 fix proposed in #490 is independent and not included here.