Skip to content

fix(layers): stop the layer cache from disposing the source image - #956

Merged
PaulHax merged 5 commits into
Kitware:mainfrom
PaulHax:layer-image-ownership
Sep 21, 2026
Merged

PaulHax merged 5 commits into
Kitware:mainfrom
PaulHax:layer-image-ownership

Conversation

@PaulHax

@PaulHax PaulHax commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

ensureSameSpace returned its input unchanged when two images already shared an
index grid. _addLayer handed that object to addVTKImageData under the layer
id ${parent}::${source}, so two cache entries referenced one vtkImageData.

The cache disposes what it owns, so removing the layer or deleting the parent
called delete() on an image the source dataset still held. vtk.js delete()
clears the model but keeps the closures, so the source's cached image became an
object whose every getter returns undefined, and it stayed reachable from the
cache for the rest of the session. Every later read of that dataset throws:
Cannot read properties of undefined (reading 'getScalars'),
r.getPointData() is undefined, or t.extent is not iterable.

To reproduce: load two images that share a grid, add one as a layer of the
other, remove the layer, then select the image that was the layer source.

addError builds a bug report from the image cache before it creates the
message, so reporting the first error threw and the message was never created.
The user saw no notification at all.

reallocateImage reaches the same end state by a second route: it released the
existing volume before allocating its replacement, so a throw from
allocateImageFromChunks left a released image behind. A re-import reaches this
with the image already in the cache and nothing evicts it on failure.

Change

  • ensureSameSpace returns a copy that shares the candidate's scalar array, so
    callers own what they get back. vtk.js delete() is not recursive, so the
    array outlives either owner and the voxel buffer is not duplicated.
  • The image cache treats a disposed image as absent, so a stale entry left by
    any other path degrades to a blank view rather than a crash.
  • addError falls back to a placeholder bug report rather than throwing.
  • reallocateImage allocates the replacement volume before releasing the old
    buffer, matching the order updateVTKImageData already uses.

ensureSameSpace returned the candidate itself when the grids already
matched, so the layer cached under `${parent}::${source}` and the source
dataset referenced one vtkImageData. The cache disposes what it owns, so
removing the layer or deleting the parent called delete() on an image the
source still held, leaving a wiped model reachable from the cache.

Return a geometry copy that shares the scalar array. vtk.js delete() only
clears the object's own model, so the array survives either owner and the
voxels are not duplicated. Both return paths now hand back an owned
object, so the ownership rule sits with the borrow instead of at each
call site.
A deleted vtkImageData keeps its closures but loses its model, so
getPointData() returns undefined rather than throwing on access. The
existing optional chain guarded the wrapper, not the model, so any image
disposed without being evicted threw on every read.
addError builds its bug report from application state that the error may
have just corrupted, and it does so before the message exists. A throw
there costs the user the message and leaves the global handler reporting
the reporter's own failure.
Layers two images that share a grid, so building the layer reuses the
source image rather than resampling it. Removing the layer and building
it again reads the source image a second time, which fails when removing
the layer released the image the source dataset still owns.

Adds the dataset menu test ids the DICOM browser already carries, and
moves the MetaImage fixture writer next to the other manifest helpers so
the zero spacing spec can share it.
@netlify

netlify Bot commented Sep 21, 2026

Copy link
Copy Markdown

Deploy Preview for volview-dev ready!

Name Link
🔨 Latest commit e6150c6
🔍 Latest deploy log https://app.netlify.com/projects/volview-dev/deploys/6ab0f6930ca1970008df8a4b
😎 Deploy Preview https://deploy-preview-956--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.

reallocateImage deleted the existing image before allocating its
replacement, so a throw from allocateImageFromChunks left the chunk image
holding a deleted vtkImageData. A re-import reaches this with the image
already in the cache and nothing evicts it on failure, leaving every
later read of that volume to find a wiped model.

Allocate first and release the old buffer once the replacement exists,
matching the order updateVTKImageData already uses.
@PaulHax
PaulHax merged commit 068e902 into Kitware:main Sep 21, 2026
8 checks passed
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