1use 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
15const 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#[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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct RendererOptions {
130 pub sample_count: u32,
132}
133
134impl Default for RendererOptions {
135 fn default() -> Self {
136 Self { sample_count: 4 }
137 }
138}
139
140#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
142#[serde(rename_all = "camelCase")]
143pub struct FrameStats {
144 pub items: u32,
146 pub chunks: u32,
148 pub chunks_drawn: u32,
150 pub chunks_rebuilt: u32,
152 pub items_tessellated: u32,
154 pub draw_calls: u32,
156 pub line_segments: u32,
158 pub triangles: u32,
160 pub glyphs: u32,
162 pub gpu_bytes: u64,
164}
165
166struct Batch {
168 slot: u64,
169 chunk: Option<usize>,
170 transient: Option<usize>,
171}
172
173pub 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 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 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 #[must_use]
441 pub fn device(&self) -> &wgpu::Device {
442 &self.device
443 }
444
445 #[must_use]
447 pub fn queue(&self) -> &wgpu::Queue {
448 &self.queue
449 }
450
451 #[must_use]
453 pub fn format(&self) -> wgpu::TextureFormat {
454 self.format
455 }
456
457 #[must_use]
459 pub fn sample_count(&self) -> u32 {
460 self.sample_count
461 }
462
463 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 pub fn apply_delta(&mut self, delta: SceneDelta) {
475 self.scene.apply(delta);
476 }
477
478 #[must_use]
480 pub fn scene(&self) -> &SceneCache {
481 &self.scene
482 }
483
484 pub fn set_view(&mut self, view: View) -> Result<(), RenderError> {
489 view.validate()?;
490 self.view = view;
491 Ok(())
492 }
493
494 #[must_use]
496 pub fn view(&self) -> View {
497 self.view
498 }
499
500 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 #[must_use]
510 pub fn theme(&self) -> &Theme {
511 &self.theme
512 }
513
514 pub fn set_grid(&mut self, grid: Grid) {
516 self.grid = grid;
517 }
518
519 pub fn set_overlay(&mut self, overlay: Overlay) {
521 self.overlay = overlay;
522 }
523
524 pub fn set_hover(&mut self, id: Option<u64>) {
526 self.hover = id;
527 }
528
529 #[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 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 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 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 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 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 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 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 #[must_use]
799 pub fn is_disposed(&self) -> bool {
800 self.disposed
801 }
802}