• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / deck.gl / 30300917783

27 Jul 2026 08:03PM UTC coverage: 83.094% (-0.2%) from 83.264%
30300917783

push

github

web-flow
feat(layers) Port SolidPolygonLayer to WebGPU (#10142)

8219 of 10391 branches covered (79.1%)

Branch coverage included in aggregate %.

20 of 51 new or added lines in 4 files covered. (39.22%)

14572 of 17037 relevant lines covered (85.53%)

18833.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.56
/modules/layers/src/solid-polygon-layer/solid-polygon-layer.ts
1
// deck.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {Layer, color, project32, picking, gouraudMaterial} from '@deck.gl/core';
6
import {Model, Geometry} from '@luma.gl/engine';
7

8
// Polygon geometry generation is managed by the polygon tesselator
9
import PolygonTesselator from './polygon-tesselator';
10

11
import {solidPolygonUniforms, SolidPolygonProps} from './solid-polygon-layer-uniforms';
12
import vsTop from './solid-polygon-layer-vertex-top.glsl';
13
import vsSide from './solid-polygon-layer-vertex-side.glsl';
14
import fs from './solid-polygon-layer-fragment.glsl';
15
import {getSolidPolygonShaderWGSL} from './solid-polygon-layer.wgsl';
16

17
import type {
18
  LayerProps,
19
  LayerDataSource,
20
  Color,
21
  Material,
22
  Accessor,
23
  AccessorFunction,
24
  UpdateParameters,
25
  GetPickingInfoParams,
26
  PickingInfo,
27
  DefaultProps
28
} from '@deck.gl/core';
29
import type {PolygonGeometry} from './polygon';
30

31
type _SolidPolygonLayerProps<DataT> = {
32
  data: LayerDataSource<DataT>;
33
  /** Whether to fill the polygons
34
   * @default true
35
   */
36
  filled?: boolean;
37
  /** Whether to extrude the polygons
38
   * @default false
39
   */
40
  extruded?: boolean;
41
  /** Whether to generate a line wireframe of the polygon.
42
   * @default false
43
   */
44
  wireframe?: boolean;
45
  /**
46
   * (Experimental) If `false`, will skip normalizing the coordinates returned by `getPolygon`.
47
   * @default true
48
   */
49
  _normalize?: boolean;
50
  /**
51
   * (Experimental) This prop is only effective with `_normalize: false`.
52
   * It specifies the winding order of rings in the polygon data, one of 'CW' (clockwise) and 'CCW' (counter-clockwise)
53
   */
54
  _windingOrder?: 'CW' | 'CCW';
55

56
  /**
57
   * (Experimental) This prop is only effective with `XYZ` data.
58
   * When true, polygon tesselation will be performed on the plane with the largest area, instead of the xy plane.
59
   * @default false
60
   */
61
  _full3d?: boolean;
62

63
  /** Elevation multiplier.
64
   * @default 1
65
   */
66
  elevationScale?: number;
67

68
  /** Polygon geometry accessor. */
69
  getPolygon?: AccessorFunction<DataT, PolygonGeometry>;
70
  /** Extrusion height accessor.
71
   * @default 1000
72
   */
73
  getElevation?: Accessor<DataT, number>;
74
  /** Fill color accessor.
75
   * @default [0, 0, 0, 255]
76
   */
77
  getFillColor?: Accessor<DataT, Color>;
78
  /** Stroke color accessor.
79
   * @default [0, 0, 0, 255]
80
   */
81
  getLineColor?: Accessor<DataT, Color>;
82

83
  /**
84
   * Material settings for lighting effect. Applies if `extruded: true`
85
   *
86
   * @default true
87
   * @see https://deck.gl/docs/developer-guide/using-lighting
88
   */
89
  material?: Material;
90
};
91

92
/** Render filled and/or extruded polygons. */
93
export type SolidPolygonLayerProps<DataT = unknown> = _SolidPolygonLayerProps<DataT> & LayerProps;
94

95
const DEFAULT_COLOR = [0, 0, 0, 255] as const;
5✔
96

97
const defaultProps: DefaultProps<SolidPolygonLayerProps> = {
5✔
98
  filled: true,
99
  extruded: false,
100
  wireframe: false,
101
  _normalize: true,
102
  _windingOrder: 'CW',
103
  _full3d: false,
104

105
  elevationScale: {type: 'number', min: 0, value: 1},
106

107
  getPolygon: {type: 'accessor', value: (f: any) => f.polygon},
6✔
108
  getElevation: {type: 'accessor', value: 1000},
109
  getFillColor: {type: 'accessor', value: DEFAULT_COLOR},
110
  getLineColor: {type: 'accessor', value: DEFAULT_COLOR},
111

112
  material: true
113
};
114

115
const ATTRIBUTE_TRANSITION = {
5✔
116
  enter: (value, chunk) => {
117
    return chunk.length ? chunk.subarray(chunk.length - value.length) : value;
×
118
  }
119
};
120

121
export default class SolidPolygonLayer<DataT = any, ExtraPropsT extends {} = {}> extends Layer<
122
  ExtraPropsT & Required<_SolidPolygonLayerProps<DataT>>
123
> {
124
  static defaultProps = defaultProps;
5✔
125
  static layerName = 'SolidPolygonLayer';
5✔
126

127
  state!: {
128
    topModel?: Model;
129
    sideModel?: Model;
130
    wireframeModel?: Model;
131
    models?: Model[];
132
    numInstances: number;
133
    polygonTesselator: PolygonTesselator;
134
  };
135

136
  getShaders(type) {
137
    const ringWindingOrderCW = !this.props._normalize && this.props._windingOrder === 'CCW' ? 0 : 1;
213✔
138

139
    return super.getShaders({
213✔
140
      vs: type === 'top' ? vsTop : vsSide,
213✔
141
      fs,
142
      source: getSolidPolygonShaderWGSL(type, Boolean(ringWindingOrderCW)),
143
      defines: {
144
        RING_WINDING_ORDER_CW: ringWindingOrderCW
145
      },
146
      modules: [project32, color, gouraudMaterial, picking, solidPolygonUniforms]
147
    });
148
  }
149

150
  get wrapLongitude(): boolean {
151
    return false;
408✔
152
  }
153

154
  getBounds(): [number[], number[]] | null {
155
    return this.getAttributeManager()?.getBounds(['vertexPositions']);
38✔
156
  }
157

158
  initializeState() {
159
    const {viewport} = this.context;
164✔
160
    let {coordinateSystem} = this.props;
164✔
161
    const {_full3d} = this.props;
164✔
162
    if (viewport.isGeospatial && coordinateSystem === 'default') {
164✔
163
      coordinateSystem = 'lnglat';
110✔
164
    }
165

166
    let preproject: ((xy: number[]) => number[]) | undefined;
167

168
    if (coordinateSystem === 'lnglat') {
164✔
169
      if (_full3d) {
111!
170
        preproject = viewport.projectPosition.bind(viewport);
×
171
      } else {
172
        preproject = viewport.projectFlat.bind(viewport);
111✔
173
      }
174
    }
175

176
    this.setState({
164✔
177
      numInstances: 0,
178
      polygonTesselator: new PolygonTesselator({
179
        // Lnglat coordinates are usually projected non-linearly, which affects tesselation results
180
        // Provide a preproject function if the coordinates are in lnglat
181
        preproject,
182
        fp64: this.use64bitPositions(),
183
        IndexType: Uint32Array
184
      })
185
    });
186

187
    const attributeManager = this.getAttributeManager()!;
164✔
188
    const noAlloc = true;
164✔
189
    const isWebGPU = this.context.device.type === 'webgpu';
164✔
190

191
    /* eslint-disable max-len */
192
    attributeManager.add({
164✔
193
      indices: {
194
        size: 1,
195
        isIndexed: true,
196
        // eslint-disable-next-line @typescript-eslint/unbound-method
197
        update: this.calculateIndices,
198
        noAlloc
199
      },
200
      vertexPositions: {
201
        size: 3,
202
        type: 'float64',
203
        stepMode: 'dynamic',
204
        fp64: this.use64bitPositions(),
205
        transition: ATTRIBUTE_TRANSITION,
206
        accessor: 'getPolygon',
207
        // eslint-disable-next-line @typescript-eslint/unbound-method
208
        update: this.calculatePositions,
209
        noAlloc,
210
        ...(isWebGPU
164!
211
          ? {}
212
          : {
213
              shaderAttributes: {
214
                nextVertexPositions: {
215
                  vertexOffset: 1
216
                }
217
              }
218
            })
219
      },
220
      ...(isWebGPU
164!
221
        ? {
222
            // WebGPU cannot express WebGL's one-vertex offset view in a buffer layout.
223
            nextVertexPositions: {
224
              size: 3,
225
              type: 'float64',
226
              stepMode: 'dynamic',
227
              fp64: this.use64bitPositions(),
228
              transition: false,
229
              // eslint-disable-next-line @typescript-eslint/unbound-method
230
              update: this.calculateNextPositions,
231
              noAlloc
232
            }
233
          }
234
        : {}),
235
      [isWebGPU ? 'vertexValid' : 'instanceVertexValid']: {
164!
236
        size: 1,
237
        type: isWebGPU ? 'float32' : 'uint16',
164!
238
        stepMode: 'instance',
239
        // eslint-disable-next-line @typescript-eslint/unbound-method
240
        update: this.calculateVertexValid,
241
        noAlloc
242
      },
243
      elevations: {
244
        size: 1,
245
        stepMode: 'dynamic',
246
        transition: ATTRIBUTE_TRANSITION,
247
        accessor: 'getElevation',
248
        bufferGroup: 'solid-polygon-instance-data'
249
      },
250
      fillColors: {
251
        size: this.props.colorFormat.length,
252
        type: 'unorm8',
253
        stepMode: 'dynamic',
254
        transition: ATTRIBUTE_TRANSITION,
255
        accessor: 'getFillColor',
256
        defaultValue: DEFAULT_COLOR,
257
        bufferGroup: 'solid-polygon-instance-data'
258
      },
259
      lineColors: {
260
        size: this.props.colorFormat.length,
261
        type: 'unorm8',
262
        stepMode: 'dynamic',
263
        transition: ATTRIBUTE_TRANSITION,
264
        accessor: 'getLineColor',
265
        defaultValue: DEFAULT_COLOR,
266
        bufferGroup: 'solid-polygon-instance-data'
267
      },
268
      /** Source polygon row, including __source.index for composite data. */
269
      rowIndexes: {
270
        size: 1,
271
        type: 'uint32',
272
        stepMode: 'dynamic',
273
        accessor: (object, {index}) => (object && object.__source ? object.__source.index : index),
22,475✔
274
        bufferGroup: 'solid-polygon-instance-data'
275
      }
276
    });
277
    /* eslint-enable max-len */
278
  }
279

280
  getPickingInfo(params: GetPickingInfoParams): PickingInfo {
281
    const info = super.getPickingInfo(params);
52✔
282
    const {index} = info;
52✔
283
    const data = this.props.data as any[];
52✔
284

285
    // Check if data comes from a composite layer, wrapped with getSubLayerRow
286
    if (data[0] && data[0].__source) {
52✔
287
      // index decoded from picking color refers to the source index
288
      info.object = data.find(d => d.__source.index === index);
20✔
289
    }
290
    return info;
52✔
291
  }
292

293
  disablePickingIndex(objectIndex: number) {
294
    const data = this.props.data as any[];
3✔
295

296
    // Check if data comes from a composite layer, wrapped with getSubLayerRow
297
    if (data[0] && data[0].__source) {
3✔
298
      // index decoded from picking color refers to the source index
299
      for (let i = 0; i < data.length; i++) {
1✔
300
        if (data[i].__source.index === objectIndex) {
5✔
301
          this._disablePickingIndex(i);
3✔
302
        }
303
      }
304
    } else {
305
      super.disablePickingIndex(objectIndex);
2✔
306
    }
307
  }
308

309
  draw({uniforms}) {
310
    const {extruded, filled, wireframe, elevationScale} = this.props;
418✔
311
    const {topModel, sideModel, wireframeModel, polygonTesselator} = this.state;
418✔
312

313
    const renderUniforms: SolidPolygonProps = {
418✔
314
      extruded: Boolean(extruded),
315
      elevationScale,
316
      isWireframe: false
317
    };
318

319
    // Note - the order is important
320
    if (wireframeModel && wireframe) {
418✔
321
      wireframeModel.setInstanceCount(polygonTesselator.instanceCount - 1);
6✔
322
      wireframeModel.shaderInputs.setProps({solidPolygon: {...renderUniforms, isWireframe: true}});
6✔
323
      wireframeModel.draw(this.context.renderPass);
6✔
324
    }
325

326
    if (sideModel && filled) {
418✔
327
      sideModel.setInstanceCount(polygonTesselator.instanceCount - 1);
27✔
328
      sideModel.shaderInputs.setProps({solidPolygon: renderUniforms});
27✔
329
      sideModel.draw(this.context.renderPass);
27✔
330
    }
331

332
    if (topModel && filled) {
418✔
333
      topModel.setVertexCount(polygonTesselator.vertexCount);
400✔
334
      topModel.shaderInputs.setProps({solidPolygon: renderUniforms});
400✔
335
      topModel.draw(this.context.renderPass);
400✔
336
    }
337
  }
338

339
  updateState(updateParams: UpdateParameters<this>) {
340
    super.updateState(updateParams);
282✔
341

342
    this.updateGeometry(updateParams);
282✔
343

344
    const {props, oldProps, changeFlags} = updateParams;
282✔
345
    const attributeManager = this.getAttributeManager();
282✔
346

347
    const regenerateModels =
348
      changeFlags.extensionsChanged ||
282✔
349
      props.filled !== oldProps.filled ||
350
      props.extruded !== oldProps.extruded;
351

352
    if (regenerateModels) {
282✔
353
      this.state.models?.forEach(model => model.destroy());
175✔
354

355
      this.setState(this._getModels());
175✔
356
      attributeManager!.invalidateAll();
175✔
357
    }
358
  }
359

360
  protected updateGeometry({props, oldProps, changeFlags}: UpdateParameters<this>) {
361
    const geometryConfigChanged =
362
      changeFlags.dataChanged ||
282✔
363
      (changeFlags.updateTriggersChanged &&
364
        (changeFlags.updateTriggersChanged.all || changeFlags.updateTriggersChanged.getPolygon));
365

366
    // When the geometry config  or the data is changed,
367
    // tessellator needs to be invoked
368
    if (geometryConfigChanged) {
282✔
369
      const {polygonTesselator} = this.state;
201✔
370
      const buffers = (props.data as any).attributes || {};
201✔
371
      polygonTesselator.updateGeometry({
201✔
372
        data: props.data,
373
        normalize: props._normalize,
374
        geometryBuffer: buffers.getPolygon,
375
        // Keep derived WebGPU attributes independent of external binary accessor buffers.
376
        buffers: this.context.device.type === 'webgpu' ? {...buffers} : buffers,
201!
377
        getGeometry: props.getPolygon,
378
        positionFormat: props.positionFormat,
379
        wrapLongitude: props.wrapLongitude,
380
        // TODO - move the flag out of the viewport
381
        resolution: this.context.viewport.resolution,
382
        fp64: this.use64bitPositions(),
383
        dataChanged: changeFlags.dataChanged,
384
        full3d: props._full3d
385
      });
386

387
      this.setState({
201✔
388
        numInstances: polygonTesselator.instanceCount,
389
        startIndices: polygonTesselator.vertexStarts
390
      });
391

392
      if (!changeFlags.dataChanged) {
201✔
393
        // Base `layer.updateState` only invalidates all attributes on data change
394
        // Cover the rest of the scenarios here
395
        this.getAttributeManager()!.invalidateAll();
4✔
396
      }
397
    }
398
  }
399

400
  protected _getModels() {
401
    const {id, filled, extruded} = this.props;
175✔
402

403
    let topModel;
404
    let sideModel;
405
    let wireframeModel;
406

407
    if (filled) {
175✔
408
      const shaders = this.getShaders('top');
165✔
409
      shaders.defines = {...shaders.defines, NON_INSTANCED_MODEL: 1};
165✔
410
      let bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: false});
165✔
411
      if (this.context.device.type === 'webgpu') {
165!
412
        // Indices are bound separately, and the top model does not bind side-only attributes.
NEW
413
        bufferLayout = bufferLayout.filter(
×
414
          layout =>
NEW
415
            layout.name !== 'indices' &&
×
416
            layout.name !== 'vertexValid' &&
417
            layout.name !== 'instanceVertexValid' &&
418
            layout.name !== 'nextVertexPositions'
419
        );
420
      }
421

422
      topModel = new Model(this.context.device, {
165✔
423
        ...shaders,
424
        id: `${id}-top`,
425
        topology: 'triangle-list',
426
        bufferLayout,
427
        isIndexed: true,
428
        userData: {
429
          excludeAttributes: {
430
            vertexValid: true,
431
            instanceVertexValid: true,
432
            nextVertexPositions: true
433
          }
434
        }
435
      });
436
    }
437
    if (extruded) {
175✔
438
      let bufferLayout = this.getAttributeManager()!.getBufferLayouts({isInstanced: true});
24✔
439
      if (this.context.device.type === 'webgpu') {
24!
440
        // Indices are owned by the top model; WebGPU vertex layouts cannot include index buffers.
NEW
441
        bufferLayout = bufferLayout.filter(layout => layout.name !== 'indices');
×
442
      }
443

444
      sideModel = new Model(this.context.device, {
24✔
445
        ...this.getShaders('side'),
446
        id: `${id}-side`,
447
        bufferLayout,
448
        geometry: new Geometry({
449
          topology: 'triangle-strip',
450
          attributes: {
451
            // top right - top left - bottom right - bottom left
452
            positions: {
453
              size: 2,
454
              value: new Float32Array([1, 0, 0, 0, 1, 1, 0, 1])
455
            }
456
          }
457
        }),
458
        isInstanced: true,
459
        userData: {
460
          excludeAttributes: {indices: true}
461
        }
462
      });
463

464
      wireframeModel = new Model(this.context.device, {
24✔
465
        ...this.getShaders('side'),
466
        id: `${id}-wireframe`,
467
        bufferLayout,
468
        geometry: new Geometry({
469
          topology: 'line-strip',
470
          attributes: {
471
            // top right - top left - bottom left - bottom right
472
            positions: {
473
              size: 2,
474
              value: new Float32Array([1, 0, 0, 0, 0, 1, 1, 1])
475
            }
476
          }
477
        }),
478
        isInstanced: true,
479
        userData: {
480
          excludeAttributes: {indices: true}
481
        }
482
      });
483
    }
484

485
    return {
175✔
486
      models: [sideModel, wireframeModel, topModel].filter(Boolean),
487
      topModel,
488
      sideModel,
489
      wireframeModel
490
    };
491
  }
