Skip to main content

dotloom_render/
gpu.rs

1//! GPU side: pipelines, buffers and the frame loop.
2
3use bytemuck::{Pod, Zeroable};
4use dotloom_scene::{Overlay, SceneDelta, flags};
5use serde::Serialize;
6
7use crate::RenderError;
8use crate::cache::{FrameParams, PrepareStats, SceneCache};
9use crate::color::{Theme, unpack};
10use crate::overlay::{Grid, grid_mesh, screen_overlay, world_overlay};
11use crate::tess::{FillVertex, GlyphInstance, LineInstance, Look, Mesh, tessellate_item};
12use crate::text::{ATLAS_SIZE, TextSystem};
13use crate::view::{View, lod_tolerance};
14
15/// Uniform slot stride (WebGPU/WebGL2 minimum dynamic offset alignment).
16const UNIFORM_STRIDE: u64 = 256;
17
18#[repr(C)]
19#[derive(Debug, Clone, Copy, Pod, Zeroable)]
20struct Xf {
21    m: [f32; 4],
22    tv: [f32; 4],
23    p: [f32; 4],
24}
25
26/// A growable GPU buffer.
27#[derive(Debug, Default)]
28pub(crate) struct GpuVec {
29    buf: Option<wgpu::Buffer>,
30    cap: u64,
31    len: u64,
32}
33
34impl GpuVec {
35    fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[u8], usage: wgpu::BufferUsages) {
36        self.len = data.len() as u64;
37        if data.is_empty() {
38            return;
39        }
40        if self.buf.is_none() || self.cap < self.len {
41            if let Some(b) = self.buf.take() {
42                b.destroy();
43            }
44            let cap = self.len.next_power_of_two().max(256);
45            self.buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
46                label: Some("dotloom chunk"),
47                size: cap,
48                usage: usage | wgpu::BufferUsages::COPY_DST,
49                mapped_at_creation: false,
50            }));
51            self.cap = cap;
52        }
53        if let Some(b) = &self.buf {
54            queue.write_buffer(b, 0, data);
55        }
56    }
57
58    fn slice(&self) -> Option<wgpu::BufferSlice<'_>> {
59        if self.len == 0 { None } else { self.buf.as_ref().map(|b| b.slice(..self.len)) }
60    }
61
62    /// Elements `range` of a buffer of `stride`-byte elements. Instanced draws bind
63    /// a sub-slice instead of using a non-zero first instance, which WebGL2 lacks.
64    fn elements(&self, range: &core::ops::Range<u32>, stride: u64) -> Option<wgpu::BufferSlice<'_>> {
65        let (start, end) = (u64::from(range.start) * stride, u64::from(range.end) * stride);
66        if start >= end || end > self.len {
67            return None;
68        }
69        self.buf.as_ref().map(|b| b.slice(start..end))
70    }
71
72    fn destroy(self) {
73        if let Some(b) = self.buf {
74            b.destroy();
75        }
76    }
77
78    fn bytes(&self) -> u64 {
79        self.cap
80    }
81}
82
83/// GPU buffers of one chunk (or transient mesh).
84#[derive(Debug, Default)]
85pub struct ChunkBuffers {
86    lines: GpuVec,
87    fill_vertices: GpuVec,
88    fill_indices: GpuVec,
89    glyphs: GpuVec,
90}
91
92impl ChunkBuffers {
93    pub(crate) fn is_empty(&self) -> bool {
94        self.lines.buf.is_none()
95            && self.fill_vertices.buf.is_none()
96            && self.fill_indices.buf.is_none()
97            && self.glyphs.buf.is_none()
98    }
99
100    pub(crate) fn destroy(self) {
101        self.lines.destroy();
102        self.fill_vertices.destroy();
103        self.fill_indices.destroy();
104        self.glyphs.destroy();
105    }
106
107    fn upload(
108        &mut self,
109        device: &wgpu::Device,
110        queue: &wgpu::Queue,
111        lines: &[LineInstance],
112        fv: &[FillVertex],
113        fi: &[u32],
114        glyphs: &[GlyphInstance],
115    ) {
116        self.lines.upload(device, queue, bytemuck::cast_slice(lines), wgpu::BufferUsages::VERTEX);
117        self.fill_vertices.upload(device, queue, bytemuck::cast_slice(fv), wgpu::BufferUsages::VERTEX);
118        self.fill_indices.upload(device, queue, bytemuck::cast_slice(fi), wgpu::BufferUsages::INDEX);
119        self.glyphs.upload(device, queue, bytemuck::cast_slice(glyphs), wgpu::BufferUsages::VERTEX);
120    }
121
122    fn bytes(&self) -> u64 {
123        self.lines.bytes() + self.fill_vertices.bytes() + self.fill_indices.bytes() + self.glyphs.bytes()
124    }
125}
126
127/// Renderer options.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct RendererOptions {
130    /// MSAA sample count (1 or 4); 4 anti-aliases fills.
131    pub sample_count: u32,
132}
133
134impl Default for RendererOptions {
135    fn default() -> Self {
136        Self { sample_count: 4 }
137    }
138}
139
140/// Statistics of the last frame.
141#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
142#[serde(rename_all = "camelCase")]
143pub struct FrameStats {
144    /// Scene items.
145    pub items: u32,
146    /// Chunks in the scene.
147    pub chunks: u32,
148    /// Chunks drawn (after culling).
149    pub chunks_drawn: u32,
150    /// Chunks whose geometry was rebuilt this frame.
151    pub chunks_rebuilt: u32,
152    /// Items tessellated this frame.
153    pub items_tessellated: u32,
154    /// Draw calls.
155    pub draw_calls: u32,
156    /// Line segments drawn.
157    pub line_segments: u32,
158    /// Fill triangles drawn.
159    pub triangles: u32,
160    /// Glyph quads drawn.
161    pub glyphs: u32,
162    /// Bytes allocated in GPU buffers for chunks.
163    pub gpu_bytes: u64,
164}
165
166/// One draw batch of the frame.
167struct Batch {
168    slot: u64,
169    chunk: Option<usize>,
170    transient: Option<usize>,
171}
172
173/// The wgpu renderer. Consumes scene deltas; owns only derived GPU/CPU caches.
174pub struct Renderer {
175    device: wgpu::Device,
176    queue: wgpu::Queue,
177    format: wgpu::TextureFormat,
178    sample_count: u32,
179    line_pipeline: wgpu::RenderPipeline,
180    fill_pipeline: wgpu::RenderPipeline,
181    glyph_pipeline: wgpu::RenderPipeline,
182    uniform_layout: wgpu::BindGroupLayout,
183    uniform_buf: wgpu::Buffer,
184    uniform_slots: u64,
185    uniform_bind: wgpu::BindGroup,
186    atlas: wgpu::Texture,
187    atlas_bind: wgpu::BindGroup,
188    msaa: Option<(wgpu::Texture, wgpu::TextureView)>,
189    /// Grid, hover, world overlay, screen overlay.
190    transient: [ChunkBuffers; 4],
191    transient_mesh: [Mesh; 4],
192    scene: SceneCache,
193    text: TextSystem,
194    view: View,
195    theme: Theme,
196    grid: Grid,
197    overlay: Overlay,
198    hover: Option<u64>,
199    last: FrameStats,
200    disposed: bool,
201}
202
203impl std::fmt::Debug for Renderer {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        f.debug_struct("Renderer")
206            .field("format", &self.format)
207            .field("sample_count", &self.sample_count)
208            .field("scene", &self.scene)
209            .finish_non_exhaustive()
210    }
211}
212
213fn uniform_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
214    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
215        label: Some("dotloom xf"),
216        entries: &[wgpu::BindGroupLayoutEntry {
217            binding: 0,
218            visibility: wgpu::ShaderStages::VERTEX,
219            ty: wgpu::BindingType::Buffer {
220                ty: wgpu::BufferBindingType::Uniform,
221                has_dynamic_offset: true,
222                min_binding_size: wgpu::BufferSize::new(core::mem::size_of::<Xf>() as u64),
223            },
224            count: None,
225        }],
226    })
227}
228
229fn uniform_bind(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, buf: &wgpu::Buffer) -> wgpu::BindGroup {
230    device.create_bind_group(&wgpu::BindGroupDescriptor {
231        label: Some("dotloom xf"),
232        layout,
233        entries: &[wgpu::BindGroupEntry {
234            binding: 0,
235            resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
236                buffer: buf,
237                offset: 0,
238                size: wgpu::BufferSize::new(core::mem::size_of::<Xf>() as u64),
239            }),
240        }],
241    })
242}
243
244fn uniform_buffer(device: &wgpu::Device, slots: u64) -> wgpu::Buffer {
245    device.create_buffer(&wgpu::BufferDescriptor {
246        label: Some("dotloom xf"),
247        size: slots * UNIFORM_STRIDE,
248        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
249        mapped_at_creation: false,
250    })
251}
252
253const LINE_ATTRS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
254    0 => Float32x2, 1 => Float32x2, 2 => Unorm8x4, 3 => Float32, 4 => Float32, 5 => Float32x4
255];
256const FILL_ATTRS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Unorm8x4];
257const GLYPH_ATTRS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
258    0 => Float32x2, 1 => Float32x2, 2 => Float32x2, 3 => Float32x4, 4 => Unorm8x4, 5 => Float32
259];
260
261impl Renderer {
262    /// Create a renderer drawing into targets of `format`.
263    ///
264    /// The device should be created with limits no larger than
265    /// [`wgpu::Limits::downlevel_webgl2_defaults`] needs; the renderer uses only
266    /// WebGL2-compatible features on every backend.
267    ///
268    /// # Errors
269    /// [`RenderError::Font`] if the bundled font fails to load (never expected).
270    pub fn new(
271        device: wgpu::Device,
272        queue: wgpu::Queue,
273        format: wgpu::TextureFormat,
274        options: RendererOptions,
275    ) -> Result<Self, RenderError> {
276        let sample_count = if options.sample_count >= 4 { 4 } else { 1 };
277        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
278            label: Some("dotloom shaders"),
279            source: wgpu::ShaderSource::Wgsl(include_str!("shaders.wgsl").into()),
280        });
281        let uniform_layout = uniform_layout(&device);
282        let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
283            label: Some("dotloom atlas"),
284            entries: &[
285                wgpu::BindGroupLayoutEntry {
286                    binding: 0,
287                    visibility: wgpu::ShaderStages::FRAGMENT,
288                    ty: wgpu::BindingType::Texture {
289                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
290                        view_dimension: wgpu::TextureViewDimension::D2,
291                        multisampled: false,
292                    },
293                    count: None,
294                },
295                wgpu::BindGroupLayoutEntry {
296                    binding: 1,
297                    visibility: wgpu::ShaderStages::FRAGMENT,
298                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
299                    count: None,
300                },
301            ],
302        });
303        let plain = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
304            label: Some("dotloom plain"),
305            bind_group_layouts: &[Some(&uniform_layout)],
306            immediate_size: 0,
307        });
308        let textured = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
309            label: Some("dotloom textured"),
310            bind_group_layouts: &[Some(&uniform_layout), Some(&atlas_layout)],
311            immediate_size: 0,
312        });
313        let target = [Some(wgpu::ColorTargetState {
314            format,
315            blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
316            write_mask: wgpu::ColorWrites::ALL,
317        })];
318        let make = |label: &str,
319                    layout: &wgpu::PipelineLayout,
320                    vs: &str,
321                    fs: &str,
322                    buffer: wgpu::VertexBufferLayout<'_>| {
323            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
324                label: Some(label),
325                layout: Some(layout),
326                vertex: wgpu::VertexState {
327                    module: &shader,
328                    entry_point: Some(vs),
329                    compilation_options: wgpu::PipelineCompilationOptions::default(),
330                    buffers: &[Some(buffer)],
331                },
332                primitive: wgpu::PrimitiveState {
333                    topology: wgpu::PrimitiveTopology::TriangleList,
334                    cull_mode: None,
335                    ..wgpu::PrimitiveState::default()
336                },
337                depth_stencil: None,
338                multisample: wgpu::MultisampleState { count: sample_count, mask: !0, alpha_to_coverage_enabled: false },
339                fragment: Some(wgpu::FragmentState {
340                    module: &shader,
341                    entry_point: Some(fs),
342                    compilation_options: wgpu::PipelineCompilationOptions::default(),
343                    targets: &target,
344                }),
345                multiview_mask: None,
346                cache: None,
347            })
348        };
349        let line_pipeline = make(
350            "dotloom lines",
351            &plain,
352            "vs_line",
353            "fs_line",
354            wgpu::VertexBufferLayout {
355                array_stride: core::mem::size_of::<LineInstance>() as u64,
356                step_mode: wgpu::VertexStepMode::Instance,
357                attributes: &LINE_ATTRS,
358            },
359        );
360        let fill_pipeline = make(
361            "dotloom fills",
362            &plain,
363            "vs_fill",
364            "fs_fill",
365            wgpu::VertexBufferLayout {
366                array_stride: core::mem::size_of::<FillVertex>() as u64,
367                step_mode: wgpu::VertexStepMode::Vertex,
368                attributes: &FILL_ATTRS,
369            },
370        );
371        let glyph_pipeline = make(
372            "dotloom glyphs",
373            &textured,
374            "vs_glyph",
375            "fs_glyph",
376            wgpu::VertexBufferLayout {
377                array_stride: core::mem::size_of::<GlyphInstance>() as u64,
378                step_mode: wgpu::VertexStepMode::Instance,
379                attributes: &GLYPH_ATTRS,
380            },
381        );
382        let atlas = device.create_texture(&wgpu::TextureDescriptor {
383            label: Some("dotloom glyph atlas"),
384            size: wgpu::Extent3d { width: ATLAS_SIZE as u32, height: ATLAS_SIZE as u32, depth_or_array_layers: 1 },
385            mip_level_count: 1,
386            sample_count: 1,
387            dimension: wgpu::TextureDimension::D2,
388            format: wgpu::TextureFormat::R8Unorm,
389            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
390            view_formats: &[],
391        });
392        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
393            label: Some("dotloom atlas"),
394            mag_filter: wgpu::FilterMode::Linear,
395            min_filter: wgpu::FilterMode::Linear,
396            ..wgpu::SamplerDescriptor::default()
397        });
398        let atlas_view = atlas.create_view(&wgpu::TextureViewDescriptor::default());
399        let atlas_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
400            label: Some("dotloom atlas"),
401            layout: &atlas_layout,
402            entries: &[
403                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&atlas_view) },
404                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
405            ],
406        });
407        let uniform_slots = 64;
408        let uniform_buf = uniform_buffer(&device, uniform_slots);
409        let uniform_bind = uniform_bind(&device, &uniform_layout, &uniform_buf);
410        Ok(Self {
411            device,
412            queue,
413            format,
414            sample_count,
415            line_pipeline,
416            fill_pipeline,
417            glyph_pipeline,
418            uniform_layout,
419            uniform_buf,
420            uniform_slots,
421            uniform_bind,
422            atlas,
423            atlas_bind,
424            msaa: None,
425            transient: Default::default(),
426            transient_mesh: Default::default(),
427            scene: SceneCache::default(),
428            text: TextSystem::with_default_font()?,
429            view: View::default(),
430            theme: Theme::default(),
431            grid: Grid::default(),
432            overlay: Overlay::default(),
433            hover: None,
434            last: FrameStats::default(),
435            disposed: false,
436        })
437    }
438
439    /// The device.
440    #[must_use]
441    pub fn device(&self) -> &wgpu::Device {
442        &self.device
443    }
444
445    /// The queue.
446    #[must_use]
447    pub fn queue(&self) -> &wgpu::Queue {
448        &self.queue
449    }
450
451    /// Target format.
452    #[must_use]
453    pub fn format(&self) -> wgpu::TextureFormat {
454        self.format
455    }
456
457    /// MSAA sample count in use.
458    #[must_use]
459    pub fn sample_count(&self) -> u32 {
460        self.sample_count
461    }
462
463    /// Apply a binary scene delta (`dotloom-scene` format).
464    ///
465    /// # Errors
466    /// [`RenderError::Scene`] for malformed input; the scene is left unchanged.
467    pub fn apply_delta_bytes(&mut self, bytes: &[u8]) -> Result<(), RenderError> {
468        let d = SceneDelta::decode(bytes).map_err(|e| RenderError::Scene(e.to_string()))?;
469        self.apply_delta(d);
470        Ok(())
471    }
472
473    /// Apply a decoded scene delta.
474    pub fn apply_delta(&mut self, delta: SceneDelta) {
475        self.scene.apply(delta);
476    }
477
478    /// The scene cache.
479    #[must_use]
480    pub fn scene(&self) -> &SceneCache {
481        &self.scene
482    }
483
484    /// Set the camera.
485    ///
486    /// # Errors
487    /// [`RenderError::InvalidView`] for non-finite or out-of-range values.
488    pub fn set_view(&mut self, view: View) -> Result<(), RenderError> {
489        view.validate()?;
490        self.view = view;
491        Ok(())
492    }
493
494    /// Current camera.
495    #[must_use]
496    pub fn view(&self) -> View {
497        self.view
498    }
499
500    /// Set colors (rebuilds cached geometry).
501    pub fn set_theme(&mut self, theme: Theme) {
502        if theme != self.theme {
503            self.theme = theme;
504            self.scene.invalidate_all();
505        }
506    }
507
508    /// Current theme.
509    #[must_use]
510    pub fn theme(&self) -> &Theme {
511        &self.theme
512    }
513
514    /// Set grid settings.
515    pub fn set_grid(&mut self, grid: Grid) {
516        self.grid = grid;
517    }
518
519    /// Set the interaction overlay.
520    pub fn set_overlay(&mut self, overlay: Overlay) {
521        self.overlay = overlay;
522    }
523
524    /// Highlight one item as hovered (drawn on top as feedback).
525    pub fn set_hover(&mut self, id: Option<u64>) {
526        self.hover = id;
527    }
528
529    /// Statistics of the last frame.
530    #[must_use]
531    pub fn last_stats(&self) -> FrameStats {
532        self.last
533    }
534
535    fn xf_world(&self, origin: [f64; 2], size: (u32, u32), alpha: f32) -> Xf {
536        let s = self.view.device_scale();
537        let (w, h) = (f64::from(size.0), f64::from(size.1));
538        let e = (origin[0] - self.view.center[0]) * s + w * 0.5;
539        let f = h * 0.5 - (origin[1] - self.view.center[1]) * s;
540        Xf {
541            m: [s as f32, 0.0, 0.0, -s as f32],
542            tv: [e as f32, f as f32, size.0 as f32, size.1 as f32],
543            p: [self.view.dpr as f32, s as f32, alpha, 0.0],
544        }
545    }
546
547    fn xf_screen(&self, size: (u32, u32)) -> Xf {
548        Xf {
549            m: [1.0, 0.0, 0.0, 1.0],
550            tv: [0.0, 0.0, size.0 as f32, size.1 as f32],
551            p: [self.view.dpr as f32, 1.0, 1.0, 0.0],
552        }
553    }
554
555    fn upload_atlas(&mut self) {
556        let Some(d) = self.text.dirty.take() else { return };
557        self.queue.write_texture(
558            wgpu::TexelCopyTextureInfo {
559                texture: &self.atlas,
560                mip_level: 0,
561                origin: wgpu::Origin3d { x: d.x as u32, y: d.y as u32, z: 0 },
562                aspect: wgpu::TextureAspect::All,
563            },
564            &self.text.atlas,
565            wgpu::TexelCopyBufferLayout {
566                offset: (d.y * ATLAS_SIZE + d.x) as u64,
567                bytes_per_row: Some(ATLAS_SIZE as u32),
568                rows_per_image: Some(d.h as u32),
569            },
570            wgpu::Extent3d { width: d.w as u32, height: d.h as u32, depth_or_array_layers: 1 },
571        );
572    }
573
574    fn ensure_msaa(&mut self, size: (u32, u32)) {
575        if self.sample_count == 1 {
576            return;
577        }
578        let ok = self.msaa.as_ref().is_some_and(|(t, _)| t.width() == size.0 && t.height() == size.1);
579        if ok {
580            return;
581        }
582        if let Some((t, _)) = self.msaa.take() {
583            t.destroy();
584        }
585        let t = self.device.create_texture(&wgpu::TextureDescriptor {
586            label: Some("dotloom msaa"),
587            size: wgpu::Extent3d { width: size.0, height: size.1, depth_or_array_layers: 1 },
588            mip_level_count: 1,
589            sample_count: self.sample_count,
590            dimension: wgpu::TextureDimension::D2,
591            format: self.format,
592            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
593            view_formats: &[],
594        });
595        let v = t.create_view(&wgpu::TextureViewDescriptor::default());
596        self.msaa = Some((t, v));
597    }
598
599    /// Draw one frame into `target` (its size defines the viewport in device pixels).
600    ///
601    /// # Errors
602    /// [`RenderError::Disposed`] after [`Renderer::dispose`].
603    pub fn render(&mut self, target: &wgpu::Texture) -> Result<FrameStats, RenderError> {
604        if self.disposed {
605            return Err(RenderError::Disposed);
606        }
607        let size = (target.width().max(1), target.height().max(1));
608        let target_view = target.create_view(&wgpu::TextureViewDescriptor::default());
609        let lod = self.view.lod();
610        let tol = lod_tolerance(lod);
611        let mut ps = PrepareStats::default();
612        // Cull with the target size actually drawn.
613        let s = self.view.device_scale();
614        let half = (f64::from(size.0) * 0.5 / s, f64::from(size.1) * 0.5 / s);
615        let visible = dotloom_geometry::Aabb::from_corners(
616            dotloom_geometry::Point::new(self.view.center[0] - half.0, self.view.center[1] - half.1),
617            dotloom_geometry::Point::new(self.view.center[0] + half.0, self.view.center[1] + half.1),
618        );
619        // Strokes and markers extend a few pixels beyond item boxes.
620        let margin = 16.0 / self.view.scale;
621        let fp = FrameParams { visible, margin, lod, lod_tol: tol, theme: &self.theme };
622        let vis = self.scene.prepare(&fp, &mut self.text, &mut ps);
623
624        // Transient meshes: grid, hover, world overlay, screen overlay.
625        let hover_mesh = self
626            .hover
627            .and_then(|id| self.scene.item(id).cloned())
628            .and_then(|it| {
629                tessellate_item(&it, Look { flags: it.flags | flags::HOVER }, tol, &self.theme, &mut self.text)
630            })
631            .unwrap_or_default();
632        self.transient_mesh = [
633            grid_mesh(&self.grid, &self.view, size, &self.theme),
634            hover_mesh,
635            world_overlay(&self.overlay, &self.view, tol, &self.theme),
636            screen_overlay(&self.overlay, &self.view, size, &self.theme),
637        ];
638        self.upload_atlas();
639
640        // Upload changed chunks.
641        for &ci in &vis {
642            let c = &mut self.scene.chunks[ci];
643            if c.uploaded != c.built
644                && let Some(d) = &c.data
645            {
646                c.gpu.upload(&self.device, &self.queue, &d.lines, &d.fill_vertices, &d.fill_indices, &d.glyphs);
647                c.uploaded = c.built;
648            }
649        }
650        for (b, m) in self.transient.iter_mut().zip(self.transient_mesh.iter()) {
651            b.upload(&self.device, &self.queue, &m.lines, &m.fill_vertices, &m.fill_indices, &m.glyphs);
652        }
653
654        // Uniforms: [grid, chunks..., hover, world overlay, screen overlay].
655        let mut xfs: Vec<Xf> = Vec::with_capacity(vis.len() + 4);
656        let mut batches: Vec<Batch> = Vec::with_capacity(vis.len() + 4);
657        xfs.push(self.xf_screen(size));
658        batches.push(Batch { slot: 0, chunk: None, transient: Some(0) });
659        for &ci in &vis {
660            let origin = self.scene.chunks[ci].data.as_ref().map_or([0.0, 0.0], |d| d.origin);
661            batches.push(Batch { slot: xfs.len() as u64, chunk: Some(ci), transient: None });
662            xfs.push(self.xf_world(origin, size, 1.0));
663        }
664        let hover_origin = self.transient_mesh[1].origin;
665        batches.push(Batch { slot: xfs.len() as u64, chunk: None, transient: Some(1) });
666        xfs.push(self.xf_world(hover_origin, size, 1.0));
667        let world_origin = self.transient_mesh[2].origin;
668        batches.push(Batch { slot: xfs.len() as u64, chunk: None, transient: Some(2) });
669        xfs.push(self.xf_world(world_origin, size, 1.0));
670        batches.push(Batch { slot: xfs.len() as u64, chunk: None, transient: Some(3) });
671        xfs.push(self.xf_screen(size));
672        let needed = xfs.len() as u64;
673        if needed > self.uniform_slots {
674            self.uniform_slots = needed.next_power_of_two();
675            self.uniform_buf.destroy();
676            self.uniform_buf = uniform_buffer(&self.device, self.uniform_slots);
677            self.uniform_bind = uniform_bind(&self.device, &self.uniform_layout, &self.uniform_buf);
678        }
679        let mut bytes = vec![0u8; (needed * UNIFORM_STRIDE) as usize];
680        for (i, x) in xfs.iter().enumerate() {
681            let at = i * UNIFORM_STRIDE as usize;
682            bytes[at..at + core::mem::size_of::<Xf>()].copy_from_slice(bytemuck::bytes_of(x));
683        }
684        self.queue.write_buffer(&self.uniform_buf, 0, &bytes);
685
686        self.ensure_msaa(size);
687        let bg = unpack(self.theme.background);
688        let clear = wgpu::Color {
689            r: f64::from(bg[0] * bg[3]),
690            g: f64::from(bg[1] * bg[3]),
691            b: f64::from(bg[2] * bg[3]),
692            a: f64::from(bg[3]),
693        };
694        let mut stats = FrameStats {
695            items: self.scene.len() as u32,
696            chunks: self.scene.chunks.len() as u32,
697            chunks_drawn: vis.len() as u32,
698            chunks_rebuilt: ps.rebuilt_chunks,
699            items_tessellated: ps.tessellated_items,
700            ..FrameStats::default()
701        };
702        let mut encoder =
703            self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("dotloom frame") });
704        {
705            let (view, resolve) = match &self.msaa {
706                Some((_, v)) => (v, Some(&target_view)),
707                None => (&target_view, None),
708            };
709            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
710                label: Some("dotloom scene"),
711                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
712                    view,
713                    depth_slice: None,
714                    resolve_target: resolve,
715                    ops: wgpu::Operations {
716                        load: wgpu::LoadOp::Clear(clear),
717                        store: if resolve.is_some() { wgpu::StoreOp::Discard } else { wgpu::StoreOp::Store },
718                    },
719                })],
720                depth_stencil_attachment: None,
721                timestamp_writes: None,
722                occlusion_query_set: None,
723                multiview_mask: None,
724            });
725            pass.set_bind_group(1, &self.atlas_bind, &[]);
726            for b in &batches {
727                let offset = (b.slot * UNIFORM_STRIDE) as u32;
728                pass.set_bind_group(0, &self.uniform_bind, &[offset]);
729                let (buffers, runs): (&ChunkBuffers, Vec<crate::cache::Run>) = match (b.chunk, b.transient) {
730                    (Some(ci), _) => {
731                        let c = &self.scene.chunks[ci];
732                        (&c.gpu, c.data.as_ref().map(|d| d.runs.clone()).unwrap_or_default())
733                    }
734                    (None, Some(t)) => {
735                        let m = &self.transient_mesh[t];
736                        let run = crate::cache::Run {
737                            fills: 0..m.fill_indices.len() as u32,
738                            lines: 0..m.lines.len() as u32,
739                            glyphs: 0..m.glyphs.len() as u32,
740                        };
741                        (&self.transient[t], vec![run])
742                    }
743                    (None, None) => continue,
744                };
745                for r in runs {
746                    if !r.fills.is_empty()
747                        && let (Some(v), Some(i)) = (buffers.fill_vertices.slice(), buffers.fill_indices.slice())
748                    {
749                        pass.set_pipeline(&self.fill_pipeline);
750                        pass.set_vertex_buffer(0, v);
751                        pass.set_index_buffer(i, wgpu::IndexFormat::Uint32);
752                        pass.draw_indexed(r.fills.clone(), 0, 0..1);
753                        stats.draw_calls += 1;
754                        stats.triangles += r.fills.len() as u32 / 3;
755                    }
756                    if let Some(v) = buffers.lines.elements(&r.lines, core::mem::size_of::<LineInstance>() as u64) {
757                        pass.set_pipeline(&self.line_pipeline);
758                        pass.set_vertex_buffer(0, v);
759                        pass.draw(0..6, 0..r.lines.len() as u32);
760                        stats.draw_calls += 1;
761                        stats.line_segments += r.lines.len() as u32;
762                    }
763                    if let Some(v) = buffers.glyphs.elements(&r.glyphs, core::mem::size_of::<GlyphInstance>() as u64) {
764                        pass.set_pipeline(&self.glyph_pipeline);
765                        pass.set_vertex_buffer(0, v);
766                        pass.draw(0..6, 0..r.glyphs.len() as u32);
767                        stats.draw_calls += 1;
768                        stats.glyphs += r.glyphs.len() as u32;
769                    }
770                }
771            }
772        }
773        self.queue.submit([encoder.finish()]);
774        stats.gpu_bytes = self.scene.chunks.iter().map(|c| c.gpu.bytes()).sum();
775        self.last = stats;
776        Ok(stats)
777    }
778
779    /// Release all GPU resources now (buffers, textures). The renderer cannot draw
780    /// afterwards. Dropping the renderer also releases them.
781    pub fn dispose(&mut self) {
782        if self.disposed {
783            return;
784        }
785        self.disposed = true;
786        self.scene.destroy_gpu();
787        for b in core::mem::take(&mut self.transient) {
788            b.destroy();
789        }
790        if let Some((t, _)) = self.msaa.take() {
791            t.destroy();
792        }
793        self.atlas.destroy();
794        self.uniform_buf.destroy();
795    }
796
797    /// Whether [`Renderer::dispose`] was called.
798    #[must_use]
799    pub fn is_disposed(&self) -> bool {
800        self.disposed
801    }
802}