Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
69a3ce6
feat(layers): add antialiasing prop to PathLayer and LineLayer
claude Aug 1, 2026
6b7f3a1
fix(layers): derive antialiasing coverage from screen-space derivatives
claude Aug 2, 2026
50e6277
test(layers): add coverage-based render test for PathLayer antialiasing
claude Aug 2, 2026
f0f955a
test(layers): run antialiasing test without MSAA, where the prop applies
claude Aug 2, 2026
fed9c28
docs(layers): correct the note on why both edge derivatives are computed
claude Aug 2, 2026
37b3377
docs: add RFC for PathLayer and LineLayer analytic antialiasing
claude Aug 2, 2026
966b13f
fix(layers): take edge derivatives before the discards in PathLayer
claude Aug 2, 2026
f319dcc
test(layers): add golden image coverage for antialiasing
claude Aug 2, 2026
d1446cb
docs: enumerate every path where deck loses MSAA
claude Aug 2, 2026
dfc1619
refactor(layers): tighten shader comments, add variety to the AA golden
claude Aug 2, 2026
ff83661
docs: relate the antialiasing RFC to existing tracker discussion
claude Aug 2, 2026
3a7d3d5
docs: lead the antialiasing RFC with a remedy matrix
claude Aug 2, 2026
95e35c8
docs: record what changes if luma.gl#2741 lands
claude Aug 2, 2026
b7cdf19
feat(layers): expose antialiasing on PolygonLayer and GeoJsonLayer, d…
claude Aug 2, 2026
d49ed7b
test(layers): state the offset guard as behavior, not as history
claude Aug 2, 2026
abb81ee
test(layers): carry the commented-out webgpu suite on the AA render test
claude Aug 2, 2026
2d98227
refactor(layers): use the shared smoothedge helper, prep LineLayer fo…
claude Aug 3, 2026
d7996ed
fix(core): remap clip-space depth for WebGPU
claude Aug 3, 2026
2c09436
docs(rfc): record what WebGPU coverage is actually asserted
claude Aug 3, 2026
2f6f0b6
test(layers): guard premultiplied alpha ordering, record alpha-to-cov…
claude Aug 3, 2026
241d28b
test(render): enable the WebGPU line-layer case, expected to fail
claude Aug 4, 2026
14434fe
Merge branch 'master' into claude/deckgl-path-aliasing-4tx9a3
chrisgervang Aug 7, 2026
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
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ jobs:
run: yarn build

- name: Run tests
# Render tests run under a virtual display. Chromium's --enable-gpu stops headless forcing
# software rendering, but the driver autodetection it falls back to needs an X display on
# Linux - without one the WebGPU canvas is never composited and captures as a blank frame.
# WebGL is unaffected either way. See https://github.com/visgl/luma.gl/issues/2874
run: |
yarn lint
yarn test-ci
xvfb-run -a --server-args="-screen 0 1280x1024x24" yarn test-ci

- name: Upload render test failure images
if: failure()
Expand Down
357 changes: 357 additions & 0 deletions dev-docs/RFCs/v9.4/path-line-antialiasing-rfc.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/api-reference/arcgis/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,9 @@ Not supported features:
- Multiple views
- Controller
- React integration

### Antialiasing

deck.gl renders into an auxiliary framebuffer here and composites the result into the ArcGIS scene. That framebuffer is not multisampled, so layers whose edges depend on MSAA — most visibly [PathLayer](../layers/path-layer.md) and [LineLayer](../layers/line-layer.md) — render with hard, aliased edges regardless of how the ArcGIS API created its context.

