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
24 changes: 23 additions & 1 deletion Sources/Rendering/OpenGL/Convolution2DPass/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,13 @@ function vtkConvolution2DPass(publicAPI, model) {
const program = model.convolutionShader;

// prepare the vertex and triangle data for the image plane to render to
model.copyVAO = vtkVertexArrayObject.newInstance();
if (!model.copyVAO) {
model.copyVAO = vtkVertexArrayObject.newInstance();
}
model.copyVAO.setOpenGLRenderWindow(viewNode);
// The vertex array refuses attributes from a program other than the one
// it was built against, so it is reset rather than replaced.
model.copyVAO.shaderProgramChanged();

model.tris.getCABO().bind();
if (
Expand Down Expand Up @@ -179,6 +184,23 @@ function vtkConvolution2DPass(publicAPI, model) {
tex.deactivate();
};

publicAPI.releaseGraphicsResources = macro.chain((viewNode) => {
if (model.framebuffer) {
model.framebuffer.releaseGraphicsResources();
model.framebuffer = null;
}
if (model.copyVAO) {
model.copyVAO.releaseGraphicsResources();
model.copyVAO = null;
}
// The shader cache owns the programs it hands out, so only drop the
// reference. A null shader is also what makes the next traverse rebuild
// the vertex array that reads from it.
model.convolutionShader = null;
model.tris.releaseGraphicsResources(viewNode);
publicAPI.modified();
}, publicAPI.releaseGraphicsResources);

publicAPI.getFragmentShaderCode = (kernelDimension) => {
// generate new shader code
const kernelLength = kernelDimension * kernelDimension;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { it, expect } from 'vitest';
import testUtils from 'vtk.js/Sources/Testing/testUtils';
import {
createConeActor,
createTrackedRenderView,
expectPassResourcesFreedOnDelete,
expectSameImageAfterPassRelease,
usePostProcessingPass,
} from 'vtk.js/Sources/Testing/renderTestUtils';

import vtkConvolution2DPass from 'vtk.js/Sources/Rendering/OpenGL/Convolution2DPass';

const createPass = (gc) =>
gc.registerResource(vtkConvolution2DPass.newInstance());

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees the convolution pass GPU objects when the view is deleted',
() => expectPassResourcesFreedOnDelete(createPass)
);

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'draws the same image after the view releases its render pass resources',
() => expectSameImageAfterPassRelease(createPass)
);

// A kernel of the given dimension that leaves the image untouched.
function identityKernel(dimension) {
const kernel = new Float32Array(dimension * dimension);
kernel[Math.floor(kernel.length / 2)] = 1;
return kernel;
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees the vertex array it rebuilds for a new kernel dimension',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderer, renderWindow, view } =
createTrackedRenderView(gc);

const pass = usePostProcessingPass(gc, view, createPass);
renderer.addActor(createConeActor(gc));
renderer.resetCamera();
renderWindow.render();
const objectsInUse = tracker.count();

// A new dimension recompiles the shader, and the vertex array that feeds
// it has to be rebuilt against the new program rather than replaced.
pass.setKernelDimension(5);
pass.setKernel(identityKernel(5));
renderWindow.render();

expect(tracker.count()).toBe(objectsInUse);

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

publicAPI.releaseGraphicsResources = macro.chain((viewNode) => {
if (model.framebuffer) {
model.framebuffer.releaseGraphicsResources();
model.framebuffer = null;
}
model.translucentPass?.releaseGraphicsResources(viewNode);
}, publicAPI.releaseGraphicsResources);

publicAPI.getZBufferTexture = () => {
if (model.framebuffer) {
return model.framebuffer.getColorTexture();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { it, expect } from 'vitest';
import testUtils from 'vtk.js/Sources/Testing/testUtils';
import {
createConeActor,
createTrackedRenderView,
} 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 vtkPixelSpaceCallbackMapper from 'vtk.js/Sources/Rendering/Core/PixelSpaceCallbackMapper';

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees the translucent pass GPU objects when the view is deleted',
() => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderer, renderWindow, emptySceneObjects } =
createTrackedRenderView(gc);

renderer.addActor(createConeActor(gc, { opacity: 0.5 }));
renderer.resetCamera();
renderWindow.render();
expect(tracker.count()).toBeGreaterThan(emptySceneObjects);

gc.releaseResources();
expect(tracker.count()).toBe(0);
}
);

// Reading z values makes the forward pass capture a depth buffer, so this
// actor is what gives the pass a framebuffer of its own to hand out.
function createDepthReadingActor(gc, callback) {
const cone = gc.registerResource(vtkConeSource.newInstance());
const mapper = gc.registerResource(vtkPixelSpaceCallbackMapper.newInstance());
mapper.setInputConnection(cone.getOutputPort());
mapper.setUseZValues(true);
mapper.setCallback(callback);
const actor = gc.registerResource(vtkActor.newInstance());
actor.setMapper(mapper);
return actor;
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'draws the same image after the view releases its render pass resources',
async () => {
const gc = testUtils.createGarbageCollector();
const { tracker, renderer, renderWindow, view, emptySceneObjects } =
createTrackedRenderView(gc);

let depthReads = 0;
renderer.addActor(createConeActor(gc, { opacity: 0.5 }));
renderer.addActor(createDepthReadingActor(gc, () => (depthReads += 1)));
renderer.resetCamera();

const beforeRelease = view.captureNextImage();
renderWindow.render();
expect(tracker.count()).toBeGreaterThan(emptySceneObjects);
expect(depthReads).toBeGreaterThan(0);

const depthReadsBeforeRelease = depthReads;
view.releaseGraphicsResources();

const afterRelease = view.captureNextImage();
renderWindow.render();
expect(await afterRelease).toBe(await beforeRelease);
expect(depthReads).toBeGreaterThan(depthReadsBeforeRelease);

gc.releaseResources();
}
);
16 changes: 6 additions & 10 deletions Sources/Rendering/OpenGL/OrderIndependentTranslucentPass/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ function vtkOpenGLOrderIndependentTranslucentPass(publicAPI, model) {
return null;
};

publicAPI.releaseGraphicsResources = (viewNode) => {
publicAPI.releaseGraphicsResources = macro.chain((viewNode) => {
if (model.framebuffer) {
model.framebuffer.releaseGraphicsResources(viewNode);
model.framebuffer = null;
Expand All @@ -371,16 +371,12 @@ function vtkOpenGLOrderIndependentTranslucentPass(publicAPI, model) {
model.copyVAO.releaseGraphicsResources(viewNode);
model.copyVAO = null;
}
if (model.copyShader) {
model.copyShader.releaseGraphicsResources(viewNode);
model.copyShader = null;
}
if (model.tris) {
model.tris.releaseGraphicsResources(viewNode);
model.tris = null;
}
// The shader cache owns the programs it hands out, so only drop the
// reference.
model.copyShader = null;
model.tris.releaseGraphicsResources(viewNode);

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.

Why remove the null check guards before calling release?

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.

tris is created in extend() and must remain available after releasing its GPU resources. The next render call expects model.tris to exist. The flow:

  • Construct pass
  • Create the tris helper
  • Build its VBO
  • Release its VAO and CABO resources
  • Keep the tris helper
  • Mark the pass modified
  • Rebuild the VBO on the next render

(and no need to null check model.copyShader before setting it to null)

publicAPI.modified();
};
}, publicAPI.releaseGraphicsResources);
}

// ----------------------------------------------------------------------------
Expand Down
17 changes: 17 additions & 0 deletions Sources/Rendering/OpenGL/RadialDistortionPass/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,23 @@ function vtkRadialDistortionPass(publicAPI, model) {
tex.deactivate();
};

publicAPI.releaseGraphicsResources = macro.chain((viewNode) => {
if (model.framebuffer) {
model.framebuffer.releaseGraphicsResources();
model.framebuffer = null;
}
if (model.copyVAO) {
model.copyVAO.releaseGraphicsResources();
model.copyVAO = null;
}
// The shader cache owns the programs it hands out, so only drop the
// reference. A null shader is also what makes the next traverse rebuild
// the vertex array that reads from it.
model.copyShader = null;
model.tris.releaseGraphicsResources(viewNode);
publicAPI.modified();
}, publicAPI.releaseGraphicsResources);

publicAPI.buildVBO = () => {
const xdim = 20;
const xtotal = xdim * 2;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { it } from 'vitest';
import {
expectPassResourcesFreedOnDelete,
expectSameImageAfterPassRelease,
} from 'vtk.js/Sources/Testing/renderTestUtils';

import vtkRadialDistortionPass from 'vtk.js/Sources/Rendering/OpenGL/RadialDistortionPass';

// A zero distortion pass is a no-op that never allocates.
const createPass = (gc) => {
const pass = gc.registerResource(vtkRadialDistortionPass.newInstance());
pass.setK1(0.2);
return pass;
};

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'frees the radial distortion pass GPU objects when the view is deleted',
() => expectPassResourcesFreedOnDelete(createPass)
);

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'draws the same image after the view releases its render pass resources',
() => expectSameImageAfterPassRelease(createPass)
);
14 changes: 14 additions & 0 deletions Sources/Rendering/OpenGL/RenderWindow/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import vtkDataArray from '../../../Common/Core/DataArray';
import vtkOpenGLTexture from '../../OpenGL/Texture';
import vtkPoints from '../../../Common/Core/Points';
import vtkRenderer from '../../Core/Renderer';
import vtkRenderPass from '../../SceneGraph/RenderPass';
import vtkTexture from '../../Core/Texture';
import vtkViewNode from '../../SceneGraph/ViewNode';
import vtkViewStream from '../../../IO/Core/ImageStream/ViewStream';
Expand Down Expand Up @@ -57,6 +58,19 @@ export interface vtkOpenGLRenderWindow extends vtkViewNode {
*/
initialize(): void;

/**
* Set the render passes this window renders through. A pass absent from
* the new list releases the GPU resources it owns as it leaves; a pass kept
* in the list, at any index, keeps them.
* @param {vtkRenderPass[] | null} renderPasses
*/
setRenderPasses(renderPasses: Nullable<vtkRenderPass[]>): boolean;

/**
*
*/
getRenderPasses(): Nullable<vtkRenderPass[]>;

/**
*
*/
Expand Down
46 changes: 46 additions & 0 deletions Sources/Rendering/OpenGL/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1142,8 +1142,53 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
);
}

