Skip to content

Remote Session ​

A remote session mirrors a scene that lives on a server. The server owns the real VTK pipeline; the browser receives serialized object state plus binary blobs, registers them into a local WebAssembly scene, and renders. This is the model used by trame and the data viewer.

Use a remote session when the data is large, generated server-side, or shared across clients. For purely in-browser work, reach for a Standalone Session instead.

Create one from a runtime:

js
import { loadAsync } from "@kitware/vtk-wasm";

const runtime = await loadAsync({ url: VTK_WASM_BUNDLE_URL });
const remote = runtime.createRemoteSession();

Wire up the network ​

A remote session does not know how to reach your server. You supply three async fetchers through bindNetwork — one for state, one for blobs, one for status — and the session calls them whenever it needs data:

js
remote.bindNetwork(fetchState, fetchHash, fetchStatus);

Each fetcher wraps one server RPC. The trame server side lives in protocol.py; a typical client implementation just forwards the arguments over your transport (e.g. session.call("vtklocal.get.state", [vtkId])).

  • fetchState(vtkId) => Promise<string> — given an object id, return that object's serialized state as a JSON string (the wire format). Backs vtklocal.get.state.

  • fetchHash(hash) => Promise<Uint8Array> — given a content hash, return the corresponding binary blob. The transport may hand you a Blob or a TypedArray, so convert the result to a Uint8Array before returning. Backs vtklocal.get.hash.

  • fetchStatus(renderWindowId) => Promise<Status> — given a render window id, return a manifest of what exists and what changed since last time, so the session knows what to pull on the next updateAsync. Backs vtklocal.get.status. The returned object carries:

    • ids — [id, mtime] pairs for every object reachable from the render window
    • hashes — content hashes of the blobs those objects reference
    • cameras — ids of the active cameras; force_push and ignore_ids select which to push to the server or leave alone (e.g. to keep the client's local camera)
    • interactor — the interactor id for the render window

Hydrate a scene ​

The example below stands in for a server with a handful of canned object states: a render window that owns a renderer whose Background is gray. The three fetchers read from that in-memory scene exactly as they would read from the wire, so the session hydrates and renders without any backend.

The state objects mirror what vtklocal.get.state returns: each carries an Id, a ClassName, its SuperClassNames, and the properties to apply. References to other objects are { "Id": n }, and fetchStatus advertises every object so the session knows what to pull on the first updateAsync. This scene has no binary data, so fetchHash is never called.

const runtime = await vtkwasm.loadAsync({
  url: "https://raw.githack.com/Kitware/vtk-wasm/dist/latest/vtk-wasm32-emscripten.tar.gz",
});
const remote = runtime.createRemoteSession();
// The "server": 
// a render window (1)
//  -> interactor (2)
//  -> renderer collection (3)
//     -> renderer (4) with a gray background
//        -> a prop collection (5)
//        -> a vtkTextActor (6)
//           -> a vtkTextProperty (7)
const RENDER_WINDOW_ID = 1;
const SCENE = {
  1: {
    ClassName: "vtkRenderWindow",
    SuperClassNames: ["vtkObjectBase", "vtkObject", "vtkWindow"],
    Id: 1, MTime: 10,
    Interactor: { Id: 2 },
    NumberOfLayers: 1,
    Renderers: { Id: 3 },
    "vtk-object-manager-kept-alive": true,
  },
  2: {
    ClassName: "vtkRenderWindowInteractor",
    SuperClassNames: ["vtkObjectBase", "vtkObject"],
    Id: 2, MTime: 10,
    RenderWindow: { Id: 1 },
  },  
  3: {
    ClassName: "vtkRendererCollection",
    SuperClassNames: ["vtkObjectBase", "vtkObject", "vtkCollection"],
    Id: 3, MTime: 10,
    Items: [{ Id: 4 }],
  },
  4: {
    ClassName: "vtkRenderer",
    SuperClassNames: ["vtkObjectBase", "vtkObject", "vtkViewport"],
    Id: 4, MTime: 10,
    Background: [0.2, 0.2, 0.2], // gray
    RenderWindow: { Id: 1 },
    ViewProps: { Id: 5 },
  },
  5: {
    ClassName: "vtkPropCollection",
    SuperClassNames: ["vtkObjectBase", "vtkObject", "vtkCollection"],
    Id: 5, MTime: 10,
    Items: [{ Id: 6 },],
  },
  6: {
    ClassName: "vtkTextActor",
    SuperClassNames: ["vtkObjectBase", "vtkObject",
        "vtkProp", "vtkActor2D", "vtkTexturedActor2D"],
    Id: 6, MTime: 10,
    Input: "Hello VTK.wasm RemoteSession",
    DisplayPosition: [20, 20],
    TextProperty: { Id: 7 },
  },
  7: {
    ClassName: "vtkTextProperty",
    SuperClassNames: ["vtkObjectBase", "vtkObject"],
    Id: 7, MTime: 10,
    FontSize: 36,
  }
};
// One object's serialized state, as a JSON string (the wire format).
async function fetchState(vtkId) {
  return JSON.stringify(SCENE[vtkId]);
}
// A binary blob by hash. This scene has none, so this is never called.
async function fetchHash(hash) {
  return new Uint8Array();
}
// What exists and what changed since last time.
async function fetchStatus(vtkId) {
  return {
    ids: Object.values(SCENE).map((s) => [s.Id, s.MTime]),
    hashes: [],
    cameras: [],
    force_push: [],
    ignore_ids: [],
  };
}
remote.bindNetwork(fetchState, fetchHash, fetchStatus);
// Own the canvas, size it, then pull the scene in and render it.
const container = document.getElementById("container");
const canvas = document.createElement("canvas");
canvas.tabindex = -1;
container.appendChild(canvas);
remote.bindCanvas(RENDER_WINDOW_ID, canvas);
const resizeObserver = new ResizeObserver((entries) => {
  // Content box already in device pixels — no devicePixelRatio math needed.
  const { inlineSize, blockSize } = entries[0].devicePixelContentBoxSize[0];
  remote.setSizeAsync(RENDER_WINDOW_ID, inlineSize, blockSize);
});
resizeObserver.observe(container);
await remote.updateAsync(RENDER_WINDOW_ID);
    

Bring your own canvas ​

The session never creates, moves, or removes canvas elements — you own them. Create a <canvas> and associate it with a render window via bindCanvas, passing either the element itself or the id of one already in the DOM:

js
const canvas = document.createElement("canvas");
remote.bindCanvas(renderWindowId, canvas);           // pass the element directly…
// remote.bindCanvas(renderWindowId, "my-canvas");   // …or the id of a canvas in the DOM
remote.setSizeAsync(renderWindowId, 800, 600);       // size the canvas + render window

Passing the element directly registers it with Emscripten's specialHTMLTargets, so the canvas needs neither an id nor to be attached to the document — handy for off-screen or framework-managed canvases. (On builds that don't expose specialHTMLTargets, the canvas must have an id so a CSS selector can be used instead.)

