WebGPU Classes

See the WebGPU section for tutorials and usage guides.

WebGPUWidget

Bases: QWidget

An abstract base class for offscreen WebGPU widgets.

The rendered content is presented via QPainter.

The scene is rendered to an offscreen texture, read back to a numpy buffer, and blitted to the widget with QPainter. This keeps the whole data flow visible and allows QPainter text overlays via render_text().

The readback is pipelined through a small ring of buffers: each frame the current image is copied into one buffer while the previous frame's buffer is mapped and read. Mapping a buffer whose copy was submitted a frame ago returns almost immediately, so the CPU never stalls waiting for the GPU to drain. The presented image therefore lags the simulation by one frame, which is imperceptible at interactive rates.

Subclasses must implement paintWebGPU() and resizeWebGPU(), and must set self.device to a wgpu device before any rendering can occur. The base class owns the render target textures and the readback machinery; resizeWebGPU() should only update subclass state (projection matrix, per-pipeline sizes etc.) and must NOT recreate render buffers itself.

Source code in ncca/ngl/webgpu/webgpu_widget.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
class WebGPUWidget(QWidget, metaclass=QWidgetABCMeta):
    """An abstract base class for offscreen WebGPU widgets.

    The rendered content is presented via QPainter.

    The scene is rendered to an offscreen texture, read back to a numpy
    buffer, and blitted to the widget with QPainter. This keeps the whole
    data flow visible and allows QPainter text overlays via render_text().

    The readback is pipelined through a small ring of buffers: each frame
    the current image is copied into one buffer while the *previous*
    frame's buffer is mapped and read. Mapping a buffer whose copy was
    submitted a frame ago returns almost immediately, so the CPU never
    stalls waiting for the GPU to drain. The presented image therefore
    lags the simulation by one frame, which is imperceptible at
    interactive rates.

    Subclasses must implement paintWebGPU() and resizeWebGPU(), and must
    set self.device to a wgpu device before any rendering can occur. The
    base class owns the render target textures and the readback machinery;
    resizeWebGPU() should only update subclass state (projection matrix,
    per-pipeline sizes etc.) and must NOT recreate render buffers itself.
    """

    def __init__(self) -> None:
        """Initialize the widget.

        Note the wgpu device is not created here; subclasses are
        responsible for creating it (self.device) before the first paint.
        """
        super().__init__()
        self.device: Optional[wgpu.GPUDevice] = None
        self.msaa_sample_count = 4
        self.text_buffer: List[Tuple[int, int, str, int, str, QColor]] = []
        self.frame_buffer: Optional[np.ndarray] = None
        self._update_timer = QTimer(self)
        self._update_timer.timeout.connect(self.update)
        # Device pixel ratio for high-DPI displays. This is re-queried on
        # resize and paint, as it changes when the window moves between
        # screens with different scale factors.
        self.ratio = self.devicePixelRatioF()
        self._initialize_buffer()

    # ------------------------------------------------------------------
    # Timer control
    # ------------------------------------------------------------------
    def start_update_timer(self, interval_ms: int) -> None:
        """Start the update timer with the given interval.

        Args:
            interval_ms (int): The interval in milliseconds.
        """
        self._update_timer.start(interval_ms)

    def stop_update_timer(self) -> None:
        """Stop the update timer."""
        self._update_timer.stop()

    # ------------------------------------------------------------------
    # Abstract interface
    # ------------------------------------------------------------------
    @abstractmethod
    def resizeWebGPU(self, width: int, height: int) -> None:
        """Handle a resize, after the buffers are recreated at the new size.

        Subclasses should update projection matrices and any per-pipeline
        sizes here. Width and height are in device pixels.

        Args:
            width (int): New width in device pixels.
            height (int): New height in device pixels.
        """
        pass

    @abstractmethod
    def paintWebGPU(self) -> None:
        """Render the WebGPU content.

        Called on every paint event; all the main rendering code goes
        here. Implementations should end by calling
        _update_colour_buffer() so the rendered frame is read back for
        presentation.
        """
        pass

    # ------------------------------------------------------------------
    # Qt event handlers
    # ------------------------------------------------------------------
    def resizeEvent(self, event: QResizeEvent) -> None:
        """Handle window resize.

        Recreates the render targets and the readback frame buffer at the
        new size (in device pixels), then notifies the subclass via
        resizeWebGPU().
        """
        self.ratio = self.devicePixelRatioF()
        width = int(event.size().width() * self.ratio)
        height = int(event.size().height() * self.ratio)

        # A minimised or degenerate window gives a zero dimension, which
        # is a wgpu validation error - skip until we have a real size.
        if width <= 0 or height <= 0:
            return super().resizeEvent(event)

        self.texture_size = (width, height)
        self.frame_buffer = np.zeros([height, width, 4], dtype=np.uint8)

        if self.device is not None:
            self._create_render_buffer()
            self.resizeWebGPU(width, height)

        return super().resizeEvent(event)

    def paintEvent(self, event: QPaintEvent) -> None:
        """Handle the paint event to render and present the WebGPU content."""
        # The device pixel ratio can change without a resize when the
        # window moves between screens - rebuild render targets if it has.
        current_ratio = self.devicePixelRatioF()
        if current_ratio != self.ratio:
            self.ratio = current_ratio
            width = int(self.width() * self.ratio)
            height = int(self.height() * self.ratio)
            if width > 0 and height > 0:
                self.texture_size = (width, height)
                self.frame_buffer = np.zeros([height, width, 4], dtype=np.uint8)
                if self.device is not None:
                    self._create_render_buffer()
                    self.resizeWebGPU(width, height)

        if self.device is not None:
            self.paintWebGPU()

        painter = QPainter(self)
        if self.frame_buffer is not None:
            self._present_image(painter, self.frame_buffer)

        # Draw any queued text. Font size is scaled relative to a base
        # window height so text keeps its proportions when resizing.
        base_height = 600.0
        scale_factor = self.height() / base_height
        for x, y, text, size, font, colour in self.text_buffer:
            scaled_size = int(size * scale_factor)
            painter.setPen(colour)
            painter.setFont(QFont(font, scaled_size))
            draw_y = y
            if y < 0:
                draw_y = self.height() + y
            painter.drawText(x, draw_y, text)
        self.text_buffer.clear()
        painter.end()

    # ------------------------------------------------------------------
    # Buffer / texture management
    # ------------------------------------------------------------------
    def _initialize_buffer(self) -> None:
        """Initialize the numpy buffer used for the final framebuffer render."""
        width = max(1, int(self.width() * self.ratio))
        height = max(1, int(self.height() * self.ratio))
        self.frame_buffer = np.zeros([height, width, 4], dtype=np.uint8)
        self.texture_size = (width, height)

    def _create_render_buffer(self) -> None:
        """Create the render target textures and the readback buffer ring.

        Requires self.device to be set. Called automatically on resize;
        subclasses should call it once after creating the device.
        Recreating the ring resets the pending flags, which conveniently
        discards any in-flight copy at the old size.
        """
        if self.device is None:
            raise RuntimeError("self.device must be set before creating render buffers")

        # The single-sample texture the MSAA render is resolved into, and
        # which is copied back to the CPU for presentation.
        self.colour_buffer_texture = self.device.create_texture(
            size=self.texture_size,
            sample_count=1,
            format=wgpu.TextureFormat.rgba8unorm,
            usage=wgpu.TextureUsage.RENDER_ATTACHMENT | wgpu.TextureUsage.COPY_SRC,
        )
        self.colour_buffer_texture_view = self.colour_buffer_texture.create_view()

        # The multisampled texture that is actually rendered to.
        self.multisample_texture = self.device.create_texture(
            size=self.texture_size,
            sample_count=self.msaa_sample_count,
            format=wgpu.TextureFormat.rgba8unorm,
            usage=wgpu.TextureUsage.RENDER_ATTACHMENT,
        )
        self.multisample_texture_view = self.multisample_texture.create_view()

        # Depth buffer (multisampled to match). Keep an explicit reference
        # to the texture as well as the view so ownership is obvious.
        self.depth_buffer_texture = self.device.create_texture(
            size=self.texture_size,
            format=wgpu.TextureFormat.depth24plus,
            usage=wgpu.TextureUsage.RENDER_ATTACHMENT,
            sample_count=self.msaa_sample_count,
        )
        self.depth_buffer_view = self.depth_buffer_texture.create_view()

        # Ring of buffers for reading the resolved texture back to the
        # CPU. Rows are padded to the spec-mandated 256 byte alignment.
        buffer_size = self._calculate_aligned_buffer_size()
        self.readback_buffers = [
            self.device.create_buffer(
                size=buffer_size,
                usage=wgpu.BufferUsage.COPY_DST | wgpu.BufferUsage.MAP_READ,
                label=f"readback_buffer_{i}",
            )
            for i in range(NUM_READBACK_BUFFERS)
        ]
        # Index of the buffer the next copy will go into, and per-buffer
        # flags recording whether it holds a frame we haven't read yet.
        self._readback_index = 0
        self._readback_pending = [False] * NUM_READBACK_BUFFERS

    def render_text(
        self,
        x: int,
        y: int,
        text: str,
        size: int = 10,
        font: str = "Arial",
        colour: Union[QColor, Qt.GlobalColor] = Qt.black,
    ) -> None:
        """Queue text to be drawn over the rendered frame.

        The text buffer is cleared each frame, so text must be re-added
        every frame (typically from paintWebGPU or a timer callback).
        Note the rendered image lags the simulation by one frame due to
        the pipelined readback, so text meant to track a moving 3D
        object's screen position will swim slightly during fast motion;
        HUD-style labels are unaffected.

        Args:
            x (int): The x-coordinate of the text.
            y (int): The y-coordinate of the text. A negative value
                positions the text relative to the bottom of the window.
            text (str): The text to render.
            size (int, optional): Base font size, scaled with window
                height. Defaults to 10.
            font (str, optional): Font family. Defaults to "Arial".
            colour: Text colour. Defaults to Qt.black.
        """
        self.text_buffer.append((x, y, text, size, font, QColor(colour)))

    def _calculate_aligned_row_size(self) -> int:
        """Calculate the row stride for texture copy operations.

        The stride is padded to the WebGPU spec's
        COPY_BYTES_PER_ROW_ALIGNMENT (256 bytes).
        """
        bytes_per_pixel = 4  # rgba8unorm
        raw_row_size = self.texture_size[0] * bytes_per_pixel
        alignment = COPY_BYTES_PER_ROW_ALIGNMENT
        return ((raw_row_size + alignment - 1) // alignment) * alignment

    def _calculate_aligned_buffer_size(self) -> int:
        """Calculate the total readback buffer size.

        This is the aligned row stride times the number of rows.
        """
        return self._calculate_aligned_row_size() * self.texture_size[1]

    def _update_colour_buffer(self) -> None:
        """Copy this frame out, and read the previous frame back in.

        A buffer is only ever mapped when it is not the copy target, and
        is unmapped before it becomes the copy target again - the
        alternation guarantees this. The first frame after startup or a
        resize has nothing pending, so the previous frame buffer contents
        are shown once and everything flows from the next frame on.
        """
        # A subclass that overrides _create_render_buffer must still create the
        # read-back ring - easiest by calling super()._create_render_buffer().
        # Without it the copy-back below would raise AttributeError on the ring
        # attributes, which the try/except would swallow every single frame,
        # leaving a grey window and no obvious cause. Fail loudly instead.
        if not getattr(self, "readback_buffers", None):
            raise RuntimeError(
                "WebGPUWidget read-back ring is not initialised. A subclass that "
                "overrides _create_render_buffer() must call "
                "super()._create_render_buffer() (or otherwise create the "
                "readback_buffers ring) before rendering."
            )

        bytes_per_row = self._calculate_aligned_row_size()
        width, height = self.texture_size
        try:
            write_idx = self._readback_index
            read_idx = (write_idx + 1) % len(self.readback_buffers)

            # Kick off the copy of this frame. We do not wait for it.
            command_encoder = self.device.create_command_encoder()
            command_encoder.copy_texture_to_buffer(
                {"texture": self.colour_buffer_texture},
                {
                    "buffer": self.readback_buffers[write_idx],
                    "bytes_per_row": bytes_per_row,
                    "rows_per_image": height,
                },
                (width, height, 1),
            )
            self.device.queue.submit([command_encoder.finish()])
            self._readback_pending[write_idx] = True

            # Read back last frame's buffer, if it holds one. The map
            # returns almost immediately because the GPU has had a full
            # frame to complete that copy.
            if self._readback_pending[read_idx]:
                buf = self.readback_buffers[read_idx]
                buf.map_sync(mode=wgpu.MapMode.READ)
                raw_data = buf.read_mapped()

                # The raw data includes per-row padding to meet the
                # alignment requirement, so build a strided view and copy
                # it to a contiguous array before unmapping.
                strided_view = np.lib.stride_tricks.as_strided(
                    np.frombuffer(raw_data, dtype=np.uint8),
                    shape=(height, width, 4),
                    strides=(bytes_per_row, 4, 1),
                )
                self.frame_buffer = np.copy(strided_view)
                buf.unmap()
                self._readback_pending[read_idx] = False

            # Alternate: next frame writes into the buffer just drained.
            self._readback_index = read_idx
        except Exception:
            # A genuine (usually transient) GPU/mapping error - log it with a
            # traceback so it is visible, and fall back to a grey frame rather
            # than tearing down the event loop.
            logger.exception("Failed to update colour buffer")
            if self.frame_buffer is not None:
                self.frame_buffer.fill(128)

    def _present_image(self, painter: QPainter, image_data: np.ndarray) -> None:
        """Present the frame buffer on the widget.

        The image is tagged with the device pixel ratio so Qt blits it
        1:1 to the physical pixels rather than rescaling a
        device-pixel-sized image into a logical-pixel rect.

        Args:
            painter (QPainter): The active painter.
            image_data (np.ndarray): The image data to render.
        """
        height, width, _ = image_data.shape
        image = QImage(
            image_data.data,
            width,
            height,
            image_data.strides[0],
            QImage.Format.Format_RGBA8888,
        )
        image.setDevicePixelRatio(self.ratio)
        painter.drawImage(0, 0, image)

__init__()

Initialize the widget.

Note the wgpu device is not created here; subclasses are responsible for creating it (self.device) before the first paint.

Source code in ncca/ngl/webgpu/webgpu_widget.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def __init__(self) -> None:
    """Initialize the widget.

    Note the wgpu device is not created here; subclasses are
    responsible for creating it (self.device) before the first paint.
    """
    super().__init__()
    self.device: Optional[wgpu.GPUDevice] = None
    self.msaa_sample_count = 4
    self.text_buffer: List[Tuple[int, int, str, int, str, QColor]] = []
    self.frame_buffer: Optional[np.ndarray] = None
    self._update_timer = QTimer(self)
    self._update_timer.timeout.connect(self.update)
    # Device pixel ratio for high-DPI displays. This is re-queried on
    # resize and paint, as it changes when the window moves between
    # screens with different scale factors.
    self.ratio = self.devicePixelRatioF()
    self._initialize_buffer()

paintEvent(event)

Handle the paint event to render and present the WebGPU content.

Source code in ncca/ngl/webgpu/webgpu_widget.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def paintEvent(self, event: QPaintEvent) -> None:
    """Handle the paint event to render and present the WebGPU content."""
    # The device pixel ratio can change without a resize when the
    # window moves between screens - rebuild render targets if it has.
    current_ratio = self.devicePixelRatioF()
    if current_ratio != self.ratio:
        self.ratio = current_ratio
        width = int(self.width() * self.ratio)
        height = int(self.height() * self.ratio)
        if width > 0 and height > 0:
            self.texture_size = (width, height)
            self.frame_buffer = np.zeros([height, width, 4], dtype=np.uint8)
            if self.device is not None:
                self._create_render_buffer()
                self.resizeWebGPU(width, height)

    if self.device is not None:
        self.paintWebGPU()

    painter = QPainter(self)
    if self.frame_buffer is not None:
        self._present_image(painter, self.frame_buffer)

    # Draw any queued text. Font size is scaled relative to a base
    # window height so text keeps its proportions when resizing.
    base_height = 600.0
    scale_factor = self.height() / base_height
    for x, y, text, size, font, colour in self.text_buffer:
        scaled_size = int(size * scale_factor)
        painter.setPen(colour)
        painter.setFont(QFont(font, scaled_size))
        draw_y = y
        if y < 0:
            draw_y = self.height() + y
        painter.drawText(x, draw_y, text)
    self.text_buffer.clear()
    painter.end()

paintWebGPU() abstractmethod

Render the WebGPU content.

Called on every paint event; all the main rendering code goes here. Implementations should end by calling _update_colour_buffer() so the rendered frame is read back for presentation.

Source code in ncca/ngl/webgpu/webgpu_widget.py
114
115
116
117
118
119
120
121
122
123
@abstractmethod
def paintWebGPU(self) -> None:
    """Render the WebGPU content.

    Called on every paint event; all the main rendering code goes
    here. Implementations should end by calling
    _update_colour_buffer() so the rendered frame is read back for
    presentation.
    """
    pass

render_text(x, y, text, size=10, font='Arial', colour=Qt.black)

Queue text to be drawn over the rendered frame.

The text buffer is cleared each frame, so text must be re-added every frame (typically from paintWebGPU or a timer callback). Note the rendered image lags the simulation by one frame due to the pipelined readback, so text meant to track a moving 3D object's screen position will swim slightly during fast motion; HUD-style labels are unaffected.

Parameters:
  • x (int) –

    The x-coordinate of the text.

  • y (int) –

    The y-coordinate of the text. A negative value positions the text relative to the bottom of the window.

  • text (str) –

    The text to render.

  • size (int, default: 10 ) –

    Base font size, scaled with window height. Defaults to 10.

  • font (str, default: 'Arial' ) –

    Font family. Defaults to "Arial".

  • colour (Union[QColor, GlobalColor], default: black ) –

    Text colour. Defaults to Qt.black.

Source code in ncca/ngl/webgpu/webgpu_widget.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def render_text(
    self,
    x: int,
    y: int,
    text: str,
    size: int = 10,
    font: str = "Arial",
    colour: Union[QColor, Qt.GlobalColor] = Qt.black,
) -> None:
    """Queue text to be drawn over the rendered frame.

    The text buffer is cleared each frame, so text must be re-added
    every frame (typically from paintWebGPU or a timer callback).
    Note the rendered image lags the simulation by one frame due to
    the pipelined readback, so text meant to track a moving 3D
    object's screen position will swim slightly during fast motion;
    HUD-style labels are unaffected.

    Args:
        x (int): The x-coordinate of the text.
        y (int): The y-coordinate of the text. A negative value
            positions the text relative to the bottom of the window.
        text (str): The text to render.
        size (int, optional): Base font size, scaled with window
            height. Defaults to 10.
        font (str, optional): Font family. Defaults to "Arial".
        colour: Text colour. Defaults to Qt.black.
    """
    self.text_buffer.append((x, y, text, size, font, QColor(colour)))

resizeEvent(event)

Handle window resize.

Recreates the render targets and the readback frame buffer at the new size (in device pixels), then notifies the subclass via resizeWebGPU().

Source code in ncca/ngl/webgpu/webgpu_widget.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def resizeEvent(self, event: QResizeEvent) -> None:
    """Handle window resize.

    Recreates the render targets and the readback frame buffer at the
    new size (in device pixels), then notifies the subclass via
    resizeWebGPU().
    """
    self.ratio = self.devicePixelRatioF()
    width = int(event.size().width() * self.ratio)
    height = int(event.size().height() * self.ratio)

    # A minimised or degenerate window gives a zero dimension, which
    # is a wgpu validation error - skip until we have a real size.
    if width <= 0 or height <= 0:
        return super().resizeEvent(event)

    self.texture_size = (width, height)
    self.frame_buffer = np.zeros([height, width, 4], dtype=np.uint8)

    if self.device is not None:
        self._create_render_buffer()
        self.resizeWebGPU(width, height)

    return super().resizeEvent(event)

resizeWebGPU(width, height) abstractmethod

Handle a resize, after the buffers are recreated at the new size.

Subclasses should update projection matrices and any per-pipeline sizes here. Width and height are in device pixels.

Parameters:
  • width (int) –

    New width in device pixels.

  • height (int) –

    New height in device pixels.

Source code in ncca/ngl/webgpu/webgpu_widget.py
101
102
103
104
105
106
107
108
109
110
111
112
@abstractmethod
def resizeWebGPU(self, width: int, height: int) -> None:
    """Handle a resize, after the buffers are recreated at the new size.

    Subclasses should update projection matrices and any per-pipeline
    sizes here. Width and height are in device pixels.

    Args:
        width (int): New width in device pixels.
        height (int): New height in device pixels.
    """
    pass

start_update_timer(interval_ms)

Start the update timer with the given interval.

Parameters:
  • interval_ms (int) –

    The interval in milliseconds.

Source code in ncca/ngl/webgpu/webgpu_widget.py
86
87
88
89
90
91
92
def start_update_timer(self, interval_ms: int) -> None:
    """Start the update timer with the given interval.

    Args:
        interval_ms (int): The interval in milliseconds.
    """
    self._update_timer.start(interval_ms)

stop_update_timer()

Stop the update timer.

Source code in ncca/ngl/webgpu/webgpu_widget.py
94
95
96
def stop_update_timer(self) -> None:
    """Stop the update timer."""
    self._update_timer.stop()

PipelineType

Bases: Enum

Enumeration of available pipeline types.

Source code in ncca/ngl/webgpu/pipeline_factory.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class PipelineType(Enum):
    """Enumeration of available pipeline types."""

    MULTI_COLOURED_LINES = "multi_coloured_lines"
    SINGLE_COLOUR_LINES = "single_colour_lines"
    MULTI_COLOURED_POINTS = "multi_coloured_points"
    SINGLE_COLOUR_POINTS = "single_colour_points"
    MULTI_COLOURED_TRIANGLES = "multi_coloured_triangles"
    SINGLE_COLOUR_TRIANGLES = "single_colour_triangles"
    TRIANGLE_LIST_MULTI_COLOURED = "triangle_list_multi_coloured"
    TRIANGLE_LIST_SINGLE_COLOUR = "triangle_list_single_colour"
    TRIANGLE_STRIP_MULTI_COLOURED = "triangle_strip_multi_coloured"
    TRIANGLE_STRIP_SINGLE_COLOUR = "triangle_strip_single_colour"
    POINT_LIST_MULTI_COLOURED = "point_list_multi_coloured"
    POINT_LIST_SINGLE_COLOUR = "point_list_single_colour"
    MULTI_COLOURED_INSTANCED_GEOMETRY = "multi_coloured_instanced_geometry"
    SINGLE_COLOUR_INSTANCED_GEOMETRY = "single_colour_instanced_geometry"

PipelineFactory

PipelineFactory is a module-level singleton instance of the factory class below — use it directly (PipelineFactory.create_pipeline(device, pipeline_type)) rather than instantiating your own.

Factory for creating pipeline instances with various configurations.

Source code in ncca/ngl/webgpu/pipeline_factory.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
class _PipelineFactory:
    """Factory for creating pipeline instances with various configurations."""

    def __init__(self) -> None:
        """Initialize the pipeline factory with default pipeline types."""
        self._pipeline_registry: Dict[PipelineType, PipelineFactoryFn] = {}
        self.register_pipeline(
            PipelineType.MULTI_COLOURED_POINTS, PointPipelineMultiColour
        )
        self.register_pipeline(
            PipelineType.SINGLE_COLOUR_POINTS, PointPipelineSingleColour
        )
        self.register_pipeline(
            PipelineType.MULTI_COLOURED_LINES, LinePipelineMultiColour
        )
        self.register_pipeline(
            PipelineType.SINGLE_COLOUR_LINES, LinePipelineSingleColour
        )
        self.register_pipeline(
            PipelineType.MULTI_COLOURED_TRIANGLES, TrianglePipelineMultiColour
        )
        self.register_pipeline(
            PipelineType.SINGLE_COLOUR_TRIANGLES, TrianglePipelineSingleColour
        )

        # Triangle pipelines pinned to a specific topology: registered as
        # factory callables (rather than classes) so the fixed `topology`
        # keyword can be bound while still forwarding any other **kwargs
        # create_pipeline receives (e.g. `colour`, `data_type`) to the
        # underlying pipeline class.
        self.register_pipeline(
            PipelineType.TRIANGLE_LIST_MULTI_COLOURED,
            lambda device, **kwargs: TrianglePipelineMultiColour(
                device, topology=wgpu.PrimitiveTopology.triangle_list, **kwargs
            ),
        )
        self.register_pipeline(
            PipelineType.TRIANGLE_LIST_SINGLE_COLOUR,
            lambda device, **kwargs: TrianglePipelineSingleColour(
                device, topology=wgpu.PrimitiveTopology.triangle_list, **kwargs
            ),
        )
        self.register_pipeline(
            PipelineType.TRIANGLE_STRIP_MULTI_COLOURED,
            lambda device, **kwargs: TrianglePipelineMultiColour(
                device, topology=wgpu.PrimitiveTopology.triangle_strip, **kwargs
            ),
        )
        self.register_pipeline(
            PipelineType.TRIANGLE_STRIP_SINGLE_COLOUR,
            lambda device, **kwargs: TrianglePipelineSingleColour(
                device, topology=wgpu.PrimitiveTopology.triangle_strip, **kwargs
            ),
        )
        self.register_pipeline(
            PipelineType.POINT_LIST_MULTI_COLOURED, PointListPipelineMultiColour
        )
        self.register_pipeline(
            PipelineType.POINT_LIST_SINGLE_COLOUR, PointListPipelineSingleColour
        )
        self.register_pipeline(
            PipelineType.MULTI_COLOURED_INSTANCED_GEOMETRY,
            InstancedGeometryPipelineMultiColour,
        )
        self.register_pipeline(
            PipelineType.SINGLE_COLOUR_INSTANCED_GEOMETRY,
            InstancedGeometryPipelineSingleColour,
        )

    def register_pipeline(
        self, pipeline_type: PipelineType, pipeline_factory: PipelineFactoryFn
    ) -> None:
        """Register a custom pipeline type.

        Args:
            pipeline_type: Enum identifier for the pipeline
            pipeline_factory: Pipeline class, or a factory callable with the
                same `(device, **kwargs) -> BaseWebGPUPipeline` signature, to
                register for this type
        """
        self._pipeline_registry[pipeline_type] = pipeline_factory

    def create_pipeline(
        self, device: wgpu.GPUDevice, pipeline_type: PipelineType, **kwargs: Any
    ) -> BaseWebGPUPipeline:
        """Create a pipeline instance.

        Args:
            device: WebGPU device
            pipeline_type: Type of pipeline to create
            **kwargs: Pipeline-specific configuration parameters

        Returns:
            Configured pipeline instance

        Raises:
            ValueError: If pipeline type is not registered
        """
        if pipeline_type not in self._pipeline_registry:
            raise ValueError(
                f"Unknown pipeline type: {pipeline_type}. Available types: {list(self._pipeline_registry.keys())}"
            )

        pipeline_factory = self._pipeline_registry[pipeline_type]
        return pipeline_factory(device, **kwargs)

__init__()

Initialize the pipeline factory with default pipeline types.

Source code in ncca/ngl/webgpu/pipeline_factory.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def __init__(self) -> None:
    """Initialize the pipeline factory with default pipeline types."""
    self._pipeline_registry: Dict[PipelineType, PipelineFactoryFn] = {}
    self.register_pipeline(
        PipelineType.MULTI_COLOURED_POINTS, PointPipelineMultiColour
    )
    self.register_pipeline(
        PipelineType.SINGLE_COLOUR_POINTS, PointPipelineSingleColour
    )
    self.register_pipeline(
        PipelineType.MULTI_COLOURED_LINES, LinePipelineMultiColour
    )
    self.register_pipeline(
        PipelineType.SINGLE_COLOUR_LINES, LinePipelineSingleColour
    )
    self.register_pipeline(
        PipelineType.MULTI_COLOURED_TRIANGLES, TrianglePipelineMultiColour
    )
    self.register_pipeline(
        PipelineType.SINGLE_COLOUR_TRIANGLES, TrianglePipelineSingleColour
    )

    # Triangle pipelines pinned to a specific topology: registered as
    # factory callables (rather than classes) so the fixed `topology`
    # keyword can be bound while still forwarding any other **kwargs
    # create_pipeline receives (e.g. `colour`, `data_type`) to the
    # underlying pipeline class.
    self.register_pipeline(
        PipelineType.TRIANGLE_LIST_MULTI_COLOURED,
        lambda device, **kwargs: TrianglePipelineMultiColour(
            device, topology=wgpu.PrimitiveTopology.triangle_list, **kwargs
        ),
    )
    self.register_pipeline(
        PipelineType.TRIANGLE_LIST_SINGLE_COLOUR,
        lambda device, **kwargs: TrianglePipelineSingleColour(
            device, topology=wgpu.PrimitiveTopology.triangle_list, **kwargs
        ),
    )
    self.register_pipeline(
        PipelineType.TRIANGLE_STRIP_MULTI_COLOURED,
        lambda device, **kwargs: TrianglePipelineMultiColour(
            device, topology=wgpu.PrimitiveTopology.triangle_strip, **kwargs
        ),
    )
    self.register_pipeline(
        PipelineType.TRIANGLE_STRIP_SINGLE_COLOUR,
        lambda device, **kwargs: TrianglePipelineSingleColour(
            device, topology=wgpu.PrimitiveTopology.triangle_strip, **kwargs
        ),
    )
    self.register_pipeline(
        PipelineType.POINT_LIST_MULTI_COLOURED, PointListPipelineMultiColour
    )
    self.register_pipeline(
        PipelineType.POINT_LIST_SINGLE_COLOUR, PointListPipelineSingleColour
    )
    self.register_pipeline(
        PipelineType.MULTI_COLOURED_INSTANCED_GEOMETRY,
        InstancedGeometryPipelineMultiColour,
    )
    self.register_pipeline(
        PipelineType.SINGLE_COLOUR_INSTANCED_GEOMETRY,
        InstancedGeometryPipelineSingleColour,
    )

create_pipeline(device, pipeline_type, **kwargs)

Create a pipeline instance.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • pipeline_type (PipelineType) –

    Type of pipeline to create

  • **kwargs (Any, default: {} ) –

    Pipeline-specific configuration parameters

Returns:
Raises:
  • ValueError

    If pipeline type is not registered

Source code in ncca/ngl/webgpu/pipeline_factory.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def create_pipeline(
    self, device: wgpu.GPUDevice, pipeline_type: PipelineType, **kwargs: Any
) -> BaseWebGPUPipeline:
    """Create a pipeline instance.

    Args:
        device: WebGPU device
        pipeline_type: Type of pipeline to create
        **kwargs: Pipeline-specific configuration parameters

    Returns:
        Configured pipeline instance

    Raises:
        ValueError: If pipeline type is not registered
    """
    if pipeline_type not in self._pipeline_registry:
        raise ValueError(
            f"Unknown pipeline type: {pipeline_type}. Available types: {list(self._pipeline_registry.keys())}"
        )

    pipeline_factory = self._pipeline_registry[pipeline_type]
    return pipeline_factory(device, **kwargs)

register_pipeline(pipeline_type, pipeline_factory)

Register a custom pipeline type.

Parameters:
  • pipeline_type (PipelineType) –

    Enum identifier for the pipeline

  • pipeline_factory (PipelineFactoryFn) –

    Pipeline class, or a factory callable with the same (device, **kwargs) -> BaseWebGPUPipeline signature, to register for this type

Source code in ncca/ngl/webgpu/pipeline_factory.py
124
125
126
127
128
129
130
131
132
133
134
135
def register_pipeline(
    self, pipeline_type: PipelineType, pipeline_factory: PipelineFactoryFn
) -> None:
    """Register a custom pipeline type.

    Args:
        pipeline_type: Enum identifier for the pipeline
        pipeline_factory: Pipeline class, or a factory callable with the
            same `(device, **kwargs) -> BaseWebGPUPipeline` signature, to
            register for this type
    """
    self._pipeline_registry[pipeline_type] = pipeline_factory

NGLToWebGPU

Maps NGL type names to WebGPU strides and vertex formats.

Source code in ncca/ngl/webgpu/webgpu_constants.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class NGLToWebGPU:
    """Maps NGL type names to WebGPU strides and vertex formats."""

    _strides = {
        "vec2": 2 * FLOAT_SIZE,
        "vec3": 3 * FLOAT_SIZE,
        "vec4": 4 * FLOAT_SIZE,
        "mat2": 4 * FLOAT_SIZE,
        "mat3": 12 * FLOAT_SIZE,
        "mat4": 16 * FLOAT_SIZE,
    }
    _vertex_format = {
        "vec2": "float32x2",
        "vec3": "float32x3",
        "vec4": "float32x4",
    }

    @staticmethod
    def stride_from_type(type: str) -> int:
        """Return the byte stride for the given NGL type name."""
        return NGLToWebGPU._strides[type.lower()]

    @staticmethod
    def vertex_format(type: str) -> str:
        """Return the WebGPU vertex format for the given NGL type name."""
        return NGLToWebGPU._vertex_format[type.lower()]

stride_from_type(type) staticmethod

Return the byte stride for the given NGL type name.

Source code in ncca/ngl/webgpu/webgpu_constants.py
26
27
28
29
@staticmethod
def stride_from_type(type: str) -> int:
    """Return the byte stride for the given NGL type name."""
    return NGLToWebGPU._strides[type.lower()]

vertex_format(type) staticmethod

Return the WebGPU vertex format for the given NGL type name.

Source code in ncca/ngl/webgpu/webgpu_constants.py
31
32
33
34
@staticmethod
def vertex_format(type: str) -> str:
    """Return the WebGPU vertex format for the given NGL type name."""
    return NGLToWebGPU._vertex_format[type.lower()]

BaseWebGPUPipeline

Bases: ABC

Abstract base class for all WebGPU rendering pipelines.

Provides common functionality for: - Buffer management and creation - Pipeline configuration - Uniform buffer handling - Resource cleanup

Source code in ncca/ngl/webgpu/base_webgpu_pipeline.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
class BaseWebGPUPipeline(ABC):
    """Abstract base class for all WebGPU rendering pipelines.

    Provides common functionality for:
    - Buffer management and creation
    - Pipeline configuration
    - Uniform buffer handling
    - Resource cleanup
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        data_type: str = "Vec3",
        stride: int = 0,
    ) -> None:
        """Initialize base pipeline.

        Args:
            device: WebGPU device
            texture_format: Colour attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            data_type: Vertex data type (e.g., "Vec3", "Vec2")
            stride: Vertex buffer stride. If 0, inferred from data_type
        """
        self.device = device
        self.texture_format = texture_format
        self.depth_format = depth_format
        self.msaa_sample_count = msaa_sample_count
        self._data_type = data_type

        if stride != 0:
            self._stride = stride
        else:
            self._stride = NGLToWebGPU.stride_from_type(self._data_type)

        # Core pipeline resources
        self.pipeline: Optional[wgpu.GPURenderPipeline] = None
        self.uniform_buffer: Optional[wgpu.GPUBuffer] = None
        self.bind_group: Optional[wgpu.GPUBindGroup] = None

        # Initialize uniform data structure
        self.uniform_data = np.zeros((), dtype=self.get_dtype())
        self._set_default_uniforms()

        # Create the pipeline
        self._create_pipeline()

    @abstractmethod
    def get_dtype(self) -> np.dtype:
        """Get the numpy dtype for the uniform buffer structure."""
        pass

    @abstractmethod
    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        pass

    @abstractmethod
    def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
        """Get vertex buffer layout configurations for the pipeline."""
        pass

    @abstractmethod
    def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
        """Get the primitive topology for the pipeline."""
        pass

    @abstractmethod
    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        pass

    @abstractmethod
    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        pass

    def _create_pipeline(self) -> None:
        """Create the render pipeline and associated resources."""
        # Load shader
        shader_module = self.device.create_shader_module(code=self._get_shader_code())

        # Create render pipeline
        self.pipeline = self.device.create_render_pipeline(
            label=self._get_pipeline_label(),
            layout="auto",
            vertex={
                "module": shader_module,
                "entry_point": "vertex_main",
                "buffers": self._get_vertex_buffer_layouts(),
            },
            fragment={
                "module": shader_module,
                "entry_point": "fragment_main",
                "targets": [{"format": self.texture_format}],
            },
            primitive={"topology": self._get_primitive_topology()},
            depth_stencil={
                "format": self.depth_format,
                "depth_write_enabled": True,
                "depth_compare": wgpu.CompareFunction.less,
            },
            multisample={"count": self.msaa_sample_count},
        )

        # Create uniform buffer
        self.uniform_buffer = self.device.create_buffer_with_data(
            data=self.uniform_data.tobytes(),
            usage=int(wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST),
            label=f"{self._get_pipeline_label()}_uniform_buffer",
        )

        # Create bind group
        bind_group_layout = self.pipeline.get_bind_group_layout(0)
        self.bind_group = self.device.create_bind_group(
            layout=bind_group_layout,
            entries=[
                {
                    "binding": 0,
                    "resource": {"buffer": self.uniform_buffer},
                }
            ],
        )

    def _create_or_update_buffer(
        self,
        current_buffer: Optional[wgpu.GPUBuffer],
        data: Union[np.ndarray, wgpu.GPUBuffer],
        usage: wgpu.BufferUsage,
        buffer_label: str,
    ) -> Tuple[Optional[wgpu.GPUBuffer], int]:
        """Create or update a GPU buffer with new data.

        Args:
            current_buffer: Existing buffer (may be None)
            data: New data (numpy array or GPU buffer)
            usage: Buffer usage flags
            buffer_label: Label for the buffer

        Returns:
            Tuple of (buffer, data_size)
        """
        if isinstance(data, wgpu.GPUBuffer):
            # Use provided buffer directly
            return data, data.size

        # Handle numpy array
        data_bytes = data.astype(np.float32).tobytes()
        data_size = len(data_bytes)

        # Create new buffer if needed or existing one is too small
        if current_buffer is None or current_buffer.size < data_size:
            if current_buffer:
                current_buffer.destroy()
            buffer = self.device.create_buffer_with_data(
                data=data_bytes,
                usage=usage,
                label=buffer_label,
            )
            return buffer, data_size
        else:
            # Update existing buffer
            self.device.queue.write_buffer(current_buffer, 0, data_bytes)
            return current_buffer, data_size

    def _process_vertex_data(
        self,
        data: Optional[Union[np.ndarray, wgpu.GPUBuffer]],
        default_value: Optional[np.ndarray] = None,
        padding_size: Optional[int] = None,
        buffer_label: str = "vertex_buffer",
    ) -> Optional[Union[wgpu.GPUBuffer, Tuple[wgpu.GPUBuffer, int]]]:
        """Process vertex data, handling numpy arrays, GPU buffers, and defaults.

        Args:
            data: Input data (numpy array, GPU buffer, or None)
            default_value: Default value if data is None
            padding_size: Size to pad arrays to (for alignment)
            buffer_label: Label for created buffers

        Returns:
            Processed buffer(s) or None
        """
        if data is None and default_value is not None:
            data = default_value

        if data is None:
            return None

        if isinstance(data, wgpu.GPUBuffer):
            return data

        # Handle numpy array
        if padding_size:
            # Pad array to specified size
            if data.ndim == 1:
                padded_data = np.zeros(padding_size, dtype=np.float32)
                padded_data[: len(data)] = data.astype(np.float32)
            else:
                padded_data = np.zeros((data.shape[0], padding_size), dtype=np.float32)
                padded_data[:, : data.shape[1]] = data.astype(np.float32)
            data = padded_data

        buffer, _ = self._create_or_update_buffer(
            None,  # Always create new for processed data
            data,
            wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            buffer_label,
        )
        return buffer

    @abstractmethod
    def set_data(self, **kwargs: Any) -> None:
        """Set rendering data (vertices, colours, etc.).

        Args:
            **kwargs: Pipeline-specific data parameters
        """
        pass

    @abstractmethod
    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
        """
        pass

    @abstractmethod
    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render using this pipeline.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
        """
        pass

    def cleanup(self) -> None:
        """Release pipeline resources. Can be overridden for additional cleanup."""
        if self.uniform_buffer:
            self.uniform_buffer.destroy()

__init__(device, texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, data_type='Vec3', stride=0)

Initialize base pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • texture_format (TextureFormat, default: rgba8unorm ) –

    Colour attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • data_type (str, default: 'Vec3' ) –

    Vertex data type (e.g., "Vec3", "Vec2")

  • stride (int, default: 0 ) –

    Vertex buffer stride. If 0, inferred from data_type

Source code in ncca/ngl/webgpu/base_webgpu_pipeline.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def __init__(
    self,
    device: wgpu.GPUDevice,
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    data_type: str = "Vec3",
    stride: int = 0,
) -> None:
    """Initialize base pipeline.

    Args:
        device: WebGPU device
        texture_format: Colour attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        data_type: Vertex data type (e.g., "Vec3", "Vec2")
        stride: Vertex buffer stride. If 0, inferred from data_type
    """
    self.device = device
    self.texture_format = texture_format
    self.depth_format = depth_format
    self.msaa_sample_count = msaa_sample_count
    self._data_type = data_type

    if stride != 0:
        self._stride = stride
    else:
        self._stride = NGLToWebGPU.stride_from_type(self._data_type)

    # Core pipeline resources
    self.pipeline: Optional[wgpu.GPURenderPipeline] = None
    self.uniform_buffer: Optional[wgpu.GPUBuffer] = None
    self.bind_group: Optional[wgpu.GPUBindGroup] = None

    # Initialize uniform data structure
    self.uniform_data = np.zeros((), dtype=self.get_dtype())
    self._set_default_uniforms()

    # Create the pipeline
    self._create_pipeline()

cleanup()

Release pipeline resources. Can be overridden for additional cleanup.

Source code in ncca/ngl/webgpu/base_webgpu_pipeline.py
259
260
261
262
def cleanup(self) -> None:
    """Release pipeline resources. Can be overridden for additional cleanup."""
    if self.uniform_buffer:
        self.uniform_buffer.destroy()

get_dtype() abstractmethod

Get the numpy dtype for the uniform buffer structure.

Source code in ncca/ngl/webgpu/base_webgpu_pipeline.py
67
68
69
70
@abstractmethod
def get_dtype(self) -> np.dtype:
    """Get the numpy dtype for the uniform buffer structure."""
    pass

render(render_pass, **kwargs) abstractmethod

Render using this pipeline.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters

Source code in ncca/ngl/webgpu/base_webgpu_pipeline.py
249
250
251
252
253
254
255
256
257
@abstractmethod
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render using this pipeline.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
    """
    pass

set_data(**kwargs) abstractmethod

Set rendering data (vertices, colours, etc.).

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific data parameters

Source code in ncca/ngl/webgpu/base_webgpu_pipeline.py
231
232
233
234
235
236
237
238
@abstractmethod
def set_data(self, **kwargs: Any) -> None:
    """Set rendering data (vertices, colours, etc.).

    Args:
        **kwargs: Pipeline-specific data parameters
    """
    pass

update_uniforms(**kwargs) abstractmethod

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters

Source code in ncca/ngl/webgpu/base_webgpu_pipeline.py
240
241
242
243
244
245
246
247
@abstractmethod
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
    """
    pass

BasePointPipeline

Bases: BaseWebGPUPipeline

Base class for point rendering pipelines.

Provides common functionality for: - Point billboarding - Quad generation - Circle clipping in fragment shader

Source code in ncca/ngl/webgpu/base_webgpu_pipeline.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
class BasePointPipeline(BaseWebGPUPipeline):
    """Base class for point rendering pipelines.

    Provides common functionality for:
    - Point billboarding
    - Quad generation
    - Circle clipping in fragment shader
    """

    def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
        """Points are rendered as triangle strips for quad generation."""
        return wgpu.PrimitiveTopology.triangle_strip

    def _get_default_vertex_layouts(
        self, has_colour_buffer: bool = False
    ) -> List[Dict[str, Any]]:
        """Get default vertex buffer layouts for point rendering.

        Args:
            has_colour_buffer: Whether to include colour buffer layout

        Returns:
            List of vertex buffer layout configurations
        """
        layouts = [
            {
                "array_stride": self._stride,
                "step_mode": "instance",
                "attributes": [
                    {
                        "format": NGLToWebGPU.vertex_format(self._data_type),
                        "offset": 0,
                        "shader_location": 0,
                    },
                ],
            },
        ]

        if has_colour_buffer:
            layouts.append(
                {
                    "array_stride": NGLToWebGPU.stride_from_type("Vec3"),
                    "step_mode": "instance",
                    "attributes": [
                        {
                            "format": NGLToWebGPU.vertex_format("Vec3"),
                            "offset": 0,
                            "shader_location": 1,
                        },
                    ],
                }
            )

        return layouts

    def _render_points(
        self,
        render_pass: wgpu.GPURenderPassEncoder,
        position_buffer: wgpu.GPUBuffer,
        colour_buffer: Optional[wgpu.GPUBuffer] = None,
        num_points: Optional[int] = None,
    ) -> None:
        """Common point rendering implementation.

        Args:
            render_pass: Active render pass encoder
            position_buffer: Buffer containing point positions
            colour_buffer: Optional buffer containing point colours
            num_points: Number of points to render
        """
        if position_buffer is None:
            return

        count = num_points if num_points is not None else getattr(self, "num_points", 0)

        render_pass.set_pipeline(self.pipeline)
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
        render_pass.set_vertex_buffer(0, position_buffer)

        if colour_buffer:
            render_pass.set_vertex_buffer(1, colour_buffer)

        # 4 vertices per quad for point rendering
        render_pass.draw(4, count)

CustomShaderPipeline

Bases: BaseWebGPUPipeline

A WebGPU pipeline that uses custom shader source provided by the user.

This pipeline allows users to provide their own WGSL shader source code while handling the boilerplate for buffer management, uniform updates, and rendering setup.

Source code in ncca/ngl/webgpu/custom_shader_pipeline.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
class CustomShaderPipeline(BaseWebGPUPipeline):
    """A WebGPU pipeline that uses custom shader source provided by the user.

    This pipeline allows users to provide their own WGSL shader source code
    while handling the boilerplate for buffer management, uniform updates,
    and rendering setup.
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        shader_source: str,
        vertex_formats: Optional[List[Union[str, wgpu.VertexFormat]]] = None,
        primitive_topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.triangle_list,
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        uniform_struct_definition: Optional[str] = None,
        pipeline_label: str = "CustomShaderPipeline",
    ) -> None:
        """Initialize custom shader pipeline.

        Args:
            device: WebGPU device
            shader_source: WGSL shader source code as string
            vertex_formats: List of vertex data formats (e.g., ["Vec3", "Vec3"] for position+colour)
            primitive_topology: Primitive topology for rendering
            texture_format: Colour attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            uniform_struct_definition: Custom uniform struct definition (optional)
            pipeline_label: Label for debugging
        """
        self._shader_source = shader_source
        self._vertex_formats = vertex_formats or ["Vec3"]
        self._primitive_topology = primitive_topology
        self._uniform_struct_definition = uniform_struct_definition
        self._pipeline_label = pipeline_label

        # Calculate stride from vertex formats
        self._stride = sum(
            NGLToWebGPU.stride_from_type(fmt) for fmt in self._vertex_formats
        )

        # Initialize buffers storage
        self.vertex_buffers: Dict[int, wgpu.GPUBuffer] = {}
        self.num_vertices = 0

        # Call parent constructor after setting up our attributes
        super().__init__(
            device=device,
            texture_format=texture_format,
            depth_format=depth_format,
            msaa_sample_count=msaa_sample_count,
            data_type=self._vertex_formats[0],  # Use first format as default
            stride=self._stride,
        )

    def get_dtype(self) -> np.dtype:
        """Get the numpy dtype for the uniform buffer structure."""
        if self._uniform_struct_definition:
            # For custom uniforms, we need to parse the struct or use a default
            # For now, use a basic MVP + colour structure
            return np.dtype(
                [
                    ("MVP", np.float32, (4, 4)),
                    ("colour", np.float32, 4),
                ]
            )
        else:
            # Default uniform structure
            return np.dtype(
                [
                    ("MVP", np.float32, (4, 4)),
                    ("colour", np.float32, 4),
                ]
            )

    def _get_shader_code(self) -> str:
        """Return the custom shader source."""
        return self._shader_source

    def _get_vertex_buffer_layouts(self) -> List[Dict[str, any]]:
        """Get vertex buffer layouts based on provided vertex formats."""
        if len(self._vertex_formats) == 1:
            # Single interleaved buffer
            return [
                {
                    "array_stride": self._stride,
                    "step_mode": "vertex",
                    "attributes": [
                        {
                            "format": NGLToWebGPU.vertex_format(
                                self._vertex_formats[0]
                            ),
                            "offset": 0,
                            "shader_location": 0,
                        },
                    ],
                },
            ]
        else:
            # Multiple separate buffers or interleaved with multiple attributes
            layouts = []
            current_offset = 0

            for i, fmt in enumerate(self._vertex_formats):
                stride = NGLToWebGPU.stride_from_type(fmt)
                layouts.append(
                    {
                        "array_stride": stride,
                        "step_mode": "vertex",
                        "attributes": [
                            {
                                "format": NGLToWebGPU.vertex_format(fmt),
                                "offset": 0,
                                "shader_location": i,
                            },
                        ],
                    }
                )
                current_offset += stride

            return layouts

    def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
        """Return the primitive topology."""
        return self._primitive_topology

    def _set_default_uniforms(self) -> None:
        """Set default uniform values."""
        self.uniform_data["MVP"] = np.eye(4, dtype=np.float32)
        self.uniform_data["colour"] = np.array([1.0, 1.0, 1.0, 1.0], dtype=np.float32)

    def _get_pipeline_label(self) -> str:
        """Return the pipeline label."""
        return self._pipeline_label

    def set_data(
        self,
        positions: Optional[np.ndarray] = None,
        colours: Optional[np.ndarray] = None,
        interleaved_data: Optional[np.ndarray] = None,
        **kwargs: Any,
    ) -> None:
        """Set vertex data for rendering.

        Args:
            positions: Vertex position data (N, 3)
            colours: Vertex colour data (N, 3) or (N, 4)
            interleaved_data: Pre-interleaved vertex data
            **kwargs: Additional vertex data arrays (e.g., velocities, life, initial_position)
        """
        if interleaved_data is not None:
            # Use pre-interleaved data
            buffer = self._create_or_update_buffer(
                self.vertex_buffers.get(0),
                interleaved_data,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                f"{self._pipeline_label}_vertex_buffer_0",
            )
            self.vertex_buffers[0] = buffer[0]
            self.num_vertices = len(interleaved_data)
        else:
            # Handle separate vertex data arrays
            binding = 0

            if positions is not None:
                buffer = self._create_or_update_buffer(
                    self.vertex_buffers.get(binding),
                    positions,
                    wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                    f"{self._pipeline_label}_vertex_buffer_{binding}",
                )
                self.vertex_buffers[binding] = buffer[0]
                self.num_vertices = len(positions)
                binding += 1

            if colours is not None:
                buffer = self._create_or_update_buffer(
                    self.vertex_buffers.get(binding),
                    colours,
                    wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                    f"{self._pipeline_label}_vertex_buffer_{binding}",
                )
                self.vertex_buffers[binding] = buffer[0]
                binding += 1

            # Handle additional vertex attributes (velocities, life, initial_position, etc.)
            for attr_name, attr_data in kwargs.items():
                if attr_data is not None:
                    buffer = self._create_or_update_buffer(
                        self.vertex_buffers.get(binding),
                        attr_data,
                        wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                        f"{self._pipeline_label}_vertex_buffer_{binding}",
                    )
                    self.vertex_buffers[binding] = buffer[0]
                    binding += 1

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Uniform values to update (e.g., mvp=matrix, colour=array)
        """
        if "mvp" in kwargs:
            self.uniform_data["MVP"] = kwargs["mvp"]

        if "colour" in kwargs:
            colour = kwargs["colour"]
            if len(colour) == 3:
                self.uniform_data["colour"] = np.array([*colour, 1.0], dtype=np.float32)
            else:
                self.uniform_data["colour"] = np.array(colour, dtype=np.float32)

        # Update the GPU buffer
        if self.uniform_buffer:
            self.device.queue.write_buffer(
                self.uniform_buffer, 0, self.uniform_data.tobytes()
            )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render using this pipeline.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Additional render parameters
        """
        if self.pipeline is None or self.num_vertices == 0:
            return

        render_pass.set_pipeline(self.pipeline)
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)

        # Set vertex buffers
        for binding, buffer in self.vertex_buffers.items():
            render_pass.set_vertex_buffer(binding, buffer)

        # Draw
        render_pass.draw(self.num_vertices)

    @classmethod
    def from_file(
        cls, device: wgpu.GPUDevice, shader_file: str, **kwargs: Any
    ) -> "CustomShaderPipeline":
        """Create a CustomShaderPipeline from a WGSL file.

        Args:
            device: WebGPU device
            shader_file: Path to WGSL shader file
            **kwargs: Additional arguments to pass to constructor

        Returns:
            CustomShaderPipeline instance
        """
        if not os.path.exists(shader_file):
            raise FileNotFoundError(f"Shader file not found: {shader_file}")

        with open(shader_file, "r", encoding="utf-8") as f:
            shader_source = f.read()

        # Use filename as default pipeline label if not provided
        if "pipeline_label" not in kwargs:
            kwargs["pipeline_label"] = f"Custom_{os.path.basename(shader_file)}"

        return cls(device, shader_source, **kwargs)