Set `antialiasing: true` on those layers to have them compute edge coverage in the shader instead. On composite layers the prop is named `lineAntialiasing` ([GeoJsonLayer](../layers/geojson-layer.md#lineantialiasing), [PolygonLayer](../layers/polygon-layer.md#lineantialiasing)).
8 changes: 8 additions & 0 deletions docs/api-reference/core/post-process-effect.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ const deckgl = new Deck({
});
```

## Remarks

### Antialiasing

Adding a post-processing effect redirects layer rendering into an offscreen framebuffer, which is not multisampled. Layers whose edges depend on the canvas' MSAA — most visibly [PathLayer](../layers/path-layer.md) and [LineLayer](../layers/line-layer.md) — therefore render with hard, aliased edges once any effect is added, even though the canvas itself was created with `antialias: true`. See [#10404](https://github.com/visgl/deck.gl/issues/10404).

Set `antialiasing: true` on those layers to have them compute edge coverage in the shader instead. On composite layers the prop is named `lineAntialiasing` ([GeoJsonLayer](../layers/geojson-layer.md#lineantialiasing), [PolygonLayer](../layers/polygon-layer.md#lineantialiasing)).

## Source

[/modules/core/src/effects/post-process-effect.ts](https://github.com/visgl/deck.gl/tree/master/modules/core/src/effects/post-process-effect.ts)
8 changes: 8 additions & 0 deletions docs/api-reference/google-maps/google-maps-overlay.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ The constructor additionally accepts the following option:

- `interleaved` (boolean) - When set to `false`, a dedicated deck.gl canvas is layered on top of the base map. If set to `true` and the Google Map is configured for Vector rendering, deck.gl layers are inserted into the Google Maps layer stack, sharing the same WebGL2RenderingContext. Default is `true`.

## Remarks

### Antialiasing

With `interleaved: true` on a Vector map, deck.gl shares the WebGL context created by Google Maps, which does not provide multisampling and exposes no option to request it. Layers whose edges depend on it — most visibly [PathLayer](../layers/path-layer.md) and [LineLayer](../layers/line-layer.md) — render with hard, aliased edges. See [#7647](https://github.com/visgl/deck.gl/issues/7647).

Set `antialiasing: true` on those layers to have them compute edge coverage in the shader instead. On composite layers the prop is named `lineAntialiasing` ([GeoJsonLayer](../layers/geojson-layer.md#lineantialiasing), [PolygonLayer](../layers/polygon-layer.md#lineantialiasing)).

## Methods

#### `setMap` {#setmap}
Expand Down
6 changes: 6 additions & 0 deletions docs/api-reference/layers/geojson-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,12 @@ Type of line joint. If `true`, draw round joints. Otherwise draw miter joints.
The maximum extent of a joint in ratio to the stroke width.
Only works if `lineJointRounded` is `false`.

#### `lineAntialiasing` (boolean, optional) {#lineantialiasing}

* Default: `false`

If `true`, lines are rendered with smoothed edges. If `false`, they are rendered with rough edges. Antialiasing can cause artifacts where a line overlaps itself. Forwarded to the underlying [PathLayer](./path-layer.md#antialiasing).

#### `lineBillboard` (boolean, optional) {#linebillboard}

* Default: `false`
Expand Down
8 changes: 8 additions & 0 deletions docs/api-reference/layers/line-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ The minimum line width in pixels. This prop can be used to prevent the line from

The maximum line width in pixels. This prop can be used to prevent the line from getting to thick when zoomed in.

#### `antialiasing` (boolean, optional) {#antialiasing}

* Default: `false`

If `true`, lines are rendered with smoothed edges. If `false`, lines are rendered with rough edges. Antialiasing can cause artifacts where lines overlap. Only the edges along the width of the line are smoothed — the two ends are not.

This computes coverage in the shader, which is one of several antialiasing techniques with different trade-offs. See [Antialiasing and Multisampling](https://luma.gl/docs/api-guide/gpu/gpu-antialiasing) in the luma.gl docs for choosing between them.


### Data Accessors

Expand Down
8 changes: 8 additions & 0 deletions docs/api-reference/layers/path-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ If `false`, the width always faces up.
The maximum extent of a joint in ratio to the stroke width.
Only works if `jointRounded` is `false`.

#### `antialiasing` (boolean, optional) {#antialiasing}

* Default: `false`

If `true`, paths are rendered with smoothed edges. If `false`, paths are rendered with rough edges. Antialiasing can cause artifacts where a path overlaps itself. Only the edges along the width of the path are smoothed — flat caps at the two ends of a path are not; set `capRounded` to `true` if those need smoothing.

This computes coverage in the shader, which is one of several antialiasing techniques with different trade-offs. See [Antialiasing and Multisampling](https://luma.gl/docs/api-guide/gpu/gpu-antialiasing) in the luma.gl docs for choosing between them.

#### `_pathType` (object, optional) {#_pathtype}

* Default: `null`
Expand Down
6 changes: 6 additions & 0 deletions docs/api-reference/layers/polygon-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ Type of joint. If `true`, draw round joints. Otherwise draw miter joints.
The maximum extent of a joint in ratio to the stroke width.
Only works if `lineJointRounded` is `false`.

#### `lineAntialiasing` (boolean, optional) {#lineantialiasing}

* Default: `false`

If `true`, the stroke is rendered with smoothed edges. If `false`, it is rendered with rough edges. Antialiasing can cause artifacts where the stroke overlaps itself. Forwarded to the underlying [PathLayer](./path-layer.md#antialiasing).

#### `material` (Material, optional) {#material}

* Default: `true`
Expand Down
4 changes: 4 additions & 0 deletions docs/api-reference/mapbox/mapbox-overlay.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ See [Deck.getCanvas](../core/deck.md#getcanvas). When using `interleaved: true`,

## Remarks

### Antialiasing

Base maps create their WebGL context with `antialias: false`, so in interleaved mode deck.gl layers receive no multisampling. Layers that rely on it — most visibly [PathLayer](../layers/path-layer.md) and [LineLayer](../layers/line-layer.md) — will look aliased against the base map. Set `antialiasing: true` on those layers, or enable MSAA on the base map itself.

### Multi-view usage

When using `MapboxOverlay` with multiple views passed to the `views` prop, only one of the views can match the base map and receive interaction.
Expand Down
5 changes: 4 additions & 1 deletion modules/core/src/shaderlib/project/project.wgsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,10 @@ fn project_position_vec2_f32(position: vec2<f32>) -> vec2<f32> {

// Transforms a common space position to clip space.
fn project_common_position_to_clipspace_with_projection(position: vec4<f32>, viewProjectionMatrix: mat4x4<f32>, center: vec4<f32>) -> vec4<f32> {
return viewProjectionMatrix * position + center;
let clipPosition = viewProjectionMatrix * position + center;
// Viewports build WebGL-convention matrices, whose clip volume is -w <= z <= w. WebGPU's is
// 0 <= z <= w, so remap depth here - without it the near half of the range is clipped away.
return vec4<f32>(clipPosition.xy, (clipPosition.z + clipPosition.w) * 0.5, clipPosition.w);
}

// Uses the project viewProjectionMatrix and center.
Expand Down
9 changes: 9 additions & 0 deletions modules/layers/src/geojson-layer/geojson-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ type _GeoJsonLayerStrokeProps<FeaturePropertiesT> = {
*/
lineCapRounded?: boolean;

/**
* If `true`, lines are rendered with smoothed edges. If `false`, lines are rendered with rough
* edges. Antialiasing can cause artifacts where a line overlaps itself. Only the edges along the
* width of the line are smoothed - flat caps at the two ends are not.
*
* @default false
*/
lineAntialiasing?: boolean;

/**
* If `true`, extrude the line in screen space (width always faces the camera).
* If `false`, the width always faces up.
Expand Down
1 change: 1 addition & 0 deletions modules/layers/src/geojson-layer/sub-layer-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const LINE_LAYER = {
lineCapRounded: 'capRounded',
lineMiterLimit: 'miterLimit',
lineBillboard: 'billboard',
lineAntialiasing: 'antialiasing',

getLineColor: 'getColor',
getLineWidth: 'getWidth'
Expand Down
8 changes: 8 additions & 0 deletions modules/layers/src/line-layer/line-layer-fragment.glsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ void main(void) {

fragColor = vColor;

if (line.antialiasing) {
// Feather one device pixel across the width, from the derivative of uv.y. The ends are left
// hard - they abut neighboring segments. See dev-docs/RFCs/v9.4/path-line-antialiasing-rfc.md
float edgeCoord = abs(uv.y);
float edgePixels = (1.0 - edgeCoord) / max(fwidth(edgeCoord), 1e-6);
fragColor.a *= smoothedge(0.0, edgePixels);
}

DECKGL_FILTER_COLOR(fragColor, geometry);
}
`;
3 changes: 3 additions & 0 deletions modules/layers/src/line-layer/line-layer-uniforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ layout(std140) uniform lineUniforms {
float widthMinPixels;
float widthMaxPixels;
float useShortestPath;
bool antialiasing;
highp int widthUnits;
} line;
`;
Expand All @@ -19,6 +20,7 @@ export type LineProps = {
widthMinPixels: number;
widthMaxPixels: number;
useShortestPath: number;
antialiasing: boolean;
widthUnits: number;
};

Expand All @@ -32,6 +34,7 @@ export const lineUniforms = {
widthMinPixels: 'f32',
widthMaxPixels: 'f32',
useShortestPath: 'f32',
antialiasing: 'f32',
widthUnits: 'i32'
}
} as const satisfies ShaderModule<LineProps>;
15 changes: 13 additions & 2 deletions modules/layers/src/line-layer/line-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const defaultProps: DefaultProps<LineLayerProps> = {
widthUnits: 'pixels',
widthScale: {type: 'number', value: 1, min: 0},
widthMinPixels: {type: 'number', value: 0, min: 0},
widthMaxPixels: {type: 'number', value: Number.MAX_SAFE_INTEGER, min: 0}
widthMaxPixels: {type: 'number', value: Number.MAX_SAFE_INTEGER, min: 0},
antialiasing: false
};

/** All properties supported by LineLayer. */
Expand Down Expand Up @@ -68,6 +69,14 @@ type _LineLayerProps<DataT> = {
*/
widthMaxPixels?: number;

/**
* If `true`, lines are rendered with smoothed edges. If `false`, lines are rendered with rough
* edges. Antialiasing can cause artifacts where lines overlap. Only the edges along the width of
* the line are smoothed - the two ends are not.
* @default false
*/
antialiasing?: boolean;

/**
* Source position of each object.
* @default object => object.sourcePosition
Expand Down Expand Up @@ -169,13 +178,15 @@ export default class LineLayer<DataT = any, ExtraProps extends {} = {}> extends
}

draw({uniforms}): void {
const {widthUnits, widthScale, widthMinPixels, widthMaxPixels, wrapLongitude} = this.props;
const {widthUnits, widthScale, widthMinPixels, widthMaxPixels, wrapLongitude, antialiasing} =
this.props;
const model = this.state.model!;
const lineProps: LineProps = {
widthUnits: UNIT[widthUnits],
widthScale,
widthMinPixels,
widthMaxPixels,
antialiasing,
useShortestPath: wrapLongitude ? 1 : 0
};
model.shaderInputs.setProps({line: lineProps});
Expand Down
12 changes: 12 additions & 0 deletions modules/layers/src/line-layer/line-layer.wgsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ struct LineUniforms {
widthMinPixels: f32,
widthMaxPixels: f32,
useShortestPath: f32,
antialiasing: f32,
widthUnits: i32,
};

Expand Down Expand Up @@ -161,6 +162,17 @@ fn fragmentMain(
// Start with the input color.
var fragColor: vec4<f32> = vColor;

// Distance to the edge in device pixels, from the derivative of uv.y. Taken in uniform control
// flow, ahead of the picking discard below
let edgeCoord = abs(uv.y);
let edgePixels = (1.0 - edgeCoord) / max(fwidth(edgeCoord), 1e-6);

if (line.antialiasing != 0.0) {
// Feather one device pixel across the width, before premultiplication below. The ends are left
// hard - they abut neighbors
fragColor.a *= smoothedge(0.0, edgePixels);
}

if (picking.isActive > 0.5) {
if (!picking_isColorValid(pickingColor)) {
discard;
Expand Down
29 changes: 26 additions & 3 deletions modules/layers/src/path-layer/path-layer-fragment.glsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,41 @@ out vec4 fragColor;
void main(void) {
geometry.uv = vPathPosition;

if (vPathPosition.y < 0.0 || vPathPosition.y > vPathLength) {
bool isCorner = vPathPosition.y < 0.0 || vPathPosition.y > vPathLength;
bool isRound = vJointType > 0.5;

// Distance to the silhouette in device pixels, from the derivative of the coordinate that
// bounds it. Computed before the discards below: derivatives are undefined once an invocation
// in the quad has been discarded. See dev-docs/RFCs/v9.4/path-line-antialiasing-rfc.md
float edgePixels = 0.0;
if (path.antialiasing) {
float bodyCoord = abs(vPathPosition.x);
float cornerCoord = length(vCornerOffset);
// Both evaluated so each derivative stays on one field across the corner/body boundary
float bodyPixels = (1.0 - bodyCoord) / max(fwidth(bodyCoord), 1e-6);
float cornerPixels = (1.0 - cornerCoord) / max(fwidth(cornerCoord), 1e-6);
edgePixels = isRound && isCorner ? cornerPixels : bodyPixels;
}

if (isCorner) {
// if joint is rounded, test distance from the corner
if (vJointType > 0.5 && length(vCornerOffset) > 1.0) {
if (isRound && length(vCornerOffset) > 1.0) {
discard;
}
// trim miter
if (vJointType < 0.5 && vMiterLength > path.miterLimit + 1.0) {
if (!isRound && vMiterLength > path.miterLimit + 1.0) {
discard;
}
}
fragColor = vColor;

if (path.antialiasing) {
// Feather one device pixel across the width only - segments abut lengthwise, which would seam.
// edgePixels is a signed device-pixel distance, and SMOOTH_EDGE_RADIUS is 0.5, so smoothedge
// ramps across exactly one pixel centered on the edge.
fragColor.a *= smoothedge(0.0, edgePixels);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AA misses outer coverage half

Medium Severity

Coverage is centered with smoothedge(0.0, edgePixels), but stroke geometry is not expanded by half a device pixel and round joints still discard past the silhouette. Pixel centers outside the edge never shade, so the outer half of the claimed one-pixel feather is lost and edges cut off around 50% alpha.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 14434fe. Configure here.


DECKGL_FILTER_COLOR(fragColor, geometry);
}
`;
4 changes: 4 additions & 0 deletions modules/layers/src/path-layer/path-layer-uniforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct PathUniforms {
jointType: f32,
capType: f32,
miterLimit: f32,
antialiasing: f32,
billboard: f32,
widthUnits: i32,
};
Expand All @@ -28,6 +29,7 @@ layout(std140) uniform pathUniforms {
float jointType;
float capType;
float miterLimit;
bool antialiasing;
bool billboard;
highp int widthUnits;
} path;
Expand All @@ -40,6 +42,7 @@ export type PathProps = {
jointType: number;
capType: number;
miterLimit: number;
antialiasing: boolean;
billboard: boolean;
widthUnits: number;
};
Expand All @@ -56,6 +59,7 @@ export const pathUniforms = {
jointType: 'f32',
capType: 'f32',
miterLimit: 'f32',
antialiasing: 'f32',
billboard: 'f32',
widthUnits: 'i32'
}
Expand Down
10 changes: 10 additions & 0 deletions modules/layers/src/path-layer/path-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ type _PathLayerProps<DataT> = {
* @default 4
*/
miterLimit?: number;
/**
* If `true`, paths are rendered with smoothed edges. If `false`, paths are rendered with rough
* edges. Antialiasing can cause artifacts where a path overlaps itself. Only the edges along the
* width of the path are smoothed - flat caps at the two ends of a path are not.
* @default false
*/
antialiasing?: boolean;
/**
* If `true`, extrude the path in screen space (width always faces the camera).
* If `false`, the width always faces up (z).
Expand Down Expand Up @@ -106,6 +113,7 @@ const defaultProps: DefaultProps<PathLayerProps> = {
jointRounded: false,
capRounded: false,
miterLimit: {type: 'number', min: 0, value: 4},
antialiasing: false,
billboard: false,
_pathType: null,

Expand Down Expand Up @@ -335,6 +343,7 @@ export default class PathLayer<DataT = any, ExtraPropsT extends {} = {}> extends
jointRounded,
capRounded,
billboard,
antialiasing,
miterLimit,
widthUnits,
widthScale,
Expand All @@ -347,6 +356,7 @@ export default class PathLayer<DataT = any, ExtraPropsT extends {} = {}> extends
jointType: Number(jointRounded),
capType: Number(capRounded),
billboard,
antialiasing,
widthUnits: UNIT[widthUnits],
widthScale,
miterLimit,
Expand Down
Loading
Loading