Description
The vtk-wasm (remote.js in vtk-wasm==v1.8.2) file combines updates through an updateInProgress counter:
when a second update() arrives while the first is still deserializing, the
second call returns immediately (doing no work) and its wrapper in the vue-component (https://github.com/Kitware/trame-vtklocal/blob/v0.17.1/vue-components/src/components/VtkLocal.js#L276) emits the
updated event right away before the latest scene state (applied inside
the first call's recursive re-run) has been deserialized. If an updated handler
then pokes (with getVtkObject(id)) a freshly created wasm object against an id the client has not instantiated yet, the wasm will show errors and code can behave unexpectedly.
vtkObjectManager: Cannot update state for object at id=N ... no such object!
Reproducer should be straightforward, needs some JS code, i will post here when i have one.
Where does the issue originate?
This project (trame-vtklocal)
Version Information
trame-vtklocal - v0.17.1 vtk-wasm - v1.8.2
Steps to Reproduce
- Run the script below.
- Then open the web page, and:
- Click "Reload (BUGGY: 2 updates)" -> the vtkObjectManager "no such object" /
RemoveAllClippingPlanes errors appear in the console.
- Click "Reload (FIXED: 1 update)" -> no errors; the single
update() emits a
single updated that fires only after the mapper has been deserialized.
The only difference between the two handlers is the number of update() calls.
import os
import vtkmodules.vtkInteractionStyle
import vtkmodules.vtkRenderingOpenGL2 # noqa: F401 (rendering backend)
from trame.app import get_server
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import client, vtklocal, vuetify3
from vtkmodules.vtkFiltersSources import vtkConeSource
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkPolyDataMapper,
vtkRenderer,
vtkRenderWindow,
vtkRenderWindowInteractor,
)
os.environ["VTK_DEFAULT_OPENGL_WINDOW"] = "vtkEGLRenderWindow"
# ---------------------------------------------------------------------------
# Minimal VTK pipeline: a cone actor whose mapper we poke from the client. Each
# reload rebuilds the mapper so it gets a fresh wasm id (one the client cannot
# already hold) -- that is the condition the premature `updated` trips over.
# ---------------------------------------------------------------------------
render_window = vtkRenderWindow(
interactor=vtkRenderWindowInteractor(), off_screen_rendering=True
)
render_window.interactor.interactor_style.SetCurrentStyleToTrackballCamera()
renderer = vtkRenderer(background=(0.15, 0.15, 0.15))
render_window.AddRenderer(renderer)
cone = vtkConeSource()
def make_mapper():
m = vtkPolyDataMapper()
m.SetInputConnection(cone.GetOutputPort())
return m
mapper = make_mapper()
actor = vtkActor()
actor.SetMapper(mapper)
renderer.AddActor(actor)
renderer.ResetCamera()
# ---------------------------------------------------------------------------
# Trame app
# ---------------------------------------------------------------------------
server = get_server(client_type="vue3")
ctrl = server.controller
# Holds the LocalView (created while building the UI) so handlers can reach it.
view = {"local": None}
# The current mapper; each reload swaps in a fresh one (new wasm id).
pipeline = {"mapper": mapper}
# Set just before the reload's final update(); the `updated` handler then pokes
# the mapper's wasm object.
poke_pending = {"value": False}
def on_view_updated(**_):
"""Fires on every client `updated`. Poke the mapper when a reload asked us
to -- this is where the premature `updated` bites in the buggy path."""
if not poke_pending["value"]:
return
poke_pending["value"] = False
local = view["local"]
assert local is not None
# Re-resolve the mapper's *current* wasm id and invoke a method on it
# client-side. If the client has not deserialized the mapper yet, the wasm
# vtkObjectManager logs the "no such object" error.
ctrl.poke_mapper(local.get_wasm_id(pipeline["mapper"]))
@ctrl.set("reload_buggy")
def reload_buggy():
"""TWO update()s in quick succession -> premature `updated` -> error."""
local = view["local"]
assert local is not None
# 1) "unload": drop the actor and push an update (prunes the old mapper).
renderer.RemoveActor(actor)
local.update()
# 2) "reload": rebuild the mapper so it gets a *fresh* wasm id (one the
# client cannot already hold), re-add the actor, and push a second update.
# The second update MUST differ from the first (here: push_camera=True) so
# trame does not coalesce the two js_call("update") into one -- two distinct
# client updates are what race.
pipeline["mapper"] = make_mapper()
actor.SetMapper(pipeline["mapper"])
renderer.AddActor(actor)
renderer.ResetCamera()
poke_pending["value"] = True
local.update(push_camera=True)
@ctrl.set("reload_fixed")
def reload_fixed():
"""ONE update() -> single `updated` after deserialization -> no error.
Rebuilds the mapper too (same fresh-id condition as the buggy path) to prove
the only thing that matters is the number of update() calls.
"""
local = view["local"]
assert local is not None
renderer.RemoveActor(actor)
pipeline["mapper"] = make_mapper()
actor.SetMapper(pipeline["mapper"])
renderer.AddActor(actor)
renderer.ResetCamera()
poke_pending["value"] = True
local.update(push_camera=True)
with SinglePageLayout(server) as layout:
layout.title.set_text("trame-vtklocal issue #69 reproducer")
# Client helper: invoke removeAllClippingPlanes() on the given mapper wasm
# id. $event is the id passed from Python.
ctrl.poke_mapper = client.JSEval(
exec="window.trame.refs.repro_view.getVtkObject($event).removeAllClippingPlanes()",
).exec
with layout.toolbar:
vuetify3.VSpacer()
vuetify3.VBtn(
"Reload (BUGGY: 2 updates)",
color="error",
variant="tonal",
click=ctrl.reload_buggy,
classes="mx-2",
)
vuetify3.VBtn(
"Reload (FIXED: 1 update)",
color="primary",
variant="tonal",
click=ctrl.reload_fixed,
)
with (
layout.content,
vuetify3.VContainer(fluid=True, classes="fill-height pa-0"),
):
view["local"] = vtklocal.LocalView(
render_window,
ref="repro_view",
updated=on_view_updated,
style="width: 100%; height: 100%;",
)
if __name__ == "__main__":
server.start()
Relevant Log Output
python ./issue-69.py --server
App running at:
- Local: http://localhost:8080/
- Network: http://127.0.0.1:8080/
Note that for multi-users you need to use and configure a launcher.
JS Error => 2026-06-17 21:01:17.508 ( 0.846s) [ 8D8374] vtkObjectManager.cxx:895 ERR| vtkObjectManager (0x8fdff0): Cannot update state for object at id=49 because there is no such object!
JS Error => TypeError: Cannot convert undefined or null to object
Description
The vtk-wasm (remote.js in vtk-wasm==v1.8.2) file combines updates through an
updateInProgresscounter:when a second
update()arrives while the first is still deserializing, thesecond call returns immediately (doing no work) and its wrapper in the vue-component (https://github.com/Kitware/trame-vtklocal/blob/v0.17.1/vue-components/src/components/VtkLocal.js#L276) emits the
updatedevent right away before the latest scene state (applied insidethe first call's recursive re-run) has been deserialized. If an
updatedhandlerthen pokes (with
getVtkObject(id)) a freshly created wasm object against anidthe client has not instantiated yet, the wasm will show errors and code can behave unexpectedly.Reproducer should be straightforward, needs some JS code, i will post here when i have one.
Where does the issue originate?
This project (trame-vtklocal)
Version Information
trame-vtklocal - v0.17.1 vtk-wasm - v1.8.2
Steps to Reproduce
RemoveAllClippingPlanes errors appear in the console.
update()emits asingle
updatedthat fires only after the mapper has been deserialized.The only difference between the two handlers is the number of
update()calls.Relevant Log Output