s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
752 lines (751 loc) • 34.8 kB
JavaScript
import encodeLayerAttribute from 'style/encodeLayerAttribute.js';
import shaderCode from '../shaders/glyph.wgsl';
/** st (0), adjustXY (1), xy (2), wh (3), texXY (4), texWH (5) */
const SUB_SHADER_BUFFER_LAYOUT = [0, 1, 2, 3, 4, 5].map((i) => ({
arrayStride: 6 * 4 * 2, // 6 attributes * 4 bytes * 2 floats
stepMode: 'instance',
attributes: [
// offset: attribute position * 4 bytes * 2 floats
{ shaderLocation: i, offset: i * 4 * 2, format: 'float32x2' },
],
}));
/** st - offsetXY (0), xy - wh (1), texXY - textWH (2), paths12 (3), paths34 (4) */
const SUB_SHADER_BUFFER_LAYOUT_PATH = [0, 1, 2, 3, 4].map((i) => ({
arrayStride: 5 * 4 * 4, // 5 attributes * 4 floats * 4 bytes
stepMode: 'instance',
attributes: [
// offset: attribute position * 4 floats * 4 bytes
{ shaderLocation: i, offset: i * 4 * 4, format: 'float32x4' },
],
}));
/**
* Compute the shader buffer collision layout given the location
* @param location - the location of the collision result index (box or path)
* @returns the layout
*/
const SHADER_BUFFER_COLLISION_COLOR_LAYOUT = (location) => [
{
// collision result index (without the proper offset)
arrayStride: 4, // 4 bytes * 1 float
stepMode: 'instance',
attributes: [{ shaderLocation: location, offset: 0, format: 'uint32' }],
},
{
// color
arrayStride: 4 * 4, // 4 floats * 4 bytes
stepMode: 'instance',
attributes: [{ shaderLocation: location + 1, offset: 0, format: 'float32x4' }],
},
];
const SHADER_BUFFER_LAYOUT = [
...SUB_SHADER_BUFFER_LAYOUT,
...SHADER_BUFFER_COLLISION_COLOR_LAYOUT(6),
];
const SHADER_BUFFER_LAYOUT_PATH = [
...SUB_SHADER_BUFFER_LAYOUT_PATH,
...SHADER_BUFFER_COLLISION_COLOR_LAYOUT(5),
];
const SUB_TEST_SHADER_BUFFER_LAYOUT_ATTR = [0, 1, 2, 3, 4].map((i) => ({
shaderLocation: i,
offset: i * 4 * 2, // 4 bytes * 2 floats * attribute position
format: 'float32x2',
}));
const TEST_SHADER_BUFFER_LAYOUT = [
{
// collision result index (without the proper offset)
arrayStride: 4 * 18, // 4 bytes * 1 uint32 at 18 float intervals
stepMode: 'instance',
attributes: [
...SUB_TEST_SHADER_BUFFER_LAYOUT_ATTR,
{
shaderLocation: 5,
offset: 4 * 10, // 4 bytes * 15 floats prior
format: 'uint32',
},
],
},
];
// the reason for the stride being 18 not 16 is to allow for the padding of the unused ID
// which you can see inside the glyph.wgsl struct `GlyphContainerPath`
const TEST_SHADER_BUFFER_LAYOUT_PATH = [
{
arrayStride: 4 * 18, // 4 bytes per float * 18 floats total
stepMode: 'instance',
attributes: [
// st
{ shaderLocation: 0, offset: 0, format: 'float32x2' },
// offset (location 1 * 4 bytes per float * 2 floats)
{ shaderLocation: 1, offset: 1 * 4 * 2, format: 'float32x2' },
// xy (4 bytes per float * 4 floats prior)
{ shaderLocation: 2, offset: 4 * 4, format: 'float32x2' },
// stPaths12 (4 bytes per float * 6 floats prior)
{ shaderLocation: 3, offset: 4 * 6, format: 'float32x4' },
// stPaths34 (4 bytes per float * 10 floats prior)
{ shaderLocation: 4, offset: 4 * 10, format: 'float32x4' },
// padding (4 bytes per float * 14 floats prior)
{ shaderLocation: 5, offset: 4 * 14, format: 'float32' },
// collision result index (4 bytes per float * 15 floats prior)
{ shaderLocation: 6, offset: 4 * 15, format: 'uint32' },
],
},
];
/** Glyph Feature is a standalone glyph render storage unit that can be drawn to the GPU */
export class GlyphFeature {
workflow;
source;
tile;
layerGuide;
count;
offset;
filterCount;
filterOffset;
isPath;
isIcon;
featureCode;
glyphUniformBuffer;
glyphBoundsBuffer;
glyphAttributeBuffer;
glyphAttributeNoStrokeBuffer;
featureCodeBuffer;
parent;
type = 'glyph';
bindGroup;
glyphBindGroup;
glyphStrokeBindGroup;
glyphFilterBindGroup;
glyphInteractiveBindGroup;
/**
* @param workflow - the glyph workflow
* @param source - the glyph source
* @param tile - the tile this feature is drawn on
* @param layerGuide - the layer guide for this feature
* @param count - the number of glyphs
* @param offset - the offset of the glyphs
* @param filterCount - the number of filter glyphs
* @param filterOffset - the offset of the filter glyphs
* @param isPath - whether the feature is a path or a point
* @param isIcon - whether the feature is an icon or a standard glyph
* @param featureCode - the encoded feature code
* @param glyphUniformBuffer - the glyph uniform buffer
* @param glyphBoundsBuffer - the glyph bounds buffer
* @param glyphAttributeBuffer - the glyph attribute buffer
* @param glyphAttributeNoStrokeBuffer - the glyph attribute buffer
* @param featureCodeBuffer - the encoded feature code that tells the GPU how to compute it's properties
* @param parent - the parent tile if applicable
*/
constructor(workflow, source, tile, layerGuide, count, offset, filterCount, filterOffset, isPath, isIcon, featureCode, glyphUniformBuffer, glyphBoundsBuffer, glyphAttributeBuffer, glyphAttributeNoStrokeBuffer, featureCodeBuffer, parent) {
this.workflow = workflow;
this.source = source;
this.tile = tile;
this.layerGuide = layerGuide;
this.count = count;
this.offset = offset;
this.filterCount = filterCount;
this.filterOffset = filterOffset;
this.isPath = isPath;
this.isIcon = isIcon;
this.featureCode = featureCode;
this.glyphUniformBuffer = glyphUniformBuffer;
this.glyphBoundsBuffer = glyphBoundsBuffer;
this.glyphAttributeBuffer = glyphAttributeBuffer;
this.glyphAttributeNoStrokeBuffer = glyphAttributeNoStrokeBuffer;
this.featureCodeBuffer = featureCodeBuffer;
this.parent = parent;
this.bindGroup = this.#buildBindGroup();
this.glyphBindGroup = this.#buildGlyphBindGroup();
this.glyphStrokeBindGroup = this.#buildStrokeBindGroup();
this.glyphFilterBindGroup = this.#buildFilterBindGroup();
this.glyphInteractiveBindGroup = this.#buildInteractiveBindGroup();
}
/** Draw this feature */
draw() {
this.workflow.draw(this);
}
/** Compute the feature's interactivity with the mouse */
compute() {
this.workflow.computeInteractive(this);
}
/** Update the shared texture's bind groups */
updateSharedTexture() {
this.glyphBindGroup = this.#buildGlyphBindGroup();
this.glyphStrokeBindGroup = this.#buildStrokeBindGroup();
}
/** Destroy and cleanup the feature */
destroy() {
this.glyphBoundsBuffer.destroy();
this.glyphUniformBuffer.destroy();
this.glyphAttributeBuffer.destroy();
this.glyphAttributeNoStrokeBuffer.destroy();
this.featureCodeBuffer.destroy();
}
/**
* Duplicate the feature
* @param tile - the tile this feature is drawn on
* @param parent - the parent tile if applicable
* @param bounds - the bounds of the tile if applicable
* @returns the duplicated feature
*/
duplicate(tile, parent, bounds) {
const { workflow, source, layerGuide, featureCodeBuffer, count, offset, filterCount, filterOffset, isPath, isIcon, featureCode, glyphBoundsBuffer, glyphUniformBuffer, glyphAttributeBuffer, glyphAttributeNoStrokeBuffer, } = this;
const { context } = workflow;
const cE = context.device.createCommandEncoder();
const newGlyphBoundsBuffer = bounds !== undefined
? context.buildGPUBuffer('Glyph Bounds Buffer', new Float32Array(bounds), GPUBufferUsage.UNIFORM)
: context.duplicateGPUBuffer(glyphBoundsBuffer, cE);
const newGlyphUniformBuffer = context.duplicateGPUBuffer(glyphUniformBuffer, cE);
const newGlyphAttributeBuffer = context.duplicateGPUBuffer(glyphAttributeBuffer, cE);
const newGlyphAttributeNoStrokeBuffer = context.duplicateGPUBuffer(glyphAttributeNoStrokeBuffer, cE);
const newFeatureCodeBuffer = context.duplicateGPUBuffer(featureCodeBuffer, cE);
context.device.queue.submit([cE.finish()]);
return new GlyphFeature(workflow, source, tile, layerGuide, count, offset, filterCount, filterOffset, isPath, isIcon, featureCode, newGlyphUniformBuffer, newGlyphBoundsBuffer, newGlyphAttributeBuffer, newGlyphAttributeNoStrokeBuffer, newFeatureCodeBuffer, parent);
}
/**
* Build the bind group for the glyph feature
* @returns the GPU Bind Group for the glyph feature
*/
#buildBindGroup() {
const { workflow, tile, parent, layerGuide, featureCodeBuffer } = this;
const { context } = workflow;
const { mask } = parent ?? tile;
const { layerBuffer, layerCodeBuffer } = layerGuide;
return context.buildGroup('Glyph Feature BindGroup', context.featureBindGroupLayout, [
mask.uniformBuffer,
mask.positionBuffer,
layerBuffer,
layerCodeBuffer,
featureCodeBuffer,
]);
}
/**
* Build a glyph fill bind group
* @returns the GPU Bind Group
*/
#buildGlyphBindGroup() {
const { glyphUniformBuffer, glyphAttributeNoStrokeBuffer } = this;
return this.#buildGlyphBindGroupContext(glyphUniformBuffer, glyphAttributeNoStrokeBuffer);
}
/**
* Build a stroke bind group
* @returns the GPU Bind Group
*/
#buildStrokeBindGroup() {
const { glyphUniformBuffer, glyphAttributeBuffer } = this;
return this.#buildGlyphBindGroupContext(glyphUniformBuffer, glyphAttributeBuffer, true);
}
/**
* Build a glyph bind group context for the glyph draw type "stroke" or "fill"
* @param glyphUniformBuffer - the glyph uniform buffer
* @param glyphAttributeBuffer - the glyph attribute buffer
* @param isStroke - whether the glyph draw type is stroke or fill
* @returns the GPU Bind Group
*/
#buildGlyphBindGroupContext(glyphUniformBuffer, glyphAttributeBuffer, isStroke = false) {
const { context, glyphBindGroupLayout, glyphFilterResultBuffer } = this.workflow;
const { device, defaultSampler, sharedTexture } = context;
return device.createBindGroup({
label: `Glyph ${isStroke ? 'Stroke' : ''} BindGroup`,
layout: glyphBindGroupLayout,
entries: [
{ binding: 1, resource: { buffer: glyphUniformBuffer } },
{ binding: 2, resource: defaultSampler },
{ binding: 3, resource: sharedTexture.createView() },
{ binding: 8, resource: { buffer: glyphFilterResultBuffer } },
{ binding: 9, resource: { buffer: glyphAttributeBuffer } },
],
});
}
/**
* Build the bind group for the glyph filters
* @returns the GPU Bind Group for the glyph filters
*/
#buildFilterBindGroup() {
const { workflow, source, glyphBoundsBuffer, glyphUniformBuffer, glyphAttributeBuffer } = this;
const { context, glyphFilterBindGroupLayout, glyphBBoxesBuffer, glyphFilterResultBuffer } = workflow;
return context.device.createBindGroup({
label: 'GlyphFilter BindGroup',
layout: glyphFilterBindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: glyphBoundsBuffer } },
{ binding: 1, resource: { buffer: glyphUniformBuffer } },
// assign the filter to both 4 or 5 as later the right binding will be used
{ binding: 4, resource: { buffer: source.glyphFilterBuffer } },
{ binding: 5, resource: { buffer: source.glyphFilterBuffer } },
{ binding: 6, resource: { buffer: glyphBBoxesBuffer } },
{ binding: 7, resource: { buffer: glyphFilterResultBuffer } },
{ binding: 9, resource: { buffer: glyphAttributeBuffer } },
],
});
}
/**
* Build an interactive bind group
* @returns a new interactive bind group
*/
#buildInteractiveBindGroup() {
const { workflow, source, glyphUniformBuffer, glyphAttributeBuffer } = this;
const { context, glyphInteractiveBindGroupLayout, glyphBBoxesBuffer, glyphFilterResultBuffer } = workflow;
return context.device.createBindGroup({
label: 'Glyph Interactive BindGroup',
layout: glyphInteractiveBindGroupLayout,
entries: [
{ binding: 1, resource: { buffer: glyphUniformBuffer } },
{ binding: 4, resource: { buffer: source.glyphFilterBuffer } },
{ binding: 5, resource: { buffer: source.glyphFilterBuffer } },
{ binding: 6, resource: { buffer: glyphBBoxesBuffer } },
{ binding: 8, resource: { buffer: glyphFilterResultBuffer } },
{ binding: 9, resource: { buffer: glyphAttributeBuffer } },
],
});
}
}
/** Glyph Workflow */
export default class GlyphWorkflow {
context;
module;
layerGuides = new Map();
pipeline;
pipelineC;
testRenderPipeline;
testCircleRenderPipeline;
bboxPipeline;
circlePipeline;
testFiltersPipeline;
testCirclePipeline;
interactivePipeline;
glyphBindGroupLayout;
glyphPipelineLayout;
glyphFilterBindGroupLayout;
glyphFilterPipelineLayout;
glyphInteractiveBindGroupLayout;
glyphInteractivePiplineLayout;
glyphBBoxesBuffer;
glyphFilterResultBuffer;
/** @param context - The WebGPU context */
constructor(context) {
this.context = context;
}
/** Setup the workflow */
async setup() {
const { context } = this;
const { device, frameBindGroupLayout, featureBindGroupLayout, interactiveBindGroupLayout } = context;
this.module = device.createShaderModule({ label: 'Glyph Shader Module', code: shaderCode });
this.glyphBBoxesBuffer = await context.buildGPUBuffer('Glyph BBoxes Buffer', new Float32Array(Array(2_000 * 5).fill(0)), GPUBufferUsage.STORAGE);
this.glyphFilterResultBuffer = context.buildGPUBuffer('Glyph Filter Result Buffer', new Float32Array(Array(2_000).fill(0)), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
this.glyphFilterBindGroupLayout = device.createBindGroupLayout({
label: 'Glyph Filter BindGroupLayout',
entries: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // bounds
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // uniforms
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // containers
{ binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // pathContainers
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, // bboxes
{ binding: 7, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, // collision results
{
binding: 9,
visibility: GPUShaderStage.COMPUTE | GPUShaderStage.VERTEX,
buffer: { type: 'uniform' },
}, // [offset, count, isStroke]
],
});
this.glyphInteractiveBindGroupLayout = device.createBindGroupLayout({
label: 'Glyph Interactive BindGroupLayout',
entries: [
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // uniforms
{ binding: 4, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // containers
{ binding: 5, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // pathContainers
{ binding: 6, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }, // bboxes
{ binding: 8, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // collision results
{
binding: 9,
visibility: GPUShaderStage.COMPUTE | GPUShaderStage.VERTEX,
buffer: { type: 'uniform' },
}, // [offset, count, isStroke]
],
});
this.glyphFilterPipelineLayout = device.createPipelineLayout({
label: 'Glyph Filter Pipeline Layout',
bindGroupLayouts: [
frameBindGroupLayout,
featureBindGroupLayout,
this.glyphFilterBindGroupLayout,
],
});
this.glyphInteractivePiplineLayout = device.createPipelineLayout({
label: 'Glyph Interactive Pipeline Layout',
bindGroupLayouts: [
frameBindGroupLayout,
featureBindGroupLayout,
this.glyphInteractiveBindGroupLayout,
interactiveBindGroupLayout,
],
});
this.glyphBindGroupLayout = device.createBindGroupLayout({
label: 'Glyph BindGroupLayout',
entries: [
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } }, // glyph uniforms
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } }, // sampler
{
binding: 3,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
texture: { sampleType: 'float' },
}, // texture
{ binding: 8, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }, // collision results
{
binding: 9,
visibility: GPUShaderStage.COMPUTE | GPUShaderStage.VERTEX,
buffer: { type: 'uniform' },
}, // [offset, count, isStroke]
],
});
this.glyphPipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [frameBindGroupLayout, featureBindGroupLayout, this.glyphBindGroupLayout],
});
this.pipeline = this.#getPipeline();
this.pipelineC = this.#getPipeline(false, true);
this.testRenderPipeline = this.#getPipeline(true);
this.testCircleRenderPipeline = this.#getPipeline(true, true);
this.bboxPipeline = this.#getComputePipeline('boxes');
this.circlePipeline = this.#getComputePipeline('circles');
this.testFiltersPipeline = this.#getComputePipeline('test');
this.interactivePipeline = this.#getComputePipeline('interactive');
}
/** Destroy and cleanup the workflow */
destroy() {
for (const { layerBuffer, layerCodeBuffer } of this.layerGuides.values()) {
layerBuffer.destroy();
layerCodeBuffer.destroy();
}
this.glyphBBoxesBuffer.destroy();
this.glyphFilterResultBuffer.destroy();
}
/**
* Build the layer definition for this workflow
* @param layerBase - the common layer attributes
* @param layer - the user defined layer attributes
* @returns a built layer definition that's ready to describe how to render a feature
*/
buildLayerDefinition(layerBase, layer) {
const { context } = this;
const { source, layerIndex, lch, visible } = layerBase;
// PRE) get layer base
const {
// layout
placement, spacing, textFamily, textField, textAnchor, textOffset, textPadding, textWordWrap, textAlign, textKerning, textLineHeight, iconFamily, iconField, iconAnchor, iconOffset, iconPadding, } = layer;
let {
// paint
textSize, iconSize, textFill, textStrokeWidth, textStroke,
// properties
geoFilter, interactive, cursor, overdraw, noShaping, viewCollisions, } = layer;
textSize = textSize ?? 16;
iconSize = iconSize ?? 16;
textFill = textFill ?? 'rgb(0, 0, 0)';
textStrokeWidth = textStrokeWidth ?? 0;
textStroke = textStroke ?? 'rgb(0, 0, 0)';
geoFilter = geoFilter ?? [];
interactive = interactive ?? false;
cursor = cursor ?? 'default';
overdraw = overdraw ?? false;
noShaping = noShaping ?? false;
viewCollisions = viewCollisions ?? false;
// 1) build definition
const layerDefinition = {
...layerBase,
type: 'glyph',
// paint
textSize,
iconSize,
textFill,
textStrokeWidth,
textStroke,
// layout
placement: placement ?? 'line',
spacing: spacing ?? 325,
textFamily: textFamily ?? '',
textField: textField ?? '',
textAnchor: textAnchor ?? 'center',
textOffset: textOffset ?? [0, 0],
textPadding: textPadding ?? [0, 0],
textWordWrap: textWordWrap ?? 0,
textAlign: textAlign ?? 'center',
textKerning: textKerning ?? 0,
textLineHeight: textLineHeight ?? 0,
iconFamily: iconFamily ?? '',
iconField: iconField ?? '',
iconAnchor: iconAnchor ?? 'center',
iconOffset: iconOffset ?? [0, 0],
iconPadding: iconPadding ?? [0, 0],
// properties
geoFilter,
interactive,
cursor,
overdraw,
noShaping,
viewCollisions,
};
// 2) build the layerCode
const layerCode = [];
for (const paint of [textSize, iconSize, textFill, textStrokeWidth, textStroke]) {
layerCode.push(...encodeLayerAttribute(paint, lch));
}
// 3) Setup layer buffers in GPU
const layerBuffer = context.buildGPUBuffer('Layer Uniform Buffer', new Float32Array([context.getDepthPosition(layerIndex), ~~lch]), GPUBufferUsage.UNIFORM);
const layerCodeBuffer = context.buildGPUBuffer('Layer Code Buffer', new Float32Array(layerCode), GPUBufferUsage.STORAGE);
// 4) Store layer guide
this.layerGuides.set(layerIndex, {
sourceName: source,
layerIndex,
layerCode,
layerBuffer,
layerCodeBuffer,
lch,
interactive,
cursor,
overdraw,
viewCollisions,
visible,
opaque: false,
});
return layerDefinition;
}
/**
* Build the source glyph data into glyph features
* @param glyphData - the input glyph data
* @param tile - the tile we are building the features for
*/
buildSource(glyphData, tile) {
const { context } = this;
const { glyphFilterBuffer, glyphQuadBuffer, glyphQuadIDBuffer: glyphQuadIndexBuffer, glyphColorBuffer, featureGuideBuffer, } = glyphData;
// prep buffers
const filterLength = glyphFilterBuffer.byteLength / 4 / 18; // 4 bytes per float at 18 float intervals
const source = {
type: 'glyph',
glyphFilterBuffer: context.buildGPUBuffer('Glyph Filter Buffer', glyphFilterBuffer, GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX),
glyphQuadBuffer: context.buildGPUBuffer('Glyph Quad Buffer', glyphQuadBuffer, GPUBufferUsage.VERTEX),
glyphQuadIndexBuffer: context.buildGPUBuffer('Glyph Quad ID Buffer', glyphQuadIndexBuffer, GPUBufferUsage.VERTEX),
glyphColorBuffer: context.buildGPUBuffer('Glyph Color Buffer', new Float32Array(glyphColorBuffer), GPUBufferUsage.VERTEX),
indexOffset: -1,
filterLength,
/** destroy the glyph source */
destroy: () => {
const { glyphFilterBuffer, glyphQuadBuffer, glyphQuadIndexBuffer, glyphColorBuffer } = source;
glyphFilterBuffer.destroy();
glyphQuadBuffer.destroy();
glyphQuadIndexBuffer.destroy();
glyphColorBuffer.destroy();
},
};
// build features
this.#buildFeatures(source, tile, new Float32Array(featureGuideBuffer));
}
/**
* Build glyph features from input glyph source
* @param source - the input glyph source
* @param tile - the tile we are building the features for
* @param featureGuideArray - the feature guide to help build the features properties
*/
#buildFeatures(source, tile, featureGuideArray) {
const { context } = this;
const features = [];
const lgl = featureGuideArray.length;
let i = 0;
while (i < lgl) {
// curlayerIndex, curType, filterOffset, filterCount, quadOffset, quadCount, encoding.length, ...encoding
const [layerIndex, isPath, isIcon, filterOffset, filterCount, offset, count, encodingSize] = featureGuideArray.slice(i, i + 8);
i += 8;
// If webgl1, we pull out the color and opacity otherwise build featureCode
let featureCode = [0];
if (encodingSize > 0)
featureCode = [...featureGuideArray.slice(i, i + encodingSize)];
// update index
i += encodingSize;
const layerGuide = this.layerGuides.get(layerIndex);
if (layerGuide === undefined)
continue;
const { overdraw } = layerGuide;
const glyphBoundsBuffer = context.buildGPUBuffer('Glyph Bounds Buffer', new Float32Array([0, 0, 1, 1]), GPUBufferUsage.UNIFORM);
const glyphUniformBuffer = context.buildGPUBuffer('Glyph Uniform Buffer', new Float32Array([0, isPath, isIcon, ~~overdraw, 1, 1]), GPUBufferUsage.UNIFORM);
const glyphAttributeBuffer = context.buildGPUBuffer('Glyph Attributes with Stroke Buffer', new Uint32Array([filterOffset, filterCount, 1]), GPUBufferUsage.UNIFORM);
const glyphAttributeNoStrokeBuffer = context.buildGPUBuffer('Glyph Attributes Buffer', new Uint32Array([filterOffset, filterCount, 0]), GPUBufferUsage.UNIFORM);
const featureCodeBuffer = context.buildGPUBuffer('Feature Code Buffer', new Float32Array(featureCode), GPUBufferUsage.STORAGE);
const feature = new GlyphFeature(this, source, tile, layerGuide, count, offset, filterCount, filterOffset, isPath === 1, isIcon === 1, featureCode, glyphUniformBuffer, glyphBoundsBuffer, glyphAttributeBuffer, glyphAttributeNoStrokeBuffer, featureCodeBuffer);
features.push(feature);
}
tile.addFeatures(features);
}
/**
* Build the render pipeline for the glyph workflows
* https://programmer.ink/think/several-best-practices-of-webgpu.html
* BEST PRACTICE 6: it is recommended to create pipeline asynchronously
* BEST PRACTICE 7: explicitly define pipeline layouts
* @param isTest - whether it is a test pipeline
* @param isPath - whether it is a path
* @returns the render pipeline
*/
#getPipeline(isTest = false, isPath = false) {
const { context, module } = this;
const { device, format, defaultBlend, sampleCount } = context;
const stencilState = {
compare: 'always',
failOp: 'keep',
depthFailOp: 'keep',
passOp: 'replace',
};
const namingPathPoint = isPath ? ' Path' : '';
const namingTestMain = isTest ? 'Test' : 'Main';
const vEntryPoint = `v${isPath ? 'Path' : ''}${namingTestMain}`;
return device.createRenderPipeline({
label: `Glyph Pipeline${namingPathPoint} ${namingTestMain}`,
layout: this.glyphPipelineLayout,
vertex: {
module,
entryPoint: vEntryPoint,
buffers: isTest
? isPath
? TEST_SHADER_BUFFER_LAYOUT_PATH
: TEST_SHADER_BUFFER_LAYOUT
: isPath
? SHADER_BUFFER_LAYOUT_PATH
: SHADER_BUFFER_LAYOUT,
},
fragment: {
module,
entryPoint: isTest ? 'fTest' : 'fMain',
targets: [{ format, blend: defaultBlend }],
},
primitive: {
topology: isTest ? 'line-list' : 'triangle-list',
cullMode: 'none',
},
multisample: { count: sampleCount },
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less-equal',
format: 'depth24plus-stencil8',
stencilFront: stencilState,
stencilBack: stencilState,
stencilReadMask: 0xffffffff,
stencilWriteMask: 0xffffffff,
},
});
}
/**
* Build a compute pipeline to check for interactive glyph data that interects with the mouse
* Or to check for which glyph's to filter in the next frame
* https://programmer.ink/think/several-best-practices-of-webgpu.html
* BEST PRACTICE 6: it is recommended to create pipeline asynchronously
* BEST PRACTICE 7: explicitly define pipeline layouts
* @param entryPoint - the kind of pipeline to build. "boxes", "circles", "test", or "interactive"
* @returns the GPU compute pipeline
*/
#getComputePipeline(entryPoint) {
const { context, module } = this;
return context.device.createComputePipeline({
label: `Glyph Filter ${entryPoint} Compute Pipeline`,
layout: entryPoint === 'interactive'
? this.glyphInteractivePiplineLayout
: this.glyphFilterPipelineLayout,
compute: { module, entryPoint },
});
}
/**
* Compute the glyph filters to see which glyphs to render
* @param features - the glyphs filter data to compute
*/
computeFilters(features) {
if (features.length === 0)
return;
const { context, bboxPipeline, circlePipeline, testFiltersPipeline } = this;
const { device, frameBufferBindGroup } = context;
const { ceil } = Math;
// prepare
const commandEncoder = device.createCommandEncoder();
const computePass = (this.context.computePass = commandEncoder.beginComputePass());
computePass.setBindGroup(0, frameBufferBindGroup);
// Step 1: Setup source offsets
// a: reset source offsets to 0
for (const { source } of features)
source.indexOffset = -1;
// b: update source offsets and assign to glyphUniformBuffer
let filterCountOffset = 0;
for (const { source, glyphUniformBuffer } of features) {
if (source.indexOffset === -1) {
source.indexOffset = filterCountOffset;
filterCountOffset += source.filterLength;
}
device.queue.writeBuffer(glyphUniformBuffer, 0, new Uint32Array([source.indexOffset]));
}
// Step 2: build bboxes or circles
for (const { isPath, bindGroup, glyphFilterBindGroup, filterCount } of features) {
context.setComputePipeline(isPath ? circlePipeline : bboxPipeline);
// set bind groups
computePass.setBindGroup(1, bindGroup);
computePass.setBindGroup(2, glyphFilterBindGroup);
computePass.dispatchWorkgroups(ceil(filterCount / 64));
}
// Step 3: test bboxes against each other
context.setComputePipeline(testFiltersPipeline);
computePass.dispatchWorkgroups(ceil(filterCountOffset / 64));
// finish
computePass.end();
device.queue.submit([commandEncoder.finish()]);
}
/**
* Compute the interactive glyph features to see which ones interact with the mouse
* @param feature - glyph feature guide
*/
computeInteractive(feature) {
const { context, interactivePipeline } = this;
const { interactiveBindGroup, computePass } = context;
const { bindGroup, glyphInteractiveBindGroup, filterCount } = feature;
// set pipeline
context.setComputePipeline(interactivePipeline);
// set bind groups
computePass.setBindGroup(1, bindGroup);
computePass.setBindGroup(2, glyphInteractiveBindGroup);
computePass.setBindGroup(3, interactiveBindGroup);
// draw
computePass.dispatchWorkgroups(Math.ceil(filterCount / 64));
}
/**
* Draw the glyph feature
* @param feature - glyph feature guide
*/
draw(feature) {
const { layerGuide: { viewCollisions, visible }, isPath, isIcon, bindGroup, glyphBindGroup, glyphStrokeBindGroup, source, count, offset, filterCount, filterOffset, } = feature;
if (!visible)
return;
// get current source data
const { context, pipeline, pipelineC, testRenderPipeline, testCircleRenderPipeline } = this;
const { glyphQuadBuffer, glyphFilterBuffer, glyphQuadIndexBuffer, glyphColorBuffer } = source;
const { passEncoder } = context;
// setup pipeline, bind groups, & buffers
context.setRenderPipeline(isPath ? pipelineC : pipeline);
passEncoder.setBindGroup(1, bindGroup);
for (let i = 0; i <= 4; i++)
passEncoder.setVertexBuffer(i, glyphQuadBuffer);
if (isPath) {
passEncoder.setVertexBuffer(5, glyphQuadIndexBuffer);
passEncoder.setVertexBuffer(6, glyphColorBuffer);
}
else {
passEncoder.setVertexBuffer(5, glyphQuadBuffer);
passEncoder.setVertexBuffer(6, glyphQuadIndexBuffer);
passEncoder.setVertexBuffer(7, glyphColorBuffer);
}
// draw
if (!isIcon) {
passEncoder.setBindGroup(2, glyphStrokeBindGroup);
passEncoder.draw(6, count, 0, offset);
}
passEncoder.setBindGroup(2, glyphBindGroup);
passEncoder.draw(6, count, 0, offset);
// draw test if needed
if (viewCollisions) {
context.setRenderPipeline(isPath ? testCircleRenderPipeline : testRenderPipeline);
for (let i = 0; i <= 5; i++)
passEncoder.setVertexBuffer(i, glyphFilterBuffer);
if (isPath)
passEncoder.setVertexBuffer(6, glyphFilterBuffer);
passEncoder.draw(isPath ? 64 : 8, filterCount, 0, filterOffset);
}
}
}