UNPKG

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