Skip to content

fix(OpenGL): release poly data mapper buffers when the view node is deleted - #3618

Open
PaulHax wants to merge 1 commit into
masterfrom
polydata-vbo-release
Open

fix(OpenGL): release poly data mapper buffers when the view node is deleted#3618
PaulHax wants to merge 1 commit into
masterfrom
polydata-vbo-release

Conversation

@PaulHax

@PaulHax PaulHax commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Context

vtkOpenGLPolyDataMapper, vtkOpenGLPolyDataMapper2D and vtkOpenGLGlyph3DMapper have no way to free the VBOs, VAOs and color texture they own on the GL context. Deleting a view node, which happens when an actor is removed or a render window closes, leaves them allocated.

#2887 gave the image and volume mappers unregisterGraphicsResourceUser chained into delete(), so their shared textures are freed when a view closes. The poly data mappers never got the per-view equivalent. Applications cannot recover the leak themselves: when views share one WebGL context, a child view's releaseGraphicsResources() is forwarded to the root view and releases the resources of every view on the context, so an application closing one view would break the views that stay open.

Found while tightening per-view GPU teardown in Kitware/VolView#932, which documents this as a known gap that needs a vtk.js-side fix: its layout swaps add and remove actors on views sharing one WebGL context, so the orphaned buffers accumulate across the session.

Results

Before: fifty addActor/removeActor cycles orphan 150 live WebGL objects on a context that stays alive, and getGraphicsMemoryInfo() reports none of it (it only walks the shared-resource map). After: deleting the view node returns the live-object count to the empty-scene baseline. Separately, releasing a live mapper's resources and rendering again produces an identical image.

Changes

  • vtkOpenGLPolyDataMapper, vtkOpenGLPolyDataMapper2D and vtkOpenGLGlyph3DMapper gain a public releaseGraphicsResources(), chained into delete(), so a view node frees its own buffers and nothing else. The release logic is shared between the two poly data mappers, which own the same buffer-holding model shape.
  • Testing/testUtils.js gains trackWebGLObjects (counts live WebGL objects by wrapping create/delete pairs on the rendering context prototypes) and createRenderContainer; the tracked-view setup and the same-image-after-release assertion live in a new Testing/renderTestUtils.
  • Documentation and TypeScript definitions were updated to match those changes

PR and Code Checklist

  • semantic-release commit messages
  • Run npm run reformat to have correctly formatted code

…eleted

vtkOpenGLPolyDataMapper, vtkOpenGLPolyDataMapper2D and
vtkOpenGLGlyph3DMapper had no way to free the VBOs, VAOs and color texture
they own on the GL context. Deleting a view node, which is what happens when
an actor is removed or a render window is closed, left them allocated. Fifty
addActor/removeActor cycles orphan 150 WebGL objects on a live context, and
getGraphicsMemoryInfo does not account for them.

The leak is not recoverable for a render window that shares a context with a
parent: releaseGraphicsResources is proxied to the root render window, so
calling it from a child frees resources its siblings still draw with.

Add releaseGraphicsResources to those mappers and chain it into delete, so a
view frees its own buffers and nothing else. The buffers are rebuilt on the
next render. The release logic is shared between the two poly data mappers,
which own the same buffer-holding model shape.

The tests drive the public API and count live WebGL objects by wrapping the
create/delete pairs on the rendering context prototypes, added to testUtils
as trackWebGLObjects alongside createRenderContainer. The tracked-view setup
and the same-image-after-release assertion they share live in renderTestUtils.
@PaulHax
PaulHax marked this pull request as ready for review August 28, 2026 21:31
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.

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.

expect(tracker.count()).toBeGreaterThan(emptySceneObjects);
expect(await afterRelease).toBe(await beforeRelease);

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 thing here.

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?

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.

2 participants