__init__(device, shader_source, vertex_formats=None, primitive_topology=wgpu.PrimitiveTopology.triangle_list, texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, uniform_struct_definition=None, pipeline_label='CustomShaderPipeline')

Initialize custom shader pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • shader_source (str) –

    WGSL shader source code as string

  • vertex_formats (Optional[List[Union[str, VertexFormat]]], default: None ) –

    List of vertex data formats (e.g., ["Vec3", "Vec3"] for position+colour)

  • primitive_topology (PrimitiveTopology, default: triangle_list ) –

    Primitive topology for rendering

  • texture_format (TextureFormat, default: rgba8unorm ) –

    Colour attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • uniform_struct_definition (Optional[str], default: None ) –

    Custom uniform struct definition (optional)

  • pipeline_label (str, default: 'CustomShaderPipeline' ) –

    Label for debugging

Source code in ncca/ngl/webgpu/custom_shader_pipeline.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def __init__(
    self,
    device: wgpu.GPUDevice,
    shader_source: str,
    vertex_formats: Optional[List[Union[str, wgpu.VertexFormat]]] = None,
    primitive_topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.triangle_list,
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    uniform_struct_definition: Optional[str] = None,
    pipeline_label: str = "CustomShaderPipeline",
) -> None:
    """Initialize custom shader pipeline.

    Args:
        device: WebGPU device
        shader_source: WGSL shader source code as string
        vertex_formats: List of vertex data formats (e.g., ["Vec3", "Vec3"] for position+colour)
        primitive_topology: Primitive topology for rendering
        texture_format: Colour attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        uniform_struct_definition: Custom uniform struct definition (optional)
        pipeline_label: Label for debugging
    """
    self._shader_source = shader_source
    self._vertex_formats = vertex_formats or ["Vec3"]
    self._primitive_topology = primitive_topology
    self._uniform_struct_definition = uniform_struct_definition
    self._pipeline_label = pipeline_label

    # Calculate stride from vertex formats
    self._stride = sum(
        NGLToWebGPU.stride_from_type(fmt) for fmt in self._vertex_formats
    )

    # Initialize buffers storage
    self.vertex_buffers: Dict[int, wgpu.GPUBuffer] = {}
    self.num_vertices = 0

    # Call parent constructor after setting up our attributes
    super().__init__(
        device=device,
        texture_format=texture_format,
        depth_format=depth_format,
        msaa_sample_count=msaa_sample_count,
        data_type=self._vertex_formats[0],  # Use first format as default
        stride=self._stride,
    )

