-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(layers) Port PathLayer to WebGPU #10113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,12 +2,14 @@ | |
| // SPDX-License-Identifier: MIT | ||
| // Copyright (c) vis.gl contributors | ||
|
|
||
| import {Layer, project32, picking, UNIT} from '@deck.gl/core'; | ||
| import {Layer, project32, color, picking, UNIT} from '@deck.gl/core'; | ||
| import {Parameters} from '@luma.gl/core'; | ||
| import {Geometry} from '@luma.gl/engine'; | ||
| import {Model} from '@luma.gl/engine'; | ||
| import PathTesselator from './path-tesselator'; | ||
|
|
||
| import {pathUniforms, PathProps} from './path-layer-uniforms'; | ||
| import source from './path-layer.wgsl'; | ||
| import vs from './path-layer-vertex.glsl'; | ||
| import fs from './path-layer-fragment.glsl'; | ||
|
|
||
|
|
@@ -135,75 +137,144 @@ export default class PathLayer<DataT = any, ExtraPropsT extends {} = {}> extends | |
| }; | ||
|
|
||
| getShaders() { | ||
| return super.getShaders({vs, fs, modules: [project32, picking, pathUniforms]}); // 'project' module added by default. | ||
| return super.getShaders({vs, fs, source, modules: [project32, color, picking, pathUniforms]}); // 'project' module added by default. | ||
| } | ||
|
|
||
| get wrapLongitude(): boolean { | ||
| return false; | ||
| } | ||
|
|
||
| getBounds(): [number[], number[]] | null { | ||
| if (this.context.device.type === 'webgpu') { | ||
| return null; | ||
| } | ||
| return this.getAttributeManager()?.getBounds(['vertexPositions']); | ||
| } | ||
|
|
||
| initializeState() { | ||
| const noAlloc = true; | ||
| const attributeManager = this.getAttributeManager(); | ||
| const enableTransitions = this.context.device.type !== 'webgpu'; | ||
| /* eslint-disable max-len */ | ||
| attributeManager!.addInstanced({ | ||
| vertexPositions: { | ||
| size: 3, | ||
| // Start filling buffer from 1 vertex in | ||
| vertexOffset: 1, | ||
| type: 'float64', | ||
| fp64: this.use64bitPositions(), | ||
| transition: ATTRIBUTE_TRANSITION, | ||
| accessor: 'getPath', | ||
| // eslint-disable-next-line @typescript-eslint/unbound-method | ||
| update: this.calculatePositions, | ||
| noAlloc, | ||
| shaderAttributes: { | ||
| instanceLeftPositions: { | ||
| vertexOffset: 0 | ||
| }, | ||
| instanceStartPositions: { | ||
| vertexOffset: 1 | ||
| if (this.context.device.type === 'webgpu') { | ||
| attributeManager!.addInstanced({ | ||
| instancePositions: { | ||
| size: 12, | ||
| type: 'float32', | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it expected we use
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On
|
||
| transition: false, | ||
| accessor: 'getPath', | ||
| // eslint-disable-next-line @typescript-eslint/unbound-method | ||
| update: this.calculateInstancePositions, | ||
| shaderAttributes: { | ||
| instanceLeftPositions: {size: 3, elementOffset: 0}, | ||
| instanceStartPositions: {size: 3, elementOffset: 3}, | ||
| instanceEndPositions: {size: 3, elementOffset: 6}, | ||
| instanceRightPositions: {size: 3, elementOffset: 9} | ||
| }, | ||
| instanceEndPositions: { | ||
| vertexOffset: 2 | ||
| noAlloc | ||
| }, | ||
| instancePositions64Low: { | ||
| size: 12, | ||
| type: 'float32', | ||
| transition: false, | ||
| accessor: 'getPath', | ||
| // eslint-disable-next-line @typescript-eslint/unbound-method | ||
| update: this.calculateInstancePositions64Low, | ||
| shaderAttributes: { | ||
| instanceLeftPositions64Low: {size: 3, elementOffset: 0}, | ||
| instanceStartPositions64Low: {size: 3, elementOffset: 3}, | ||
| instanceEndPositions64Low: {size: 3, elementOffset: 6}, | ||
| instanceRightPositions64Low: {size: 3, elementOffset: 9} | ||
| }, | ||
| instanceRightPositions: { | ||
| vertexOffset: 3 | ||
| noAlloc | ||
| }, | ||
| instanceTypes: { | ||
| size: 1, | ||
| // eslint-disable-next-line @typescript-eslint/unbound-method | ||
| update: this.calculateSegmentTypes, | ||
| noAlloc | ||
| }, | ||
| instanceStrokeWidths: { | ||
| size: 1, | ||
| accessor: 'getWidth', | ||
| transition: false, | ||
| defaultValue: 1 | ||
| }, | ||
| instanceColors: { | ||
| size: this.props.colorFormat.length, | ||
| type: 'unorm8', | ||
| accessor: 'getColor', | ||
| transition: false, | ||
| defaultValue: DEFAULT_COLOR | ||
| }, | ||
| instancePickingColors: { | ||
| size: 4, | ||
| type: 'uint8', | ||
| accessor: (object, {index, target: value}) => | ||
| this.encodePickingColor( | ||
| object && object.__source ? object.__source.index : index, | ||
| value | ||
| ) | ||
| } | ||
| }); | ||
| } else { | ||
| attributeManager!.addInstanced({ | ||
| vertexPositions: { | ||
| size: 3, | ||
| // Start filling buffer from 1 vertex in | ||
| vertexOffset: 1, | ||
| type: 'float64', | ||
| fp64: this.use64bitPositions(), | ||
| transition: enableTransitions ? ATTRIBUTE_TRANSITION : false, | ||
| accessor: 'getPath', | ||
| // eslint-disable-next-line @typescript-eslint/unbound-method | ||
| update: this.calculatePositions, | ||
| noAlloc, | ||
| shaderAttributes: { | ||
| instanceLeftPositions: { | ||
| vertexOffset: 0 | ||
| }, | ||
| instanceStartPositions: { | ||
| vertexOffset: 1 | ||
| }, | ||
| instanceEndPositions: { | ||
| vertexOffset: 2 | ||
| }, | ||
| instanceRightPositions: { | ||
| vertexOffset: 3 | ||
| } | ||
| } | ||
| }, | ||
| instanceTypes: { | ||
| size: 1, | ||
| // eslint-disable-next-line @typescript-eslint/unbound-method | ||
| update: this.calculateSegmentTypes, | ||
| noAlloc | ||
| }, | ||
| instanceStrokeWidths: { | ||
| size: 1, | ||
| accessor: 'getWidth', | ||
| transition: enableTransitions ? ATTRIBUTE_TRANSITION : false, | ||
| defaultValue: 1 | ||
| }, | ||
| instanceColors: { | ||
| size: this.props.colorFormat.length, | ||
| type: 'unorm8', | ||
| accessor: 'getColor', | ||
| transition: enableTransitions ? ATTRIBUTE_TRANSITION : false, | ||
| defaultValue: DEFAULT_COLOR | ||
| }, | ||
| instancePickingColors: { | ||
| size: 4, | ||
| type: 'uint8', | ||
| accessor: (object, {index, target: value}) => | ||
| this.encodePickingColor( | ||
| object && object.__source ? object.__source.index : index, | ||
| value | ||
| ) | ||
| } | ||
| }, | ||
| instanceTypes: { | ||
| size: 1, | ||
| type: 'uint8', | ||
| // eslint-disable-next-line @typescript-eslint/unbound-method | ||
| update: this.calculateSegmentTypes, | ||
| noAlloc | ||
| }, | ||
| instanceStrokeWidths: { | ||
| size: 1, | ||
| accessor: 'getWidth', | ||
| transition: ATTRIBUTE_TRANSITION, | ||
| defaultValue: 1 | ||
| }, | ||
| instanceColors: { | ||
| size: this.props.colorFormat.length, | ||
| type: 'unorm8', | ||
| accessor: 'getColor', | ||
| transition: ATTRIBUTE_TRANSITION, | ||
| defaultValue: DEFAULT_COLOR | ||
| }, | ||
| instancePickingColors: { | ||
| size: 4, | ||
| type: 'uint8', | ||
| accessor: (object, {index, target: value}) => | ||
| this.encodePickingColor(object && object.__source ? object.__source.index : index, value) | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| /* eslint-enable max-len */ | ||
|
|
||
| this.setState({ | ||
|
|
@@ -317,6 +388,31 @@ export default class PathLayer<DataT = any, ExtraPropsT extends {} = {}> extends | |
| } | ||
|
|
||
| protected _getModel(): Model { | ||
| const parameters = | ||
| this.context.device.type === 'webgpu' | ||
| ? ({ | ||
| depthWriteEnabled: true, | ||
| depthCompare: 'less-equal' | ||
| } satisfies Parameters) | ||
| : undefined; | ||
| const bufferLayout = | ||
| this.context.device.type === 'webgpu' | ||
| ? this.getAttributeManager()!.getBufferLayouts() | ||
| : this.getAttributeManager()! | ||
| .getBufferLayouts() | ||
| .map(layout => | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extract to helper?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On extracting the buffer layout branch to a helperAgreed. I extracted the |
||
| layout.name === 'vertexPositions' | ||
| ? { | ||
| ...layout, | ||
| attributes: (layout.attributes || []).filter( | ||
| attribute => | ||
| attribute.attribute !== 'vertexPositions' && | ||
| attribute.attribute !== 'vertexPositions64Low' | ||
| ) | ||
| } | ||
| : layout | ||
| ); | ||
|
|
||
| /* | ||
| * _ | ||
| * "-_ 1 3 5 | ||
|
|
@@ -364,14 +460,15 @@ export default class PathLayer<DataT = any, ExtraPropsT extends {} = {}> extends | |
| return new Model(this.context.device, { | ||
| ...this.getShaders(), | ||
| id: this.props.id, | ||
| bufferLayout: this.getAttributeManager()!.getBufferLayouts(), | ||
| bufferLayout, | ||
| geometry: new Geometry({ | ||
| topology: 'triangle-list', | ||
| attributes: { | ||
| indices: new Uint16Array(SEGMENT_INDICES), | ||
| positions: {value: new Float32Array(SEGMENT_POSITIONS), size: 2} | ||
| } | ||
| }), | ||
| parameters, | ||
| isInstanced: true | ||
| }); | ||
| } | ||
|
|
@@ -383,10 +480,47 @@ export default class PathLayer<DataT = any, ExtraPropsT extends {} = {}> extends | |
| attribute.value = pathTesselator.get('positions'); | ||
| } | ||
|
|
||
| protected calculateInstancePositions(attribute) { | ||
| this._calculateInterleavedInstancePositions(attribute, false); | ||
| } | ||
|
|
||
| protected calculateInstancePositions64Low(attribute) { | ||
| this._calculateInterleavedInstancePositions(attribute, true); | ||
| } | ||
|
|
||
| protected calculateSegmentTypes(attribute) { | ||
| const {pathTesselator} = this.state; | ||
|
|
||
| attribute.startIndices = pathTesselator.vertexStarts; | ||
| attribute.value = pathTesselator.get('segmentTypes'); | ||
| } | ||
|
|
||
| protected _calculateInterleavedInstancePositions(attribute, lowPart: boolean) { | ||
| const {pathTesselator} = this.state; | ||
| const value = pathTesselator.get('positions'); | ||
|
|
||
| if (!value) { | ||
| attribute.value = null; | ||
| return; | ||
| } | ||
|
|
||
| const numInstances = pathTesselator.instanceCount; | ||
| const result = new Float32Array(numInstances * 12); | ||
|
|
||
| for (let i = 0; i < numInstances; i++) { | ||
| const sourceIndex = i * 3; | ||
| const targetIndex = i * 12; | ||
| for (let vertexOffset = 0; vertexOffset < 4; vertexOffset++) { | ||
| const sourceOffset = sourceIndex + vertexOffset * 3; | ||
| const targetOffset = targetIndex + vertexOffset * 3; | ||
| for (let j = 0; j < 3; j++) { | ||
| const position = value[sourceOffset + j]; | ||
| result[targetOffset + j] = lowPart ? position - Math.fround(position) : position; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| attribute.startIndices = pathTesselator.vertexStarts; | ||
| attribute.value = result; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has the
vertexOffset: 1been missed in the WebGPU port?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On
vertexOffset: 1Good catch. It is preserved semantically in the WebGPU path.
WebGPU does not support the same packed
vertexOffsetlayout we use in the WebGL path, so the port materializes the equivalent neighbor window explicitly in_calculateInterleavedInstancePositions()using offsets[-1, 0, 1, 2]. That reproduces the padded access pattern fromvertexOffset: 1rather than dropping it.