Skip to content

fix(dicom): preserve slices across incremental imports - #935

Open
PaulHax wants to merge 1 commit into
dicom-volume-bufferfrom
dicom-chunk-index
Open

fix(dicom): preserve slices across incremental imports#935
PaulHax wants to merge 1 commit into
dicom-volume-bufferfrom
dicom-chunk-index

Conversation

@PaulHax

@PaulHax PaulHax commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Depends on #934. This PR targets dicom-volume-buffer so its diff contains only the incremental-import changes.

Problem

addChunks appends the new chunks, re-sorts the whole list, and reallocates the
volume buffer. A second call is reachable when importing more slices of an
already-open series, and it discarded pixels that had already loaded:

  • Previously decoded slices were zeroed by the reallocation and never rewritten,
    yet chunkStatus still reported them Loaded.

A related race sits inside the decode itself. onChunkHasData captures its
index before awaiting the decode, and addChunks can re-sort and reallocate
while that await is outstanding, so the index it writes through may no longer
belong to the chunk it decoded.

The old processNewChunks path also indexed already-loaded incoming chunks by
their position in the arrival array rather than the sorted volume. Current UI
imports hand over metadata-only chunks, so that particular case was a latent API
trap rather than a user-reachable path. The reallocation loss and in-flight
decode race are reachable through the current incremental-import path.

The import flow had a gap of its own: the image cache only calls startLoad
when it first registers an image, so chunks added to an already-registered
image were sorted and allocated for but never asked to load their data. Their
slices stayed zero until something else triggered the load.

These failures were identified through code inspection and reproduced with
controlled unit tests; they were not traced to a reported user dataset.

Change

processNewChunks becomes processLoadedChunks, which walks this.chunks in
sorted order after the reallocation and redecodes every chunk that already holds
data. Chunks that arrive later still emit doneData and take the normal path;
chunks whose data is already in memory never will, so their slices are rewritten
here.

onChunkHasData recomputes the chunk's slot from this.chunks after the decode
resolves, and uses that for the write offset, the status update, and the emitted
extent. A chunk no longer in the list belongs to a disposed or superseded image
and its result is dropped rather than written into someone else's slice.

addChunks marked every chunk holding data as Loaded before reallocating, but
reallocating zeroes their slices and only the redecode that follows rewrites
them. Between the two the image reported complete over an empty buffer. Those
chunks are Loading until their slice lands.

A decode that settles after a reallocation belongs to the old buffer generation.
Its result is now discarded rather than allowed to overwrite the replacement
attempt's status or emit an obsolete error. Current-generation failures address
chunks rather than captured indices and resolve the slot when the result lands,
so a failure cannot mark a healthy slice errored.

addChunks calls are serialized through a promise queue. Two overlapping calls
each pushed their chunks and awaited a sort of the shared list, and whichever
sort settled last won, so an older sort could drop the chunks a newer call had
added. Each addition now waits for the one before it, and a failed addition
does not block the next.

importChunks in the DICOM store asks an already-registered image to
startLoad after adding to it, so the new chunks load. Chunk.loadData is a
no-op for chunks that already hold data, so the chunks kept across the
re-import are not fetched again.

The constructor seam for splitAndSort and the ITK read, introduced with the
buffer-type change, lets a unit test drive addChunks in two batches with
arrival order differing from sorted order and assert on decoded pixel
placement and the emitted extents.

Adding chunks to an existing DICOM image re-sorts the list and reallocates
the volume buffer. Slices already decoded were zeroed by the reallocation
and never rewritten while their status still said Loaded. A decode in
flight across that reallocation wrote through the slot it captured before
the sort, so its pixels or its error could land on another chunk's slice.
Two overlapping additions each awaited a sort of the shared list, and the
sort that settled last won, dropping the newer call's chunks. The image
cache also starts a load only when it first registers an image, so chunks
added on a re-import were allocated for but never asked to load.

Redecode every chunk that holds data after a reallocation and report those
slices as Loading until the rewrite lands. Resolve a chunk's slot after its
decode settles, and discard any result from a previous allocation
generation, so completion order cannot overwrite current state. Address
failures by chunk rather than by captured index. Serialize additions
through a promise queue. Ask an already-registered image to start loading
after a re-import adds to it.
@netlify

netlify Bot commented Sep 2, 2026

Copy link
Copy Markdown

Deploy Preview for volview-dev ready!

Name Link
🔨 Latest commit 0aaa947
🔍 Latest deploy log https://app.netlify.com/projects/volview-dev/deploys/6a979a0f355f5000082f43c5
😎 Deploy Preview https://deploy-preview-935--volview-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

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