Skip to content

error in back-to-back update when new objects are created in between #69

Description

@jspanchu

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

  1. Run the script below.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions