From fb2e548e74de2e7ed4cf129a20858ebd829f969d Mon Sep 17 00:00:00 2001 From: Ib Green Date: Thu, 30 Jul 2026 16:04:22 -0400 Subject: [PATCH 1/2] test: expand WebGPU coverage and CI --- .github/workflows/test.yml | 2 + .../developer-guide/wind-showcase.md | 2 +- docs/webgpu.md | 25 ++-- docs/whats-new.md | 18 ++- examples/geo-layers/wind/app.ts | 26 ++-- .../test/webgpu-layers.browser.spec.ts | 79 +++++++++- modules/geo-layers/README.md | 6 +- .../test/webgpu-layers.browser.spec.ts | 135 ++++++++++++++++++ .../wind-layer/wind-layers.browser.spec.ts | 26 +++- .../layers/test/webgpu-layers.browser.spec.ts | 4 +- .../layers/trace-webgpu.browser.spec.ts | 18 ++- vitest.config.ts | 27 ++-- website/src/components/docs/webgpu-support.js | 33 ++++- website/src/examples/geo-layers/wind.mdx | 5 +- 14 files changed, 343 insertions(+), 63 deletions(-) create mode 100644 modules/geo-layers/test/webgpu-layers.browser.spec.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f94beb95..9296158d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -260,6 +260,8 @@ jobs: - name: Run headless browser tests run: yarn test-headless + env: + DECK_GL_COMMUNITY_SOFTWARE_WEBGPU: 'true' website-build: runs-on: ubuntu-22.04 diff --git a/docs/modules/geo-layers/developer-guide/wind-showcase.md b/docs/modules/geo-layers/developer-guide/wind-showcase.md index 11b5ed29..abfd2cd5 100644 --- a/docs/modules/geo-layers/developer-guide/wind-showcase.md +++ b/docs/modules/geo-layers/developer-guide/wind-showcase.md @@ -196,7 +196,7 @@ buffers on every slider event. | `WindLayer` | Supported | Supported | Native GLSL/WGSL filled arrows and portable line shafts. | | `ElevationLayer` | Supported | Blocked | Image-derived mountain terrain depends on upstream `TerrainLayer`. | | `DelaunayCoverLayer` | Supported | Supported | Native GLSL/WGSL station-triangulated surface. | -| Complete original showcase | Supported | In progress | WebGPU uses station terrain while image-derived mountains remain blocked. | +| Complete original showcase | Supported | In progress | WebGPU renders state boundaries and uses station terrain while image-derived mountains remain blocked. | See the full [WebGPU compatibility matrix](/docs/webgpu). diff --git a/docs/webgpu.md b/docs/webgpu.md index 2d6d4d2e..b46daed9 100644 --- a/docs/webgpu.md +++ b/docs/webgpu.md @@ -27,7 +27,7 @@ deck.gl-community is adding WebGPU support incrementally while continuing to sup | `@deck.gl-community/trace-layers` | `TraceGraphLayer` and `TracePreparedStateLayer` | ✅ | ✅ | Browser-verified span blocks, backgrounds, outlines, row separators, fast labels, and straight dependency markers. | | `@deck.gl-community/trace-layers` | `TraceProcessLayer` | ✅ | ✅ | Automatically selects WebGPU-compatible binary blocks, fast span and overflow labels, and straight dependency rendering. | | `@deck.gl-community/trace-layers` | Counter sparklines | ✅ | ✅ | Preserves every sparkline vertex as a dual-backend `LineLayer` segment. | -| `@deck.gl-community/trace-layers` | Curved dependencies and dashed separators | ✅ | 🚧 | Curves depend on upstream `ArcLayer`; portable separators use solid horizontal lines. | +| `@deck.gl-community/trace-layers` | Curved dependencies and row separators | ✅ | ✅ | Browser-verified upstream `ArcLayer` curves and portable solid `LineLayer` separators. | | `@deck.gl-community/trace-layers` | `DeckTraceGraph` and Tracevis overview | ✅ | 🚧 | Managed devices and backend-neutral timing are available; complete legend, minimap, picking, and application validation remains in progress. | | `@deck.gl-community/graph-layers` | `GraphLayer`, `EdgeLayer`, and node layers | ✅ | 🚧 | Static path edges, arrow decorators, and rounded nodes are portable; complete graph styling, images, labels, layouts, and picking still require end-to-end validation. | | `@deck.gl-community/graph-layers` | `RoundedRectangleLayer` | ✅ | ✅ | Rounded corners are CPU-tessellated and rendered with upstream dual-backend `PolygonLayer`. | @@ -38,14 +38,15 @@ deck.gl-community is adding WebGPU support incrementally while continuing to sup | `@deck.gl-community/geo-layers` | `WindLayer` | ✅ | ✅ | Native WGSL/GLSL filled-arrow triangles and portable line shafts and arrowheads. | | `@deck.gl-community/geo-layers` | `ElevationLayer` | ✅ | ❌ | Image-derived mountain terrain depends on upstream `TerrainLayer`; skipped safely on WebGPU. | | `@deck.gl-community/geo-layers` | `DelaunayCoverLayer` | ✅ | ✅ | Native WGSL/GLSL station triangles, elevation scaling, and height-based coloring. | -| `@deck.gl-community/geo-layers` | Complete Wind Map showcase | ✅ | 🚧 | GPU particles, arrows, labels, and station terrain are portable; image terrain and map boundaries remain upstream-dependent. | -| `@deck.gl-community/geo-layers` | Tile and global-grid layers | ✅ | 🚧 | Validate upstream sublayers, tile formats, and picking. | -| `@deck.gl-community/arrow-layers` | `GeoArrowPathLayer` and `GeoArrowSolidPolygonLayer` | ✅ | ✅ | Browser-verified zero-copy binary path and polygon attributes. | -| `@deck.gl-community/arrow-layers` | Remaining GeoArrow layers | ✅ | 🚧 | Validate each upstream renderer; `GeoArrowTripsLayer` still has custom shader work. | +| `@deck.gl-community/geo-layers` | Complete Wind Map showcase | ✅ | 🚧 | GPU particles, arrows, labels, state boundaries, and station terrain are portable; image terrain remains upstream-dependent. | +| `@deck.gl-community/geo-layers` | `GlobalGridLayer` and `TileGridLayer` borders | ✅ | ✅ | Browser-verified upstream polygon and path renderers using local grid and tile data. | +| `@deck.gl-community/geo-layers` | `SharedTile2DLayer` and `TileSourceLayer` | ✅ | 🚧 | Validate tile formats, texture upload, labels, and picking. | +| `@deck.gl-community/arrow-layers` | Column, heatmap, path, point-cloud, polygon, scatterplot, and solid-polygon layers | ✅ | ✅ | Browser-verified Arrow binary attributes through the corresponding upstream renderers. | +| `@deck.gl-community/arrow-layers` | Arc, H3, text, and trips layers | ✅ | 🚧 | Arc binary attributes exceed the baseline eight-vertex-buffer limit; the others require upstream renderer validation or remaining custom shader work. | | `@deck.gl-community/editable-layers` | GeoJSON paths, polygons, and edit handles | ✅ | ✅ | Browser-verified `EditableGeoJsonLayer` rendering in `ModifyMode`, including the WebGPU picking-width shader path. | | `@deck.gl-community/editable-layers` | Editing and selection interactions | ✅ | 🚧 | Pointer, drag, snapping, and selection behavior still require browser interaction coverage on WebGPU. | | `@deck.gl-community/basemap-layers` | `BasemapLayer` | ✅ | 🚧 | Support depends on the selected style's polygon, path, and label sublayers. | -| `@deck.gl-community/three` | `TreeLayer` | ✅ | ❌ | Depends on the external Three.js renderer and canvas integration. | +| `@deck.gl-community/three` | `TreeLayer` | ✅ | 🚧 | Three.js builds the geometry, but deck.gl renders it through upstream `SimpleMeshLayer`, whose WebGPU port is still pending. | | `@deck.gl-community/leaflet` | Leaflet map overlay | ✅ | ❌ | A host-owned WebGL context cannot be switched to WebGPU. | | `@deck.gl-community/bing-maps` | Bing Maps overlay | ✅ | ❌ | A host-owned WebGL context cannot be switched to WebGPU. | | `@deck.gl-community/widgets` | `DeviceManagerController` and `DeviceTabsWidget` | ✅ | ✅ | Selects and attaches an independently managed real rendering device. | @@ -124,14 +125,14 @@ routes now render through the selected backend. | Existing reference | `SkyboxLayer` | Provides native WGSL and GLSL sources, portable cubemap bindings, and a switchable skybox example. | | First wave | `BlockLayer`, `DependencyArrowLayer` marker geometry, `HorizonGraphLayer`, and `MultiHorizonGraphLayer` | Native WGSL and existing GLSL are maintained together. Stacked horizon dividers use the upstream dual-backend `LineLayer`; the website injects real WebGPU/WebGL2 device selection into the skybox, path, block, and horizon examples. | | Wind showcase | `ParticleLayer`, wind-field utilities, `WindLayer`, and `DelaunayCoverLayer` | WebGL2 transform-feedback, WebGPU compute, native arrow triangles, and station-surface rendering are browser-verified. Image-based mountain terrain still depends on upstream `TerrainLayer`. | -| Path and polygon unblock | `PathOutlineLayer`, `PathMarkerLayer`, `DependencyArrowLayer`, `TimelineLayer` geometry, GeoArrow paths and polygons, editable GeoJSON, and static graph geometry | deck.gl 9.4 alpha.2 supplies dual-backend path and polygon shaders. Community layers use them directly, with a local WGSL dash plugin until `PathStyleExtension` gains native WGSL. | -| Second wave | `FastTextLayer` | Add a small WGSL compatibility shader to the existing glyph layer, following luma.gl `master`'s `TextRenderer` and Arrow text patterns while retaining the published luma.gl 9.3 dependency line. | -| Trace rendering | `TraceGraphLayer`, `TracePreparedStateLayer`, `TraceProcessLayer`, and counter sparklines | Reuse shared dual-backend blocks, fast text, and lines; preserve external float32 trace attributes; automatically select portable text and straight dependency routes on WebGPU. | -| Upstream v10 | `TextRenderer` and Arrow text | Replace the compatibility path with luma.gl's optimized text and Arrow renderers after their currently private v10 modules are published. | +| Path and polygon unblock | `PathOutlineLayer`, `PathMarkerLayer`, `DependencyArrowLayer`, `TimelineLayer` geometry, GeoArrow renderers, editable GeoJSON, global-grid and tile-border layers, and static graph geometry | deck.gl 9.4 alpha.2 supplies dual-backend path and polygon shaders. Community layers use them directly, with a local WGSL dash plugin until `PathStyleExtension` gains native WGSL. | +| Fast text | `FastTextLayer` | The existing bitmap/SDF glyph layer now has native WGSL and remains available while upstream `TextLayer` WebGPU support stabilizes. | +| Trace rendering | `TraceGraphLayer`, `TracePreparedStateLayer`, `TraceProcessLayer`, and counter sparklines | Reuse shared dual-backend blocks, fast text, arcs, and lines; preserve external float32 trace attributes; validate straight and curved dependency routes on WebGPU. | +| Upstream follow-ups | `TextLayer`, `TripsLayer`, `SimpleMeshLayer`, and specialized geo-cell layers | Adopt and validate upstream WebGPU implementations as they become available; keep local fallbacks narrowly scoped. | | Graph geometry | `RoundedRectangleLayer`, `PathEdgeLayer`, and `EdgeArrowLayer` | Replace the fragment-only rounded rectangle and mesh arrowhead with CPU-tessellated polygons, then validate static path and polygon geometry on both backends. Full `GraphLayer` integration remains in progress. | | Dedicated redesign | `FlowPathLayer` and animated graph flows | The current transform-feedback implementation is incomplete and WebGL-specific. Replace it with a backend-neutral animation or compute design; do not treat shader translation alone as a port. | -| Subsequent validation | Remaining Arrow, editable interactions, geospatial tiles, and basemap layers | Validate remaining upstream sublayers, picking interactions, tile and texture formats, and each demonstrated example independently. | -| Host-dependent integrations | Three.js, Leaflet, Bing Maps, and external map renderers | Support depends on the host renderer and canvas ownership. A host-owned WebGL context cannot be switched to WebGPU by adding device tabs. | +| Subsequent validation | Arrow arc/H3/text/trips, editable interactions, geospatial tile sources, and basemap layers | Reduce Arrow arc vertex-buffer usage; validate remaining upstream sublayers, picking interactions, tile and texture formats, and each demonstrated example independently. | +| Host-dependent integrations | Leaflet, Bing Maps, and external map renderers | Support depends on the host renderer and canvas ownership. A host-owned WebGL context cannot be switched to WebGPU by adding device tabs. | The skybox map example also composes a basemap. `SkyboxLayer` itself has native WebGPU shaders, while complete basemap compatibility remains subject to the downstream GeoJSON, polygon, path, and label sublayers used by the selected style. diff --git a/docs/whats-new.md b/docs/whats-new.md index 894d5a4c..a3f50c92 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -30,18 +30,24 @@ Scope tracked in the [v9.4 milestone](https://github.com/visgl/deck.gl-community `float32-filterable`. - `RoundedRectangleLayer`, `PathEdgeLayer`, and `EdgeArrowLayer`: replaced WebGL-only graph primitives with CPU-tessellated polygons and browser-verified upstream path rendering. -- GeoArrow path and solid-polygon layers now have browser coverage for zero-copy binary attributes - on WebGL2 and WebGPU. +- GeoArrow column, heatmap, path, point-cloud, polygon, scatterplot, and solid-polygon layers now + have browser coverage for Arrow binary attributes on WebGL2 and WebGPU. The compatibility matrix + records the remaining baseline vertex-buffer limit in `GeoArrowArcLayer`. - `EditableGeoJsonLayer`: browser-verified polygon, path, and edit-handle rendering on WebGPU, including its picking-width shader customization. -- `TraceGraphLayer`, `TracePreparedStateLayer`, and `TraceProcessLayer`: ported trace backgrounds, binary span blocks, outlines, labels, overflow labels, separators, and straight dependencies by reusing dual-backend community layers. +- `TraceGraphLayer`, `TracePreparedStateLayer`, and `TraceProcessLayer`: ported trace backgrounds, binary span blocks, outlines, labels, overflow labels, separators, and straight and curved dependencies by reusing dual-backend community layers. - Trace counter sparklines now preserve their full geometry using portable `LineLayer` segments. +- `GlobalGridLayer`, `TileGridLayer` borders, and the wind showcase's state boundaries now have + local-data browser coverage on WebGL2 and WebGPU. +- Headless CI enables Chromium's software WebGPU adapter, so native WebGPU validation tests run + even on runners without hardware adapters instead of being skipped. - Every website gallery example and live layer-reference example now receives a standalone `DeviceTabsWidget` from the shared imperative host, with an independent device manager, WebGPU preference, WebGL2 fallback, renderer remounting, and preserved view state. - Every documentation page now displays a generated WebGPU compatibility badge linked to the support matrix. Verified and blocked layer pages override their package-level status, and - backend-neutral APIs are marked not applicable. + backend-neutral data, grid-adapter, layout, loader, mode, and trace-model APIs are marked not + applicable. ### `@deck.gl-community/geo-layers` @@ -87,7 +93,9 @@ Scope tracked in the [v9.4 milestone](https://github.com/visgl/deck.gl-community - `TraceEngine` now owns mounted trace selection, collapse, layout, prepared scene, and diagnostics state below React; `DeckTraceGraph` renders an engine instead of receiving the full controlled trace-view state directly. - `TraceChunkStore` now exposes source-owned graph-data materialization and retained-state diagnostics for incremental windows, while `TraceStoreLayer` composes that flow as a low-level deck layer. - `DeckTraceGraph`, deck controllers/layers, trace loaders, and the Tracevis React surface now consume shared `@deck.gl-community/panels`, `@deck.gl-community/widgets`, and `@deck.gl-community/infovis-layers` APIs instead of vendored upstream copies. -- Main trace rendering, binary span geometry, straight dependency arrows, row separators, span borders, overflow labels, and counter sparklines now work on WebGPU and WebGL2 using shared portable layers. +- Main trace rendering, binary span geometry, straight and curved dependency arrows, row + separators, span borders, overflow labels, and counter sparklines now work on WebGPU and WebGL2 + using shared portable layers. - Managed trace viewers accept caller-owned rendering devices and backend-neutral GPU-timing callbacks. - Added the website Tracevis example for exercising trace loading, selection, catalog, and Studio visualization settings workflows. diff --git a/examples/geo-layers/wind/app.ts b/examples/geo-layers/wind/app.ts index a622f00b..d277a0c3 100644 --- a/examples/geo-layers/wind/app.ts +++ b/examples/geo-layers/wind/app.ts @@ -451,20 +451,18 @@ export function mountWindExample( elevationScale: ELEVATION_SCALE, opacity: 0.32 }), - boundaries: - !isWebgpu && - new GeoJsonLayer({ - id: 'wind-state-boundaries', - data: US_STATE_BOUNDARIES, - filled: false, - stroked: true, - getLineColor: [177, 188, 205, 95], - getLineWidth: 1, - lineWidthUnits: 'pixels', - lineWidthMinPixels: 0.65, - parameters: {depthCompare: 'always', depthWriteEnabled: false}, - pickable: false - }), + boundaries: new GeoJsonLayer({ + id: 'wind-state-boundaries', + data: US_STATE_BOUNDARIES, + filled: false, + stroked: true, + getLineColor: [177, 188, 205, 95], + getLineWidth: 1, + lineWidthUnits: 'pixels', + lineWidthMinPixels: 0.65, + parameters: {depthCompare: 'always', depthWriteEnabled: false}, + pickable: false + }), wind: createWindLayer(field), particles: createParticleLayer(field), labels: isWebgpu diff --git a/modules/arrow-layers/test/webgpu-layers.browser.spec.ts b/modules/arrow-layers/test/webgpu-layers.browser.spec.ts index 5183761c..f8f218c1 100644 --- a/modules/arrow-layers/test/webgpu-layers.browser.spec.ts +++ b/modules/arrow-layers/test/webgpu-layers.browser.spec.ts @@ -16,7 +16,15 @@ import { } from 'apache-arrow'; import {describe, expect, it} from 'vitest'; -import {GeoArrowPathLayer, GeoArrowSolidPolygonLayer} from '../src'; +import { + GeoArrowColumnLayer, + GeoArrowHeatmapLayer, + GeoArrowPathLayer, + GeoArrowPointCloudLayer, + GeoArrowPolygonLayer, + GeoArrowScatterplotLayer, + GeoArrowSolidPolygonLayer +} from '../src'; type BrowserGpu = {requestAdapter: () => Promise}; type NativeGpuError = {error?: {message?: string}}; @@ -27,9 +35,12 @@ type NativeGpuDevice = { }; const pointType = new FixedSizeList(2, new Field('xy', new Float32())); +const point3DType = new FixedSizeList(3, new Field('xyz', new Float32())); const lineStringType = new List(new Field('vertices', pointType)); const polygonType = new List(new Field('rings', lineStringType)); const table = tableFromArrays({id: [0]}); +const points = vectorFromArray([[0, 0]], pointType); +const pointCloudPoints = vectorFromArray([[0, 0, 8]], point3DType); const lineStrings = vectorFromArray( [ [ @@ -82,6 +93,66 @@ async function renderGeoArrowLayers(type: 'webgl' | 'webgpu'): Promise { material: false, pickable: true }); + const layers = [ + new GeoArrowColumnLayer({ + id: `geoarrow-column-${type}`, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + data: table, + getPosition: points, + getFillColor: [16, 185, 129, 180], + getElevation: 10, + radius: 5, + diskResolution: 8, + material: false, + pickable: true + }), + new GeoArrowHeatmapLayer({ + id: `geoarrow-heatmap-${type}`, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + data: table, + getPosition: points, + getWeight: 1, + radiusPixels: 24, + pickable: false + }), + new GeoArrowPointCloudLayer({ + id: `geoarrow-point-cloud-${type}`, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + data: table, + getPosition: pointCloudPoints, + getColor: [244, 63, 94, 255], + pointSize: 6, + material: false, + pickable: true + }), + new GeoArrowPolygonLayer({ + id: `geoarrow-composite-polygon-${type}`, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + data: table, + getPolygon: polygons, + getFillColor: [168, 85, 247, 90], + getLineColor: [88, 28, 135, 255], + getLineWidth: 2, + lineWidthUnits: 'pixels', + earcutWorkerUrl: null, + material: false, + pickable: true + }), + new GeoArrowScatterplotLayer({ + id: `geoarrow-scatterplot-${type}`, + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + data: table, + getPosition: points, + getRadius: 7, + radiusUnits: 'common', + getFillColor: [6, 182, 212, 200], + getLineColor: [8, 47, 73, 255], + stroked: true, + pickable: true + }), + polygonLayer, + pathLayer + ]; let device: Device | undefined; let deck: Deck | undefined; @@ -114,7 +185,7 @@ async function renderGeoArrowLayers(type: 'webgl' | 'webgpu'): Promise { height: 128, views: new OrthographicView({id: 'geoarrow-webgpu-test', flipY: false}), initialViewState: {target: [0, 0, 0], zoom: 0}, - layers: [polygonLayer, pathLayer], + layers, onAfterRender: () => { if (!polygonLayer.state.table || !polygonLayer.state.triangles) { return; @@ -142,11 +213,11 @@ async function renderGeoArrowLayers(type: 'webgl' | 'webgpu'): Promise { } describe('GeoArrow graphics backend compatibility', () => { - it('renders binary path and polygon attributes on WebGL2', async () => { + it('renders binary attributes through portable upstream renderers on WebGL2', async () => { await renderGeoArrowLayers('webgl'); }, 20_000); - it('renders binary path and polygon attributes on WebGPU', async ({skip}) => { + it('renders binary attributes through portable upstream renderers on WebGPU', async ({skip}) => { const gpu = (navigator as Navigator & {gpu?: BrowserGpu}).gpu; if (!gpu || !(await gpu.requestAdapter())) { skip('This browser does not expose an available WebGPU adapter.'); diff --git a/modules/geo-layers/README.md b/modules/geo-layers/README.md index ade2b71d..2b071f52 100644 --- a/modules/geo-layers/README.md +++ b/modules/geo-layers/README.md @@ -37,9 +37,9 @@ const layers = [ ]; ``` -WebGL2 and WebGPU particle simulation are independently browser-tested. Complete WebGPU rendering -of the mountain-and-arrow scene remains in progress because its upstream terrain, polygon, and -path sublayers are not yet universally portable. +WebGL2 and WebGPU particle simulation are independently browser-tested. Wind arrows, state +boundaries, and station-triangulated terrain are portable; complete rendering of the original +mountain scene remains in progress because upstream `TerrainLayer` does not yet support WebGPU. See the [wind showcase guide](https://deck.gl-community.github.io/docs/modules/geo-layers/developer-guide/wind-showcase) and [standalone example](https://github.com/visgl/deck.gl-community/tree/master/examples/geo-layers/wind). diff --git a/modules/geo-layers/test/webgpu-layers.browser.spec.ts b/modules/geo-layers/test/webgpu-layers.browser.spec.ts new file mode 100644 index 00000000..a0dfcdd9 --- /dev/null +++ b/modules/geo-layers/test/webgpu-layers.browser.spec.ts @@ -0,0 +1,135 @@ +// deck.gl-community +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {COORDINATE_SYSTEM, Deck, OrthographicView} from '@deck.gl/core'; +import {luma, type Device} from '@luma.gl/core'; +import {webgl2Adapter} from '@luma.gl/webgl'; +import {webgpuAdapter} from '@luma.gl/webgpu'; +import {describe, expect, it} from 'vitest'; + +import {GlobalGridLayer, SharedTile2DHeader, TileGridLayer, type GlobalGrid} from '../src'; + +type BrowserGpu = {requestAdapter: () => Promise}; +type NativeGpuError = {error?: {message?: string}}; +type NativeGpuDevice = { + addEventListener: (type: 'uncapturederror', listener: (event: NativeGpuError) => void) => void; + removeEventListener: (type: 'uncapturederror', listener: (event: NativeGpuError) => void) => void; + queue: {onSubmittedWorkDone: () => Promise}; +}; + +const TEST_GRID: GlobalGrid = { + name: 'browser-test-grid', + hasNumericRepresentation: false, + cellToLngLat: () => [0, 0], + cellToBoundary: () => [ + [-28, -20], + [-4, -20], + [-4, 4], + [-28, 4] + ] +}; + +function createPortableGeoLayers() { + const tile = new SharedTile2DHeader({x: 0, y: 0, z: 0}); + tile.bbox = {left: 4, top: -20, right: 28, bottom: 4}; + + return [ + new GlobalGridLayer({ + id: 'webgpu-global-grid', + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + data: [{cellId: 'test-cell'}], + globalGrid: TEST_GRID, + filled: true, + stroked: true, + getFillColor: [14, 165, 233, 160], + getLineColor: [3, 105, 161, 255], + getLineWidth: 2, + lineWidthUnits: 'pixels', + pickable: true + }), + new TileGridLayer({ + id: 'webgpu-tile-grid', + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, + tile, + showLabel: false, + borderColor: [245, 158, 11, 255], + borderWidthMinPixels: 2 + }) + ]; +} + +async function renderPortableGeoLayers(type: 'webgl' | 'webgpu'): Promise { + const parent = document.createElement('div'); + parent.style.width = '128px'; + parent.style.height = '128px'; + document.body.append(parent); + + let device: Device | undefined; + let deck: Deck | undefined; + let nativeDevice: NativeGpuDevice | undefined; + const validationErrors: string[] = []; + const captureValidationError = (event: NativeGpuError): void => { + validationErrors.push(event.error?.message ?? 'Unknown WebGPU validation error.'); + }; + + try { + device = await luma.createDevice({ + type, + adapters: [webgl2Adapter, webgpuAdapter], + createCanvasContext: {container: parent} + }); + if (type === 'webgpu') { + nativeDevice = (device as Device & {handle?: NativeGpuDevice}).handle; + nativeDevice?.addEventListener('uncapturederror', captureValidationError); + } + + await new Promise((resolve, reject) => { + const timeout = window.setTimeout(() => { + reject(new Error(`Timed out while rendering portable geospatial layers with ${type}.`)); + }, 10_000); + + deck = new Deck({ + device, + parent, + width: 128, + height: 128, + views: new OrthographicView({id: 'geo-layers-webgpu-test', flipY: false}), + initialViewState: {target: [0, 0, 0], zoom: 0}, + layers: createPortableGeoLayers(), + onAfterRender: () => { + window.clearTimeout(timeout); + resolve(); + }, + onError: error => { + window.clearTimeout(timeout); + reject(error); + } + }); + }); + + await nativeDevice?.queue.onSubmittedWorkDone(); + expect(device.type).toBe(type); + expect(validationErrors).toEqual([]); + } finally { + nativeDevice?.removeEventListener('uncapturederror', captureValidationError); + deck?.finalize(); + device?.destroy(); + parent.remove(); + } +} + +describe('geospatial graphics backend compatibility', () => { + it('renders global-grid polygons and tile outlines on WebGL2', async () => { + await renderPortableGeoLayers('webgl'); + }, 20_000); + + it('renders global-grid polygons and tile outlines on WebGPU', async ({skip}) => { + const gpu = (navigator as Navigator & {gpu?: BrowserGpu}).gpu; + if (!gpu || !(await gpu.requestAdapter())) { + skip('This browser does not expose an available WebGPU adapter.'); + } + + await renderPortableGeoLayers('webgpu'); + }, 20_000); +}); diff --git a/modules/geo-layers/test/wind-layer/wind-layers.browser.spec.ts b/modules/geo-layers/test/wind-layer/wind-layers.browser.spec.ts index 8f7c557d..15eae47a 100644 --- a/modules/geo-layers/test/wind-layer/wind-layers.browser.spec.ts +++ b/modules/geo-layers/test/wind-layer/wind-layers.browser.spec.ts @@ -3,6 +3,7 @@ // Copyright (c) vis.gl contributors import {Deck, MapView} from '@deck.gl/core'; +import {GeoJsonLayer} from '@deck.gl/layers'; import {luma, type Device} from '@luma.gl/core'; import {webgl2Adapter} from '@luma.gl/webgl'; import {webgpuAdapter} from '@luma.gl/webgpu'; @@ -33,6 +34,20 @@ const STATIONS: WindStation[] = [ {name: 'northwest', long: 100, lat: 39, elv: 240}, {name: 'northeast', long: 96, lat: 39, elv: 320} ]; +const STATE_BOUNDARY = { + type: 'Feature' as const, + properties: {}, + geometry: { + type: 'LineString' as const, + coordinates: [ + [-100, 35], + [-96, 35], + [-96, 39], + [-100, 39], + [-100, 35] + ] + } +}; function createElevationData(): string { const canvas = document.createElement('canvas'); @@ -89,6 +104,15 @@ function createLayers( gridHeight: 6, elevationScale: 2 }), + new GeoJsonLayer({ + id: 'test-wind-state-boundary', + data: STATE_BOUNDARY, + filled: false, + stroked: true, + getLineColor: [177, 188, 205, 255], + getLineWidth: 1, + lineWidthUnits: 'pixels' + }), particles ]; } @@ -223,7 +247,7 @@ describe('wind showcase rendering', () => { await renderWindLayers('webgl'); }, 20_000); - it('renders portable wind arrows, station terrain, and GPU-resident particles on WebGPU', async ({ + it('renders portable boundaries, arrows, station terrain, and particles on WebGPU', async ({ skip }) => { const gpu = (navigator as Navigator & {gpu?: BrowserGpu}).gpu; diff --git a/modules/layers/test/webgpu-layers.browser.spec.ts b/modules/layers/test/webgpu-layers.browser.spec.ts index 3ee00991..b9e88cee 100644 --- a/modules/layers/test/webgpu-layers.browser.spec.ts +++ b/modules/layers/test/webgpu-layers.browser.spec.ts @@ -425,7 +425,7 @@ async function renderPortableLayers(type: 'webgl' | 'webgpu'): Promise { describe('community graphics backend compatibility', () => { it('renders custom shaders, paths, polygons, graph, timeline, and editing on WebGL2', async () => { await renderPortableLayers('webgl'); - }, 20_000); + }, 30_000); it('renders custom shaders, paths, polygons, graph, timeline, and editing on WebGPU', async ({ skip @@ -436,5 +436,5 @@ describe('community graphics backend compatibility', () => { } await renderPortableLayers('webgpu'); - }, 20_000); + }, 30_000); }); diff --git a/modules/trace-layers/src/layers/layers/trace-webgpu.browser.spec.ts b/modules/trace-layers/src/layers/layers/trace-webgpu.browser.spec.ts index ba694f4d..eb0b9af2 100644 --- a/modules/trace-layers/src/layers/layers/trace-webgpu.browser.spec.ts +++ b/modules/trace-layers/src/layers/layers/trace-webgpu.browser.spec.ts @@ -149,7 +149,8 @@ function createTraceGraph(): TraceGraph { /** Renders the complete public graph layer with an actual selected luma device. */ async function renderTraceGraph( type: 'webgl' | 'webgpu', - enableFastTextLayer: boolean + enableFastTextLayer: boolean, + lineRoutingMode: TraceVisSettings['lineRoutingMode'] = 'straight' ): Promise { const parent = document.createElement('div'); parent.style.width = '256px'; @@ -175,9 +176,11 @@ async function renderTraceGraph( nativeGpuDevice?.addEventListener('uncapturederror', handleValidationError); const graph = createTraceGraph(); const layer = new TraceGraphLayer({ - id: `trace-graph-${type}-${enableFastTextLayer ? 'fast-text' : 'default-text'}`, + id: `trace-graph-${type}-${lineRoutingMode}-${ + enableFastTextLayer ? 'fast-text' : 'default-text' + }`, traceGraphs: [graph], - settings: {...TRACE_SETTINGS, enableFastTextLayer}, + settings: {...TRACE_SETTINGS, enableFastTextLayer, lineRoutingMode}, showRowSeparators: true }); await new Promise((resolve, reject) => { @@ -245,4 +248,13 @@ describe('trace graph graphics backend compatibility', () => { await renderTraceGraph('webgpu', true); }, 30_000); + + it('renders curved dependencies on WebGPU', async ({skip}) => { + const gpu = (navigator as Navigator & {gpu?: BrowserGpu}).gpu; + if (!gpu || !(await gpu.requestAdapter())) { + skip('This browser does not expose an available WebGPU adapter.'); + } + + await renderTraceGraph('webgpu', true, 'curve'); + }, 30_000); }); diff --git a/vitest.config.ts b/vitest.config.ts index eaa01169..852449dd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -83,19 +83,20 @@ const BROWSER_OPTIMIZE_DEPS_CONFIG = { const BROWSER_TEST_EXCLUDE = ['modules/**/dist/**', 'dev/**/dist/**']; const HEADLESS_BROWSER_PROVIDER = - process.env.GITHUB_ACTIONS === 'true' - ? playwright({launchOptions: {channel: 'chrome'}}) - : process.env.DECK_GL_COMMUNITY_SOFTWARE_WEBGPU === 'true' - ? playwright({ - launchOptions: { - args: [ - '--enable-unsafe-webgpu', - '--enable-unsafe-swiftshader', - '--use-angle=swiftshader', - '--enable-features=Vulkan,WebGPU' - ] - } - }) + process.env.DECK_GL_COMMUNITY_SOFTWARE_WEBGPU === 'true' + ? playwright({ + launchOptions: { + ...(process.env.GITHUB_ACTIONS === 'true' ? {channel: 'chrome'} : {}), + args: [ + '--enable-unsafe-webgpu', + '--enable-unsafe-swiftshader', + '--use-angle=swiftshader', + '--enable-features=Vulkan,WebGPU' + ] + } + }) + : process.env.GITHUB_ACTIONS === 'true' + ? playwright({launchOptions: {channel: 'chrome'}}) : playwright(); const CONFIG = defineConfig({ diff --git a/website/src/components/docs/webgpu-support.js b/website/src/components/docs/webgpu-support.js index eeb8fd7d..b028a74a 100644 --- a/website/src/components/docs/webgpu-support.js +++ b/website/src/components/docs/webgpu-support.js @@ -1,8 +1,8 @@ const SUPPORTED_DOC_IDS = new Set([ 'modules/geo-layers/api-reference/delaunay-cover-layer', - 'modules/geo-layers/api-reference/delaunay-interpolation', + 'modules/geo-layers/api-reference/global-grid-layer', 'modules/geo-layers/api-reference/particle-layer', - 'modules/geo-layers/api-reference/wind-field', + 'modules/geo-layers/api-reference/tile-grid-layer', 'modules/geo-layers/api-reference/wind-layer', 'modules/graph-layers/api-reference/layers/edge-arrow-layer', 'modules/graph-layers/api-reference/layers/path-edge-layer', @@ -23,6 +23,27 @@ const UNSUPPORTED_DOC_IDS = new Set([ 'modules/graph-layers/api-reference/layers/flow-path-layer' ]); +const NOT_APPLICABLE_DOC_IDS = new Set([ + 'modules/basemap-layers/api-reference/map-style', + 'modules/basemap-layers/api-reference/map-style-loader', + 'modules/geo-layers/api-reference/a5-grid', + 'modules/geo-layers/api-reference/delaunay-interpolation', + 'modules/geo-layers/api-reference/geohash-grid', + 'modules/geo-layers/api-reference/global-grid', + 'modules/geo-layers/api-reference/h3-grid', + 'modules/geo-layers/api-reference/quadkey-grid', + 'modules/geo-layers/api-reference/s2-grid', + 'modules/geo-layers/api-reference/shared-tileset-2d', + 'modules/geo-layers/api-reference/wind-field' +]); + +const NOT_APPLICABLE_DOC_ID_PATTERNS = [ + /^modules\/editable-layers\/api-reference\/(?:edit-modes|widgets)\//, + /^modules\/graph-layers\/api-reference\/(?:internal|layouts|loaders|styling)\//, + /^modules\/graph-layers\/api-reference\/(?:classic-graph|edge|graph|node|tabular-graph)$/, + /^modules\/trace-layers\/api-reference\/trace\// +]; + const MODULE_STATUS = { 'arrow-layers': 'partial', 'basemap-layers': 'partial', @@ -36,7 +57,7 @@ const MODULE_STATUS = { leaflet: 'unsupported', panels: 'not-applicable', react: 'not-applicable', - three: 'unsupported', + three: 'partial', 'timeline-layers': 'partial', 'trace-layers': 'partial', widgets: 'supported' @@ -77,6 +98,12 @@ export function getDocWebGpuStatus(docId = '') { if (UNSUPPORTED_DOC_IDS.has(docId)) { return 'unsupported'; } + if ( + NOT_APPLICABLE_DOC_IDS.has(docId) || + NOT_APPLICABLE_DOC_ID_PATTERNS.some(pattern => pattern.test(docId)) + ) { + return 'not-applicable'; + } const moduleName = /^modules\/([^/]+)/.exec(docId)?.[1]; return MODULE_STATUS[moduleName] ?? 'mixed'; diff --git a/website/src/examples/geo-layers/wind.mdx b/website/src/examples/geo-layers/wind.mdx index a69602d5..a23453f3 100644 --- a/website/src/examples/geo-layers/wind.mdx +++ b/website/src/examples/geo-layers/wind.mdx @@ -9,8 +9,9 @@ import Demo from './wind'; :::caution Work in progress The reusable wind showcase and its layer APIs are experimental. GPU particle simulation is -browser-tested on WebGL2 and WebGPU; the complete terrain-and-arrows scene remains WebGL2-first -while upstream terrain, polygon, and path compatibility is in progress. +browser-tested on WebGL2 and WebGPU. Wind arrows, state boundaries, labels, and station terrain +render on both backends; image-derived mountain terrain remains WebGL2-only pending upstream +`TerrainLayer` support. ::: The example recreates From c2f90696a455e4d874b418d2dbbe552e4e7c85cf Mon Sep 17 00:00:00 2001 From: Ib Green Date: Thu, 30 Jul 2026 18:22:05 -0400 Subject: [PATCH 2/2] chore: separate WebGPU CI enablement --- .github/workflows/test.yml | 2 -- docs/whats-new.md | 2 -- .../test/webgpu-layers.browser.spec.ts | 6 ++++- .../layers/test/webgpu-layers.browser.spec.ts | 4 +-- vitest.config.ts | 27 +++++++++---------- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9296158d..f94beb95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -260,8 +260,6 @@ jobs: - name: Run headless browser tests run: yarn test-headless - env: - DECK_GL_COMMUNITY_SOFTWARE_WEBGPU: 'true' website-build: runs-on: ubuntu-22.04 diff --git a/docs/whats-new.md b/docs/whats-new.md index a3f50c92..a200f6f3 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -39,8 +39,6 @@ Scope tracked in the [v9.4 milestone](https://github.com/visgl/deck.gl-community - Trace counter sparklines now preserve their full geometry using portable `LineLayer` segments. - `GlobalGridLayer`, `TileGridLayer` borders, and the wind showcase's state boundaries now have local-data browser coverage on WebGL2 and WebGPU. -- Headless CI enables Chromium's software WebGPU adapter, so native WebGPU validation tests run - even on runners without hardware adapters instead of being skipped. - Every website gallery example and live layer-reference example now receives a standalone `DeviceTabsWidget` from the shared imperative host, with an independent device manager, WebGPU preference, WebGL2 fallback, renderer remounting, and preserved view state. diff --git a/modules/geo-layers/test/webgpu-layers.browser.spec.ts b/modules/geo-layers/test/webgpu-layers.browser.spec.ts index a0dfcdd9..06217a95 100644 --- a/modules/geo-layers/test/webgpu-layers.browser.spec.ts +++ b/modules/geo-layers/test/webgpu-layers.browser.spec.ts @@ -114,7 +114,11 @@ async function renderPortableGeoLayers(type: 'webgl' | 'webgpu'): Promise } finally { nativeDevice?.removeEventListener('uncapturederror', captureValidationError); deck?.finalize(); - device?.destroy(); + // Deck finalization releases WebGPU resources; leave final WebGPU teardown to the browser so + // this test remains compatible with shared software adapters when CI enables them separately. + if (device?.type !== 'webgpu') { + device?.destroy(); + } parent.remove(); } } diff --git a/modules/layers/test/webgpu-layers.browser.spec.ts b/modules/layers/test/webgpu-layers.browser.spec.ts index b9e88cee..3ee00991 100644 --- a/modules/layers/test/webgpu-layers.browser.spec.ts +++ b/modules/layers/test/webgpu-layers.browser.spec.ts @@ -425,7 +425,7 @@ async function renderPortableLayers(type: 'webgl' | 'webgpu'): Promise { describe('community graphics backend compatibility', () => { it('renders custom shaders, paths, polygons, graph, timeline, and editing on WebGL2', async () => { await renderPortableLayers('webgl'); - }, 30_000); + }, 20_000); it('renders custom shaders, paths, polygons, graph, timeline, and editing on WebGPU', async ({ skip @@ -436,5 +436,5 @@ describe('community graphics backend compatibility', () => { } await renderPortableLayers('webgpu'); - }, 30_000); + }, 20_000); }); diff --git a/vitest.config.ts b/vitest.config.ts index 852449dd..eaa01169 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -83,20 +83,19 @@ const BROWSER_OPTIMIZE_DEPS_CONFIG = { const BROWSER_TEST_EXCLUDE = ['modules/**/dist/**', 'dev/**/dist/**']; const HEADLESS_BROWSER_PROVIDER = - process.env.DECK_GL_COMMUNITY_SOFTWARE_WEBGPU === 'true' - ? playwright({ - launchOptions: { - ...(process.env.GITHUB_ACTIONS === 'true' ? {channel: 'chrome'} : {}), - args: [ - '--enable-unsafe-webgpu', - '--enable-unsafe-swiftshader', - '--use-angle=swiftshader', - '--enable-features=Vulkan,WebGPU' - ] - } - }) - : process.env.GITHUB_ACTIONS === 'true' - ? playwright({launchOptions: {channel: 'chrome'}}) + process.env.GITHUB_ACTIONS === 'true' + ? playwright({launchOptions: {channel: 'chrome'}}) + : process.env.DECK_GL_COMMUNITY_SOFTWARE_WEBGPU === 'true' + ? playwright({ + launchOptions: { + args: [ + '--enable-unsafe-webgpu', + '--enable-unsafe-swiftshader', + '--use-angle=swiftshader', + '--enable-features=Vulkan,WebGPU' + ] + } + }) : playwright(); const CONFIG = defineConfig({