from_file(device, shader_file, **kwargs) classmethod

Create a CustomShaderPipeline from a WGSL file.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • shader_file (str) –

    Path to WGSL shader file

  • **kwargs (Any, default: {} ) –

    Additional arguments to pass to constructor

Returns:
Source code in ncca/ngl/webgpu/custom_shader_pipeline.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
@classmethod
def from_file(
    cls, device: wgpu.GPUDevice, shader_file: str, **kwargs: Any
) -> "CustomShaderPipeline":
    """Create a CustomShaderPipeline from a WGSL file.

    Args:
        device: WebGPU device
        shader_file: Path to WGSL shader file
        **kwargs: Additional arguments to pass to constructor

    Returns:
        CustomShaderPipeline instance
    """
    if not os.path.exists(shader_file):
        raise FileNotFoundError(f"Shader file not found: {shader_file}")

    with open(shader_file, "r", encoding="utf-8") as f:
        shader_source = f.read()

    # Use filename as default pipeline label if not provided
    if "pipeline_label" not in kwargs:
        kwargs["pipeline_label"] = f"Custom_{os.path.basename(shader_file)}"

    return cls(device, shader_source, **kwargs)

get_dtype()

Get the numpy dtype for the uniform buffer structure.

Source code in ncca/ngl/webgpu/custom_shader_pipeline.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def get_dtype(self) -> np.dtype:
    """Get the numpy dtype for the uniform buffer structure."""
    if self._uniform_struct_definition:
        # For custom uniforms, we need to parse the struct or use a default
        # For now, use a basic MVP + colour structure
        return np.dtype(
            [
                ("MVP", np.float32, (4, 4)),
                ("colour", np.float32, 4),
            ]
        )
    else:
        # Default uniform structure
        return np.dtype(
            [
                ("MVP", np.float32, (4, 4)),
                ("colour", np.float32, 4),
            ]
        )

