Skip to main content

valo_renderer/
renderer.rs

1use valo_dl::DisplayList;
2use valo_geometry::Color;
3
4use crate::contours::ContourCache;
5use crate::glyphs::GlyphStore;
6use crate::gpu_timer::GpuTimer;
7use crate::host_buffer::HostBuffer;
8use crate::images::ImageStore;
9use crate::pipelines::PipelineCache;
10use crate::plan::{FramePlan, PassColor, PlannedPass, Planner};
11use crate::pool::TargetPool;
12
13/// `RenderTarget` is the destination for one render operation.
14///
15/// `texture` must be the resource behind `view` and match `format` and `size`.
16/// Advanced blends and backdrop filters require `COPY_SRC` texture usage.
17pub struct RenderTarget<'a> {
18    /// `view` is the attachment into which Valo renders.
19    pub view: &'a wgpu::TextureView,
20    /// `texture` is the resource behind `view`.
21    pub texture: &'a wgpu::Texture,
22    /// `format` is the pixel format exposed by `view`.
23    pub format: wgpu::TextureFormat,
24    /// `size` is the renderable area in pixels.
25    pub size: [u32; 2],
26    /// `clear` replaces existing pixels when set and preserves them when `None`.
27    pub clear: Option<Color>,
28}
29
30/// `RenderStats` reports the work performed by one render operation.
31#[derive(Clone, Copy, Debug, Default)]
32pub struct RenderStats {
33    /// `ops` is the number of replayed operations, including nested lists.
34    pub ops: u32,
35    /// `draws` is the number of logical draws remaining after culling.
36    pub draws: u32,
37    /// `clips` is the number of encoded clip operations.
38    pub clips: u32,
39    /// `culled` is the number of draws skipped because their bounds miss the target.
40    pub culled: u32,
41    /// `layers_rendered` is the number of offscreen layers actually rendered.
42    pub layers_rendered: u32,
43    /// `layers_elided` is the number of save layers applied without an offscreen target.
44    pub layers_elided: u32,
45    /// `snapshots` is the number of target-region copies used by destination reads.
46    pub snapshots: u32,
47    /// `backdrops` is the number of backdrop regions that ran a blur chain.
48    pub backdrops: u32,
49    /// `shared_backdrops` is the number of backdrop regions that reused a shared blur.
50    pub shared_backdrops: u32,
51    /// `filter_passes` is the number of encoded image-filter passes.
52    pub filter_passes: u32,
53    /// `text_tiers` contains glyph-run counts for `[bitmap mask, SDF, outline]`.
54    pub text_tiers: [u32; 3],
55    /// `glyph_rasters` is the number of glyphs rasterized after cache misses.
56    pub glyph_rasters: u32,
57    /// `raster_quads` is the number of cached display lists drawn as image quads.
58    pub raster_quads: u32,
59    /// `raster_fills` is the number of display lists rendered into cache textures.
60    pub raster_fills: u32,
61    /// `atlas_gcs` is the number of full glyph-atlas collections.
62    pub atlas_gcs: u32,
63    /// `held_rasters` is the number of bitmap or SDF misses served by a resident size.
64    pub held_rasters: u32,
65    /// `opaque_reordered` is the number of opaque draws moved earlier for depth culling.
66    pub opaque_reordered: u32,
67    /// `gpu_ms` is the previous resolved frame's GPU time in milliseconds.
68    ///
69    /// It is zero until a timestamp resolves or when timestamps are unavailable.
70    pub gpu_ms: f32,
71    /// `blocks_created` is the number of transient upload blocks allocated this frame.
72    pub blocks_created: u32,
73    /// `cpu_ms` is total CPU time spent in rendering and submission.
74    pub cpu_ms: f32,
75    /// `draw_calls` is the number of encoded GPU draw commands.
76    ///
77    /// One logical draw may require several commands, while batched glyphs may
78    /// share one.
79    pub draw_calls: u32,
80    /// `render_passes` is the number of encoded render passes.
81    pub render_passes: u32,
82    /// `pipeline_switches` is the number of encoded pipeline changes.
83    pub pipeline_switches: u32,
84    /// `vertex_bytes` is the number of transient vertex bytes uploaded.
85    pub vertex_bytes: u64,
86    /// `uniform_bytes` is the number of transient uniform bytes uploaded.
87    pub uniform_bytes: u64,
88    /// `plan_ms` is the CPU time spent replaying and planning.
89    pub plan_ms: f32,
90    /// `encode_ms` is the CPU time spent compiling, uploading, encoding, and submitting.
91    pub encode_ms: f32,
92}
93
94/// `RendererCore` replays display lists on a host-owned wgpu device.
95///
96/// Hosts normally use the `valo` crate's `Context` instead. This type is the
97/// GPU core that context wraps: it owns caches and pipelines, and holds no
98/// application content of its own.
99pub struct RendererCore {
100    device: wgpu::Device,
101    queue: wgpu::Queue,
102    host: HostBuffer,
103    pipelines: PipelineCache,
104    images: ImageStore,
105    pool: TargetPool,
106    glyphs: GlyphStore,
107    contours: ContourCache,
108    ramps: crate::ramps::RampCache,
109    rasters: crate::raster::ListRasterCache,
110    /// The one linear sampler every filter/composite bind group shares —
111    /// created once (a per-frame create is a JS hop on wasm).
112    sampler: wgpu::Sampler,
113    timer: GpuTimer,
114}
115
116impl RendererCore {
117    /// `new` creates a renderer from a host-owned device and queue.
118    pub fn new(device: wgpu::Device, queue: wgpu::Queue) -> Self {
119        let host = HostBuffer::new(&device);
120        let pipelines = PipelineCache::new(&device, host.bind_group_layout());
121        let images = ImageStore::new(&device, &queue);
122        let pool = TargetPool::new(&device);
123        let glyphs = GlyphStore::new(&device, &queue);
124        let timer = GpuTimer::new(&device, &queue);
125        let sampler = crate::plan::linear_sampler(&device);
126        Self {
127            device,
128            queue,
129            host,
130            pipelines,
131            images,
132            pool,
133            glyphs,
134            contours: ContourCache::new(),
135            ramps: crate::ramps::RampCache::new(),
136            rasters: crate::raster::ListRasterCache::new(),
137            sampler,
138            timer,
139        }
140    }
141
142    /// `images` returns the image store used for uploads and sampling.
143    pub fn images(&mut self) -> &mut ImageStore {
144        &mut self.images
145    }
146
147    /// `device` returns the device used by this renderer.
148    pub fn device(&self) -> &wgpu::Device {
149        &self.device
150    }
151
152    /// `set_text_tiers` controls how text is rendered across font-size ranges.
153    ///
154    /// Valo uses bitmap masks below `sdf_min`, SDF below `path_min`, and
155    /// outlines above it. The defaults suit normal use; override them only for
156    /// specialized scaling or zoom behavior.
157    pub fn set_text_tiers(&mut self, tiers: crate::TextTiers) {
158        self.glyphs.tiers = tiers;
159    }
160
161    /// `set_hide_missing_glyphs` controls whether unresolved characters render blank.
162    ///
163    /// By default, unresolved characters render the font's `.notdef` glyph,
164    /// usually a "tofu" box. This is common when CJK fallback fonts are missing.
165    /// Use [`valo_text::FontDemand`] to detect characters hidden by this option.
166    pub fn set_hide_missing_glyphs(&mut self, hide: bool) {
167        self.glyphs.set_hide_missing_glyphs(hide);
168    }
169
170    /// `set_text_raster_hold` allows existing text rasters to stand in for missing sizes.
171    ///
172    /// This applies to bitmap-mask and SDF text, not vector outlines. It is
173    /// useful during rapid zooming: enable it while the gesture is active and
174    /// clear it afterward so the next frame renders sharply.
175    pub fn set_text_raster_hold(&mut self, held: bool) {
176        self.glyphs.set_text_raster_hold(held);
177    }
178
179    /// `set_raster_hold` allows cached display-list textures to be reused at any scale.
180    ///
181    /// This is useful during rapid zooming: enable it when the gesture starts
182    /// and clear it when the view settles so caches refill at the final scale.
183    pub fn set_raster_hold(&mut self, held: bool) {
184        self.rasters.set_hold(held);
185    }
186
187    /// `render` draws a display list into a target and returns frame statistics.
188    ///
189    /// Each call submits one command buffer.
190    pub fn render(&mut self, dl: &DisplayList, target: &RenderTarget) -> RenderStats {
191        #[cfg(feature = "trace")]
192        let _span = tracing::info_span!("valo.render", draws = dl.draw_count()).entered();
193        let t0 = web_time::Instant::now();
194        let blocks_before = self.host.blocks_created;
195
196        self.host.begin_frame();
197        let plan = self.plan(dl, target);
198        let t_planned = web_time::Instant::now();
199
200        let mut stats = plan.stats;
201        self.glyphs.flush_uploads();
202        (stats.uniform_bytes, stats.vertex_bytes) = self.upload_and_compile(&plan);
203        self.encode_and_submit(&plan, target, &mut stats);
204        (stats.glyph_rasters, stats.atlas_gcs, stats.held_rasters) = self.glyphs.frame_counters();
205        self.pool.end_frame();
206        self.images.end_frame();
207        self.contours.end_frame();
208        self.glyphs.end_frame();
209        self.ramps.end_frame();
210        self.rasters.end_frame();
211
212        stats.render_passes = plan.passes.len() as u32;
213        stats.blocks_created = (self.host.blocks_created - blocks_before) as u32;
214        stats.plan_ms = (t_planned - t0).as_secs_f32() * 1000.0;
215        stats.encode_ms = t_planned.elapsed().as_secs_f32() * 1000.0;
216        stats.cpu_ms = t0.elapsed().as_secs_f32() * 1000.0;
217        stats.gpu_ms = self.timer.latest_ms(&self.device);
218        stats
219    }
220
221    /// `memory_report` returns resource counts and estimated GPU memory usage.
222    ///
223    /// The `counters` feature adds the counters reported by wgpu.
224    pub fn memory_report(&self) -> crate::MemoryReport {
225        crate::MemoryReport {
226            images: self.images.report(),
227            atlas: self.glyphs.report_atlas(),
228            targets: self.pool.report(),
229            host_buffer: self.host.report(),
230            contours: self.contours.report(),
231            glyph_paths: self.glyphs.report_paths(),
232            ramps: self.ramps.report(),
233            raster_cache: self.rasters.report(),
234            wgpu: crate::report::wgpu_counters(&self.device),
235        }
236    }
237
238    fn plan(&mut self, dl: &DisplayList, target: &RenderTarget) -> FramePlan {
239        #[cfg(feature = "trace")]
240        let _span = tracing::info_span!("valo.plan").entered();
241        // Disjoint field borrows: the planner mutates arenas + pools while
242        // reading the pipeline cache's layouts.
243        let Self {
244            device,
245            queue,
246            host,
247            images,
248            pool,
249            pipelines,
250            glyphs,
251            contours,
252            ramps,
253            rasters,
254            sampler,
255            ..
256        } = self;
257        Planner::new(
258            device, queue, host, images, pool, pipelines, glyphs, contours, ramps, rasters,
259            sampler, target, dl,
260        )
261        .run(dl)
262    }
263
264    fn upload_and_compile(&mut self, plan: &FramePlan) -> (u64, u64) {
265        let bytes = self.host.flush(&self.queue);
266        for pass in &plan.passes {
267            for step in &pass.steps {
268                self.pipelines.ensure(&self.device, step.key);
269            }
270        }
271        bytes
272    }
273
274    fn encode_and_submit(
275        &mut self,
276        plan: &FramePlan,
277        target: &RenderTarget,
278        stats: &mut RenderStats,
279    ) {
280        #[cfg(feature = "trace")]
281        let _span = tracing::info_span!("valo.encode", passes = plan.passes.len()).entered();
282        let mut encoder = self
283            .device
284            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
285                label: Some("valo.frame"),
286            });
287        for (index, pass) in plan.passes.iter().enumerate() {
288            self.encode_copies(&mut encoder, pass);
289            let timing = self.timer.pass_writes(index, plan.passes.len());
290            self.encode_pass(&mut encoder, pass, target, timing, stats);
291        }
292        self.timer.end_frame(&mut encoder);
293        self.queue.submit(std::iter::once(encoder.finish()));
294        self.timer.after_submit();
295    }
296
297    /// Dst snapshots for advanced blends: land BEFORE the segment that
298    /// samples them. Same origin in src and dst — the snapshot shares the
299    /// target's coordinates, so only the region under the draw is copied.
300    fn encode_copies(&self, encoder: &mut wgpu::CommandEncoder, pass: &PlannedPass) {
301        for copy in &pass.pre_copies {
302            let origin = wgpu::Origin3d {
303                x: copy.origin[0],
304                y: copy.origin[1],
305                z: 0,
306            };
307            encoder.copy_texture_to_texture(
308                copy_at(&copy.src, origin),
309                copy_at(&copy.dst, origin),
310                wgpu::Extent3d {
311                    width: copy.size[0],
312                    height: copy.size[1],
313                    depth_or_array_layers: 1,
314                },
315            );
316        }
317    }
318
319    fn encode_pass(
320        &self,
321        encoder: &mut wgpu::CommandEncoder,
322        pass: &PlannedPass,
323        target: &RenderTarget,
324        timing: Option<wgpu::RenderPassTimestampWrites>,
325        stats: &mut RenderStats,
326    ) {
327        let color = match &pass.color {
328            PassColor::Main { msaa } => color_attachment(msaa, Some(target.view), pass),
329            PassColor::Layer { msaa, resolve } => color_attachment(msaa, Some(resolve), pass),
330            PassColor::Filter { view } => color_attachment(view, None, pass),
331        };
332        let color_attachments = [Some(color)];
333        let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
334            label: Some("valo.pass"),
335            color_attachments: &color_attachments,
336            depth_stencil_attachment: pass
337                .depth
338                .as_ref()
339                .map(|depth| depth_attachment(depth, pass.clear_depth, pass.store)),
340            timestamp_writes: timing,
341            occlusion_query_set: None,
342            multiview_mask: None,
343        });
344        rp.set_stencil_reference(0);
345        let mut bound = None;
346        for step in &pass.steps {
347            if bound != Some(step.key) {
348                rp.set_pipeline(self.pipelines.get(&step.key));
349                bound = Some(step.key);
350                stats.pipeline_switches += 1;
351            }
352            stats.draw_calls += 1;
353            rp.set_bind_group(
354                0,
355                self.host.bind_group(step.uniforms.block),
356                &[step.uniforms.offset],
357            );
358            if let Some(texture) = &step.texture {
359                rp.set_bind_group(1, texture, &[]);
360            }
361            match step.mesh {
362                None => rp.draw(0..6, 0..1),
363                Some((slot, vertex_count)) => {
364                    let buffer = self.host.vertex_buffer(slot.block);
365                    rp.set_vertex_buffer(0, buffer.slice(slot.offset..slot.offset + slot.bytes));
366                    rp.draw(0..vertex_count, 0..1);
367                }
368            }
369        }
370    }
371}
372
373fn copy_at(texture: &wgpu::Texture, origin: wgpu::Origin3d) -> wgpu::TexelCopyTextureInfo<'_> {
374    wgpu::TexelCopyTextureInfo {
375        texture,
376        mip_level: 0,
377        origin,
378        aspect: wgpu::TextureAspect::All,
379    }
380}
381
382/// Main/layer passes render ×4 and resolve every segment; MSAA contents are
383/// stored only when a later segment resumes the target (the final segment
384/// discards — skips the 4× write-out on tiled GPUs). Filter passes render
385/// 1-sample straight into `view` (no resolve) and always store.
386fn color_attachment<'a>(
387    view: &'a wgpu::TextureView,
388    resolve: Option<&'a wgpu::TextureView>,
389    pass: &PlannedPass,
390) -> wgpu::RenderPassColorAttachment<'a> {
391    wgpu::RenderPassColorAttachment {
392        view,
393        depth_slice: None,
394        resolve_target: resolve,
395        ops: wgpu::Operations {
396            load: match pass.clear {
397                Some(c) => wgpu::LoadOp::Clear(wgpu::Color {
398                    r: (c.r * c.a) as f64,
399                    g: (c.g * c.a) as f64,
400                    b: (c.b * c.a) as f64,
401                    a: c.a as f64,
402                }),
403                None => wgpu::LoadOp::Load,
404            },
405            store: store_op(pass.store),
406        },
407    }
408}
409
410/// Depth/stencil persist across a target's segments (clip ceilings survive
411/// pass breaks); only the first segment clears, only resumed segments store.
412fn depth_attachment(
413    view: &wgpu::TextureView,
414    clear: bool,
415    store: bool,
416) -> wgpu::RenderPassDepthStencilAttachment<'_> {
417    wgpu::RenderPassDepthStencilAttachment {
418        view,
419        depth_ops: Some(wgpu::Operations {
420            load: if clear {
421                wgpu::LoadOp::Clear(0.0)
422            } else {
423                wgpu::LoadOp::Load
424            },
425            store: store_op(store),
426        }),
427        stencil_ops: Some(wgpu::Operations {
428            load: if clear {
429                wgpu::LoadOp::Clear(0)
430            } else {
431                wgpu::LoadOp::Load
432            },
433            store: store_op(store),
434        }),
435    }
436}
437
438fn store_op(store: bool) -> wgpu::StoreOp {
439    if store {
440        wgpu::StoreOp::Store
441    } else {
442        wgpu::StoreOp::Discard
443    }
444}