Skip to content

Commit 93f4a3a

Browse files
committed
feat: add SharedRenderWindow for external WebGL2 context sharing
1 parent 428a225 commit 93f4a3a

File tree

11 files changed

+1465
-4
lines changed

11 files changed

+1465
-4
lines changed
110 KB
Loading

Examples/Rendering/SharedContext/index.js

Lines changed: 404 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import vtkOpenGLRenderWindow, {
2+
IOpenGLRenderWindowInitialValues,
3+
} from '../RenderWindow';
4+
5+
export interface ISharedRenderWindowInitialValues
6+
extends IOpenGLRenderWindowInitialValues {
7+
autoClear?: boolean;
8+
autoClearColor?: boolean;
9+
autoClearDepth?: boolean;
10+
}
11+
12+
export type SharedRenderCallback = () => void;
13+
14+
export interface vtkSharedRenderWindow extends vtkOpenGLRenderWindow {
15+
/** Reset vtk.js render state and render into a host-owned WebGL2 context. */
16+
renderShared(options?: Record<string, any>): void;
17+
18+
/** Reset vtk.js GL state and sync size before shared-context rendering. */
19+
prepareSharedRender(options?: Record<string, any>): void;
20+
21+
syncSizeFromCanvas(): boolean;
22+
23+
setRenderCallback(callback?: SharedRenderCallback | null): void;
24+
25+
setAutoClear(autoClear: boolean): boolean;
26+
getAutoClear(): boolean;
27+
28+
setAutoClearColor(autoClearColor: boolean): boolean;
29+
getAutoClearColor(): boolean;
30+
31+
setAutoClearDepth(autoClearDepth: boolean): boolean;
32+
getAutoClearDepth(): boolean;
33+
}
34+
35+
export function extend(
36+
publicAPI: object,
37+
model: object,
38+
initialValues?: ISharedRenderWindowInitialValues
39+
): void;
40+
41+
export function newInstance(
42+
initialValues?: ISharedRenderWindowInitialValues
43+
): vtkSharedRenderWindow;
44+
45+
export function createFromContext(
46+
canvas: HTMLCanvasElement,
47+
gl: WebGL2RenderingContext,
48+
options?: ISharedRenderWindowInitialValues
49+
): vtkSharedRenderWindow;
50+
51+
export declare const vtkSharedRenderWindow: {
52+
newInstance: typeof newInstance;
53+
extend: typeof extend;
54+
createFromContext: typeof createFromContext;
55+
};
56+
export default vtkSharedRenderWindow;
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import macro from 'vtk.js/Sources/macros';
2+
import { extend as extendOpenGLRenderWindow } from 'vtk.js/Sources/Rendering/OpenGL/RenderWindow';
3+
import vtkSharedRenderer from 'vtk.js/Sources/Rendering/OpenGL/SharedRenderer';
4+
5+
const PIXEL_STORE_STATE = [
6+
['packAlignment', 'PACK_ALIGNMENT', 4],
7+
['unpackAlignment', 'UNPACK_ALIGNMENT', 4],
8+
['unpackFlipY', 'UNPACK_FLIP_Y_WEBGL', false],
9+
['unpackPremultiplyAlpha', 'UNPACK_PREMULTIPLY_ALPHA_WEBGL', false],
10+
[
11+
'unpackColorspaceConversion',
12+
'UNPACK_COLORSPACE_CONVERSION_WEBGL',
13+
'BROWSER_DEFAULT_WEBGL',
14+
],
15+
['packRowLength', 'PACK_ROW_LENGTH', 0],
16+
['packSkipRows', 'PACK_SKIP_ROWS', 0],
17+
['packSkipPixels', 'PACK_SKIP_PIXELS', 0],
18+
['unpackRowLength', 'UNPACK_ROW_LENGTH', 0],
19+
['unpackImageHeight', 'UNPACK_IMAGE_HEIGHT', 0],
20+
['unpackSkipRows', 'UNPACK_SKIP_ROWS', 0],
21+
['unpackSkipPixels', 'UNPACK_SKIP_PIXELS', 0],
22+
['unpackSkipImages', 'UNPACK_SKIP_IMAGES', 0],
23+
];
24+
25+
function getSupportedState(gl, stateSpecs) {
26+
return stateSpecs.filter(([, valueName]) => gl[valueName] !== undefined);
27+
}
28+
29+
function isWebGL2Context(gl) {
30+
return (
31+
typeof WebGL2RenderingContext !== 'undefined' &&
32+
gl instanceof WebGL2RenderingContext
33+
);
34+
}
35+
36+
function resetGLState(gl, shaderCache) {
37+
const pixelStoreState = getSupportedState(gl, PIXEL_STORE_STATE);
38+
39+
gl.disable(gl.BLEND);
40+
gl.disable(gl.CULL_FACE);
41+
gl.disable(gl.DEPTH_TEST);
42+
gl.disable(gl.POLYGON_OFFSET_FILL);
43+
gl.disable(gl.SCISSOR_TEST);
44+
gl.disable(gl.STENCIL_TEST);
45+
if (gl.SAMPLE_ALPHA_TO_COVERAGE) {
46+
gl.disable(gl.SAMPLE_ALPHA_TO_COVERAGE);
47+
}
48+
49+
gl.blendEquation(gl.FUNC_ADD);
50+
gl.blendFunc(gl.ONE, gl.ZERO);
51+
gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ZERO);
52+
gl.blendColor(0, 0, 0, 0);
53+
54+
gl.colorMask(true, true, true, true);
55+
gl.clearColor(0, 0, 0, 0);
56+
57+
gl.depthMask(true);
58+
gl.depthFunc(gl.LESS);
59+
gl.clearDepth(1);
60+
61+
gl.stencilMask(0xffffffff);
62+
gl.stencilFunc(gl.ALWAYS, 0, 0xffffffff);
63+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
64+
gl.clearStencil(0);
65+
66+
gl.cullFace(gl.BACK);
67+
gl.frontFace(gl.CCW);
68+
69+
gl.polygonOffset(0, 0);
70+
71+
gl.activeTexture(gl.TEXTURE0);
72+
73+
pixelStoreState.forEach(([, paramName, defaultValue]) => {
74+
const value =
75+
typeof defaultValue === 'string' ? gl[defaultValue] : defaultValue;
76+
gl.pixelStorei(gl[paramName], value);
77+
});
78+
79+
if (gl.bindRenderbuffer) {
80+
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
81+
}
82+
83+
gl.useProgram(null);
84+
85+
gl.lineWidth(1);
86+
87+
const width = gl.drawingBufferWidth;
88+
const height = gl.drawingBufferHeight;
89+
gl.scissor(0, 0, width, height);
90+
gl.viewport(0, 0, width, height);
91+
92+
if (gl.bindVertexArray) {
93+
gl.bindVertexArray(null);
94+
}
95+
96+
if (shaderCache) {
97+
shaderCache.setLastShaderProgramBound(null);
98+
}
99+
}
100+
101+
function vtkSharedRenderWindow(publicAPI, model) {
102+
model.classHierarchy.push('vtkSharedRenderWindow');
103+
let renderEventSubscription = null;
104+
let renderCallback = null;
105+
let suppressRenderEvent = false;
106+
let savedEnableRender = null;
107+
const superGet3DContext = publicAPI.get3DContext;
108+
109+
function getInteractor() {
110+
return model.renderable?.getInteractor?.();
111+
}
112+
113+
function clearRenderEventSubscription() {
114+
if (renderEventSubscription) {
115+
renderEventSubscription.unsubscribe();
116+
renderEventSubscription = null;
117+
}
118+
}
119+
120+
function bindRenderEvent(interactor) {
121+
if (!interactor?.onRenderEvent || !renderCallback) {
122+
return;
123+
}
124+
125+
renderEventSubscription = interactor.onRenderEvent(() => {
126+
if (!suppressRenderEvent) {
127+
renderCallback?.();
128+
}
129+
});
130+
}
131+
132+
publicAPI.renderShared = (options = {}) => {
133+
publicAPI.prepareSharedRender(options);
134+
try {
135+
if (model.renderable) {
136+
if (renderCallback && !renderEventSubscription) {
137+
publicAPI.setRenderCallback(renderCallback);
138+
}
139+
140+
const interactor = getInteractor();
141+
let previousEnableRender;
142+
if (interactor?.getEnableRender) {
143+
previousEnableRender = interactor.getEnableRender();
144+
if (!previousEnableRender) {
145+
interactor.setEnableRender(true);
146+
}
147+
}
148+
149+
suppressRenderEvent = true;
150+
try {
151+
model.renderable.preRender?.();
152+
if (interactor) {
153+
interactor.render();
154+
} else {
155+
const views = model.renderable.getViews?.() || [];
156+
views.forEach((view) => view.traverseAllPasses());
157+
}
158+
} finally {
159+
suppressRenderEvent = false;
160+
if (
161+
interactor?.setEnableRender &&
162+
previousEnableRender !== undefined
163+
) {
164+
interactor.setEnableRender(previousEnableRender);
165+
}
166+
}
167+
}
168+
} finally {
169+
const shaderCache = publicAPI.getShaderCache();
170+
if (shaderCache) {
171+
shaderCache.setLastShaderProgramBound(null);
172+
}
173+
}
174+
};
175+
176+
publicAPI.get3DContext = (options) => {
177+
if (model.context) {
178+
return model.context;
179+
}
180+
return superGet3DContext(options);
181+
};
182+
183+
/**
184+
* Sync internal size state from the canvas's actual drawing buffer dimensions.
185+
* Use this when sharing a WebGL context with another library (like MapLibre)
186+
* that manages the canvas size. Returns true if size changed.
187+
*/
188+
publicAPI.syncSizeFromCanvas = () => {
189+
if (!model.context) return false;
190+
const width = model.context.drawingBufferWidth;
191+
const height = model.context.drawingBufferHeight;
192+
return publicAPI.setSize(width, height);
193+
};
194+
195+
publicAPI.prepareSharedRender = () => {
196+
publicAPI.syncSizeFromCanvas();
197+
const gl = model.context;
198+
if (!gl) return;
199+
resetGLState(gl, publicAPI.getShaderCache());
200+
};
201+
202+
publicAPI.setRenderCallback = (callback) => {
203+
renderCallback = callback || null;
204+
clearRenderEventSubscription();
205+
206+
const interactor = getInteractor();
207+
if (renderCallback && interactor?.onRenderEvent) {
208+
// Render requests flow through the interactor RenderEvent; redirect those
209+
// to the host render loop while keeping draw calls inside renderShared().
210+
if (savedEnableRender === null && interactor.getEnableRender) {
211+
savedEnableRender = interactor.getEnableRender();
212+
}
213+
interactor?.setEnableRender?.(false);
214+
bindRenderEvent(interactor);
215+
return;
216+
}
217+
218+
if (!renderCallback && interactor && savedEnableRender !== null) {
219+
interactor.setEnableRender?.(savedEnableRender);
220+
savedEnableRender = null;
221+
}
222+
};
223+
}
224+
225+
const DEFAULT_VALUES = {
226+
autoClear: false,
227+
autoClearColor: true,
228+
autoClearDepth: true,
229+
};
230+
231+
export function extend(publicAPI, model, initialValues = {}) {
232+
const mergedValues = { ...DEFAULT_VALUES, ...initialValues };
233+
extendOpenGLRenderWindow(publicAPI, model, mergedValues);
234+
macro.setGet(publicAPI, model, [
235+
'autoClear',
236+
'autoClearColor',
237+
'autoClearDepth',
238+
]);
239+
vtkSharedRenderWindow(publicAPI, model);
240+
publicAPI
241+
.getViewNodeFactory()
242+
.registerOverride('vtkRenderer', vtkSharedRenderer.newInstance);
243+
}
244+
245+
export const newInstance = macro.newInstance(extend, 'vtkSharedRenderWindow');
246+
247+
export function createFromContext(canvas, gl, options = {}) {
248+
if (!isWebGL2Context(gl)) {
249+
throw new Error('vtkSharedRenderWindow requires a WebGL2 context');
250+
}
251+
if (gl.canvas && gl.canvas !== canvas) {
252+
throw new Error(
253+
'vtkSharedRenderWindow requires the provided canvas to match gl.canvas'
254+
);
255+
}
256+
257+
return newInstance({
258+
...options,
259+
canvas,
260+
context: gl,
261+
manageCanvas: false,
262+
webgl2: true,
263+
});
264+
}
265+
266+
export default { newInstance, extend, createFromContext };

0 commit comments

Comments
 (0)