// 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; }