492

493
  protected calculateIndices(attribute) {
494
    const {polygonTesselator} = this.state;
187✔
495
    attribute.startIndices = polygonTesselator.indexStarts;
187✔
496
    attribute.value = polygonTesselator.get('indices');
187✔
497
  }
498

499
  protected calculatePositions(attribute) {
500
    const {polygonTesselator} = this.state;
187✔
501
    attribute.startIndices = polygonTesselator.vertexStarts;
187✔
502

503
    const binaryPositions = (this.props.data as any).attributes?.getPolygon;
187✔
504
    if (this.context.device.type === 'webgpu' && ArrayBuffer.isView(binaryPositions?.value)) {
187!
NEW
505
      const {value, size = 3, offset = 0, stride} = binaryPositions;
×
NEW
506
      const elementOffset = offset / value.BYTES_PER_ELEMENT;
×
NEW
507
      const elementStride = stride ? stride / value.BYTES_PER_ELEMENT : size;
×
NEW
508
      const positions = new Float64Array(polygonTesselator.instanceCount * 3);
×
509

NEW
510
      for (let vertexIndex = 0; vertexIndex < polygonTesselator.instanceCount; vertexIndex++) {
×
NEW
511
        const sourceIndex = elementOffset + vertexIndex * elementStride;
×
NEW
512
        const targetIndex = vertexIndex * 3;
×
NEW
513
        positions[targetIndex] = value[sourceIndex];
×
NEW
514
        positions[targetIndex + 1] = value[sourceIndex + 1];
×
NEW
515
        positions[targetIndex + 2] = size > 2 ? value[sourceIndex + 2] : 0;
×
516
      }
517

NEW
518
      attribute.value = positions;
×
NEW
519
      return;
×
520
    }
521

522
    attribute.value = polygonTesselator.get('positions');
187✔
523
  }
