UNPKG

s2maps-gpu

Version:

S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.

477 lines (476 loc) 20.8 kB
import encodeLayerAttribute from 'style/encodeLayerAttribute.js'; import shaderCode from '../shaders/fill.wgsl'; const SHADER_BUFFER_LAYOUT = [ { // position arrayStride: 4 * 2, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x2' }], }, { // code arrayStride: 4, attributes: [{ shaderLocation: 1, offset: 0, format: 'uint32' }], }, ]; /** Fill Feature is a standalone fill render storage unit that can be drawn to the GPU */ export class FillFeature { workflow; layerGuide; maskLayer; source; count; offset; tile; featureCodeBuffer; fillTexturePositions; fillInteractiveBuffer; featureCode; parent; type = 'fill'; bindGroup; fillPatternBindGroup; fillInteractiveBindGroup; /** * @param workflow - the fill workflow * @param layerGuide - the layer guide for this feature * @param maskLayer - whether or not the layer is a mask type or not * @param source - the fill or mask source * @param count - the number of points * @param offset - the offset of the points * @param tile - the tile that the feature is drawn on * @param featureCodeBuffer - the encoded feature code that tells the GPU how to compute it's properties * @param fillTexturePositions - the fill texture positions * @param fillInteractiveBuffer - if interactive, this buffer helps the GPU compute interactivity * @param featureCode - the encoded feature code that tells the GPU how to compute it's properties * @param parent - the parent tile if applicable */ constructor(workflow, layerGuide, maskLayer, source, count, offset, tile, featureCodeBuffer, fillTexturePositions, fillInteractiveBuffer, featureCode = [0], parent) { this.workflow = workflow; this.layerGuide = layerGuide; this.maskLayer = maskLayer; this.source = source; this.count = count; this.offset = offset; this.tile = tile; this.featureCodeBuffer = featureCodeBuffer; this.fillTexturePositions = fillTexturePositions; this.fillInteractiveBuffer = fillInteractiveBuffer; this.featureCode = featureCode; this.parent = parent; this.fillPatternBindGroup = tile.context.createPatternBindGroup(fillTexturePositions); this.bindGroup = this.#buildBindGroup(); if (fillInteractiveBuffer !== undefined) this.fillInteractiveBindGroup = this.#buildInteractiveBindGroup(); } /** Draw the feature */ draw() { const { maskLayer, tile, parent, workflow } = this; const { mask } = parent ?? tile; workflow.context.setStencilReference(tile.tmpMaskID); if (maskLayer) workflow.drawMask(mask, this); else workflow.draw(this); } /** Compute the feature's interactivity with the mouse */ compute() { this.workflow.computeInteractive(this); } /** Update the shared texture's bind groups */ updateSharedTexture() { const { context } = this.workflow; this.fillPatternBindGroup = context.createPatternBindGroup(this.fillTexturePositions); } /** Destroy and cleanup the feature */ destroy() { this.featureCodeBuffer.destroy(); this.fillTexturePositions.destroy(); this.fillInteractiveBuffer?.destroy(); } /** * Duplicate this point * @param tile - the tile that is being duplicated * @param parent - the parent tile if applicable * @returns the duplicated feature */ duplicate(tile, parent) { const { workflow, layerGuide, maskLayer, source, count, offset, featureCodeBuffer, fillInteractiveBuffer, featureCode, fillTexturePositions, } = this; const { context } = this.workflow; const cE = context.device.createCommandEncoder(); const newFeatureCodeBuffer = context.duplicateGPUBuffer(featureCodeBuffer, cE); const newFillTexturePositions = context.duplicateGPUBuffer(fillTexturePositions, cE); const newFillInteractiveBuffer = fillInteractiveBuffer !== undefined ? context.duplicateGPUBuffer(fillInteractiveBuffer, cE) : undefined; context.device.queue.submit([cE.finish()]); return new FillFeature(workflow, layerGuide, maskLayer, source, count, offset, tile, newFeatureCodeBuffer, newFillTexturePositions, newFillInteractiveBuffer, featureCode, parent); } /** * Build the bind group for the fill feature * @returns the GPU Bind Group for the fill feature */ #buildBindGroup() { const { workflow, tile, parent, layerGuide, featureCodeBuffer } = this; const { context } = workflow; const { mask } = parent ?? tile; const { layerBuffer, layerCodeBuffer } = layerGuide; return context.buildGroup('Fill Feature BindGroup', context.featureBindGroupLayout, [ mask.uniformBuffer, mask.positionBuffer, layerBuffer, layerCodeBuffer, featureCodeBuffer, ]); } /** * Build an interactive bind group for this feature * @returns the GPU Bind Group */ #buildInteractiveBindGroup() { const { workflow, tile, source, fillInteractiveBuffer } = this; if (fillInteractiveBuffer === undefined) throw new Error('Fill Interactive Buffer is undefined'); if (!('idBuffer' in source)) throw new Error('Source does not have an idBuffer'); return tile.context.buildGroup('Fill Interactive BindGroup', workflow.fillInteractiveBindGroupLayout, [fillInteractiveBuffer, source.vertexBuffer, source.indexBuffer, source.idBuffer]); } } /** Fill Workflow */ export default class FillWorkflow { context; layerGuides = new Map(); interactivePipeline; maskPipeline; fillPipeline; maskFillPipeline; invertPipeline; #shaderModule; #pipelineLayout; fillInteractiveBindGroupLayout; /** @param context - The WebGPU context */ constructor(context) { this.context = context; } /** Setup the workflow */ async setup() { const { device, frameBindGroupLayout, featureBindGroupLayout, maskPatternBindGroupLayout } = this.context; this.#shaderModule = device.createShaderModule({ label: 'Fill Shader Module', code: shaderCode, }); this.#pipelineLayout = device.createPipelineLayout({ label: 'Fill Pipeline Layout', bindGroupLayouts: [frameBindGroupLayout, featureBindGroupLayout, maskPatternBindGroupLayout], }); this.maskPipeline = await this.#getPipeline('mask'); this.fillPipeline = await this.#getPipeline('fill'); this.maskFillPipeline = await this.#getPipeline('mask-fill'); this.invertPipeline = await this.#getPipeline('invert'); this.interactivePipeline = await this.#getComputePipeline(); } /** Destroy and cleanup the workflow */ destroy() { for (const { layerBuffer, layerCodeBuffer } of this.layerGuides.values()) { layerBuffer.destroy(); layerCodeBuffer.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 { pattern } = layer; let { color, opacity, patternFamily, patternMovement, invert, opaque, interactive, cursor } = layer; invert = invert ?? false; opaque = opaque ?? false; interactive = interactive ?? false; cursor = cursor ?? 'default'; // 1) build definition color = color ?? 'rgb(0, 0, 0)'; opacity = opacity ?? 1; patternFamily = patternFamily ?? '__images'; patternMovement = patternMovement ?? false; const layerDefinition = { ...layerBase, type: 'fill', // paint color, opacity, // layout pattern, patternFamily, patternMovement, // propreties invert, interactive, opaque, cursor, }; // 2) build the layerCode const layerCode = []; for (const paint of [color, opacity]) { 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, invert, opaque, pattern: pattern !== undefined, interactive, visible, }); return layerDefinition; } /** * given a set of layerIndexes that use Masks and the tile of interest * @param definition - layer definition that uses masks * @param tile - the tile that needs a mask */ buildMaskFeature(definition, tile) { const { context } = this; const { zoom, mask } = tile; const { layerIndex, minzoom, maxzoom } = definition; // not in the zoom range, ignore if (zoom < minzoom || zoom > maxzoom) return; const layerGuide = this.layerGuides.get(layerIndex); if (layerGuide === undefined) return; const featureCodeBuffer = context.buildGPUBuffer('Feature Code Buffer', new Float32Array([0]), GPUBufferUsage.STORAGE); const fillTexturePositions = context.buildGPUBuffer('Fill Texture Positions', new Float32Array([0, 0, 0, 0, 0]), GPUBufferUsage.UNIFORM); const feature = new FillFeature(this, layerGuide, true, mask, mask.count, mask.offset, tile, featureCodeBuffer, fillTexturePositions); tile.addFeatures([feature]); } /** * Build the source fill data into fill features * @param fillData - the input fill data * @param tile - the tile we are building the features for */ buildSource(fillData, tile) { const { context } = this; const { vertexBuffer, indexBuffer, idBuffer, codeTypeBuffer, featureGuideBuffer } = fillData; // prep buffers const source = { type: 'fill', vertexBuffer: context.buildGPUBuffer('Fill Vertex Buffer', new Float32Array(vertexBuffer), GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE), indexBuffer: context.buildGPUBuffer('Fill Index Buffer', new Uint32Array(indexBuffer), GPUBufferUsage.INDEX | GPUBufferUsage.STORAGE), idBuffer: context.buildGPUBuffer('Fill ID Buffer', new Uint32Array(idBuffer), GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE), codeTypeBuffer: context.buildGPUBuffer('Fill Code Type Buffer', new Uint32Array(codeTypeBuffer), GPUBufferUsage.VERTEX), /** destroy the fill source */ destroy: () => { const { vertexBuffer, indexBuffer, idBuffer, codeTypeBuffer } = source; vertexBuffer.destroy(); indexBuffer.destroy(); idBuffer.destroy(); codeTypeBuffer.destroy(); }, }; // build features this.#buildFeatures(source, tile, new Float32Array(featureGuideBuffer)); } /** * Build fill features from input fill source * @param source - the input fill 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) { // grab the size, layerIndex, count, and offset, and update the index const [layerIndex, count, offset, encodingSize] = featureGuideArray.slice(i, i + 4); i += 4; // build featureCode let featureCode = [0]; if (encodingSize > 0) featureCode = [...featureGuideArray.slice(i, i + encodingSize)]; // update index i += encodingSize; // get the pattern const [texX, texY, texW, texH, patternMovement] = featureGuideArray.slice(i, i + 5); i += 5; const layerGuide = this.layerGuides.get(layerIndex); if (layerGuide === undefined) continue; const featureCodeBuffer = context.buildGPUBuffer('Feature Code Buffer', new Float32Array(featureCode), GPUBufferUsage.STORAGE); const fillTexturePositions = context.buildGPUBuffer('Fill Texture Positions', new Float32Array([texX, texY, texW, texH, patternMovement]), GPUBufferUsage.UNIFORM); const fillInteractiveBuffer = context.buildGPUBuffer('Fill Interactive Buffer', new Uint32Array([offset / 3, count / 3]), GPUBufferUsage.UNIFORM); features.push(new FillFeature(this, layerGuide, false, source, count, offset, tile, featureCodeBuffer, fillTexturePositions, fillInteractiveBuffer, featureCode)); } tile.addFeatures(features); } /** * Get the associating pipeline with the input type * 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 type - pipeline type (fill, mask, invert, mask-fill) * @returns the pipeline */ async #getPipeline(type) { const { context } = this; const { device, format, defaultBlend, sampleCount, projection } = context; const invert = type === 'invert'; const mask = type === 'mask'; const maskFill = type === 'mask-fill'; const stencilState = { compare: mask ? 'always' : 'equal', failOp: 'keep', depthFailOp: 'keep', passOp: 'replace', }; return await device.createRenderPipelineAsync({ label: `Fill ${type} Pipeline`, layout: this.#pipelineLayout, vertex: { module: this.#shaderModule, entryPoint: 'vMain', buffers: SHADER_BUFFER_LAYOUT, }, fragment: { module: this.#shaderModule, entryPoint: 'fMain', targets: [ { format, writeMask: mask || invert ? 0 : GPUColorWrite.ALL, blend: defaultBlend, }, ], }, primitive: { topology: mask || maskFill ? 'triangle-strip' : 'triangle-list', cullMode: projection === 'S2' ? 'back' : 'front', stripIndexFormat: mask || maskFill ? 'uint32' : undefined, }, multisample: { count: sampleCount }, depthStencil: { depthWriteEnabled: !mask, depthCompare: mask ? 'always' : 'less', format: 'depth24plus-stencil8', stencilFront: stencilState, stencilBack: stencilState, stencilReadMask: 0xffffffff, stencilWriteMask: 0xffffffff, }, }); } /** * Build a compute pipeline to check for interactive fill data that interects with the mouse * 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 * @returns the GPU compute pipeline */ async #getComputePipeline() { const { context } = this; const { device, frameBindGroupLayout, featureBindGroupLayout, interactiveBindGroupLayout } = context; this.fillInteractiveBindGroupLayout = device.createBindGroupLayout({ label: 'Fill Interactive BindGroupLayout', entries: [ { binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } }, // interactive offset & count { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // positions { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // indexes { binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } }, // ids ], }); const layout = device.createPipelineLayout({ label: 'Fill Interactive Pipeline Layout', bindGroupLayouts: [ frameBindGroupLayout, featureBindGroupLayout, this.fillInteractiveBindGroupLayout, interactiveBindGroupLayout, ], }); return await device.createComputePipelineAsync({ label: 'Fill Interactive Pipeline', layout, compute: { module: this.#shaderModule, entryPoint: 'interactive' }, }); } /** * Draw a fill feature to the GPU * @param feature - fill feature guide */ draw(feature) { const { context, invertPipeline, fillPipeline } = this; // get current source data const { passEncoder } = context; const { tile, parent, bindGroup, fillPatternBindGroup, source, count, offset, layerGuide: { visible, invert }, } = feature; const { vertexBuffer, indexBuffer, codeTypeBuffer } = source; const pipeline = invert ? invertPipeline : fillPipeline; const { mask } = parent ?? tile; // if the layer is not visible, move on if (!visible) return; // setup pipeline, bind groups, & buffers context.setRenderPipeline(pipeline); passEncoder.setBindGroup(1, bindGroup); passEncoder.setBindGroup(2, fillPatternBindGroup); passEncoder.setVertexBuffer(0, vertexBuffer); passEncoder.setIndexBuffer(indexBuffer, 'uint32'); passEncoder.setVertexBuffer(1, codeTypeBuffer); // draw passEncoder.drawIndexed(count, 1, offset); if (invert) this.drawMask(mask, feature); } /** * Draw a mask to the GPU * @param mask - mask source * @param feature - fill feature guide */ drawMask(mask, feature) { const { vertexBuffer, indexBuffer, codeTypeBuffer, bindGroup, fillPatternBindGroup, count, offset, } = mask; const { context, maskPipeline, maskFillPipeline } = this; // if the layer is not visible, move on if (feature?.layerGuide?.visible === false) return; // get current source data const { passEncoder } = context; // setup pipeline, bind groups, & buffers this.context.setRenderPipeline(feature === undefined ? maskPipeline : maskFillPipeline); passEncoder.setBindGroup(1, feature?.bindGroup ?? bindGroup); passEncoder.setBindGroup(2, feature?.fillPatternBindGroup ?? fillPatternBindGroup); passEncoder.setVertexBuffer(0, vertexBuffer); passEncoder.setIndexBuffer(indexBuffer, 'uint32'); passEncoder.setVertexBuffer(1, codeTypeBuffer); // draw passEncoder.drawIndexed(count, 1, offset); } /** * Compute the interactive fill features in current view * @param feature - fill feature guide */ computeInteractive(feature) { const { layerGuide: { visible }, bindGroup, fillInteractiveBindGroup, count, } = feature; if (!visible || fillInteractiveBindGroup === undefined) return; const { computePass, interactiveBindGroup } = this.context; this.context.setComputePipeline(this.interactivePipeline); // set bind group & draw computePass.setBindGroup(1, bindGroup); computePass.setBindGroup(2, fillInteractiveBindGroup); computePass.setBindGroup(3, interactiveBindGroup); computePass.dispatchWorkgroups(Math.ceil(count / 3 / 64)); } }