Skip to main content

teksilo_render/
stream_buffer.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Persistent per-pipeline streaming buffers.
5//!
6//! Each frame, the renderer accumulates vertex data into per-pipeline
7//! batches and flushes them to the GPU. The historical code path called
8//! `device.create_buffer_init` on every flush, allocating and dropping
9//! a fresh `wgpu::Buffer` pair (vertex + index) per batch — a known
10//! antipattern that causes driver allocation pressure at high glyph
11//! counts.
12//!
13//! [`StreamBuffer`] owns a single growable GPU buffer that persists
14//! across frames. [`StreamBuffer::ensure_capacity`] is called once per
15//! frame with the worst-case byte count (requires `&mut self`), growing
16//! the underlying buffer if needed. After that, [`StreamBuffer::write`]
17//! copies a batch to the GPU at the current write offset and advances
18//! the cursor — all via interior mutability, so calls can interleave
19//! freely with other `&self` methods on the renderer.
20
21use std::cell::Cell;
22
23/// A growable, ring-reset GPU buffer for streaming vertex or index data
24/// across contiguous batches within a single frame.
25///
26/// Capacity growth requires `&mut self`; the per-batch `write` path only
27/// needs `&self` thanks to a `Cell<u64>` cursor, so it composes cleanly
28/// with methods that borrow other parts of the renderer immutably.
29pub struct StreamBuffer {
30    buffer: Option<wgpu::Buffer>,
31    capacity_bytes: u64,
32    write_offset: Cell<u64>,
33    usage: wgpu::BufferUsages,
34    label: &'static str,
35}
36
37impl StreamBuffer {
38    pub fn new(usage: wgpu::BufferUsages, label: &'static str) -> Self {
39        Self {
40            buffer: None,
41            capacity_bytes: 0,
42            write_offset: Cell::new(0),
43            usage,
44            label,
45        }
46    }
47
48    /// Reset the write cursor to the start of the buffer.
49    ///
50    /// Call once at the top of `render()` — previous frame's contents
51    /// are orphaned and overwritten in place.
52    pub fn reset(&self) {
53        self.write_offset.set(0);
54    }
55
56    /// Grow the underlying buffer if `required_bytes` exceeds the current
57    /// capacity. Growth rounds up to the next power of two (minimum 1 KiB)
58    /// to amortize reallocation. If the existing buffer already fits, this
59    /// is a no-op.
60    pub fn ensure_capacity(&mut self, device: &wgpu::Device, required_bytes: u64) {
61        if required_bytes <= self.capacity_bytes && self.buffer.is_some() {
62            return;
63        }
64        let new_cap = required_bytes.max(1024).next_power_of_two();
65        self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
66            label: Some(self.label),
67            size: new_cap,
68            usage: self.usage | wgpu::BufferUsages::COPY_DST,
69            mapped_at_creation: false,
70        }));
71        self.capacity_bytes = new_cap;
72    }
73
74    /// Upload `data` at the current write cursor and advance the cursor.
75    ///
76    /// Returns `Some((buffer, offset, len_bytes))` on success, or `None`
77    /// if [`ensure_capacity`](Self::ensure_capacity) was never called OR
78    /// the write would overflow the buffer's capacity. The caller should
79    /// slice the returned buffer at `offset..offset + len` when binding.
80    ///
81    /// An overflow means the frame-start sizing undercounted this
82    /// pipeline's quads (see `stream_quad_counts` in `renderer.rs`).
83    /// Debug builds assert with the accounting details; release builds
84    /// skip the batch — a dropped draw is recoverable, whereas
85    /// forwarding an out-of-bounds `write_buffer` to wgpu is a
86    /// validation error that kills the device.
87    pub fn write(&self, queue: &wgpu::Queue, data: &[u8]) -> Option<(&wgpu::Buffer, u64, u64)> {
88        let buf = self.buffer.as_ref()?;
89        let offset = self.write_offset.get();
90        let len = data.len() as u64;
91        if offset + len > self.capacity_bytes {
92            debug_assert!(
93                false,
94                "StreamBuffer overflow: {} + {} > {} ({}) — frame-start quad count \
95                 undercounted this pipeline; the batch is dropped",
96                offset, len, self.capacity_bytes, self.label
97            );
98            return None;
99        }
100        queue.write_buffer(buf, offset, data);
101        self.write_offset.set(offset + len);
102        Some((buf, offset, len))
103    }
104}
105
106/// All per-pipeline streaming buffers owned by a `Renderer`.
107///
108/// Grouped so the renderer can `reset()` them all at once and so macros
109/// can pass a single handle around the render loop.
110pub struct StreamBuffers {
111    pub rect: StreamBuffer,
112    pub sdf: StreamBuffer,
113    pub quad: StreamBuffer,
114    pub shadow: StreamBuffer,
115    /// Shader-driven animated-quad vertices (procedural pipeline —
116    /// IndeterminateSweep, future Pulse / Shimmer). Per-frame uniform
117    /// state lives in a separate `wgpu::Buffer` owned by `Renderer`.
118    pub anim_proc: StreamBuffer,
119    /// Gradient-filled path vertices (Tier 3, `path_gradient_pipeline`).
120    /// Solid-filled paths still stream through `quad` above; only
121    /// `PathEntry`s whose `paint_data` is a gradient variant land here.
122    pub path_gradient: StreamBuffer,
123    /// Shared index buffer — quad indices are deterministic so one buffer
124    /// serves every pipeline that renders quads.
125    pub index: StreamBuffer,
126}
127
128impl StreamBuffers {
129    pub fn new() -> Self {
130        Self {
131            rect: StreamBuffer::new(wgpu::BufferUsages::VERTEX, "rect_stream"),
132            sdf: StreamBuffer::new(wgpu::BufferUsages::VERTEX, "sdf_stream"),
133            quad: StreamBuffer::new(wgpu::BufferUsages::VERTEX, "quad_stream"),
134            shadow: StreamBuffer::new(wgpu::BufferUsages::VERTEX, "shadow_stream"),
135            anim_proc: StreamBuffer::new(wgpu::BufferUsages::VERTEX, "anim_proc_stream"),
136            path_gradient: StreamBuffer::new(wgpu::BufferUsages::VERTEX, "path_gradient_stream"),
137            index: StreamBuffer::new(wgpu::BufferUsages::INDEX, "index_stream"),
138        }
139    }
140
141    pub fn reset(&self) {
142        self.rect.reset();
143        self.sdf.reset();
144        self.quad.reset();
145        self.shadow.reset();
146        self.anim_proc.reset();
147        self.path_gradient.reset();
148        self.index.reset();
149    }
150}
151
152impl Default for StreamBuffers {
153    fn default() -> Self {
154        Self::new()
155    }
156}