render(render_pass, **kwargs)

Render using this pipeline.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Additional render parameters

Source code in ncca/ngl/webgpu/custom_shader_pipeline.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render using this pipeline.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Additional render parameters
    """
    if self.pipeline is None or self.num_vertices == 0:
        return

    render_pass.set_pipeline(self.pipeline)
    render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)

    # Set vertex buffers
    for binding, buffer in self.vertex_buffers.items():
        render_pass.set_vertex_buffer(binding, buffer)

    # Draw
    render_pass.draw(self.num_vertices)

set_data(positions=None, colours=None, interleaved_data=None, **kwargs)

Set vertex data for rendering.

Parameters:
  • positions (Optional[ndarray], default: None ) –

    Vertex position data (N, 3)

  • colours (Optional[ndarray], default: None ) –

    Vertex colour data (N, 3) or (N, 4)

  • interleaved_data (Optional[ndarray], default: None ) –

    Pre-interleaved vertex data

  • **kwargs (Any, default: {} ) –

    Additional vertex data arrays (e.g., velocities, life, initial_position)

Source code in ncca/ngl/webgpu/custom_shader_pipeline.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def set_data(
    self,
    positions: Optional[np.ndarray] = None,
    colours: Optional[np.ndarray] = None,
    interleaved_data: Optional[np.ndarray] = None,
    **kwargs: Any,
) -> None:
    """Set vertex data for rendering.

    Args:
        positions: Vertex position data (N, 3)
        colours: Vertex colour data (N, 3) or (N, 4)
        interleaved_data: Pre-interleaved vertex data
        **kwargs: Additional vertex data arrays (e.g., velocities, life, initial_position)
    """
    if interleaved_data is not None:
        # Use pre-interleaved data
        buffer = self._create_or_update_buffer(
            self.vertex_buffers.get(0),
            interleaved_data,
            wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            f"{self._pipeline_label}_vertex_buffer_0",
        )
        self.vertex_buffers[0] = buffer[0]
        self.num_vertices = len(interleaved_data)
    else:
        # Handle separate vertex data arrays
        binding = 0

        if positions is not None:
            buffer = self._create_or_update_buffer(
                self.vertex_buffers.get(binding),
                positions,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                f"{self._pipeline_label}_vertex_buffer_{binding}",
            )
            self.vertex_buffers[binding] = buffer[0]
            self.num_vertices = len(positions)
            binding += 1

        if colours is not None:
            buffer = self._create_or_update_buffer(
                self.vertex_buffers.get(binding),
                colours,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                f"{self._pipeline_label}_vertex_buffer_{binding}",
            )
            self.vertex_buffers[binding] = buffer[0]
            binding += 1

        # Handle additional vertex attributes (velocities, life, initial_position, etc.)
        for attr_name, attr_data in kwargs.items():
            if attr_data is not None:
                buffer = self._create_or_update_buffer(
                    self.vertex_buffers.get(binding),
                    attr_data,
                    wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                    f"{self._pipeline_label}_vertex_buffer_{binding}",
                )
                self.vertex_buffers[binding] = buffer[0]
                binding += 1

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Uniform values to update (e.g., mvp=matrix, colour=array)

Source code in ncca/ngl/webgpu/custom_shader_pipeline.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Uniform values to update (e.g., mvp=matrix, colour=array)
    """
    if "mvp" in kwargs:
        self.uniform_data["MVP"] = kwargs["mvp"]

    if "colour" in kwargs:
        colour = kwargs["colour"]
        if len(colour) == 3:
            self.uniform_data["colour"] = np.array([*colour, 1.0], dtype=np.float32)
        else:
            self.uniform_data["colour"] = np.array(colour, dtype=np.float32)

    # Update the GPU buffer
    if self.uniform_buffer:
        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

Built-in pipeline classes

These are the concrete classes behind each PipelineType; you normally create them through PipelineFactory rather than directly.

PointPipelineMultiColour

Bases: BasePointPipeline

A reusable pipeline for rendering points in WebGPU.

Features: - Instanced rendering of points as quads - Per-point colours - Configurable point size - Model, View Projection matrix support pass a projection only for 2D - MSAA support

Source code in ncca/ngl/webgpu/point_pipeline.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
class PointPipelineMultiColour(BasePointPipeline):
    """A reusable pipeline for rendering points in WebGPU.

    Features:
    - Instanced rendering of points as quads
    - Per-point colours
    - Configurable point size
    - Model, View Projection matrix support pass a projection only for 2D
    - MSAA support
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        data_type: str = "Vec3",
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        stride: int = 0,
    ) -> None:
        """Initialize the point rendering pipeline.

        Args:
            device: WebGPU device
            data_type: NGL vertex data type name (e.g. "Vec3")
            texture_format: colour attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
        """
        # Pipeline-specific buffer tracking
        self.position_buffer: Optional[wgpu.GPUBuffer] = None
        self.colour_buffer: Optional[wgpu.GPUBuffer] = None
        self.num_points: int = 0

        super().__init__(
            device=device,
            texture_format=texture_format,
            depth_format=depth_format,
            msaa_sample_count=msaa_sample_count,
            data_type=data_type,
            stride=stride,
        )

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
                ("ViewMatrix", "float32", (4, 4)),
                ("size", "float32"),
                ("padding", np.uint32, 3),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return POINT_SHADER_MULTI_COLOURED

    def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
        """Get vertex buffer layout configurations for the pipeline."""
        return self._get_default_vertex_layouts(has_colour_buffer=True)

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        self.uniform_data["size"] = 1.0  # Default point size
        self.uniform_data["ViewMatrix"] = np.eye(4, dtype=np.float32)

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        return "point_pipeline_multi_coloured"

    def set_data(
        self,
        positions: np.ndarray | wgpu.GPUBuffer,
        colours: np.ndarray | wgpu.GPUBuffer | None = None,
    ) -> None:
        """Set the point data for rendering.

        Args:
            positions: Nx2 array of point positions or a pre-existing GPUBuffer.
            colours: Nx3 array of point colours (RGB) or a pre-existing GPUBuffer.
                    If None, uses white.
        """
        # Handle positions
        if isinstance(positions, wgpu.GPUBuffer):
            self.position_buffer = positions
            self.num_points = positions.size // self._stride
        else:  # numpy array
            self.num_points = len(positions)
            self.position_buffer, _ = self._create_or_update_buffer(
                self.position_buffer,
                positions,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                "point_pipeline_multi_coloured_position_buffer",
            )

        # Handle colours
        if colours is None:
            # Create default white colours
            default_colours = np.ones((self.num_points, 3), dtype=np.float32)
            colour_result = self._process_vertex_data(
                None,
                default_colours,
                padding_size=4,  # Pad to vec4 for alignment
                buffer_label="point_pipeline_multi_coloured_colour_buffer",
            )
            if isinstance(colour_result, wgpu.GPUBuffer):
                self.colour_buffer = colour_result
            elif colour_result:
                self.colour_buffer = colour_result[0]
            else:
                self.colour_buffer = None
        else:
            colour_result = self._process_vertex_data(
                colours,
                None,
                padding_size=4,  # Pad to vec4 for alignment
                buffer_label="point_pipeline_multi_coloured_colour_buffer",
            )
            if isinstance(colour_result, wgpu.GPUBuffer):
                self.colour_buffer = colour_result
            elif colour_result:
                self.colour_buffer = colour_result[0]
            else:
                self.colour_buffer = None

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 model view projection matrix
                - view_matrix: 4x4 view matrix for billboarding calculations
                - point_size: Size of points in world units
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
            self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]

        if "point_size" in kwargs and kwargs["point_size"] is not None:
            self.uniform_data["size"] = kwargs["point_size"]

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the points.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_points: Number of points to render (defaults to all)
        """
        num_points = kwargs.get("num_points", None)

        if self.position_buffer is None or self.colour_buffer is None:
            return

        count = num_points if num_points is not None else self.num_points

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
        render_pass.set_vertex_buffer(0, self.position_buffer)
        render_pass.set_vertex_buffer(1, self.colour_buffer)
        render_pass.draw(4, count)  # 4 vertices per quad, instanced

    def cleanup(self) -> None:
        """Release resources."""
        if self.position_buffer:
            self.position_buffer.destroy()
        if self.colour_buffer:
            self.colour_buffer.destroy()
        super().cleanup()

__init__(device, data_type='Vec3', texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, stride=0)

Initialize the point rendering pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • data_type (str, default: 'Vec3' ) –

    NGL vertex data type name (e.g. "Vec3")

  • texture_format (TextureFormat, default: rgba8unorm ) –

    colour attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • stride (int, default: 0 ) –

    The stride of the vertex buffer. If 0, it is inferred from data_type.

Source code in ncca/ngl/webgpu/point_pipeline.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(
    self,
    device: wgpu.GPUDevice,
    data_type: str = "Vec3",
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    stride: int = 0,
) -> None:
    """Initialize the point rendering pipeline.

    Args:
        device: WebGPU device
        data_type: NGL vertex data type name (e.g. "Vec3")
        texture_format: colour attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
    """
    # Pipeline-specific buffer tracking
    self.position_buffer: Optional[wgpu.GPUBuffer] = None
    self.colour_buffer: Optional[wgpu.GPUBuffer] = None
    self.num_points: int = 0

    super().__init__(
        device=device,
        texture_format=texture_format,
        depth_format=depth_format,
        msaa_sample_count=msaa_sample_count,
        data_type=data_type,
        stride=stride,
    )

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/point_pipeline.py
186
187
188
189
190
191
192
def cleanup(self) -> None:
    """Release resources."""
    if self.position_buffer:
        self.position_buffer.destroy()
    if self.colour_buffer:
        self.colour_buffer.destroy()
    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/point_pipeline.py
59
60
61
62
63
64
65
66
67
68
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
            ("ViewMatrix", "float32", (4, 4)),
            ("size", "float32"),
            ("padding", np.uint32, 3),
        ]
    )

render(render_pass, **kwargs)

Render the points.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_points: Number of points to render (defaults to all)

Source code in ncca/ngl/webgpu/point_pipeline.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the points.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_points: Number of points to render (defaults to all)
    """
    num_points = kwargs.get("num_points", None)

    if self.position_buffer is None or self.colour_buffer is None:
        return

    count = num_points if num_points is not None else self.num_points

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
    render_pass.set_vertex_buffer(0, self.position_buffer)
    render_pass.set_vertex_buffer(1, self.colour_buffer)
    render_pass.draw(4, count)  # 4 vertices per quad, instanced

set_data(positions, colours=None)

Set the point data for rendering.

Parameters:
  • positions (ndarray | GPUBuffer) –

    Nx2 array of point positions or a pre-existing GPUBuffer.

  • colours (ndarray | GPUBuffer | None, default: None ) –

    Nx3 array of point colours (RGB) or a pre-existing GPUBuffer. If None, uses white.

Source code in ncca/ngl/webgpu/point_pipeline.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def set_data(
    self,
    positions: np.ndarray | wgpu.GPUBuffer,
    colours: np.ndarray | wgpu.GPUBuffer | None = None,
) -> None:
    """Set the point data for rendering.

    Args:
        positions: Nx2 array of point positions or a pre-existing GPUBuffer.
        colours: Nx3 array of point colours (RGB) or a pre-existing GPUBuffer.
                If None, uses white.
    """
    # Handle positions
    if isinstance(positions, wgpu.GPUBuffer):
        self.position_buffer = positions
        self.num_points = positions.size // self._stride
    else:  # numpy array
        self.num_points = len(positions)
        self.position_buffer, _ = self._create_or_update_buffer(
            self.position_buffer,
            positions,
            wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            "point_pipeline_multi_coloured_position_buffer",
        )

    # Handle colours
    if colours is None:
        # Create default white colours
        default_colours = np.ones((self.num_points, 3), dtype=np.float32)
        colour_result = self._process_vertex_data(
            None,
            default_colours,
            padding_size=4,  # Pad to vec4 for alignment
            buffer_label="point_pipeline_multi_coloured_colour_buffer",
        )
        if isinstance(colour_result, wgpu.GPUBuffer):
            self.colour_buffer = colour_result
        elif colour_result:
            self.colour_buffer = colour_result[0]
        else:
            self.colour_buffer = None
    else:
        colour_result = self._process_vertex_data(
            colours,
            None,
            padding_size=4,  # Pad to vec4 for alignment
            buffer_label="point_pipeline_multi_coloured_colour_buffer",
        )
        if isinstance(colour_result, wgpu.GPUBuffer):
            self.colour_buffer = colour_result
        elif colour_result:
            self.colour_buffer = colour_result[0]
        else:
            self.colour_buffer = None

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 model view projection matrix - view_matrix: 4x4 view matrix for billboarding calculations - point_size: Size of points in world units

Source code in ncca/ngl/webgpu/point_pipeline.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 model view projection matrix
            - view_matrix: 4x4 view matrix for billboarding calculations
            - point_size: Size of points in world units
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
        self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]

    if "point_size" in kwargs and kwargs["point_size"] is not None:
        self.uniform_data["size"] = kwargs["point_size"]

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )

PointPipelineSingleColour

Bases: BasePointPipeline

A reusable pipeline for rendering points in WebGPU.

Features: - Instanced rendering of points as quads - Single colour for all points - Configurable point size - Model, View Projection matrix support pass a projection only for 2D - MSAA support

Source code in ncca/ngl/webgpu/point_pipeline.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
class PointPipelineSingleColour(BasePointPipeline):
    """A reusable pipeline for rendering points in WebGPU.

    Features:
    - Instanced rendering of points as quads
    - Single colour for all points
    - Configurable point size
    - Model, View Projection matrix support pass a projection only for 2D
    - MSAA support
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        data_type: str = "Vec3",
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        stride: int = 0,
    ) -> None:
        """Initialize the point rendering pipeline.

        Args:
            device: WebGPU device
            data_type: NGL vertex data type name (e.g. "Vec3")
            texture_format: colour attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
        """
        # Pipeline-specific buffer tracking
        self.position_buffer: Optional[wgpu.GPUBuffer] = None
        self.num_points: int = 0

        super().__init__(
            device=device,
            texture_format=texture_format,
            depth_format=depth_format,
            msaa_sample_count=msaa_sample_count,
            data_type=data_type,
            stride=stride,
        )

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
                ("ViewMatrix", "float32", (4, 4)),
                ("ColourSize", "float32", 4),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return POINT_SHADER_SINGLE_COLOUR

    def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
        """Get vertex buffer layout configurations for the pipeline."""
        return self._get_default_vertex_layouts(has_colour_buffer=False)

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        self.uniform_data["ColourSize"] = np.array(
            [1.0, 1.0, 1.0, 1.0], dtype=np.float32
        )  # Default White with point size 1
        self.uniform_data["ViewMatrix"] = np.eye(4, dtype=np.float32)

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        return "point_pipeline_single_colour"

    def set_data(
        self,
        positions: np.ndarray | wgpu.GPUBuffer,
        colours: np.ndarray | wgpu.GPUBuffer | None = None,
    ) -> None:
        """Set the point data for rendering.

        Args:
            positions: Nx2 array of point positions or a pre-existing GPUBuffer.
            colours: Ignored for single colour pipeline
        """
        # Handle positions
        if isinstance(positions, wgpu.GPUBuffer):
            self.position_buffer = positions
            self.num_points = positions.size // self._stride
        else:  # numpy array
            self.num_points = len(positions)
            self.position_buffer, _ = self._create_or_update_buffer(
                self.position_buffer,
                positions,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                "point_pipeline_single_colour_position_buffer",
            )

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 model view projection matrix
                - view_matrix: 4x4 view matrix for billboarding calculations
                - colour: 3-element array of RGB colour values
                - point_size: Size of points in world units
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
            self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]

        if "colour" in kwargs and kwargs["colour"] is not None:
            self.uniform_data["ColourSize"][:3] = kwargs["colour"]

        if "point_size" in kwargs and kwargs["point_size"] is not None:
            self.uniform_data["ColourSize"][3] = kwargs["point_size"]

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the points.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_points: Number of points to render (defaults to all)
        """
        num_points = kwargs.get("num_points", None)

        if self.position_buffer is None:
            return

        count = num_points if num_points is not None else self.num_points

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
        render_pass.set_vertex_buffer(0, self.position_buffer)
        render_pass.draw(4, count)  # 4 vertices per quad, instanced

    def cleanup(self) -> None:
        """Release resources."""
        if self.position_buffer:
            self.position_buffer.destroy()
        super().cleanup()

__init__(device, data_type='Vec3', texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, stride=0)

Initialize the point rendering pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • data_type (str, default: 'Vec3' ) –

    NGL vertex data type name (e.g. "Vec3")

  • texture_format (TextureFormat, default: rgba8unorm ) –

    colour attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • stride (int, default: 0 ) –

    The stride of the vertex buffer. If 0, it is inferred from data_type.

Source code in ncca/ngl/webgpu/point_pipeline.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def __init__(
    self,
    device: wgpu.GPUDevice,
    data_type: str = "Vec3",
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    stride: int = 0,
) -> None:
    """Initialize the point rendering pipeline.

    Args:
        device: WebGPU device
        data_type: NGL vertex data type name (e.g. "Vec3")
        texture_format: colour attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
    """
    # Pipeline-specific buffer tracking
    self.position_buffer: Optional[wgpu.GPUBuffer] = None
    self.num_points: int = 0

    super().__init__(
        device=device,
        texture_format=texture_format,
        depth_format=depth_format,
        msaa_sample_count=msaa_sample_count,
        data_type=data_type,
        stride=stride,
    )

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/point_pipeline.py
338
339
340
341
342
def cleanup(self) -> None:
    """Release resources."""
    if self.position_buffer:
        self.position_buffer.destroy()
    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/point_pipeline.py
238
239
240
241
242
243
244
245
246
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
            ("ViewMatrix", "float32", (4, 4)),
            ("ColourSize", "float32", 4),
        ]
    )

render(render_pass, **kwargs)

Render the points.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_points: Number of points to render (defaults to all)

Source code in ncca/ngl/webgpu/point_pipeline.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the points.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_points: Number of points to render (defaults to all)
    """
    num_points = kwargs.get("num_points", None)

    if self.position_buffer is None:
        return

    count = num_points if num_points is not None else self.num_points

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
    render_pass.set_vertex_buffer(0, self.position_buffer)
    render_pass.draw(4, count)  # 4 vertices per quad, instanced

set_data(positions, colours=None)

Set the point data for rendering.

Parameters:
  • positions (ndarray | GPUBuffer) –

    Nx2 array of point positions or a pre-existing GPUBuffer.

  • colours (ndarray | GPUBuffer | None, default: None ) –

    Ignored for single colour pipeline

Source code in ncca/ngl/webgpu/point_pipeline.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
def set_data(
    self,
    positions: np.ndarray | wgpu.GPUBuffer,
    colours: np.ndarray | wgpu.GPUBuffer | None = None,
) -> None:
    """Set the point data for rendering.

    Args:
        positions: Nx2 array of point positions or a pre-existing GPUBuffer.
        colours: Ignored for single colour pipeline
    """
    # Handle positions
    if isinstance(positions, wgpu.GPUBuffer):
        self.position_buffer = positions
        self.num_points = positions.size // self._stride
    else:  # numpy array
        self.num_points = len(positions)
        self.position_buffer, _ = self._create_or_update_buffer(
            self.position_buffer,
            positions,
            wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            "point_pipeline_single_colour_position_buffer",
        )

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 model view projection matrix - view_matrix: 4x4 view matrix for billboarding calculations - colour: 3-element array of RGB colour values - point_size: Size of points in world units