524

525
  protected calculateVertexValid(attribute) {
526
    const binaryVertexValid = (this.props.data as any).attributes?.instanceVertexValid?.value;
188✔
527
    const vertexValid =
528
      this.context.device.type === 'webgpu' && binaryVertexValid
188!
529
        ? binaryVertexValid
530
        : this.state.polygonTesselator.get('vertexValid');
531
    attribute.value =
188✔
532
      this.context.device.type === 'webgpu' && vertexValid
376!
533
        ? Float32Array.from(vertexValid)
534
        : vertexValid;
535
  }
536

537
  protected calculateNextPositions(attribute) {
NEW
538
    const {polygonTesselator} = this.state;
×
NEW
539
    const attributes = this.getAttributeManager()!.getAttributes();
×
NEW
540
    const positions = attributes.vertexPositions.value;
×
541
    const vertexValid =
NEW
542
      (this.props.data as any).attributes?.instanceVertexValid?.value ||
×
543
      attributes.vertexValid?.value ||
544
      polygonTesselator.get('vertexValid');
NEW
545
    attribute.startIndices = polygonTesselator.vertexStarts;
×
546

NEW
547
    if (!positions) {
×
NEW
548
      attribute.value = positions;
×
NEW
549
      return;
×
550
    }
551

NEW
552
    const vertexCount = positions.length / 3;
×
NEW
553
    const nextPositions = new (positions.constructor as typeof Float32Array)(positions.length);
×
NEW
554
    for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
×
NEW
555
      const sourceIndex = vertexIndex * 3;
×
556
      const nextSourceIndex =
NEW
557
        vertexValid?.[vertexIndex] && vertexIndex + 1 < vertexCount ? sourceIndex + 3 : sourceIndex;
×
NEW
558
      for (let componentIndex = 0; componentIndex < 3; componentIndex++) {
×
NEW
559
        nextPositions[sourceIndex + componentIndex] = positions[nextSourceIndex + componentIndex];
×
560
      }
561
    }
562

NEW
563
    attribute.value = nextPositions;
×
564
  }
565
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc