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
22 changes: 22 additions & 0 deletions Sources/Rendering/OpenGL/Glyph3DMapper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,28 @@ function vtkOpenGLGlyph3DMapper(publicAPI, model) {
);
}
};

const glyphBuffers = () =>
[
model.matrixBuffer,
model.normalBuffer,
model.colorBuffer,
model.pickBuffer,
].filter(Boolean);

publicAPI.getAllocatedGPUMemoryInBytes = () =>
superClass.getAllocatedGPUMemoryInBytes() +
glyphBuffers().reduce(
(memUsed, buffer) => memUsed + buffer.getAllocatedGPUMemoryInBytes(),
0
);

publicAPI.releaseGraphicsResources = (renderWindow) => {
glyphBuffers().forEach((buffer) => buffer.releaseGraphicsResources());
// glyph buffers re-upload only when the renderable is newer than this
model.glyphBOBuildTime.set({ mtime: 0 });
superClass.releaseGraphicsResources(renderWindow);
};
}

// ----------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { it, expect } from 'vitest';
import testUtils from 'vtk.js/Sources/Testing/testUtils';
import {
createTrackedRenderView,
expectSameImageAfterRelease,
} from 'vtk.js/Sources/Testing/renderTestUtils';

import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor';
import vtkConeSource from 'vtk.js/Sources/Filters/Sources/ConeSource';
import vtkGlyph3DMapper from 'vtk.js/Sources/Rendering/Core/Glyph3DMapper';
import vtkPlaneSource from 'vtk.js/Sources/Filters/Sources/PlaneSource';

function createGlyphActor(gc) {
const planeSource = gc.registerResource(vtkPlaneSource.newInstance());
const coneSource = gc.registerResource(vtkConeSource.newInstance());
const mapper = gc.registerResource(vtkGlyph3DMapper.newInstance());
mapper.setInputConnection(planeSource.getOutputPort(), 0);
mapper.setInputConnection(coneSource.getOutputPort(), 1);
const actor = gc.registerResource(vtkActor.newInstance());
actor.setMapper(mapper);
return actor;
}

// Glyphs add per-instance matrix, normal, color and pick buffers.
it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees the GPU objects of a glyph actor removed from a view',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderer, renderWindow, emptySceneObjects } =
createTrackedRenderView(gc);

const actor = createGlyphActor(gc);
renderer.addActor(actor);
renderer.resetCamera();
renderWindow.render();
expect(tracker.count()).toBeGreaterThan(emptySceneObjects);

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

gc.releaseResources();
}
);

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'rebuilds the same image after releaseGraphicsResources',
() => expectSameImageAfterRelease(createGlyphActor)
);
13 changes: 13 additions & 0 deletions Sources/Rendering/OpenGL/Helper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,17 @@ export const newInstance = macro.newInstance(extend);

// ----------------------------------------------------------------------------

// Shared by the poly data mappers, whose primitives are vtkHelper instances.
export function releasePolyDataMapperResources(publicAPI, model, renderWindow) {
model.primitives.forEach((prim) => prim.releaseGraphicsResources());
model.internalColorTexture?.releaseGraphicsResources(
renderWindow ?? model._openGLRenderWindow
);
// Force the buffers to be rebuilt on the next render
model.VBOBuildString = null;
publicAPI.modified();
}

// ----------------------------------------------------------------------------

export default { newInstance, extend, primTypes };
7 changes: 6 additions & 1 deletion Sources/Rendering/OpenGL/PolyDataMapper/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ preparefor rendering

do the rendering

### releaseGraphicsResources(openGLRenderWindow)

Release the GPU resources owned by this OpenGL mapper view node. If the view
node remains active, the resources are recreated as needed on a subsequent
render.

## Shader customization

In order to provide specific properties for rendering, you will have to add
Expand Down Expand Up @@ -144,4 +150,3 @@ mapperSpecificProp.ShaderCallbacks.push({

Defined 'userData' will be the first parameters which will be passed to the callback.
These callbacks will be executed when updateShaders() of OpenGLPolyDataMapper is called.

12 changes: 11 additions & 1 deletion Sources/Rendering/OpenGL/PolyDataMapper/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { mat3, mat4, vec3 } from 'gl-matrix';

import * as macro from 'vtk.js/Sources/macros';
import vtkHelper from 'vtk.js/Sources/Rendering/OpenGL/Helper';
import vtkHelper, {
releasePolyDataMapperResources,
} from 'vtk.js/Sources/Rendering/OpenGL/Helper';
import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
import vtkOpenGLTexture from 'vtk.js/Sources/Rendering/OpenGL/Texture';
Expand Down Expand Up @@ -2018,6 +2020,14 @@ function vtkOpenGLPolyDataMapper(publicAPI, model) {
// Return in MB
return memUsed;
};

publicAPI.releaseGraphicsResources = (renderWindow) =>
releasePolyDataMapperResources(publicAPI, model, renderWindow);

publicAPI.delete = macro.chain(
() => publicAPI.releaseGraphicsResources(),
publicAPI.delete
);
}

// ----------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { it, expect } from 'vitest';
import testUtils from 'vtk.js/Sources/Testing/testUtils';
import {
createTrackedRenderView,
expectSameImageAfterRelease,
} from 'vtk.js/Sources/Testing/renderTestUtils';

import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor';
import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction';
import vtkConeSource from 'vtk.js/Sources/Filters/Sources/ConeSource';
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper';
import vtkOpenGLRenderWindow from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow';
import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer';
import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow';

function createConeActor(gc) {
const cone = gc.registerResource(vtkConeSource.newInstance());
const mapper = gc.registerResource(vtkMapper.newInstance());
mapper.setInputConnection(cone.getOutputPort());
const actor = gc.registerResource(vtkActor.newInstance());
actor.setMapper(mapper);
return actor;
}

// A lookup table makes the mapper own a color texture.
function createScalarColoredConeActor(gc) {
const cone = gc.registerResource(vtkConeSource.newInstance());
cone.update();
const polyData = cone.getOutputData();
const pointCount = polyData.getPoints().getNumberOfPoints();
polyData.getPointData().setScalars(
vtkDataArray.newInstance({
name: 'scalars',
values: Float32Array.from({ length: pointCount }, (_, i) => i),
})
);

const lookupTable = gc.registerResource(
vtkColorTransferFunction.newInstance()
);
lookupTable.addRGBPoint(0, 0, 0, 1);
lookupTable.addRGBPoint(pointCount, 1, 0, 0);

const mapper = gc.registerResource(vtkMapper.newInstance());
mapper.setInputData(polyData);
mapper.setLookupTable(lookupTable);
mapper.setUseLookupTableScalarRange(true);
mapper.setInterpolateScalarsBeforeMapping(true);
const actor = gc.registerResource(vtkActor.newInstance());
actor.setMapper(mapper);
return actor;
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees the GPU objects of actors removed from a view',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderer, renderWindow, emptySceneObjects } =
createTrackedRenderView(gc);

const actors = [
createConeActor(gc),
createConeActor(gc),
createScalarColoredConeActor(gc),
];
actors.forEach((actor) => renderer.addActor(actor));
renderer.resetCamera();
renderWindow.render();
expect(tracker.count()).toBeGreaterThan(emptySceneObjects);

actors.forEach((actor) => renderer.removeActor(actor));
renderWindow.render();
expect(tracker.count()).toBe(emptySceneObjects);

gc.releaseResources();

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.

Since this is inside an async call, this may never get called if the test fails early leaking resources. Recommend enclosing inside an afterEach to ensure to ensure cleanup.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

thanks for taking a look!

Agreed, a trailing gc.releaseResources() is skipped whenever the test doesn't reach the end.

Put the cleanup in createGarbageCollector instead of afterEach:

onTestFinished(releaseResources);

expectSameImageAfterRelease creates its own gc, so an afterEach in the test file would have nothing to release. Registering it in createGarbageCollector covers all three spots and matches createWebGPUTestDevice in the same file. Releasing twice is a no-op.

}
);

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees only the closed view GPU objects on a shared context',
async () => {
const gc = testUtils.createGarbageCollector();
const tracker = testUtils.trackWebGLObjects();

const rootRenderWindow = gc.registerResource(vtkRenderWindow.newInstance());
const rootView = gc.registerResource(vtkOpenGLRenderWindow.newInstance());
rootRenderWindow.addView(rootView);
rootView.initialize();

// The child render windows, their view nodes and their renderers are not
// gc-registered: the closing one is deleted explicitly below.
const addChildView = () => {
const childRenderWindow = vtkRenderWindow.newInstance();
rootRenderWindow.addRenderWindow(childRenderWindow);
const childView = rootView.addMissingNode(childRenderWindow);
childRenderWindow.addView(childView);
childView.setContainer(testUtils.createRenderContainer(gc));
childView.setSize(200, 200);

const renderer = vtkRenderer.newInstance();
childRenderWindow.addRenderer(renderer);
renderer.addActor(createConeActor(gc));
renderer.resetCamera();

return { childRenderWindow, childView };
};

const closing = addChildView();
const surviving = addChildView();
rootRenderWindow.render();

const bothViewsObjects = tracker.count();
const survivingBefore = surviving.childView.captureNextImage();
rootRenderWindow.render();
expect(tracker.count()).toBe(bothViewsObjects);

rootRenderWindow.removeRenderWindow(closing.childRenderWindow);
closing.childRenderWindow.delete();

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.

Wouldn't this lead to double deletion if the garbage collector already tracks resources?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No. The child render windows aren't gc-registered, and neither are their view nodes or renderers. The gc holds the root render window, the root view, the containers, and each child's cone source, mapper and actor, so releaseResources never reaches closing.childRenderWindow.

removeRenderWindow() plus the next build pass is what deletes the view node and frees the buffers. The explicit delete() is the application-side disposal that goes with closing one of several views sharing a GL context.


const survivingAfter = surviving.childView.captureNextImage();
rootRenderWindow.render();

expect(tracker.count()).toBeLessThan(bothViewsObjects);
expect(await survivingAfter).toBe(await survivingBefore);

gc.releaseResources();

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.

Same as before.

}
);

