Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/website/highway/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {GeoJsonLayer} from '@deck.gl/layers';
import {scaleLinear, scaleThreshold} from 'd3-scale';
import {CSVLoader} from '@loaders.gl/csv';
import {load} from '@loaders.gl/core';
import {Device} from '@luma.gl/core';

import {Feature, LineString, MultiLineString} from 'geojson';
import type {Color, PickingInfo, MapViewState} from '@deck.gl/core';
Expand Down Expand Up @@ -130,11 +131,13 @@ function renderTooltip({
}

export default function App({
device,
roads = DATA_URL.ROADS,
year,
accidents,
mapStyle = MAP_STYLE
}: {
device?: Device;
roads?: string | Road[];
accidents?: Accident[];
year?: number;
Expand Down Expand Up @@ -183,6 +186,7 @@ export default function App({

return (
<DeckGL
device={device}
layers={layers}
pickingRadius={5}
initialViewState={INITIAL_VIEW_STATE}
Expand Down
23 changes: 20 additions & 3 deletions modules/layers/src/path-layer/path-layer-uniforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,23 @@

import type {ShaderModule} from '@luma.gl/shadertools';

const uniformBlock = `\
const uniformBlockWGSL = /* wgsl */ `\
struct PathUniforms {
widthScale: f32,
widthMinPixels: f32,
widthMaxPixels: f32,
jointType: f32,
capType: f32,
miterLimit: f32,
billboard: f32,
widthUnits: i32,
};

@group(0) @binding(1)
var<uniform> path: PathUniforms;
`;

const uniformBlockGLSL = `\
uniform pathUniforms {
float widthScale;
float widthMinPixels;
Expand All @@ -30,8 +46,9 @@ export type PathProps = {

export const pathUniforms = {
name: 'path',
vs: uniformBlock,
fs: uniformBlock,
source: uniformBlockWGSL,
vs: uniformBlockGLSL,
fs: uniformBlockGLSL,
uniformTypes: {
widthScale: 'f32',
widthMinPixels: 'f32',
Expand Down
240 changes: 187 additions & 53 deletions modules/layers/src/path-layer/path-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has the vertexOffset: 1 been missed in the WebGPU port?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On vertexOffset: 1

Good catch. It is preserved semantically in the WebGPU path.

WebGPU does not support the same packed vertexOffset layout 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 from vertexOffset: 1 rather than dropping it.

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',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected we use float32? WebGL has float64.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On float32 vs float64

This is still the fp64 path logically, not an intentional downgrade to plain fp32.

WGSL vertex attributes here are not using float64, so on WebGPU the layer uploads:

  • float32 high parts
  • float32 64Low residuals

and reconstructs through project_position_vec3_f64(...) in the shader. So the representation is different, but it is meant to preserve the same high/low position split as the existing fp64 path.


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({
Expand Down Expand Up @@ -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 =>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract to helper?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On extracting the buffer layout branch to a helper

Agreed. I extracted the _getModel() buffer-layout branching/filtering into a small helper to keep the model setup easier to read, without broadening the refactor beyond that.

layout.name === 'vertexPositions'
? {
...layout,
attributes: (layout.attributes || []).filter(
attribute =>
attribute.attribute !== 'vertexPositions' &&
attribute.attribute !== 'vertexPositions64Low'
)
}
: layout
);

/*
* _
* "-_ 1 3 5
Expand Down Expand Up @@ -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
});
}
Expand All @@ -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;
}
}
Loading
Loading