Source code in ncca/ngl/webgpu/point_pipeline.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 model view projection matrix
            - view_matrix: 4x4 view matrix for billboarding calculations
            - colour: 3-element array of RGB colour values
            - point_size: Size of points in world units
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
        self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]

    if "colour" in kwargs and kwargs["colour"] is not None:
        self.uniform_data["ColourSize"][:3] = kwargs["colour"]

    if "point_size" in kwargs and kwargs["point_size"] is not None:
        self.uniform_data["ColourSize"][3] = kwargs["point_size"]

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )

PointListPipelineMultiColour

Bases: BaseWebGPUPipeline

A pipeline for rendering points using WebGPU's native point-list topology.

Features: - Native WebGPU point-list rendering (no billboarding) - Model, View Projection matrix support - MSAA support

Source code in ncca/ngl/webgpu/point_list_pipeline.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
class PointListPipelineMultiColour(BaseWebGPUPipeline):
    """A pipeline for rendering points using WebGPU's native point-list topology.

    Features:
    - Native WebGPU point-list rendering (no billboarding)
    - Model, View Projection matrix support
    - MSAA support
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        data_type: str = "Vec3",
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        stride: int = 0,
    ) -> None:
        """Initialize the point list rendering pipeline.

        Args:
            device: WebGPU device
            data_type: Vertex data type (e.g., "Vec3", "Vec2")
            texture_format: colour attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
        """
        # Pipeline-specific buffer tracking
        self.position_buffer: Optional[wgpu.GPUBuffer] = None
        self.colour_buffer: Optional[wgpu.GPUBuffer] = None
        self.num_points: int = 0

        super().__init__(
            device=device,
            texture_format=texture_format,
            depth_format=depth_format,
            msaa_sample_count=msaa_sample_count,
            data_type=data_type,
            stride=stride,
        )

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return POINT_LIST_SHADER_MULTI_COLOURED

    def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
        """Points are rendered as point list."""
        return wgpu.PrimitiveTopology.point_list

    def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
        """Get vertex buffer layout configurations for the pipeline."""
        position_layout = {
            "array_stride": self._stride,
            "step_mode": wgpu.VertexStepMode.vertex,
            "attributes": [
                {
                    "format": NGLToWebGPU.vertex_format(self._data_type),
                    "offset": 0,
                    "shader_location": 0,
                }
            ],
        }

        colour_layout = {
            "array_stride": 12,  # 3 * float32 for RGB
            "step_mode": wgpu.VertexStepMode.vertex,
            "attributes": [
                {
                    "format": wgpu.VertexFormat.float32x3,
                    "offset": 0,
                    "shader_location": 1,
                }
            ],
        }

        return [position_layout, colour_layout]

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        ...

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        return "point_list_pipeline_multi_coloured"

    def set_data(
        self,
        positions: np.ndarray | wgpu.GPUBuffer,
        colours: np.ndarray | wgpu.GPUBuffer | None = None,
    ) -> None:
        """Set the point data for rendering.

        Args:
            positions: Nx3 array of point positions or a pre-existing GPUBuffer.
            colours: Nx3 array of point colours (RGB) or a pre-existing GPUBuffer.
                    If None, uses white.
        """
        # Handle positions
        if isinstance(positions, wgpu.GPUBuffer):
            self.position_buffer = positions
            self.num_points = positions.size // self._stride
        else:  # numpy array
            self.num_points = len(positions)
            self.position_buffer, _ = self._create_or_update_buffer(
                self.position_buffer,
                positions,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                "point_list_pipeline_multi_coloured_position_buffer",
            )

        # Handle colours
        if colours is None:
            # Create default white colours
            default_colours = np.ones((self.num_points, 3), dtype=np.float32)
            self.colour_buffer, _ = self._create_or_update_buffer(
                self.colour_buffer,
                default_colours,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                "point_list_pipeline_multi_coloured_colour_buffer",
            )
        else:
            self.colour_buffer, _ = self._create_or_update_buffer(
                self.colour_buffer,
                colours,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                "point_list_pipeline_multi_coloured_colour_buffer",
            )

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 model view projection matrix
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the points.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_points: Number of points to render (defaults to all)
        """
        num_points = kwargs.get("num_points", None)

        if self.position_buffer is None or self.colour_buffer is None:
            return

        count = num_points if num_points is not None else self.num_points

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
        render_pass.set_vertex_buffer(0, self.position_buffer)
        render_pass.set_vertex_buffer(1, self.colour_buffer)
        render_pass.draw(count)  # Draw points as point list

    def cleanup(self) -> None:
        """Release resources."""
        if self.position_buffer:
            self.position_buffer.destroy()
        if self.colour_buffer:
            self.colour_buffer.destroy()
        super().cleanup()

__init__(device, data_type='Vec3', texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, stride=0)

Initialize the point list rendering pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • data_type (str, default: 'Vec3' ) –

    Vertex data type (e.g., "Vec3", "Vec2")

  • texture_format (TextureFormat, default: rgba8unorm ) –

    colour attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • stride (int, default: 0 ) –

    The stride of the vertex buffer. If 0, it is inferred from data_type.

Source code in ncca/ngl/webgpu/point_list_pipeline.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def __init__(
    self,
    device: wgpu.GPUDevice,
    data_type: str = "Vec3",
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    stride: int = 0,
) -> None:
    """Initialize the point list rendering pipeline.

    Args:
        device: WebGPU device
        data_type: Vertex data type (e.g., "Vec3", "Vec2")
        texture_format: colour attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
    """
    # Pipeline-specific buffer tracking
    self.position_buffer: Optional[wgpu.GPUBuffer] = None
    self.colour_buffer: Optional[wgpu.GPUBuffer] = None
    self.num_points: int = 0

    super().__init__(
        device=device,
        texture_format=texture_format,
        depth_format=depth_format,
        msaa_sample_count=msaa_sample_count,
        data_type=data_type,
        stride=stride,
    )

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/point_list_pipeline.py
192
193
194
195
196
197
198
def cleanup(self) -> None:
    """Release resources."""
    if self.position_buffer:
        self.position_buffer.destroy()
    if self.colour_buffer:
        self.colour_buffer.destroy()
    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/point_list_pipeline.py
61
62
63
64
65
66
67
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
        ]
    )

render(render_pass, **kwargs)

Render the points.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_points: Number of points to render (defaults to all)

Source code in ncca/ngl/webgpu/point_list_pipeline.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the points.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_points: Number of points to render (defaults to all)
    """
    num_points = kwargs.get("num_points", None)

    if self.position_buffer is None or self.colour_buffer is None:
        return

    count = num_points if num_points is not None else self.num_points

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
    render_pass.set_vertex_buffer(0, self.position_buffer)
    render_pass.set_vertex_buffer(1, self.colour_buffer)
    render_pass.draw(count)  # Draw points as point list

set_data(positions, colours=None)

Set the point data for rendering.

Parameters:
  • positions (ndarray | GPUBuffer) –

    Nx3 array of point positions or a pre-existing GPUBuffer.

  • colours (ndarray | GPUBuffer | None, default: None ) –

    Nx3 array of point colours (RGB) or a pre-existing GPUBuffer. If None, uses white.

Source code in ncca/ngl/webgpu/point_list_pipeline.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def set_data(
    self,
    positions: np.ndarray | wgpu.GPUBuffer,
    colours: np.ndarray | wgpu.GPUBuffer | None = None,
) -> None:
    """Set the point data for rendering.

    Args:
        positions: Nx3 array of point positions or a pre-existing GPUBuffer.
        colours: Nx3 array of point colours (RGB) or a pre-existing GPUBuffer.
                If None, uses white.
    """
    # Handle positions
    if isinstance(positions, wgpu.GPUBuffer):
        self.position_buffer = positions
        self.num_points = positions.size // self._stride
    else:  # numpy array
        self.num_points = len(positions)
        self.position_buffer, _ = self._create_or_update_buffer(
            self.position_buffer,
            positions,
            wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            "point_list_pipeline_multi_coloured_position_buffer",
        )

    # Handle colours
    if colours is None:
        # Create default white colours
        default_colours = np.ones((self.num_points, 3), dtype=np.float32)
        self.colour_buffer, _ = self._create_or_update_buffer(
            self.colour_buffer,
            default_colours,
            wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            "point_list_pipeline_multi_coloured_colour_buffer",
        )
    else:
        self.colour_buffer, _ = self._create_or_update_buffer(
            self.colour_buffer,
            colours,
            wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            "point_list_pipeline_multi_coloured_colour_buffer",
        )

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 model view projection matrix

Source code in ncca/ngl/webgpu/point_list_pipeline.py
156
157
158
159
160
161
162
163
164
165
166
167
168
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 model view projection matrix
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )

PointListPipelineSingleColour

Bases: BaseWebGPUPipeline

A pipeline for rendering points using WebGPU's native point-list topology.

Features: - Native WebGPU point-list rendering (no billboarding) - Single colour for all points - Model, View Projection matrix support - MSAA support

Source code in ncca/ngl/webgpu/point_list_pipeline.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
class PointListPipelineSingleColour(BaseWebGPUPipeline):
    """A pipeline for rendering points using WebGPU's native point-list topology.

    Features:
    - Native WebGPU point-list rendering (no billboarding)
    - Single colour for all points
    - Model, View Projection matrix support
    - MSAA support
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        data_type: str = "Vec3",
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        stride: int = 0,
    ) -> None:
        """Initialize the point list rendering pipeline.

        Args:
            device: WebGPU device
            data_type: Vertex data type (e.g., "Vec3", "Vec2")
            texture_format: colour attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
        """
        # Pipeline-specific buffer tracking
        self.position_buffer: Optional[wgpu.GPUBuffer] = None
        self.num_points: int = 0

        super().__init__(
            device=device,
            texture_format=texture_format,
            depth_format=depth_format,
            msaa_sample_count=msaa_sample_count,
            data_type=data_type,
            stride=stride,
        )

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
                ("Colour", "float32", 3),
                ("padding", "float32"),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return POINT_LIST_SHADER_SINGLE_COLOUR

    def _get_primitive_topology(self) -> wgpu.PrimitiveTopology:
        """Points are rendered as point list."""
        return wgpu.PrimitiveTopology.point_list

    def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
        """Get vertex buffer layout configurations for the pipeline."""
        position_layout = {
            "array_stride": self._stride,
            "step_mode": wgpu.VertexStepMode.vertex,
            "attributes": [
                {
                    "format": NGLToWebGPU.vertex_format(self._data_type),
                    "offset": 0,
                    "shader_location": 0,
                }
            ],
        }

        return [position_layout]

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        self.uniform_data["Colour"] = np.array(
            [1.0, 1.0, 1.0], dtype=np.float32
        )  # Default White
        self.uniform_data["padding"] = 0.0

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        return "point_list_pipeline_single_colour"

    def set_data(
        self,
        positions: np.ndarray | wgpu.GPUBuffer,
        colours: np.ndarray | wgpu.GPUBuffer | None = None,
    ) -> None:
        """Set the point data for rendering.

        Args:
            positions: Nx3 array of point positions or a pre-existing GPUBuffer.
            colours: Ignored for single colour pipeline
        """
        # Handle positions
        if isinstance(positions, wgpu.GPUBuffer):
            self.position_buffer = positions
            self.num_points = positions.size // self._stride
        else:  # numpy array
            self.num_points = len(positions)
            self.position_buffer, _ = self._create_or_update_buffer(
                self.position_buffer,
                positions,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                "point_list_pipeline_single_colour_position_buffer",
            )

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 model view projection matrix
                - colour: 3-element array of RGB colour values
                - point_size: Size of points
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        if "colour" in kwargs and kwargs["colour"] is not None:
            self.uniform_data["Colour"] = kwargs["colour"]

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the points.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_points: Number of points to render (defaults to all)
        """
        num_points = kwargs.get("num_points", None)

        if self.position_buffer is None:
            return

        count = num_points if num_points is not None else self.num_points

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
        render_pass.set_vertex_buffer(0, self.position_buffer)
        render_pass.draw(count)  # Draw points as point list

    def cleanup(self) -> None:
        """Release resources."""
        if self.position_buffer:
            self.position_buffer.destroy()
        super().cleanup()

__init__(device, data_type='Vec3', texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, stride=0)

Initialize the point list rendering pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • data_type (str, default: 'Vec3' ) –

    Vertex data type (e.g., "Vec3", "Vec2")

  • texture_format (TextureFormat, default: rgba8unorm ) –

    colour attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • stride (int, default: 0 ) –

    The stride of the vertex buffer. If 0, it is inferred from data_type.

Source code in ncca/ngl/webgpu/point_list_pipeline.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def __init__(
    self,
    device: wgpu.GPUDevice,
    data_type: str = "Vec3",
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    stride: int = 0,
) -> None:
    """Initialize the point list rendering pipeline.

    Args:
        device: WebGPU device
        data_type: Vertex data type (e.g., "Vec3", "Vec2")
        texture_format: colour attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
    """
    # Pipeline-specific buffer tracking
    self.position_buffer: Optional[wgpu.GPUBuffer] = None
    self.num_points: int = 0

    super().__init__(
        device=device,
        texture_format=texture_format,
        depth_format=depth_format,
        msaa_sample_count=msaa_sample_count,
        data_type=data_type,
        stride=stride,
    )

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/point_list_pipeline.py
352
353
354
355
356
def cleanup(self) -> None:
    """Release resources."""
    if self.position_buffer:
        self.position_buffer.destroy()
    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/point_list_pipeline.py
243
244
245
246
247
248
249
250
251
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
            ("Colour", "float32", 3),
            ("padding", "float32"),
        ]
    )

render(render_pass, **kwargs)

Render the points.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_points: Number of points to render (defaults to all)

Source code in ncca/ngl/webgpu/point_list_pipeline.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the points.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_points: Number of points to render (defaults to all)
    """
    num_points = kwargs.get("num_points", None)

    if self.position_buffer is None:
        return

    count = num_points if num_points is not None else self.num_points

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
    render_pass.set_vertex_buffer(0, self.position_buffer)
    render_pass.draw(count)  # Draw points as point list

set_data(positions, colours=None)

Set the point data for rendering.

Parameters:
  • positions (ndarray | GPUBuffer) –

    Nx3 array of point positions or a pre-existing GPUBuffer.

  • colours (ndarray | GPUBuffer | None, default: None ) –

    Ignored for single colour pipeline

Source code in ncca/ngl/webgpu/point_list_pipeline.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def set_data(
    self,
    positions: np.ndarray | wgpu.GPUBuffer,
    colours: np.ndarray | wgpu.GPUBuffer | None = None,
) -> None:
    """Set the point data for rendering.

    Args:
        positions: Nx3 array of point positions or a pre-existing GPUBuffer.
        colours: Ignored for single colour pipeline
    """
    # Handle positions
    if isinstance(positions, wgpu.GPUBuffer):
        self.position_buffer = positions
        self.num_points = positions.size // self._stride
    else:  # numpy array
        self.num_points = len(positions)
        self.position_buffer, _ = self._create_or_update_buffer(
            self.position_buffer,
            positions,
            wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            "point_list_pipeline_single_colour_position_buffer",
        )

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 model view projection matrix - colour: 3-element array of RGB colour values - point_size: Size of points

Source code in ncca/ngl/webgpu/point_list_pipeline.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 model view projection matrix
            - colour: 3-element array of RGB colour values
            - point_size: Size of points
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    if "colour" in kwargs and kwargs["colour"] is not None:
        self.uniform_data["Colour"] = kwargs["colour"]

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )

LinePipelineMultiColour

Bases: BaseLinePipeline

A reusable pipeline for rendering lines in WebGPU with per-vertex colors.

Features: - Line strips or line segments - Per-vertex colors - MVP matrix support - MSAA support

Source code in ncca/ngl/webgpu/line_pipeline.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
class LinePipelineMultiColour(BaseLinePipeline):
    """A reusable pipeline for rendering lines in WebGPU with per-vertex colors.

    Features:
    - Line strips or line segments
    - Per-vertex colors
    - MVP matrix support
    - MSAA support
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        data_type: str = "Vec3",
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        stride: int = 0,
        topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.line_list,
    ) -> None:
        """Initialize the line rendering pipeline.

        Args:
            device: WebGPU device
            data_type: NGL vertex data type name (e.g. "Vec3")
            texture_format: Color attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
            topology: Primitive topology (line_list or line_strip)
        """
        # Pipeline-specific buffer tracking
        self.vertex_buffer: Optional[wgpu.GPUBuffer] = None
        self.color_buffer: Optional[wgpu.GPUBuffer] = None
        self.num_vertices: int = 0

        super().__init__(
            device=device,
            texture_format=texture_format,
            depth_format=depth_format,
            msaa_sample_count=msaa_sample_count,
            data_type=data_type,
            stride=stride,
            topology=topology,
        )

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return LINE_SHADER_MULTI_COLOURED

    def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
        """Get vertex buffer layout configurations for the pipeline."""
        return [
            {
                "array_stride": self._stride,
                "step_mode": wgpu.VertexStepMode.vertex,
                "attributes": [
                    {
                        "format": NGLToWebGPU.vertex_format(self._data_type),
                        "offset": 0,
                        "shader_location": 0,
                    },
                ],
            },
            {
                "array_stride": NGLToWebGPU.stride_from_type("Vec3"),
                "step_mode": wgpu.VertexStepMode.vertex,
                "attributes": [
                    {
                        "format": NGLToWebGPU.vertex_format("Vec3"),
                        "offset": 0,
                        "shader_location": 1,
                    },
                ],
            },
        ]

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        pass

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        return "line_pipeline_multi_coloured"

    def set_data(
        self,
        positions: np.ndarray | wgpu.GPUBuffer | None = None,
        colors: np.ndarray | wgpu.GPUBuffer | None = None,
        **kwargs: Any,
    ) -> None:
        """Set the line data for rendering.

        Args:
            positions: Nx2/Nx3 array of line positions or a pre-existing GPUBuffer.
            colors: Nx3 array of line colors (RGB) or a pre-existing GPUBuffer.
            **kwargs: Unused, accepted for interface compatibility.
        """
        if positions is not None:
            if isinstance(positions, wgpu.GPUBuffer):
                self.vertex_buffer = positions
                self.num_vertices = positions.size // self._stride
            else:  # numpy array
                self.vertex_buffer, buffer_size = self._create_or_update_buffer(
                    self.vertex_buffer,
                    positions,
                    wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                    "line_pipeline_multi_coloured_position_buffer",
                )
                self.num_vertices = buffer_size // self._stride

        if colors is not None:
            if isinstance(colors, wgpu.GPUBuffer):
                self.color_buffer = colors
            else:
                color_result = self._process_vertex_data(
                    colors,
                    None,
                    padding_size=4,  # Pad to vec4 for alignment
                    buffer_label="line_pipeline_multi_coloured_colour_buffer",
                )
                if isinstance(color_result, wgpu.GPUBuffer):
                    self.color_buffer = color_result
                elif color_result:
                    self.color_buffer = color_result[0]
                else:
                    self.color_buffer = None

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 projection matrix
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the lines.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_vertices: Number of vertices to render (defaults to all)
        """
        num_vertices = kwargs.get("num_vertices", None)

        if self.vertex_buffer is None or self.color_buffer is None:
            return

        count = num_vertices if num_vertices is not None else self.num_vertices

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
        render_pass.set_vertex_buffer(0, self.vertex_buffer)
        render_pass.set_vertex_buffer(1, self.color_buffer)
        render_pass.draw(count)

    def cleanup(self) -> None:
        """Release resources."""
        if self.vertex_buffer:
            self.vertex_buffer.destroy()
        if self.color_buffer:
            self.color_buffer.destroy()
        super().cleanup()

__init__(device, data_type='Vec3', texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, stride=0, topology=wgpu.PrimitiveTopology.line_list)

Initialize the line rendering pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • data_type (str, default: 'Vec3' ) –

    NGL vertex data type name (e.g. "Vec3")

  • texture_format (TextureFormat, default: rgba8unorm ) –

    Color attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • stride (int, default: 0 ) –

    The stride of the vertex buffer. If 0, it is inferred from data_type.

  • topology (PrimitiveTopology, default: line_list ) –

    Primitive topology (line_list or line_strip)

Source code in ncca/ngl/webgpu/line_pipeline.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def __init__(
    self,
    device: wgpu.GPUDevice,
    data_type: str = "Vec3",
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    stride: int = 0,
    topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.line_list,
) -> None:
    """Initialize the line rendering pipeline.

    Args:
        device: WebGPU device
        data_type: NGL vertex data type name (e.g. "Vec3")
        texture_format: Color attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
        topology: Primitive topology (line_list or line_strip)
    """
    # Pipeline-specific buffer tracking
    self.vertex_buffer: Optional[wgpu.GPUBuffer] = None
    self.color_buffer: Optional[wgpu.GPUBuffer] = None
    self.num_vertices: int = 0

    super().__init__(
        device=device,
        texture_format=texture_format,
        depth_format=depth_format,
        msaa_sample_count=msaa_sample_count,
        data_type=data_type,
        stride=stride,
        topology=topology,
    )

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/line_pipeline.py
227
228
229
230
231
232
233
def cleanup(self) -> None:
    """Release resources."""
    if self.vertex_buffer:
        self.vertex_buffer.destroy()
    if self.color_buffer:
        self.color_buffer.destroy()
    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/line_pipeline.py
101
102
103
104
105
106
107
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
        ]
    )

render(render_pass, **kwargs)

Render the lines.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_vertices: Number of vertices to render (defaults to all)

Source code in ncca/ngl/webgpu/line_pipeline.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the lines.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_vertices: Number of vertices to render (defaults to all)
    """
    num_vertices = kwargs.get("num_vertices", None)

    if self.vertex_buffer is None or self.color_buffer is None:
        return

    count = num_vertices if num_vertices is not None else self.num_vertices

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
    render_pass.set_vertex_buffer(0, self.vertex_buffer)
    render_pass.set_vertex_buffer(1, self.color_buffer)
    render_pass.draw(count)

set_data(positions=None, colors=None, **kwargs)

Set the line data for rendering.

Parameters:
  • positions (ndarray | GPUBuffer | None, default: None ) –

    Nx2/Nx3 array of line positions or a pre-existing GPUBuffer.

  • colors (ndarray | GPUBuffer | None, default: None ) –

    Nx3 array of line colors (RGB) or a pre-existing GPUBuffer.

  • **kwargs (Any, default: {} ) –

    Unused, accepted for interface compatibility.

Source code in ncca/ngl/webgpu/line_pipeline.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def set_data(
    self,
    positions: np.ndarray | wgpu.GPUBuffer | None = None,
    colors: np.ndarray | wgpu.GPUBuffer | None = None,
    **kwargs: Any,
) -> None:
    """Set the line data for rendering.

    Args:
        positions: Nx2/Nx3 array of line positions or a pre-existing GPUBuffer.
        colors: Nx3 array of line colors (RGB) or a pre-existing GPUBuffer.
        **kwargs: Unused, accepted for interface compatibility.
    """
    if positions is not None:
        if isinstance(positions, wgpu.GPUBuffer):
            self.vertex_buffer = positions
            self.num_vertices = positions.size // self._stride
        else:  # numpy array
            self.vertex_buffer, buffer_size = self._create_or_update_buffer(
                self.vertex_buffer,
                positions,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                "line_pipeline_multi_coloured_position_buffer",
            )
            self.num_vertices = buffer_size // self._stride

    if colors is not None:
        if isinstance(colors, wgpu.GPUBuffer):
            self.color_buffer = colors
        else:
            color_result = self._process_vertex_data(
                colors,
                None,
                padding_size=4,  # Pad to vec4 for alignment
                buffer_label="line_pipeline_multi_coloured_colour_buffer",
            )
            if isinstance(color_result, wgpu.GPUBuffer):
                self.color_buffer = color_result
            elif color_result:
                self.color_buffer = color_result[0]
            else:
                self.color_buffer = None

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 projection matrix

Source code in ncca/ngl/webgpu/line_pipeline.py
191
192
193
194
195
196
197
198
199
200
201
202
203
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 projection matrix
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )

LinePipelineSingleColour

Bases: BaseLinePipeline

A reusable pipeline for rendering lines in WebGPU with single color.

Features: - Line strips or line segments - Single color for all lines - MVP matrix support - MSAA support

Source code in ncca/ngl/webgpu/line_pipeline.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
class LinePipelineSingleColour(BaseLinePipeline):
    """A reusable pipeline for rendering lines in WebGPU with single color.

    Features:
    - Line strips or line segments
    - Single color for all lines
    - MVP matrix support
    - MSAA support
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        data_type: str = "Vec3",
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        stride: int = 0,
        topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.line_list,
        colour: Tuple[float, float, float] = (1.0, 1.0, 1.0),
    ) -> None:
        """Initialize line rendering pipeline.

        Args:
            device: WebGPU device
            data_type: NGL vertex data type name (e.g. "Vec3")
            texture_format: Color attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
            topology: Primitive topology (line_list or line_strip)
            colour: RGB color tuple for lines (default white)
        """
        # Pipeline-specific buffer tracking
        self.vertex_buffer: Optional[wgpu.GPUBuffer] = None
        self.num_vertices: int = 0
        self._colour = np.array(colour, dtype=np.float32)

        super().__init__(
            device=device,
            texture_format=texture_format,
            depth_format=depth_format,
            msaa_sample_count=msaa_sample_count,
            data_type=data_type,
            stride=stride,
            topology=topology,
        )

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
                ("Colour", "float32", 3),
                ("padding", "float32", 1),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return LINE_SHADER_SINGLE_COLOUR

    def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
        """Get vertex buffer layout configurations for the pipeline."""
        return [
            {
                "array_stride": self._stride,
                "step_mode": wgpu.VertexStepMode.vertex,
                "attributes": [
                    {
                        "format": NGLToWebGPU.vertex_format(self._data_type),
                        "offset": 0,
                        "shader_location": 0,
                    },
                ],
            },
        ]

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        pass

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        return "line_pipeline_single_colour"

    def set_data(
        self,
        positions: np.ndarray | wgpu.GPUBuffer | None = None,
        colors: np.ndarray | wgpu.GPUBuffer | None = None,
        **kwargs: Any,
    ) -> None:
        """Set the line data for rendering.

        Args:
            positions: Nx2/Nx3 array of line positions or a pre-existing GPUBuffer.
            colors: Ignored for single colour pipeline
            **kwargs: Unused, accepted for interface compatibility.
        """
        if positions is not None:
            if isinstance(positions, wgpu.GPUBuffer):
                self.vertex_buffer = positions
                self.num_vertices = positions.size // self._stride
            else:  # numpy array
                self.vertex_buffer, buffer_size = self._create_or_update_buffer(
                    self.vertex_buffer,
                    positions,
                    wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                    "line_pipeline_single_colour_position_buffer",
                )
                self.num_vertices = buffer_size // self._stride

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 projection matrix
                - colour: RGB color tuple
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        if "colour" in kwargs and kwargs["colour"] is not None:
            colour = np.array(kwargs["colour"], dtype=np.float32)
            if colour.shape == (3,):
                self.uniform_data["Colour"] = colour
                self._colour = colour

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def set_color(self, colour: Tuple[float, float, float]) -> None:
        """Set the color for the lines.

        Args:
            colour: RGB color tuple
        """
        colour_array = np.array(colour, dtype=np.float32)
        if colour_array.shape == (3,):
            self.uniform_data["Colour"] = colour_array
            self._colour = colour_array
            self.device.queue.write_buffer(
                self.uniform_buffer, 0, self.uniform_data.tobytes()
            )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the lines.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_vertices: Number of vertices to render (defaults to all)
        """
        num_vertices = kwargs.get("num_vertices", None)

        if self.vertex_buffer is None:
            return

        count = num_vertices if num_vertices is not None else self.num_vertices

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
        render_pass.set_vertex_buffer(0, self.vertex_buffer)
        render_pass.draw(count)

    def cleanup(self) -> None:
        """Release resources."""
        if self.vertex_buffer:
            self.vertex_buffer.destroy()
        super().cleanup()

__init__(device, data_type='Vec3', texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, stride=0, topology=wgpu.PrimitiveTopology.line_list, colour=(1.0, 1.0, 1.0))

Initialize line rendering pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • data_type (str, default: 'Vec3' ) –

    NGL vertex data type name (e.g. "Vec3")

  • texture_format (TextureFormat, default: rgba8unorm ) –

    Color attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • stride (int, default: 0 ) –

    The stride of the vertex buffer. If 0, it is inferred from data_type.

  • topology (PrimitiveTopology, default: line_list ) –

    Primitive topology (line_list or line_strip)

  • colour (Tuple[float, float, float], default: (1.0, 1.0, 1.0) ) –

    RGB color tuple for lines (default white)

Source code in ncca/ngl/webgpu/line_pipeline.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
def __init__(
    self,
    device: wgpu.GPUDevice,
    data_type: str = "Vec3",
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    stride: int = 0,
    topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.line_list,
    colour: Tuple[float, float, float] = (1.0, 1.0, 1.0),
) -> None:
    """Initialize line rendering pipeline.

    Args:
        device: WebGPU device
        data_type: NGL vertex data type name (e.g. "Vec3")
        texture_format: Color attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
        topology: Primitive topology (line_list or line_strip)
        colour: RGB color tuple for lines (default white)
    """
    # Pipeline-specific buffer tracking
    self.vertex_buffer: Optional[wgpu.GPUBuffer] = None
    self.num_vertices: int = 0
    self._colour = np.array(colour, dtype=np.float32)

    super().__init__(
        device=device,
        texture_format=texture_format,
        depth_format=depth_format,
        msaa_sample_count=msaa_sample_count,
        data_type=data_type,
        stride=stride,
        topology=topology,
    )

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/line_pipeline.py
404
405
406
407
408
def cleanup(self) -> None:
    """Release resources."""
    if self.vertex_buffer:
        self.vertex_buffer.destroy()
    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/line_pipeline.py
284
285
286
287
288
289
290
291
292
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
            ("Colour", "float32", 3),
            ("padding", "float32", 1),
        ]
    )

render(render_pass, **kwargs)

Render the lines.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_vertices: Number of vertices to render (defaults to all)

Source code in ncca/ngl/webgpu/line_pipeline.py
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the lines.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_vertices: Number of vertices to render (defaults to all)
    """
    num_vertices = kwargs.get("num_vertices", None)

    if self.vertex_buffer is None:
        return

    count = num_vertices if num_vertices is not None else self.num_vertices

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
    render_pass.set_vertex_buffer(0, self.vertex_buffer)
    render_pass.draw(count)

set_color(colour)

Set the color for the lines.

Parameters:
  • colour (Tuple[float, float, float]) –

    RGB color tuple

Source code in ncca/ngl/webgpu/line_pipeline.py
369
370
371
372
373
374
375
376
377
378
379
380
381
def set_color(self, colour: Tuple[float, float, float]) -> None:
    """Set the color for the lines.

    Args:
        colour: RGB color tuple
    """
    colour_array = np.array(colour, dtype=np.float32)
    if colour_array.shape == (3,):
        self.uniform_data["Colour"] = colour_array
        self._colour = colour_array
        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

set_data(positions=None, colors=None, **kwargs)

Set the line data for rendering.

Parameters:
  • positions (ndarray | GPUBuffer | None, default: None ) –

    Nx2/Nx3 array of line positions or a pre-existing GPUBuffer.

  • colors (ndarray | GPUBuffer | None, default: None ) –

    Ignored for single colour pipeline

  • **kwargs (Any, default: {} ) –

    Unused, accepted for interface compatibility.

Source code in ncca/ngl/webgpu/line_pipeline.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
def set_data(
    self,
    positions: np.ndarray | wgpu.GPUBuffer | None = None,
    colors: np.ndarray | wgpu.GPUBuffer | None = None,
    **kwargs: Any,
) -> None:
    """Set the line data for rendering.

    Args:
        positions: Nx2/Nx3 array of line positions or a pre-existing GPUBuffer.
        colors: Ignored for single colour pipeline
        **kwargs: Unused, accepted for interface compatibility.
    """
    if positions is not None:
        if isinstance(positions, wgpu.GPUBuffer):
            self.vertex_buffer = positions
            self.num_vertices = positions.size // self._stride
        else:  # numpy array
            self.vertex_buffer, buffer_size = self._create_or_update_buffer(
                self.vertex_buffer,
                positions,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                "line_pipeline_single_colour_position_buffer",
            )
            self.num_vertices = buffer_size // self._stride

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 projection matrix - colour: RGB color tuple

Source code in ncca/ngl/webgpu/line_pipeline.py
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 projection matrix
            - colour: RGB color tuple
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    if "colour" in kwargs and kwargs["colour"] is not None:
        colour = np.array(kwargs["colour"], dtype=np.float32)
        if colour.shape == (3,):
            self.uniform_data["Colour"] = colour
            self._colour = colour

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )

TrianglePipelineMultiColour

Bases: BaseTrianglePipeline

A reusable pipeline for rendering triangles in WebGPU with per-vertex colors.

Features: - Triangle lists or triangle strips - Per-vertex colors - MVP matrix support - MSAA support

Source code in ncca/ngl/webgpu/triangle_pipeline.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class TrianglePipelineMultiColour(BaseTrianglePipeline):
    """A reusable pipeline for rendering triangles in WebGPU with per-vertex colors.

    Features:
    - Triangle lists or triangle strips
    - Per-vertex colors
    - MVP matrix support
    - MSAA support
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        data_type: str = "Vec3",
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        stride: int = 0,
        topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.triangle_list,
    ) -> None:
        """Initialize the triangle rendering pipeline.

        Args:
            device: WebGPU device
            data_type: NGL vertex data type name (e.g. "Vec3")
            texture_format: Color attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
            topology: Triangle topology (triangle_list or triangle_strip)
        """
        # Pipeline-specific buffer tracking
        self.vertex_buffer: Optional[wgpu.GPUBuffer] = None
        self.color_buffer: Optional[wgpu.GPUBuffer] = None
        self.num_vertices: int = 0

        super().__init__(
            device=device,
            texture_format=texture_format,
            depth_format=depth_format,
            msaa_sample_count=msaa_sample_count,
            data_type=data_type,
            stride=stride,
            topology=topology,
        )

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
                ("padding", "float32", 4),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return TRIANGLE_SHADER_MULTI_COLOURED

    def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
        """Get vertex buffer layout configurations for the pipeline."""
        return [
            {
                "array_stride": self._stride,
                "step_mode": "vertex",
                "attributes": [
                    {
                        "format": NGLToWebGPU.vertex_format(self._data_type),
                        "offset": 0,
                        "shader_location": 0,
                    },
                ],
            },
            {
                "array_stride": NGLToWebGPU.stride_from_type("Vec3"),
                "step_mode": "vertex",
                "attributes": [
                    {
                        "format": NGLToWebGPU.vertex_format("Vec3"),
                        "offset": 0,
                        "shader_location": 1,
                    },
                ],
            },
        ]

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        pass  # No specific defaults for triangle pipeline

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        topology_name = (
            "list"
            if self._topology == wgpu.PrimitiveTopology.triangle_list
            else "strip"
        )
        return f"triangle_pipeline_multi_coloured_{topology_name}"

    def set_data(
        self,
        positions: np.ndarray | wgpu.GPUBuffer | None = None,
        colors: np.ndarray | wgpu.GPUBuffer | None = None,
        **kwargs: Any,
    ) -> None:
        """Set the triangle data for rendering.

        Args:
            positions: Nx2/Nx3 array of triangle positions or a pre-existing GPUBuffer.
            colors: Nx3 array of triangle colors (RGB) or a pre-existing GPUBuffer.
            **kwargs: Unused, accepted for interface compatibility.
        """
        if positions is not None:
            if isinstance(positions, wgpu.GPUBuffer):
                self.vertex_buffer = positions
                self.num_vertices = positions.size // self._stride
            else:  # numpy array
                self.vertex_buffer, buffer_size = self._create_or_update_buffer(
                    self.vertex_buffer,
                    positions,
                    wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                    f"triangle_pipeline_multi_coloured_position_buffer_{self._get_pipeline_label()}",
                )
                self.num_vertices = buffer_size // self._stride

        if colors is not None:
            if isinstance(colors, wgpu.GPUBuffer):
                self.color_buffer = colors
            else:
                color_result = self._process_vertex_data(
                    colors,
                    None,
                    padding_size=4,  # Pad to vec4 for alignment
                    buffer_label=f"triangle_pipeline_multi_coloured_colour_buffer_{self._get_pipeline_label()}",
                )
                if isinstance(color_result, wgpu.GPUBuffer):
                    self.color_buffer = color_result
                elif color_result:
                    self.color_buffer = color_result[0]
                else:
                    self.color_buffer = None

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 projection matrix
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the triangles.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_vertices: Number of vertices to render (defaults to all)
        """
        num_vertices = kwargs.get("num_vertices", None)

        if self.vertex_buffer is None or self.color_buffer is None:
            return

        count = num_vertices if num_vertices is not None else self.num_vertices

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
        render_pass.set_vertex_buffer(0, self.vertex_buffer)
        render_pass.set_vertex_buffer(1, self.color_buffer)
        render_pass.draw(count)

    def cleanup(self) -> None:
        """Release resources."""
        if self.vertex_buffer:
            self.vertex_buffer.destroy()
        if self.color_buffer:
            self.color_buffer.destroy()
        super().cleanup()

__init__(device, data_type='Vec3', texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, stride=0, topology=wgpu.PrimitiveTopology.triangle_list)

Initialize the triangle rendering pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • data_type (str, default: 'Vec3' ) –

    NGL vertex data type name (e.g. "Vec3")

  • texture_format (TextureFormat, default: rgba8unorm ) –

    Color attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • stride (int, default: 0 ) –

    The stride of the vertex buffer. If 0, it is inferred from data_type.

  • topology (PrimitiveTopology, default: triangle_list ) –

    Triangle topology (triangle_list or triangle_strip)

