# Got any cleanup breaking changes for VTK.js?

**URL:** <https://discourse.vtk.org/t/got-any-cleanup-breaking-changes-for-vtk-js/16566>\
**Category:** Web\
**Created:** [October 19, 2026, 9:18pm UTC](https://discourse.vtk.org/t/got-any-cleanup-breaking-changes-for-vtk-js/16566 "2026-10-19T21:18:21Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![PaulHax](https://discourse.vtk.org/user_avatar/discourse.vtk.org/paulhax/32/2748_2.png) [@PaulHax](https://discourse.vtk.org/u/PaulHax)\
**Post date:** [October 19, 2026, 9:18pm UTC](https://discourse.vtk.org/t/got-any-cleanup-breaking-changes-for-vtk-js/16566/1 "2026-10-19T21:18:21Z")

</div>

I’ed like to get the [big TypeScript .d.ts](https://github.com/Kitware/vtk-js/pull/3689) PR merged this Thursday. I’ve stacked some [deprication PRs](https://github.com/Kitware/vtk-js/pull/3693) coming into force on top of the breaking change TypeScript PR.

Do folks have other little breaking cleanup PRs to include or a reason to hold the major version release?

---

<div class="post-metadata">

**Author:** ![PaulHax](https://discourse.vtk.org/user_avatar/discourse.vtk.org/paulhax/32/2748_2.png) [@PaulHax](https://discourse.vtk.org/u/PaulHax)\
**Post date:** [October 26, 2026, 3:21pm UTC](https://discourse.vtk.org/t/got-any-cleanup-breaking-changes-for-vtk-js/16566/2 "2026-10-26T15:21:02Z")

</div>

There has been an ask to hold the release until we get these fixes in:

> <https://github.com/Kitware/vtk-js/pull/3683>
>
> \### Context
> 
> When an image property has no RGB transfer function or no piecewise… function, \`vtkOpenGLImageMapper\`, \`vtkOpenGLImageResliceMapper\` and \`vtkOpenGLImageCPRMapper\` build a new default color or opacity lookup texture on every buffer object rebuild and never delete the old one. The render window's texture cache is keyed on the transfer function, so a \`null\` key always misses and the texture is never registered for release.
> 
> \`vtkOpenGLImageMapper\` and \`vtkOpenGLImageCPRMapper\` make it worse: after an actor or property change that leaves the vertices alone (a new color window, say), they rebuild on every render, because \`VBOBuildTime\` is only marked when the vertices change. So each slice leaks one texture per missing function on every frame after the first window/level change.
> 
> Affected: 36.2.0 to 37.4.2 and master at \`009a4b2a\`. Workaround until a release: give every image property an RGB transfer function and a piecewise function (they can be shared).
> 
> \### Changes
> 
> \- With no transfer function, each of the three mappers keeps its default textures in \`model.\_defaultColorTexture\` and \`model.\_defaultPwfTexture\`, with a hash of what each was built from: the texture width, plus the interpolation type in \`vtkOpenGLImageMapper\`. A texture is rebuilt only when its hash changes, the old one is released, and \`unregisterGraphicsResources\` releases both on delete and on a render window switch. Textures in the shared cache are left to the cache, as before.
> \- \`vtkOpenGLImageMapper\` and \`vtkOpenGLImageCPRMapper\` mark \`VBOBuildTime\` at the end of every completed rebuild, as \`vtkOpenGLImageResliceMapper\` already does.
> \- No public API, documentation or TypeScript changes.
> 
> \#### Why the default textures are mapper-owned
> 
> The default color texture depends on the mapper's \`colorTextureWidth\` and, in \`vtkOpenGLImageMapper\`, its interpolation type, so mappers in one render window can need different defaults and there is no transfer function to key a shared entry on. Owning them per mapper costs two small textures per mapper (about 4 KB for one component at the default width of 1024).
> 
> \### Results
> 
> New tests in \`Sources/Rendering/OpenGL/{ImageMapper,ImageResliceMapper,ImageCPRMapper}/test/testReleaseGraphicsResources.js\` count live GL objects with \`createTrackedRenderView\`. One slice of a 16 bit image with no transfer functions makes 5 GL objects on its first render.
> 
> | Scenario | master | this PR |
> |---|---|---|
> | ImageMapper: buffer object builds over 20 renders after one color window change | 20 | 1 |
> | ImageMapper: live GL objects after 15 slice changes | 35 | 5 |
> | ImageResliceMapper: live GL objects after 15 slice plane moves and a color window change | 37 | 5 |
> | ImageCPRMapper: buffer object builds over 20 renders after one color window change | 20 | 1 |
> | ImageCPRMapper: live GL objects after 15 width changes | 35 | 5 |
> | Any of the three: GL objects freed when the slice is removed | 1 | 3 |
> 
> The tests also check that the default texture handles stay the same across slice-only changes, and that changing \`colorTextureWidth\` or \`opacityTextureWidth\` replaces only that texture and frees the old one (the live count stays at 5). All 11 tests fail on master and pass on this branch.
> 
> The full suite passes: 233 test files, 652 tests passed and 13 skipped. It ran in headless Chromium with SwiftShader (\`CI=1\`) in \`mcr.microsoft.com/playwright:v1.60.0-noble\`. \`oxfmt --check\` and \`oxlint\` pass.
> 
> The vertex buffer and VAO that a removed slice leaves behind (why the removal test expects 3 freed objects and not 5) are fixed separately in #3684.
> 
> \### PR Checklist
> 
> \- \[\] GitHub Actions CI passed: \[semantic-release\](https://github.com/semantic-release/semantic-release) commit messages, lint, and tests
> \- \[x\] Test coverage added
> \- \[x\] Documentation and TypeScript definitions are updated to match these changes (no public API change)

> <https://github.com/Kitware/vtk-js/pull/3684>
>
> \### Context
> 
> \`vtkOpenGLImageMapper\`, \`vtkOpenGLImageResliceMapper\` and \`vtkOpenG…LImageCPRMapper\` each own a \`model.tris\` helper, an instance of \`vtkOpenGLHelper\` that holds the image quad's vertex buffer (CABO) and VAO. None of the three ever calls \`model.tris.releaseGraphicsResources()\`. Every removed or deleted image slice mapper, and every mapper moved to a different render window, leaves its vertex buffer and VAO behind.
> 
> This surfaced while testing #3683, which fixes the same three mappers' default lookup texture leak. That is a different bug with a different cause, and the two PRs don't depend on each other.
> 
> Confirmed at master, \`009a4b2a9be3164820803a8d76065b45bbae0be3\`.
> 
> \#### Root cause
> 
> Links are pinned to master at \`009a4b2a9be3164820803a8d76065b45bbae0be3\`.
> 
> 1. \`vtkOpenGLHelper.releaseGraphicsResources\` already does the work: it releases the VAO and the CABO, which owns the vertex buffer (\[Helper L34-L38\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/Helper/index.js#L34-L38)).
> 2. Each of the three mappers has an \`unregisterGraphicsResources(renderWindow)\` function that releases the mapper's own non-shared resources before unregistering the shared ones, and it never touches \`model.tris\`: \[ImageMapper L57-L69\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/ImageMapper/index.js#L57-L69) releases \`model.openGLTexture\` the same way; \[ImageResliceMapper L122-L127\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/ImageResliceMapper/index.js#L122-L127) and \[ImageCPRMapper L37-L42\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/ImageCPRMapper/index.js#L37-L42) only unregister shared resources, since neither owns a non-shared texture of its own.
> 3. That function runs in exactly the two places that matter. When the mapper moves to a different render window, it runs on the old window right before \`model.tris.setOpenGLRenderWindow\` repoints \`tris\` at the new one: \[ImageMapper L79-L92\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/ImageMapper/index.js#L79-L92), \[ImageResliceMapper L142-L155\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/ImageResliceMapper/index.js#L142-L155), \[ImageCPRMapper L52-L69\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/ImageCPRMapper/index.js#L52-L69). When the mapper is deleted, it runs from the delete chain: \[ImageMapper L1596-L1600\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/ImageMapper/index.js#L1596-L1600), \[ImageResliceMapper L2015-L2027\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/ImageResliceMapper/index.js#L2015-L2027), \[ImageCPRMapper L1413-L1417\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/ImageCPRMapper/index.js#L1413-L1417). Neither path frees \`model.tris\`, so its VAO and vertex buffer from the old context are never released.
> 4. \`vtkOpenGLVolumeMapper\` does not have this bug. It also owns a \`model.tris\` helper, and its own cleanup already calls \`model.tris.releaseGraphicsResources(openGLRenderWindow)\` (\[VolumeMapper L1206-L1219\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/VolumeMapper/index.js#L1206-L1219)), called from the same two places: the render window switch (\[VolumeMapper L137-L154\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/VolumeMapper/index.js#L137-L154)) and delete (\[VolumeMapper L1221-L1235\](https://github.com/Kitware/vtk-js/blob/009a4b2a9be3164820803a8d76065b45bbae0be3/Sources/Rendering/OpenGL/VolumeMapper/index.js#L1221-L1235)). This PR gives the three image mappers the same shape.
> 5. Only one of those two call sites is reachable in practice. Removing an actor from a renderer, and moving a renderer (or a child render window) to a different render window, both prune the mapper's view node through \`removeUnusedNodes\`, which deletes it exactly the way an explicit \`mapper.delete()\` does. I could not find a way to reach the render-window-switch branch itself, the \`oldOglRenderWindow !== model.\_openGLRenderWindow\` check inside each mapper's own \`buildPass\`, while the mapper's view node survives. Every move of a renderer or a child render window between two render windows deletes the old view node and builds a fresh one under the new parent, with nothing stale to rebuild. I confirmed this by instrumenting that branch directly: across both ways of moving a renderer, it never ran.
> 
> \### Results
> 
> All numbers come from the new tests, run on master and on this branch. Each scene is one image slice with a real RGB transfer function and piecewise function on the property, so the color and opacity lookup textures land on the render window's shared cache and are already released correctly on removal. That isolates the count to the two GL objects this PR touches: the tris VAO and its vertex buffer.
> 
> | Scenario | master | this PR |
> |---|---|---|
> | ImageMapper: live GL objects left behind after the slice leaves the scene | 2 | 0 |
> | ImageResliceMapper: live GL objects left behind after the slice leaves the scene | 2 | 0 |
> | ImageCPRMapper: live GL objects left behind after the slice leaves the scene | 2 | 0 |
> 
> All three new tests fail on master with the same assertion error, \`expected 2 to be +0\`: one VAO and one vertex buffer short of the empty-scene baseline. All three pass on this branch.
> 
> Each mapper also has a second test that moves its renderer to an independent second render window instead of removing the actor, then checks the slice still draws there and the first window's object count drops back to what it was before the move. On master, these three also fail, off by the same 2 objects, for the same reason: \`removeUnusedNodes\` prunes the renderer that left through the normal delete path, not through the render-window-switch branch. In every run, with or without the fix, the slice draws correctly in the second window, because a moved renderer's view node is always built fresh. All six tests pass on this branch.
> 
> The full suite passes on this branch: 233 test files (226 passed, 7 skipped), 660 tests (647 passed, 13 skipped), 0 failed, in 136 seconds. It ran in headless Chromium with SwiftShader (\`CI=1\`) in the \`mcr.microsoft.com/playwright:v1.60.0-noble\` image.
> 
> \### Changes
> 
> \- \`vtkOpenGLImageMapper\`, \`vtkOpenGLImageResliceMapper\` and \`vtkOpenGLImageCPRMapper\`: \`unregisterGraphicsResources\` now also calls \`model.tris.releaseGraphicsResources(renderWindow)\`. This is the function each mapper already calls both on a render window switch and on delete, so the single call covers both cases without a new call site.
> \- New tests: \`Sources/Rendering/OpenGL/{ImageMapper,ImageResliceMapper,ImageCPRMapper}/test/testReleaseGraphicsResources.js\`. One test per mapper builds one image slice with a real transfer function and piecewise function, renders it, removes the actor from the renderer, and checks that the tracked GL object count returns to the pre-render baseline. A second test per mapper moves its renderer to a second, independent render window, checks the slice still draws there by reading the pixel at the viewport's center, and checks the first window's object count returns to what it was before the move.
> \- No public API, documentation or TypeScript changes.
> 
> \#### Notes for review
> 
> \- The transfer function and piecewise function in the new tests are load-bearing, not decoration. Without them, \`getRGBTransferFunction()\` and \`getPiecewiseFunction()\` return null and the mapper falls into the still-open default-lookup-texture leak in the same three mappers, which would add more leaked objects to the count and make the result say nothing specific about \`tris\`.
> \- \`model.tris.releaseGraphicsResources(renderWindow)\` passes the render window through even though \`vtkOpenGLHelper.releaseGraphicsResources\` ignores the argument today. That matches the existing call to \`model.openGLTexture.releaseGraphicsResources(renderWindow)\` right next to it in \`ImageMapper\`, and how \`VolumeMapper\` calls the same method on its own \`tris\`.
> \- I went looking for a real trigger for the render-window-switch branch, since releasing \`model.tris\` there could leave a surviving mapper's quad empty: \`ImageMapper\` and \`ImageCPRMapper\` rebuild the quad only when their inputs change, while \`ImageResliceMapper\` also rebuilds when the quad has no elements. I could not find one. Moving a renderer between two independent render windows, and moving a child render window between two root windows, both delete the old view node and build a fresh one elsewhere; instrumenting the switch branch directly showed zero hits across either operation. If you know a path that reaches it, the same \`!model.tris.getCABO().getElementCount()\` check \`ImageResliceMapper\` uses would cover the other two, and I can add it here.
> \- The renderer-move tests ended up measuring the original leak from a second angle rather than the switch branch: moving a renderer away prunes the mapper's view node through \`removeUnusedNodes\`, the same delete path \`mapper.delete()\` and an explicit \`removeActor\` already use. That is still a real, user-reachable way to hit the original leak, so the tests stay as a second, independent check of it.
> \- Not fixed here: \`vtkOpenGLImageResliceMapper\` releases its label outline textures with no render window argument, and the three mappers' still-open default-lookup-texture leak. Both are tracked separately and neither is part of this change.
> 
> \### PR Checklist
> 
> \- \[\] GitHub Actions CI passed: semantic-release commit messages, lint, and tests (locally: full suite passes, \`oxfmt --check\` passes, \`oxlint\` reports 0 errors with 3 pre-existing warnings unrelated to this change, and \`commitlint\` passes on the commit message)
> \- \[x\] Test coverage added
> \- \[x\] Documentation and TypeScript definitions are updated to match these changes (no public API change)

---

<div class="post-metadata">

**Author:** ![sankhesh](https://discourse.vtk.org/user_avatar/discourse.vtk.org/sankhesh/32/73_2.png) [@sankhesh](https://discourse.vtk.org/u/sankhesh)\
**Post date:** [October 26, 2026, 6:32pm UTC](https://discourse.vtk.org/t/got-any-cleanup-breaking-changes-for-vtk-js/16566/3 "2026-10-26T18:32:11Z")

</div>

They are approved and ready to merge. Please feel free to go ahead unless there’s anything else.
