Skip to main content

densa_render/
gpu.rs

1use crate::atlas::{AtlasError, GpuAtlas};
2use bytemuck::{Pod, Zeroable};
3use densa_core::Color;
4use densa_draw::Instance;
5use densa_text::AtlasUpdate;
6use std::mem::size_of;
7
8#[repr(C)]
9#[derive(Debug, Clone, Copy, Pod, Zeroable)]
10struct Globals {
11    screen: [f32; 2],
12    _pad: [f32; 2],
13}
14
15/// GPU resources for drawing a frame of [`Instance`]s in one draw call.
16///
17/// Everything is created up front; per frame only [`GpuRenderer::apply_atlas_updates`] (work
18/// only when new glyphs appeared), [`GpuRenderer::prepare`] (two buffer writes) and
19/// [`GpuRenderer::render`] (one pass, one draw) run.
20#[derive(Debug)]
21pub struct GpuRenderer {
22    pipeline: wgpu::RenderPipeline,
23    bind_group_layout: wgpu::BindGroupLayout,
24    bind_group: wgpu::BindGroup,
25    sampler: wgpu::Sampler,
26    globals: wgpu::Buffer,
27    instances: wgpu::Buffer,
28    atlas: GpuAtlas,
29    /// `atlas.version()` that `bind_group` refers to.
30    bound_atlas: u64,
31    /// The first atlas failure; once set the atlas no longer mirrors the cache.
32    failed: Option<AtlasError>,
33    capacity: usize,
34}
35
36impl GpuRenderer {
37    /// A renderer drawing up to `capacity` instances. Its glyph atlas starts empty and follows
38    /// the glyph cache through [`GpuRenderer::apply_atlas_updates`].
39    ///
40    /// `format` must match the color target `render` will draw into; use an sRGB format
41    /// (or an sRGB view of the surface) so linear shader colors display correctly.
42    pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat, capacity: usize) -> Self {
43        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
44            label: Some("densa atlas sampler"),
45            // Nearest: glyphs are drawn texel-for-pixel at whole-pixel positions.
46            mag_filter: wgpu::FilterMode::Nearest,
47            min_filter: wgpu::FilterMode::Nearest,
48            ..Default::default()
49        });
50        let globals = device.create_buffer(&wgpu::BufferDescriptor {
51            label: Some("densa globals"),
52            size: size_of::<Globals>() as u64,
53            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
54            mapped_at_creation: false,
55        });
56        let instances = device.create_buffer(&wgpu::BufferDescriptor {
57            label: Some("densa instances"),
58            size: (capacity.max(1) * size_of::<Instance>()) as u64,
59            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
60            mapped_at_creation: false,
61        });
62        let atlas = GpuAtlas::new(device);
63        let bind_group_layout = bind_group_layout(device);
64        let bind_group = bind_group(device, &bind_group_layout, &globals, &atlas, &sampler);
65        let pipeline = pipeline(device, &bind_group_layout, format);
66
67        Self {
68            pipeline,
69            bind_group_layout,
70            bind_group,
71            sampler,
72            globals,
73            instances,
74            bound_atlas: atlas.version(),
75            failed: None,
76            atlas,
77            capacity,
78        }
79    }
80
81    /// Instances the GPU buffer holds; more are clipped by [`GpuRenderer::prepare`].
82    pub fn capacity(&self) -> usize {
83        self.capacity
84    }
85
86    /// The largest atlas side `device` supports; pass it to
87    /// `GlyphCache::limit_side` so the cache never outgrows the device.
88    pub fn max_atlas_side(device: &wgpu::Device) -> u32 {
89        device.limits().max_texture_dimension_2d
90    }
91
92    /// The atlas failure latched by [`apply_atlas_updates`](Self::apply_atlas_updates), if
93    /// any: then the renderer draws nothing until it is recreated.
94    pub fn atlas_error(&self) -> Option<AtlasError> {
95        self.failed
96    }
97
98    /// Current glyph atlas size in texels, `(width, height)`.
99    pub fn atlas_size(&self) -> (u32, u32) {
100        self.atlas.size()
101    }
102
103    /// Applies the glyph cache's atlas changes in order (`GlyphCache::drain_updates`):
104    /// grows the texture, keeping its texels, and copies new glyphs in. Returns how many
105    /// glyph writes were applied.
106    ///
107    /// # Errors
108    /// [`AtlasError`] if the texture can no longer mirror the cache (too large for the device,
109    /// or a malformed update). The error is latched: later updates are not applied, every
110    /// later call returns it, and [`prepare`](Self::prepare) draws nothing, since glyphs
111    /// would sample the wrong texels. Recreate the renderer and the glyph cache to recover.
112    pub fn apply_atlas_updates(
113        &mut self,
114        device: &wgpu::Device,
115        queue: &wgpu::Queue,
116        updates: impl IntoIterator<Item = AtlasUpdate>,
117    ) -> Result<usize, AtlasError> {
118        if let Some(error) = self.failed {
119            return Err(error);
120        }
121        let result = self.atlas.apply(device, queue, updates);
122        self.failed = result.err();
123        // Rebuilt even on error: a resize before the failing update replaced the texture.
124        if self.atlas.version() != self.bound_atlas {
125            self.bind_group = bind_group(
126                device,
127                &self.bind_group_layout,
128                &self.globals,
129                &self.atlas,
130                &self.sampler,
131            );
132            self.bound_atlas = self.atlas.version();
133        }
134        result
135    }
136
137    /// Uploads the frame's instances for a `screen` of (width, height) physical pixels.
138    /// Returns how many will be drawn: clipped to capacity, and 0 after an atlas failure.
139    pub fn prepare(&self, queue: &wgpu::Queue, screen: (u32, u32), instances: &[Instance]) -> u32 {
140        if self.failed.is_some() {
141            return 0;
142        }
143        let globals = Globals {
144            screen: [screen.0.max(1) as f32, screen.1.max(1) as f32],
145            _pad: [0.0; 2],
146        };
147        queue.write_buffer(&self.globals, 0, bytemuck::bytes_of(&globals));
148        let drawn = &instances[..instances.len().min(self.capacity)];
149        if !drawn.is_empty() {
150            queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(drawn));
151        }
152        // Lossless: capacity is bounded by the GPU buffer size, far below u32::MAX.
153        drawn.len() as u32
154    }
155
156    /// Clears `target` to `clear` and draws the first `count` prepared instances.
157    pub fn render(
158        &self,
159        encoder: &mut wgpu::CommandEncoder,
160        target: &wgpu::TextureView,
161        clear: Color,
162        count: u32,
163    ) {
164        let clear = wgpu::Color {
165            r: f64::from(clear.r),
166            g: f64::from(clear.g),
167            b: f64::from(clear.b),
168            a: f64::from(clear.a),
169        };
170        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
171            label: Some("densa instances"),
172            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
173                view: target,
174                depth_slice: None,
175                resolve_target: None,
176                ops: wgpu::Operations {
177                    load: wgpu::LoadOp::Clear(clear),
178                    store: wgpu::StoreOp::Store,
179                },
180            })],
181            depth_stencil_attachment: None,
182            timestamp_writes: None,
183            occlusion_query_set: None,
184            multiview_mask: None,
185        });
186        if count == 0 {
187            return;
188        }
189        pass.set_pipeline(&self.pipeline);
190        pass.set_bind_group(0, &self.bind_group, &[]);
191        pass.set_vertex_buffer(0, self.instances.slice(..));
192        pass.draw(0..4, 0..count);
193    }
194}
195
196fn bind_group(
197    device: &wgpu::Device,
198    layout: &wgpu::BindGroupLayout,
199    globals: &wgpu::Buffer,
200    atlas: &GpuAtlas,
201    sampler: &wgpu::Sampler,
202) -> wgpu::BindGroup {
203    let view = atlas.view();
204    device.create_bind_group(&wgpu::BindGroupDescriptor {
205        label: Some("densa bind group"),
206        layout,
207        entries: &[
208            wgpu::BindGroupEntry {
209                binding: 0,
210                resource: globals.as_entire_binding(),
211            },
212            wgpu::BindGroupEntry {
213                binding: 1,
214                resource: wgpu::BindingResource::TextureView(&view),
215            },
216            wgpu::BindGroupEntry {
217                binding: 2,
218                resource: wgpu::BindingResource::Sampler(sampler),
219            },
220        ],
221    })
222}
223
224fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
225    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
226        label: Some("densa bind group layout"),
227        entries: &[
228            wgpu::BindGroupLayoutEntry {
229                binding: 0,
230                visibility: wgpu::ShaderStages::VERTEX,
231                ty: wgpu::BindingType::Buffer {
232                    ty: wgpu::BufferBindingType::Uniform,
233                    has_dynamic_offset: false,
234                    min_binding_size: None,
235                },
236                count: None,
237            },
238            wgpu::BindGroupLayoutEntry {
239                binding: 1,
240                // Vertex too: the vertex shader normalizes texel UVs by the atlas size.
241                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
242                ty: wgpu::BindingType::Texture {
243                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
244                    view_dimension: wgpu::TextureViewDimension::D2,
245                    multisampled: false,
246                },
247                count: None,
248            },
249            wgpu::BindGroupLayoutEntry {
250                binding: 2,
251                visibility: wgpu::ShaderStages::FRAGMENT,
252                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
253                count: None,
254            },
255        ],
256    })
257}
258
259fn pipeline(
260    device: &wgpu::Device,
261    layout: &wgpu::BindGroupLayout,
262    format: wgpu::TextureFormat,
263) -> wgpu::RenderPipeline {
264    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
265        label: Some("densa shader"),
266        source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
267    });
268    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
269        label: Some("densa pipeline layout"),
270        bind_group_layouts: &[Some(layout)],
271        immediate_size: 0,
272    });
273    const ATTRIBUTES: [wgpu::VertexAttribute; 3] =
274        wgpu::vertex_attr_array![0 => Float32x4, 1 => Float32x4, 2 => Float32x4];
275    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
276        label: Some("densa pipeline"),
277        layout: Some(&pipeline_layout),
278        vertex: wgpu::VertexState {
279            module: &shader,
280            entry_point: Some("vs_main"),
281            compilation_options: Default::default(),
282            buffers: &[Some(wgpu::VertexBufferLayout {
283                array_stride: size_of::<Instance>() as u64,
284                step_mode: wgpu::VertexStepMode::Instance,
285                attributes: &ATTRIBUTES,
286            })],
287        },
288        primitive: wgpu::PrimitiveState {
289            topology: wgpu::PrimitiveTopology::TriangleStrip,
290            ..Default::default()
291        },
292        depth_stencil: None,
293        multisample: wgpu::MultisampleState::default(),
294        fragment: Some(wgpu::FragmentState {
295            module: &shader,
296            entry_point: Some("fs_main"),
297            compilation_options: Default::default(),
298            targets: &[Some(wgpu::ColorTargetState {
299                format,
300                blend: Some(wgpu::BlendState::ALPHA_BLENDING),
301                write_mask: wgpu::ColorWrites::ALL,
302            })],
303        }),
304        multiview_mask: None,
305        cache: None,
306    })
307}