Source code in ncca/ngl/webgpu/triangle_pipeline.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def __init__(
    self,
    device: wgpu.GPUDevice,
    data_type: str = "Vec3",
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    stride: int = 0,
    topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.triangle_list,
) -> None:
    """Initialize the triangle rendering pipeline.

    Args:
        device: WebGPU device
        data_type: NGL vertex data type name (e.g. "Vec3")
        texture_format: Color attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
        topology: Triangle topology (triangle_list or triangle_strip)
    """
    # Pipeline-specific buffer tracking
    self.vertex_buffer: Optional[wgpu.GPUBuffer] = None
    self.color_buffer: Optional[wgpu.GPUBuffer] = None
    self.num_vertices: int = 0

    super().__init__(
        device=device,
        texture_format=texture_format,
        depth_format=depth_format,
        msaa_sample_count=msaa_sample_count,
        data_type=data_type,
        stride=stride,
        topology=topology,
    )

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/triangle_pipeline.py
236
237
238
239
240
241
242
def cleanup(self) -> None:
    """Release resources."""
    if self.vertex_buffer:
        self.vertex_buffer.destroy()
    if self.color_buffer:
        self.color_buffer.destroy()
    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/triangle_pipeline.py
104
105
106
107
108
109
110
111
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
            ("padding", "float32", 4),
        ]
    )

render(render_pass, **kwargs)

Render the triangles.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_vertices: Number of vertices to render (defaults to all)

Source code in ncca/ngl/webgpu/triangle_pipeline.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the triangles.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_vertices: Number of vertices to render (defaults to all)
    """
    num_vertices = kwargs.get("num_vertices", None)

    if self.vertex_buffer is None or self.color_buffer is None:
        return

    count = num_vertices if num_vertices is not None else self.num_vertices

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
    render_pass.set_vertex_buffer(0, self.vertex_buffer)
    render_pass.set_vertex_buffer(1, self.color_buffer)
    render_pass.draw(count)

set_data(positions=None, colors=None, **kwargs)

Set the triangle data for rendering.

Parameters:
  • positions (ndarray | GPUBuffer | None, default: None ) –

    Nx2/Nx3 array of triangle positions or a pre-existing GPUBuffer.

  • colors (ndarray | GPUBuffer | None, default: None ) –

    Nx3 array of triangle colors (RGB) or a pre-existing GPUBuffer.

  • **kwargs (Any, default: {} ) –

    Unused, accepted for interface compatibility.

Source code in ncca/ngl/webgpu/triangle_pipeline.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def set_data(
    self,
    positions: np.ndarray | wgpu.GPUBuffer | None = None,
    colors: np.ndarray | wgpu.GPUBuffer | None = None,
    **kwargs: Any,
) -> None:
    """Set the triangle data for rendering.

    Args:
        positions: Nx2/Nx3 array of triangle positions or a pre-existing GPUBuffer.
        colors: Nx3 array of triangle colors (RGB) or a pre-existing GPUBuffer.
        **kwargs: Unused, accepted for interface compatibility.
    """
    if positions is not None:
        if isinstance(positions, wgpu.GPUBuffer):
            self.vertex_buffer = positions
            self.num_vertices = positions.size // self._stride
        else:  # numpy array
            self.vertex_buffer, buffer_size = self._create_or_update_buffer(
                self.vertex_buffer,
                positions,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                f"triangle_pipeline_multi_coloured_position_buffer_{self._get_pipeline_label()}",
            )
            self.num_vertices = buffer_size // self._stride

    if colors is not None:
        if isinstance(colors, wgpu.GPUBuffer):
            self.color_buffer = colors
        else:
            color_result = self._process_vertex_data(
                colors,
                None,
                padding_size=4,  # Pad to vec4 for alignment
                buffer_label=f"triangle_pipeline_multi_coloured_colour_buffer_{self._get_pipeline_label()}",
            )
            if isinstance(color_result, wgpu.GPUBuffer):
                self.color_buffer = color_result
            elif color_result:
                self.color_buffer = color_result[0]
            else:
                self.color_buffer = None

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 projection matrix

Source code in ncca/ngl/webgpu/triangle_pipeline.py
200
201
202
203
204
205
206
207
208
209
210
211
212
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 projection matrix
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )

TrianglePipelineSingleColour

Bases: BaseTrianglePipeline

A reusable pipeline for rendering triangles in WebGPU with single color.

Features: - Triangle lists or triangle strips - Single color for all triangles - MVP matrix support - MSAA support

Source code in ncca/ngl/webgpu/triangle_pipeline.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
class TrianglePipelineSingleColour(BaseTrianglePipeline):
    """A reusable pipeline for rendering triangles in WebGPU with single color.

    Features:
    - Triangle lists or triangle strips
    - Single color for all triangles
    - MVP matrix support
    - MSAA support
    """

    def __init__(
        self,
        device: wgpu.GPUDevice,
        data_type: str = "Vec3",
        texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
        depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
        msaa_sample_count: int = 4,
        stride: int = 0,
        topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.triangle_list,
        colour: Tuple[float, float, float] = (1.0, 1.0, 1.0),
    ) -> None:
        """Initialize the triangle rendering pipeline.

        Args:
            device: WebGPU device
            data_type: NGL vertex data type name (e.g. "Vec3")
            texture_format: Color attachment format
            depth_format: Depth attachment format
            msaa_sample_count: Number of MSAA samples
            stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
            topology: Triangle topology (triangle_list or triangle_strip)
            colour: RGB color tuple for triangles (default white)
        """
        # Pipeline-specific buffer tracking
        self.vertex_buffer: Optional[wgpu.GPUBuffer] = None
        self.num_vertices: int = 0
        self._colour = np.array(colour, dtype=np.float32)

        super().__init__(
            device=device,
            texture_format=texture_format,
            depth_format=depth_format,
            msaa_sample_count=msaa_sample_count,
            data_type=data_type,
            stride=stride,
            topology=topology,
        )

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
                ("Colour", "float32", 3),
                ("padding", "float32", 1),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return TRIANGLE_SHADER_SINGLE_COLOUR

    def _get_vertex_buffer_layouts(self) -> List[Dict[str, Any]]:
        """Get vertex buffer layout configurations for the pipeline."""
        return [
            {
                "array_stride": self._stride,
                "step_mode": "vertex",
                "attributes": [
                    {
                        "format": NGLToWebGPU.vertex_format(self._data_type),
                        "offset": 0,
                        "shader_location": 0,
                    },
                ],
            },
        ]

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        pass  # No specific defaults for triangle pipeline

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        topology_name = (
            "list"
            if self._topology == wgpu.PrimitiveTopology.triangle_list
            else "strip"
        )
        return f"triangle_pipeline_single_colour_{topology_name}"

    def set_data(
        self,
        positions: np.ndarray | wgpu.GPUBuffer | None = None,
        colors: np.ndarray | wgpu.GPUBuffer | None = None,
        **kwargs: Any,
    ) -> None:
        """Set the triangle data for rendering.

        Args:
            positions: Nx2/Nx3 array of triangle positions or a pre-existing GPUBuffer.
            colors: Ignored for single colour pipeline
            **kwargs: Unused, accepted for interface compatibility.
        """
        if positions is not None:
            if isinstance(positions, wgpu.GPUBuffer):
                self.vertex_buffer = positions
                self.num_vertices = positions.size // self._stride
            else:  # numpy array
                self.vertex_buffer, buffer_size = self._create_or_update_buffer(
                    self.vertex_buffer,
                    positions,
                    wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                    f"triangle_pipeline_single_colour_position_buffer_{self._get_pipeline_label()}",
                )
                self.num_vertices = buffer_size // self._stride

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 projection matrix
                - colour: RGB color tuple
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        if "colour" in kwargs and kwargs["colour"] is not None:
            colour = np.array(kwargs["colour"], dtype=np.float32)
            if colour.shape == (3,):
                self.uniform_data["Colour"] = colour
                self._colour = colour

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def set_color(self, colour: Tuple[float, float, float]) -> None:
        """Set the color for the triangles.

        Args:
            colour: RGB color tuple
        """
        colour_array = np.array(colour, dtype=np.float32)
        if colour_array.shape == (3,):
            self.uniform_data["Colour"] = colour_array
            self._colour = colour_array
            self.device.queue.write_buffer(
                self.uniform_buffer, 0, self.uniform_data.tobytes()
            )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the triangles.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_vertices: Number of vertices to render (defaults to all)
        """
        num_vertices = kwargs.get("num_vertices", None)

        if self.vertex_buffer is None:
            return

        count = num_vertices if num_vertices is not None else self.num_vertices

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
        render_pass.set_vertex_buffer(0, self.vertex_buffer)
        render_pass.draw(count)

    def cleanup(self) -> None:
        """Release resources."""
        if self.vertex_buffer:
            self.vertex_buffer.destroy()
        super().cleanup()

__init__(device, data_type='Vec3', texture_format=wgpu.TextureFormat.rgba8unorm, depth_format=wgpu.TextureFormat.depth24plus, msaa_sample_count=4, stride=0, topology=wgpu.PrimitiveTopology.triangle_list, colour=(1.0, 1.0, 1.0))

Initialize the triangle rendering pipeline.

Parameters:
  • device (GPUDevice) –

    WebGPU device

  • data_type (str, default: 'Vec3' ) –

    NGL vertex data type name (e.g. "Vec3")

  • texture_format (TextureFormat, default: rgba8unorm ) –

    Color attachment format

  • depth_format (TextureFormat, default: depth24plus ) –

    Depth attachment format

  • msaa_sample_count (int, default: 4 ) –

    Number of MSAA samples

  • stride (int, default: 0 ) –

    The stride of the vertex buffer. If 0, it is inferred from data_type.

  • topology (PrimitiveTopology, default: triangle_list ) –

    Triangle topology (triangle_list or triangle_strip)

  • colour (Tuple[float, float, float], default: (1.0, 1.0, 1.0) ) –

    RGB color tuple for triangles (default white)

Source code in ncca/ngl/webgpu/triangle_pipeline.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
def __init__(
    self,
    device: wgpu.GPUDevice,
    data_type: str = "Vec3",
    texture_format: wgpu.TextureFormat = wgpu.TextureFormat.rgba8unorm,
    depth_format: wgpu.TextureFormat = wgpu.TextureFormat.depth24plus,
    msaa_sample_count: int = 4,
    stride: int = 0,
    topology: wgpu.PrimitiveTopology = wgpu.PrimitiveTopology.triangle_list,
    colour: Tuple[float, float, float] = (1.0, 1.0, 1.0),
) -> None:
    """Initialize the triangle rendering pipeline.

    Args:
        device: WebGPU device
        data_type: NGL vertex data type name (e.g. "Vec3")
        texture_format: Color attachment format
        depth_format: Depth attachment format
        msaa_sample_count: Number of MSAA samples
        stride: The stride of the vertex buffer. If 0, it is inferred from data_type.
        topology: Triangle topology (triangle_list or triangle_strip)
        colour: RGB color tuple for triangles (default white)
    """
    # Pipeline-specific buffer tracking
    self.vertex_buffer: Optional[wgpu.GPUBuffer] = None
    self.num_vertices: int = 0
    self._colour = np.array(colour, dtype=np.float32)

    super().__init__(
        device=device,
        texture_format=texture_format,
        depth_format=depth_format,
        msaa_sample_count=msaa_sample_count,
        data_type=data_type,
        stride=stride,
        topology=topology,
    )

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/triangle_pipeline.py
418
419
420
421
422
def cleanup(self) -> None:
    """Release resources."""
    if self.vertex_buffer:
        self.vertex_buffer.destroy()
    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/triangle_pipeline.py
293
294
295
296
297
298
299
300
301
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
            ("Colour", "float32", 3),
            ("padding", "float32", 1),
        ]
    )

render(render_pass, **kwargs)

Render the triangles.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_vertices: Number of vertices to render (defaults to all)

Source code in ncca/ngl/webgpu/triangle_pipeline.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the triangles.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_vertices: Number of vertices to render (defaults to all)
    """
    num_vertices = kwargs.get("num_vertices", None)

    if self.vertex_buffer is None:
        return

    count = num_vertices if num_vertices is not None else self.num_vertices

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)
    render_pass.set_vertex_buffer(0, self.vertex_buffer)
    render_pass.draw(count)

set_color(colour)

Set the color for the triangles.

Parameters:
  • colour (Tuple[float, float, float]) –

    RGB color tuple

Source code in ncca/ngl/webgpu/triangle_pipeline.py
383
384
385
386
387
388
389
390
391
392
393
394
395
def set_color(self, colour: Tuple[float, float, float]) -> None:
    """Set the color for the triangles.

    Args:
        colour: RGB color tuple
    """
    colour_array = np.array(colour, dtype=np.float32)
    if colour_array.shape == (3,):
        self.uniform_data["Colour"] = colour_array
        self._colour = colour_array
        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

set_data(positions=None, colors=None, **kwargs)

Set the triangle data for rendering.

Parameters:
  • positions (ndarray | GPUBuffer | None, default: None ) –

    Nx2/Nx3 array of triangle positions or a pre-existing GPUBuffer.

  • colors (ndarray | GPUBuffer | None, default: None ) –

    Ignored for single colour pipeline

  • **kwargs (Any, default: {} ) –

    Unused, accepted for interface compatibility.

Source code in ncca/ngl/webgpu/triangle_pipeline.py
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
def set_data(
    self,
    positions: np.ndarray | wgpu.GPUBuffer | None = None,
    colors: np.ndarray | wgpu.GPUBuffer | None = None,
    **kwargs: Any,
) -> None:
    """Set the triangle data for rendering.

    Args:
        positions: Nx2/Nx3 array of triangle positions or a pre-existing GPUBuffer.
        colors: Ignored for single colour pipeline
        **kwargs: Unused, accepted for interface compatibility.
    """
    if positions is not None:
        if isinstance(positions, wgpu.GPUBuffer):
            self.vertex_buffer = positions
            self.num_vertices = positions.size // self._stride
        else:  # numpy array
            self.vertex_buffer, buffer_size = self._create_or_update_buffer(
                self.vertex_buffer,
                positions,
                wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                f"triangle_pipeline_single_colour_position_buffer_{self._get_pipeline_label()}",
            )
            self.num_vertices = buffer_size // self._stride

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 projection matrix - colour: RGB color tuple

Source code in ncca/ngl/webgpu/triangle_pipeline.py
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 projection matrix
            - colour: RGB color tuple
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    if "colour" in kwargs and kwargs["colour"] is not None:
        colour = np.array(kwargs["colour"], dtype=np.float32)
        if colour.shape == (3,):
            self.uniform_data["Colour"] = colour
            self._colour = colour

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )

InstancedGeometryPipelineMultiColour

Bases: BaseInstancedGeometryPipeline

A reusable pipeline for rendering instanced geometry in WebGPU with per-instance colors.

Features: - Instanced rendering of arbitrary geometry using interleaved x,y,z,nx,ny,nz,u,v format - Per-instance colors - Per-instance positioning - Configurable instance transformation matrix - Model, View Projection matrix support - MSAA support

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
class InstancedGeometryPipelineMultiColour(BaseInstancedGeometryPipeline):
    """A reusable pipeline for rendering instanced geometry in WebGPU with per-instance colors.

    Features:
    - Instanced rendering of arbitrary geometry using interleaved x,y,z,nx,ny,nz,u,v format
    - Per-instance colors
    - Per-instance positioning
    - Configurable instance transformation matrix
    - Model, View Projection matrix support
    - MSAA support
    """

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
                ("ViewMatrix", "float32", (4, 4)),
                ("instance_transform", "float32", (4, 4)),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return INSTANCED_SHADER_MULTI_COLOURED

    def _get_vertex_buffer_layouts(self) -> list:
        """Get vertex buffer layout configurations for the pipeline."""
        return self._get_default_vertex_layouts()

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        self.uniform_data["instance_transform"] = np.eye(4, dtype=np.float32)
        self.uniform_data["ViewMatrix"] = np.eye(4, dtype=np.float32)

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        return "instanced_geometry_pipeline_multi_colour"

    def set_data(self, **kwargs: Any) -> None:
        """Set the instanced geometry data for rendering.

        Args:
            **kwargs: Pipeline-specific data parameters
                - positions: Nx3 array of instance positions or a pre-existing GPUBuffer.
                - colours: Nx3 array of instance colors (RGB) or a pre-existing GPUBuffer.
                           If None, uses white.
                - geometry_data: Mx8 array of interleaved geometry data in format
                                x,y,z,nx,ny,nz,u,v or pre-existing GPUBuffer.
                                Must match the format output by PrimData methods.
        """
        positions = kwargs.get("positions")
        colours = kwargs.get("colours")
        geometry_data = kwargs.get("geometry_data")

        self._set_position_data(positions)
        self._setup_instance_id_buffer()
        self._set_colour_data(colours)
        self._set_geometry_data(geometry_data)

    def _set_position_data(self, positions: np.ndarray | wgpu.GPUBuffer) -> None:
        """Set instance position data from GPUBuffer or numpy array."""
        if isinstance(positions, wgpu.GPUBuffer):
            self.position_buffer = positions
            self.num_instances = positions.size // self._stride
        else:
            self.num_instances = len(positions)
            if self.position_buffer:
                self.position_buffer.destroy()
            self.position_buffer = self.device.create_buffer_with_data(
                data=positions.astype(np.float32).tobytes(),
                usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                label="instanced_geometry_multi_colour_position_buffer",
            )

    def _setup_instance_id_buffer(self) -> None:
        """Create buffer containing instance IDs."""
        if self.instance_id_buffer:
            self.instance_id_buffer.destroy()
        self.instance_id_buffer = super()._create_instance_id_buffer(self.num_instances)

    def _set_colour_data(self, colours: np.ndarray | wgpu.GPUBuffer | None) -> None:
        """Set colour data from GPUBuffer, numpy array, or create default."""
        if self.colour_buffer:
            self.colour_buffer.destroy()
            self.colour_buffer = None

        if colours is None:
            self._create_default_colours()
        else:
            self._create_colour_buffer(colours)

    def _create_default_colours(self) -> None:
        """Create default white colours for all instances."""
        default_colours = np.ones((self.num_instances, 3), dtype=np.float32)
        self.colour_buffer = self.device.create_buffer_with_data(
            data=default_colours.tobytes(),
            usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
        )

    def _create_colour_buffer(self, colours: np.ndarray | wgpu.GPUBuffer) -> None:
        """Create colour buffer from GPUBuffer or numpy array."""
        if isinstance(colours, wgpu.GPUBuffer):
            self.colour_buffer = colours
        else:
            colour_array = colours.astype(np.float32)
            self.colour_buffer = self.device.create_buffer_with_data(
                data=colour_array.tobytes(),
                usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            )

    def _set_geometry_data(
        self, geometry_data: np.ndarray | wgpu.GPUBuffer | None
    ) -> None:
        """Set geometry data from GPUBuffer or numpy array."""
        if geometry_data is None:
            raise ValueError(GEOM_ERROR)

        if isinstance(geometry_data, wgpu.GPUBuffer):
            self.geometry_buffer = geometry_data
            self.num_vertices = geometry_data.size // (8 * 4)
        else:
            self._process_geometry_array(geometry_data)

    def _process_geometry_array(self, geometry_data: np.ndarray) -> None:
        """Process geometry numpy array and create buffer."""
        geometry_data = np.asarray(geometry_data, dtype=np.float32)
        geometry_data = self._validate_and_reshape_geometry(geometry_data)
        self.num_vertices = geometry_data.shape[0]

        if self.geometry_buffer:
            self.geometry_buffer.destroy()
        self.geometry_buffer = self.device.create_buffer_with_data(
            data=geometry_data.tobytes(),
            usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            label="instanced_geometry_buffer",
        )

    def _validate_and_reshape_geometry(self, geometry_data: np.ndarray) -> np.ndarray:
        """Validate geometry data dimensions and reshape if needed."""
        if geometry_data.ndim == 1:
            geometry_data = geometry_data.reshape(-1, 8)
        elif geometry_data.ndim != 2:
            raise ValueError(
                f"geometry_data must be 1D or 2D array, got {geometry_data.ndim}D"
            )

        if geometry_data.shape[1] != 8:
            raise ValueError(
                f"geometry_data must have 8 components (x,y,z,nx,ny,nz,u,v), got {geometry_data.shape[1]}"
            )

        return geometry_data

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 model view projection matrix
                - view_matrix: 4x4 view matrix
                - instance_transform: 4x4 transformation matrix for each instance
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
            self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]

        if "instance_transform" in kwargs and kwargs["instance_transform"] is not None:
            self.uniform_data["instance_transform"] = kwargs["instance_transform"]

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the instanced geometry.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_instances: Number of instances to render (defaults to all)
        """
        num_instances = kwargs.get("num_instances", None)

        if (
            self.position_buffer is None
            or self.colour_buffer is None
            or self.instance_id_buffer is None
            or self.geometry_buffer is None
        ):
            return

        count = num_instances if num_instances is not None else self.num_instances

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)

        # Set instance buffers (match shader layout)
        render_pass.set_vertex_buffer(0, self.position_buffer)  # location(0) position
        render_pass.set_vertex_buffer(1, self.colour_buffer)  # location(1) colour
        render_pass.set_vertex_buffer(
            2, self.instance_id_buffer
        )  # location(2) instance_id

        # Set single interleaved geometry buffer
        render_pass.set_vertex_buffer(
            3, self.geometry_buffer
        )  # locations(3,4,5) interleaved

        render_pass.draw(self.num_vertices, count)

    def cleanup(self) -> None:
        """Release resources."""
        if self.position_buffer:
            self.position_buffer.destroy()
        if self.colour_buffer:
            self.colour_buffer.destroy()
        if self.instance_id_buffer:
            self.instance_id_buffer.destroy()
        if self.geometry_buffer:
            self.geometry_buffer.destroy()

        super().cleanup()

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
375
376
377
378
379
380
381
382
383
384
385
386
def cleanup(self) -> None:
    """Release resources."""
    if self.position_buffer:
        self.position_buffer.destroy()
    if self.colour_buffer:
        self.colour_buffer.destroy()
    if self.instance_id_buffer:
        self.instance_id_buffer.destroy()
    if self.geometry_buffer:
        self.geometry_buffer.destroy()

    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
