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

visgl / luma.gl / 27856922126

20 Jun 2026 02:04AM UTC coverage: 71.991%. First build
27856922126

Pull #2682

github

web-flow
Merge 0f579b91a into bd6f08e3a
Pull Request #2682: [codex] Add A-buffer order-independent transparency

9523 of 14925 branches covered (63.81%)

Branch coverage included in aggregate %.

25 of 135 new or added lines in 6 files covered. (18.52%)

19393 of 25241 relevant lines covered (76.83%)

4256.46 hits per line

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

16.0
/modules/engine/src/modules/a-buffer/a-buffer.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {Buffer, TextureView} from '@luma.gl/core';
6
import type {ShaderModule, ShaderPlugin} from '@luma.gl/shadertools';
7

8
export type ABufferShaderModuleProps = {
9
  /** Whether the current draw should append fragments into the A-buffer. */
10
  isActive?: boolean;
11
  /** Width and height of the target framebuffer in device pixels. */
12
  framebufferSize?: [number, number];
13
  /** Top row of the horizontal slice currently being captured. */
14
  sliceStartY?: number;
15
  /** Per-pixel head pointers plus fragment counters. */
16
  headPointers?: Buffer;
17
  /** Packed translucent fragment records. */
18
  fragments?: Buffer;
19
  /** Opaque depth generated by the base pass. */
20
  opaqueDepthTexture?: TextureView;
21
};
22

23
/** Uniform values consumed by the {@link aBuffer} shader module. */
24
export type ABufferShaderModuleUniforms = {
25
  /** Whether capture is active for the current draw. */
26
  isActive: boolean;
27
  /** Width and height of the target framebuffer in device pixels. */
28
  framebufferSize: [number, number];
29
  /** Top row of the horizontal slice currently being captured. */
30
  sliceStartY: number;
31
};
32

33
/** Storage and texture bindings consumed by the {@link aBuffer} shader module. */
34
export type ABufferShaderModuleBindings = {
35
  /** Per-pixel linked-list heads and allocation counters. */
36
  headPointers?: Buffer;
37
  /** Packed premultiplied-color, depth, and next-pointer records. */
38
  fragments?: Buffer;
39
  /** Opaque depth sampled before allocating translucent fragment records. */
40
  opaqueDepthTexture?: TextureView;
41
};
42

43
// GPUShaderStage.FRAGMENT without requiring the WebGPU global on WebGL-only pages.
44
const SHADER_STAGE_FRAGMENT = 0x2;
122✔
45

46
const A_BUFFER_WGSL = /* wgsl */ `\
122✔
47
const A_BUFFER_EMPTY_FRAGMENT_POINTER: u32 = 0u;
48

49
struct ABufferUniforms {
50
  isActive: i32,
51
  framebufferSize: vec2<f32>,
52
  sliceStartY: i32,
53
};
54

55
struct ABufferHeadPointers {
56
  nextFragmentIndex: atomic<u32>,
57
  droppedFragmentCount: atomic<u32>,
58
  heads: array<atomic<u32>>,
59
};
60

61
struct ABufferFragment {
62
  color: u32,
63
  depth: f32,
64
  next: u32,
65
};
66

67
struct ABufferFragments {
68
  fragments: array<ABufferFragment>,
69
};
70

71
@group(0) @binding(auto) var<uniform> aBuffer: ABufferUniforms;
72
@group(0) @binding(auto) var<storage, read_write> headPointers: ABufferHeadPointers;
73
@group(0) @binding(auto) var<storage, read_write> fragments: ABufferFragments;
74
@group(0) @binding(auto) var opaqueDepthTexture: texture_depth_2d;
75

76
fn aBuffer_getPixelIndex(fragmentPosition: vec4<f32>) -> u32 {
77
  let pixelX = u32(fragmentPosition.x);
78
  let pixelY = u32(i32(fragmentPosition.y) - aBuffer.sliceStartY);
79
  return pixelY * u32(aBuffer.framebufferSize.x) + pixelX;
80
}
81

82
fn aBuffer_capturePremultipliedColor(
83
  color: vec4<f32>,
84
  fragmentPosition: vec4<f32>
85
) -> vec4<f32> {
86
  if (aBuffer.isActive == 0 || color.a <= 0.0) {
87
    return color;
88
  }
89

90
  let fragmentCoordinates = vec2<i32>(fragmentPosition.xy);
91
  let opaqueDepth = textureLoad(opaqueDepthTexture, fragmentCoordinates, 0);
92
  if (fragmentPosition.z >= opaqueDepth) {
93
    return color;
94
  }
95

96
  let fragmentIndex = atomicAdd(&headPointers.nextFragmentIndex, 1u);
97
  if (fragmentIndex >= arrayLength(&fragments.fragments)) {
98
    atomicAdd(&headPointers.droppedFragmentCount, 1u);
99
    return color;
100
  }
101

102
  let pixelIndex = aBuffer_getPixelIndex(fragmentPosition);
103
  if (pixelIndex >= arrayLength(&headPointers.heads)) {
104
    atomicAdd(&headPointers.droppedFragmentCount, 1u);
105
    return color;
106
  }
107

108
  let fragmentPointer = fragmentIndex + 1u;
109
  let nextFragmentPointer = atomicExchange(&headPointers.heads[pixelIndex], fragmentPointer);
110
  fragments.fragments[fragmentIndex] = ABufferFragment(
111
    pack4x8unorm(clamp(color, vec4<f32>(0.0), vec4<f32>(1.0))),
112
    fragmentPosition.z,
113
    nextFragmentPointer
114
  );
115
  return color;
116
}
117

118
fn aBuffer_captureStraightColor(
119
  color: vec4<f32>,
120
  fragmentPosition: vec4<f32>
121
) -> vec4<f32> {
122
  return aBuffer_capturePremultipliedColor(
123
    vec4<f32>(color.rgb * color.a, color.a),
124
    fragmentPosition
125
  );
126
}
127
`;
128