// A child window owns no context and frees GPU objects through methods
// proxied to the root window, so it can only release while the root is alive.
const canReachGLContext = () => {
const rootWindow = model.rootOpenGLRenderWindow;
return rootWindow ? !rootWindow.isDeleted() : !!model.context;
};

// Render passes are not view nodes and are not registered graphics
// resources, so nothing else in teardown reaches the framebuffers they own.
// Kept module local so deleting a view or replacing its passes only releases
// what that view owns. The public release is context-wide and starts at root.
function releaseRenderPassResources(renderPasses, viewNode = publicAPI) {
if (!canReachGLContext()) {
return;
}
renderPasses
?.filter((renderPass) => !renderPass.isDeleted())
.forEach((renderPass) => renderPass.releaseGraphicsResources?.(viewNode));
}

function releaseRenderWindowPassResources(viewNode) {
if (viewNode.isDeleted() || !viewNode.isA('vtkOpenGLRenderWindow')) {
return;
}
releaseRenderPassResources(viewNode.getRenderPasses(), viewNode);
viewNode.getChildrenByReference().forEach(releaseRenderWindowPassResources);
}

// A pass the window no longer renders through is unreachable from teardown,
// so it has to give up what it owns as it leaves.
const superSetRenderPasses = publicAPI.setRenderPasses;
publicAPI.setRenderPasses = (renderPasses) => {
const replacedPasses = model.renderPasses;
if (!superSetRenderPasses(renderPasses)) {
return false;
}
releaseRenderPassResources(
replacedPasses?.filter(
(renderPass) => !model.renderPasses?.includes(renderPass)
)
);
return true;
};

publicAPI.delete = macro.chain(
() => {
releaseRenderPassResources(model.renderPasses);
if (model.context) {
deleteGLContext();
}
Expand Down Expand Up @@ -1222,6 +1267,7 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
};

publicAPI.releaseGraphicsResources = () => {
releaseRenderWindowPassResources(publicAPI);
// Clear the shader cache
if (model.shaderCache !== null) {
model.shaderCache.releaseGraphicsResources(publicAPI);
Expand Down
Loading
Loading