173
174
175
176
177
178
179
180
181
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
            ("ViewMatrix", "float32", (4, 4)),
            ("instance_transform", "float32", (4, 4)),
        ]
    )

render(render_pass, **kwargs)

Render the instanced geometry.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_instances: Number of instances to render (defaults to all)

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the instanced geometry.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_instances: Number of instances to render (defaults to all)
    """
    num_instances = kwargs.get("num_instances", None)

    if (
        self.position_buffer is None
        or self.colour_buffer is None
        or self.instance_id_buffer is None
        or self.geometry_buffer is None
    ):
        return

    count = num_instances if num_instances is not None else self.num_instances

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)

    # Set instance buffers (match shader layout)
    render_pass.set_vertex_buffer(0, self.position_buffer)  # location(0) position
    render_pass.set_vertex_buffer(1, self.colour_buffer)  # location(1) colour
    render_pass.set_vertex_buffer(
        2, self.instance_id_buffer
    )  # location(2) instance_id

    # Set single interleaved geometry buffer
    render_pass.set_vertex_buffer(
        3, self.geometry_buffer
    )  # locations(3,4,5) interleaved

    render_pass.draw(self.num_vertices, count)

set_data(**kwargs)

Set the instanced geometry data for rendering.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific data parameters - positions: Nx3 array of instance positions or a pre-existing GPUBuffer. - colours: Nx3 array of instance colors (RGB) or a pre-existing GPUBuffer. If None, uses white. - geometry_data: Mx8 array of interleaved geometry data in format x,y,z,nx,ny,nz,u,v or pre-existing GPUBuffer. Must match the format output by PrimData methods.

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def set_data(self, **kwargs: Any) -> None:
    """Set the instanced geometry data for rendering.

    Args:
        **kwargs: Pipeline-specific data parameters
            - positions: Nx3 array of instance positions or a pre-existing GPUBuffer.
            - colours: Nx3 array of instance colors (RGB) or a pre-existing GPUBuffer.
                       If None, uses white.
            - geometry_data: Mx8 array of interleaved geometry data in format
                            x,y,z,nx,ny,nz,u,v or pre-existing GPUBuffer.
                            Must match the format output by PrimData methods.
    """
    positions = kwargs.get("positions")
    colours = kwargs.get("colours")
    geometry_data = kwargs.get("geometry_data")

    self._set_position_data(positions)
    self._setup_instance_id_buffer()
    self._set_colour_data(colours)
    self._set_geometry_data(geometry_data)

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 model view projection matrix - view_matrix: 4x4 view matrix - instance_transform: 4x4 transformation matrix for each instance

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 model view projection matrix
            - view_matrix: 4x4 view matrix
            - instance_transform: 4x4 transformation matrix for each instance
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
        self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]

    if "instance_transform" in kwargs and kwargs["instance_transform"] is not None:
        self.uniform_data["instance_transform"] = kwargs["instance_transform"]

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )

InstancedGeometryPipelineSingleColour

Bases: BaseInstancedGeometryPipeline

A reusable pipeline for rendering instanced geometry in WebGPU with single color.

Features: - Instanced rendering of arbitrary geometry using interleaved x,y,z,nx,ny,nz,u,v format - Single color for all instances - Per-instance positioning - Configurable instance transformation matrix - Model, View Projection matrix support - MSAA support

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
class InstancedGeometryPipelineSingleColour(BaseInstancedGeometryPipeline):
    """A reusable pipeline for rendering instanced geometry in WebGPU with single color.

    Features:
    - Instanced rendering of arbitrary geometry using interleaved x,y,z,nx,ny,nz,u,v format
    - Single color for all instances
    - Per-instance positioning
    - Configurable instance transformation matrix
    - Model, View Projection matrix support
    - MSAA support
    """

    def get_dtype(self) -> np.dtype:
        """Get the data type of the pipeline."""
        return np.dtype(
            [
                ("MVP", "float32", (4, 4)),
                ("ViewMatrix", "float32", (4, 4)),
                ("colour", "float32", 4),  # Vec4 for alignment (RGB + padding)
                ("instance_transform", "float32", (4, 4)),
            ]
        )

    def _get_shader_code(self) -> str:
        """Get the WGSL shader code for this pipeline."""
        return INSTANCED_SHADER_SINGLE_COLOUR

    def _get_vertex_buffer_layouts(self) -> list:
        """Get vertex buffer layout configurations for the pipeline."""
        return self._get_default_vertex_layouts()

    def _set_default_uniforms(self) -> None:
        """Set default values for uniform data."""
        self.uniform_data["colour"] = np.array(
            [1.0, 1.0, 1.0, 1.0], dtype=np.float32
        )  # White
        self.uniform_data["instance_transform"] = np.eye(4, dtype=np.float32)
        self.uniform_data["ViewMatrix"] = np.eye(4, dtype=np.float32)

    def _get_pipeline_label(self) -> str:
        """Get the label for the pipeline."""
        return "instanced_geometry_pipeline_single_colour"

    def set_data(self, **kwargs: Any) -> None:
        """Set the instanced geometry data for rendering.

        Args:
            **kwargs: Pipeline-specific data parameters
                - positions: Nx3 array of instance positions or a pre-existing GPUBuffer.
                - colours: Nx3 array of instance colors (RGB) or a pre-existing GPUBuffer.
                           If None, uses white.
                - geometry_data: Mx8 array of interleaved geometry data in format
                                x,y,z,nx,ny,nz,u,v or pre-existing GPUBuffer.
                                Must match the format output by PrimData methods.
        """
        positions = kwargs.get("positions")
        colours = kwargs.get("colours")
        geometry_data = kwargs.get("geometry_data")

        self._set_position_data(positions)
        self._setup_instance_id_buffer()
        self._set_colour_data(colours)
        self._set_geometry_data(geometry_data)

    def _set_position_data(self, positions: np.ndarray | wgpu.GPUBuffer) -> None:
        """Set instance position data from GPUBuffer or numpy array."""
        if isinstance(positions, wgpu.GPUBuffer):
            self.position_buffer = positions
            self.num_instances = positions.size // self._stride
        else:
            self.num_instances = len(positions)
            if self.position_buffer:
                self.position_buffer.destroy()
            self.position_buffer = self.device.create_buffer_with_data(
                data=positions.astype(np.float32).tobytes(),
                usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
                label="instanced_geometry_multi_colour_position_buffer",
            )

    def _setup_instance_id_buffer(self) -> None:
        """Create buffer containing instance IDs."""
        if self.instance_id_buffer:
            self.instance_id_buffer.destroy()
        self.instance_id_buffer = super()._create_instance_id_buffer(self.num_instances)

    def _set_colour_data(self, colours: np.ndarray | wgpu.GPUBuffer | None) -> None:
        """Set colour data from GPUBuffer, numpy array, or create default."""
        if self.colour_buffer:
            self.colour_buffer.destroy()
            self.colour_buffer = None

        if colours is None:
            self._create_default_colours()
        else:
            self._create_colour_buffer(colours)

    def _create_default_colours(self) -> None:
        """Create default white colours for all instances."""
        default_colours = np.ones((self.num_instances, 3), dtype=np.float32)
        self.colour_buffer = self.device.create_buffer_with_data(
            data=default_colours.tobytes(),
            usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
        )

    def _create_colour_buffer(self, colours: np.ndarray | wgpu.GPUBuffer) -> None:
        """Create colour buffer from GPUBuffer or numpy array."""
        if isinstance(colours, wgpu.GPUBuffer):
            self.colour_buffer = colours
        else:
            colour_array = colours.astype(np.float32)
            self.colour_buffer = self.device.create_buffer_with_data(
                data=colour_array.tobytes(),
                usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            )

    def _set_geometry_data(
        self, geometry_data: np.ndarray | wgpu.GPUBuffer | None
    ) -> None:
        """Set geometry data from GPUBuffer or numpy array."""
        if geometry_data is None:
            raise ValueError(GEOM_ERROR)

        if isinstance(geometry_data, wgpu.GPUBuffer):
            self.geometry_buffer = geometry_data
            self.num_vertices = geometry_data.size // (8 * 4)
        else:
            self._process_geometry_array(geometry_data)

    def _process_geometry_array(self, geometry_data: np.ndarray) -> None:
        """Process geometry numpy array and create buffer."""
        geometry_data = np.asarray(geometry_data, dtype=np.float32)
        geometry_data = self._validate_and_reshape_geometry(geometry_data)
        self.num_vertices = geometry_data.shape[0]

        if self.geometry_buffer:
            self.geometry_buffer.destroy()
        self.geometry_buffer = self.device.create_buffer_with_data(
            data=geometry_data.tobytes(),
            usage=wgpu.BufferUsage.VERTEX | wgpu.BufferUsage.COPY_DST,
            label="instanced_geometry_buffer",
        )

    def _validate_and_reshape_geometry(self, geometry_data: np.ndarray) -> np.ndarray:
        """Validate geometry data dimensions and reshape if needed."""
        if geometry_data.ndim == 1:
            geometry_data = geometry_data.reshape(-1, 8)
        elif geometry_data.ndim != 2:
            raise ValueError(
                f"geometry_data must be 1D or 2D array, got {geometry_data.ndim}D"
            )

        if geometry_data.shape[1] != 8:
            raise ValueError(
                f"geometry_data must have 8 components (x,y,z,nx,ny,nz,u,v), got {geometry_data.shape[1]}"
            )

        return geometry_data

    def update_uniforms(self, **kwargs: Any) -> None:
        """Update uniform buffer values.

        Args:
            **kwargs: Pipeline-specific uniform parameters
                - mvp: 4x4 model view projection matrix
                - view_matrix: 4x4 view matrix
                - colour: 3-element array of RGB color values
                - instance_transform: 4x4 transformation matrix for each instance
        """
        if "mvp" in kwargs and kwargs["mvp"] is not None:
            self.uniform_data["MVP"] = kwargs["mvp"]

        if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
            self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]

        if "colour" in kwargs and kwargs["colour"] is not None:
            self.uniform_data["colour"][:3] = kwargs["colour"]

        if "instance_transform" in kwargs and kwargs["instance_transform"] is not None:
            self.uniform_data["instance_transform"] = kwargs["instance_transform"]

        self.device.queue.write_buffer(
            self.uniform_buffer, 0, self.uniform_data.tobytes()
        )

    def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
        """Render the instanced geometry.

        Args:
            render_pass: Active render pass encoder
            **kwargs: Pipeline-specific render parameters
                - num_instances: Number of instances to render (defaults to all)
        """
        num_instances = kwargs.get("num_instances", None)

        if (
            self.position_buffer is None
            or self.colour_buffer is None
            or self.instance_id_buffer is None
            or self.geometry_buffer is None
        ):
            return

        count = num_instances if num_instances is not None else self.num_instances

        render_pass.set_pipeline(self.pipeline)
        if self.bind_group:
            render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)

        # Set instance buffers (must match base class layout)
        render_pass.set_vertex_buffer(0, self.position_buffer)
        render_pass.set_vertex_buffer(1, self.colour_buffer)  # Dummy colour buffer
        render_pass.set_vertex_buffer(2, self.instance_id_buffer)

        # Set single interleaved geometry buffer
        render_pass.set_vertex_buffer(
            3, self.geometry_buffer
        )  # locations(3,4,5) interleaved

        render_pass.draw(self.num_vertices, count)

    def cleanup(self) -> None:
        """Release resources."""
        if self.position_buffer:
            self.position_buffer.destroy()
        if self.colour_buffer:
            self.colour_buffer.destroy()
        if self.instance_id_buffer:
            self.instance_id_buffer.destroy()
        if self.geometry_buffer:
            self.geometry_buffer.destroy()

        super().cleanup()

cleanup()

Release resources.

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
609
610
611
612
613
614
615
616
617
618
619
620
def cleanup(self) -> None:
    """Release resources."""
    if self.position_buffer:
        self.position_buffer.destroy()
    if self.colour_buffer:
        self.colour_buffer.destroy()
    if self.instance_id_buffer:
        self.instance_id_buffer.destroy()
    if self.geometry_buffer:
        self.geometry_buffer.destroy()

    super().cleanup()

get_dtype()

Get the data type of the pipeline.

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
401
402
403
404
405
406
407
408
409
410
def get_dtype(self) -> np.dtype:
    """Get the data type of the pipeline."""
    return np.dtype(
        [
            ("MVP", "float32", (4, 4)),
            ("ViewMatrix", "float32", (4, 4)),
            ("colour", "float32", 4),  # Vec4 for alignment (RGB + padding)
            ("instance_transform", "float32", (4, 4)),
        ]
    )

render(render_pass, **kwargs)

Render the instanced geometry.

Parameters:
  • render_pass (GPURenderPassEncoder) –

    Active render pass encoder

  • **kwargs (Any, default: {} ) –

    Pipeline-specific render parameters - num_instances: Number of instances to render (defaults to all)

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
def render(self, render_pass: wgpu.GPURenderPassEncoder, **kwargs: Any) -> None:
    """Render the instanced geometry.

    Args:
        render_pass: Active render pass encoder
        **kwargs: Pipeline-specific render parameters
            - num_instances: Number of instances to render (defaults to all)
    """
    num_instances = kwargs.get("num_instances", None)

    if (
        self.position_buffer is None
        or self.colour_buffer is None
        or self.instance_id_buffer is None
        or self.geometry_buffer is None
    ):
        return

    count = num_instances if num_instances is not None else self.num_instances

    render_pass.set_pipeline(self.pipeline)
    if self.bind_group:
        render_pass.set_bind_group(0, self.bind_group, [], 0, 999999)

    # Set instance buffers (must match base class layout)
    render_pass.set_vertex_buffer(0, self.position_buffer)
    render_pass.set_vertex_buffer(1, self.colour_buffer)  # Dummy colour buffer
    render_pass.set_vertex_buffer(2, self.instance_id_buffer)

    # Set single interleaved geometry buffer
    render_pass.set_vertex_buffer(
        3, self.geometry_buffer
    )  # locations(3,4,5) interleaved

    render_pass.draw(self.num_vertices, count)

set_data(**kwargs)

Set the instanced geometry data for rendering.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific data parameters - positions: Nx3 array of instance positions or a pre-existing GPUBuffer. - colours: Nx3 array of instance colors (RGB) or a pre-existing GPUBuffer. If None, uses white. - geometry_data: Mx8 array of interleaved geometry data in format x,y,z,nx,ny,nz,u,v or pre-existing GPUBuffer. Must match the format output by PrimData methods.

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
def set_data(self, **kwargs: Any) -> None:
    """Set the instanced geometry data for rendering.

    Args:
        **kwargs: Pipeline-specific data parameters
            - positions: Nx3 array of instance positions or a pre-existing GPUBuffer.
            - colours: Nx3 array of instance colors (RGB) or a pre-existing GPUBuffer.
                       If None, uses white.
            - geometry_data: Mx8 array of interleaved geometry data in format
                            x,y,z,nx,ny,nz,u,v or pre-existing GPUBuffer.
                            Must match the format output by PrimData methods.
    """
    positions = kwargs.get("positions")
    colours = kwargs.get("colours")
    geometry_data = kwargs.get("geometry_data")

    self._set_position_data(positions)
    self._setup_instance_id_buffer()
    self._set_colour_data(colours)
    self._set_geometry_data(geometry_data)

update_uniforms(**kwargs)

Update uniform buffer values.

Parameters:
  • **kwargs (Any, default: {} ) –

    Pipeline-specific uniform parameters - mvp: 4x4 model view projection matrix - view_matrix: 4x4 view matrix - colour: 3-element array of RGB color values - instance_transform: 4x4 transformation matrix for each instance

Source code in ncca/ngl/webgpu/instanced_geometry_pipeline.py
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
def update_uniforms(self, **kwargs: Any) -> None:
    """Update uniform buffer values.

    Args:
        **kwargs: Pipeline-specific uniform parameters
            - mvp: 4x4 model view projection matrix
            - view_matrix: 4x4 view matrix
            - colour: 3-element array of RGB color values
            - instance_transform: 4x4 transformation matrix for each instance
    """
    if "mvp" in kwargs and kwargs["mvp"] is not None:
        self.uniform_data["MVP"] = kwargs["mvp"]

    if "view_matrix" in kwargs and kwargs["view_matrix"] is not None:
        self.uniform_data["ViewMatrix"] = kwargs["view_matrix"]

    if "colour" in kwargs and kwargs["colour"] is not None:
        self.uniform_data["colour"][:3] = kwargs["colour"]

    if "instance_transform" in kwargs and kwargs["instance_transform"] is not None:
        self.uniform_data["instance_transform"] = kwargs["instance_transform"]

    self.device.queue.write_buffer(
        self.uniform_buffer, 0, self.uniform_data.tobytes()
    )