1#[cfg(not(target_arch = "wasm32"))]
174use alloc::boxed::Box;
175use alloc::collections::BTreeMap;
176use alloc::format;
177use alloc::rc::{Rc, Weak};
178use alloc::string::String;
179use alloc::sync::Arc;
180use alloc::vec;
181use alloc::vec::Vec;
182use core::sync::atomic::{AtomicU32, Ordering};
183
184use gpu::{
185 Access, Adapter, BindEntry, BindGroup, BindGroupDesc, BindGroupLayout, Binding, BindingKind, Buffer, BufferDesc, BufferUsage,
186 ColorAttachment, ColorTarget, Cull, DataLayout, Device, DeviceDesc, Format, GpuError, Instance, Layout, Level, Load, MemoryKind,
187 PipelineLayoutDesc, PresentMode, RenderPassDesc, RenderPipeline, RenderPipelineDesc, Resource, ShaderModule, ShaderStage, Stages,
188 Swapchain, SwapchainDesc, Texture, TextureDesc, TextureRegion, TextureUsage, Topology, TryAcquire,
189};
190use rt::sync::SpinLock;
191
192use super::auto::{next_word, skip_ws, word_eq};
193use super::canvas::{CanvasGpu, CanvasView};
194use super::cmd::{render_layer, Cmd, DrawRef, Gfx};
195use super::consts::*;
196use super::dirty::TOP_BLOCKED;
197use super::{App, GpuOutput, Ui};
198use gfx::{Dc, Surface, FMT_A8, FMT_XRGB};
199use text::Text;
200
201const DEFAULT_ON: bool = true;
204const DEFAULT_ON_LINUX: bool = true;
207const DEFAULT_ON_BROWSER: bool = true;
211const PRESENT: PresentMode = PresentMode::Mailbox;
215const IMAGES: u32 = 2;
217const MIN_PRESENT_US: u64 = if cfg!(target_arch = "wasm32") { 0 } else { 4_000 };
220const STABLE_US: u64 = 100_000;
222const RETRY_US: u64 = 2_000;
224const JOB_US: u64 = 8_000;
226#[cfg(not(target_arch = "wasm32"))]
229const WORKER_STACK: usize = if cfg!(windows) { 2 << 20 } else { 8 << 20 };
230const RETRY_FIRST_MS: u64 = 2_000;
232const RETRY_MAX_MS: u64 = 300_000;
233const BIG_UPLOAD: u64 = 128 << 10;
235const TRIM_US: u64 = 1_200_000;
236const REMOTE_LOOK_US: u64 = 10_000_000;
238const KEPT_US: u64 = 500_000;
240const UPPER: usize = L_COUNT - 1;
242const CELL: i32 = 32;
244const CELL_ROW: usize = CELL as usize * 4;
245const CELL_BYTES: usize = CELL as usize * CELL_ROW;
246const NO_BLOCK: u32 = u32::MAX;
247const LAYER_STEP: i32 = 256;
250const LAYER_SPARE: i32 = 128;
251const LAYER_SHRINK: i64 = 4;
253const LINGER_US: u64 = 1_000_000;
255const IMAGE_BUDGET: u64 = 256 << 20;
257const IMAGE_STEP_BYTES: u64 = 4 << 20;
259const IMAGES_KEPT: usize = 64;
261const IMAGE_UNUSED_US: u64 = 30_000_000;
263const TILE: i32 = 256;
265const TILE_BORDER: i32 = 1;
266const TILE_TEX: i32 = TILE + 2 * TILE_BORDER;
267const TILE_BYTES: u64 = TILE_TEX as u64 * TILE_TEX as u64 * 4;
268const CANVAS_BUDGET: u64 = 128 << 20;
270const FILL_US: u64 = 5_000;
272const SETTLE_US: u64 = 180_000;
274const CANVAS_UNUSED_US: u64 = 30_000_000;
276const K_MIN: f64 = 0.55;
278const K_MAX: f64 = 1.9;
279const PHASE_TOL: f64 = 1.0 / 64.0;
281const LEVELS_MAX: usize = 6;
283const SHOW_TRIES: u32 = 300;
285
286#[cfg(not(target_arch = "wasm32"))]
289mod spv {
290 include!("gpu_out_spv.rs");
291}
292
293pub const GPU_OUT_WGSL: &str = include_str!("gpu_out.wgsl");
295#[cfg(not(target_arch = "wasm32"))]
296pub const GPU_OUT_VS: &[u32] = spv::VS;
297#[cfg(not(target_arch = "wasm32"))]
298pub const GPU_OUT_FS: &[u32] = spv::FS;
299#[cfg(not(target_arch = "wasm32"))]
300pub const GPU_OUT_FS_IMAGE: &[u32] = spv::FS_IMAGE;
301#[cfg(not(target_arch = "wasm32"))]
302pub const GPU_OUT_FS_TILE: &[u32] = spv::FS_TILE;
303
304#[derive(Clone, Copy, PartialEq, Eq, Debug)]
305enum Stage {
306 Cpu,
307 Job,
308 Switch,
309 Gpu,
310}
311
312struct Tex {
313 t: Texture,
314 group: BindGroup,
315 w: i32,
316 h: i32,
317}
318
319#[derive(Default)]
321struct Store {
322 idx: Vec<u32>,
323 px: Vec<u8>,
324 free: Vec<u32>,
325 used: u32,
326}
327
328impl Store {
329 fn has(&self, i: usize) -> bool {
330 self.idx.get(i).is_some_and(|&b| b != NO_BLOCK)
331 }
332
333 fn get(&self, i: usize) -> &[u8] {
334 &self.px[self.idx[i] as usize * CELL_BYTES..][..CELL_BYTES]
335 }
336
337 fn empty(&mut self) {
339 self.idx.clear();
340 self.px.clear();
341 self.free.clear();
342 self.used = 0;
343 }
344
345 fn reserve(&mut self, n: usize) {
347 self.px.reserve_exact((n + self.used as usize).saturating_mul(CELL_BYTES).saturating_sub(self.px.len()));
348 }
349
350 fn put(&mut self, i: usize, cells: usize) -> &mut [u8] {
352 if self.idx.len() != cells {
353 self.empty();
354 self.idx.resize(cells, NO_BLOCK);
355 }
356 let mut b = self.idx[i];
357 if b == NO_BLOCK {
358 b = self.free.pop().unwrap_or_else(|| {
359 let n = (self.px.len() / CELL_BYTES) as u32;
360 self.px.resize(self.px.len() + CELL_BYTES, 0);
361 n
362 });
363 self.idx[i] = b;
364 self.used += 1;
365 }
366 &mut self.px[b as usize * CELL_BYTES..][..CELL_BYTES]
367 }
368
369 fn drop_cell(&mut self, i: usize) {
370 if self.has(i) {
371 self.free.push(self.idx[i]);
372 self.idx[i] = NO_BLOCK;
373 self.used -= 1;
374 }
375 }
376}
377
378struct LayerTex {
380 t: Texture,
381 group: BindGroup,
382 ox: i32,
383 oy: i32,
384 w: i32,
385 h: i32,
386 fresh: bool, }
388
389#[derive(Default)]
392struct Upper {
393 tex: Option<LayerTex>,
394 rects: Vec<[i32; 4]>,
395 store: Store,
396}
397
398struct ImageTex {
400 _t: Texture,
401 group: BindGroup,
402}
403
404struct ImageLoad {
406 t: Texture,
407 group: BindGroup,
408 row: i32,
409}
410
411struct Image {
413 key: usize,
414 weak: Weak<Surface>, ver: u32,
416 bytes: u64,
417 acc: u64,
418 opaque: Option<bool>,
419 tex: Option<Rc<ImageTex>>,
420 load: Option<ImageLoad>,
421 tick: u64,
422 used_us: u64,
423}
424
425struct TileTex {
427 t: Texture,
428 group: BindGroup,
429 fresh: core::cell::Cell<bool>, }
431
432struct Tile {
433 tex: Rc<TileTex>,
434 tick: u64,
435}
436
437struct TileLevel {
439 scale: f64,
440 fx: f64,
441 fy: f64,
442 tiles: BTreeMap<(i32, i32), Tile>,
443}
444
445struct Canvas {
447 win: usize,
448 id: u32,
449 ver: u32,
450 view: CanvasView,
452 prev: CanvasView,
453 known: bool,
454 damaged: bool,
455 apart: bool,
457 until: u64,
458 moved: bool,
459 levels: Vec<TileLevel>,
460}
461
462struct TileDraw {
465 tex: Rc<TileTex>,
466 b: [i32; 4],
467 place: [i32; 4],
468}
469
470enum QuadSrc {
474 Image { tex: Rc<ImageTex>, src: Rc<Surface>, w: i32, h: i32, step: [u32; 2] },
475 Canvas { f: DrawRef, w: i32, h: i32, vis: [i32; 4], tiles: Vec<TileDraw>, copy: bool, loose: bool },
476}
477
478struct Quad {
480 x: i32,
481 y: i32,
482 src: QuadSrc,
483 rects: Vec<[i32; 4]>,
484}
485
486#[derive(Default)]
488struct Split {
489 upper: [Upper; UPPER],
490 active: bool,
492 linger_at: u64,
494 clean: bool,
496 drawn: bool,
498 err: Option<GpuError>,
499 m: Vec<u8>,
502 nr: Vec<u8>,
503 rects: Vec<[i32; 4]>,
504 cols: usize,
507 rows: usize,
508 hole: Vec<u8>,
509 owed: Vec<bool>,
510 stale: Vec<bool>,
511 any_owed: bool,
512 any_stale: bool,
513 quads: Vec<Quad>,
514 part: Vec<bool>,
517 redraw: Vec<bool>,
518 moved: bool,
519}
520
521impl Split {
522 fn reset(&mut self) {
524 *self = Split::default();
525 }
526}
527
528struct Win {
530 pw: i32,
531 stage: Stage,
532 sc: Option<Swapchain>, surf: Option<gpu::Surface>, tex: Option<Tex>,
535 rb: Option<Buffer>, #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
537 rb_px: Vec<u8>,
538 rb_keep: Option<Vec<u8>>, full: bool, undef: bool, pending: bool, hidden: bool, apart_before: bool, last_present: u64,
545 job: u32, lost: bool, size: (i32, i32),
548 stable_at: u64,
549 fail_at: u64, fail_ms: u64,
551 out: GpuOutput,
552 sp: Split,
553 #[cfg(target_arch = "wasm32")]
554 verify_wait: Vec<Verify>,
555}
556
557#[cfg(target_arch = "wasm32")]
559struct Verify {
560 rb: Buffer,
561 fb: Surface,
562 loose: Vec<[i32; 4]>,
563}
564
565#[cfg(target_arch = "wasm32")]
567struct Probe {
568 job: u32,
569 win: usize,
570 core: Core,
571 built: Built,
572 rb: Buffer,
573 px: Vec<u8>,
574 at: u64,
575}
576
577struct Core {
578 inst: Instance,
579 dev: Device,
580 bgl: BindGroupLayout,
581 pipe: RenderPipeline,
582 pipe_image: RenderPipeline,
583 pipe_tile: RenderPipeline,
584 format: Format,
585 name: String,
586}
587
588enum Dev {
589 None,
590 Starting(u32),
591 Ready(Core),
592 Retry(u64),
593 Never(String),
594}
595
596struct Built {
597 surf: gpu::Surface,
598 sc: Swapchain,
599 tex: Texture,
600 group: BindGroup,
601}
602
603enum Done {
604 Device { job: u32, win: usize, r: gpu::Result<(Core, Built)> },
605 Window { job: u32, win: usize, r: gpu::Result<Built> },
606}
607
608struct Jobs {
609 done: SpinLock<Vec<Done>>,
610 running: AtomicU32,
611}
612
613pub(crate) struct S {
615 want: bool,
616 verify: bool,
617 trace: bool,
618 script: bool,
619 skip: bool,
620 dev: Dev,
621 wins: Vec<Option<Win>>,
622 jobs: Arc<Jobs>,
623 threads: Vec<plat::Thread>,
624 next_job: u32,
625 retry_ms: u64,
626 ready_at: u64,
627 trim_at: u64,
628 t0: u64,
629 script_pw: [i32; super::consts::WIN_MAX],
630 script_asked: [(i32, i32); super::consts::WIN_MAX],
631 layers_on: bool,
632 images_on: bool,
633 images: Vec<Image>,
634 image_budget: u64,
635 image_bytes: u64,
636 image_uploads: u64,
637 image_hits: u64,
638 image_evicted: u64,
639 tick: u64,
640 look_at: u64,
641 canvases_on: bool,
642 canvases: Vec<Canvas>,
643 canvas_cpu: String,
644 canvas_pan: String,
645 canvas_zoom: String,
646 canvas_budget: u64,
647 canvas_settle_us: u64,
648 canvas_unused_us: u64,
649 tile_bytes: u64,
650 tile_free: Vec<Rc<TileTex>>,
651 tile_sf: Surface,
652 tile_fills: u64,
653 tile_fill_us: u64,
654 tile_evicted: u64,
655 canvas_settles: u64,
656 canvas_free_at: u64,
657 said_ok: bool, tile_marked: u64, diff_marked: u64, #[cfg(target_arch = "wasm32")]
661 web_inst: Option<Instance>, #[cfg(target_arch = "wasm32")]
663 probe: Option<Probe>,
664}
665
666impl Default for S {
667 fn default() -> S {
668 S {
669 want: false,
670 verify: false,
671 trace: false,
672 script: false,
673 skip: false,
674 dev: Dev::None,
675 wins: Vec::new(),
676 jobs: Arc::new(Jobs { done: SpinLock::new(Vec::new()), running: AtomicU32::new(0) }),
677 threads: Vec::new(),
678 next_job: 1,
679 retry_ms: 0,
680 ready_at: 0,
681 trim_at: 0,
682 t0: 0,
683 script_pw: [-1; super::consts::WIN_MAX],
684 script_asked: [(0, 0); super::consts::WIN_MAX],
685 layers_on: false,
686 images_on: false,
687 images: Vec::new(),
688 image_budget: IMAGE_BUDGET,
689 image_bytes: 0,
690 image_uploads: 0,
691 image_hits: 0,
692 image_evicted: 0,
693 tick: 0,
694 look_at: 0,
695 canvases_on: false,
696 canvases: Vec::new(),
697 canvas_cpu: String::new(),
698 canvas_pan: String::new(),
699 canvas_zoom: String::new(),
700 canvas_budget: CANVAS_BUDGET,
701 canvas_settle_us: SETTLE_US,
702 canvas_unused_us: CANVAS_UNUSED_US,
703 tile_bytes: 0,
704 tile_free: Vec::new(),
705 tile_sf: Surface::default(),
706 tile_fills: 0,
707 tile_fill_us: 0,
708 tile_evicted: 0,
709 canvas_settles: 0,
710 canvas_free_at: 0,
711 said_ok: false,
712 tile_marked: 0,
713 diff_marked: 0,
714 #[cfg(target_arch = "wasm32")]
715 web_inst: None,
716 #[cfg(target_arch = "wasm32")]
717 probe: None,
718 }
719 }
720}
721
722fn env_on(name: &str) -> bool {
723 plat::env(name).is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("on"))
724}
725
726fn wanted() -> bool {
727 let on = match plat::env("jfgui_GPU").as_deref() {
728 Some("0") | Some("off") => false,
729 Some("1") | Some("on") => true,
730 _ if cfg!(target_arch = "wasm32") => DEFAULT_ON_BROWSER && !plat::args().iter().any(|a| a == "--agent"),
732 _ => if cfg!(windows) { DEFAULT_ON } else { DEFAULT_ON_LINUX }, };
734 on && !plat::win_remote_session()
735}
736
737fn ms(a: u64, b: u64) -> f64 {
738 b.saturating_sub(a) as f64 / 1000.0
739}
740
741impl S {
742 fn setup(&mut self) {
743 self.verify = env_on("jfgui_GPU_VERIFY");
744 self.trace = env_on("jfgui_GPU_TRACE");
745 self.t0 = plat::time_us();
746 self.retry_ms = plat::env("jfgui_GPU_RETRY_MS").and_then(|v| v.parse().ok()).unwrap_or(RETRY_FIRST_MS);
747 let off = |name: &str| plat::env(name).is_some_and(|v| v == "0" || v.eq_ignore_ascii_case("off"));
748 self.layers_on = !off("jfgui_GPU_LAYERS");
749 self.images_on = self.layers_on && !off("jfgui_GPU_IMAGES");
750 if let Some(mb) = plat::env("jfgui_GPU_IMAGE_MB").and_then(|v| v.parse::<u64>().ok()) {
751 self.image_budget = mb << 20;
752 }
753 self.canvases_on = self.layers_on && !off("jfgui_GPU_CANVAS");
754 self.canvas_cpu = plat::env("jfgui_GPU_CANVAS_CPU").unwrap_or_default();
755 self.canvas_pan = plat::env("jfgui_GPU_CANVAS_PAN").unwrap_or_default();
756 self.canvas_zoom = plat::env("jfgui_GPU_CANVAS_ZOOM").unwrap_or_default();
757 if let Some(mb) = plat::env("jfgui_GPU_CANVAS_MB").and_then(|v| v.parse::<u64>().ok()) {
758 self.canvas_budget = mb << 20;
759 }
760 if let Some(ms) = plat::env("jfgui_GPU_CANVAS_SETTLE_MS").and_then(|v| v.parse::<u64>().ok()) {
761 self.canvas_settle_us = ms * 1000;
762 }
763 }
764
765 fn canvas_mode(&self, name: &str, gpu: CanvasGpu) -> CanvasGpu {
767 let named = |list: &str| list.split(',').any(|n| n == name);
768 if named(&self.canvas_cpu) {
769 CanvasGpu::Cpu
770 } else if named(&self.canvas_zoom) {
771 CanvasGpu::Zoom
772 } else if named(&self.canvas_pan) {
773 CanvasGpu::Pan
774 } else {
775 gpu
776 }
777 }
778
779 fn canvases_gone(&mut self) {
781 for c in self.canvases.iter_mut() {
782 c.levels.clear();
783 c.apart = false;
784 c.moved = false;
785 }
786 self.tile_free.clear();
787 self.tile_bytes = 0;
788 self.tile_sf = Surface::default();
789 self.canvas_free_at = 0;
790 }
791
792 fn tile_texture(&mut self, tick: u64) -> Option<Rc<TileTex>> {
795 if self.tile_bytes + TILE_BYTES <= self.canvas_budget {
796 if let Some(k) = self.tile_free.iter().position(|t| Rc::strong_count(t) == 1) {
797 return Some(self.tile_free.swap_remove(k));
798 }
799 }
800 while self.tile_bytes + TILE_BYTES > self.canvas_budget {
802 if self.tile_free.pop().is_some() {
803 self.tile_bytes -= TILE_BYTES; continue;
805 }
806 let mut old: Option<(usize, usize, (i32, i32), u64)> = None;
807 for (ci, c) in self.canvases.iter().enumerate() {
808 for (li, l) in c.levels.iter().enumerate() {
809 for (key, t) in l.tiles.iter() {
810 if t.tick != tick && old.is_none_or(|o| t.tick < o.3) {
811 old = Some((ci, li, *key, t.tick));
812 }
813 }
814 }
815 }
816 let (ci, li, key, _) = old?;
817 self.canvases[ci].levels[li].tiles.remove(&key)?;
818 self.tile_evicted += 1;
819 self.tile_bytes -= TILE_BYTES;
820 }
821 let Dev::Ready(core) = &self.dev else { return None };
822 let t = core.dev.create_texture(&TextureDesc::d2(TILE_TEX as u32, TILE_TEX as u32, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED)).ok()?;
823 let group = core.dev.create_bind_group(&BindGroupDesc {
824 layout: &core.bgl,
825 entries: &[BindEntry { binding: 0, resource: Resource::Texture(t.view()) }],
826 }).ok()?;
827 self.tile_bytes += TILE_BYTES;
828 Some(Rc::new(TileTex { t, group, fresh: core::cell::Cell::new(true) }))
829 }
830
831 fn tiles_free(&mut self, tiles: BTreeMap<(i32, i32), Tile>) {
833 self.tile_free.extend(tiles.into_values().map(|t| t.tex));
834 }
835
836 fn evict(&mut self, k: usize) {
838 let e = &mut self.images[k];
839 if e.tex.take().is_some() | e.load.take().is_some() {
840 self.image_bytes -= e.bytes;
841 self.image_evicted += 1;
842 self.trim_at = plat::time_us() + TRIM_US;
843 }
844 }
845
846 fn images_gone(&mut self) {
848 for e in self.images.iter_mut() {
849 e.tex = None;
850 e.load = None;
851 }
852 self.image_bytes = 0;
853 }
854
855 fn desc(&self) -> SwapchainDesc {
856 SwapchainDesc { present: PRESENT, copy_src: self.verify, min_images: IMAGES, ..SwapchainDesc::DEFAULT }
857 }
858
859 fn note(&self, what: &str) {
860 if self.trace {
861 plat::write_stderr(format!("gpu: {:9.2} ms {what}\n", ms(self.t0, plat::time_us())).as_bytes());
862 }
863 }
864
865 fn win(&mut self, i: usize) -> Option<&mut Win> {
866 self.wins.get_mut(i).and_then(|w| w.as_mut())
867 }
868
869 #[cfg(target_arch = "wasm32")]
871 fn spawn(&mut self, f: impl FnOnce() -> Done + 'static) -> bool {
872 let d = f();
873 self.jobs.done.lock().push(d);
874 true
875 }
876
877 #[cfg(target_arch = "wasm32")]
881 fn web_start(&mut self) -> bool {
882 if self.probe.is_some() {
883 self.web_probe();
884 return true;
885 }
886 if !matches!(self.dev, Dev::None | Dev::Starting(WEB_PREPARING)) {
887 return matches!(self.dev, Dev::Starting(_));
888 }
889 let Some((i, pw)) = self.wins.iter().enumerate().find_map(|(i, w)| w.as_ref().filter(|w| w.stage == Stage::Cpu).map(|w| (i, w.pw))) else {
890 return true;
891 };
892 if self.web_inst.is_none() {
893 self.note("start: adapter and device asked for");
894 match Instance::new() {
895 Ok(inst) => {
896 self.web_inst = Some(inst);
897 self.dev = Dev::Starting(WEB_PREPARING);
898 }
899 Err(e) => {
900 self.note(&format!("no device: {e}"));
901 if let Some(w) = self.win(i) {
902 w.out.error = Some(format!("{e}"));
903 }
904 self.dev = Dev::Never(format!("{e}"));
905 return true;
906 }
907 }
908 }
909 let inst = self.web_inst.as_ref().map(|x| x.prepare());
910 match inst {
911 Some(Ok(true)) => {}
912 Some(Err(e)) => {
913 self.note(&format!("no device: {e}"));
914 self.web_inst = None;
915 if let Some(w) = self.win(i) {
916 w.out.error = Some(format!("{e}"));
917 }
918 self.dev = if matches!(e, GpuError::Failed(_)) {
919 let at = plat::time_us() + self.retry_ms * 1000;
920 self.retry_ms = (self.retry_ms * 2).min(RETRY_MAX_MS);
921 Dev::Retry(at)
922 } else {
923 Dev::Never(format!("{e}")) };
925 return true;
926 }
927 _ => return true,
928 }
929 let Some(nw) = plat::win_gpu_child(pw) else { return true };
931 let job = self.next_job;
932 self.next_job += 1;
933 let desc = self.desc();
934 let trace = self.trace;
935 let t0 = plat::time_us();
936 let r = build_device(self.web_inst.take(), nw, pw, None, desc, trace);
937 self.note(&format!("device, pipelines, swapchain: {:.2} ms on this thread", ms(t0, plat::time_us())));
938 if let Some(w) = self.win(i) {
939 w.stage = Stage::Job;
940 w.job = job;
941 }
942 self.dev = Dev::Starting(job);
943 match r.and_then(|(core, built)| {
944 let (rb, px) = probe_start(&core)?;
945 Ok(Probe { job, win: i, core, built, rb, px, at: plat::time_us() })
946 }) {
947 Ok(p) => self.probe = Some(p),
948 Err(e) => self.jobs.done.lock().push(Done::Device { job, win: i, r: Err(e) }),
949 }
950 true
951 }
952
953 #[cfg(target_arch = "wasm32")]
955 fn web_probe(&mut self) {
956 let Some(p) = &self.probe else { return };
957 let mut got = vec![0u8; p.px.len()];
958 let r = match p.rb.read(0, &mut got) {
959 Err(GpuError::NotReady) if plat::time_us() < p.at + PROBE_US => return,
960 Err(GpuError::NotReady) => Err(GpuError::Timeout),
961 Err(e) => Err(e),
962 Ok(()) if got == p.px => Ok(()),
963 Ok(()) => Err(GpuError::Unsupported("the browser's WebGPU draws other pixels")),
964 };
965 let Some(p) = self.probe.take() else { return };
966 self.note(&format!("the small picture: {:?}, {:.2} ms", r, ms(p.at, plat::time_us())));
967 let (job, win) = (p.job, p.win);
968 self.jobs.done.lock().push(Done::Device { job, win, r: r.map(|_| (p.core, p.built)) });
969 }
970
971 #[cfg(not(target_arch = "wasm32"))]
973 fn spawn(&mut self, f: impl FnOnce() -> Done + Send + 'static) -> bool {
974 let jobs = self.jobs.clone();
975 jobs.running.fetch_add(1, Ordering::AcqRel);
976 let j = jobs.clone();
977 let stack = plat::env("jfgui_GPU_WORKER_KB").and_then(|v| v.parse::<usize>().ok()).map_or(WORKER_STACK, |k| k << 10);
979 let t = plat::gpu_thread_spawn(stack, Box::new(move || {
980 let d = f();
981 j.done.lock().push(d);
982 j.running.fetch_sub(1, Ordering::AcqRel);
983 }));
984 match t {
985 Some(t) => {
986 self.threads.push(t);
987 true
988 }
989 None => {
990 jobs.running.fetch_sub(1, Ordering::AcqRel);
991 false
992 }
993 }
994 }
995}
996
997#[cfg(target_arch = "wasm32")]
1001const WEB_PREPARING: u32 = 0;
1002#[cfg(target_arch = "wasm32")]
1004const PROBE_US: u64 = 5_000_000;
1005
1006#[cfg(target_arch = "wasm32")]
1009fn probe_start(core: &Core) -> gpu::Result<(Buffer, Vec<u8>)> {
1010 const N: u32 = 8;
1011 const ROW: usize = 256;
1012 let dev = &core.dev;
1013 let src = dev.create_texture(&TextureDesc::d2(N, N, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED))?;
1014 let group = dev.create_bind_group(&BindGroupDesc {
1015 layout: &core.bgl,
1016 entries: &[BindEntry { binding: 0, resource: Resource::Texture(src.view()) }],
1017 })?;
1018 let target = dev.create_texture(&TextureDesc::d2(N, N, core.format, TextureUsage::COLOR_TARGET | TextureUsage::COPY_SRC))?;
1019 let rb = dev.create_buffer(&BufferDesc { size: (ROW * N as usize) as u64, usage: BufferUsage::NONE, memory: MemoryKind::Readback })?;
1020 let mut px = vec![0u8; (N * N * 4) as usize];
1022 let mut want = vec![0u8; ROW * N as usize];
1023 for y in 0..N as usize {
1024 for x in 0..N as usize {
1025 let t = [(x * 30 + 1) as u8, (y * 30 + 2) as u8, ((x ^ y) * 17 + 3) as u8, 0x55];
1026 px[(y * N as usize + x) * 4..][..4].copy_from_slice(&t);
1027 let rgba = core.format == Format::Rgba8Unorm;
1028 let c = if rgba { [t[2], t[1], t[0], 255] } else { [t[0], t[1], t[2], 255] };
1029 want[y * ROW + x * 4..][..4].copy_from_slice(&c);
1030 }
1031 }
1032 dev.queue().write_texture_rects(&src, &[[0, 0, N as i32, N as i32]], &px, N * 4, Layout::Undefined, Layout::Sampled)?;
1033 let mut e = dev.encoder()?;
1034 e.begin_render(&RenderPassDesc {
1035 color: &[ColorAttachment { view: target.view(), load: Load::Discard, clear: [0.0; 4], store: true, before: Layout::Undefined, after: Layout::CopySrc }],
1036 depth: None,
1037 });
1038 let half = N / 2;
1039 e.set_render_pipeline(&core.pipe);
1040 e.set_bind_group(0, &group);
1041 e.push_constants(0, &[0i32; 4]);
1042 e.set_scissor(0, 0, N, half);
1043 e.draw(3, 1, 0, 0);
1044 e.set_render_pipeline(&core.pipe_image);
1045 e.set_bind_group(0, &group);
1046 e.push_constants(0, &[0i32, 0, 65536, 65536]);
1047 e.set_scissor(0, half, half, half);
1048 e.draw(3, 1, 0, 0);
1049 e.set_render_pipeline(&core.pipe_tile);
1050 e.set_bind_group(0, &group);
1051 e.set_scissor(half, half, half, half);
1052 e.draw(3, 1, 0, 0);
1053 e.end_render();
1054 e.copy_texture_to_buffer(TextureRegion::mip(&target, 0), &rb, DataLayout { offset: 0, bytes_per_row: ROW as u32, rows_per_image: 0 });
1055 dev.queue().submit([e.finish()?])?;
1056 Ok((rb, want))
1057}
1058
1059fn build_window(dev: &Device, bgl: &BindGroupLayout, surf: gpu::Surface, old: Option<Swapchain>, desc: &SwapchainDesc) -> gpu::Result<Built> {
1061 let sc = match old {
1062 Some(mut sc) => {
1063 sc.rebuild()?;
1064 sc
1065 }
1066 None => dev.create_swapchain(&surf, desc)?,
1067 };
1068 let (w, h) = sc.size();
1069 if w == 0 || h == 0 {
1070 return Err(GpuError::NotReady); }
1072 let tex = dev.create_texture(&TextureDesc::d2(w, h, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED))?;
1073 let group = dev.create_bind_group(&BindGroupDesc {
1074 layout: bgl,
1075 entries: &[BindEntry { binding: 0, resource: Resource::Texture(tex.view()) }],
1076 })?;
1077 Ok(Built { surf, sc, tex, group })
1078}
1079
1080fn build_device(prepared: Option<Instance>, nw: plat::NativeWindow, pw: i32, display: Option<(u32, u32)>, desc: SwapchainDesc, trace: bool) -> gpu::Result<(Core, Built)> {
1082 let t0 = plat::time_us();
1083 #[cfg(target_os = "android")]
1084 if env_on("jfgui_GPU_TEST_DIE") {
1085 rt::os::exit(3); }
1087 let inst = match prepared {
1088 Some(inst) => inst, None => Instance::new()?,
1090 };
1091 let t1 = plat::time_us();
1092 let surf = gpu::Surface::native(&inst, nw, pw)?;
1093 let list: Vec<Adapter> = inst.adapters_low_power(display)?;
1094 let adapter = list.into_iter().find(|a| a.can_present(&surf)).ok_or(GpuError::NoAdapter)?;
1095 let t2 = plat::time_us();
1096 let dev = adapter.create_device(&DeviceDesc { max_level: Level::Basis })?;
1097 let t3 = plat::time_us();
1098 let bgl = dev.create_bind_group_layout(&[Binding { binding: 0, kind: BindingKind::Texture, stages: Stages::FRAGMENT }])?;
1099 let built = build_window(&dev, &bgl, surf, None, &desc)?;
1100 let t4 = plat::time_us();
1101 let format = built.sc.format();
1102 #[cfg(not(target_arch = "wasm32"))]
1103 let (vs, fs, fs_image, fs_tile) = (ShaderModule::from_spirv(&dev, spv::VS)?, ShaderModule::from_spirv(&dev, spv::FS)?,
1104 ShaderModule::from_spirv(&dev, spv::FS_IMAGE)?, ShaderModule::from_spirv(&dev, spv::FS_TILE)?);
1105 #[cfg(target_arch = "wasm32")]
1107 let (vs, fs, fs_image, fs_tile) = {
1108 let m = ShaderModule::from_wgsl(&dev, GPU_OUT_WGSL)?;
1109 (m.clone(), m.clone(), m.clone(), m)
1110 };
1111 let layout = dev.create_pipeline_layout(&PipelineLayoutDesc { groups: &[&bgl], push_constants: 16 })?;
1112 let pipeline = |fragment: &'_ ShaderModule, entry: &'static str| {
1113 dev.create_render_pipeline(&RenderPipelineDesc {
1114 layout: &layout,
1115 vertex: ShaderStage::new(&vs, "vs"),
1116 fragment: Some(ShaderStage::new(fragment, entry)),
1117 buffers: &[],
1118 topology: Topology::Triangles,
1119 cull: Cull::None,
1120 front_cw: false,
1121 targets: &[ColorTarget { format, blend: None, write_mask: 0xF }],
1122 depth: None,
1123 })
1124 };
1125 let pipe = pipeline(&fs, "fs")?;
1126 let pipe_image = pipeline(&fs_image, "fs_image")?;
1127 let pipe_tile = pipeline(&fs_tile, "fs_tile")?;
1128 let t5 = plat::time_us();
1129 if trace {
1130 plat::write_stderr(format!(
1131 "gpu: worker: instance {:.2} ms, adapter {:.2}, device {:.2}, swapchain + texture {:.2}, pipeline {:.2} ({})\n",
1132 ms(t0, t1), ms(t1, t2), ms(t2, t3), ms(t3, t4), ms(t4, t5), adapter.info().name).as_bytes());
1133 }
1134 let name = adapter.info().name.clone();
1135 Ok((Core { inst, dev, bgl, pipe, pipe_image, pipe_tile, format, name }, built))
1136}
1137
1138fn upload(w: &mut Win, core: &Core, fb: &Surface, rects: Option<&[[i32; 4]]>, trim_at: &mut u64) -> gpu::Result<()> {
1142 let Some(tex) = &w.tex else { return Ok(()) };
1143 let all = [[0, 0, fb.w, fb.h]];
1144 let (rs, before) = match rects {
1145 Some(r) if !w.full && !w.undef => (r, Layout::Sampled),
1146 _ => (&all[..], Layout::Undefined),
1148 };
1149 core.dev.queue().write_texture_rects(&tex.t, rs, &fb.px, fb.stride as u32, before, Layout::Sampled)?;
1150 let bytes: u64 = rs.iter().map(|r| r[2].max(0) as u64 * r[3].max(0) as u64 * 4).sum();
1151 w.out.upload_bytes += bytes;
1152 w.out.upload_base += bytes;
1153 if let Some(sc) = &w.sc {
1154 sc.damage(if rects.is_some() && before == Layout::Sampled { Some(rs) } else { None });
1155 }
1156 if bytes > BIG_UPLOAD {
1157 *trim_at = plat::time_us() + TRIM_US;
1158 }
1159 w.full = false;
1160 w.undef = false;
1161 w.pending = true;
1162 Ok(())
1163}
1164
1165enum Shown {
1166 Done,
1167 Wait,
1169 NoArea,
1170 Stale,
1171}
1172
1173fn show(w: &mut Win, core: &Core, fb: &Surface) -> gpu::Result<Shown> {
1175 let (Some(sc), Some(tex)) = (w.sc.as_mut(), w.tex.as_ref()) else { return Ok(Shown::Stale) };
1176 let fr = match sc.try_acquire()? {
1177 TryAcquire::Frame(f) => f,
1178 TryAcquire::NotReady => return Ok(Shown::Wait),
1179 TryAcquire::NoArea => return Ok(Shown::NoArea),
1180 TryAcquire::Stale => return Ok(Shown::Stale),
1181 };
1182 let sc = &*sc;
1183 #[cfg(target_arch = "wasm32")]
1185 let rb_frame = match &w.rb {
1186 Some(_) => Some(core.dev.create_buffer(&BufferDesc { size: (fb.w as u64 * 4 + 255 & !255) * fb.h as u64, usage: BufferUsage::NONE, memory: MemoryKind::Readback })?),
1187 None => None,
1188 };
1189 #[cfg(target_arch = "wasm32")]
1190 let rb_now = rb_frame.as_ref();
1191 #[cfg(not(target_arch = "wasm32"))]
1192 let rb_now = w.rb.as_ref();
1193 let row_bytes = if cfg!(target_arch = "wasm32") { (fb.w as u32 * 4 + 255) & !255 } else { 0 };
1194 let mut e = core.dev.encoder()?;
1195 let after = if rb_now.is_some() { Layout::CopySrc } else { Layout::Present };
1196 e.begin_render(&RenderPassDesc {
1197 color: &[ColorAttachment { view: sc.view(&fr), load: Load::Discard, clear: [0.0; 4], store: true, before: Layout::Undefined, after }],
1198 depth: None,
1199 });
1200 e.set_render_pipeline(&core.pipe);
1201 e.set_bind_group(0, &tex.group);
1202 e.push_constants(0, &[0i32; 4]);
1203 e.draw(3, 1, 0, 0);
1204 let scissor = |e: &mut gpu::CommandEncoder, r: &[i32; 4]| e.set_scissor(r[0] as u32, r[1] as u32, r[2] as u32, r[3] as u32);
1205 if !w.sp.quads.is_empty() {
1207 for q in &w.sp.quads {
1208 match &q.src {
1209 QuadSrc::Image { tex, step, .. } => {
1210 e.set_render_pipeline(&core.pipe_image);
1211 e.set_bind_group(0, &tex.group);
1212 e.push_constants(0, &[q.x, q.y, step[0] as i32, step[1] as i32]);
1213 for r in &q.rects {
1214 scissor(&mut e, r);
1215 e.draw(3, 1, 0, 0);
1216 }
1217 }
1218 QuadSrc::Canvas { tiles, copy, .. } => {
1219 e.set_render_pipeline(if *copy { &core.pipe } else { &core.pipe_tile });
1220 for t in tiles {
1221 let mut bound = false;
1222 for r in &q.rects {
1223 let (x0, y0) = (t.b[0].max(r[0]), t.b[1].max(r[1]));
1224 let (x1, y1) = (t.b[2].min(r[0] + r[2]), t.b[3].min(r[1] + r[3]));
1225 if x0 < x1 && y0 < y1 {
1226 if !bound {
1227 bound = true;
1228 e.set_bind_group(0, &t.tex.group);
1229 e.push_constants(0, &t.place);
1230 }
1231 scissor(&mut e, &[x0, y0, x1 - x0, y1 - y0]);
1232 e.draw(3, 1, 0, 0);
1233 }
1234 }
1235 }
1236 }
1237 }
1238 }
1239 e.set_render_pipeline(&core.pipe);
1240 }
1241 for u in w.sp.upper.iter().filter(|u| !u.rects.is_empty()) {
1243 if let Some(t) = &u.tex {
1244 e.set_bind_group(0, &t.group);
1245 e.push_constants(0, &[t.ox, t.oy, 0, 0]);
1246 for r in &u.rects {
1247 scissor(&mut e, r);
1248 e.draw(3, 1, 0, 0);
1249 }
1250 }
1251 }
1252 e.end_render();
1253 if let Some(rb) = rb_now {
1254 let img = sc.texture(&fr);
1255 e.copy_texture_to_buffer(TextureRegion::mip(img, 0), rb, DataLayout { bytes_per_row: row_bytes, ..DataLayout::default() });
1256 e.texture_barrier(img, Layout::CopySrc, Layout::Present);
1257 e.barrier(Access::COPY_WRITE, Access::HOST_READ);
1258 }
1259 let cmd = e.finish()?;
1260 let id = sc.present(fr, [cmd])?;
1261 w.pending = false;
1262 w.last_present = plat::time_us();
1263 w.out.frames += 1;
1264 #[cfg(target_arch = "wasm32")]
1265 {
1266 let _ = id;
1267 if let Some(rb) = rb_frame {
1268 let mut snap = Surface::new(fb.w, fb.h, FMT_XRGB);
1269 let n = snap.px.len();
1270 snap.px.copy_from_slice(&fb.px[..n]);
1271 let loose = w.sp.quads.iter().filter(|q| matches!(q.src, QuadSrc::Canvas { loose: true, .. })).flat_map(|q| q.rects.iter().copied()).collect();
1272 w.verify_wait.push(Verify { rb, fb: snap, loose });
1273 }
1274 }
1275 #[cfg(not(target_arch = "wasm32"))]
1276 if let Some(rb) = &w.rb {
1277 core.dev.wait(id, 5_000_000_000)?;
1278 sc.flush()?;
1279 let n = fb.w as usize * fb.h as usize * 4;
1280 w.rb_px.resize(n, 0);
1281 rb.read(0, &mut w.rb_px[..n])?;
1282 let swap_rb = core.format == Format::Rgba8Unorm;
1283 if let Some(keep) = w.rb_keep.as_mut() {
1284 keep.clear();
1285 keep.extend_from_slice(&w.rb_px[..n]);
1286 }
1287 for q in &w.sp.quads {
1289 if matches!(q.src, QuadSrc::Canvas { loose: true, .. }) {
1290 loose_compare(&mut w.rb_px, fb, &q.rects, swap_rb, &mut w.out);
1291 }
1292 }
1293 let (x, y, bad) = compare(&w.rb_px, fb, swap_rb);
1294 w.out.verified += 1;
1295 if bad != 0 {
1296 w.out.verify_bad += 1;
1297 plat::write_stderr(format!("gpu: the presented image differs from the framebuffer in {bad} pixels, first at {x}, {y}\n").as_bytes());
1298 }
1299 }
1300 Ok(Shown::Done)
1301}
1302
1303#[cfg(target_arch = "wasm32")]
1305fn verify_poll(w: &mut Win, core: &Core) {
1306 let swap_rb = core.format == Format::Rgba8Unorm;
1307 while let Some(v) = w.verify_wait.first_mut() {
1308 let row = v.fb.w as usize * 4;
1309 let padded = (row + 255) & !255;
1310 let mut raw = vec![0u8; padded * v.fb.h as usize];
1311 match v.rb.read(0, &mut raw) {
1312 Err(GpuError::NotReady) => return,
1313 Err(_) => {
1314 w.verify_wait.clear();
1315 return;
1316 }
1317 Ok(()) => {}
1318 }
1319 let mut img: Vec<u8> = Vec::with_capacity(row * v.fb.h as usize);
1320 for r in raw.chunks_exact(padded) {
1321 img.extend_from_slice(&r[..row]);
1322 }
1323 loose_compare(&mut img, &v.fb, &v.loose, swap_rb, &mut w.out);
1324 let (x, y, bad) = compare(&img, &v.fb, swap_rb);
1325 w.out.verified += 1;
1326 if bad != 0 {
1327 w.out.verify_bad += 1;
1328 plat::write_stderr(format!("gpu: the presented image differs from the framebuffer in {bad} pixels, first at {x}, {y}\n").as_bytes());
1329 }
1330 w.verify_wait.remove(0);
1331 }
1332}
1333
1334fn compare(img: &[u8], fb: &Surface, swap_rb: bool) -> (i32, i32, u64) {
1338 let (mut fx, mut fy, mut bad) = (0, 0, 0u64);
1339 let row = fb.w as usize * 4;
1340 for y in 0..fb.h as usize {
1341 let a = &img[y * row..y * row + row];
1342 let b = &fb.px[y * fb.stride as usize..y * fb.stride as usize + row];
1343 for (x, (p, q)) in a.chunks_exact(4).zip(b.chunks_exact(4)).enumerate() {
1345 let same = if swap_rb { p[0] == q[2] && p[1] == q[1] && p[2] == q[0] } else { p[..3] == q[..3] };
1346 if !same || p[3] != 255 {
1347 if bad == 0 {
1348 fx = x as i32;
1349 fy = y as i32;
1350 }
1351 bad += 1;
1352 }
1353 }
1354 }
1355 (fx, fy, bad)
1356}
1357
1358fn loose_compare(img: &mut [u8], fb: &Surface, rects: &[[i32; 4]], swap_rb: bool, out: &mut GpuOutput) {
1361 let row = fb.w as usize * 4;
1362 let (mut n, mut sum, mut top) = (0u64, 0u64, 0u32);
1363 for r in rects {
1364 for y in r[1] as usize..(r[1] + r[3]) as usize {
1365 let a = &mut img[y * row + r[0] as usize * 4..y * row + (r[0] + r[2]) as usize * 4];
1366 let b = &fb.px[y * fb.stride as usize + r[0] as usize * 4..y * fb.stride as usize + (r[0] + r[2]) as usize * 4];
1367 for (p, q) in a.chunks_exact_mut(4).zip(b.chunks_exact(4)) {
1368 let c = if swap_rb { [q[2], q[1], q[0]] } else { [q[0], q[1], q[2]] };
1369 let d = (0..3).map(|i| p[i].abs_diff(c[i]) as u32).max().unwrap_or(0);
1370 if d != 0 {
1371 n += 1;
1372 sum += d as u64;
1373 top = top.max(d);
1374 p[..3].copy_from_slice(&c);
1375 }
1376 }
1377 }
1378 }
1379 out.canvas_diff_frames += (n != 0) as u64;
1380 out.canvas_diff_px += n;
1381 out.canvas_diff_sum += sum;
1382 out.canvas_diff_max = out.canvas_diff_max.max(top);
1383}
1384
1385fn flr(x: f64) -> f64 {
1387 let t = x as i64 as f64;
1388 if t > x { t - 1.0 } else { t }
1389}
1390
1391fn rnd(x: f64) -> f64 {
1392 flr(x + 0.5)
1393}
1394
1395fn level_for(c: &mut Canvas, v: CanvasView, zoom: bool) -> usize {
1399 let zoom = zoom && c.prev.scale != v.scale;
1400 let near = |l: &TileLevel, v: CanvasView| {
1401 let (dx, dy) = (v.ox - l.fx, v.oy - l.fy);
1402 (dx - rnd(dx)).abs() < PHASE_TOL && (dy - rnd(dy)).abs() < PHASE_TOL
1403 };
1404 let mut best: Option<(usize, f64)> = None;
1405 for (n, l) in c.levels.iter().enumerate() {
1406 let k = v.scale / l.scale;
1407 if k == 1.0 {
1408 if near(l, v) {
1409 return n;
1410 }
1411 } else if zoom && (K_MIN..=K_MAX).contains(&k) {
1412 let far = if k < 1.0 { 1.0 / k } else { k };
1413 if best.is_none_or(|b| far < b.1) {
1414 best = Some((n, far));
1415 }
1416 }
1417 }
1418 if let Some(b) = best {
1419 return b.0;
1420 }
1421 let p = c.prev;
1422 let from = if zoom && p.scale > 0.0 && (K_MIN..=K_MAX).contains(&(v.scale / p.scale)) { p } else { v };
1423 c.levels.push(TileLevel { scale: from.scale, fx: from.ox - flr(from.ox), fy: from.oy - flr(from.oy), tiles: BTreeMap::new() });
1424 c.levels.len() - 1
1425}
1426
1427fn cell_box(i: usize, cols: usize, w: i32, h: i32) -> [i32; 4] {
1431 let (c, r) = ((i % cols) as i32, (i / cols) as i32);
1432 [c * CELL, r * CELL, ((c + 1) * CELL).min(w), ((r + 1) * CELL).min(h)]
1433}
1434
1435fn cell_save(fb: &Surface, b: [i32; 4], to: &mut [u8]) {
1436 let n = (b[2] - b[0]) as usize * 4;
1437 for y in b[1]..b[3] {
1438 let s = y as usize * fb.stride as usize + b[0] as usize * 4;
1439 let d = (y - b[1]) as usize * CELL_ROW;
1440 to[d..d + n].copy_from_slice(&fb.px[s..s + n]);
1441 }
1442}
1443
1444fn cell_load(fb: &mut Surface, b: [i32; 4], from: &[u8]) {
1445 let n = (b[2] - b[0]) as usize * 4;
1446 for y in b[1]..b[3] {
1447 let d = y as usize * fb.stride as usize + b[0] as usize * 4;
1448 let s = (y - b[1]) as usize * CELL_ROW;
1449 fb.px[d..d + n].copy_from_slice(&from[s..s + n]);
1450 }
1451}
1452
1453fn cell_rects(out: &mut Vec<[i32; 4]>, cols: usize, rows: usize, w: i32, h: i32, f: impl Fn(usize) -> bool) {
1456 out.clear();
1457 for row in 0..rows {
1458 let mut c = 0;
1459 while c < cols {
1460 if !f(row * cols + c) {
1461 c += 1;
1462 continue;
1463 }
1464 let mut e = c + 1;
1465 while e < cols && f(row * cols + e) {
1466 e += 1;
1467 }
1468 let (x, rw, y) = (c as i32 * CELL, (e as i32 * CELL).min(w) - c as i32 * CELL, row as i32 * CELL);
1469 let rh = (y + CELL).min(h) - y;
1470 if let Some(r) = out.iter_mut().find(|r| r[0] == x && r[2] == rw && r[1] + r[3] == y) {
1471 r[3] += rh;
1472 } else {
1473 out.push([x, y, rw, rh]);
1474 }
1475 c = e;
1476 }
1477 }
1478}
1479
1480fn quad_blit(fb: &mut Surface, q: &Quad, b: [i32; 4], text: &mut Text) {
1482 let mut dc = Dc::new(fb.w, fb.h);
1483 dc.clip_push(b[0], b[1], b[2] - b[0], b[3] - b[1]);
1484 match &q.src {
1485 QuadSrc::Image { src, w, h, .. } => dc.blit_scaled(fb, src, q.x, q.y, *w, *h),
1486 QuadSrc::Canvas { f, w, h, vis, .. } => {
1487 dc.clip_push(vis[0], vis[1], vis[2] - vis[0], vis[3] - vis[1]);
1488 dc.clip_push(q.x, q.y, *w, *h);
1489 f.draw(&mut Gfx { dc: &mut dc, sf: fb, text }, [q.x, q.y, *w, *h]);
1490 dc.clip_pop();
1491 dc.clip_pop();
1492 }
1493 }
1494}
1495
1496fn surface_opaque(sf: &Surface) -> bool {
1498 if sf.fmt == FMT_XRGB {
1499 return true;
1500 }
1501 let (row, stride) = (sf.w as usize * 4, sf.stride as usize);
1502 sf.fmt != FMT_A8 && sf.px.chunks(stride).take(sf.h as usize).all(|r| r[..row].chunks_exact(4).all(|p| p[3] == 255))
1503}
1504
1505fn image_texture(core: &Core, sf: &Surface) -> gpu::Result<ImageLoad> {
1507 let t = core.dev.create_texture(&TextureDesc::d2(sf.w as u32, sf.h as u32, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED))?;
1508 let group = core.dev.create_bind_group(&BindGroupDesc {
1509 layout: &core.bgl,
1510 entries: &[BindEntry { binding: 0, resource: Resource::Texture(t.view()) }],
1511 })?;
1512 Ok(ImageLoad { t, group, row: 0 })
1513}
1514
1515fn layer_texture(core: &Core, bb: [i32; 4], fw: i32, fh: i32) -> gpu::Result<LayerTex> {
1517 let size = |n: i32, lim: i32| ((n + LAYER_SPARE + LAYER_STEP - 1) / LAYER_STEP * LAYER_STEP).min(lim);
1518 let (w, h) = (size(bb[2] - bb[0], fw), size(bb[3] - bb[1], fh));
1519 let ox = (bb[0] - (w - (bb[2] - bb[0])) / 2).clamp(0, fw - w);
1520 let oy = (bb[1] - (h - (bb[3] - bb[1])) / 2).clamp(0, fh - h);
1521 let t = core.dev.create_texture(&TextureDesc::d2(w as u32, h as u32, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED))?;
1522 let group = core.dev.create_bind_group(&BindGroupDesc {
1523 layout: &core.bgl,
1524 entries: &[BindEntry { binding: 0, resource: Resource::Texture(t.view()) }],
1525 })?;
1526 Ok(LayerTex { t, group, ox, oy, w, h, fresh: true })
1527}
1528
1529impl Ui {
1532 pub(crate) fn gp_window_mode(&mut self) {
1534 let s = &mut self.m.gout;
1535 s.want = wanted();
1536 s.setup();
1537 }
1538
1539 pub(crate) fn gp_present(&mut self, cur: u32, pw: i32, n: i32) -> bool {
1541 if !self.m.gout.want {
1542 return false;
1543 }
1544 self.gp_poll();
1545 let t0 = plat::time_us();
1546 let i = cur as usize;
1547 if self.m.gout.verify {
1548 self.gp_settle(i); }
1550 let s = &mut self.m.gout;
1551 let Dev::Ready(core) = &s.dev else { return false };
1552 let Some(Some(w)) = s.wins.get_mut(i) else { return false };
1553 if w.stage != Stage::Gpu {
1554 return false;
1555 }
1556 if let Some(e) = w.sp.err.take() {
1557 self.gp_failed(i, e);
1558 return true;
1559 }
1560 let fb = &self.wp.rt.fb;
1561 let fits = w.tex.as_ref().is_some_and(|t| (t.w, t.h) == (fb.w, fb.h));
1562 if !fits {
1563 self.gp_leave(i, "the size changed");
1564 return true;
1565 }
1566 if plat::win_state(pw) & plat::WST_MINIMIZED != 0 {
1567 self.gp_flatten(i); return true;
1569 }
1570 let rects = if n < 0 { None } else { Some(&self.drects[..n as usize]) };
1571 let soon = (t0 < w.last_present + MIN_PRESENT_US && !s.script) || cfg!(target_arch = "wasm32");
1574 let apart = w.sp.drawn || w.sp.active || !w.sp.quads.is_empty();
1576 let before = core::mem::replace(&mut w.apart_before, apart);
1577 if apart || before {
1578 if let Some(sc) = &w.sc {
1579 sc.damage(None);
1580 }
1581 }
1582 let up = if core::mem::take(&mut w.sp.drawn) { Ok(()) } else { upload(w, core, fb, rects, &mut s.trim_at) };
1584 let r = up.and_then(|_| if soon { Ok(Shown::Wait) } else { show(w, core, fb) });
1585 w.out.present_us += plat::time_us().saturating_sub(t0);
1586 match r {
1587 Ok(Shown::Done) | Ok(Shown::Wait) => {}
1588 Ok(Shown::NoArea) => {
1589 self.gp_flatten(i);
1590 return true;
1591 }
1592 Ok(Shown::Stale) => {
1593 self.gp_leave(i, "stale");
1594 return true;
1595 }
1596 Err(e) => {
1597 self.gp_failed(i, e);
1598 return true;
1599 }
1600 }
1601 plat::present_account(t0, true);
1602 true
1603 }
1604
1605 pub(crate) fn gp_cpu_done(&mut self, cur: u32, pw: i32) {
1607 let s = &mut self.m.gout;
1608 if !s.want {
1609 return;
1610 }
1611 let i = cur as usize;
1612 if s.wins.len() <= i {
1613 s.wins.resize_with(i + 1, || None);
1614 }
1615 let now = plat::time_us();
1616 let w = s.wins[i].get_or_insert_with(|| Win {
1617 pw,
1618 stage: Stage::Cpu,
1619 sc: None,
1620 surf: None,
1621 tex: None,
1622 rb: None,
1623 rb_px: Vec::new(),
1624 rb_keep: None,
1625 full: true,
1626 undef: true,
1627 pending: false,
1628 hidden: false,
1629 apart_before: false,
1630 last_present: 0,
1631 job: 0,
1632 lost: false,
1633 size: (0, 0),
1634 stable_at: now,
1635 fail_at: 0,
1636 fail_ms: 0,
1637 out: GpuOutput::default(),
1638 sp: Split::default(),
1639 #[cfg(target_arch = "wasm32")]
1640 verify_wait: Vec::new(),
1641 });
1642 w.pw = pw;
1643 w.out.cpu_frames += 1;
1644 let size = (self.wp.rt.fb.w, self.wp.rt.fb.h);
1645 if w.size != size {
1646 w.size = size;
1647 w.stable_at = now;
1648 }
1649 }
1650
1651 fn gp_leave(&mut self, i: usize, why: &str) {
1654 self.gp_settle(i);
1655 if let Some(p) = self.uiw_parts_mut(i as u32) {
1656 p.dirty.busy = false;
1657 }
1658 let Some(fb) = self.uiw_parts(i as u32).map(|p| &p.rt.fb) else { return };
1659 let size = (fb.w, fb.h);
1660 let Some(Some(w)) = self.m.gout.wins.get(i) else { return };
1661 let pw = w.pw;
1662 let t0 = plat::time_us();
1663 plat::win_present(pw, &Ui::frame_of(fb), None);
1664 plat::win_gpu_child_show(pw, false);
1665 let now = plat::time_us();
1666 let s = &mut self.m.gout;
1667 s.note(&format!("window {i}: back to the CPU path ({why}): {:.2} ms", ms(t0, now)));
1668 let Some(w) = s.win(i) else { return };
1669 w.stage = Stage::Cpu;
1670 w.tex = None;
1671 w.sp.reset();
1672 w.pending = false;
1673 w.size = size;
1674 w.stable_at = now;
1675 w.out.leaves += 1;
1676 #[cfg(target_arch = "wasm32")]
1678 {
1679 w.sc = None;
1680 w.surf = None;
1681 }
1682 w.out.cpu_frames += 1;
1683 }
1684
1685 fn gp_failed(&mut self, i: usize, e: GpuError) {
1688 plat::log_warn("gpu", &format!("output: {e}"));
1689 self.m.gout.note(&format!("window {i}: {e}"));
1690 if matches!(e, GpuError::SurfaceLost) {
1691 self.gp_leave(i, "surface lost");
1692 let now = plat::time_us();
1693 if let Some(w) = self.m.gout.win(i) {
1694 w.sc = None;
1695 w.surf = None;
1696 w.fail_ms = (w.fail_ms * 2).clamp(RETRY_FIRST_MS, RETRY_MAX_MS);
1697 w.fail_at = now + w.fail_ms * 1000;
1698 }
1699 return;
1700 }
1701 let n = self.m.gout.wins.len();
1702 for k in 0..n {
1703 let on = self.m.gout.wins[k].as_ref().is_some_and(|w| w.stage == Stage::Gpu);
1704 if on {
1705 self.gp_leave(k, "failure");
1706 }
1707 if let Some(w) = self.m.gout.win(k) {
1708 w.stage = Stage::Cpu;
1709 w.sc = None;
1710 w.surf = None;
1711 w.tex = None;
1712 w.rb = None;
1713 w.pending = false;
1714 w.sp.reset();
1715 w.out.error = Some(format!("{e}"));
1716 }
1717 }
1718 let s = &mut self.m.gout;
1719 s.images_gone();
1720 s.canvases_gone();
1721 let now = plat::time_us();
1722 if matches!(e, GpuError::Invalid(_) | GpuError::Unsupported(_)) {
1723 s.dev = Dev::Never(format!("{e}"));
1724 return;
1725 }
1726 if now.saturating_sub(s.ready_at) > 60_000_000 {
1728 s.retry_ms = plat::env("jfgui_GPU_RETRY_MS").and_then(|v| v.parse().ok()).unwrap_or(RETRY_FIRST_MS);
1729 }
1730 s.dev = Dev::Retry(now + s.retry_ms * 1000);
1731 s.retry_ms = (s.retry_ms * 2).min(RETRY_MAX_MS);
1732 }
1733
1734 fn gp_poll(&mut self) {
1736 #[cfg(target_arch = "wasm32")]
1737 {
1738 let s = &mut self.m.gout;
1739 s.web_probe();
1740 if let Dev::Ready(core) = &s.dev {
1741 for w in s.wins.iter_mut().flatten() {
1742 verify_poll(w, core);
1743 }
1744 }
1745 }
1746 let done = core::mem::take(&mut *self.m.gout.jobs.done.lock());
1747 for d in done {
1748 match d {
1749 Done::Device { job, win, r } => {
1750 if !matches!(self.m.gout.dev, Dev::Starting(j) if j == job) {
1751 continue;
1752 }
1753 match r {
1754 Ok((core, built)) => {
1755 let s = &mut self.m.gout;
1756 s.note(&format!("device ready: {}", core.name));
1757 s.dev = Dev::Ready(core);
1758 s.ready_at = plat::time_us();
1759 self.gp_built(win, built);
1760 }
1761 Err(e) => {
1762 let s = &mut self.m.gout;
1763 s.note(&format!("no device: {e}"));
1764 match e {
1765 GpuError::DeviceLost | GpuError::OutOfMemory | GpuError::Timeout | GpuError::Failed(_) | GpuError::SurfaceLost
1766 | GpuError::NotReady => {
1767 s.dev = Dev::Retry(plat::time_us() + s.retry_ms * 1000);
1768 s.retry_ms = (s.retry_ms * 2).min(RETRY_MAX_MS);
1769 plat::gpu_out_state(plat::GPU_STATE_UNKNOWN);
1770 }
1771 _ => {
1772 s.dev = Dev::Never(format!("{e}")); plat::gpu_out_state(plat::GPU_STATE_NEVER);
1774 }
1775 }
1776 if let Some(w) = s.win(win) {
1777 w.stage = Stage::Cpu;
1778 w.lost = false;
1779 w.out.error = Some(format!("{e}"));
1780 #[cfg(target_arch = "wasm32")]
1782 plat::win_gpu_child_show(w.pw, false);
1783 }
1784 }
1785 }
1786 }
1787 Done::Window { job, win, r } => {
1788 let live = self.m.gout.win(win).is_some_and(|w| w.job == job && w.stage == Stage::Job);
1789 if !live {
1790 continue;
1791 }
1792 match r {
1793 Ok(b) => self.gp_built(win, b),
1794 Err(e) => {
1795 let now = plat::time_us();
1796 let s = &mut self.m.gout;
1797 s.note(&format!("window {win}: no swapchain: {e}"));
1798 if let Some(w) = s.win(win) {
1799 w.stage = Stage::Cpu;
1800 w.stable_at = now;
1801 if !core::mem::take(&mut w.lost) {
1802 w.out.error = Some(format!("{e}"));
1803 w.fail_ms = (w.fail_ms * 2).clamp(RETRY_FIRST_MS, RETRY_MAX_MS);
1804 w.fail_at = now + w.fail_ms * 1000;
1805 }
1806 }
1807 }
1808 }
1809 }
1810 }
1811 }
1812 let s = &mut self.m.gout;
1813 if !s.threads.is_empty() && s.jobs.running.load(Ordering::Acquire) == 0 {
1814 for t in s.threads.drain(..) {
1815 t.join();
1816 }
1817 }
1818 }
1819
1820 fn gp_built(&mut self, i: usize, b: Built) {
1822 let verify = self.m.gout.verify;
1823 let s = &mut self.m.gout;
1824 let Dev::Ready(core) = &s.dev else { return };
1825 let (dev, format) = (core.dev.clone(), core.format);
1826 let Some(w) = s.win(i) else { return };
1827 if core::mem::take(&mut w.lost) {
1828 w.stage = Stage::Cpu; return;
1830 }
1831 if b.sc.format() != format {
1832 w.stage = Stage::Cpu;
1833 w.fail_at = u64::MAX;
1834 return;
1835 }
1836 let (tw, th) = b.sc.size();
1837 let tex = Tex { t: b.tex, group: b.group, w: tw as i32, h: th as i32 };
1838 w.surf = Some(b.surf);
1839 w.sc = Some(b.sc);
1840 w.tex = Some(tex);
1841 w.full = true;
1842 w.undef = true;
1843 w.rb = None;
1844 if verify {
1845 let size = if cfg!(target_arch = "wasm32") { 4 } else { tw as u64 * th as u64 * 4 };
1847 w.rb = dev.create_buffer(&BufferDesc { size, usage: BufferUsage::NONE, memory: MemoryKind::Readback }).ok();
1848 }
1849 w.stage = Stage::Switch;
1850 if !unseen(w) {
1851 self.gp_switch(i);
1852 }
1853 }
1854
1855 fn gp_switch(&mut self, i: usize) {
1857 let t0 = plat::time_us();
1858 let mut s = core::mem::take(&mut self.m.gout);
1859 let mut failed = None;
1860 let mut again = false;
1861 let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
1862 if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
1863 if w.tex.as_ref().is_none_or(|t| (t.w, t.h) != (fb.w, fb.h)) || !plat::win_gpu_child_alone(w.pw) {
1864 w.stage = Stage::Cpu; w.stable_at = t0;
1866 again = true;
1867 } else {
1868 let ready = w.sc.as_mut().map_or(Ok(true), |sc| sc.prepare());
1869 let shown = match ready {
1870 Ok(true) => upload(w, core, fb, None, &mut s.trim_at).and_then(|_| show(w, core, fb)),
1871 Ok(false) => Ok(Shown::Wait), Err(e) => Err(e),
1873 };
1874 match shown {
1875 Ok(Shown::Done) => {
1876 plat::win_gpu_child_show(w.pw, true);
1877 plat::win_release_kept(w.pw);
1878 w.stage = Stage::Gpu;
1879 w.out.switches += 1;
1880 w.out.error = None;
1881 if !core::mem::replace(&mut s.said_ok, true) {
1882 plat::gpu_out_state(plat::GPU_STATE_OK);
1883 }
1884 }
1885 Ok(Shown::Wait) => w.pending = true,
1886 Ok(Shown::NoArea) | Ok(Shown::Stale) => {
1887 w.stage = Stage::Cpu;
1888 w.stable_at = t0;
1889 }
1890 Err(e) => failed = Some(e),
1891 }
1892 }
1893 }
1894 self.m.gout = s;
1895 if let Some(e) = failed {
1896 self.gp_failed(i, e);
1897 } else if !again {
1898 let t1 = plat::time_us();
1899 let on = self.m.gout.wins.get(i).and_then(|w| w.as_ref()).is_some_and(|w| w.stage == Stage::Gpu);
1900 if on {
1901 self.m.gout.note(&format!("window {i}: switched to the swapchain ({:.2} ms)", ms(t0, t1)));
1902 }
1903 }
1904 }
1905
1906 pub(crate) fn gp_tick(&mut self) {
1908 if !self.m.gout.want {
1909 return;
1910 }
1911 self.gp_poll();
1912 #[cfg(target_arch = "wasm32")]
1914 {
1915 let e = if let Dev::Ready(core) = &self.m.gout.dev { core.dev.poll().err() } else { None };
1916 let e = e.or(plat::win_gpu_child_blank().then_some(GpuError::Unsupported("the canvas shows nothing")));
1918 if let Some(e) = e {
1919 self.gp_failed(0, e);
1920 return;
1921 }
1922 }
1923 let n = self.m.gout.wins.len();
1924 let now = plat::time_us();
1925 self.gp_canvas_tick(now);
1926 for w in self.m.gout.wins.iter_mut().flatten() {
1927 if w.sp.linger_at != 0 && now >= w.sp.linger_at {
1928 w.sp.linger_at = 0;
1929 for u in w.sp.upper.iter_mut().filter(|u| u.rects.is_empty()) {
1930 u.tex = None;
1931 u.store = Store::default();
1932 }
1933 self.m.gout.trim_at = now + TRIM_US; }
1935 }
1936 for i in 0..n {
1937 let Some(w) = self.m.gout.wins[i].as_ref() else { continue };
1938 if matches!(w.stage, Stage::Gpu | Stage::Switch) && w.sc.as_ref().is_some_and(|sc| sc.busy()) {
1940 if let Some(Err(e)) = w.sc.as_ref().map(|sc| sc.flush()) {
1941 self.gp_failed(i, e);
1942 continue;
1943 }
1944 }
1945 let Some(w) = self.m.gout.wins[i].as_ref() else { continue };
1946 match w.stage {
1947 Stage::Switch if !unseen(w) => self.gp_switch(i),
1948 Stage::Gpu if unseen(w) => self.gp_leave(i, "hidden"),
1949 Stage::Gpu if !plat::win_gpu_child_alone(w.pw) => self.gp_leave(i, "something else shows"),
1951 Stage::Gpu if w.pending && plat::time_us() >= w.last_present + MIN_PRESENT_US => self.gp_retry(i),
1952 _ => {}
1953 }
1954 }
1955 self.gp_jobs();
1956 let s = &mut self.m.gout;
1957 if s.trim_at != 0 && plat::time_us() >= s.trim_at {
1958 s.trim_at = 0;
1959 if let Dev::Ready(core) = &s.dev {
1960 let _ = core.dev.poll();
1961 }
1962 }
1963 }
1964
1965 fn gp_retry(&mut self, i: usize) {
1968 if self.m.gout.verify {
1969 self.gp_settle(i);
1970 }
1971 let mut s = core::mem::take(&mut self.m.gout);
1972 let mut r = Ok(Shown::Done);
1973 let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
1974 if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
1975 if plat::win_state(w.pw) & plat::WST_MINIMIZED == 0 {
1976 let t0 = plat::time_us();
1977 r = if w.full { upload(w, core, fb, None, &mut s.trim_at) } else { Ok(()) }.and_then(|_| show(w, core, fb));
1978 plat::present_account(t0, false);
1979 }
1980 }
1981 self.m.gout = s;
1982 match r {
1983 Ok(Shown::Stale) => self.gp_leave(i, "stale"),
1984 Err(e) => self.gp_failed(i, e),
1985 _ => {}
1986 }
1987 }
1988
1989 fn gp_jobs(&mut self) {
1991 let now = plat::time_us();
1992 let s = &mut self.m.gout;
1993 if plat::win_in_sizemove() {
1994 return;
1995 }
1996 match s.dev {
1997 Dev::None => {}
1998 Dev::Retry(at) if at <= now => s.dev = Dev::None,
1999 _ => {}
2000 }
2001 #[cfg(target_arch = "wasm32")]
2002 if s.web_start() {
2003 return;
2004 }
2005 if matches!(s.dev, Dev::None) {
2006 if plat::win_remote_session() {
2007 s.dev = Dev::Retry(now + REMOTE_LOOK_US); return;
2009 }
2010 let Some((i, pw)) = s.wins.iter().enumerate().find_map(|(i, w)| w.as_ref().filter(|w| w.stage == Stage::Cpu).map(|w| (i, w.pw))) else {
2011 return;
2012 };
2013 let Some(nw) = plat::win_gpu_child(pw) else {
2014 if !plat::win_gpu_child_later(pw) {
2015 s.dev = Dev::Never(String::from("no child window"));
2016 }
2017 return;
2018 };
2019 let display = plat::win_display_adapter(pw);
2020 let desc = s.desc();
2021 let job = s.next_job;
2022 s.next_job += 1;
2023 let trace = s.trace;
2024 s.note("start: instance, device, swapchain on a worker");
2025 plat::gpu_out_state(plat::GPU_STATE_TRYING);
2026 let ok = s.spawn(move || Done::Device { job, win: i, r: build_device(None, nw, pw, display, desc, trace) });
2027 if ok {
2028 s.dev = Dev::Starting(job);
2029 if let Some(w) = s.win(i) {
2030 w.stage = Stage::Job;
2031 w.job = job;
2032 }
2033 } else {
2034 s.dev = Dev::Never(String::from("no thread"));
2035 }
2036 return;
2037 }
2038 let Dev::Ready(core) = &s.dev else { return };
2039 let (dev, bgl, inst) = (core.dev.clone(), core.bgl.clone(), core.inst.clone());
2040 let n = s.wins.len();
2041 let desc = s.desc();
2042 for i in 0..n {
2043 let Some(w) = s.wins[i].as_ref() else { continue };
2044 if w.stage != Stage::Cpu || unseen(w) || now < w.fail_at || now < w.stable_at + STABLE_US {
2045 continue;
2046 }
2047 let pw = w.pw;
2048 if plat::win_state(pw) & plat::WST_MINIMIZED != 0 || plat::win_size(pw) != w.size {
2049 continue;
2050 }
2051 let Some(nw) = plat::win_gpu_child(pw) else { continue };
2052 let job = s.next_job;
2053 s.next_job += 1;
2054 let Some(w) = s.wins[i].as_mut() else { continue };
2055 let old = w.sc.take();
2056 let surf = match w.surf.take() {
2057 Some(x) => {
2058 x.track_size(); x
2060 }
2061 None => match gpu::Surface::native(&inst, nw, pw) {
2062 Ok(x) => x,
2063 Err(_) => {
2064 w.fail_at = u64::MAX;
2065 continue;
2066 }
2067 },
2068 };
2069 w.stage = Stage::Job;
2070 w.job = job;
2071 let desc = desc;
2072 let (d2, b2) = (dev.clone(), bgl.clone());
2073 s.note(&format!("window {i}: swapchain on a worker"));
2074 let ok = s.spawn(move || Done::Window { job, win: i, r: build_window(&d2, &b2, surf, old, &desc) });
2075 if !ok {
2076 if let Some(w) = s.win(i) {
2077 w.stage = Stage::Cpu;
2078 w.fail_at = u64::MAX;
2079 }
2080 }
2081 }
2082 }
2083
2084 pub(crate) fn gp_deadline(&self) -> Option<u64> {
2086 let s = &self.m.gout;
2087 if !s.want || s.wins.is_empty() {
2088 return None;
2089 }
2090 let now = plat::time_us();
2091 let mut d = u64::MAX;
2092 if s.jobs.running.load(Ordering::Acquire) != 0 {
2093 d = now + JOB_US;
2094 }
2095 match s.dev {
2096 Dev::None if s.wins.iter().flatten().next().is_some_and(|w| !plat::win_gpu_child_later(w.pw)) => d = now,
2097 Dev::Retry(at) => d = d.min(at),
2098 _ => {}
2099 }
2100 if s.trim_at != 0 {
2101 d = d.min(s.trim_at);
2102 }
2103 if !s.script {
2104 for c in s.canvases.iter().filter(|c| c.apart) {
2105 d = d.min(c.until);
2106 }
2107 }
2108 if s.canvas_free_at != 0 {
2109 d = d.min(s.canvas_free_at);
2110 }
2111 let ready = matches!(s.dev, Dev::Ready(_));
2112 let moving = plat::win_in_sizemove();
2113 for w in s.wins.iter().flatten() {
2114 if w.sp.linger_at != 0 {
2115 d = d.min(w.sp.linger_at);
2116 }
2117 let minimised = plat::win_state(w.pw) & plat::WST_MINIMIZED != 0;
2118 let on_way = w.sc.as_ref().is_some_and(|sc| sc.busy());
2120 match w.stage {
2121 Stage::Switch if !minimised && !unseen(w) => d = d.min(now + RETRY_US),
2122 Stage::Gpu if on_way => d = d.min(now + RETRY_US.min(1000)),
2123 Stage::Gpu if w.pending && !minimised => {
2124 let retry = (w.last_present + MIN_PRESENT_US).max(now + RETRY_US.min(1000));
2125 d = d.min(w.sc.as_ref().map_or(retry, |sc| sc.next_look(retry)));
2126 }
2127 Stage::Cpu if ready && !minimised && !moving && !unseen(w) && w.fail_at != u64::MAX && !plat::win_gpu_child_later(w.pw) => {
2128 let settle = if plat::win_size(w.pw) == w.size { 1000 } else { 16_000 };
2130 d = d.min((w.stable_at + STABLE_US).max(w.fail_at).max(now + settle));
2131 }
2132 _ => {}
2133 }
2134 }
2135 if d == u64::MAX { None } else { Some(d) }
2136 }
2137
2138 #[cfg(target_os = "android")]
2143 pub(crate) fn gp_surface_gone(&mut self) -> bool {
2144 if !self.m.gout.want {
2145 return false;
2146 }
2147 let mut waits = false;
2148 for i in 0..self.m.gout.wins.len() {
2149 let Some(stage) = self.m.gout.wins[i].as_ref().map(|w| w.stage) else { continue };
2150 if stage == Stage::Gpu {
2151 self.gp_leave(i, "the surface went");
2152 waits = true;
2153 }
2154 let Some(w) = self.m.gout.win(i) else { continue };
2155 if stage == Stage::Job {
2156 w.lost = true; } else {
2158 w.stage = Stage::Cpu;
2159 }
2160 w.sc = None;
2161 w.surf = None;
2162 w.tex = None;
2163 w.rb = None;
2164 w.pending = false;
2165 w.sp.reset();
2166 }
2167 waits
2168 }
2169
2170 fn gp_wait_jobs(&mut self, ms: u64) {
2173 let dl = plat::time_us() + ms * 1000;
2174 while self.m.gout.jobs.running.load(Ordering::Acquire) != 0 && plat::time_us() < dl {
2175 plat::wait(plat::time_us() + 1000);
2176 }
2177 self.gp_poll();
2178 }
2179
2180 pub(crate) fn gp_window_gone(&mut self, i: u32) {
2182 let i = i as usize;
2183 if self.m.gout.wins.get(i).and_then(|w| w.as_ref()).is_none() {
2184 return;
2185 }
2186 if self.m.gout.jobs.running.load(Ordering::Acquire) != 0 {
2187 self.gp_wait_jobs(3000);
2188 }
2189 if let Some(w) = self.m.gout.wins.get_mut(i) {
2190 *w = None;
2191 }
2192 let s = &mut self.m.gout;
2193 for k in (0..s.canvases.len()).rev() {
2194 if s.canvases[k].win == i {
2195 let c = s.canvases.remove(k);
2196 let n: usize = c.levels.iter().map(|l| l.tiles.len()).sum();
2197 s.tile_bytes -= n as u64 * TILE_BYTES;
2198 }
2199 }
2200 }
2201
2202 pub(crate) fn gp_window_show(&mut self, i: u32, show: bool) {
2205 let i = i as usize;
2206 let Some(stage) = self.m.gout.wins.get(i).and_then(|w| w.as_ref()).map(|w| w.stage) else { return };
2207 if !show && stage == Stage::Gpu {
2208 self.gp_leave(i, "hidden");
2209 }
2210 let now = plat::time_us();
2211 if let Some(w) = self.m.gout.win(i) {
2212 w.hidden = !show;
2213 w.stable_at = now;
2214 }
2215 }
2216
2217 pub(crate) fn gp_shutdown(&mut self) {
2220 if !self.m.gout.want {
2221 return;
2222 }
2223 let t0 = plat::time_us();
2224 self.gp_wait_jobs(2000);
2225 let s = &mut self.m.gout;
2226 for w in s.wins.iter().flatten() {
2228 plat::win_show(w.pw, plat::PWS_HIDE);
2229 }
2230 if env_on("jfgui_GPU_VALIDATE") {
2233 s.wins.clear();
2234 s.images.clear();
2235 s.canvases.clear();
2236 s.tile_free.clear();
2237 s.dev = Dev::None;
2238 } else {
2239 core::mem::forget(core::mem::take(&mut s.wins));
2240 core::mem::forget(core::mem::take(&mut s.images));
2241 core::mem::forget(core::mem::take(&mut s.canvases));
2242 core::mem::forget(core::mem::take(&mut s.tile_free));
2243 core::mem::forget(core::mem::replace(&mut s.dev, Dev::None));
2244 }
2245 s.want = false;
2246 s.note(&format!("shut down: {:.2} ms", ms(t0, plat::time_us())));
2247 }
2248
2249 pub fn gpu_output(&self, win: u32) -> GpuOutput {
2251 let s = &self.m.gout;
2252 let mut o = s.wins.get(win as usize).and_then(|w| w.as_ref()).map_or_else(GpuOutput::default, |w| {
2253 let mut o = w.out.clone();
2254 o.on = w.stage == Stage::Gpu;
2255 o.building = matches!(w.stage, Stage::Job | Stage::Switch);
2256 o.pending = w.pending;
2257 for u in &w.sp.upper {
2258 if let Some(t) = &u.tex {
2259 o.layer_textures += !u.rects.is_empty() as u32;
2260 o.layer_bytes += t.w as u64 * t.h as u64 * 4;
2261 }
2262 o.layer_store += u.store.px.capacity() as u64;
2263 }
2264 o.image_quads = w.sp.quads.iter().filter(|q| matches!(q.src, QuadSrc::Image { .. })).count() as u32;
2265 o.canvas_quads = w.sp.quads.len() as u32 - o.image_quads;
2266 o
2267 });
2268 o.wanted = s.want;
2269 o.image_textures = s.images.iter().filter(|e| e.tex.is_some()).count() as u32;
2270 o.image_bytes = s.image_bytes;
2271 o.image_uploads = s.image_uploads;
2272 o.image_hits = s.image_hits;
2273 o.image_evicted = s.image_evicted;
2274 o.canvas_apart = s.canvases.iter().filter(|c| c.apart && c.win == win as usize).count() as u32;
2275 o.canvas_tiles = s.canvases.iter().flat_map(|c| c.levels.iter()).map(|l| l.tiles.len() as u32).sum();
2276 o.canvas_bytes = s.tile_bytes;
2277 o.canvas_fills = s.tile_fills;
2278 o.canvas_fill_us = s.tile_fill_us;
2279 o.canvas_evicted = s.tile_evicted;
2280 o.canvas_settles = s.canvas_settles;
2281 match &s.dev {
2282 Dev::Ready(core) => {
2283 o.adapter = core.name.clone();
2284 o.reserved = core.dev.memory_reserved();
2285 o.present_mode = s.wins.get(win as usize).and_then(|w| w.as_ref()).and_then(|w| w.sc.as_ref()).map_or("", |sc| match sc.present_mode() {
2286 PresentMode::Fifo => "fifo",
2287 PresentMode::Mailbox => "mailbox",
2288 PresentMode::Immediate => "immediate",
2289 });
2290 }
2291 Dev::Never(why) => {
2292 o.never = true;
2293 if o.error.is_none() {
2294 o.error = Some(why.clone());
2295 }
2296 }
2297 _ => {}
2298 }
2299 o
2300 }
2301
2302 pub fn gpu_test_lose(&mut self) {
2305 if let Dev::Ready(core) = &self.m.gout.dev {
2306 core.dev.test_lose();
2307 }
2308 }
2309
2310 pub fn gpu_test_image_budget(&mut self, bytes: u64) {
2312 self.m.gout.image_budget = bytes;
2313 }
2314
2315 pub fn gpu_test_canvas_budget(&mut self, bytes: u64) {
2317 self.m.gout.canvas_budget = bytes;
2318 }
2319
2320 pub fn gpu_test_canvas_unused(&mut self, ms: u64) {
2322 self.m.gout.canvas_unused_us = ms * 1000;
2323 }
2324
2325 pub fn gpu_canvas_settle(&mut self) {
2327 let mut any = false;
2328 let s = &mut self.m.gout;
2329 for c in s.canvases.iter_mut().filter(|c| c.apart) {
2330 c.apart = false;
2331 any = true;
2332 s.canvas_settles += 1;
2333 }
2334 if any {
2335 self.invalidate();
2336 }
2337 }
2338
2339 pub fn gpu_settle(&mut self) {
2341 self.gp_settle_all();
2342 }
2343
2344 pub(crate) fn gp_split_wanted(&self) -> bool {
2348 let s = &self.m.gout;
2349 s.want && s.layers_on && s.wins.get(self.uiw_cur as usize).and_then(|w| w.as_ref()).is_some_and(|w| w.stage == Stage::Gpu)
2350 }
2351
2352 fn gp_can_split(&self, i: usize) -> bool {
2355 let Some(Some(w)) = self.m.gout.wins.get(i) else { return false };
2356 let fb = &self.wp.rt.fb;
2357 !self.m.ins.on && !w.full && !w.undef && w.tex.as_ref().is_some_and(|t| (t.w, t.h) == (fb.w, fb.h)) && plat::win_state(w.pw) & plat::WST_MINIMIZED == 0
2358 }
2359
2360 pub(crate) fn gp_plan_images(&mut self) -> bool {
2363 let i = self.uiw_cur as usize;
2364 if !self.gp_can_split(i) {
2365 return true; }
2367 let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
2368 let opaque_dc = self.dc.opac == 255;
2369 let Ui { m, wp, .. } = self;
2370 let s = &mut m.gout;
2371 s.tick += 1;
2372 let (tick, now) = (s.tick, plat::time_us());
2373 let d = &wp.dirty;
2374 let (cols, rows) = (d.cols as usize, d.rows as usize);
2375 let total = cols * rows;
2376 let Some(Some(w)) = s.wins.get_mut(i) else { return false };
2377 let sp = &mut w.sp;
2378 if (sp.cols, sp.rows) != (cols, rows) || sp.hole.len() != total {
2379 (sp.cols, sp.rows) = (cols, rows);
2380 sp.hole = vec![0; total];
2381 sp.owed = vec![false; total];
2382 sp.stale = vec![false; total];
2383 sp.part = vec![false; total];
2384 sp.redraw = vec![false; total];
2385 sp.any_owed = false;
2386 sp.any_stale = false;
2387 }
2388 sp.hole.fill(0);
2389 sp.part.fill(false);
2390 let mut quads = core::mem::take(&mut sp.quads);
2391 quads.clear();
2392 let (cur, prev) = (d.cells.as_deref().unwrap_or(&[]), d.cells_prev.as_deref().unwrap_or(&[]));
2394 let mut k = 0;
2395 while k < s.images.len() {
2396 if s.images[k].weak.strong_count() == 0 {
2397 s.evict(k);
2398 s.images.remove(k);
2399 } else {
2400 k += 1;
2401 }
2402 }
2403 if s.images_on && opaque_dc {
2404 for (tag0, seen) in d.imgs.iter().enumerate() {
2405 let tag = tag0 as u8 + 1;
2406 let Some(Cmd::Image { x, y, w: iw, h: ih, sf, ver }) = wp.cmd.layers[0].get(seen.index as usize) else { continue };
2407 if sf.w <= 0 || sf.h <= 0 || sf.px.is_empty() || sf.fmt == FMT_A8 {
2408 continue;
2409 }
2410 let key = Rc::as_ptr(sf) as usize;
2411 let vis = seen.vis;
2412 let (c0, c1) = (((vis[0] + CELL - 1) >> 5).max(0) as usize, (((vis[2].min(fw) + (if vis[2] >= fw { CELL - 1 } else { 0 })) >> 5).max(0) as usize).min(cols));
2414 let (r0, r1) = (((vis[1] + CELL - 1) >> 5).max(0) as usize, (((vis[3].min(fh) + (if vis[3] >= fh { CELL - 1 } else { 0 })) >> 5).max(0) as usize).min(rows));
2415 let mut changed = 0u64;
2416 let mut any = false;
2417 for r in r0..r1 {
2418 for c in c0..c1 {
2419 let k = r * cols + c;
2420 if d.top.get(k) == Some(&tag) {
2421 any = true;
2422 if cur.get(k) != prev.get(k) {
2423 changed += CELL_BYTES as u64;
2424 }
2425 }
2426 }
2427 }
2428 if !any {
2429 continue;
2430 }
2431 let mut q = 0;
2433 while q < s.images.len() {
2434 if s.images[q].key == key && s.images[q].ver != *ver {
2435 s.evict(q);
2436 s.images.remove(q);
2437 } else {
2438 q += 1;
2439 }
2440 }
2441 let at = match s.images.iter().position(|e| e.key == key) {
2442 Some(at) => {
2443 s.images[at].acc += changed;
2444 at
2445 }
2446 None => {
2447 if s.images.len() >= IMAGES_KEPT {
2448 let Some(old) = (0..s.images.len()).min_by_key(|&k| s.images[k].tick) else { continue };
2449 s.evict(old);
2450 s.images.remove(old);
2451 }
2452 s.images.push(Image {
2453 key,
2454 weak: Rc::downgrade(sf),
2455 ver: *ver,
2456 bytes: sf.w as u64 * sf.h as u64 * 4,
2457 acc: 0,
2458 opaque: None,
2459 tex: None,
2460 load: None,
2461 tick,
2462 used_us: now,
2463 });
2464 s.images.len() - 1
2465 }
2466 };
2467 s.images[at].tick = tick;
2468 s.images[at].used_us = now;
2469 let e = &s.images[at];
2471 if e.tex.is_none() && e.load.is_none() && e.acc >= e.bytes && e.bytes <= s.image_budget {
2472 if let Dev::Ready(core) = &s.dev {
2473 let lim = core.dev.limits().max_texture_2d as i32;
2474 let opaque = e.opaque.unwrap_or_else(|| surface_opaque(sf));
2475 s.images[at].opaque = Some(opaque);
2476 if opaque && sf.w <= lim && sf.h <= lim {
2477 let need = s.images[at].bytes;
2478 while s.image_bytes + need > s.image_budget {
2479 let old = (0..s.images.len()).filter(|&k| (s.images[k].tex.is_some() || s.images[k].load.is_some()) && s.images[k].tick != tick).min_by_key(|&k| s.images[k].tick);
2480 let Some(old) = old else { break };
2481 s.evict(old);
2482 }
2483 if s.image_bytes + need <= s.image_budget {
2484 if let Dev::Ready(core) = &s.dev {
2485 match image_texture(core, sf) {
2488 Ok(load) => {
2489 s.images[at].load = Some(load);
2490 s.image_bytes += need;
2491 }
2492 Err(_) => s.images[at].opaque = Some(false),
2493 }
2494 }
2495 }
2496 }
2497 }
2498 }
2499 if let (Dev::Ready(core), Some(load)) = (&s.dev, s.images[at].load.as_mut()) {
2501 let t0 = plat::time_us();
2502 let rows = ((IMAGE_STEP_BYTES / (sf.w as u64 * 4)).max(1) as i32).min(sf.h - load.row);
2503 let before = if load.row == 0 { Layout::Undefined } else { Layout::Sampled };
2504 match core.dev.queue().write_texture_rects(&load.t, &[[0, load.row, sf.w, rows]], &sf.px, sf.stride as u32, before, Layout::Sampled) {
2505 Ok(()) => {
2506 load.row += rows;
2507 let done = load.row >= sf.h;
2508 let bytes = sf.w as u64 * rows as u64 * 4;
2509 s.trim_at = plat::time_us() + TRIM_US;
2510 if let Some(Some(w)) = s.wins.get_mut(i) {
2511 w.out.upload_bytes += bytes;
2512 }
2513 if done {
2514 if let Some(load) = s.images[at].load.take() {
2515 s.images[at].tex = Some(Rc::new(ImageTex { _t: load.t, group: load.group }));
2516 s.image_uploads += 1;
2517 }
2518 s.note(&format!("image {} x {} as a texture, the last part {:.2} ms", sf.w, sf.h, ms(t0, plat::time_us())));
2519 }
2520 }
2521 Err(_) => {
2522 s.evict(at);
2523 s.images[at].opaque = Some(false);
2524 }
2525 }
2526 }
2527 let Some(tex) = s.images[at].tex.clone() else { continue };
2528 if quads.len() >= TOP_BLOCKED as usize - 1 {
2529 continue;
2530 }
2531 s.image_hits += 1;
2532 let Some(Some(w)) = s.wins.get_mut(i) else { continue };
2533 let sp = &mut w.sp;
2534 let q = quads.len() as u8 + 1;
2535 for r in r0..r1 {
2536 for c in c0..c1 {
2537 let k = r * cols + c;
2538 if d.top.get(k) == Some(&tag) {
2539 sp.hole[k] = q;
2540 }
2541 }
2542 }
2543 let mut rects = Vec::new();
2544 let hole = &sp.hole;
2545 cell_rects(&mut rects, cols, rows, fw, fh, |k| hole[k] == q);
2546 let step = [(((sf.w as u64) << 16) / *iw as u64) as u32, (((sf.h as u64) << 16) / *ih as u64) as u32];
2547 quads.push(Quad { x: *x, y: *y, src: QuadSrc::Image { tex, src: sf.clone(), w: *iw, h: *ih, step }, rects });
2548 }
2549 }
2550 if now >= s.look_at && !s.images.is_empty() {
2552 s.look_at = now + IMAGE_UNUSED_US / 4;
2553 for k in 0..s.images.len() {
2554 if now.saturating_sub(s.images[k].used_us) > IMAGE_UNUSED_US {
2555 s.evict(k);
2556 }
2557 }
2558 }
2559 let Some(Some(w)) = s.wins.get_mut(i) else { return false };
2560 w.sp.quads = quads;
2561 !w.sp.quads.is_empty()
2562 }
2563
2564 pub(crate) fn gp_canvas_wanted(&self) -> bool {
2566 let s = &self.m.gout;
2567 s.want && s.canvases_on
2568 }
2569
2570 pub(crate) fn gp_canvas_steady(&self, id: u32, ver: u32) -> bool {
2573 let i = self.uiw_cur as usize;
2574 let s = &self.m.gout;
2575 s.want && s.canvases_on && !self.rtl && self.gp_split_wanted() && s.canvases.iter().find(|c| c.win == i && c.id == id).is_some_and(|c| c.ver == ver && !c.damaged)
2576 }
2577
2578 pub(crate) fn gp_canvas_begin(&mut self) {
2581 let i = self.uiw_cur as usize;
2582 let can = self.wp.dirty.split && !self.rtl && self.dc.opac == 255 && self.gp_can_split(i);
2583 let now = plat::time_us();
2584 let Ui { m, wp, .. } = self;
2585 let s = &mut m.gout;
2586 let (settle, unused) = (s.canvas_settle_us, s.canvas_unused_us);
2587 wp.dirty.canv.clear();
2588 for rec in wp.canv.recs.iter() {
2589 let d = &rec.d;
2590 let mode = s.canvas_mode(d.name, d.gpu);
2591 if mode == CanvasGpu::Cpu {
2592 continue;
2593 }
2594 let ci = s.canvases.iter().position(|c| c.win == i && c.id == d.id).unwrap_or_else(|| {
2595 s.canvases.push(Canvas {
2596 win: i,
2597 id: d.id,
2598 ver: d.ver,
2599 view: d.view,
2600 prev: d.view,
2601 known: false,
2602 damaged: false,
2603 apart: false,
2604 until: 0,
2605 moved: false,
2606 levels: Vec::new(),
2607 });
2608 s.canvases.len() - 1
2609 });
2610 let new_ver = s.canvases[ci].ver != d.ver;
2611 if new_ver {
2612 let levels = core::mem::take(&mut s.canvases[ci].levels);
2613 for l in levels {
2614 s.tiles_free(l.tiles);
2615 }
2616 }
2617 let c = &mut s.canvases[ci];
2618 let changed = new_ver || c.damaged;
2619 let moved = c.known && c.view != d.view && d.view.scale > 0.0 && d.view.scale < 1.0e6;
2620 let (dx, dy) = (d.view.ox - c.view.ox, d.view.oy - c.view.oy);
2621 let whole = d.view.scale == c.view.scale && (dx - rnd(dx)).abs() < PHASE_TOL && (dy - rnd(dy)).abs() < PHASE_TOL;
2622 c.moved = false;
2623 if changed || !can || rec.layer != 0 {
2624 c.apart = false; } else if moved {
2626 c.apart = mode == CanvasGpu::Zoom || whole;
2627 if c.apart {
2628 c.moved = true;
2629 c.until = now + settle;
2630 s.canvas_free_at = c.until + unused;
2631 }
2632 }
2633 let c = &mut s.canvases[ci];
2634 if moved {
2635 c.prev = c.view;
2636 }
2637 c.ver = d.ver;
2638 c.view = d.view;
2639 c.damaged = false;
2640 c.known = true;
2641 if c.apart {
2642 wp.dirty.canv.push((rec.index, d.rect));
2643 }
2644 }
2645 }
2646
2647 pub(crate) fn gp_plan_canvases(&mut self) -> bool {
2650 let i = self.uiw_cur as usize;
2651 if self.wp.dirty.canv.is_empty() {
2652 return false;
2653 }
2654 let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
2655 let Ui { m, wp, text, .. } = self;
2656 let font = text.get_font();
2657 let s = &mut m.gout;
2658 s.tick += 1;
2659 let (tick, t0) = (s.tick, plat::time_us());
2660 let fill_end = if s.script { u64::MAX } else { t0 + FILL_US };
2661 let d = &wp.dirty;
2662 let (cols, rows) = (d.cols as usize, d.rows as usize);
2663 let Some(Some(w)) = s.wins.get_mut(i) else { return false };
2664 let mut quads = core::mem::take(&mut w.sp.quads);
2665 let mut sf = core::mem::take(&mut s.tile_sf);
2666 if sf.px.is_empty() {
2667 sf = Surface::new(TILE_TEX, TILE_TEX, FMT_XRGB);
2668 }
2669 for (tag0, seen) in d.imgs.iter().enumerate() {
2670 let Some(Cmd::Custom { x, y, w: cw, h: ch, f: Some(f), .. }) = wp.cmd.layers[0].get(seen.index as usize) else { continue };
2671 let Some(rec) = wp.canv.recs.iter().find(|r| r.layer == 0 && r.index == seen.index) else { continue };
2672 let Some(ci) = s.canvases.iter().position(|c| c.win == i && c.id == rec.d.id) else { continue };
2673 if quads.len() >= TOP_BLOCKED as usize - 1 {
2674 continue;
2675 }
2676 let (tag, vis, v) = (tag0 as u8 + 1, seen.vis, rec.d.view);
2677 let zoom = s.canvas_mode(rec.d.name, rec.d.gpu) == CanvasGpu::Zoom;
2678 let (moved, li) = {
2679 let c = &mut s.canvases[ci];
2680 (c.moved, level_for(c, v, zoom))
2681 };
2682 let mut li = li;
2683 if s.canvases[ci].levels.len() > LEVELS_MAX {
2684 let old = s.canvases[ci].levels.remove(0);
2685 s.tiles_free(old.tiles);
2686 li -= 1;
2687 }
2688 let (lscale, fx, fy) = {
2690 let l = &s.canvases[ci].levels[li];
2691 (l.scale, l.fx, l.fy)
2692 };
2693 let k = v.scale / lscale;
2694 let (dx, dy) = (v.ox - fx, v.oy - fy);
2695 let (rx, ry) = (rnd(dx), rnd(dy));
2696 let copy = k == 1.0 && (dx - rx).abs() < PHASE_TOL && (dy - ry).abs() < PHASE_TOL;
2697 let t = TILE as f64;
2698 let range = |w0: i32, w1: i32, o: f64, f: f64| -> (i32, i32) {
2699 let g = |w: f64| (w - o) / k + f;
2700 (flr(g(w0 as f64 - 1.0) / t) as i32, flr(g(w1 as f64 + 1.0) / t) as i32)
2701 };
2702 let (ix0, ix1) = range(vis[0], vis[2], v.ox, fx);
2703 let (iy0, iy1) = range(vis[1], vis[3], v.oy, fy);
2704 let edge = |g: f64, o: f64, f: f64| -> i32 { (flr(o + (g - f) * k - 0.5) + 1.0) as i32 };
2706 let mut tiles = Vec::new();
2707 let mut missing: Vec<[i32; 4]> = Vec::new();
2708 for iy in iy0..=iy1 {
2709 for ix in ix0..=ix1 {
2710 let (gx, gy) = (ix as f64 * t, iy as f64 * t);
2711 let (b, place) = if copy {
2712 let (x0, y0) = (rx as i32 + ix * TILE, ry as i32 + iy * TILE);
2713 ([x0, y0, x0 + TILE, y0 + TILE], [x0 - TILE_BORDER, y0 - TILE_BORDER, 0, 0])
2714 } else {
2715 let b = [edge(gx, v.ox, fx), edge(gy, v.oy, fy), edge(gx + t, v.ox, fx), edge(gy + t, v.oy, fy)];
2716 let at = |g: f64, o: f64, f: f64| rnd((o + (g - TILE_BORDER as f64 - f) * k) * 256.0) as i32;
2717 let step = rnd(65536.0 / k) as i32;
2718 (b, [at(gx, v.ox, fx), at(gy, v.oy, fy), step, step])
2719 };
2720 if b[0] >= vis[2] || b[2] <= vis[0] || b[1] >= vis[3] || b[3] <= vis[1] || b[0] >= b[2] || b[1] >= b[3] {
2721 continue;
2722 }
2723 if let Some(tile) = s.canvases[ci].levels[li].tiles.get_mut(&(ix, iy)) {
2724 tile.tick = tick;
2725 tiles.push(TileDraw { tex: tile.tex.clone(), b, place });
2726 continue;
2727 }
2728 let t1 = plat::time_us();
2729 let tex = if t1 < fill_end { s.tile_texture(tick) } else { None };
2730 let Some(tex) = tex else {
2731 missing.push(b);
2732 continue;
2733 };
2734 let tv = CanvasView { scale: lscale, ox: fx - gx + TILE_BORDER as f64, oy: fy - gy + TILE_BORDER as f64 };
2735 let mut dc = Dc::new(TILE_TEX, TILE_TEX);
2736 dc.begin(&sf);
2737 rec.d.content.draw_view(&mut Gfx { dc: &mut dc, sf: &mut sf, text: &mut *text }, tv, [0, 0, TILE_TEX, TILE_TEX]);
2738 dc.end();
2739 let Dev::Ready(core) = &s.dev else {
2740 missing.push(b);
2741 continue;
2742 };
2743 let before = if tex.fresh.replace(false) { Layout::Undefined } else { Layout::Sampled };
2744 if core.dev.queue().write_texture_rects(&tex.t, &[[0, 0, TILE_TEX, TILE_TEX]], &sf.px, sf.stride as u32, before, Layout::Sampled).is_err() {
2745 missing.push(b); continue;
2747 }
2748 s.tile_fills += 1;
2749 s.tile_fill_us += plat::time_us().saturating_sub(t1);
2750 s.trim_at = plat::time_us() + TRIM_US;
2751 if let Some(Some(w)) = s.wins.get_mut(i) {
2752 w.out.upload_bytes += TILE_BYTES;
2753 }
2754 tiles.push(TileDraw { tex: tex.clone(), b, place });
2755 s.canvases[ci].levels[li].tiles.insert((ix, iy), Tile { tex, tick });
2756 }
2757 }
2758 let Some(Some(w)) = s.wins.get_mut(i) else { continue };
2760 let sp = &mut w.sp;
2761 if sp.hole.len() != cols * rows {
2762 continue;
2763 }
2764 let q = quads.len() as u8 + 1;
2765 let span = |a: i32, b: i32, n: usize| ((a.max(0) >> 5) as usize, (((b + CELL - 1).max(0) >> 5) as usize).min(n));
2766 let (c0, c1) = span(vis[0], vis[2], cols);
2767 let (r0, r1) = span(vis[1], vis[3], rows);
2768 for r in r0..r1 {
2769 for c in c0..c1 {
2770 let k = r * cols + c;
2771 if d.top.get(k) == Some(&tag) {
2772 let b = cell_box(k, cols, fw, fh);
2773 sp.hole[k] = q;
2774 sp.part[k] = b[0] < vis[0] || b[1] < vis[1] || b[2] > vis[2] || b[3] > vis[3];
2775 if moved {
2776 sp.owed[k] = true;
2777 sp.stale[k] = true;
2778 }
2779 }
2780 if moved {
2781 sp.redraw[k] = true;
2782 }
2783 }
2784 }
2785 for b in &missing {
2786 let (c0, c1) = span(b[0].max(vis[0]), b[2].min(vis[2]), cols);
2787 let (r0, r1) = span(b[1].max(vis[1]), b[3].min(vis[3]), rows);
2788 for r in r0..r1 {
2789 for c in c0..c1 {
2790 let k = r * cols + c;
2791 if sp.hole[k] == q {
2792 sp.hole[k] = 0;
2793 sp.part[k] = false;
2794 }
2795 }
2796 }
2797 }
2798 sp.moved |= moved;
2799 w.out.canvas_frames += moved as u64;
2800 let mut rects = Vec::new();
2801 let hole = &sp.hole;
2802 cell_rects(&mut rects, cols, rows, fw, fh, |k| hole[k] == q);
2803 for r in rects.iter_mut() {
2804 let (x0, y0) = (r[0].max(vis[0]), r[1].max(vis[1]));
2805 let (x1, y1) = ((r[0] + r[2]).min(vis[2]), (r[1] + r[3]).min(vis[3]));
2806 *r = [x0, y0, x1 - x0, y1 - y0];
2807 }
2808 rects.retain(|r| r[2] > 0 && r[3] > 0);
2809 if !rects.is_empty() {
2810 let loose = !(rec.d.exact_pan && copy);
2811 quads.push(Quad { x: *x, y: *y, src: QuadSrc::Canvas { f: f.clone(), w: *cw, h: *ch, vis, tiles, copy, loose }, rects });
2812 }
2813 }
2814 s.tile_sf = sf;
2815 text.set_font(font.0, font.1);
2816 if let Some(Some(w)) = s.wins.get_mut(i) {
2817 w.sp.quads = quads;
2818 }
2819 true
2820 }
2821
2822 fn gp_canvas_tick(&mut self, now: u64) {
2825 let s = &mut self.m.gout;
2826 if !s.script && s.canvases.iter().any(|c| c.apart && now >= c.until) {
2827 for c in s.canvases.iter_mut().filter(|c| c.apart && now >= c.until) {
2828 c.apart = false;
2829 s.canvas_settles += 1;
2830 }
2831 self.invalidate();
2832 }
2833 let s = &mut self.m.gout;
2834 if s.canvas_free_at != 0 && now >= s.canvas_free_at {
2835 s.canvases_gone();
2836 s.trim_at = now + TRIM_US;
2837 }
2838 }
2839
2840 pub(crate) fn gp_canvas_damage(&mut self, id: u32, r: [f64; 4]) {
2842 let i = self.uiw_cur as usize;
2843 let S { canvases, tile_free, .. } = &mut self.m.gout;
2844 let Some(c) = canvases.iter_mut().find(|c| c.win == i && c.id == id) else { return };
2845 c.damaged = true;
2846 for l in c.levels.iter_mut() {
2847 let t = TILE as f64;
2848 let cell = |u: f64, f: f64, m: f64| flr((u * l.scale + f + m) / t) as i32;
2849 let (ix0, ix1) = (cell(r[0], l.fx, -2.0), cell(r[2], l.fx, 2.0));
2850 let (iy0, iy1) = (cell(r[1], l.fy, -2.0), cell(r[3], l.fy, 2.0));
2851 l.tiles.retain(|&(ix, iy), tile| {
2852 let hit = ix >= ix0 && ix <= ix1 && iy >= iy0 && iy <= iy1;
2853 if hit {
2854 tile_free.push(tile.tex.clone());
2855 }
2856 !hit
2857 });
2858 }
2859 }
2860
2861 pub(crate) fn gp_split_begin(&mut self, n: i32) -> bool {
2864 if !self.wp.dirty.split {
2865 return false;
2866 }
2867 let i = self.uiw_cur as usize;
2868 let can = self.gp_can_split(i);
2869 let upper_now = self.wp.cmd.layers[1..].iter().any(|l| !l.is_empty());
2870 let Some(w) = self.m.gout.win(i) else { return false };
2871 let sp = &mut w.sp;
2872 let kept = sp.active || !sp.quads.is_empty() || sp.any_stale || sp.moved;
2873 if !can {
2874 if kept {
2875 self.gp_flatten(i);
2876 }
2877 if let Some(w) = self.m.gout.win(i).filter(|_| upper_now) {
2878 w.sp.clean = false; }
2880 self.wp.dirty.busy = false;
2881 return false;
2882 }
2883 if !kept && !upper_now {
2884 sp.clean = true; self.wp.dirty.busy = false;
2886 return false;
2887 }
2888 let stale = sp.any_stale && sp.stale.iter().zip(sp.hole.iter()).any(|(&a, &h)| a && h == 0);
2889 if n == 0 && !stale && !sp.moved {
2890 return false; }
2892 if self.wp.rt.present_n == 0 {
2893 self.wp.rt.present_n = -1;
2894 }
2895 true
2896 }
2897
2898 pub(crate) fn gp_split_draw(&mut self, n: i32) {
2902 let icon_style = (self.style, self.color(T_ACCENT), self.theme_dark, self.color(T_TEXT_DISABLED), self.wp.rt.dpi as u32);
2903 let i = self.uiw_cur as usize;
2904 let Ui { m, wp, dc, text, icon_hook, drects, .. } = self;
2905 let s = &mut m.gout;
2906 let S { dev, wins, trim_at, .. } = s;
2907 let (Dev::Ready(core), Some(Some(w))) = (&*dev, wins.get_mut(i)) else {
2908 for l in wp.cmd.layers.iter() {
2909 render_layer(l, 0, dc, &mut wp.rt.fb, text, icon_hook, icon_style);
2910 }
2911 return;
2912 };
2913 let d = &wp.dirty;
2914 let (cols, rows) = (d.cols as usize, d.rows as usize);
2915 let total = cols * rows;
2916 let fb = &mut wp.rt.fb;
2917 let (fw, fh) = (fb.w, fb.h);
2918 let Win { sp, tex: base, out, pending, .. } = w;
2919 if sp.hole.len() != total {
2920 (sp.cols, sp.rows) = (cols, rows);
2921 sp.hole = vec![0; total];
2922 sp.owed = vec![false; total];
2923 sp.stale = vec![false; total];
2924 sp.part = vec![false; total];
2925 sp.redraw = vec![false; total];
2926 }
2927 let Split { upper, m: lowest, nr, rects, hole, owed, stale, quads, linger_at, part, redraw, .. } = sp;
2928 lowest.clear();
2930 lowest.resize(total, L_COUNT as u8);
2931 nr.clear();
2932 nr.resize(total, 0);
2933 let all = d.forced || (!sp.active && !sp.clean);
2934 let now: [&[u64]; L_COUNT] = core::array::from_fn(|l| d.through(true, l));
2935 let was: [&[u64]; L_COUNT] = core::array::from_fn(|l| d.through(false, l));
2936 for k in 0..total {
2937 let mut low = L_COUNT;
2938 if all || was[0].len() != total {
2939 low = 0;
2940 } else {
2941 for l in 0..L_COUNT {
2942 if now[l][k] != was[l][k] {
2943 low = l;
2944 break;
2945 }
2946 }
2947 }
2948 if stale[k] && hole[k] == 0 {
2949 low = 0; }
2951 for l in 1..L_COUNT {
2952 if now[l][k] != now[l - 1][k] {
2953 nr[k] |= 1 << l;
2954 }
2955 }
2956 if redraw[k] && (hole[k] == 0 || nr[k] != 0) {
2959 low = 0;
2960 }
2961 lowest[k] = low as u8;
2962 }
2963 if n >= 0 {
2965 for r in drects.iter().take(n as usize).skip(d.n0.max(0) as usize) {
2966 let (c0, c1) = ((r[0].max(0) >> 5) as usize, (((r[0] + r[2] + CELL - 1) >> 5).max(0) as usize).min(cols));
2967 let (r0, r1) = ((r[1].max(0) >> 5) as usize, (((r[1] + r[3] + CELL - 1) >> 5).max(0) as usize).min(rows));
2968 for row in r0..r1 {
2969 lowest[row * cols + c0..row * cols + c1.max(c0)].fill(0);
2970 }
2971 }
2972 }
2973 let mut err = None;
2975 for (u, l) in upper.iter_mut().zip(1..) {
2976 let bit = 1u8 << l;
2977 let mut bb = [i32::MAX, i32::MAX, i32::MIN, i32::MIN];
2978 for k in (0..total).filter(|&k| nr[k] & bit != 0) {
2979 let b = cell_box(k, cols, fw, fh);
2980 bb = [bb[0].min(b[0]), bb[1].min(b[1]), bb[2].max(b[2]), bb[3].max(b[3])];
2981 }
2982 if bb[0] >= bb[2] {
2983 if u.tex.is_some() && !u.rects.is_empty() {
2984 *linger_at = plat::time_us() + LINGER_US;
2985 }
2986 continue;
2987 }
2988 let area = (bb[2] - bb[0]) as i64 * (bb[3] - bb[1]) as i64;
2989 let fits = u.tex.as_ref().is_some_and(|t| {
2990 bb[0] >= t.ox && bb[1] >= t.oy && bb[2] <= t.ox + t.w && bb[3] <= t.oy + t.h
2991 && (t.w as i64 * t.h as i64) <= (area * LAYER_SHRINK).max((LAYER_STEP * LAYER_STEP) as i64)
2992 });
2993 if !fits {
2994 u.tex = None;
2995 match layer_texture(core, bb, fw, fh) {
2996 Ok(t) => u.tex = Some(t),
2997 Err(e) => err = Some(e),
2998 }
2999 for k in (0..total).filter(|&k| nr[k] & bit != 0) {
3000 lowest[k] = lowest[k].min(l);
3001 }
3002 }
3003 }
3004 let stride = fb.stride as usize;
3006 for l in 0..L_COUNT {
3007 let bit = 1u8 << l;
3008 if l > 0 {
3009 for k in 0..total {
3010 let low = lowest[k] as usize;
3011 if low > l {
3012 continue;
3013 }
3014 let b = cell_box(k, cols, fw, fh);
3015 if low == l {
3016 if let Some(u) = upper[l - 1..].iter().find(|u| u.store.has(k)) {
3019 cell_load(fb, b, u.store.get(k));
3020 owed[k] = false;
3021 }
3022 }
3023 if nr[k] & bit != 0 {
3024 let st = &upper[l - 1].store;
3025 if !st.has(k) && st.free.is_empty() && st.px.len() + CELL_BYTES > st.px.capacity() {
3026 let n = (k..total).filter(|&c| nr[c] & bit != 0 && !st.has(c)).count();
3028 upper[l - 1].store.reserve(n);
3029 }
3030 if owed[k] && hole[k] != 0 {
3031 if let Some(q) = quads.get(hole[k] as usize - 1) {
3032 quad_blit(fb, q, b, text);
3033 }
3034 owed[k] = false;
3035 }
3036 cell_save(fb, b, upper[l - 1].store.put(k, total));
3037 }
3038 }
3039 }
3040 let is_base = l == 0;
3041 cell_rects(rects, cols, rows, fw, fh, |k| (lowest[k] as usize) <= l && (if is_base { hole[k] == 0 || part[k] } else { nr[k] & bit != 0 }));
3042 for r in rects.iter() {
3043 dc.clip_push(r[0], r[1], r[2], r[3]);
3044 render_layer(&wp.cmd.layers[l], 0, dc, fb, text, icon_hook, icon_style);
3045 dc.clip_pop();
3046 }
3047 let bytes: u64 = rects.iter().map(|r| r[2] as u64 * r[3] as u64 * 4).sum();
3048 if is_base {
3049 out.base_px += bytes / 4;
3050 for k in 0..total {
3051 if lowest[k] == 0 {
3052 let h = hole[k] != 0;
3053 owed[k] = h;
3054 stale[k] = h;
3055 }
3056 }
3057 if err.is_none() {
3058 if let Some(t) = base.as_ref() {
3059 if let Err(e) = core.dev.queue().write_texture_rects(&t.t, rects, &fb.px, stride as u32, Layout::Sampled, Layout::Sampled) {
3060 err = Some(e);
3061 }
3062 }
3063 }
3064 out.upload_base += bytes;
3065 } else {
3066 let u = &mut upper[l - 1];
3067 if err.is_none() {
3068 if let Some(t) = u.tex.as_mut() {
3069 let before = if t.fresh { Layout::Undefined } else { Layout::Sampled };
3070 match core.dev.queue().write_texture_rects_at(&t.t, rects, (t.ox, t.oy), &fb.px, stride as u32, before, Layout::Sampled) {
3071 Ok(()) => t.fresh = t.fresh && rects.is_empty(),
3072 Err(e) => err = Some(e),
3073 }
3074 }
3075 }
3076 for k in 0..total {
3077 if nr[k] & bit == 0 {
3078 u.store.drop_cell(k);
3079 }
3080 }
3081 if u.store.used == 0 {
3082 u.store.empty();
3083 }
3084 cell_rects(&mut u.rects, cols, rows, fw, fh, |k| nr[k] & bit != 0);
3085 }
3086 out.upload_bytes += bytes;
3087 if bytes > BIG_UPLOAD {
3088 *trim_at = plat::time_us() + TRIM_US;
3089 }
3090 }
3091 if sp.moved {
3093 for k in 0..total {
3094 if sp.redraw[k] && sp.hole[k] != 0 && sp.nr[k] == 0 {
3095 sp.owed[k] = true;
3096 }
3097 }
3098 }
3099 sp.any_owed = sp.owed.iter().any(|&v| v);
3100 sp.any_stale = sp.stale.iter().any(|&v| v);
3101 sp.redraw.fill(false);
3102 sp.moved = false;
3103 sp.active = sp.upper.iter().any(|u| !u.rects.is_empty());
3104 sp.clean = true;
3105 sp.drawn = true;
3106 sp.err = err;
3107 *pending = true;
3108 out.split_frames += 1;
3109 wp.dirty.busy = sp.active || !sp.quads.is_empty();
3110 }
3111
3112 pub(crate) fn gp_frame_done(&mut self, t0: u64, n: i32, split: bool) {
3114 let i = self.uiw_cur as usize;
3115 let (fw, fh) = (self.wp.rt.fb.w as u64, self.wp.rt.fb.h as u64);
3116 let px = if split {
3117 0
3118 } else if n < 0 {
3119 fw * fh
3120 } else {
3121 self.drects.iter().take(n as usize).map(|r| r[2].max(0) as u64 * r[3].max(0) as u64).sum()
3122 };
3123 if let Some(w) = self.m.gout.win(i) {
3124 w.out.render_us += plat::time_us().saturating_sub(t0);
3125 w.out.base_px += px;
3126 }
3127 }
3128
3129 fn gp_settle(&mut self, i: usize) {
3131 let Some(Some(w)) = self.m.gout.wins.get_mut(i) else { return };
3132 if !w.sp.any_owed {
3133 return;
3134 }
3135 let sp = &mut w.sp;
3136 let (mut owed, hole, quads) = (core::mem::take(&mut sp.owed), core::mem::take(&mut sp.hole), core::mem::take(&mut sp.quads));
3137 let (cols, rows) = (sp.cols, sp.rows);
3138 sp.any_owed = false;
3139 let font = self.text.get_font();
3140 let cur = self.uiw_cur as usize;
3141 let Ui { wp, m, text, .. } = self;
3142 let parts = if i == cur { Some(wp) } else { m.win.recs[i & (WIN_MAX - 1)].save.as_deref_mut() };
3143 if let Some(p) = parts {
3144 let fb = &mut p.rt.fb;
3145 let (fw, fh) = (fb.w, fb.h);
3146 let mut rects = Vec::new();
3147 for (q, quad) in quads.iter().enumerate() {
3148 let tag = q as u8 + 1;
3149 cell_rects(&mut rects, cols, rows, fw, fh, |k| owed[k] && hole[k] == tag);
3150 for r in &rects {
3151 quad_blit(fb, quad, [r[0], r[1], r[0] + r[2], r[1] + r[3]], text);
3152 }
3153 }
3154 }
3155 self.text.set_font(font.0, font.1);
3156 owed.fill(false);
3157 let Some(Some(w)) = self.m.gout.wins.get_mut(i) else { return };
3158 (w.sp.owed, w.sp.hole, w.sp.quads) = (owed, hole, quads);
3159 }
3160
3161 pub(crate) fn gp_settle_all(&mut self) {
3163 for i in 0..self.m.gout.wins.len() {
3164 self.gp_settle(i);
3165 }
3166 }
3167
3168 fn gp_flatten(&mut self, i: usize) {
3171 self.gp_settle(i);
3172 let s = &mut self.m.gout;
3173 let trim = plat::time_us() + TRIM_US;
3174 let mut freed = false;
3175 if let Some(Some(w)) = s.wins.get_mut(i) {
3176 freed = w.sp.active;
3177 w.sp.reset();
3178 w.full = true;
3179 w.pending = true;
3180 }
3181 if freed {
3182 s.trim_at = trim;
3183 }
3184 if let Some(p) = self.uiw_parts_mut(i as u32) {
3185 p.dirty.busy = false;
3186 }
3187 }
3188
3189 pub(crate) fn gp_repaired(&mut self) {
3191 let i = self.uiw_cur as usize;
3192 if self.m.gout.wins.get(i).and_then(|w| w.as_ref()).is_some_and(|w| w.stage == Stage::Gpu) {
3193 self.gp_flatten(i);
3194 }
3195 }
3196
3197 pub(crate) fn gp_scripted(&self) -> bool {
3201 self.m.gout.script
3202 }
3203
3204 pub(crate) fn gp_script_pw(&mut self, cur: u32) -> i32 {
3208 let i = cur as usize & (super::consts::WIN_MAX - 1);
3209 let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
3210 let s = &mut self.m.gout;
3211 let mut pw = s.script_pw[i];
3212 if pw < 0 {
3213 let off = 40 + 48 * i as i32;
3214 let owner = if cfg!(windows) { plat::PW_NOOWNER } else { s.script_pw.iter().copied().find(|&p| p >= 0).unwrap_or(plat::PW_NOOWNER) };
3216 pw = plat::win_create("gpu output test", 320, 240, plat::PWF_SYSFRAME | plat::PWF_RESIZABLE, owner, off, off);
3217 if pw < 0 {
3218 return -1;
3219 }
3220 s.script_pw[i] = pw;
3221 plat::win_test(plat::WT_NOANIM, pw, [0; 4]);
3222 plat::win_show(pw, plat::PWS_SHOWNA);
3223 }
3224 if plat::win_size(pw) != (fw, fh) && s.script_asked[i] != (fw, fh) {
3226 s.script_asked[i] = (fw, fh);
3227 plat::win_set_rect(pw, plat::PW_KEEP, plat::PW_KEEP, fw, fh);
3228 }
3229 script_pump();
3230 pw
3231 }
3232
3233 fn gp_scoped(&self) -> usize {
3234 self.uiw_cur as usize
3235 }
3236
3237 fn gp_script_wait(&mut self, ms: u64) -> bool {
3239 let dl = plat::time_us() + ms * 1000;
3240 loop {
3241 script_pump();
3242 self.gp_tick();
3243 let s = &self.m.gout;
3244 let building = s.jobs.running.load(Ordering::Acquire) != 0;
3245 let all = s.wins.iter().flatten().all(|w| w.stage == Stage::Gpu || kept_at_other_size(w));
3248 if matches!(s.dev, Dev::Never(_)) {
3249 return false;
3250 }
3251 if all && !building && !s.wins.is_empty() {
3252 return true;
3253 }
3254 if plat::time_us() >= dl {
3255 return false;
3256 }
3257 plat::wait(plat::time_us() + 1000);
3258 }
3259 }
3260
3261 fn gp_skip(&mut self, why: &str) {
3262 self.m.gout.skip = true;
3263 plat::write_stdout(format!("SKIP gpu: {why}\n").as_bytes());
3264 }
3265
3266 pub(crate) fn sc_gpu(&mut self, app: &mut dyn App, a: &[u8]) {
3268 if let Some(r) = word_eq(a, "on") {
3269 self.m.gout.script = true;
3270 self.m.gout.setup();
3271 self.m.gout.want = true;
3272 self.m.gout.verify = word_eq(skip_ws(r), "fast").is_none();
3273 self.m.gout.trace = env_on("jfgui_GPU_TRACE");
3274 self.m.gout.skip = false;
3275 self.dirty_force_full();
3276 self.invalidate();
3277 self.run_passes(app);
3278 self.render();
3279 self.present();
3280 if !self.gp_script_wait(20_000) {
3281 let why = match &self.m.gout.dev {
3282 Dev::Never(w) => w.clone(),
3283 _ => {
3284 let stages: Vec<(usize, Stage)> = self.m.gout.wins.iter().enumerate().filter_map(|(i, w)| w.as_ref().map(|w| (i, w.stage))).collect();
3285 format!("the swapchain did not come: {stages:?}")
3286 }
3287 };
3288 if matches!(self.m.gout.dev, Dev::Never(_)) {
3289 self.gp_skip(&why);
3290 } else {
3291 self.fail(why.as_bytes());
3292 }
3293 }
3294 return;
3295 }
3296 if word_eq(a, "off").is_some() {
3297 self.m.gout.script = false;
3298 self.gp_wait_jobs(3000);
3299 let s = &mut self.m.gout;
3300 s.wins.clear();
3301 s.images.clear();
3302 s.image_bytes = 0;
3303 s.canvases.clear();
3304 s.tile_free.clear();
3305 s.tile_bytes = 0;
3306 s.dev = Dev::None;
3307 s.want = false;
3308 for pw in s.script_pw.iter_mut() {
3309 if *pw >= 0 {
3310 plat::win_destroy(*pw);
3311 *pw = -1;
3312 }
3313 }
3314 script_pump();
3315 return;
3316 }
3317 if !self.m.gout.script {
3318 self.fail(b"gpu on first");
3319 return;
3320 }
3321 if word_eq(a, "wait").is_some() {
3322 if self.m.gout.skip {
3323 return;
3324 }
3325 let (t, _) = super::runtime::parse_int(next_word(a));
3326 self.render();
3327 self.present();
3328 if !self.gp_script_wait(if t > 0 { t as u64 } else { 20_000 }) {
3329 self.fail(b"not on the swapchain");
3330 }
3331 return;
3332 }
3333 if word_eq(a, "lose").is_some() {
3334 self.gpu_test_lose();
3335 return;
3336 }
3337 if let Some(n) = word_eq(a, "budget") {
3338 let (v, _) = super::runtime::parse_int(skip_ws(n));
3339 self.gpu_test_image_budget(v.max(0) as u64);
3340 return;
3341 }
3342 if let Some(n) = word_eq(a, "tilebudget") {
3343 let (v, _) = super::runtime::parse_int(skip_ws(n));
3344 self.gpu_test_canvas_budget(v.max(0) as u64);
3345 return;
3346 }
3347 if let Some(r) = word_eq(a, "shot") {
3348 let r = skip_ws(r);
3349 self.gp_shot(&String::from_utf8_lossy(r).into_owned());
3350 return;
3351 }
3352 if word_eq(a, "settle").is_some() {
3353 if !self.m.gout.skip {
3354 self.gpu_canvas_settle();
3355 self.render();
3356 self.present();
3357 }
3358 return;
3359 }
3360 if word_eq(a, "hold").is_some() {
3361 let (t, _) = super::runtime::parse_int(next_word(a));
3362 let dl = plat::time_us() + t.max(0) as u64 * 1000;
3363 while plat::time_us() < dl {
3364 script_pump();
3365 self.gp_tick();
3366 plat::gpu_wait(plat::time_us() + 10_000);
3367 }
3368 return;
3369 }
3370 if word_eq(a, "print").is_some() {
3371 let o = self.gpu_output(self.gp_scoped() as u32);
3372 plat::write_stdout(format!("gpu: {o:?}\n").as_bytes());
3373 return;
3374 }
3375 let Some(r) = word_eq(a, "expect") else {
3376 self.fail(b"unknown gpu command");
3377 return;
3378 };
3379 let r = skip_ws(r);
3380 if self.m.gout.skip {
3381 return;
3382 }
3383 self.render();
3384 self.present();
3385 let i = self.gp_scoped();
3386 let needs_gpu = ["gpu", "apart", "tiles", "layers", "images"].iter().any(|w| word_eq(r, w).is_some());
3389 if needs_gpu && self.m.gout.win(i).is_some_and(|w| w.stage != Stage::Gpu && kept_at_other_size(w)) {
3390 plat::write_stdout(format!("note: gpu expect {}: the system keeps the window at another size (no result)\n", String::from_utf8_lossy(r).trim_end()).as_bytes());
3391 return;
3392 }
3393 if word_eq(r, "gpu").is_some() || word_eq(r, "cpu").is_some() {
3394 let want_gpu = word_eq(r, "gpu").is_some();
3395 let on = self.gpu_output(i as u32).on;
3396 if on != want_gpu {
3397 self.fail(if on { b"on the swapchain" } else { b"on the CPU path" });
3398 }
3399 } else if word_eq(r, "same").is_some() {
3400 self.gp_expect_same();
3401 } else if word_eq(r, "window").is_some() {
3402 self.gp_expect_pixels(i, false);
3403 } else if word_eq(r, "screen").is_some() {
3404 self.gp_expect_pixels(i, true);
3405 } else if let Some(n) = word_eq(r, "upload") {
3406 let (max, _) = super::runtime::parse_int(skip_ws(n));
3407 let bytes = self.m.gout.win(i).map_or(0, |w| core::mem::take(&mut w.out.upload_bytes));
3408 if bytes > max as u64 {
3409 self.fail(format!("{bytes} bytes uploaded").as_bytes());
3410 }
3411 } else if let Some(n) = word_eq(r, "base") {
3412 let (max, _) = super::runtime::parse_int(skip_ws(n));
3413 let bytes = self.m.gout.win(i).map_or(0, |w| core::mem::take(&mut w.out.upload_base));
3414 if bytes > max as u64 {
3415 self.fail(format!("{bytes} bytes uploaded to the base texture").as_bytes());
3416 }
3417 } else if let Some(n) = word_eq(r, "raster") {
3418 let (max, _) = super::runtime::parse_int(skip_ws(n));
3419 let px = self.m.gout.win(i).map_or(0, |w| core::mem::take(&mut w.out.base_px));
3420 if px > max as u64 {
3421 self.fail(format!("{px} pixels of the base layer drawn").as_bytes());
3422 }
3423 } else if let Some(n) = word_eq(r, "layers") {
3424 let (want, _) = super::runtime::parse_int(skip_ws(n));
3425 let have = self.gpu_output(i as u32).layer_textures;
3426 if have != want as u32 {
3427 self.fail(format!("{have} layer textures").as_bytes());
3428 }
3429 } else if let Some(n) = word_eq(r, "images") {
3430 let (want, _) = super::runtime::parse_int(skip_ws(n));
3431 let have = self.gpu_output(i as u32).image_textures;
3432 if have != want as u32 {
3433 self.fail(format!("{have} image textures").as_bytes());
3434 }
3435 } else if let Some(n) = word_eq(r, "apart") {
3436 let (want, _) = super::runtime::parse_int(skip_ws(n));
3437 let have = self.gpu_output(i as u32).canvas_apart;
3438 if have != want as u32 {
3439 self.fail(format!("{have} canvases apart").as_bytes());
3440 }
3441 } else if let Some(n) = word_eq(r, "tiles") {
3442 let (want, _) = super::runtime::parse_int(skip_ws(n));
3443 let have = self.gpu_output(i as u32).canvas_tiles;
3444 if have != want as u32 {
3445 self.fail(format!("{have} canvas tiles").as_bytes());
3446 }
3447 } else if let Some(n) = word_eq(r, "fills") {
3448 let (max, _) = super::runtime::parse_int(skip_ws(n));
3449 let s = &mut self.m.gout;
3450 let fills = s.tile_fills - core::mem::replace(&mut s.tile_marked, s.tile_fills);
3451 if fills > max as u64 {
3452 self.fail(format!("{fills} tiles drawn").as_bytes());
3453 }
3454 } else if word_eq(r, "nodiff").is_some() {
3455 let o = self.gpu_output(i as u32);
3456 let px = o.canvas_diff_px - core::mem::replace(&mut self.m.gout.diff_marked, o.canvas_diff_px);
3457 if px != 0 {
3458 self.fail(format!("{px} pixels of a canvas's tiles differ, by {} at most in this window so far", o.canvas_diff_max).as_bytes());
3459 }
3460 } else {
3461 self.fail(b"unknown gpu expectation");
3462 }
3463 }
3464
3465 fn gp_shot(&mut self, file: &str) {
3467 if self.m.gout.skip {
3468 return;
3469 }
3470 self.render();
3471 self.present();
3472 let i = self.gp_scoped();
3473 self.gp_settle_all();
3474 let mut s = core::mem::take(&mut self.m.gout);
3475 let mut raw = None;
3476 let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
3477 let (mut w_px, mut h_px) = (0, 0);
3478 if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
3479 (w_px, h_px) = (fb.w, fb.h);
3480 w.rb_keep = Some(Vec::new());
3481 for _ in 0..SHOW_TRIES {
3482 match show(w, core, fb) {
3483 Ok(Shown::Wait) => plat::gpu_wait(plat::time_us() + 4_000),
3484 _ => break,
3485 }
3486 }
3487 raw = w.rb_keep.take().filter(|v| !v.is_empty());
3488 if core.format == Format::Rgba8Unorm {
3489 if let Some(v) = raw.as_mut() {
3490 for p in v.chunks_exact_mut(4) {
3491 p.swap(0, 2);
3492 }
3493 }
3494 }
3495 }
3496 self.m.gout = s;
3497 script_pump();
3498 let saved = raw.is_some_and(|v| {
3499 let mut sf = Surface::new(w_px, h_px, FMT_XRGB);
3500 sf.px.copy_from_slice(&v);
3501 Ui::save_png(&sf, file)
3502 });
3503 if !saved {
3504 self.fail(b"no presented image (gpu on fast reads nothing back)");
3505 }
3506 }
3507
3508 fn gp_expect_same(&mut self) {
3511 if self.m.gout.canvases.iter().any(|c| c.apart) {
3512 self.gpu_canvas_settle();
3513 self.render();
3514 self.present();
3515 }
3516 self.gp_settle_all();
3517 let mut s = core::mem::take(&mut self.m.gout);
3518 let mut msg = None;
3519 let mut any = false;
3520 for i in 0..s.wins.len() {
3521 let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
3522 if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
3523 if w.stage != Stage::Gpu {
3524 continue;
3525 }
3526 any = true;
3527 let frames = w.out.verified;
3528 for _ in 0..SHOW_TRIES {
3529 match show(w, core, fb) {
3530 Ok(Shown::Wait) => {
3531 plat::gpu_wait(plat::time_us() + 4_000); }
3533 Ok(_) => break,
3534 Err(e) => {
3535 msg = Some(format!("{e}"));
3536 break;
3537 }
3538 }
3539 }
3540 if w.out.verified == frames && msg.is_none() {
3541 msg = Some(format!("window {i}: no frame presented"));
3542 }
3543 if w.out.verify_bad != 0 {
3544 msg = Some(format!("window {i}: {} of {} presented images differ", w.out.verify_bad, w.out.verified));
3545 w.out.verify_bad = 0;
3546 }
3547 }
3548 }
3549 if !any && msg.is_none() {
3550 if s.wins.iter().flatten().any(kept_at_other_size) {
3551 plat::write_stdout(b"note: gpu expect same: the system keeps the window at another size (no result)\n");
3552 } else {
3553 msg = Some(String::from("no window on the swapchain"));
3554 }
3555 }
3556 self.m.gout = s;
3557 self.m.gout.diff_marked = self.gpu_output(self.gp_scoped() as u32).canvas_diff_px;
3558 script_pump();
3559 if let Some(m) = msg {
3560 self.fail(m.as_bytes());
3561 }
3562 }
3563
3564 fn gp_expect_pixels(&mut self, i: usize, screen: bool) {
3566 self.gp_settle_all();
3567 let pw = self.m.gout.script_pw[i & (super::consts::WIN_MAX - 1)];
3568 if pw < 0 {
3569 self.fail(b"no window");
3570 return;
3571 }
3572 if !cfg!(windows) && screen {
3576 let mut s = core::mem::take(&mut self.m.gout);
3577 let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
3578 if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
3579 for _ in 0..SHOW_TRIES {
3580 if w.stage != Stage::Gpu || !matches!(show(w, core, fb), Ok(Shown::Wait)) {
3581 break;
3582 }
3583 plat::gpu_wait(plat::time_us() + 4_000);
3584 }
3585 }
3586 self.m.gout = s;
3587 }
3588 for _ in 0..SHOW_TRIES {
3590 let open = self.m.gout.wins.get(i).and_then(|w| w.as_ref()).is_some_and(|w| w.pending || w.sc.as_ref().is_some_and(|sc| sc.busy()));
3591 if !open {
3592 break;
3593 }
3594 script_pump();
3595 self.gp_tick();
3596 plat::gpu_wait(plat::time_us() + 4_000);
3597 }
3598 for _ in 0..12 {
3600 script_pump();
3601 self.gp_tick();
3602 plat::gpu_wait(plat::time_us() + 8_000);
3603 }
3604 let Some(fb) = self.uiw_parts(i as u32).map(|p| &p.rt.fb) else { return };
3605 let (w, h) = (fb.w, fb.h);
3606 let px = if screen && cfg!(windows) {
3607 plat::win_get_rect(pw).and_then(|r| plat::win_test_screen(r[0], r[1], w, h))
3608 } else {
3609 plat::win_test_image(pw).filter(|x| x.0 == w && x.1 == h).map(|x| x.2)
3610 };
3611 let Some(px) = px else {
3612 if cfg!(windows) {
3613 self.fail(b"no picture");
3614 } else {
3615 plat::write_stdout(b"note: gpu expect screen: no way to read the window back here (no result)\n");
3617 }
3618 return;
3619 };
3620 let mut bad = 0u64;
3621 let mut first = (0, 0);
3622 let mut some = Vec::new(); let row = w as usize * 4;
3624 for y in 0..h as usize {
3625 let a = &px[y * row..y * row + row];
3626 let b = &fb.px[y * fb.stride as usize..y * fb.stride as usize + row];
3627 for (x, (p, q)) in a.chunks_exact(4).zip(b.chunks_exact(4)).enumerate() {
3628 let corner = screen && (x < 8 || x + 8 >= w as usize) && (y < 8 || y + 8 >= h as usize);
3630 if p[..3] != q[..3] && !corner {
3631 if bad == 0 {
3632 first = (x, y);
3633 }
3634 if bad % 4096 == 0 {
3635 some.push([x as i32, y as i32, 0, 0]);
3636 }
3637 bad += 1;
3638 }
3639 }
3640 }
3641 if bad != 0 {
3642 let total = w as u64 * h as u64;
3643 if screen && some.iter().any(|&p| plat::win_test(plat::WT_FROM_POINT, pw, p) != 1) {
3645 plat::write_stdout(format!("note: gpu expect screen: {bad} of {total} pixels differ: another window lies on top of ours\n").as_bytes());
3646 } else {
3647 self.fail(format!("{bad} pixels differ, first at {}, {}", first.0, first.1).as_bytes());
3648 }
3649 }
3650 }
3651}
3652
3653fn unseen(w: &Win) -> bool {
3655 w.hidden || plat::win_state(w.pw) & plat::WST_HIDDEN != 0
3656}
3657
3658fn kept_at_other_size(w: &Win) -> bool {
3660 plat::win_size(w.pw) != w.size && plat::time_us() >= w.stable_at + KEPT_US
3661}
3662
3663fn script_pump() {
3665 plat::wait(0);
3666 let mut keep: Vec<plat::Event> = vec![];
3667 while let Some(e) = plat::evq_pop() {
3668 if e.typ == plat::E_WAKE {
3669 keep.push(e);
3670 }
3671 }
3672 for e in keep {
3673 plat::evq_push(e);
3674 }
3675}