@luma.gl/engine
Version:
3D Engine Components for luma.gl
155 lines (135 loc) • 4.73 kB
text/typescript
// luma.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import type {Device, Framebuffer, RenderPass, Texture} from '@luma.gl/core';
const DEBUG_FRAMEBUFFER_STATE_KEY = '__debugFramebufferState';
const DEFAULT_MARGIN_PX = 8;
type DebugFramebufferOptions = {
id: string;
minimap?: boolean;
opaque?: boolean;
top?: string;
left?: string;
rgbaScale?: number;
};
type DebugFramebufferState = {
flushing: boolean;
queuedFramebuffers: Framebuffer[];
};
/**
* Debug utility to blit queued offscreen framebuffers into the default framebuffer
* without CPU readback. Currently implemented for WebGL only.
*/
export function debugFramebuffer(
renderPass: RenderPass,
source: Framebuffer | Texture | null,
options: DebugFramebufferOptions
): void {
if (renderPass.device.type !== 'webgl') {
return;
}
const state = getDebugFramebufferState(renderPass.device);
if (state.flushing) {
return;
}
if (isDefaultRenderPass(renderPass)) {
flushDebugFramebuffers(renderPass, options, state);
return;
}
if (source && isFramebuffer(source) && source.handle !== null) {
if (!state.queuedFramebuffers.includes(source)) {
state.queuedFramebuffers.push(source);
}
}
}
function flushDebugFramebuffers(
renderPass: RenderPass,
options: DebugFramebufferOptions,
state: DebugFramebufferState
): void {
if (state.queuedFramebuffers.length === 0) {
return;
}
const webglDevice = renderPass.device as Device & {gl: WebGL2RenderingContext};
const {gl} = webglDevice;
const previousReadFramebuffer = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
const previousDrawFramebuffer = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
const [targetWidth, targetHeight] = renderPass.device
.getDefaultCanvasContext()
.getDrawingBufferSize();
let topPx = parseCssPixel(options.top, DEFAULT_MARGIN_PX);
const leftPx = parseCssPixel(options.left, DEFAULT_MARGIN_PX);
state.flushing = true;
try {
for (const framebuffer of state.queuedFramebuffers) {
const [targetX0, targetY0, targetX1, targetY1, previewHeight] = getOverlayRect({
framebuffer,
targetWidth,
targetHeight,
topPx,
leftPx,
minimap: options.minimap
});
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer.handle as WebGLFramebuffer | null);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
gl.blitFramebuffer(
0,
0,
framebuffer.width,
framebuffer.height,
targetX0,
targetY0,
targetX1,
targetY1,
gl.COLOR_BUFFER_BIT,
gl.NEAREST
);
topPx += previewHeight + DEFAULT_MARGIN_PX;
}
} finally {
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousReadFramebuffer);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDrawFramebuffer);
state.flushing = false;
}
}
function getOverlayRect(options: {
framebuffer: Framebuffer;
targetWidth: number;
targetHeight: number;
topPx: number;
leftPx: number;
minimap?: boolean;
}): [number, number, number, number, number] {
const {framebuffer, targetWidth, targetHeight, topPx, leftPx, minimap} = options;
const maxWidth = minimap ? Math.max(Math.floor(targetWidth / 4), 1) : targetWidth;
const maxHeight = minimap ? Math.max(Math.floor(targetHeight / 4), 1) : targetHeight;
const scale = Math.min(maxWidth / framebuffer.width, maxHeight / framebuffer.height);
const previewWidth = Math.max(Math.floor(framebuffer.width * scale), 1);
const previewHeight = Math.max(Math.floor(framebuffer.height * scale), 1);
const targetX0 = leftPx;
const targetY0 = Math.max(targetHeight - topPx - previewHeight, 0);
const targetX1 = targetX0 + previewWidth;
const targetY1 = targetY0 + previewHeight;
return [targetX0, targetY0, targetX1, targetY1, previewHeight];
}
function getDebugFramebufferState(device: Device): DebugFramebufferState {
device.userData[DEBUG_FRAMEBUFFER_STATE_KEY] ||= {
flushing: false,
queuedFramebuffers: []
} satisfies DebugFramebufferState;
return device.userData[DEBUG_FRAMEBUFFER_STATE_KEY] as DebugFramebufferState;
}
function isFramebuffer(value: Framebuffer | Texture): value is Framebuffer {
return 'colorAttachments' in value;
}
function isDefaultRenderPass(renderPass: RenderPass): boolean {
const framebuffer = renderPass.props.framebuffer as {handle?: unknown} | null;
return !framebuffer || framebuffer.handle === null;
}
function parseCssPixel(value: string | undefined, defaultValue: number): number {
if (!value) {
return defaultValue;
}
const parsedValue = Number.parseInt(value, 10);
return Number.isFinite(parsedValue) ? parsedValue : defaultValue;
}