Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/Rendering/OpenGL/ImageCPRMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ function vtkOpenGLImageCPRMapper(publicAPI, model) {
model.classHierarchy.push('vtkOpenGLImageCPRMapper');

function unregisterGraphicsResources(renderWindow) {
// The tris helper is not shared
model.tris.releaseGraphicsResources(renderWindow);
[model._scalars, model._colorTransferFunc, model._pwFunc].forEach(
(coreObject) =>
renderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { it, expect } from 'vitest';
import testUtils from 'vtk.js/Sources/Testing/testUtils';
import { createTrackedRenderView } from 'vtk.js/Sources/Testing/renderTestUtils';

import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageCPRMapper from 'vtk.js/Sources/Rendering/Core/ImageCPRMapper';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkImageSlice from 'vtk.js/Sources/Rendering/Core/ImageSlice';
import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction';
import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData';

const sideLen = 32;
const width = 20;

// A real transfer function and piecewise function put the color and opacity
// lookup textures on the render window's shared cache, which already frees
// them on removal. That isolates this test to the tris VAO and vertex
// buffer, the pair of GL objects the mapper never releases.
function renderCPRSlice(gc) {
const tracked = createTrackedRenderView(gc);

const imageData = vtkImageData.newInstance();
imageData.setDimensions(sideLen, sideLen, sideLen);
imageData.getPointData().setScalars(
vtkDataArray.newInstance({
name: 'scalars',
values: Uint16Array.from({ length: sideLen ** 3 }, (_, i) => i % 4096),
})
);

// A straight centerline through the middle of the volume
const centerline = vtkPolyData.newInstance();
centerline
.getPoints()
.setData(Float32Array.from([16, 16, 2, 16, 16, 16, 16, 16, 29]), 3);
centerline.getLines().setData(Uint16Array.from([3, 0, 1, 2]));

const color = gc.registerResource(vtkColorTransferFunction.newInstance());
color.addRGBPoint(0, 0, 0, 0);
color.addRGBPoint(4095, 1, 1, 1);
const opacity = gc.registerResource(vtkPiecewiseFunction.newInstance());
opacity.addPoint(0, 1);
opacity.addPoint(4095, 1);

const mapper = gc.registerResource(vtkImageCPRMapper.newInstance());
mapper.setUseUniformOrientation(true);
mapper.setImageData(imageData);
mapper.setCenterlineData(centerline);
mapper.setWidth(width);
const actor = gc.registerResource(vtkImageSlice.newInstance());
actor.setMapper(mapper);
actor.getProperty().setRGBTransferFunction(0, color);
actor.getProperty().setPiecewiseFunction(0, opacity);

tracked.renderer.addActor(actor);
tracked.renderer.resetCamera();
tracked.renderWindow.render();
return { ...tracked, actor };
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees its vertex buffer and VAO when the slice leaves the scene',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderer, renderWindow, actor, emptySceneObjects } =
renderCPRSlice(gc);
expect(tracker.count()).toBeGreaterThan(emptySceneObjects);

renderer.removeActor(actor);
renderWindow.render();
expect(tracker.count()).toBe(emptySceneObjects);

gc.releaseResources();
}
);

// Every voxel is the same bright value, so any pixel inside the slice reads
// back near white regardless of exactly where the camera frames it. The
// background is pure blue, which no grayscale transfer function can produce,
// so a red channel near zero means nothing was drawn.
function createUniformCPRActor(gc) {
const imageData = vtkImageData.newInstance();
imageData.setDimensions(sideLen, sideLen, sideLen);
imageData.getPointData().setScalars(
vtkDataArray.newInstance({
name: 'scalars',
values: Uint16Array.from({ length: sideLen ** 3 }, () => 4095),
})
);

const centerline = vtkPolyData.newInstance();
centerline
.getPoints()
.setData(Float32Array.from([16, 16, 2, 16, 16, 16, 16, 16, 29]), 3);
centerline.getLines().setData(Uint16Array.from([3, 0, 1, 2]));

const color = gc.registerResource(vtkColorTransferFunction.newInstance());
color.addRGBPoint(0, 0, 0, 0);
color.addRGBPoint(4095, 1, 1, 1);
const opacity = gc.registerResource(vtkPiecewiseFunction.newInstance());
opacity.addPoint(0, 1);
opacity.addPoint(4095, 1);

const mapper = gc.registerResource(vtkImageCPRMapper.newInstance());
mapper.setUseUniformOrientation(true);
mapper.setImageData(imageData);
mapper.setCenterlineData(centerline);
mapper.setWidth(width);
const actor = gc.registerResource(vtkImageSlice.newInstance());
actor.setMapper(mapper);
actor.getProperty().setRGBTransferFunction(0, color);
actor.getProperty().setPiecewiseFunction(0, opacity);
return actor;
}

function readCenterPixel(view) {
const gl = view.getContext();
const pixel = new Uint8Array(4);
gl.readPixels(100, 100, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
return pixel;
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'keeps drawing the slice after its renderer moves to a different render window',
() => {
const gc = testUtils.createGarbageCollector();
const windowA = createTrackedRenderView(gc);
windowA.renderer.setBackground(0, 0, 1);

const actor = createUniformCPRActor(gc);
windowA.renderer.addActor(actor);
windowA.renderer.resetCamera();
windowA.renderWindow.render();
expect(readCenterPixel(windowA.view)[0]).toBeGreaterThan(200);

const objectsWithActorInA = windowA.tracker.count();

// A second render window, sharing nothing with the first. Moving the
// renderer (not just the actor) into it is what makes the mapper's own
// buildPass see a different vtkOpenGLRenderWindow ancestor.
const windowB = createTrackedRenderView(gc);
windowB.renderer.setBackground(0, 0, 1);

const { renderer } = windowA;
windowA.renderWindow.removeRenderer(renderer);
windowB.renderWindow.removeRenderer(windowB.renderer);
windowB.renderWindow.addRenderer(renderer);
renderer.resetCamera();
windowB.renderWindow.render();

expect(readCenterPixel(windowB.view)[0]).toBeGreaterThan(200);

// Give window A's view tree a chance to prune the renderer it no longer
// has, which is where the departed mapper's resources get released.
windowA.renderWindow.render();
expect(windowA.tracker.count()).toBe(objectsWithActorInA);

gc.releaseResources();
}
);
3 changes: 2 additions & 1 deletion Sources/Rendering/OpenGL/ImageMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ function vtkOpenGLImageMapper(publicAPI, model) {
model.classHierarchy.push('vtkOpenGLImageMapper');

function unregisterGraphicsResources(renderWindow) {
// The openGLTexture is not shared
// The openGLTexture and the tris helper are not shared
model.openGLTexture.releaseGraphicsResources(renderWindow);
model.tris.releaseGraphicsResources(renderWindow);
// All these other resources are shared
[
model._colorTransferFunc,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { it, expect } from 'vitest';
import testUtils from 'vtk.js/Sources/Testing/testUtils';
import { createTrackedRenderView } from 'vtk.js/Sources/Testing/renderTestUtils';

import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
import vtkImageMapper from 'vtk.js/Sources/Rendering/Core/ImageMapper';
import vtkImageSlice from 'vtk.js/Sources/Rendering/Core/ImageSlice';
import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction';
import { SlicingMode } from 'vtk.js/Sources/Rendering/Core/ImageMapper/Constants';

const sideLen = 32;
const sliceCount = 16;

// A real transfer function and piecewise function put the color and opacity
// lookup textures on the render window's shared cache, which already frees
// them on removal. That isolates this test to the tris VAO and vertex
// buffer, the pair of GL objects the mapper never releases.
function renderImageSlice(gc) {
const tracked = createTrackedRenderView(gc);

const imageData = vtkImageData.newInstance();
imageData.setDimensions(sideLen, sideLen, sliceCount);
imageData.getPointData().setScalars(
vtkDataArray.newInstance({
name: 'scalars',
values: Uint16Array.from(
{ length: sideLen * sideLen * sliceCount },
(_, i) => i % 4096
),
})
);

const color = gc.registerResource(vtkColorTransferFunction.newInstance());
color.addRGBPoint(0, 0, 0, 0);
color.addRGBPoint(4095, 1, 1, 1);
const opacity = gc.registerResource(vtkPiecewiseFunction.newInstance());
opacity.addPoint(0, 1);
opacity.addPoint(4095, 1);

const mapper = gc.registerResource(vtkImageMapper.newInstance());
mapper.setInputData(imageData);
mapper.setSlicingMode(SlicingMode.K);
const actor = gc.registerResource(vtkImageSlice.newInstance());
actor.setMapper(mapper);
actor.getProperty().setRGBTransferFunction(0, color);
actor.getProperty().setPiecewiseFunction(0, opacity);

tracked.renderer.addActor(actor);
tracked.renderer.resetCamera();
tracked.renderWindow.render();
return { ...tracked, actor };
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees its vertex buffer and VAO when the slice leaves the scene',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderer, renderWindow, actor, emptySceneObjects } =
renderImageSlice(gc);
expect(tracker.count()).toBeGreaterThan(emptySceneObjects);

renderer.removeActor(actor);
renderWindow.render();
expect(tracker.count()).toBe(emptySceneObjects);

gc.releaseResources();
}
);

// Every voxel is the same bright value, so any pixel inside the slice reads
// back near white regardless of exactly where the camera frames it. The
// background is pure blue, which no grayscale transfer function can produce,
// so a red channel near zero means nothing was drawn.
function createUniformImageSliceActor(gc) {
const imageData = vtkImageData.newInstance();
imageData.setDimensions(sideLen, sideLen, sliceCount);
imageData.getPointData().setScalars(
vtkDataArray.newInstance({
name: 'scalars',
values: Uint16Array.from(
{ length: sideLen * sideLen * sliceCount },
() => 4095
),
})
);

const color = gc.registerResource(vtkColorTransferFunction.newInstance());
color.addRGBPoint(0, 0, 0, 0);
color.addRGBPoint(4095, 1, 1, 1);
const opacity = gc.registerResource(vtkPiecewiseFunction.newInstance());
opacity.addPoint(0, 1);
opacity.addPoint(4095, 1);

const mapper = gc.registerResource(vtkImageMapper.newInstance());
mapper.setInputData(imageData);
mapper.setSlicingMode(SlicingMode.K);
const actor = gc.registerResource(vtkImageSlice.newInstance());
actor.setMapper(mapper);
actor.getProperty().setRGBTransferFunction(0, color);
actor.getProperty().setPiecewiseFunction(0, opacity);
return actor;
}

function readCenterPixel(view) {
const gl = view.getContext();
const pixel = new Uint8Array(4);
gl.readPixels(100, 100, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
return pixel;
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'keeps drawing the slice after its renderer moves to a different render window',
() => {
const gc = testUtils.createGarbageCollector();
const windowA = createTrackedRenderView(gc);
windowA.renderer.setBackground(0, 0, 1);

const actor = createUniformImageSliceActor(gc);
windowA.renderer.addActor(actor);
windowA.renderer.resetCamera();
windowA.renderWindow.render();
expect(readCenterPixel(windowA.view)[0]).toBeGreaterThan(200);

const objectsWithActorInA = windowA.tracker.count();

// A second render window, sharing nothing with the first. Moving the
// renderer (not just the actor) into it is what makes the mapper's own
// buildPass see a different vtkOpenGLRenderWindow ancestor.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update this comment to match what the test exercises. Moving the renderer creates a fresh mapper view node in window B and deletes the old one in window A; it does not reach the surviving mapper's render-window-switch branch. The PR description explains this correctly. The same comment appears in the CPR and reslice tests.

const windowB = createTrackedRenderView(gc);
windowB.renderer.setBackground(0, 0, 1);

const { renderer } = windowA;
windowA.renderWindow.removeRenderer(renderer);
windowB.renderWindow.removeRenderer(windowB.renderer);
windowB.renderWindow.addRenderer(renderer);
renderer.resetCamera();
windowB.renderWindow.render();

expect(readCenterPixel(windowB.view)[0]).toBeGreaterThan(200);

// Give window A's view tree a chance to prune the renderer it no longer
// has, which is where the departed mapper's resources get released.
windowA.renderWindow.render();
expect(windowA.tracker.count()).toBe(objectsWithActorInA);

gc.releaseResources();
}
);
2 changes: 2 additions & 0 deletions Sources/Rendering/OpenGL/ImageResliceMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ function vtkOpenGLImageResliceMapper(publicAPI, model) {
}

function unregisterGraphicsResources(renderWindow) {
// The tris helper is not shared
model.tris.releaseGraphicsResources(renderWindow);
// Convert to an array using the spread operator as Firefox doesn't support Iterator.forEach()
[...graphicsResourceReferenceCount.keys()].forEach((coreObject) =>
renderWindow.unregisterGraphicsResourceUser(coreObject, publicAPI)
Expand Down
Loading
Loading