From c4bdf834dea331dd543c3544fd5839dcca747d1b Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 25 Mar 2026 21:29:11 -0400 Subject: [PATCH 1/4] feat(RenderWindow): add manageCanvas for externally owned canvases --- .../Rendering/OpenGL/RenderWindow/index.d.ts | 5 ++++ .../Rendering/OpenGL/RenderWindow/index.js | 29 +++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.d.ts b/Sources/Rendering/OpenGL/RenderWindow/index.d.ts index 6b7c75fa258..076cb10c032 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.d.ts +++ b/Sources/Rendering/OpenGL/RenderWindow/index.d.ts @@ -21,6 +21,7 @@ export interface IOpenGLRenderWindowInitialValues { context?: WebGLRenderingContext | WebGL2RenderingContext; context2D?: CanvasRenderingContext2D; canvas?: HTMLCanvasElement; + manageCanvas?: boolean; cursorVisibility?: boolean; cursor?: string; textureUnitManager?: null; @@ -93,6 +94,10 @@ export interface vtkOpenGLRenderWindow extends vtkViewNode { */ setCanvas(canvas: Nullable): boolean; + getManageCanvas(): boolean; + + setManageCanvas(manageCanvas: boolean): boolean; + /** * Check if a point is in the viewport. * @param {Number} x The x coordinate. diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index 2385223fe0d..dea32cec402 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -141,7 +141,7 @@ function vtkOpenGLRenderWindow(publicAPI, model) { const previousSize = [0, 0]; function updateWindow() { // Canvas size - if (model.renderable) { + if (model.renderable && model.manageCanvas) { if ( model.size[0] !== previousSize[0] || model.size[1] !== previousSize[1] @@ -160,7 +160,9 @@ function vtkOpenGLRenderWindow(publicAPI, model) { } // Offscreen ? - model.canvas.style.display = model.useOffScreen ? 'none' : 'block'; + if (model.manageCanvas) { + model.canvas.style.display = model.useOffScreen ? 'none' : 'block'; + } // Cursor type if (model.el) { @@ -549,15 +551,28 @@ function vtkOpenGLRenderWindow(publicAPI, model) { if (model.deleted) { return null; } + const requestedSize = + !!size || scale !== 1 + ? size || model.size.map((val) => val * scale) + : null; + const requiresCanvasResize = + requestedSize !== null && + (requestedSize[0] !== model.size[0] || + requestedSize[1] !== model.size[1]); + const screenshotSize = requiresCanvasResize ? requestedSize : null; + + if (!model.manageCanvas && requiresCanvasResize) { + throw new Error( + 'Resizing screenshot capture requires manageCanvas=true on vtkOpenGLRenderWindow' + ); + } + model.imageFormat = format; const previous = model.notifyStartCaptureImage; model.notifyStartCaptureImage = true; model._screenshot = { - size: - !!size || scale !== 1 - ? size || model.size.map((val) => val * scale) - : null, + size: screenshotSize, }; return new Promise((resolve, reject) => { @@ -1307,6 +1322,7 @@ const DEFAULT_VALUES = { context: null, context2D: null, canvas: null, + manageCanvas: true, cursorVisibility: true, cursor: 'pointer', textureUnitManager: null, @@ -1377,6 +1393,7 @@ export function extend(publicAPI, model, initialValues = {}) { 'context', 'context2D', 'canvas', + 'manageCanvas', 'renderPasses', 'notifyStartCaptureImage', 'defaultToWebgl2', From f30568dc627bea7306516f0a3d32821e6629df6c Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 25 Mar 2026 21:29:15 -0400 Subject: [PATCH 2/4] feat: add SharedRenderWindow for external WebGL2 context sharing --- .../public/gallery/SharedContext.jpg | Bin 0 -> 112968 bytes Examples/Rendering/SharedContext/index.js | 405 ++++++++++++++++++ .../OpenGL/SharedRenderWindow/index.d.ts | 60 +++ .../OpenGL/SharedRenderWindow/index.js | 298 +++++++++++++ .../OpenGL/SharedRenderWindow/test/helpers.js | 51 +++ .../test/testSharedRenderWindow.js | 272 ++++++++++++ .../test/testSharedRenderWindowGLState.js | 168 ++++++++ .../testSharedRenderWindowHostSurvival.js | 113 +++++ .../OpenGL/SharedRenderer/index.d.ts | 58 +++ .../Rendering/OpenGL/SharedRenderer/index.js | 62 +++ .../Rendering/OpenGL/ViewNodeFactory/index.js | 9 +- .../SceneGraph/ViewNodeFactory/index.js | 3 +- 12 files changed, 1495 insertions(+), 4 deletions(-) create mode 100644 Documentation/public/gallery/SharedContext.jpg create mode 100644 Examples/Rendering/SharedContext/index.js create mode 100644 Sources/Rendering/OpenGL/SharedRenderWindow/index.d.ts create mode 100644 Sources/Rendering/OpenGL/SharedRenderWindow/index.js create mode 100644 Sources/Rendering/OpenGL/SharedRenderWindow/test/helpers.js create mode 100644 Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindow.js create mode 100644 Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowGLState.js create mode 100644 Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowHostSurvival.js create mode 100644 Sources/Rendering/OpenGL/SharedRenderer/index.d.ts create mode 100644 Sources/Rendering/OpenGL/SharedRenderer/index.js diff --git a/Documentation/public/gallery/SharedContext.jpg b/Documentation/public/gallery/SharedContext.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d802321ba6742ed4e15f4472ca004bcf40b5354e GIT binary patch literal 112968 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 literal 0 HcmV?d00001 diff --git a/Examples/Rendering/SharedContext/index.js b/Examples/Rendering/SharedContext/index.js new file mode 100644 index 00000000000..f83d6b4eec9 --- /dev/null +++ b/Examples/Rendering/SharedContext/index.js @@ -0,0 +1,405 @@ +import '@kitware/vtk.js/favicon'; + +// Load the rendering pieces we want to use (for both WebGL and WebGPU) +import '@kitware/vtk.js/Rendering/Profiles/Geometry'; + +import vtkCellArray from '@kitware/vtk.js/Common/Core/CellArray'; +import vtkPoints from '@kitware/vtk.js/Common/Core/Points'; +import vtkPolyData from '@kitware/vtk.js/Common/DataModel/PolyData'; +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkConeSource from '@kitware/vtk.js/Filters/Sources/ConeSource'; +import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer'; +import vtkRenderWindow from '@kitware/vtk.js/Rendering/Core/RenderWindow'; +import vtkLight from '@kitware/vtk.js/Rendering/Core/Light'; +import vtkSharedRenderWindow from '@kitware/vtk.js/Rendering/OpenGL/SharedRenderWindow'; +import { mat4, vec3 } from 'gl-matrix'; + +// ---------------------------------------------------------------------------- +// Alpine marker data for geo-positioned cones on terrain +// ---------------------------------------------------------------------------- + +const cities = [ + { name: 'Seefeld', lng: 11.1871, lat: 47.3305, color: [1.0, 0.5, 0.0] }, + { name: 'Innsbruck', lng: 11.4041, lat: 47.2692, color: [0.5, 1.0, 0.0] }, + { + name: 'Hall in Tirol', + lng: 11.5079, + lat: 47.2839, + color: [0.0, 0.5, 1.0], + }, +]; + +const CONE_HEIGHT_METERS = 900.0; +const CONE_CENTER_HEIGHT_RATIO = 0.25; +const SPHERE_CENTER_HEIGHT_RATIO = 1.05; + +function createRouteActor() { + const points = vtkPoints.newInstance({ dataType: 'Float64Array' }); + const pointValues = new Float64Array(cities.length * 3); + + points.setData(pointValues, 3); + const polyData = vtkPolyData.newInstance(); + polyData.setPoints(points); + polyData.setLines( + vtkCellArray.newInstance({ + values: Uint16Array.from([cities.length, 0, 1, 2]), + }) + ); + + const mapper = vtkMapper.newInstance(); + mapper.setInputData(polyData); + + const actor = vtkActor.newInstance(); + actor.setMapper(mapper); + actor.getProperty().setColor(0.95, 0.15, 0.15); + actor.getProperty().setLighting(false); + actor.getProperty().setLineWidth(3.0); + + return { actor, points, polyData }; +} + +function createCityActors(city) { + const coneSource = vtkConeSource.newInstance({ + height: 1.0, + radius: 0.3, + resolution: 12, + direction: [0, 0, -1], + capping: true, + }); + const coneMapper = vtkMapper.newInstance(); + coneMapper.setInputConnection(coneSource.getOutputPort()); + const coneActor = vtkActor.newInstance(); + coneActor.setMapper(coneMapper); + coneActor.getProperty().setColor(...city.color); + coneActor.getProperty().setAmbient(0.4); + coneActor.getProperty().setDiffuse(0.6); + + const sphereSource = vtkSphereSource.newInstance({ + radius: 0.3, + thetaResolution: 32, + phiResolution: 32, + }); + const sphereMapper = vtkMapper.newInstance(); + sphereMapper.setInputConnection(sphereSource.getOutputPort()); + const sphereActor = vtkActor.newInstance(); + sphereActor.setMapper(sphereMapper); + sphereActor.getProperty().setColor(...city.color); + sphereActor.getProperty().setAmbient(0.0); + sphereActor.getProperty().setDiffuse(1.0); + + return { city, coneActor, sphereActor }; +} + +// ---------------------------------------------------------------------------- +// Load MapLibre GL JS dynamically +// ---------------------------------------------------------------------------- + +function loadMapLibre() { + return new Promise((resolve, reject) => { + if (window.maplibregl) { + resolve(window.maplibregl); + return; + } + + const link = document.createElement('link'); + link.href = 'https://unpkg.com/maplibre-gl@5.21.1/dist/maplibre-gl.css'; + link.rel = 'stylesheet'; + document.head.appendChild(link); + + const script = document.createElement('script'); + script.src = 'https://unpkg.com/maplibre-gl@5.21.1/dist/maplibre-gl.js'; + script.onload = () => resolve(window.maplibregl); + script.onerror = reject; + document.head.appendChild(script); + }); +} + +// ---------------------------------------------------------------------------- +// Setup page layout +// ---------------------------------------------------------------------------- + +document.body.style.margin = '0'; +document.body.style.padding = '0'; + +const mapContainer = document.createElement('div'); +mapContainer.id = 'map'; +mapContainer.style.width = '100vw'; +mapContainer.style.height = '100vh'; +document.body.appendChild(mapContainer); + +const MAPLIBRE_NORTH_UP = [0, -1, 0]; +function computeCameraTargetMercator(maplibregl, transform) { + return maplibregl.MercatorCoordinate.fromLngLat( + transform.center, + transform.elevation + ); +} + +function computeCameraMercator(targetMercator, transform) { + const cameraToCenterDistanceMeters = + transform.cameraToCenterDistance / transform.pixelsPerMeter; + const metersToMercator = targetMercator.meterInMercatorCoordinateUnits(); + const cameraToCenterDistanceMercator = + cameraToCenterDistanceMeters * metersToMercator; + const dzMercator = + cameraToCenterDistanceMercator * Math.cos(transform.pitchInRadians); + const dhMercator = Math.sqrt( + Math.max( + 0, + cameraToCenterDistanceMercator * cameraToCenterDistanceMercator - + dzMercator * dzMercator + ) + ); + + return { + x: targetMercator.x + dhMercator * Math.sin(-transform.bearingInRadians), + y: targetMercator.y + dhMercator * Math.cos(-transform.bearingInRadians), + z: targetMercator.z + dzMercator, + }; +} + +function computeViewUp(transform) { + const cameraToWorldRotation = new Float64Array(16); + const viewUp = vec3.fromValues(...MAPLIBRE_NORTH_UP); + + mat4.identity(cameraToWorldRotation); + mat4.rotateZ( + cameraToWorldRotation, + cameraToWorldRotation, + transform.bearingInRadians + ); + mat4.rotateX( + cameraToWorldRotation, + cameraToWorldRotation, + -transform.pitchInRadians + ); + mat4.rotateZ( + cameraToWorldRotation, + cameraToWorldRotation, + transform.rollInRadians + ); + vec3.transformMat4(viewUp, viewUp, cameraToWorldRotation); + vec3.normalize(viewUp, viewUp); + + return viewUp; +} + +function computeViewMatrix(cameraMercator, targetMercator, viewUp) { + const eye = vec3.fromValues( + cameraMercator.x, + cameraMercator.y, + cameraMercator.z + ); + const target = vec3.fromValues( + targetMercator.x, + targetMercator.y, + targetMercator.z + ); + const viewMatrix = new Float64Array(16); + + mat4.lookAt(viewMatrix, eye, target, viewUp); + return viewMatrix; +} + +// ---------------------------------------------------------------------------- +// Main initialization +// ---------------------------------------------------------------------------- + +async function init() { + const maplibregl = await loadMapLibre(); + + // Create MapLibre map + const map = new maplibregl.Map({ + container: 'map', + zoom: 12, + center: [11.39085, 47.27574], + pitch: 70, + maxZoom: 18, + maxPitch: 85, + antialias: true, + style: { + version: 8, + sources: { + osm: { + type: 'raster', + tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'], + tileSize: 256, + attribution: '© OpenStreetMap Contributors', + maxzoom: 19, + }, + terrainSource: { + type: 'raster-dem', + url: 'https://demotiles.maplibre.org/terrain-tiles/tiles.json', + tileSize: 256, + }, + hillshadeSource: { + type: 'raster-dem', + url: 'https://demotiles.maplibre.org/terrain-tiles/tiles.json', + tileSize: 256, + }, + }, + layers: [ + { + id: 'osm', + type: 'raster', + source: 'osm', + }, + { + id: 'hills', + type: 'hillshade', + source: 'hillshadeSource', + layout: { visibility: 'visible' }, + paint: { 'hillshade-shadow-color': '#473B24' }, + }, + ], + terrain: { + source: 'terrainSource', + exaggeration: 1, + }, + }, + }); + + await new Promise((resolve) => { + map.on('load', resolve); + }); + + // Create VTK render window and renderer + const renderWindow = vtkRenderWindow.newInstance(); + const renderer = vtkRenderer.newInstance(); + renderer.setBackground(0, 0, 0, 0); + renderer.setPreserveColorBuffer(true); + renderer.setPreserveDepthBuffer(true); + // Shared-context rendering uses a MapLibre-provided matrix, so bypass + // vtk.js automatic headlights and drive a camera-following scene light. + renderer.setAutomaticLightCreation(false); + renderWindow.addRenderer(renderer); + + const viewLight = vtkLight.newInstance(); + viewLight.setLightTypeToSceneLight(); + viewLight.setPositional(false); + renderer.addLight(viewLight); + + const cityActors = cities.map((city) => { + const actors = createCityActors(city); + actors.coneActor.setVisibility(false); + actors.sphereActor.setVisibility(false); + renderer.addActor(actors.coneActor); + renderer.addActor(actors.sphereActor); + return actors; + }); + const route = createRouteActor(); + route.actor.setVisibility(false); + renderer.addActor(route.actor); + let scenePlaced = false; + + function placeSceneOnTerrain() { + if (scenePlaced) { + return; + } + + const routePointValues = new Float64Array(cities.length * 3); + + cityActors.forEach(({ city, coneActor, sphereActor }, index) => { + const elevation = map.queryTerrainElevation([city.lng, city.lat]) || 0; + const mercator = maplibregl.MercatorCoordinate.fromLngLat( + [city.lng, city.lat], + elevation + ); + const scale = + mercator.meterInMercatorCoordinateUnits() * CONE_HEIGHT_METERS; + const sphereCenterZ = mercator.z + scale * SPHERE_CENTER_HEIGHT_RATIO; + + coneActor.setPosition( + mercator.x, + mercator.y, + mercator.z + scale * CONE_CENTER_HEIGHT_RATIO + ); + coneActor.setScale(scale, scale, scale); + coneActor.setVisibility(true); + + sphereActor.setPosition(mercator.x, mercator.y, sphereCenterZ); + sphereActor.setScale(scale * 0.9, -scale * 0.9, scale * 0.9); + sphereActor.setVisibility(true); + + routePointValues.set([mercator.x, mercator.y, sphereCenterZ], index * 3); + }); + + route.points.setData(routePointValues, 3); + route.polyData.modified(); + route.actor.setVisibility(true); + scenePlaced = true; + map.triggerRepaint(); + } + + // Store VTK objects that will be initialized in onAdd + let openglRenderWindow = null; + + // Use CustomLayerInterface for proper matrix access + const vtkLayer = { + id: 'vtk-cones', + type: 'custom', + renderingMode: '3d', + + onAdd(mapInstance, gl) { + const canvas = mapInstance.getCanvas(); + openglRenderWindow = vtkSharedRenderWindow.createFromContext(canvas, gl); + renderWindow.addView(openglRenderWindow); + }, + + render(renderGl, args) { + if (!openglRenderWindow || !scenePlaced) return; + const camera = renderer.getActiveCamera(); + const transform = map.transform; + const targetMercator = computeCameraTargetMercator(maplibregl, transform); + const cameraMercator = computeCameraMercator(targetMercator, transform); + const viewMatrix = computeViewMatrix( + cameraMercator, + targetMercator, + computeViewUp(transform) + ); + const inverseViewMatrix = new Float64Array(16); + const projectionMatrix = new Float64Array(16); + + viewLight.setPosition( + cameraMercator.x, + cameraMercator.y, + cameraMercator.z + ); + viewLight.setFocalPoint( + targetMercator.x, + targetMercator.y, + targetMercator.z + ); + + mat4.invert(inverseViewMatrix, viewMatrix); + mat4.multiply( + projectionMatrix, + args.defaultProjectionData.mainMatrix, + inverseViewMatrix + ); + + camera.setViewMatrix(viewMatrix); + camera.setProjectionMatrix(projectionMatrix); + camera.modified(); + + // Shared rendering does not preserve host GL state, so restore any state + // this layer changes after vtk.js renders. + // MapLibre's projection includes a handedness flip, so compensate while + // rendering vtk.js geometry in the shared context. + const previousFrontFace = renderGl.getParameter(renderGl.FRONT_FACE); + renderGl.frontFace(renderGl.CW); + try { + openglRenderWindow.renderShared(); + } finally { + renderGl.frontFace(previousFrontFace); + } + }, + }; + + map.addLayer(vtkLayer); + map.once('idle', placeSceneOnTerrain); +} + +init(); diff --git a/Sources/Rendering/OpenGL/SharedRenderWindow/index.d.ts b/Sources/Rendering/OpenGL/SharedRenderWindow/index.d.ts new file mode 100644 index 00000000000..f803c40e36b --- /dev/null +++ b/Sources/Rendering/OpenGL/SharedRenderWindow/index.d.ts @@ -0,0 +1,60 @@ +import vtkOpenGLRenderWindow, { + IOpenGLRenderWindowInitialValues, +} from '../RenderWindow'; + +export interface ISharedRenderWindowInitialValues + extends IOpenGLRenderWindowInitialValues { + autoClear?: boolean; + autoClearColor?: boolean; + autoClearDepth?: boolean; +} + +export type SharedRenderCallback = () => void; + +export interface ISharedRenderOptions { + drawBuffers?: number[]; +} + +export interface vtkSharedRenderWindow extends vtkOpenGLRenderWindow { + /** Reset vtk.js render state and render into a host-owned WebGL2 context. */ + renderShared(options?: ISharedRenderOptions): void; + + /** Reset vtk.js GL state and sync size before shared-context rendering. */ + prepareSharedRender(options?: ISharedRenderOptions): void; + + syncSizeFromCanvas(): boolean; + + setRenderCallback(callback?: SharedRenderCallback | null): void; + + setAutoClear(autoClear: boolean): boolean; + getAutoClear(): boolean; + + setAutoClearColor(autoClearColor: boolean): boolean; + getAutoClearColor(): boolean; + + setAutoClearDepth(autoClearDepth: boolean): boolean; + getAutoClearDepth(): boolean; +} + +export function extend( + publicAPI: object, + model: object, + initialValues?: ISharedRenderWindowInitialValues +): void; + +export function newInstance( + initialValues?: ISharedRenderWindowInitialValues +): vtkSharedRenderWindow; + +export function createFromContext( + canvas: HTMLCanvasElement, + gl: WebGL2RenderingContext, + options?: ISharedRenderWindowInitialValues +): vtkSharedRenderWindow; + +export declare const vtkSharedRenderWindow: { + newInstance: typeof newInstance; + extend: typeof extend; + createFromContext: typeof createFromContext; +}; +export default vtkSharedRenderWindow; diff --git a/Sources/Rendering/OpenGL/SharedRenderWindow/index.js b/Sources/Rendering/OpenGL/SharedRenderWindow/index.js new file mode 100644 index 00000000000..4a1a7faa0d6 --- /dev/null +++ b/Sources/Rendering/OpenGL/SharedRenderWindow/index.js @@ -0,0 +1,298 @@ +import macro from 'vtk.js/Sources/macros'; +import { extend as extendOpenGLRenderWindow } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow'; +import vtkSharedRenderer from 'vtk.js/Sources/Rendering/OpenGL/SharedRenderer'; + +const PIXEL_STORE_STATE = [ + ['packAlignment', 'PACK_ALIGNMENT', 4], + ['unpackAlignment', 'UNPACK_ALIGNMENT', 4], + ['unpackFlipY', 'UNPACK_FLIP_Y_WEBGL', false], + ['unpackPremultiplyAlpha', 'UNPACK_PREMULTIPLY_ALPHA_WEBGL', false], + [ + 'unpackColorspaceConversion', + 'UNPACK_COLORSPACE_CONVERSION_WEBGL', + 'BROWSER_DEFAULT_WEBGL', + ], + ['packRowLength', 'PACK_ROW_LENGTH', 0], + ['packSkipRows', 'PACK_SKIP_ROWS', 0], + ['packSkipPixels', 'PACK_SKIP_PIXELS', 0], + ['unpackRowLength', 'UNPACK_ROW_LENGTH', 0], + ['unpackImageHeight', 'UNPACK_IMAGE_HEIGHT', 0], + ['unpackSkipRows', 'UNPACK_SKIP_ROWS', 0], + ['unpackSkipPixels', 'UNPACK_SKIP_PIXELS', 0], + ['unpackSkipImages', 'UNPACK_SKIP_IMAGES', 0], +]; + +function getSupportedState(gl, stateSpecs) { + return stateSpecs.filter(([, valueName]) => gl[valueName] !== undefined); +} + +function isWebGL2Context(gl) { + return ( + typeof WebGL2RenderingContext !== 'undefined' && + gl instanceof WebGL2RenderingContext + ); +} + +function getDefaultDrawBuffers(gl) { + const framebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + return framebuffer ? [gl.COLOR_ATTACHMENT0] : [gl.BACK]; +} + +function applyVTKRenderDefaults(gl) { + gl.blendFuncSeparate( + gl.SRC_ALPHA, + gl.ONE_MINUS_SRC_ALPHA, + gl.ONE, + gl.ONE_MINUS_SRC_ALPHA + ); + gl.depthFunc(gl.LEQUAL); + gl.enable(gl.BLEND); +} + +function resetGLState(gl, shaderCache, options = {}) { + const pixelStoreState = getSupportedState(gl, PIXEL_STORE_STATE); + + gl.disable(gl.BLEND); + gl.disable(gl.CULL_FACE); + gl.disable(gl.DEPTH_TEST); + gl.disable(gl.POLYGON_OFFSET_FILL); + gl.disable(gl.SCISSOR_TEST); + gl.disable(gl.STENCIL_TEST); + if (gl.SAMPLE_ALPHA_TO_COVERAGE) { + gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE); + } + + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.ONE, gl.ZERO); + gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ZERO); + gl.blendColor(0, 0, 0, 0); + + gl.colorMask(true, true, true, true); + gl.clearColor(0, 0, 0, 0); + + gl.depthMask(true); + gl.depthFunc(gl.LESS); + gl.clearDepth(1); + + gl.stencilMask(0xffffffff); + gl.stencilFunc(gl.ALWAYS, 0, 0xffffffff); + gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); + gl.clearStencil(0); + + gl.cullFace(gl.BACK); + gl.frontFace(gl.CCW); + + gl.polygonOffset(0, 0); + + gl.activeTexture(gl.TEXTURE0); + + pixelStoreState.forEach(([, paramName, defaultValue]) => { + const value = + typeof defaultValue === 'string' ? gl[defaultValue] : defaultValue; + gl.pixelStorei(gl[paramName], value); + }); + + if (gl.bindRenderbuffer) { + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + } + + gl.useProgram(null); + + gl.lineWidth(1); + + const width = gl.drawingBufferWidth; + const height = gl.drawingBufferHeight; + gl.scissor(0, 0, width, height); + gl.viewport(0, 0, width, height); + + if (gl.bindVertexArray) { + gl.bindVertexArray(null); + } + + if (gl.drawBuffers) { + gl.drawBuffers(options.drawBuffers || getDefaultDrawBuffers(gl)); + } + + applyVTKRenderDefaults(gl); + + if (shaderCache) { + shaderCache.setLastShaderProgramBound(null); + } +} + +function vtkSharedRenderWindow(publicAPI, model) { + model.classHierarchy.push('vtkSharedRenderWindow'); + let renderEventSubscription = null; + let renderCallback = null; + let suppressRenderEvent = false; + let savedEnableRender = null; + const superGet3DContext = publicAPI.get3DContext; + + function getInteractor() { + return model.renderable?.getInteractor?.(); + } + + function clearRenderEventSubscription() { + if (renderEventSubscription) { + renderEventSubscription.unsubscribe(); + renderEventSubscription = null; + } + } + + function bindRenderEvent(interactor) { + if (!interactor?.onRenderEvent || !renderCallback) { + return; + } + + renderEventSubscription = interactor.onRenderEvent(() => { + if (!suppressRenderEvent) { + renderCallback?.(); + } + }); + } + + publicAPI.renderShared = (options = {}) => { + publicAPI.prepareSharedRender(options); + try { + if (model.renderable) { + if (renderCallback && !renderEventSubscription) { + publicAPI.setRenderCallback(renderCallback); + } + + const interactor = getInteractor(); + let previousEnableRender; + if (interactor?.getEnableRender) { + previousEnableRender = interactor.getEnableRender(); + if (!previousEnableRender) { + interactor.setEnableRender(true); + } + } + + suppressRenderEvent = true; + try { + model.renderable.preRender?.(); + if (interactor) { + interactor.render(); + } else { + const views = model.renderable.getViews?.() || []; + views.forEach((view) => view.traverseAllPasses()); + } + } finally { + suppressRenderEvent = false; + if ( + interactor?.setEnableRender && + previousEnableRender !== undefined + ) { + interactor.setEnableRender(previousEnableRender); + } + } + } + } finally { + const shaderCache = publicAPI.getShaderCache(); + if (shaderCache) { + shaderCache.setLastShaderProgramBound(null); + } + } + }; + + publicAPI.get3DContext = (options) => { + if (model.context) { + return model.context; + } + return superGet3DContext(options); + }; + + /** + * Sync internal size state from the canvas's actual drawing buffer dimensions. + * Use this when sharing a WebGL context with another library (like MapLibre) + * that manages the canvas size. Returns true if size changed. + */ + publicAPI.syncSizeFromCanvas = () => { + if (!model.context) return false; + const width = model.context.drawingBufferWidth; + const height = model.context.drawingBufferHeight; + return publicAPI.setSize(width, height); + }; + + publicAPI.prepareSharedRender = (options = {}) => { + publicAPI.syncSizeFromCanvas(); + const gl = model.context; + if (!gl) return; + resetGLState(gl, publicAPI.getShaderCache(), options); + }; + + publicAPI.setRenderCallback = (callback) => { + renderCallback = callback || null; + clearRenderEventSubscription(); + + const interactor = getInteractor(); + if (renderCallback && interactor?.onRenderEvent) { + // Render requests flow through the interactor RenderEvent; redirect those + // to the host render loop while keeping draw calls inside renderShared(). + if (savedEnableRender === null && interactor.getEnableRender) { + savedEnableRender = interactor.getEnableRender(); + } + interactor?.setEnableRender?.(false); + bindRenderEvent(interactor); + return; + } + + if (!renderCallback && interactor && savedEnableRender !== null) { + interactor.setEnableRender?.(savedEnableRender); + savedEnableRender = null; + } + }; + + publicAPI.delete = macro.chain(() => { + clearRenderEventSubscription(); + if (savedEnableRender !== null) { + const interactor = getInteractor(); + interactor?.setEnableRender?.(savedEnableRender); + savedEnableRender = null; + } + renderCallback = null; + }, publicAPI.delete); +} + +const DEFAULT_VALUES = { + autoClear: false, + autoClearColor: true, + autoClearDepth: true, +}; + +export function extend(publicAPI, model, initialValues = {}) { + const mergedValues = { ...DEFAULT_VALUES, ...initialValues }; + extendOpenGLRenderWindow(publicAPI, model, mergedValues); + macro.setGet(publicAPI, model, [ + 'autoClear', + 'autoClearColor', + 'autoClearDepth', + ]); + vtkSharedRenderWindow(publicAPI, model); + publicAPI + .getViewNodeFactory() + .registerOverride('vtkRenderer', vtkSharedRenderer.newInstance); +} + +export const newInstance = macro.newInstance(extend, 'vtkSharedRenderWindow'); + +export function createFromContext(canvas, gl, options = {}) { + if (!isWebGL2Context(gl)) { + throw new Error('vtkSharedRenderWindow requires a WebGL2 context'); + } + if (gl.canvas && gl.canvas !== canvas) { + throw new Error( + 'vtkSharedRenderWindow requires the provided canvas to match gl.canvas' + ); + } + + return newInstance({ + ...options, + canvas, + context: gl, + manageCanvas: false, + webgl2: true, + }); +} + +export default { newInstance, extend, createFromContext }; diff --git a/Sources/Rendering/OpenGL/SharedRenderWindow/test/helpers.js b/Sources/Rendering/OpenGL/SharedRenderWindow/test/helpers.js new file mode 100644 index 00000000000..21d1c535be0 --- /dev/null +++ b/Sources/Rendering/OpenGL/SharedRenderWindow/test/helpers.js @@ -0,0 +1,51 @@ +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import vtkConeSource from 'vtk.js/Sources/Filters/Sources/ConeSource'; +import vtkSharedRenderWindow from 'vtk.js/Sources/Rendering/OpenGL/SharedRenderWindow'; +import { GET_UNDERLYING_CONTEXT } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow/ContextProxy'; + +export default function createSharedWindow( + gc, + t, + { width = 400, height = 400, background = [0.2, 0.3, 0.4] } = {} +) { + const container = document.querySelector('body'); + const renderWindowContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(renderWindowContainer); + + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + const renderer = gc.registerResource(vtkRenderer.newInstance()); + renderWindow.addRenderer(renderer); + renderer.setBackground(...background); + + const actor = gc.registerResource(vtkActor.newInstance()); + renderer.addActor(actor); + const mapper = gc.registerResource(vtkMapper.newInstance()); + actor.setMapper(mapper); + const cone = gc.registerResource(vtkConeSource.newInstance()); + mapper.setInputConnection(cone.getOutputPort()); + + const glWindow = gc.registerResource(renderWindow.newAPISpecificView()); + glWindow.setContainer(renderWindowContainer); + renderWindow.addView(glWindow); + glWindow.setSize(width, height); + + const glProxy = glWindow.get3DContext(); + const gl = glProxy?.[GET_UNDERLYING_CONTEXT]?.(); + t.ok(gl, 'WebGL context created'); + + const sharedWindow = gc.registerResource( + vtkSharedRenderWindow.createFromContext(glWindow.getCanvas(), gl) + ); + sharedWindow.setAutoClear(true); + sharedWindow.setSize(width, height); + renderWindow.removeView(glWindow); + renderWindow.addView(sharedWindow); + renderer.resetCamera(); + + return { gl, sharedWindow, renderer, renderWindow }; +} diff --git a/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindow.js b/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindow.js new file mode 100644 index 00000000000..9642c16dbaf --- /dev/null +++ b/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindow.js @@ -0,0 +1,272 @@ +import test from 'tape'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; +import 'vtk.js/Sources/Rendering/Misc/RenderingAPIs'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import vtkConeSource from 'vtk.js/Sources/Filters/Sources/ConeSource'; +import vtkSphereSource from 'vtk.js/Sources/Filters/Sources/SphereSource'; +import vtkCubeSource from 'vtk.js/Sources/Filters/Sources/CubeSource'; +import vtkSharedRenderWindow from 'vtk.js/Sources/Rendering/OpenGL/SharedRenderWindow'; +import { GET_UNDERLYING_CONTEXT } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow/ContextProxy'; + +import baseline from '../../../Core/RenderWindow/test/testMultipleRenderers.png'; +import baseline2 from '../../../Core/RenderWindow/test/testMultipleRenderers2.png'; + +test.onlyIfWebGL('Test shared render window from existing context', (t) => { + const gc = testUtils.createGarbageCollector(); + + // Create some control UI + const container = document.querySelector('body'); + const renderWindowContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(renderWindowContainer); + + // create what we will view + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + + // Upper renderer + const upperRenderer = gc.registerResource(vtkRenderer.newInstance()); + upperRenderer.setViewport(0, 0.5, 1, 1); + renderWindow.addRenderer(upperRenderer); + upperRenderer.setBackground(0.32, 0.34, 0.43); + + const coneActor = gc.registerResource(vtkActor.newInstance()); + upperRenderer.addActor(coneActor); + + const coneMapper = gc.registerResource(vtkMapper.newInstance()); + coneActor.setMapper(coneMapper); + + const coneSource = gc.registerResource( + vtkConeSource.newInstance({ height: 1.0 }) + ); + coneMapper.setInputConnection(coneSource.getOutputPort()); + + // Lower left renderer + const lowerLeftRenderer = gc.registerResource(vtkRenderer.newInstance()); + lowerLeftRenderer.setViewport(0, 0, 0.5, 0.5); + renderWindow.addRenderer(lowerLeftRenderer); + lowerLeftRenderer.setBackground(0, 0.5, 0); + + const sphereActor = gc.registerResource(vtkActor.newInstance()); + lowerLeftRenderer.addActor(sphereActor); + + const sphereMapper = gc.registerResource(vtkMapper.newInstance()); + sphereActor.setMapper(sphereMapper); + + const sphereSource = gc.registerResource(vtkSphereSource.newInstance()); + sphereMapper.setInputConnection(sphereSource.getOutputPort()); + + // Lower right renderer + const lowerRightRenderer = gc.registerResource(vtkRenderer.newInstance()); + lowerRightRenderer.setViewport(0.5, 0, 1, 0.5); + renderWindow.addRenderer(lowerRightRenderer); + lowerRightRenderer.setBackground(0, 0, 0.5); + + const cubeActor = gc.registerResource(vtkActor.newInstance()); + lowerRightRenderer.addActor(cubeActor); + + const cubeMapper = gc.registerResource(vtkMapper.newInstance()); + cubeActor.setMapper(cubeMapper); + + const cubeSource = gc.registerResource(vtkCubeSource.newInstance()); + cubeMapper.setInputConnection(cubeSource.getOutputPort()); + + const glWindow = gc.registerResource(renderWindow.newAPISpecificView()); + glWindow.setContainer(renderWindowContainer); + renderWindow.addView(glWindow); + glWindow.setSize(400, 400); + + // Force context creation on the OpenGL render window + const glProxy = glWindow.get3DContext(); + const gl = glProxy?.[GET_UNDERLYING_CONTEXT]?.(); + t.ok(gl, 'Shared WebGL context created'); + + const sharedWindow = gc.registerResource( + vtkSharedRenderWindow.createFromContext(glWindow.getCanvas(), gl) + ); + sharedWindow.setAutoClear(true); + sharedWindow.setSize(400, 400); + + renderWindow.removeView(glWindow); + renderWindow.addView(sharedWindow); + + upperRenderer.resetCamera(); + lowerLeftRenderer.resetCamera(); + lowerRightRenderer.resetCamera(); + + const promise = sharedWindow + .captureNextImage() + .then((image) => + testUtils.compareImages( + image, + [baseline, baseline2], + 'Rendering/OpenGL/SharedRenderWindow/testSharedRenderWindow', + t, + 5 + ) + ) + .finally(gc.releaseResources); + sharedWindow.renderShared(); + return promise; +}); + +test.onlyIfWebGL( + 'Test shared render window keeps vtkSharedRenderer local to its factory', + (t) => { + const gc = testUtils.createGarbageCollector(); + const container = document.querySelector('body'); + + const sharedContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(sharedContainer); + + const sharedRenderWindow = gc.registerResource( + vtkRenderWindow.newInstance() + ); + const sharedRenderer = gc.registerResource(vtkRenderer.newInstance()); + sharedRenderWindow.addRenderer(sharedRenderer); + + const sharedGlWindow = gc.registerResource( + sharedRenderWindow.newAPISpecificView() + ); + sharedGlWindow.setContainer(sharedContainer); + sharedRenderWindow.addView(sharedGlWindow); + sharedGlWindow.setSize(200, 200); + + const sharedGlProxy = sharedGlWindow.get3DContext(); + const sharedGl = sharedGlProxy?.[GET_UNDERLYING_CONTEXT]?.(); + t.ok(sharedGl, 'Shared-context source window created'); + + const sharedWindow = gc.registerResource( + vtkSharedRenderWindow.createFromContext( + sharedGlWindow.getCanvas(), + sharedGl + ) + ); + sharedRenderWindow.removeView(sharedGlWindow); + sharedRenderWindow.addView(sharedWindow); + sharedRenderWindow.render(); + + const sharedRendererNode = sharedWindow.getViewNodeFor(sharedRenderer); + t.ok( + sharedRendererNode?.isA('vtkSharedRenderer'), + 'Shared window uses vtkSharedRenderer' + ); + + const normalContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(normalContainer); + + const normalRenderWindow = gc.registerResource( + vtkRenderWindow.newInstance() + ); + const normalRenderer = gc.registerResource(vtkRenderer.newInstance()); + normalRenderWindow.addRenderer(normalRenderer); + + const normalGlWindow = gc.registerResource( + normalRenderWindow.newAPISpecificView() + ); + normalGlWindow.setContainer(normalContainer); + normalRenderWindow.addView(normalGlWindow); + normalGlWindow.setSize(200, 200); + normalRenderWindow.render(); + + const normalRendererNode = normalGlWindow.getViewNodeFor(normalRenderer); + t.ok( + normalRendererNode?.isA('vtkOpenGLRenderer'), + 'Normal window keeps vtkOpenGLRenderer' + ); + t.notOk( + normalRendererNode?.isA('vtkSharedRenderer'), + 'Normal window does not inherit vtkSharedRenderer' + ); + + gc.releaseResources(); + t.end(); + } +); + +test.onlyIfWebGL('Test shared render window rejects WebGL1 contexts', (t) => { + const canvas = document.createElement('canvas'); + const gl = + canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + + if (!gl) { + t.pass('WebGL1 unavailable in this environment'); + t.end(); + return; + } + + t.throws( + () => vtkSharedRenderWindow.createFromContext(canvas, gl), + /WebGL2 context/, + 'createFromContext rejects WebGL1 contexts' + ); + t.end(); +}); + +test.onlyIfWebGL( + 'Test shared render window does not manage external canvas DOM state', + (t) => { + const gc = testUtils.createGarbageCollector(); + const container = document.querySelector('body'); + const renderWindowContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(renderWindowContainer); + + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + const renderer = gc.registerResource(vtkRenderer.newInstance()); + renderWindow.addRenderer(renderer); + + const glWindow = gc.registerResource(renderWindow.newAPISpecificView()); + glWindow.setContainer(renderWindowContainer); + renderWindow.addView(glWindow); + glWindow.setSize(200, 200); + + const glProxy = glWindow.get3DContext(); + const gl = glProxy?.[GET_UNDERLYING_CONTEXT]?.(); + t.ok(gl, 'Shared WebGL context created'); + + const canvas = glWindow.getCanvas(); + canvas.style.display = 'inline-block'; + const originalWidth = canvas.width; + const originalHeight = canvas.height; + const originalDisplay = canvas.style.display; + + const sharedWindow = gc.registerResource( + vtkSharedRenderWindow.createFromContext(canvas, gl) + ); + renderWindow.removeView(glWindow); + renderWindow.addView(sharedWindow); + + sharedWindow.setSize(123, 77); + sharedWindow.setUseOffScreen(true); + + t.equal(canvas.width, originalWidth, 'External canvas width preserved'); + t.equal(canvas.height, originalHeight, 'External canvas height preserved'); + t.equal( + canvas.style.display, + originalDisplay, + 'External canvas display preserved' + ); + + t.throws( + () => + sharedWindow.captureNextImage('image/png', { + size: [100, 100], + }), + /manageCanvas=true/, + 'Resize capture rejects when canvas management is disabled' + ); + + gc.releaseResources(); + t.end(); + } +); diff --git a/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowGLState.js b/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowGLState.js new file mode 100644 index 00000000000..bbc5a0e94d9 --- /dev/null +++ b/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowGLState.js @@ -0,0 +1,168 @@ +import test from 'tape'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import 'vtk.js/Sources/Rendering/Misc/RenderingAPIs'; +import createSharedWindow from './helpers'; + +function createDirtyHostResources(gl) { + return { + arrayBuffer: gl.createBuffer(), + elementArrayBuffer: gl.createBuffer(), + renderbuffer: gl.createRenderbuffer(), + texture: gl.createTexture(), + }; +} + +function deleteDirtyHostResources(gl, resources) { + gl.deleteBuffer(resources.arrayBuffer); + gl.deleteBuffer(resources.elementArrayBuffer); + gl.deleteRenderbuffer(resources.renderbuffer); + gl.deleteTexture(resources.texture); +} + +function dirtyHostGLState(gl, resources) { + gl.enable(gl.BLEND); + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + gl.enable(gl.SCISSOR_TEST); + gl.depthMask(false); + gl.colorMask(true, false, true, false); + gl.clearColor(1, 0, 0, 1); + gl.scissor(20, 30, 80, 90); + gl.viewport(20, 30, 120, 130); + + gl.bindBuffer(gl.ARRAY_BUFFER, resources.arrayBuffer); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, resources.elementArrayBuffer); + gl.bindRenderbuffer(gl.RENDERBUFFER, resources.renderbuffer); + + gl.activeTexture(gl.TEXTURE3); + gl.bindTexture(gl.TEXTURE_2D, resources.texture); + + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 2); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + + // Defaults that applyVTKRenderDefaults should restore. + gl.disable(gl.BLEND); + gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ZERO); + gl.depthFunc(gl.GREATER); +} + +function createHostFramebuffer(gl, width, height) { + const framebuffer = gl.createFramebuffer(); + const colorTexture = gl.createTexture(); + const depthRenderbuffer = gl.createRenderbuffer(); + + gl.bindTexture(gl.TEXTURE_2D, colorTexture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + width, + height, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + null + ); + + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + colorTexture, + 0 + ); + + gl.bindRenderbuffer(gl.RENDERBUFFER, depthRenderbuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, + gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, + depthRenderbuffer + ); + + return { framebuffer, colorTexture, depthRenderbuffer }; +} + +function deleteHostFramebuffer(gl, fbo) { + gl.deleteFramebuffer(fbo.framebuffer); + gl.deleteTexture(fbo.colorTexture); + gl.deleteRenderbuffer(fbo.depthRenderbuffer); +} + +test.onlyIfWebGL( + 'Test renderShared resets host GL state and applies vtk.js defaults', + (t) => { + const gc = testUtils.createGarbageCollector(); + const { gl, sharedWindow } = createSharedWindow(gc, t); + + const hostResources = createDirtyHostResources(gl); + dirtyHostGLState(gl, hostResources); + + sharedWindow.prepareSharedRender(); + + t.equal(gl.isEnabled(gl.BLEND), true, 'Blending is enabled'); + t.equal( + gl.getParameter(gl.BLEND_SRC_RGB), + gl.SRC_ALPHA, + 'RGB blend source matches vtk.js default' + ); + t.equal( + gl.getParameter(gl.DEPTH_FUNC), + gl.LEQUAL, + 'Depth function matches vtk.js default' + ); + + sharedWindow.renderShared(); + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + const px = new Uint8Array(4); + gl.readPixels(5, 5, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); + t.ok( + px[0] > 20 && px[1] > 40 && px[2] > 60, + `Shared render cleared the full framebuffer despite host state, got rgba(${px[0]},${px[1]},${px[2]},${px[3]})` + ); + + deleteDirtyHostResources(gl, hostResources); + gc.releaseResources(); + t.end(); + } +); + +test.onlyIfWebGL( + 'Test renderShared draws into the currently bound host framebuffer', + (t) => { + const gc = testUtils.createGarbageCollector(); + const { gl, sharedWindow } = createSharedWindow(gc, t); + + const hostFramebuffer = createHostFramebuffer(gl, 400, 400); + gl.bindFramebuffer(gl.FRAMEBUFFER, hostFramebuffer.framebuffer); + t.equal( + gl.checkFramebufferStatus(gl.FRAMEBUFFER), + gl.FRAMEBUFFER_COMPLETE, + 'Host framebuffer is complete' + ); + gl.drawBuffers([gl.NONE]); + + sharedWindow.renderShared(); + + gl.bindFramebuffer(gl.FRAMEBUFFER, hostFramebuffer.framebuffer); + const px = new Uint8Array(4); + gl.readPixels(200, 200, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); + t.ok( + px[0] > 0 || px[1] > 0 || px[2] > 0, + `Shared render wrote into the host framebuffer, got rgba(${px[0]},${px[1]},${px[2]},${px[3]})` + ); + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + deleteHostFramebuffer(gl, hostFramebuffer); + gc.releaseResources(); + t.end(); + } +); diff --git a/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowHostSurvival.js b/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowHostSurvival.js new file mode 100644 index 00000000000..3983d2bb5cb --- /dev/null +++ b/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowHostSurvival.js @@ -0,0 +1,113 @@ +import test from 'tape'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import 'vtk.js/Sources/Rendering/Misc/RenderingAPIs'; +import createSharedWindow from './helpers'; + +const VERT_SRC = ` + attribute vec2 aPos; + void main() { gl_Position = vec4(aPos, 0.0, 1.0); } +`; +const FRAG_SRC = ` + precision mediump float; + uniform vec4 uColor; + void main() { gl_FragColor = uColor; } +`; + +function createHostProgram(gl) { + const vs = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vs, VERT_SRC); + gl.compileShader(vs); + + const fs = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fs, FRAG_SRC); + gl.compileShader(fs); + + const prog = gl.createProgram(); + gl.attachShader(prog, vs); + gl.attachShader(prog, fs); + gl.linkProgram(prog); + + gl.detachShader(prog, vs); + gl.detachShader(prog, fs); + gl.deleteShader(vs); + gl.deleteShader(fs); + + return prog; +} + +function createHostVAO(gl, prog) { + const verts = new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]); + const vao = gl.createVertexArray(); + gl.bindVertexArray(vao); + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW); + const loc = gl.getAttribLocation(prog, 'aPos'); + gl.enableVertexAttribArray(loc); + gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); + gl.bindVertexArray(null); + return { vao, buf }; +} + +function setupHostState(gl, prog, vao, width, height) { + gl.useProgram(prog); + gl.bindVertexArray(vao); + gl.viewport(width / 2, 0, width / 2, height); + gl.scissor(width / 2, 0, width / 2, height); + gl.enable(gl.SCISSOR_TEST); + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.disable(gl.DEPTH_TEST); + gl.depthMask(false); +} + +function hostDraw(gl, prog) { + const colorLoc = gl.getUniformLocation(prog, 'uColor'); + gl.uniform4f(colorLoc, 0.0, 0.8, 0.0, 1.0); + gl.drawArrays(gl.TRIANGLES, 0, 6); +} + +test.onlyIfWebGL( + 'Test host raw WebGL rendering can resume after renderShared when state is rebound', + (t) => { + const gc = testUtils.createGarbageCollector(); + const { gl, sharedWindow } = createSharedWindow(gc, t, { + background: [0.1, 0.1, 0.2], + }); + + const hostProg = createHostProgram(gl); + const { vao: hostVAO, buf: hostBuf } = createHostVAO(gl, hostProg); + const W = 400; + const H = 400; + + setupHostState(gl, hostProg, hostVAO, W, H); + hostDraw(gl, hostProg); + + sharedWindow.renderShared(); + + setupHostState(gl, hostProg, hostVAO, W, H); + hostDraw(gl, hostProg); + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + const px = new Uint8Array(4); + gl.readPixels(300, 200, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); + t.ok( + px[1] > 150 && px[0] < 50 && px[2] < 50, + `Right half center should be green, got rgba(${px[0]},${px[1]},${px[2]},${px[3]})` + ); + + gl.readPixels(100, 200, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px); + const isHostGreen = px[1] > 150 && px[0] < 50 && px[2] < 50; + t.notOk( + isHostGreen, + `Left half should not be host green, got rgba(${px[0]},${px[1]},${px[2]},${px[3]})` + ); + + gl.deleteProgram(hostProg); + gl.deleteVertexArray(hostVAO); + gl.deleteBuffer(hostBuf); + gc.releaseResources(); + t.end(); + } +); diff --git a/Sources/Rendering/OpenGL/SharedRenderer/index.d.ts b/Sources/Rendering/OpenGL/SharedRenderer/index.d.ts new file mode 100644 index 00000000000..d7f13777a9b --- /dev/null +++ b/Sources/Rendering/OpenGL/SharedRenderer/index.d.ts @@ -0,0 +1,58 @@ +import vtkViewNode, { + IViewNodeInitialValues, +} from '../../../Rendering/SceneGraph/ViewNode'; + +export interface ISharedRendererInitialValues extends IViewNodeInitialValues { + context?: WebGLRenderingContext | WebGL2RenderingContext | null; + selector?: any; + _openGLRenderWindow?: any; +} + +export interface vtkSharedRenderer extends vtkViewNode { + buildPass(prepass: boolean): void; + + updateLights(): number; + + zBufferPass(prepass: boolean): void; + + opaqueZBufferPass(prepass: boolean): void; + + cameraPass(prepass: boolean): void; + + getAspectRatio(): number; + + getTiledSizeAndOrigin(): { + usize: number; + vsize: number; + lowerLeftU: number; + lowerLeftV: number; + }; + + clear(): void; + + releaseGraphicsResources(): void; + + setOpenGLRenderWindow(rw: any): void; + + getShaderCache(): any; + + getSelector(): any; + + setSelector(selector: any): boolean; +} + +export function extend( + publicAPI: object, + model: object, + initialValues?: ISharedRendererInitialValues +): void; + +export function newInstance( + initialValues?: ISharedRendererInitialValues +): vtkSharedRenderer; + +export declare const vtkSharedRenderer: { + newInstance: typeof newInstance; + extend: typeof extend; +}; +export default vtkSharedRenderer; diff --git a/Sources/Rendering/OpenGL/SharedRenderer/index.js b/Sources/Rendering/OpenGL/SharedRenderer/index.js new file mode 100644 index 00000000000..a873113a0cf --- /dev/null +++ b/Sources/Rendering/OpenGL/SharedRenderer/index.js @@ -0,0 +1,62 @@ +import macro from 'vtk.js/Sources/macros'; +import { extend as extendOpenGLRenderer } from 'vtk.js/Sources/Rendering/OpenGL/Renderer'; + +function vtkSharedRenderer(publicAPI, model) { + model.classHierarchy.push('vtkSharedRenderer'); + + publicAPI.clear = () => { + const gl = model.context; + const openGLRenderWindow = model._openGLRenderWindow; + + const autoClear = openGLRenderWindow?.getAutoClear?.() ?? true; + if (autoClear === false) { + const ts = publicAPI.getTiledSizeAndOrigin(); + gl.enable(gl.SCISSOR_TEST); + gl.scissor(ts.lowerLeftU, ts.lowerLeftV, ts.usize, ts.vsize); + gl.viewport(ts.lowerLeftU, ts.lowerLeftV, ts.usize, ts.vsize); + gl.enable(gl.DEPTH_TEST); + return; + } + + const shouldClearColor = openGLRenderWindow?.getAutoClearColor?.() ?? true; + const shouldClearDepth = openGLRenderWindow?.getAutoClearDepth?.() ?? true; + + let clearMask = 0; + + if (!model.renderable.getTransparent() && shouldClearColor) { + const background = model.renderable.getBackgroundByReference(); + gl.clearColor(background[0], background[1], background[2], background[3]); + // eslint-disable-next-line no-bitwise + clearMask |= gl.COLOR_BUFFER_BIT; + } + + if (!model.renderable.getPreserveDepthBuffer() && shouldClearDepth) { + gl.clearDepth(1.0); + // eslint-disable-next-line no-bitwise + clearMask |= gl.DEPTH_BUFFER_BIT; + gl.depthMask(true); + } + + gl.colorMask(true, true, true, true); + + const ts = publicAPI.getTiledSizeAndOrigin(); + gl.enable(gl.SCISSOR_TEST); + gl.scissor(ts.lowerLeftU, ts.lowerLeftV, ts.usize, ts.vsize); + gl.viewport(ts.lowerLeftU, ts.lowerLeftV, ts.usize, ts.vsize); + + if (clearMask) { + gl.clear(clearMask); + } + + gl.enable(gl.DEPTH_TEST); + }; +} + +export function extend(publicAPI, model, initialValues = {}) { + extendOpenGLRenderer(publicAPI, model, initialValues); + vtkSharedRenderer(publicAPI, model); +} + +export const newInstance = macro.newInstance(extend, 'vtkSharedRenderer'); + +export default { newInstance, extend }; diff --git a/Sources/Rendering/OpenGL/ViewNodeFactory/index.js b/Sources/Rendering/OpenGL/ViewNodeFactory/index.js index c72cbc1b9eb..116489666c1 100644 --- a/Sources/Rendering/OpenGL/ViewNodeFactory/index.js +++ b/Sources/Rendering/OpenGL/ViewNodeFactory/index.js @@ -27,12 +27,17 @@ const DEFAULT_VALUES = {}; export function extend(publicAPI, model, initialValues = {}) { Object.assign(model, DEFAULT_VALUES, initialValues); - // Static class mapping shared across instances - model.overrides = CLASS_MAPPING; + // Each factory inherits the shared OpenGL mappings while still allowing + // instance-local overrides such as vtkSharedRenderer. + model.overrides = Object.create(CLASS_MAPPING); // Inheritance vtkViewNodeFactory.extend(publicAPI, model, initialValues); + publicAPI.registerOverride = (className, fn) => { + model.overrides[className] = fn; + }; + // Object methods vtkOpenGLViewNodeFactory(publicAPI, model); } diff --git a/Sources/Rendering/SceneGraph/ViewNodeFactory/index.js b/Sources/Rendering/SceneGraph/ViewNodeFactory/index.js index d36b128a38e..b7904e1999f 100644 --- a/Sources/Rendering/SceneGraph/ViewNodeFactory/index.js +++ b/Sources/Rendering/SceneGraph/ViewNodeFactory/index.js @@ -21,9 +21,8 @@ function vtkViewNodeFactory(publicAPI, model) { let cpt = 0; let className = dataObject.getClassName(cpt++); let isObject = false; - const keys = Object.keys(model.overrides); while (className && !isObject) { - if (keys.indexOf(className) !== -1) { + if (className in model.overrides) { isObject = true; } else { className = dataObject.getClassName(cpt++); From 5d1f87a0b088e1bcf7615d2a2f1d48376e8bb6aa Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 8 Apr 2026 11:00:13 -0400 Subject: [PATCH 3/4] refactor: move method definitions into function bodies Move registerOverride from extend() into the main function body for both ViewNodeFactory and SharedRenderWindow. --- Sources/Rendering/OpenGL/SharedRenderWindow/index.js | 8 +++++--- Sources/Rendering/OpenGL/ViewNodeFactory/index.js | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/Rendering/OpenGL/SharedRenderWindow/index.js b/Sources/Rendering/OpenGL/SharedRenderWindow/index.js index 4a1a7faa0d6..d943d513f4b 100644 --- a/Sources/Rendering/OpenGL/SharedRenderWindow/index.js +++ b/Sources/Rendering/OpenGL/SharedRenderWindow/index.js @@ -122,6 +122,11 @@ function resetGLState(gl, shaderCache, options = {}) { function vtkSharedRenderWindow(publicAPI, model) { model.classHierarchy.push('vtkSharedRenderWindow'); + + publicAPI + .getViewNodeFactory() + .registerOverride('vtkRenderer', vtkSharedRenderer.newInstance); + let renderEventSubscription = null; let renderCallback = null; let suppressRenderEvent = false; @@ -269,9 +274,6 @@ export function extend(publicAPI, model, initialValues = {}) { 'autoClearDepth', ]); vtkSharedRenderWindow(publicAPI, model); - publicAPI - .getViewNodeFactory() - .registerOverride('vtkRenderer', vtkSharedRenderer.newInstance); } export const newInstance = macro.newInstance(extend, 'vtkSharedRenderWindow'); diff --git a/Sources/Rendering/OpenGL/ViewNodeFactory/index.js b/Sources/Rendering/OpenGL/ViewNodeFactory/index.js index 116489666c1..a9282d33929 100644 --- a/Sources/Rendering/OpenGL/ViewNodeFactory/index.js +++ b/Sources/Rendering/OpenGL/ViewNodeFactory/index.js @@ -14,6 +14,10 @@ export function registerOverride(className, fn) { function vtkOpenGLViewNodeFactory(publicAPI, model) { // Set our className model.classHierarchy.push('vtkOpenGLViewNodeFactory'); + + publicAPI.registerOverride = (className, fn) => { + model.overrides[className] = fn; + }; } // ---------------------------------------------------------------------------- @@ -34,10 +38,6 @@ export function extend(publicAPI, model, initialValues = {}) { // Inheritance vtkViewNodeFactory.extend(publicAPI, model, initialValues); - publicAPI.registerOverride = (className, fn) => { - model.overrides[className] = fn; - }; - // Object methods vtkOpenGLViewNodeFactory(publicAPI, model); } From db65e0808fd39ea6fbfd003b02d1cc2269e2b54a Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 7 May 2026 15:34:54 -0400 Subject: [PATCH 4/4] fix(SharedRenderWindow): guard manageCanvas and respect host drawBuffers Gate the screenshot-path canvas display toggle on manageCanvas, matching the existing guard at line 164 so externally owned canvases are never restyled. In getDefaultDrawBuffers, query the host's actual DRAW_BUFFER0..N state when an FBO is bound rather than assuming COLOR_ATTACHMENT0. Falls back to [COLOR_ATTACHMENT0] when all slots are NONE so callers that left the default state untouched still get a valid render target. --- .../Rendering/OpenGL/RenderWindow/index.js | 4 +- .../OpenGL/SharedRenderWindow/index.js | 33 ++++++++++++++++- .../test/testSharedRenderWindowGLState.js | 37 +++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/Sources/Rendering/OpenGL/RenderWindow/index.js b/Sources/Rendering/OpenGL/RenderWindow/index.js index dea32cec402..a740a1a6c71 100644 --- a/Sources/Rendering/OpenGL/RenderWindow/index.js +++ b/Sources/Rendering/OpenGL/RenderWindow/index.js @@ -611,7 +611,9 @@ function vtkOpenGLRenderWindow(publicAPI, model) { model._screenshot.placeHolder = model.el.appendChild(tmpImg); // hide the main canvas - model.canvas.style.display = 'none'; + if (model.manageCanvas) { + model.canvas.style.display = 'none'; + } // remember the main canvas original size, then resize it model._screenshot.originalSize = model.size; diff --git a/Sources/Rendering/OpenGL/SharedRenderWindow/index.js b/Sources/Rendering/OpenGL/SharedRenderWindow/index.js index d943d513f4b..42dbeb82da6 100644 --- a/Sources/Rendering/OpenGL/SharedRenderWindow/index.js +++ b/Sources/Rendering/OpenGL/SharedRenderWindow/index.js @@ -35,7 +35,19 @@ function isWebGL2Context(gl) { function getDefaultDrawBuffers(gl) { const framebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); - return framebuffer ? [gl.COLOR_ATTACHMENT0] : [gl.BACK]; + if (!framebuffer) return [gl.BACK]; + const max = gl.getParameter(gl.MAX_DRAW_BUFFERS); + const buffers = []; + for (let i = 0; i < max; i += 1) { + buffers.push(gl.getParameter(gl.DRAW_BUFFER0 + i)); + } + // gl.drawBuffers requires bufs[i] to be NONE or COLOR_ATTACHMENTi, so + // dropping a NONE in the middle of the array would shift later attachments + // to the wrong slots. Trim trailing NONEs only. + while (buffers.length > 1 && buffers[buffers.length - 1] === gl.NONE) { + buffers.pop(); + } + return buffers[0] === gl.NONE ? [gl.COLOR_ATTACHMENT0] : buffers; } function applyVTKRenderDefaults(gl) { @@ -61,6 +73,13 @@ function resetGLState(gl, shaderCache, options = {}) { if (gl.SAMPLE_ALPHA_TO_COVERAGE) { gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE); } + // Hosts using transform feedback can leave RASTERIZER_DISCARD enabled, + // which silently produces blank vtk frames. Force-clear it. + gl.disable(gl.RASTERIZER_DISCARD); + // Reset SAMPLE_COVERAGE — a non-default value would cull vtk fragments in + // multisampled contexts. + gl.disable(gl.SAMPLE_COVERAGE); + gl.sampleCoverage(1, false); gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.ONE, gl.ZERO); @@ -73,6 +92,7 @@ function resetGLState(gl, shaderCache, options = {}) { gl.depthMask(true); gl.depthFunc(gl.LESS); gl.clearDepth(1); + gl.depthRange(0, 1); gl.stencilMask(0xffffffff); gl.stencilFunc(gl.ALWAYS, 0, 0xffffffff); @@ -96,6 +116,17 @@ function resetGLState(gl, shaderCache, options = {}) { gl.bindRenderbuffer(gl.RENDERBUFFER, null); } + // Unbind PBOs. A bound PIXEL_UNPACK_BUFFER turns texImage2D's data argument + // into a buffer offset (silent corruption); a bound PIXEL_PACK_BUFFER + // routes readPixels into the buffer instead of returning data. + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); + gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null); + + // Reset readBuffer to the default for the bound framebuffer. + gl.readBuffer( + gl.getParameter(gl.FRAMEBUFFER_BINDING) ? gl.COLOR_ATTACHMENT0 : gl.BACK + ); + gl.useProgram(null); gl.lineWidth(1); diff --git a/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowGLState.js b/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowGLState.js index bbc5a0e94d9..837f91fcf28 100644 --- a/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowGLState.js +++ b/Sources/Rendering/OpenGL/SharedRenderWindow/test/testSharedRenderWindowGLState.js @@ -10,6 +10,8 @@ function createDirtyHostResources(gl) { elementArrayBuffer: gl.createBuffer(), renderbuffer: gl.createRenderbuffer(), texture: gl.createTexture(), + pixelPackBuffer: gl.createBuffer(), + pixelUnpackBuffer: gl.createBuffer(), }; } @@ -18,6 +20,8 @@ function deleteDirtyHostResources(gl, resources) { gl.deleteBuffer(resources.elementArrayBuffer); gl.deleteRenderbuffer(resources.renderbuffer); gl.deleteTexture(resources.texture); + gl.deleteBuffer(resources.pixelPackBuffer); + gl.deleteBuffer(resources.pixelUnpackBuffer); } function dirtyHostGLState(gl, resources) { @@ -41,6 +45,14 @@ function dirtyHostGLState(gl, resources) { gl.pixelStorei(gl.UNPACK_ALIGNMENT, 2); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); + // States resetGLState must normalize for vtk to render correctly. + gl.enable(gl.RASTERIZER_DISCARD); + gl.depthRange(0.2, 0.8); + gl.enable(gl.SAMPLE_COVERAGE); + gl.sampleCoverage(0.25, true); + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, resources.pixelPackBuffer); + gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, resources.pixelUnpackBuffer); + // Defaults that applyVTKRenderDefaults should restore. gl.disable(gl.BLEND); gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ZERO); @@ -118,6 +130,31 @@ test.onlyIfWebGL( gl.LEQUAL, 'Depth function matches vtk.js default' ); + t.equal( + gl.isEnabled(gl.RASTERIZER_DISCARD), + false, + 'RASTERIZER_DISCARD disabled' + ); + t.deepEqual( + Array.from(gl.getParameter(gl.DEPTH_RANGE)), + [0, 1], + 'DEPTH_RANGE reset to default' + ); + t.equal( + gl.isEnabled(gl.SAMPLE_COVERAGE), + false, + 'SAMPLE_COVERAGE disabled' + ); + t.equal( + gl.getParameter(gl.PIXEL_PACK_BUFFER_BINDING), + null, + 'PIXEL_PACK_BUFFER unbound' + ); + t.equal( + gl.getParameter(gl.PIXEL_UNPACK_BUFFER_BINDING), + null, + 'PIXEL_UNPACK_BUFFER unbound' + ); sharedWindow.renderShared();