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}