When the canvas goes away, unbindCanvas removes the listeners, unregisters the target, and forgets the mapping, leaving the element itself untouched.

Drive updates ​

Call updateAsync to pull the latest server state for a render window and render it.

js
await remote.updateAsync(renderWindowId);

To surface download progress (state + blob counts), register a callback with addProgressCallback; it returns a function that removes the callback.

Reading state ​

Once synchronized, inspect objects locally without another round trip via getState and getStateValue, or get a controllable proxy with getVtkObject.

Objects are owned by the server ​

The server owns the scene's object lifecycle, so a remote session cannot create or destroy objects from the client. The remote.vtk namespace exists only to control existing objects: use getVtkObject (or remote.vtk.getVtkObject(id)) to obtain a proxy for an object the server already created.

Calling a constructor such as remote.vtk.vtkActor() or proxy.$delete() is a no-op: it logs a warning to the console and returns undefined / false rather than mutating the scene. To add or remove objects, do it on the server and pull the change in with update.

js
const actor = remote.getVtkObject(actorId); // ✅ control an existing object
actor.visibility = false;

remote.vtk.vtkActor();                       // ⚠️ warns, returns undefined
actor.$delete();                             // ⚠️ warns, returns false

Cleaning up ​

remote.dispose() frees the C++ session and detaches the interaction listeners from your canvases (the canvas elements are left in place). As with standalone sessions, using works too:

js
using remote = runtime.createRemoteSession();

Reference: RemoteSession · VtkWasmRuntime