// The scalar-colored actor makes the released color texture come back too.
it.skipIf(__VTK_TEST_NO_WEBGL__)(
'rebuilds the same image after releaseGraphicsResources',
() => expectSameImageAfterRelease(createScalarColoredConeActor)
);
7 changes: 6 additions & 1 deletion Sources/Rendering/OpenGL/PolyDataMapper2D/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Prepare for rendering.

Do the rendering.

### releaseGraphicsResources(openGLRenderWindow)

Release the GPU resources owned by this OpenGL mapper view node. If the view
node remains active, the resources are recreated as needed on a subsequent
render.

## Shader customization

In order to provide specific properties for rendering, you will have to add
Expand Down Expand Up @@ -144,4 +150,3 @@ mapperSpecificProp.ShaderCallbacks.push({

Defined 'userData' will be the first parameters which will be passed to the callback.
These callbacks will be executed when updateShaders() of OpenGLPolyDataMapper is called.

12 changes: 11 additions & 1 deletion Sources/Rendering/OpenGL/PolyDataMapper2D/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import { mat4 } from 'gl-matrix';

import * as macro from 'vtk.js/Sources/macros';
import vtkHelper from 'vtk.js/Sources/Rendering/OpenGL/Helper';
import vtkHelper, {
releasePolyDataMapperResources,
} from 'vtk.js/Sources/Rendering/OpenGL/Helper';
import vtkPoints from 'vtk.js/Sources/Common/Core/Points';
import vtkPolyData2DFS from 'vtk.js/Sources/Rendering/OpenGL/glsl/vtkPolyData2DFS.glsl';
import vtkPolyData2DVS from 'vtk.js/Sources/Rendering/OpenGL/glsl/vtkPolyData2DVS.glsl';
Expand Down Expand Up @@ -755,6 +757,14 @@ function vtkOpenGLPolyDataMapper2D(publicAPI, model) {
// Return in MB
return memUsed;
};

publicAPI.releaseGraphicsResources = (renderWindow) =>
releasePolyDataMapperResources(publicAPI, model, renderWindow);

publicAPI.delete = macro.chain(
() => publicAPI.releaseGraphicsResources(),
publicAPI.delete
);
}

// ----------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { it, expect } from 'vitest';
import testUtils from 'vtk.js/Sources/Testing/testUtils';
import { createTrackedRenderView } from 'vtk.js/Sources/Testing/renderTestUtils';

import vtkActor2D from 'vtk.js/Sources/Rendering/Core/Actor2D';
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
import vtkLineSource from 'vtk.js/Sources/Filters/Sources/LineSource';
import vtkMapper2D from 'vtk.js/Sources/Rendering/Core/Mapper2D';

function createLineActor2D(gc) {
const line = gc.registerResource(
vtkLineSource.newInstance({ point1: [2, 2, 0], point2: [14, 14, 0] })
);
const coordinate = gc.registerResource(vtkCoordinate.newInstance());
coordinate.setCoordinateSystemToWorld();

const mapper = gc.registerResource(vtkMapper2D.newInstance());
mapper.setInputConnection(line.getOutputPort());
mapper.setTransformCoordinate(coordinate);
mapper.setScalarVisibility(false);

const actor = gc.registerResource(vtkActor2D.newInstance());
actor.setMapper(mapper);
return actor;
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees the GPU objects of a 2D actor removed from a view',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderer, renderWindow, emptySceneObjects } =
createTrackedRenderView(gc);

const actor = createLineActor2D(gc);
renderer.addActor2D(actor);
renderer.resetCamera();
renderWindow.render();
expect(tracker.count()).toBeGreaterThan(emptySceneObjects);

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

gc.releaseResources();
}
);
Loading
Loading