Skip to main content

pfx_game/
painter.rs

1use std::collections::BTreeMap;
2
3use pfx_gpu::window::Size;
4use pfx_gpu::{Gpu, wgpu};
5use pfx_input::{GlyphAtlas, KeyLabels};
6use pfx_live::flat::look::Look;
7use pfx_live::flat::{
8    Draw, Environment, FlatIcons, FlatScene, FlatSprites, FlatText, Glyphs, IDENTITY, Matrix,
9    ShadowCurve, Shape, Srgba, multiply, translation,
10};
11use pfx_live::renderer::Renderer;
12use pfx_live::scene::Staged;
13use pfx_live::text::{GlyphPages, GpuAtlas, RichQuad, page_quads};
14use pfx_live::upscale::{Compose, Filter, Overlay, Upscale, clip};
15use pfx_load::scene::Scene;
16use pfx_play::{Anchor, PaintFrame, Ui, Warmer, Warming};
17use pfx_text::{Face, Fallback, Placement, Prefill, Representation, Span, TextEngine};
18
19use crate::config::Config;
20use crate::driver::Driver;
21use crate::prompt::{self, Part, PromptQuads, Sources, Style};
22
23pub struct Painter {
24    renderer: Renderer,
25    staged: Option<Staged>,
26    upscale: Upscale,
27    text: Option<TextEngine>,
28    pages: GlyphPages,
29    filter: Filter,
30    render_scale: f32,
31    seed: u32,
32    none: ShadowCurve,
33    glyphs: Option<(GlyphAtlas, GpuAtlas)>,
34    icons: Option<FlatIcons>,
35    sprites: Option<FlatSprites>,
36    environment: Option<Environment>,
37    fallbacks: BTreeMap<String, Vec<String>>,
38    prompts: Vec<PromptQuads>,
39    uploads: usize,
40    waits: bool,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub struct Drawn {
45    pub render: [u32; 2],
46    pub output: [u32; 2],
47    pub labels: usize,
48    pub prompts: usize,
49    pub uploads: usize,
50}
51
52const BLACK: Srgba = Srgba([0.0, 0.0, 0.0, 1.0]);
53
54enum Sheet {
55    Page(u32),
56    Glyphs,
57}
58
59struct Run {
60    sheet: Sheet,
61    quads: Vec<RichQuad>,
62    colour: Srgba,
63    elevation: f32,
64    id: u32,
65    transform: Matrix,
66}
67
68fn share(anchor: Anchor) -> f32 {
69    match anchor {
70        Anchor::Start => 0.0,
71        Anchor::Center => 0.5,
72        Anchor::End => 1.0,
73    }
74}
75
76fn anchor(anchor: Anchor) -> pfx_text::Anchor {
77    match anchor {
78        Anchor::Start => pfx_text::Anchor::Start,
79        Anchor::Center => pfx_text::Anchor::Center,
80        Anchor::End => pfx_text::Anchor::End,
81    }
82}
83
84impl Painter {
85    pub fn new(
86        gpu: Gpu,
87        scene: Option<&Scene>,
88        config: &Config,
89        window: Size,
90    ) -> Result<Self, String> {
91        let width = window.width.max(1);
92        let height = window.height.max(1);
93        let mut renderer = Renderer::new(gpu, width, height).map_err(|error| error.to_string())?;
94        renderer.set_exposure(config.exposure)?;
95        let staged = scene.map(|scene| renderer.stage(scene)).transpose()?;
96        if let (Some(budget), Some(_)) = (config.budget, &staged) {
97            renderer.set_dynamic_resolution(Some(budget))?;
98        }
99        let upscale = Upscale::new(&renderer.gpu().device);
100        let text = match config.fonts.split_first() {
101            Some((first, rest)) => {
102                let mut engine = TextEngine::new(first)
103                    .map_err(|error| format!("the game's first font: {error:?}"))?;
104                for font in rest {
105                    engine
106                        .register_font(font)
107                        .map_err(|error| format!("the game's font: {error:?}"))?;
108                }
109                Some(engine)
110            }
111            None => None,
112        };
113        let gpu = renderer.gpu().clone();
114        let icons = config
115            .icons
116            .clone()
117            .map(|icons| FlatIcons::new(&gpu.device, &gpu.queue, icons));
118        let sprites = config
119            .sprites
120            .clone()
121            .map(|sprites| FlatSprites::new(&gpu.device, &gpu.queue, sprites));
122        let environment = config
123            .environment
124            .as_ref()
125            .map(|texels| {
126                Environment::new(
127                    &gpu.device,
128                    &gpu.queue,
129                    texels.width,
130                    texels.height,
131                    &texels.linear,
132                    texels.intensity,
133                )
134            })
135            .transpose()?;
136        Ok(Self {
137            renderer,
138            staged,
139            upscale,
140            text,
141            pages: GlyphPages::new(),
142            filter: config.filter,
143            render_scale: config.render_scale,
144            seed: config.seed as u32,
145            none: ShadowCurve::none(),
146            glyphs: None,
147            icons,
148            sprites,
149            environment,
150            fallbacks: BTreeMap::new(),
151            prompts: Vec::new(),
152            uploads: 0,
153            waits: false,
154        })
155    }
156
157    pub fn wait_for_frames(&mut self, waits: bool) {
158        self.waits = waits;
159    }
160
161    pub fn renderer(&self) -> &Renderer {
162        &self.renderer
163    }
164
165    pub fn renderer_mut(&mut self) -> &mut Renderer {
166        &mut self.renderer
167    }
168
169    pub fn staged(&self) -> Option<&Staged> {
170        self.staged.as_ref()
171    }
172
173    pub fn text(&self) -> Option<&TextEngine> {
174        self.text.as_ref()
175    }
176
177    pub fn glyph_pages(&self) -> &GlyphPages {
178        &self.pages
179    }
180
181    pub fn prompt_quads(&self) -> &[PromptQuads] {
182        &self.prompts
183    }
184
185    pub fn upscale(&self) -> &Upscale {
186        &self.upscale
187    }
188
189    pub fn warm(
190        &mut self,
191        driver: &mut Driver,
192        format: wgpu::TextureFormat,
193        pass: u32,
194        elapsed: f64,
195        budget: f64,
196    ) -> Result<Warming, String> {
197        let window = driver.window();
198        let size = [window.width.max(1), window.height.max(1)];
199        let pixels_per_unit = driver
200            .report()
201            .map_or(1.0, |report| report.pixels_per_unit());
202        if pass == 0 {
203            self.upscale.warm(&self.renderer.gpu().device, format);
204        }
205        let mut warmer = PainterWarmer {
206            painter: self,
207            format,
208            size,
209            pixels_per_unit,
210        };
211        driver.warm(&mut warmer, pass, elapsed, budget)
212    }
213
214    fn sync_pages(&mut self) -> Result<usize, String> {
215        let Some(engine) = &mut self.text else {
216            return Ok(0);
217        };
218        let changes = engine.take_atlas_changes();
219        let gpu = self.renderer.gpu();
220        self.pages
221            .sync(&gpu.device, &gpu.queue, &changes)
222            .map_err(|error| format!("the UI's glyph pages: {error:?}"))?;
223        Ok(changes.cells.len())
224    }
225
226    fn fallbacks(&mut self, family: &str, faces: &[String]) -> Result<(), String> {
227        if faces.is_empty() || self.fallbacks.get(family).is_some_and(|set| set == faces) {
228            return Ok(());
229        }
230        let Some(engine) = &mut self.text else {
231            return Ok(());
232        };
233        let list: Vec<Fallback> = faces.iter().map(|face| Fallback::new(face)).collect();
234        engine
235            .set_fallbacks(family, None, &list)
236            .map_err(|error| format!("the fallbacks of {family:?}: {error:?}"))?;
237        self.fallbacks.insert(family.to_owned(), faces.to_vec());
238        Ok(())
239    }
240
241    fn render_size(&mut self, content: [u32; 2], scale: Option<Size>) -> Result<[u32; 2], String> {
242        if self.renderer.dynamic_resolution().is_some() {
243            let (width, height) = self.renderer.apply_dynamic_resolution(content)?;
244            return Ok([width, height]);
245        }
246        let size = scale.ok_or("the render scale must be a positive finite factor")?;
247        if self.renderer.size() != (size.width, size.height) {
248            self.renderer.resize(size.width, size.height)?;
249        }
250        Ok([size.width, size.height])
251    }
252
253    fn ensure_glyph_atlas(&mut self) -> Result<(), String> {
254        if self.glyphs.is_none() {
255            let atlas =
256                GlyphAtlas::build().map_err(|error| format!("the input glyph atlas: {error:?}"))?;
257            let gpu = self.renderer.gpu();
258            let upload = GpuAtlas::new(&gpu.device, &gpu.queue, &atlas.atlas)
259                .map_err(|error| format!("the input glyph atlas: {error:?}"))?;
260            self.glyphs = Some((atlas, upload));
261        }
262        Ok(())
263    }
264
265    fn runs(
266        &mut self,
267        ui: &Ui,
268        parts: &[Vec<Part>],
269        labels: &KeyLabels,
270        pixels_per_unit: f32,
271    ) -> Result<Vec<Run>, String> {
272        self.prompts.clear();
273        self.uploads = 0;
274        if ui.labels.is_empty() && ui.prompts.is_empty() {
275            self.uploads = self.sync_pages()?;
276            return Ok(Vec::new());
277        }
278        if !ui.prompts.is_empty() {
279            self.ensure_glyph_atlas()?;
280        }
281        let placement = prompt::placement(pixels_per_unit);
282        let mut runs = Vec::new();
283        if let Some(engine) = &mut self.text {
284            engine.begin_frame();
285        }
286        for label in &ui.labels {
287            self.fallbacks(&label.family, &label.fallbacks)?;
288            let Some(engine) = &mut self.text else {
289                return Err(
290                    "the game's UI has labels but the config names no font (Config::font)".into(),
291                );
292            };
293            let span = Span {
294                text: label.text.clone(),
295                face: Face {
296                    family: label.family.clone(),
297                    size: label.size,
298                    line: label.line_height(),
299                    weight: label.weight,
300                    italic: false,
301                    spacing: 0.0,
302                },
303                color: [1.0; 4],
304            };
305            let spans = [span];
306            let drop = match label.height {
307                Some(height) => {
308                    let block = engine
309                        .layout_spans(&spans, label.wrap, placement.scale, anchor(label.anchor))
310                        .map_err(|error| format!("label {:?}: {error:?}", label.text))?;
311                    (height - block.height) * share(label.vertical)
312                }
313                None => 0.0,
314            };
315            let at = Placement {
316                origin: [label.at[0], label.at[1] + drop],
317                ..placement
318            };
319            let placed = engine
320                .place_spans(&spans, label.wrap, anchor(label.anchor), &at)
321                .map_err(|error| format!("label {:?}: {error:?}", label.text))?;
322            let transform = label.transform.map_or(IDENTITY, |turn| {
323                let [x, y] = label.at;
324                multiply(multiply(translation(x, y), turn), translation(-x, -y))
325            });
326            for (page, quads) in page_quads(&placed.quads) {
327                runs.push(Run {
328                    sheet: Sheet::Page(page),
329                    quads,
330                    colour: label.colour,
331                    elevation: label.elevation,
332                    id: label.id,
333                    transform,
334                });
335            }
336        }
337        if let Some((atlas, _)) = &self.glyphs {
338            for (prompt, parts) in ui.prompts.iter().zip(parts) {
339                let mut sources = Sources {
340                    atlas,
341                    text: self.text.as_mut(),
342                    labels,
343                };
344                let quads = prompt::layout(
345                    parts,
346                    prompt.at,
347                    prompt.height,
348                    &Style::of(prompt, pixels_per_unit),
349                    &mut sources,
350                )?;
351                runs.push(Run {
352                    sheet: Sheet::Glyphs,
353                    quads: quads.glyphs.clone(),
354                    colour: prompt.colour,
355                    elevation: prompt.elevation,
356                    id: prompt.id,
357                    transform: IDENTITY,
358                });
359                for (page, quads) in &quads.labels {
360                    runs.push(Run {
361                        sheet: Sheet::Page(*page),
362                        quads: quads.clone(),
363                        colour: prompt.colour,
364                        elevation: prompt.elevation,
365                        id: prompt.id,
366                        transform: IDENTITY,
367                    });
368                }
369                self.prompts.push(quads);
370            }
371        }
372        if let Some(engine) = &mut self.text {
373            engine.end_frame();
374        }
375        self.uploads = self.sync_pages()?;
376        Ok(runs)
377    }
378
379    fn clear_source(&mut self, colour: Srgba) -> Result<(wgpu::TextureView, [u32; 2]), String> {
380        let gpu = self.renderer.gpu();
381        let size = [1, 1];
382        let source =
383            self.upscale
384                .source_target(&gpu.device, size, self.renderer.output_format())?;
385        let [r, g, b, a] = colour.0.map(f64::from);
386        let mut encoder = gpu
387            .device
388            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
389                label: Some("pfx game clear"),
390            });
391        encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
392            multiview_mask: None,
393            label: Some("pfx game clear"),
394            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
395                view: &source,
396                resolve_target: None,
397                depth_slice: None,
398                ops: wgpu::Operations {
399                    load: wgpu::LoadOp::Clear(wgpu::Color { r, g, b, a }),
400                    store: wgpu::StoreOp::Store,
401                },
402            })],
403            depth_stencil_attachment: None,
404            timestamp_writes: None,
405            occlusion_query_set: None,
406        });
407        gpu.queue.submit([encoder.finish()]);
408        Ok((source, size))
409    }
410
411    fn scene_source(
412        &mut self,
413        driver: &mut Driver,
414        content: [u32; 2],
415        scale: Option<Size>,
416    ) -> Result<(wgpu::TextureView, [u32; 2]), String> {
417        let size = self.render_size(content, scale)?;
418        let (capacity_width, capacity_height) = self.renderer.capacity();
419        let source = self.upscale.source_target(
420            &self.renderer.gpu().device,
421            [capacity_width, capacity_height],
422            self.renderer.output_format(),
423        )?;
424        let aspect = size[0] as f32 / size[1] as f32;
425        let Some(staged) = &self.staged else {
426            return Err("a 3D frame needs a scene".into());
427        };
428        let finish = staged.finish();
429        let frame = driver
430            .session_mut()
431            .present(staged, &mut self.renderer, aspect, self.seed)
432            .map_err(|error| error.to_string())?;
433        if self.waits {
434            self.renderer
435                .render(&frame.scene, &frame.text, &frame.effects, finish, &source)?;
436        } else {
437            self.renderer
438                .submit(&frame.scene, &frame.text, &frame.effects, finish, &source)?;
439        }
440        Ok((source, size))
441    }
442
443    pub fn draw(
444        &mut self,
445        driver: &mut Driver,
446        output: &wgpu::TextureView,
447        format: wgpu::TextureFormat,
448    ) -> Result<Option<Drawn>, String> {
449        let window = driver.window();
450        let Some(report) = driver.report().copied() else {
451            return Ok(None);
452        };
453        let content = [
454            (report.content.width.round() as u32).max(1),
455            (report.content.height.round() as u32).max(1),
456        ];
457        let ui = driver.shared_ui();
458        let (source, size) = if self.staged.is_some() {
459            self.scene_source(driver, content, report.render_size(self.render_scale))?
460        } else {
461            self.clear_source(ui.clear.unwrap_or(BLACK))?
462        };
463        let gpu = self.renderer.gpu();
464        let curve = ui.curve.as_ref().unwrap_or(&self.none);
465        let backdrop = FlatScene {
466            layout: report.window_units(),
467            clear: None,
468            curve,
469            light: ui.light,
470            draws: &ui.backdrop,
471            groups: &ui.groups,
472            text: &[],
473            icons: self.icons.as_ref(),
474            sprites: self.sprites.as_ref(),
475            environment: self.environment.as_ref(),
476            post: false,
477            frame: driver.frames() as u32,
478            seed: self.seed,
479        };
480        let backdrop = (report.backdrop && !ui.backdrop.is_empty()).then_some(&backdrop);
481        let output_size = [window.width.max(1), window.height.max(1)];
482        let mut encoder = gpu
483            .device
484            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
485                label: Some("pfx game frame"),
486            });
487        self.upscale.encode(
488            &gpu.device,
489            &gpu.queue,
490            &mut encoder,
491            &Compose {
492                source: &source,
493                source_format: self.renderer.output_format(),
494                source_size: size,
495                output,
496                output_format: format,
497                output_size,
498                content: report.content,
499                bars: report.bar_colour,
500                filter: self.filter,
501                backdrop,
502            },
503            None,
504        )?;
505        gpu.queue.submit([encoder.finish()]);
506        let content_units = report.content_units();
507        let paint = PaintFrame {
508            format,
509            output: output_size,
510            content: [
511                report.content.x,
512                report.content.y,
513                report.content.width,
514                report.content.height,
515            ],
516            clip: clip(report.content, output_size),
517            render: self.staged.is_some().then_some(size),
518            layout: report.layout_units(),
519            content_units: [
520                content_units.x,
521                content_units.y,
522                content_units.width,
523                content_units.height,
524            ],
525            window_units: report.window_units(),
526            pixels_per_unit: report.pixels_per_unit(),
527            frame: driver.frames(),
528            seed: self.seed,
529        };
530        driver
531            .session_mut()
532            .game_mut()
533            .paint(&mut self.renderer, output, &paint)
534            .map_err(|error| error.to_string())?;
535        let runs = self.runs(
536            &ui,
537            driver.prompt_parts(),
538            driver.labels(),
539            report.pixels_per_unit(),
540        )?;
541        let mut draws = ui.draws.clone();
542        let mut texts = Vec::with_capacity(runs.len());
543        for run in &runs {
544            let atlas = match run.sheet {
545                Sheet::Page(page) => self
546                    .pages
547                    .page(page)
548                    .ok_or_else(|| format!("the UI's glyph page {page} is missing"))?,
549                Sheet::Glyphs => self
550                    .glyphs
551                    .as_ref()
552                    .map(|(_, atlas)| atlas)
553                    .ok_or("the input glyph atlas is missing")?,
554            };
555            texts.push(FlatText {
556                atlas,
557                glyphs: Glyphs::Quads(&run.quads),
558                colour: run.colour,
559            });
560            draws.push(
561                Draw::new(Shape::Text(texts.len() - 1))
562                    .transform(run.transform)
563                    .elevation(run.elevation)
564                    .id(run.id),
565            );
566        }
567        let look = ui.look.as_ref().map(|look| look.frame());
568        if !draws.is_empty() || look.is_some() {
569            let hud = FlatScene {
570                layout: report.layout_units(),
571                clear: None,
572                curve: ui.curve.as_ref().unwrap_or(&self.none),
573                light: ui.light,
574                draws: &draws,
575                groups: &ui.groups,
576                text: &texts,
577                icons: self.icons.as_ref(),
578                sprites: self.sprites.as_ref(),
579                environment: self.environment.as_ref(),
580                post: false,
581                frame: driver.frames() as u32,
582                seed: self.seed,
583            };
584            let gpu = self.renderer.gpu();
585            let mut encoder = gpu
586                .device
587                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
588                    label: Some("pfx game ui"),
589                });
590            self.upscale.encode_overlay(
591                &gpu.device,
592                &gpu.queue,
593                &mut encoder,
594                &Overlay {
595                    scene: &hud,
596                    look: look.as_ref(),
597                    output,
598                    output_format: format,
599                    output_size,
600                    content: report.content,
601                },
602                None,
603            )?;
604            gpu.queue.submit([encoder.finish()]);
605        }
606        driver
607            .session_mut()
608            .draw_hud_on(&mut self.renderer, output, format, output_size);
609        Ok(Some(Drawn {
610            render: if self.staged.is_some() { size } else { [0, 0] },
611            output: output_size,
612            labels: ui.labels.len(),
613            prompts: ui.prompts.len(),
614            uploads: self.uploads,
615        }))
616    }
617}
618
619pub struct Warmup {
620    passes: u32,
621    start_ns: Option<u64>,
622    budget: f64,
623}
624
625impl Warmup {
626    pub fn new(budget: f64) -> Self {
627        Self {
628            passes: 0,
629            start_ns: None,
630            budget,
631        }
632    }
633
634    pub fn passes(&self) -> u32 {
635        self.passes
636    }
637
638    pub fn pass(
639        &mut self,
640        painter: &mut Painter,
641        driver: &mut Driver,
642        format: wgpu::TextureFormat,
643        now_ns: u64,
644    ) -> Result<bool, String> {
645        let start = *self.start_ns.get_or_insert(now_ns);
646        let elapsed = now_ns.saturating_sub(start) as f64 / 1e9;
647        let pass = self.passes;
648        self.passes += 1;
649        if pass > 0 && elapsed >= self.budget {
650            return Ok(true);
651        }
652        Ok(painter.warm(driver, format, pass, elapsed, self.budget)? == Warming::Done)
653    }
654}
655
656struct PainterWarmer<'a> {
657    painter: &'a mut Painter,
658    format: wgpu::TextureFormat,
659    size: [u32; 2],
660    pixels_per_unit: f32,
661}
662
663impl Warmer for PainterWarmer<'_> {
664    fn glyphs(
665        &mut self,
666        family: &str,
667        weight: u16,
668        sizes: &[f32],
669        chars: &str,
670    ) -> Result<usize, String> {
671        let Some(engine) = &mut self.painter.text else {
672            return Err("warming glyphs needs a font in the config (Config::font)".into());
673        };
674        let list: Vec<Prefill> = sizes
675            .iter()
676            .map(|&size| Prefill {
677                face: Face {
678                    family: family.to_owned(),
679                    size,
680                    line: size * 1.25,
681                    weight,
682                    italic: false,
683                    spacing: 0.0,
684                },
685                chars: chars.to_owned(),
686                scale: self.pixels_per_unit.max(f32::MIN_POSITIVE),
687                representation: Representation::Msdf,
688                bins: [true; 4],
689                number: None,
690            })
691            .collect();
692        let made = engine
693            .prefill(&list)
694            .map_err(|error| format!("warming the glyphs of {family:?}: {error:?}"))?;
695        self.painter.sync_pages()?;
696        Ok(made)
697    }
698
699    fn looks(&mut self, looks: &[Look]) -> Result<(), String> {
700        let gpu = self.painter.renderer.gpu();
701        self.painter
702            .upscale
703            .warm_overlay(&gpu.device, &gpu.queue, self.format, self.size, looks)
704    }
705
706    fn renderer(&mut self) -> Option<&mut Renderer> {
707        Some(&mut self.painter.renderer)
708    }
709}