Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions Sources/Rendering/OpenGL/Framebuffer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ function vtkFramebuffer(publicAPI, model) {
// publicAPI.getDrawMode = () => model.context.DRAW_FRAMEBUFFER;
// publicAPI.getReadMode = () => model.context.READ_FRAMEBUFFER;

// Keep the render window's JS-side binding mirror in step with binds made
// through this class. Inert until someone seeds the tracker (see
// OpenGLRenderWindow.setFramebufferBinding).
function syncTrackedBinding(binding) {
const renderWindow = model._openGLRenderWindow;
if (renderWindow && renderWindow.getFramebufferBinding() !== undefined) {
renderWindow.setFramebufferBinding(binding);
}
}

publicAPI.saveCurrentBindingsAndBuffers = (modeIn) => {
const mode =
typeof modeIn !== 'undefined' ? modeIn : publicAPI.getBothMode();
Expand All @@ -29,10 +39,12 @@ function vtkFramebuffer(publicAPI, model) {
return;
}

// gl.getParameter(FRAMEBUFFER_BINDING) is a synchronous CPU/GPU sync
// point; prefer the render window's JS-side mirror when it is seeded.
const gl = model.context;
model.previousDrawBinding = gl.getParameter(
model.context.FRAMEBUFFER_BINDING
);
const tracked = model._openGLRenderWindow.getFramebufferBinding();
model.previousDrawBinding =
tracked !== undefined ? tracked : gl.getParameter(gl.FRAMEBUFFER_BINDING);
model.previousActiveFramebuffer =
model._openGLRenderWindow.getActiveFramebuffer();
};
Expand All @@ -58,6 +70,7 @@ function vtkFramebuffer(publicAPI, model) {

const gl = model.context;
gl.bindFramebuffer(gl.FRAMEBUFFER, model.previousDrawBinding);
syncTrackedBinding(model.previousDrawBinding);
model._openGLRenderWindow.setActiveFramebuffer(
model.previousActiveFramebuffer
);
Expand All @@ -73,6 +86,10 @@ function vtkFramebuffer(publicAPI, model) {
mode = model.context.FRAMEBUFFER;
}
model.context.bindFramebuffer(mode, model.glFramebuffer);
if (mode !== model.context.READ_FRAMEBUFFER) {
// READ_FRAMEBUFFER binds do not change FRAMEBUFFER_BINDING.
syncTrackedBinding(model.glFramebuffer);
}
for (let i = 0; i < model.colorBuffers.length; i++) {
model.colorBuffers[i].bind();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { it, expect } from 'vitest';

import vtkFramebuffer from 'vtk.js/Sources/Rendering/OpenGL/Framebuffer';
import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow';
import 'vtk.js/Sources/Rendering/Misc/RenderingAPIs';

function createContextHarness() {
const renderWindow = vtkRenderWindow.newInstance();
const view = renderWindow.newAPISpecificView('WebGL');
renderWindow.addView(view);
view.initialize();
const gl = view.getContext();

// Count FRAMEBUFFER_BINDING readbacks while keeping getParameter working.
const rawGetParameter = gl.getParameter.bind(gl);
const counter = { bindingQueries: 0 };
gl.getParameter = (pname) => {
if (pname === gl.FRAMEBUFFER_BINDING) {
counter.bindingQueries += 1;
}
return rawGetParameter(pname);
};

const makeFramebuffer = () => {
const framebuffer = vtkFramebuffer.newInstance();
framebuffer.setOpenGLRenderWindow(view);
framebuffer.create(4, 4);
return framebuffer;
};

const currentBinding = () => rawGetParameter(gl.FRAMEBUFFER_BINDING);

// Deleting the own property restores the prototype method.
const removeSpy = () => delete gl.getParameter;

return { view, counter, makeFramebuffer, currentBinding, removeSpy };
}

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'save/restore reads the binding back from GL when tracking is not seeded',
() => {
const { view, counter, makeFramebuffer, currentBinding, removeSpy } =
createContextHarness();
const framebuffer = makeFramebuffer();

expect(view.getFramebufferBinding()).toBeUndefined();

framebuffer.saveCurrentBindingsAndBuffers();
expect(counter.bindingQueries).toBe(1);

framebuffer.bind();
// Binds do not seed tracking on their own.
expect(view.getFramebufferBinding()).toBeUndefined();

framebuffer.restorePreviousBindingsAndBuffers();
expect(currentBinding()).toBe(null);

framebuffer.saveCurrentBindingsAndBuffers();
expect(counter.bindingQueries).toBe(2);
removeSpy();
}
);

it.skipIf(__VTK_TEST_NO_WEBGL__)(
'seeded binding tracking avoids getParameter and follows bind/restore',
() => {
const { view, counter, makeFramebuffer, currentBinding, removeSpy } =
createContextHarness();
const outer = makeFramebuffer();
const inner = makeFramebuffer();

// The caller (e.g. a host embedding vtk.js) declares what is bound.
view.setFramebufferBinding(null);

outer.saveCurrentBindingsAndBuffers();
outer.bind();
expect(view.getFramebufferBinding()).toBe(outer.getGLFramebuffer());

// Nested save/restore (a mapper capturing state inside a render pass)
// must see the outer binding through the mirror.
inner.saveCurrentBindingsAndBuffers();
inner.bind();
expect(view.getFramebufferBinding()).toBe(inner.getGLFramebuffer());
inner.restorePreviousBindingsAndBuffers();
expect(currentBinding()).toBe(outer.getGLFramebuffer());
expect(view.getFramebufferBinding()).toBe(outer.getGLFramebuffer());

outer.restorePreviousBindingsAndBuffers();
expect(currentBinding()).toBe(null);
expect(view.getFramebufferBinding()).toBe(null);

expect(counter.bindingQueries).toBe(0);
removeSpy();
}
);
6 changes: 5 additions & 1 deletion Sources/Rendering/OpenGL/Helper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ function vtkOpenGLHelper(publicAPI, model) {
const mode = publicAPI.getOpenGLMode(rep);
const wideLines = publicAPI.haveWideLines(ren, actor);
const gl = model.context;
const depthMask = gl.getParameter(gl.DEPTH_WRITEMASK);
// Only the point-picking pass alters the depth mask, and reading it
// back with gl.getParameter can stall, so scope the save/restore to
// the pointPicking path instead of paying it on every draw call.
let depthMask;
if (model.pointPicking) {
depthMask = gl.getParameter(gl.DEPTH_WRITEMASK);
gl.depthMask(false);
}
const drawingLines = mode === gl.LINES;
Expand Down
16 changes: 16 additions & 0 deletions Sources/Rendering/OpenGL/RenderWindow/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,22 @@ export interface vtkOpenGLRenderWindow extends vtkViewNode {
options: WebGLContextAttributes
): Nullable<WebGLRenderingContext>;

/**
* Seed/update the JS-side mirror of the raw WebGLFramebuffer binding
* (null = default framebuffer) so framebuffer save/restore can avoid
* gl.getParameter(FRAMEBUFFER_BINDING) readbacks. Tracking is inert until
* first seeded; once seeded, callers binding framebuffers directly on the
* context must keep it in sync.
* @param binding
*/
setFramebufferBinding(binding: Nullable<WebGLFramebuffer>): void;

/**
* The tracked raw framebuffer binding, or undefined when tracking has not
* been seeded.
*/
getFramebufferBinding(): Nullable<WebGLFramebuffer> | undefined;

/**
*
* @param {CanvasRenderingContext2DSettings} options
Expand Down
11 changes: 11 additions & 0 deletions Sources/Rendering/OpenGL/RenderWindow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,17 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
model.activeFramebuffer = newActiveFramebuffer;
};

// JS-side mirror of the raw WebGLFramebuffer binding (null = default
// framebuffer), so FBO save/restore can avoid the CPU/GPU sync stall of a
// gl.getParameter(FRAMEBUFFER_BINDING) readback. Tracking is inert
// (undefined = unknown, readback fallback) until a caller seeds it; once
// seeded, vtkFramebuffer keeps it in sync, and callers binding framebuffers
// directly on the context must do the same.
publicAPI.setFramebufferBinding = (binding) => {
model.framebufferBinding = binding;
};
publicAPI.getFramebufferBinding = () => model.framebufferBinding;

const superSetSize = publicAPI.setSize;
publicAPI.setSize = (width, height) => {
const modified = superSetSize(width, height);
Expand Down
Loading