import { input } from "gpu.js";
const CreateTexture = {
name: "create-texture-unorm",
inject: {
"fs:#decl": "uniform sampler2D textureName;",
"fs:DECKGL_FILTER_COLOR": "color = texture(textureName, geometry.uv);",
},
getUniforms: (props = {}) => ({
textureName: props.textureName,
}),
};
/**
* Get WebGL context from luma device
* @private
* @param {Object} device - luma device object
* @returns {WebGL2RenderingContext|WebGLRenderingContext} WebGL context
* @throws {Error} If unable to resolve WebGL context
*/
const getDeviceGl = (device) => {
if (device && device.gl && typeof device.gl.bindTexture === "function") {
return device.gl;
}
if (device && device.handle && typeof device.handle.bindTexture === "function") {
return device.handle;
}
throw new Error("[renderTile] Unable to resolve WebGL context from luma device");
};
const BLIT_RESOURCES = new WeakMap();
const SOURCE_FORMAT_CACHE = new WeakMap();
const LUT_TEXTURE_CACHE = new WeakMap();
/**
* Destroy luma texture reference from tile data result
* @param {Object} tileDataResult - Tile data with _lumaTexture property
* @returns {void}
* @public
*/
const destroyLumaTextureRef = (tileDataResult) => {
if (!tileDataResult) {
return;
}
const texture = tileDataResult._lumaTexture;
if (texture && typeof texture.destroy === "function" && !texture.destroyed) {
texture.destroy();
}
tileDataResult._lumaTexture = null;
tileDataResult._lumaTextureFormat = null;
};
/**
* Attach texture disposal hooks to tile data result
* Ensures luma textures are properly cleaned up when tiles are disposed
* @private
* @param {Object} tileDataResult - Tile data to attach disposer to
* @returns {void}
*/
const attachTileTextureDisposer = (tileDataResult) => {
if (!tileDataResult || tileDataResult._lumaTextureDisposerAttached) {
return;
}
const previousDispose = typeof tileDataResult.dispose === "function"
? tileDataResult.dispose.bind(tileDataResult)
: null;
const previousDestroy = typeof tileDataResult.destroy === "function"
? tileDataResult.destroy.bind(tileDataResult)
: null;
tileDataResult.dispose = () => {
destroyLumaTextureRef(tileDataResult);
if (previousDispose) {
previousDispose();
}
};
tileDataResult.destroy = () => {
destroyLumaTextureRef(tileDataResult);
if (previousDestroy) {
previousDestroy();
}
};
tileDataResult._lumaTextureDisposerAttached = true;
};
/**
* Compile WebGL shader
* @private
* @param {WebGL2RenderingContext} gl - WebGL context
* @param {number} type - Shader type (gl.VERTEX_SHADER or gl.FRAGMENT_SHADER)
* @param {string} source - GLSL shader source code
* @returns {WebGLShader} Compiled shader
* @throws {Error} If shader compilation fails
*/
const compileShader = (gl, type, source) => {
const shader = gl.createShader(type);
if (!shader) {
throw new Error("[renderTile] Failed to create shader for texture blit");
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(shader) || "unknown shader compile error";
gl.deleteShader(shader);
throw new Error(`[renderTile] Texture blit shader compile failed: ${log}`);
}
return shader;
};
/**
* Get or create shader resources for texture blitting with optional LUT mapping
* Resources are cached per WebGL context
* @private
* @param {WebGL2RenderingContext} gl - WebGL context
* @returns {Object} Blit resources (program, vao, framebuffer, uniforms)
*/
const getBlitResources = (gl) => {
let resources = BLIT_RESOURCES.get(gl);
if (resources) {
return resources;
}
const vertexSource = `#version 300 es
out vec2 vUv;
void main() {
vec2 pos;
vec2 uv;
if (gl_VertexID == 0) {
pos = vec2(-1.0, -1.0);
uv = vec2(0.0, 0.0);
} else if (gl_VertexID == 1) {
pos = vec2(1.0, -1.0);
uv = vec2(1.0, 0.0);
} else if (gl_VertexID == 2) {
pos = vec2(-1.0, 1.0);
uv = vec2(0.0, 1.0);
} else {
pos = vec2(1.0, 1.0);
uv = vec2(1.0, 1.0);
}
vUv = uv;
gl_Position = vec4(pos, 0.0, 1.0);
}`;
const fragmentSource = `#version 300 es
precision highp float;
uniform sampler2D uSource;
uniform sampler2D uColorLUT;
uniform int uUseLut;
uniform float uDomainMin;
uniform float uDomainMax;
in vec2 vUv;
out vec4 outColor;
void main() {
// Use exact texel fetch to avoid edge sampling artifacts on some Chrome/WebGL paths.
ivec2 texSize = textureSize(uSource, 0);
ivec2 texelCoord = ivec2(gl_FragCoord.xy);
texelCoord = clamp(texelCoord, ivec2(0), texSize - ivec2(1));
vec4 src = texelFetch(uSource, texelCoord, 0);
if (uUseLut == 1) {
float domainRange = max(0.0000001, uDomainMax - uDomainMin);
float normalized = clamp((src.r - uDomainMin) / domainRange, 0.0, 1.0);
int idx = int(floor(normalized * 255.0));
vec4 lutColor = texelFetch(uColorLUT, ivec2(idx, 0), 0);
outColor = vec4(lutColor.rgb, lutColor.a * src.a);
} else {
outColor = src;
}
}`;
const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSource);
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
const program = gl.createProgram();
if (!program) {
gl.deleteShader(vs);
gl.deleteShader(fs);
throw new Error("[renderTile] Failed to create shader program for texture blit");
}
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
gl.deleteShader(vs);
gl.deleteShader(fs);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const log = gl.getProgramInfoLog(program) || "unknown program link error";
gl.deleteProgram(program);
throw new Error(`[renderTile] Texture blit program link failed: ${log}`);
}
const vao = gl.createVertexArray();
if (!vao) {
gl.deleteProgram(program);
throw new Error("[renderTile] Failed to create VAO for texture blit");
}
const framebuffer = gl.createFramebuffer();
if (!framebuffer) {
gl.deleteVertexArray(vao);
gl.deleteProgram(program);
throw new Error("[renderTile] Failed to create framebuffer for texture blit");
}
const sourceUniform = gl.getUniformLocation(program, "uSource");
const colorLUTUniform = gl.getUniformLocation(program, "uColorLUT");
const useLutUniform = gl.getUniformLocation(program, "uUseLut");
const domainMinUniform = gl.getUniformLocation(program, "uDomainMin");
const domainMaxUniform = gl.getUniformLocation(program, "uDomainMax");
resources = {
program,
vao,
framebuffer,
sourceUniform,
colorLUTUniform,
useLutUniform,
domainMinUniform,
domainMaxUniform,
};
BLIT_RESOURCES.set(gl, resources);
return resources;
};
/**
* Release all cached blit resources for a WebGL context
* Deletes framebuffers, VAOs, programs, and LUT textures
* @param {WebGL2RenderingContext} gl - WebGL context
* @returns {void}
* @public
*/
const releaseBlitResources = (gl) => {
if (!gl) {
return;
}
const resources = BLIT_RESOURCES.get(gl);
if (resources) {
if (resources.framebuffer) {
gl.deleteFramebuffer(resources.framebuffer);
}
if (resources.vao) {
gl.deleteVertexArray(resources.vao);
}
if (resources.program) {
gl.deleteProgram(resources.program);
}
BLIT_RESOURCES.delete(gl);
}
SOURCE_FORMAT_CACHE.delete(gl);
const lutCache = LUT_TEXTURE_CACHE.get(gl);
if (lutCache) {
for (const texture of lutCache.values()) {
gl.deleteTexture(texture);
}
LUT_TEXTURE_CACHE.delete(gl);
}
};
/**
* Get or create a cached LUT texture for the given colorLUT array
* Creates a 256x1 RGBA texture from the color lookup table
* Cached per WebGL context and colorLUT array reference
* @private
* @param {WebGL2RenderingContext} gl - WebGL context
* @param {Array<Array<number>>} colorLUT - 256x4 color LUT (normalized RGBA)
* @returns {WebGLTexture} LUT texture
* @throws {Error} If texture creation fails
*/
const ensureLutTexture = (gl, colorLUT) => {
let lutCache = LUT_TEXTURE_CACHE.get(gl);
if (!lutCache) {
lutCache = new Map();
LUT_TEXTURE_CACHE.set(gl, lutCache);
}
const cached = lutCache.get(colorLUT);
if (cached) {
return cached;
}
const lutData = new Uint8Array(256 * 4);
for (let i = 0; i < 256; i++) {
const c = colorLUT[i] || [0, 0, 0, 0];
lutData[i * 4 + 0] = Math.max(0, Math.min(255, Math.round((Number(c[0]) || 0) * 255)));
lutData[i * 4 + 1] = Math.max(0, Math.min(255, Math.round((Number(c[1]) || 0) * 255)));
lutData[i * 4 + 2] = Math.max(0, Math.min(255, Math.round((Number(c[2]) || 0) * 255)));
lutData[i * 4 + 3] = Math.max(0, Math.min(255, Math.round((Number(c[3]) || 0) * 255)));
}
const texture = gl.createTexture();
if (!texture) {
throw new Error("[renderTile] Failed to create LUT texture");
}
const previousActiveTexture = gl.getParameter(gl.ACTIVE_TEXTURE);
const previousTexture2D = gl.getParameter(gl.TEXTURE_BINDING_2D);
const previousUnpackAlignment = gl.getParameter(gl.UNPACK_ALIGNMENT);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
256,
1,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
lutData
);
gl.activeTexture(previousActiveTexture);
gl.bindTexture(gl.TEXTURE_2D, previousTexture2D);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, previousUnpackAlignment);
lutCache.set(colorLUT, texture);
return texture;
};
const detectSourceTextureFormat = (gl, sourceTexture) => {
const previousFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
const probeFramebuffer = gl.createFramebuffer();
if (!probeFramebuffer) {
return "rgba8unorm";
}
let detectedFormat = "rgba8unorm";
try {
gl.bindFramebuffer(gl.FRAMEBUFFER, probeFramebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, sourceTexture, 0);
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (status !== gl.FRAMEBUFFER_COMPLETE) {
return "rgba8unorm";
}
const componentType = gl.getFramebufferAttachmentParameter(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE
);
if (componentType === gl.FLOAT) {
detectedFormat = "rgba16float";
} else {
detectedFormat = "rgba8unorm";
}
} catch (_error) {
detectedFormat = "rgba8unorm";
} finally {
gl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer);
gl.deleteFramebuffer(probeFramebuffer);
}
return detectedFormat;
};
const captureGLState = (gl) => ({
viewport: gl.getParameter(gl.VIEWPORT),
scissorBox: gl.getParameter(gl.SCISSOR_BOX),
depthTest: gl.isEnabled(gl.DEPTH_TEST),
stencilTest: gl.isEnabled(gl.STENCIL_TEST),
scissorTest: gl.isEnabled(gl.SCISSOR_TEST),
cullFace: gl.isEnabled(gl.CULL_FACE),
blend: gl.isEnabled(gl.BLEND),
colorMask: gl.getParameter(gl.COLOR_WRITEMASK),
});
const restoreGLState = (gl, state) => {
if (state.depthTest) gl.enable(gl.DEPTH_TEST); else gl.disable(gl.DEPTH_TEST);
if (state.stencilTest) gl.enable(gl.STENCIL_TEST); else gl.disable(gl.STENCIL_TEST);
if (state.scissorTest) gl.enable(gl.SCISSOR_TEST); else gl.disable(gl.SCISSOR_TEST);
if (state.cullFace) gl.enable(gl.CULL_FACE); else gl.disable(gl.CULL_FACE);
if (state.blend) gl.enable(gl.BLEND); else gl.disable(gl.BLEND);
};
const ensureLumaTexture = (tileDataResult, width, height, format) => {
const device = tileDataResult && tileDataResult.device;
if (!device || typeof device.createTexture !== "function") {
throw new Error("[renderTile] Missing luma device; cannot build RasterModule texture");
}
attachTileTextureDisposer(tileDataResult);
let texture = tileDataResult._lumaTexture || null;
const needsNewTexture =
!texture ||
texture.destroyed ||
texture.width !== width ||
texture.height !== height ||
tileDataResult._lumaTextureFormat !== format;
if (needsNewTexture) {
if (texture && typeof texture.destroy === "function" && !texture.destroyed) {
texture.destroy();
}
texture = device.createTexture({
format,
width,
height,
mipmaps: false,
sampler: {
minFilter: "nearest",
magFilter: "nearest",
addressModeU: "clamp-to-edge",
addressModeV: "clamp-to-edge",
},
});
tileDataResult._lumaTexture = texture;
tileDataResult._lumaTextureFormat = format;
}
return texture;
};
const copyGpuTextureToLumaTexture = (
device,
sourceTexture,
targetTexture,
width,
height,
debugValidation = false,
colorMapping = null
) => {
const gl = getDeviceGl(device);
const blit = getBlitResources(gl);
const previousFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
const previousReadFramebuffer = gl.getParameter(gl.READ_FRAMEBUFFER_BINDING);
const previousDrawFramebuffer = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
const previousProgram = gl.getParameter(gl.CURRENT_PROGRAM);
const previousVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
const previousActiveTexture = gl.getParameter(gl.ACTIVE_TEXTURE);
const previousTexture2D = gl.getParameter(gl.TEXTURE_BINDING_2D);
const previousTextureBinding1 = (() => {
gl.activeTexture(gl.TEXTURE1);
const binding = gl.getParameter(gl.TEXTURE_BINDING_2D);
gl.activeTexture(previousActiveTexture);
return binding;
})();
const previousViewport = gl.getParameter(gl.VIEWPORT);
const previousScissorBox = gl.getParameter(gl.SCISSOR_BOX);
const previousDepthTest = gl.isEnabled(gl.DEPTH_TEST);
const previousStencilTest = gl.isEnabled(gl.STENCIL_TEST);
const previousScissorTest = gl.isEnabled(gl.SCISSOR_TEST);
const previousCullFace = gl.isEnabled(gl.CULL_FACE);
const previousBlend = gl.isEnabled(gl.BLEND);
const previousColorMask = gl.getParameter(gl.COLOR_WRITEMASK);
try {
// Isolate blit draw from deck/luma state to avoid silent clipping/discard.
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.STENCIL_TEST);
gl.disable(gl.SCISSOR_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
gl.colorMask(true, true, true, true);
// Shader path: explicit texelFetch copy avoids browser/driver-specific blit anomalies.
gl.bindFramebuffer(gl.FRAMEBUFFER, blit.framebuffer);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, targetTexture.handle, 0);
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (status !== gl.FRAMEBUFFER_COMPLETE) {
throw new Error(`[renderTile] Incomplete framebuffer during texture blit (status: ${status})`);
}
gl.viewport(0, 0, width, height);
gl.useProgram(blit.program);
gl.bindVertexArray(blit.vao);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, sourceTexture);
// Guarantee source texture completeness for sampling in the blit shader.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.uniform1i(blit.sourceUniform, 0);
const useLut = Boolean(colorMapping && colorMapping.colorLUT && Array.isArray(colorMapping.colorLUT));
gl.uniform1i(blit.useLutUniform, useLut ? 1 : 0);
if (useLut) {
const lutTexture = ensureLutTexture(gl, colorMapping.colorLUT);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, lutTexture);
gl.uniform1i(blit.colorLUTUniform, 1);
const domainMin = Number(colorMapping.domainMin);
const domainMax = Number(colorMapping.domainMax);
gl.uniform1f(blit.domainMinUniform, Number.isFinite(domainMin) ? domainMin : -1.0);
gl.uniform1f(blit.domainMaxUniform, Number.isFinite(domainMax) ? domainMax : 1.0);
gl.activeTexture(gl.TEXTURE0);
}
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
if (debugValidation) {
const blitError = gl.getError();
if (blitError !== gl.NO_ERROR) {
throw new Error(`[renderTile] Texture blit draw failed with GL error code ${blitError}`);
}
}
} finally {
gl.bindFramebuffer(gl.FRAMEBUFFER, previousFramebuffer);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, previousReadFramebuffer);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDrawFramebuffer);
gl.useProgram(previousProgram);
gl.bindVertexArray(previousVao);
gl.activeTexture(previousActiveTexture);
gl.bindTexture(gl.TEXTURE_2D, previousTexture2D);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, previousTextureBinding1);
gl.activeTexture(previousActiveTexture);
gl.viewport(previousViewport[0], previousViewport[1], previousViewport[2], previousViewport[3]);
gl.scissor(previousScissorBox[0], previousScissorBox[1], previousScissorBox[2], previousScissorBox[3]);
restoreGLState(gl, {
depthTest: previousDepthTest,
stencilTest: previousStencilTest,
scissorTest: previousScissorTest,
cullFace: previousCullFace,
blend: previousBlend
});
gl.colorMask(previousColorMask[0], previousColorMask[1], previousColorMask[2], previousColorMask[3]);
}
};
const toRasterModules = (tileDataResult, gpuTexture, width, height, debugValidation = false, colorMapping = null) => {
const device = tileDataResult && tileDataResult.device;
const gl = getDeviceGl(device);
const handle = gpuTexture && (gpuTexture.texture || gpuTexture);
if (!handle) {
throw new Error("[renderTile] GPU.js pipeline did not produce a texture handle");
}
const sourceFormat = SOURCE_FORMAT_CACHE.get(gl) || detectSourceTextureFormat(gl, handle);
const formatsToTry = sourceFormat === "rgba16float"
? ["rgba16float", "rgba8unorm"]
: ["rgba8unorm", "rgba16float"];
let lumaTexture = null;
let lastError = null;
for (let i = 0; i < formatsToTry.length; i++) {
const format = formatsToTry[i];
try {
lumaTexture = ensureLumaTexture(tileDataResult, width, height, format);
copyGpuTextureToLumaTexture(device, handle, lumaTexture, width, height, debugValidation, colorMapping);
SOURCE_FORMAT_CACHE.set(gl, format);
lastError = null;
break;
} catch (error) {
lastError = error;
}
}
if (!lumaTexture || lastError) {
throw new Error(`[renderTile] Failed to copy GPU texture into luma texture: ${lastError ? lastError.message : "unknown error"}`);
}
return [
{
module: CreateTexture,
props: { textureName: lumaTexture },
},
];
};
const extractGpuTextureHandle = (kernelOutput, kernel) => {
if (kernelOutput && kernelOutput.texture) {
return kernelOutput.texture;
}
if (kernelOutput && typeof kernelOutput === "object") {
if (kernelOutput.texture && kernelOutput.texture.texture) {
return kernelOutput.texture.texture;
}
if (kernelOutput.webGlTexture) {
return kernelOutput.webGlTexture;
}
}
// Graphical kernels may not return the texture object directly.
if (kernel && kernel.texture) {
return kernel.texture;
}
if (kernel && kernel.kernel && kernel.kernel.texture) {
return kernel.kernel.texture;
}
return null;
};
const runKernelWithIsolatedState = (device, kernel, kernelArgs, width, height) => {
const gl = getDeviceGl(device);
const previousState = captureGLState(gl);
try {
// Prevent deck/luma state from clipping GPU.js offscreen draws.
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.STENCIL_TEST);
gl.disable(gl.SCISSOR_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
gl.colorMask(true, true, true, true);
gl.viewport(0, 0, width, height);
// Support both single argument and multiple arguments for pipelined kernels
return Array.isArray(kernelArgs) ? kernel(...kernelArgs) : kernel(kernelArgs);
} finally {
gl.viewport(previousState.viewport[0], previousState.viewport[1], previousState.viewport[2], previousState.viewport[3]);
gl.scissor(previousState.scissorBox[0], previousState.scissorBox[1], previousState.scissorBox[2], previousState.scissorBox[3]);
restoreGLState(gl, previousState);
gl.colorMask(previousState.colorMask[0], previousState.colorMask[1], previousState.colorMask[2], previousState.colorMask[3]);
}
};
/**
* Generate a 256-entry color lookup table from gradient colors and stops
* Performs linear interpolation between stops on CPU to avoid GPU.js limitations
* @param {Array<Array<number>>} colors - RGBA colors (normalized 0-1)
* @param {Array<number>} stops - Scalar stop values
* @param {number[]|null} domain - Optional scalar domain [min, max], defaults to [-1, 1]
* @returns {Array<Array<number>>} 256x4 color LUT (normalized RGBA)
* @private
*/
const generateColorLUT = (colors, stops, domain = null) => {
const lut = new Array(256);
const numStops = stops.length;
let domainMin = -1.0;
let domainMax = 1.0;
if (Array.isArray(domain) && domain.length === 2) {
const parsedMin = Number(domain[0]);
const parsedMax = Number(domain[1]);
if (Number.isFinite(parsedMin) && Number.isFinite(parsedMax) && parsedMax > parsedMin) {
domainMin = parsedMin;
domainMax = parsedMax;
}
}
const domainRange = domainMax - domainMin;
let segmentIdx = 0;
for (let i = 0; i < 256; i++) {
const t = i / 255.0;
const scalarVal = domainMin + t * domainRange;
if (scalarVal <= stops[0]) {
const c = colors[0];
lut[i] = [c[0], c[1], c[2], c[3]];
continue;
}
if (scalarVal >= stops[numStops - 1]) {
const c = colors[numStops - 1];
lut[i] = [c[0], c[1], c[2], c[3]];
continue;
}
while (segmentIdx < numStops - 2 && scalarVal >= stops[segmentIdx + 1]) {
segmentIdx++;
}
const t0 = stops[segmentIdx];
const t1 = stops[segmentIdx + 1];
const localT = (scalarVal - t0) / (t1 - t0);
const c0 = colors[segmentIdx];
const c1 = colors[segmentIdx + 1];
lut[i] = [
c0[0] * (1 - localT) + c1[0] * localT,
c0[1] * (1 - localT) + c1[1] * localT,
c0[2] * (1 - localT) + c1[2] * localT,
c0[3] * (1 - localT) + c1[3] * localT
];
}
return lut;
};
/**
* Render a tile using the active style from the multiband instance
* Applies GPU-accelerated style kernel to raster data and returns RasterModule array
*
* For Type 1 styles: Applies kernel directly to produce RGBA output
* For Type 2 styles: Applies kernel to produce scalar values, then maps through color LUT via GPU shader
*
* @param {Object} tileDataResult - The tile data with texture information
* @param {Float32Array} tileDataResult.data - Band-major float32 raster data
* @param {number} tileDataResult.width - Tile width in pixels
* @param {number} tileDataResult.height - Tile height in pixels
* @param {number} tileDataResult.bandCount - Number of bands
* @param {Object} tileDataResult.device - WebGL device
* @param {Object} multibandInstance - The multiband instance with GPU and style management
* @returns {Array<Object>} Array of RasterModule objects backed by luma textures
* @throws {Error} If no active style or kernel is available
* @public
*/
const renderTile = (tileDataResult, multibandInstance) => {
const debugValidation = Boolean(multibandInstance && multibandInstance.debug === true);
if (tileDataResult && tileDataResult.data) {
const [data, rawWidth, rawHeight, bandCount] = [
tileDataResult.data,
tileDataResult.width,
tileDataResult.height,
tileDataResult.bandCount
];
const width = Math.max(1, Math.floor(Number(rawWidth) || 0));
const height = Math.max(1, Math.floor(Number(rawHeight) || 0));
const activeStyle = (
multibandInstance._activeStyle && multibandInstance._activeStyle.name === multibandInstance._activeStyleName
)
? multibandInstance._activeStyle
: multibandInstance._styles.find((s) => s.name === multibandInstance._activeStyleName);
if (!activeStyle) {
throw new Error('[renderTile] No active style found');
}
multibandInstance._activeStyle = activeStyle;
const render = multibandInstance._getActiveStyleKernel();
if (!render) {
throw new Error('[renderTile] No active style kernel available');
}
render.setOutput([width, height]);
const kernelInput = input(data, [width, height, bandCount]);
let kernelOutput = runKernelWithIsolatedState(tileDataResult.device, render, kernelInput, width, height);
let intermediateTexture = null;
let colorMapping = null;
if (activeStyle.rgbaColors && activeStyle.stops) {
const styleDomain = activeStyle.domain || [-1.0, 1.0];
const domainMin = styleDomain[0];
const domainMax = styleDomain[1];
if (!activeStyle.colorLUT) {
activeStyle.colorLUT = generateColorLUT(activeStyle.rgbaColors, activeStyle.stops, styleDomain);
}
// Keep reference to intermediate texture for cleanup
intermediateTexture = kernelOutput;
colorMapping = {
colorLUT: activeStyle.colorLUT,
domainMin,
domainMax,
};
}
const textureHandle = extractGpuTextureHandle(kernelOutput, render);
if (!textureHandle) {
throw new Error("[renderTile] GPU.js kernel did not expose a texture handle");
}
try {
return toRasterModules(tileDataResult, textureHandle, width, height, debugValidation, colorMapping);
} finally {
// Clean up GPU.js intermediate textures after hand-off to luma.
if (intermediateTexture && typeof intermediateTexture.delete === "function") {
intermediateTexture.delete();
}
if (kernelOutput && kernelOutput !== intermediateTexture && typeof kernelOutput.delete === "function") {
kernelOutput.delete();
}
}
} else {
throw new Error('[renderTile] Missing tile data for GPU render');
}
};
export { renderTile, releaseBlitResources, destroyLumaTextureRef };