import { CoreRenderer, type BufferInfo, type CoreRendererOptions, type RendererCapabilities } from '../CoreRenderer.js'; import { SdfRenderOp } from './SdfRenderOp.js'; import type { CoreContextTexture } from '../CoreContextTexture.js'; import { type WebGlColor } from './internal/RendererUtils.js'; import { WebGlCtxTexture } from './WebGlCtxTexture.js'; import { Texture, type TextureCoords } from '../../textures/Texture.js'; import { BufferCollection } from './internal/BufferCollection.js'; import { WebGlShaderProgram } from './WebGlShaderProgram.js'; import { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; import { CoreNode } from '../../CoreNode.js'; import type { WebGlShaderType } from './WebGlShaderNode.js'; import { WebGlShaderNode } from './WebGlShaderNode.js'; export type WebGlRendererOptions = CoreRendererOptions; export type WebGlRenderOp = CoreNode | SdfRenderOp; export declare class WebGlRenderer extends CoreRenderer { glw: WebGlContextWrapper; quadBuffer: ArrayBuffer; fQuadBuffer: Float32Array; uiQuadBuffer: Uint32Array; /** * Separate buffer for RTT quad data. Required when DIRTY_QUAD_BUFFER is on: * main-scene nodes own permanent slots in `quadBuffer` and only rewrite when * dirty, so if RTT wrote into the same backing storage starting at index 0 * it would silently overwrite (and corrupt) main-scene slots whose owners * aren't dirty this frame. Allocated lazily on first RTT. */ rttQuadBuffer: ArrayBuffer | null; fRttQuadBuffer: Float32Array | null; uiRttQuadBuffer: Uint32Array | null; renderOps: WebGlRenderOp[]; /** * Deferred queue for SDF text render ops, used when RENDER_TEXT_BATCHING is * true. All text encountered during the frame is collected here and appended * to renderOps at the end (see flushTextRenderOps). This guarantees that all * text in a frame draws in a single contiguous run of draw calls, which is * the whole point of text batching. * * Side effect by design: text always draws on top of any non-text quads that * came after it in tree order (unless those quads carry an explicit zIndex, * which forces an early flush in addQuad). This is intentional — UI text * sitting above adjacent backgrounds/icons is the common case, and the * batching win is only worth taking if we don't break the run with mid-frame * flushes. If you need a non-text quad to land above earlier text, give it * a non-zero zIndex. */ coreTextRenderOps: WebGlRenderOp[]; curBufferIdx: number; curRenderOp: WebGlRenderOp | null; rttNodes: CoreNode[]; activeRttNode: CoreNode | null; /** * Shared vertex buffer for all SDF text glyphs. * Layout per vertex (6 floats = 24 bytes): * [0] x (float) - world pixel X * [1] y (float) - world pixel Y * [2] u (float) - atlas U * [3] v (float) - atlas V * [4] color (uint32) - ABGR packed, read as vec4 normalized * [5] distRange (float) - SDF distance range * * 4 vertices per glyph → 24 float units per glyph. * Triangles are formed via the shared element index buffer. */ sdfBuffer: ArrayBuffer; fSdfBuffer: Float32Array; uiSdfBuffer: Uint32Array; sdfBufferIdx: number; /** Running count of SDF quads written this frame (for element offset). */ sdfQuadCount: number; sdfQuadBufferCollection: BufferCollection; curSdfRenderOp: SdfRenderOp | null; /** * Whether the shared SDF buffer's bytes may differ from what the GPU * currently holds. Set by every write path that produces fresh bytes * (cache-miss recompute, translated copy), by anything that can shift * offsets or resize the buffer (render-list rebuild, RTT partial upload, * backing-store growth), and consumed by {@link uploadSdfBuffer}. The * exact cache-hit mem-copy path deliberately does NOT set it — it writes * byte-identical data at identical offsets. Conservative direction: when * in doubt, set it — a redundant upload is correct, a wrong skip is a * glitch. */ sdfBufferChanged: boolean; /** * Float32 length of the last main-pass SDF upload — the size half of the * skip test in {@link uploadSdfBuffer}. */ lastUploadedSdfSize: number; /** * When true, the entire quad buffer is re-uploaded to the GPU via bufferData * (DYNAMIC_DRAW) rather than the surgical per-node bufferSubData path. * Set to true on first frame and whenever the renderList changes structurally * (node added / removed / reordered). */ needsFullUpload: boolean; defaultTextureCoords: TextureCoords; defaultShaderNode: WebGlShaderNode | null; quadBufferCollection: BufferCollection; /** * Shared static element (index) buffer for quad rendering. * * @remarks * Bound once globally, but also recorded into each shader program's Vertex * Array Object since the element-array binding is part of VAO state. */ indexBuffer: WebGLBuffer | null; clearColor: WebGlColor; /** * White pixel texture used by default when no texture is specified. */ quadBufferUsage: number; numQuadsRendered: number; /** * Number of float32 elements last uploaded to the GPU via bufferData. * Used to detect when curBufferIdx has grown beyond the GPU buffer's * capacity, requiring a full re-upload even when needsFullUpload is false. */ lastUploadedBufferSize: number; /** * Count of main-scene nodes whose quad data changed this frame and which * own a buffer slot. Accumulated for free during the addQuad pass (which * already branches on isQuadDirty) and consumed in render() to choose * between surgical bufferSubData uploads and a single full bufferData, * avoiding a separate counting loop. Reset each frame in reset(). */ dirtyQuadCount: number; /** * Whether the renderer is currently rendering to a texture. */ renderToTextureActive: boolean; constructor(options: WebGlRendererOptions); /** * Listen for WebGL context loss on the canvas. * * @remarks * On low-RAM devices (e.g. Chromium 123+ after backgrounding) the GPU * context is dropped, after which `gl.createTexture()` and friends return * null and the engine would crash. We pause the render loop via the Stage * flag and surface a `contextLost` event so consumers can react. * * We intentionally do NOT call `event.preventDefault()` (which would ask the * browser to restore the context) and do NOT listen for * `webglcontextrestored`: the engine cannot rebuild its GPU resources * in-place, so the supported recovery is to reload the app. See BROWSERS.md. */ private attachContextLossListeners; reset(): void; createShaderProgram(shaderType: WebGlShaderType, props: Record): WebGlShaderProgram; createShaderNode(shaderKey: string, shaderType: WebGlShaderType, props?: Record, program?: WebGlShaderProgram): WebGlShaderNode>; supportsShaderType(shaderType: Readonly): boolean; createCtxTexture(textureSource: Texture): CoreContextTexture; /** * This function adds a quad (a rectangle composed of two triangles) to the WebGL rendering pipeline. * * It takes a set of options that define the quad's properties, such as its dimensions, colors, texture, shader, and transformation matrix. * The function first updates the shader properties with the current dimensions if necessary, then sets the default texture if none is provided. * It then checks if a new render operation is needed, based on the current shader and clipping rectangle. * If a new render operation is needed, it creates one and updates the current render operation. * The function then adjusts the texture coordinates based on the texture options and adds the texture to the texture manager. * * Finally, it calculates the vertices for the quad, taking into account any transformations, and adds them to the quad buffer. * The function updates the length and number of quads in the current render operation, and updates the current buffer index. */ addQuad(node: CoreNode): void; /** * Replace the existing RenderOp with a new one that uses the specified Shader * and starts at the specified buffer index. * * @param shader * @param bufferIdx */ private newRenderOp; /** * Test if the current Render operation can be reused for the specified parameters. * @param params * @returns */ reuseRenderOp(node: CoreNode): boolean; /** * add RenderOp to the render pipeline */ addRenderOp(renderable: WebGlRenderOp): void; flushTextRenderOps(): void; /** * Append pre-transformed SDF glyph vertices to the shared SDF buffer * and manage SDF render op batching. * * @remarks * This method pre-transforms glyph positions from design units to world * pixel space on the CPU, packs per-vertex color and distanceRange, and * writes them into the shared SDF buffer. Compatible consecutive calls * (same atlas, same clipping, same RTT state) are merged into a single * SdfRenderOp, resulting in one draw call for many text nodes. */ addSdfQuads(glyphs: Float32Array, glyphCount: number, fontScale: number, transform: Float32Array, color: number, worldAlpha: number, distanceRange: number, atlasTexture: WebGlCtxTexture, clippingRect: import('../../lib/utils.js').RectWithValid, width: number, height: number, parentHasRenderTexture: boolean, framebufferDimensions: import('../../../common/CommonTypes.js').Dimensions | null, sdfShader: WebGlShaderNode): void; /** * Fast path: copy pre-computed cached SDF vertex data into the shared * buffer and create/extend an SdfRenderOp. * * @remarks * When a text node hasn't changed (same layout, transform, color, alpha), * the per-glyph matrix multiplication is skipped entirely. The cached * Float32Array is written via a single `Float32Array.set()` (memcpy), * which is orders of magnitude faster than the per-glyph computation path. */ addSdfCachedQuads(cachedVertices: Float32Array, numGlyphs: number, atlasTexture: WebGlCtxTexture, clippingRect: import('../../lib/utils.js').RectWithValid, worldAlpha: number, width: number, height: number, parentHasRenderTexture: boolean, framebufferDimensions: import('../../../common/CommonTypes.js').Dimensions | null, sdfShader: WebGlShaderNode): void; /** * Append cached SDF vertices translated by (dx, dy) to the shared buffer. * * @remarks * The scroll fast path: a text node whose transform changed by pure * translation reuses its world-space vertex cache — one mem-copy plus two * adds per vertex instead of full per-glyph matrix math, and the cache * keeps its original base so nothing is re-snapshotted per frame. * * The copy MUST stay a typed-array `set` (bit-exact memcpy): packed ABGR * colors live in the same Float32Array and some bit patterns are float32 * NaNs, which element-wise float reads/writes may canonicalize and corrupt. * Only the two position floats of each vertex are touched after the copy. */ addSdfTranslatedQuads(cachedVertices: Float32Array, numGlyphs: number, dx: number, dy: number, atlasTexture: WebGlCtxTexture, clippingRect: import('../../lib/utils.js').RectWithValid, worldAlpha: number, width: number, height: number, parentHasRenderTexture: boolean, framebufferDimensions: import('../../../common/CommonTypes.js').Dimensions | null, sdfShader: WebGlShaderNode): void; /** * Shared batching logic for SDF render ops. * Called by both `addSdfQuads` (full compute) and `addSdfCachedQuads` (fast copy). */ private finalizeSdfBatch; /** * Resizes the shared SDF ArrayBuffer if the required size (in floats) goes beyond * the current buffer capacity. */ private ensureSdfBufferCapacity; /** * Render the current set of RenderOps to render to the specified surface. * * On the first frame after a renderList structural change (`needsFullUpload` * is true) the entire quad buffer is re-allocated on the GPU with * `bufferData(DYNAMIC_DRAW)`. On every subsequent frame only the slots of * nodes flagged `isQuadDirty` are surgically updated via `bufferSubData`, * leaving the rest of the GPU's buffer unchanged. * * TODO: 'screen' is the only supported surface at the moment. * * @param surface */ render(surface?: 'screen' | CoreContextTexture): void; /** * Upload the shared SDF buffer for the main pass, skipping the driver-side * `bufferData` copy when the bytes provably match what the GPU already * holds: every write this frame was an exact cache-hit mem-copy * (`sdfBufferChanged` false) and the total size matches the previous * upload. Exact hits write byte-identical data, and identical offsets are * guaranteed because every source of reorder or resize — cache-miss * recompute, translated copy, render-list rebuild, RTT partial upload, * backing-store growth — sets `sdfBufferChanged`. */ private uploadSdfBuffer; getQuadCount(): number; getRenderOpCount(): number; renderToTexture(node: CoreNode): void; /** * Inserts an RTT node into `this.rttNodes` while maintaining the correct rendering order based on hierarchy. * * Rendering order for RTT nodes is critical when nested RTT nodes exist in a parent-child relationship. * Specifically: * - Child RTT nodes must be rendered before their RTT-enabled parents to ensure proper texture composition. * - If an RTT node is added and it has existing RTT children, it should be rendered after those children. * * This function addresses both cases by: * 1. **Checking Upwards**: It traverses the node's hierarchy upwards to identify any RTT parent * already in `rttNodes`. If an RTT parent is found, the new node is placed before this parent. * 2. **Checking Downwards**: It traverses the node’s children recursively to find any RTT-enabled * children that are already in `rttNodes`. If such children are found, the new node is inserted * after the last (highest index) RTT child node. * * The final calculated insertion index ensures the new node is positioned in `rttNodes` to respect * both parent-before-child and child-before-parent rendering rules, preserving the correct order * for the WebGL renderer. * * @param node - The RTT-enabled CoreNode to be added to `rttNodes` in the appropriate hierarchical position. */ private insertRTTNodeInOrder; private findMaxChildRTTIndex; renderRTTNodes(): void; /** * Recursively walk the subtree of an RTT node and add quads for all * renderable descendants. This restores the recursive behavior that was * lost when `stage.addQuads(child)` was replaced with `child.renderQuads(this)`. */ private addRTTQuads; /** * Render pass for RTT: always does a full buffer upload since RTT quads * use temporary sequential buffer slots that are rebuilt each frame. */ private renderRTT; updateViewport(): void; removeRTTNode(node: CoreNode): void; getBufferInfo(): BufferInfo | null; getCapabilities(): RendererCapabilities; /** * Drain the GL error queue once and report whether a GL_OUT_OF_MEMORY was * seen since the last call. * * @remarks * `gl.getError()` forces a CPU↔GPU sync, so this is deliberately invoked at * most once per frame by the Stage rather than after each texture upload. * `getError()` returns one error at a time, so we drain a bounded number of * queued errors to ensure a non-OOM error ahead of the OOM doesn't mask it * for this frame. Non-OOM errors are ignored here (the renderer otherwise * only inspects them in development builds). */ checkForOutOfMemory(): boolean; getDefaultShaderNode(): WebGlShaderNode; getTextureCoords(node: CoreNode): TextureCoords | undefined; /** * Resets all per-node quad buffer slot assignments and schedules a full GPU * buffer re-upload on the next render call. * * Called by Stage.requestRenderListUpdate() whenever the render list changes * structurally (node added, removed, or reordered). After this call, the * next addQuad() pass will reassign compact, contiguous slots starting from 0. */ invalidateQuadBuffer(): void; /** * Sets the glClearColor to the specified color. * * @param color - The color to set as the clear color, represented as a 32-bit integer. */ updateClearColor(color: number): void; }