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

visgl / luma.gl / 28112330114

24 Jun 2026 04:07PM UTC coverage: 70.649% (-0.03%) from 70.68%
28112330114

push

github

web-flow
[codex] Move render draw state to render passes (#2689)

10077 of 16053 branches covered (62.77%)

Branch coverage included in aggregate %.

97 of 112 new or added lines in 8 files covered. (86.61%)

14 existing lines in 5 files now uncovered.

20430 of 27128 relevant lines covered (75.31%)

4011.66 hits per line

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

69.44
/modules/webgpu/src/adapter/resources/webgpu-render-bundle.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {
6
  Bindings,
7
  BindingsByGroup,
8
  RenderPassBindingOptions,
9
  RenderPassDrawOptions,
10
  RenderBundleEncoderProps,
11
  ResourceProps,
12
  VertexArray
13
} from '@luma.gl/core';
14
import {
15
  Buffer,
16
  RenderBundle,
17
  RenderBundleEncoder,
18
  RenderPipeline,
19
  _getDefaultBindGroupFactory
20
} from '@luma.gl/core';
21
import {getWebGPUTextureFormat} from '../helpers/convert-texture-format';
22
import type {WebGPUDevice} from '../webgpu-device';
23
import {WebGPUBuffer} from './webgpu-buffer';
24
import {WebGPURenderPipeline} from './webgpu-render-pipeline';
25

26
/** WebGPU implementation of immutable reusable render commands. */
27
export class WebGPURenderBundle extends RenderBundle {
28
  readonly device: WebGPUDevice;
29
  readonly handle: GPURenderBundle;
30

31
  override get [Symbol.toStringTag](): string {
32
    return 'WebGPURenderBundle';
×
33
  }
34

35
  /**
36
   * Wraps a finished native WebGPU render bundle.
37
   * @param device - Device that owns the native bundle.
38
   * @param props - Resource metadata and the finished native handle.
39
   */
40
  constructor(device: WebGPUDevice, props: ResourceProps & {handle: GPURenderBundle}) {
41
    super(device, props);
2✔
42
    this.device = device;
2✔
43
    this.handle = props.handle;
2✔
44
    this.handle.label = this.props.id;
2✔
45
  }
46

47
  /** Releases the luma.gl resource wrapper. */
48
  override destroy(): void {
49
    this.destroyResource();
2✔
50
  }
51
}
52

53
/** WebGPU implementation of reusable draw-command recording. */
54
export class WebGPURenderBundleEncoder extends RenderBundleEncoder {
55
  readonly device: WebGPUDevice;
56
  readonly handle: GPURenderBundleEncoder;
57

58
  /** Active pipeline */
59
  pipeline: WebGPURenderPipeline | null = null;
2✔
60

61
  /** Latest bindings applied to this encoder */
62
  bindings: Bindings | BindingsByGroup = {};
2✔
63
  private bindingsPipeline: WebGPURenderPipeline | null = null;
2✔
64

65
  /** Vertex array used by subsequent draw commands. */
66
  vertexArray: VertexArray | null = null;
2✔
67

68
  private recordingErrorScopeOpen = false;
2✔
69

70
  /**
71
   * Creates a native WebGPU render bundle encoder.
72
   * @param device - Device that records and owns the finished bundle.
73
   * @param props - Resource metadata and render-attachment compatibility requirements.
74
   */
75
  constructor(device: WebGPUDevice, props: RenderBundleEncoderProps = {}) {
4✔
76
    super(device, props);
4✔
77
    this.device = device;
4✔
78

79
    const descriptor = this.getRenderBundleEncoderDescriptor();
4✔
80
    this.device.pushErrorScope('validation');
4✔
81
    const suppliedHandle = this.props.handle as GPURenderBundleEncoder | undefined;
4✔
82
    this.handle = suppliedHandle || this.device.handle.createRenderBundleEncoder(descriptor);
4✔
83
    this.device.popErrorScope((error: GPUError) => {
4✔
84
      this.device.reportError(new Error(`${this} creation failed:\n"${error.message}"`), this)();
×
85
      this.device.debug();
×
86
    });
87
    this.device.pushErrorScope('validation');
4✔
88
    this.recordingErrorScopeOpen = true;
4✔
89
    this.handle.label = this.props.id;
4✔
90
  }
91

92
  /** Releases the encoder without producing a render bundle. */
93
  override destroy(): void {
94
    this.popRecordingErrorScope('destroy');
2✔
95
    this.destroyResource();
2✔
96
  }
97

98
  /**
99
   * Completes native recording and invalidates this encoder.
100
   * @returns An immutable reusable WebGPU render bundle.
101
   */
102
  finish(): WebGPURenderBundle {
103
    const handle = this.handle.finish({label: this.id});
2✔
104
    const renderBundle = new WebGPURenderBundle(this.device, {
2✔
105
      id: this.id,
106
      userData: this.userData,
107
      handle
108
    });
109
    this.popRecordingErrorScope('finish');
2✔
110
    this.destroy();
2✔
111
    return renderBundle;
2✔
112
  }
113

114
  /** Sets the render pipeline used by subsequent draw commands. */
115
  setPipeline(pipeline: RenderPipeline): void {
116
    this.pipeline = pipeline as WebGPURenderPipeline;
1✔
117
    this.device.pushErrorScope('validation');
1✔
118
    this.handle.setPipeline(this.pipeline.handle);
1✔
119
    this.device.popErrorScope((error: GPUError) => {
1✔
120
      this.device.reportError(new Error(`${this} setPipeline failed:\n"${error.message}"`), this)();
×
121
      this.device.debug();
×
122
    });
123
  }
124

125
  /** Sets bindings used by subsequent draw commands. */
126
  setBindings(bindings: Bindings | BindingsByGroup, options?: RenderPassBindingOptions): void {
127
    if (!this.pipeline) {
1!
NEW
128
      throw new Error('RenderPass.setPipeline() must be called before setBindings()');
×
129
    }
130
    this.bindings = bindings;
1✔
131
    this.bindingsPipeline = this.pipeline;
1✔
132
    const bindGroups = _getDefaultBindGroupFactory(this.device).getBindGroups(
1✔
133
      this.pipeline,
134
      bindings,
135
      options?._bindGroupCacheKeys
136
    );
137
    for (const [group, bindGroup] of Object.entries(bindGroups)) {
1✔
138
      if (bindGroup) {
×
139
        this.handle.setBindGroup(Number(group), bindGroup as GPUBindGroup);
×
140
      }
141
    }
142
  }
143

144
  /** Selects the vertex array used by subsequent draw commands. */
145
  setVertexArray(vertexArray: VertexArray): void {
146
    this.vertexArray = vertexArray;
1✔
147
  }
148

149
  /** Sets the index buffer used by subsequent indexed draw commands. */
150
  setIndexBuffer(
151
    buffer: Buffer,
152
    indexFormat: GPUIndexFormat,
153
    offset: number = 0,
×
154
    size?: number
155
  ): void {
156
    this.handle.setIndexBuffer((buffer as WebGPUBuffer).handle, indexFormat, offset, size);
×
157
  }
158

159
  /** Sets one vertex buffer used by subsequent draw commands. */
160
  setVertexBuffer(slot: number, buffer: Buffer, offset: number = 0): void {
×
161
    this.handle.setVertexBuffer(slot, (buffer as WebGPUBuffer).handle, offset);
×
162
  }
163

164
  /** Records an indexed or non-indexed draw command. */
165
  draw(options: RenderPassDrawOptions): boolean {
166
    if (!this.pipeline) {
1!
NEW
167
      throw new Error('RenderPass.setPipeline() must be called before draw()');
×
168
    }
169
    if (this.pipeline.shaderLayout.bindings.length > 0 && this.bindingsPipeline !== this.pipeline) {
1!
NEW
170
      throw new Error('RenderPass.setBindings() must be called after setPipeline() before draw()');
×
171
    }
172

173
    this.vertexArray?.bindBeforeRender(this);
1✔
174
    if (options.indexCount) {
1!
175
      this.handle.drawIndexed(
×
176
        options.indexCount,
177
        options.instanceCount,
178
        options.firstIndex,
179
        options.baseVertex,
180
        options.firstInstance
181
      );
182
    } else {
183
      this.handle.draw(
1✔
184
        options.vertexCount || 0,
1!
185
        options.instanceCount || 1,
2✔
186
        options.firstVertex,
187
        options.firstInstance
188
      );
189
    }
190
    this.vertexArray?.unbindAfterRender(this);
1✔
191
    return true;
1✔
192
  }
193

194
  /** Reserved for indirect draw support. */
195
  drawIndirect(): void {
196
    // drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): void;
197
    // drawIndexedIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): void;
198
  }
199

200
  /**
201
   * Render bundle encoders cannot replay other render bundles.
202
   * @throws Always throws.
203
   */
204
  executeBundles(_bundles: Iterable<RenderBundle>): void {
205
    throw new Error('RenderBundleEncoder.executeBundles() is not supported');
×
206
  }
207

208
  /** Begins a labeled debug group in the recorded command stream. */
209
  pushDebugGroup(groupLabel: string): void {
210
    this.handle.pushDebugGroup(groupLabel);
2✔
211
  }
212

213
  /** Ends the most recently begun debug group. */
214
  popDebugGroup(): void {
215
    this.handle.popDebugGroup();
2✔
216
  }
217

218
  /** Inserts a labeled debug marker into the recorded command stream. */
219
  insertDebugMarker(markerLabel: string): void {
220
    this.handle.insertDebugMarker(markerLabel);
×
221
  }
222

223
  private popRecordingErrorScope(operationName: 'destroy' | 'finish'): void {
224
    if (!this.recordingErrorScopeOpen) {
4✔
225
      return;
2✔
226
    }
227

228
    this.recordingErrorScopeOpen = false;
2✔
229
    this.device.popErrorScope((error: GPUError) => {
2✔
230
      this.device.reportError(
×
231
        new Error(`${this} ${operationName} failed:\n"${error.message}"`),
232
        this
233
      )();
234
      this.device.debug();
×
235
    });
236
  }
237

238
  private getRenderBundleEncoderDescriptor(): GPURenderBundleEncoderDescriptor {
239
    const renderBundleEncoderProps = this.props as unknown as Required<RenderBundleEncoderProps>;
2✔
240
    const descriptor: GPURenderBundleEncoderDescriptor = {
2✔
241
      label: this.props.id,
242
      colorFormats: renderBundleEncoderProps.colorAttachmentFormats.map(format =>
243
        format ? getWebGPUTextureFormat(format) : null
2!
244
      ),
245
      sampleCount: renderBundleEncoderProps.sampleCount
246
    };
247

248
    if (renderBundleEncoderProps.depthStencilAttachmentFormat) {
2!
249
      descriptor.depthStencilFormat = getWebGPUTextureFormat(
2✔
250
        renderBundleEncoderProps.depthStencilAttachmentFormat
251
      );
252
    }
253
    if (this.props.depthReadOnly) {
2!
254
      descriptor.depthReadOnly = true;
×
255
    }
256
    if (this.props.stencilReadOnly) {
2!
257
      descriptor.stencilReadOnly = true;
×
258
    }
259

260
    return descriptor;
2✔
261
  }
262
}
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