From 0054bd645293a95a96e70c73a9c30609b4fb1e22 Mon Sep 17 00:00:00 2001 From: Pessimistress Date: Fri, 10 Oct 2025 21:14:05 -0700 Subject: [PATCH 1/5] port IconLayer to WebGPU --- modules/core/src/lib/attribute/data-column.ts | 6 +- modules/core/src/lib/attribute/gl-utils.ts | 6 +- modules/layers/src/icon-layer/icon-layer.ts | 65 +++++---- .../layers/src/icon-layer/icon-layer.wgsl.ts | 129 ++++++++++++++++++ 4 files changed, 172 insertions(+), 34 deletions(-) create mode 100644 modules/layers/src/icon-layer/icon-layer.wgsl.ts diff --git a/modules/core/src/lib/attribute/data-column.ts b/modules/core/src/lib/attribute/data-column.ts index 2b66519acd9..8e17cd641c4 100644 --- a/modules/core/src/lib/attribute/data-column.ts +++ b/modules/core/src/lib/attribute/data-column.ts @@ -268,11 +268,10 @@ export default class DataColumn { options: Partial | null = null ): BufferLayout { const accessor = this.getAccessor(); - const attributes: BufferAttributeLayout[] = []; + const attributes: (BufferAttributeLayout | null)[] = []; const result: BufferLayout = { name: this.id, - byteStride: getStride(accessor), - attributes + byteStride: getStride(accessor) }; if (this.doublePrecision) { @@ -307,6 +306,7 @@ export default class DataColumn { } else { attributes.push(getBufferAttributeLayout(attributeName, accessor, this.device.type)); } + result.attributes = attributes.filter(Boolean) as BufferAttributeLayout[]; return result; } diff --git a/modules/core/src/lib/attribute/gl-utils.ts b/modules/core/src/lib/attribute/gl-utils.ts index 286a441768f..f18f86eac0c 100644 --- a/modules/core/src/lib/attribute/gl-utils.ts +++ b/modules/core/src/lib/attribute/gl-utils.ts @@ -26,7 +26,11 @@ export function getBufferAttributeLayout( name: string, accessor: BufferAccessor, deviceType: 'webgpu' | 'wegbgl' | string -): BufferAttributeLayout { +): BufferAttributeLayout | null { + if ((accessor.size as number) > 4) { + // Definitely not valid. TODO - stricter validation? + return null; + } // TODO(ibgreen): WebGPU change. Currently we always use normalized 8 bit integers const type = deviceType === 'webgpu' && accessor.type === 'uint8' ? 'unorm8' : accessor.type; return { diff --git a/modules/layers/src/icon-layer/icon-layer.ts b/modules/layers/src/icon-layer/icon-layer.ts index 8f86ec84f56..c9f28e4ff3e 100644 --- a/modules/layers/src/icon-layer/icon-layer.ts +++ b/modules/layers/src/icon-layer/icon-layer.ts @@ -2,13 +2,14 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {Layer, project32, picking, log, UNIT} from '@deck.gl/core'; +import {Layer, color, project32, picking, log, UNIT} from '@deck.gl/core'; import {SamplerProps, Texture} from '@luma.gl/core'; import {Model, Geometry} from '@luma.gl/engine'; import {iconUniforms, IconProps} from './icon-layer-uniforms'; import vs from './icon-layer-vertex.glsl'; import fs from './icon-layer-fragment.glsl'; +import {shaderWGSL as source} from './icon-layer.wgsl'; import IconManager from './icon-manager'; import type { @@ -25,6 +26,7 @@ import type { } from '@deck.gl/core'; import type {UnpackedIcon, IconMapping, LoadIconErrorContext} from './icon-manager'; +import {Parameters} from '@luma.gl/core'; type _IconLayerProps = { data: LayerDataSource; @@ -139,7 +141,7 @@ export default class IconLayer extends }; getShaders() { - return super.getShaders({vs, fs, modules: [project32, picking, iconUniforms]}); + return super.getShaders({vs, fs, source, modules: [project32, color, picking, iconUniforms]}); } initializeState() { @@ -166,24 +168,25 @@ export default class IconLayer extends accessor: 'getSize', defaultValue: 1 }, - instanceOffsets: { - size: 2, - accessor: 'getIcon', - // eslint-disable-next-line @typescript-eslint/unbound-method - transform: this.getInstanceOffset - }, - instanceIconFrames: { - size: 4, - accessor: 'getIcon', - // eslint-disable-next-line @typescript-eslint/unbound-method - transform: this.getInstanceIconFrame - }, - instanceColorModes: { - size: 1, - type: 'uint8', + instanceIconDefs: { + size: 7, accessor: 'getIcon', // eslint-disable-next-line @typescript-eslint/unbound-method - transform: this.getInstanceColorMode + transform: this.getInstanceIconDef, + shaderAttributes: { + instanceOffsets: { + size: 2, + elementOffset: 0 + }, + instanceIconFrames: { + size: 4, + elementOffset: 2 + }, + instanceColorModes: { + size: 1, + elementOffset: 6 + } + } }, instanceColors: { size: this.props.colorFormat.length, @@ -286,6 +289,13 @@ export default class IconLayer extends } protected _getModel(): Model { + const parameters = + this.context.device.type === 'webgpu' + ? ({ + depthWriteEnabled: true, + depthCompare: 'less-equal' + } satisfies Parameters) + : undefined; // The icon-layer vertex shader uses 2d positions // specifed via: in vec2 positions; const positions = [-1, -1, 1, -1, -1, 1, 1, 1]; @@ -305,7 +315,8 @@ export default class IconLayer extends } } }), - isInstanced: true + isInstanced: true, + parameters }); } @@ -322,23 +333,17 @@ export default class IconLayer extends } } - protected getInstanceOffset(icon: string): number[] { + protected getInstanceIconDef(icon: string): number[] { const { + x, + y, width, height, + mask, anchorX = width / 2, anchorY = height / 2 } = this.state.iconManager.getIconMapping(icon); - return [width / 2 - anchorX, height / 2 - anchorY]; - } - - protected getInstanceColorMode(icon: string): number { - const mapping = this.state.iconManager.getIconMapping(icon); - return mapping.mask ? 1 : 0; - } - protected getInstanceIconFrame(icon: string): number[] { - const {x, y, width, height} = this.state.iconManager.getIconMapping(icon); - return [x, y, width, height]; + return [width / 2 - anchorX, height / 2 - anchorY, x, y, width, height, mask ? 1 : 0]; } } diff --git a/modules/layers/src/icon-layer/icon-layer.wgsl.ts b/modules/layers/src/icon-layer/icon-layer.wgsl.ts new file mode 100644 index 00000000000..1ba09cbd0fb --- /dev/null +++ b/modules/layers/src/icon-layer/icon-layer.wgsl.ts @@ -0,0 +1,129 @@ +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +export const shaderWGSL = /* wgsl */ `\ +struct IconUniforms { + sizeScale: f32, + iconsTextureDim: vec2, + sizeBasis: f32, + sizeMinPixels: f32, + sizeMaxPixels: f32, + billboard: i32, + sizeUnits: i32, + alphaCutoff: f32 +}; + +@group(0) @binding(2) var icon: IconUniforms; +@group(0) @binding(3) var iconsTexture : texture_2d; +@group(0) @binding(4) var iconsTextureSampler : sampler; + +fn rotate_by_angle(vertex: vec2, angle_deg: f32) -> vec2 { + let angle_radian = angle_deg * PI / 180.0; + let c = cos(angle_radian); + let s = sin(angle_radian); + let rotation = mat2x2(vec2(c, s), vec2(-s, c)); + return rotation * vertex; +} + +struct Attributes { + @location(0) positions: vec2, + + @location(1) instancePositions: vec3, + @location(2) instancePositions64Low: vec3, + @location(3) instanceSizes: f32, + @location(4) instanceAngles: f32, + @location(5) instanceColors: vec4, + @location(6) instancePickingColors: vec3, + @location(7) instanceIconFrames: vec4, + @location(8) instanceColorModes: f32, + @location(9) instanceOffsets: vec2, + @location(10) instancePixelOffset: vec2, +}; + +struct Varyings { + @builtin(position) position: vec4, + + @location(0) vColorMode: f32, + @location(1) vColor: vec4, + @location(2) vTextureCoords: vec2, + @location(3) uv: vec2, +}; + +@vertex +fn vertexMain(inp: Attributes) -> Varyings { + // write geometry fields used by filters + FS + geometry.worldPosition = inp.instancePositions; + geometry.uv = inp.positions; + geometry.pickingColor = inp.instancePickingColors; + + var outp: Varyings; + outp.uv = inp.positions; + + let iconSize = inp.instanceIconFrames.zw; + + // convert size in meters to pixels, then clamp + let sizePixels = clamp( + project_unit_size_to_pixel(inp.instanceSizes * icon.sizeScale, icon.sizeUnits), + icon.sizeMinPixels, icon.sizeMaxPixels + ); + + // scale icon height to match instanceSize + let iconConstraint = select(iconSize.y, iconSize.x, icon.sizeBasis == 0.0); + let instanceScale = select(sizePixels / iconConstraint, 0.0, iconConstraint == 0.0); + + // scale and rotate vertex in "pixel" units; then add per-instance pixel offset + var pixelOffset = inp.positions / 2.0 * iconSize + inp.instanceOffsets; + pixelOffset = rotate_by_angle(pixelOffset, inp.instanceAngles) * instanceScale; + pixelOffset = pixelOffset + inp.instancePixelOffset; + pixelOffset.y = pixelOffset.y * -1.0; + + if (icon.billboard != 0) { + var pos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, vec3(0.0)); // TODO, &geometry.position); + // DECKGL_FILTER_GL_POSITION(pos, geometry); + + var offset = vec3(pixelOffset, 0.0); + // DECKGL_FILTER_SIZE(offset, geometry); + let clipOffset = project_pixel_size_to_clipspace(offset.xy); + pos = vec4(pos.x + clipOffset.x, pos.y + clipOffset.y, pos.z, pos.w); + outp.position = pos; + } else { + var offset_common = vec3(project_pixel_size_vec2(pixelOffset), 0.0); + // DECKGL_FILTER_SIZE(offset_common, geometry); + var pos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, offset_common); // TODO, &geometry.position); + // DECKGL_FILTER_GL_POSITION(pos, geometry); + outp.position = pos; + } + + let uvMix = (inp.positions.xy + vec2(1.0, 1.0)) * 0.5; + outp.vTextureCoords = mix(inp.instanceIconFrames.xy, inp.instanceIconFrames.xy + iconSize, uvMix) / icon.iconsTextureDim; + + outp.vColor = inp.instanceColors; + // DECKGL_FILTER_COLOR(outp.vColor, geometry); + + outp.vColorMode = inp.instanceColorModes; + + return outp; +} + +@fragment +fn fragmentMain(inp: Varyings) -> @location(0) vec4 { + // expose to deck.gl filter hooks + geometry.uv = inp.uv; + + let texColor = textureSample(iconsTexture, iconsTextureSampler, inp.vTextureCoords); + + // if colorMode == 0, use pixel color from the texture + // if colorMode == 1 (or picking), use texture as transparency mask + let rgb = mix(texColor.rgb, inp.vColor.rgb, inp.vColorMode); + let a = texColor.a * color.opacity * inp.vColor.a; + + if (a < icon.alphaCutoff) { + discard; + } + + var fragColor = deckgl_premultiplied_alpha(vec4(rgb, a)); + // DECKGL_FILTER_COLOR(fragColor, geometry); + return fragColor; +} +`; From 9c1460eec3be8d0ec7c6f39fd72354afe68e4ba3 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Fri, 10 Oct 2025 21:45:46 -0700 Subject: [PATCH 2/5] Fix MultiIconLayer --- .../multi-icon-layer/multi-icon-layer.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts index aa20d536902..5f2bc35ad02 100644 --- a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts +++ b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts @@ -115,17 +115,13 @@ export default class MultiIconLayer extends } } - protected getInstanceOffset(icons: string): number[] { - return icons ? Array.from(icons).flatMap(icon => super.getInstanceOffset(icon)) : EMPTY_ARRAY; - } - - getInstanceColorMode(icons: string): number { - return 1; // mask - } - - getInstanceIconFrame(icons: string): number[] { + protected getInstanceIconDef(icons: string): number[] { return icons - ? Array.from(icons).flatMap(icon => super.getInstanceIconFrame(icon)) + ? Array.from(icons).flatMap(icon => { + const def = super.getInstanceIconDef(icon); + def[6] = 1; // mask + return def; + }) : EMPTY_ARRAY; } } From b21ffcbb45f7ef7a001bb13b3f14c1667bee8898 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 16 Dec 2025 14:42:12 -0800 Subject: [PATCH 3/5] fix TextLayer --- .../multi-icon-layer/multi-icon-layer.ts | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts index 5f2bc35ad02..75db284f78a 100644 --- a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts +++ b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts @@ -9,14 +9,14 @@ import {SdfProps, sdfUniforms} from './sdf-uniforms'; import fs from './multi-icon-layer-fragment.glsl'; import type {IconLayerProps} from '../../icon-layer/icon-layer'; -import type {Accessor, Color, UpdateParameters, DefaultProps} from '@deck.gl/core'; +import type {AccessorFunction, Color, UpdateParameters, DefaultProps} from '@deck.gl/core'; // TODO expose as layer properties const DEFAULT_BUFFER = 192.0 / 256; const EMPTY_ARRAY = []; type _MultiIconLayerProps = { - getIconOffsets?: Accessor; + getIconOffsets?: AccessorFunction; sdf?: boolean; smoothing?: number; outlineWidth?: number; @@ -54,14 +54,14 @@ export default class MultiIconLayer extends super.initializeState(); const attributeManager = this.getAttributeManager(); + const instanceIconDefs = attributeManager!.attributes.instanceIconDefs; + instanceIconDefs.settings.transform = undefined; + instanceIconDefs.settings.accessor = (object, objectInfo) => + this.getIconAndOffset(object, objectInfo); attributeManager!.addInstanced({ - instanceOffsets: { - size: 2, - accessor: 'getIconOffsets' - }, instancePickingColors: { type: 'uint8', - size: 3, + size: 4, accessor: (object, {index, target: value}) => this.encodePickingColor(index, value) } }); @@ -69,9 +69,16 @@ export default class MultiIconLayer extends updateState(params: UpdateParameters) { super.updateState(params); - const {props, oldProps} = params; + const {props, oldProps, changeFlags} = params; let {outlineColor} = props; + if ( + changeFlags.updateTriggersChanged && + (changeFlags.updateTriggersChanged.getIcon || + changeFlags.updateTriggersChanged.getIconOffsets) + ) { + this.getAttributeManager()!.invalidate('instanceIconDefs'); + } if (outlineColor !== oldProps.outlineColor) { outlineColor = outlineColor.map(x => x / 255) as Color; outlineColor[3] = Number.isFinite(outlineColor[3]) ? outlineColor[3] : 1; @@ -115,10 +122,22 @@ export default class MultiIconLayer extends } } - protected getInstanceIconDef(icons: string): number[] { - return icons - ? Array.from(icons).flatMap(icon => { - const def = super.getInstanceIconDef(icon); + protected getIconAndOffset( + object: DataT, + objectInfo: { + index: number; + data: any; + target: any[]; + } + ): number[] { + const {getIcon, getIconOffsets} = this.props; + const text = getIcon(object, objectInfo) as string; // forwarded getText + const offsets = getIconOffsets(object, objectInfo); // text length x 2 + return text + ? Array.from(text).flatMap((char, i) => { + const def = super.getInstanceIconDef(char); + def[0] = offsets[i * 2]; + def[1] = offsets[i * 2 + 1]; def[6] = 1; // mask return def; }) From 5f8a66313bd4a40e5faba5ddb4ac2bea1efdb22a Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 16 Dec 2025 19:13:50 -0800 Subject: [PATCH 4/5] fix tests --- .../src/text-layer/multi-icon-layer/multi-icon-layer.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts index 75db284f78a..68e229d0144 100644 --- a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts +++ b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts @@ -56,8 +56,7 @@ export default class MultiIconLayer extends const attributeManager = this.getAttributeManager(); const instanceIconDefs = attributeManager!.attributes.instanceIconDefs; instanceIconDefs.settings.transform = undefined; - instanceIconDefs.settings.accessor = (object, objectInfo) => - this.getIconAndOffset(object, objectInfo); + instanceIconDefs.settings.accessor = this.getIconAndOffset.bind(this); attributeManager!.addInstanced({ instancePickingColors: { type: 'uint8', @@ -130,7 +129,7 @@ export default class MultiIconLayer extends target: any[]; } ): number[] { - const {getIcon, getIconOffsets} = this.props; + const {getIcon, getIconOffsets} = this.getCurrentLayer()!.props; const text = getIcon(object, objectInfo) as string; // forwarded getText const offsets = getIconOffsets(object, objectInfo); // text length x 2 return text From 90e2f8ba0578df8a33c90a959f00fb0ba33610f6 Mon Sep 17 00:00:00 2001 From: Xiaoji Chen Date: Tue, 16 Dec 2025 19:28:28 -0800 Subject: [PATCH 5/5] Use updater --- .../multi-icon-layer/multi-icon-layer.ts | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts index 68e229d0144..5c2c1ea8fb6 100644 --- a/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts +++ b/modules/layers/src/text-layer/multi-icon-layer/multi-icon-layer.ts @@ -2,14 +2,20 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import {log} from '@deck.gl/core'; +import {log, createIterable} from '@deck.gl/core'; import IconLayer from '../../icon-layer/icon-layer'; import {SdfProps, sdfUniforms} from './sdf-uniforms'; import fs from './multi-icon-layer-fragment.glsl'; import type {IconLayerProps} from '../../icon-layer/icon-layer'; -import type {AccessorFunction, Color, UpdateParameters, DefaultProps} from '@deck.gl/core'; +import type { + Attribute, + AccessorFunction, + Color, + UpdateParameters, + DefaultProps +} from '@deck.gl/core'; // TODO expose as layer properties const DEFAULT_BUFFER = 192.0 / 256; @@ -55,8 +61,8 @@ export default class MultiIconLayer extends const attributeManager = this.getAttributeManager(); const instanceIconDefs = attributeManager!.attributes.instanceIconDefs; - instanceIconDefs.settings.transform = undefined; - instanceIconDefs.settings.accessor = this.getIconAndOffset.bind(this); + // eslint-disable-next-line @typescript-eslint/unbound-method + instanceIconDefs.settings.update = this.calculateInstanceIconDefs; attributeManager!.addInstanced({ instancePickingColors: { type: 'uint8', @@ -121,25 +127,30 @@ export default class MultiIconLayer extends } } - protected getIconAndOffset( - object: DataT, - objectInfo: { - index: number; - data: any; - target: any[]; - } - ): number[] { - const {getIcon, getIconOffsets} = this.getCurrentLayer()!.props; - const text = getIcon(object, objectInfo) as string; // forwarded getText - const offsets = getIconOffsets(object, objectInfo); // text length x 2 - return text - ? Array.from(text).flatMap((char, i) => { + protected calculateInstanceIconDefs( + attribute: Attribute, + {startRow, endRow}: {startRow: number; endRow: number} + ) { + const {data, getIcon, getIconOffsets} = this.props; + let i = attribute.getVertexOffset(startRow); + const output = attribute.value as Float32Array; + const {iterable, objectInfo} = createIterable(data, startRow, endRow); + for (const object of iterable) { + objectInfo.index++; + const text = getIcon(object, objectInfo) as string; // forwarded getText + const offsets = getIconOffsets(object, objectInfo); // text length x 2 + if (text) { + let j = 0; + for (const char of Array.from(text)) { const def = super.getInstanceIconDef(char); - def[0] = offsets[i * 2]; - def[1] = offsets[i * 2 + 1]; + def[0] = offsets[j * 2]; + def[1] = offsets[j * 2 + 1]; def[6] = 1; // mask - return def; - }) - : EMPTY_ARRAY; + output.set(def, i); + i += attribute.size; + j++; + } + } + } } }