129
function getUniforms(
130
  props: ABufferShaderModuleProps = {},
×
131
  previousUniforms?: ABufferShaderModuleUniforms
132
): Partial<ABufferShaderModuleUniforms & ABufferShaderModuleBindings> {
NEW
133
  const uniforms = {...previousUniforms} as ABufferShaderModuleUniforms;
×
134

NEW
135
  if (props.isActive !== undefined) {
×
NEW
136
    uniforms.isActive = Boolean(props.isActive);
×
137
  }
NEW
138
  if (props.framebufferSize) {
×
NEW
139
    uniforms.framebufferSize = props.framebufferSize;
×
140
  }
NEW
141
  if (props.sliceStartY !== undefined) {
×
NEW
142
    uniforms.sliceStartY = props.sliceStartY;
×
143
  }
144

NEW
145
  return {
×
146
    ...uniforms,
147
    ...(props.headPointers ? {headPointers: props.headPointers} : {}),
×
148
    ...(props.fragments ? {fragments: props.fragments} : {}),
×
149
    ...(props.opaqueDepthTexture ? {opaqueDepthTexture: props.opaqueDepthTexture} : {})
×
150
  };
151
}
152

153
/**
154
 * WGSL shader module that appends translucent fragments to a per-pixel linked-list A-buffer.
155
 *
156
 * Fragment shaders call `aBuffer_captureStraightColor` for straight-alpha colors or
157
 * `aBuffer_capturePremultipliedColor` for premultiplied colors. Both helpers return the input
158
 * color so they can wrap an existing fragment output expression.
159
 */
160
export const aBuffer = {
122✔
161
  name: 'aBuffer',
162
  source: A_BUFFER_WGSL,
163
  bindingLayout: [
164
    {name: 'headPointers', group: 0, visibility: SHADER_STAGE_FRAGMENT},
165
    {name: 'fragments', group: 0, visibility: SHADER_STAGE_FRAGMENT},
166
    {name: 'opaqueDepthTexture', group: 0, visibility: SHADER_STAGE_FRAGMENT}
167
  ],
168
  uniformTypes: {
169
    isActive: 'i32',
170
    framebufferSize: 'vec2<f32>',
171
    sliceStartY: 'i32'
172
  },
173
  defaultUniforms: {
174
    isActive: false,
175
    framebufferSize: [1, 1],
176
    sliceStartY: 0
177
  },
178
  getUniforms
179
} as const satisfies ShaderModule<
180
  ABufferShaderModuleProps,
181
  ABufferShaderModuleUniforms,
182
  ABufferShaderModuleBindings
183
>;
184

185
/**
186
 * Shader plugin that installs the {@link aBuffer} module in WGSL models.
187
 *
188
 * Fragment shaders must still call one of the `aBuffer_capture*` helpers.
189
 */
190
export const aBufferPlugin = {
122✔
191
  name: 'aBuffer',
192
  wgsl: {
193
    modules: [aBuffer as ShaderModule]
194
  }
195
} as const satisfies ShaderPlugin;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc