Skip to main content

ui/
gpu_out.rs

1// gpu_out -- UI windows on the screen through a swapchain (Windows; Linux in programs against glibc;
2// Android; the browser: docs/GPU.md "Ausgabe der UI", "Ausgabe unter Linux", "Ausgabe unter Android",
3// "Ausgabe im Browser")
4//
5// The UI rasterizes on the CPU as ever, the framebuffer stays the truth.  This module only shows
6// it: the dirty rectangles the runtime computed go into a texture of the window, a triangle draws
7// the texture to the swapchain image, the image is presented.  All windows share one device.
8//
9//   gp_window_mode()             window_main: is the output wanted (jfgui_GPU, Remote Desktop)
10//   gp_present(cur, pw, n)       uiw_present: true when the GPU showed the frame, else the caller
11//                                presents through the CPU path and calls gp_cpu_done(cur, pw)
12//   gp_tick() / gp_deadline()    the window loop: finished jobs, frames that found no free image;
13//                                the time of the next look (None: nothing to look at, no wake-up)
14//   gp_window_gone(i)            before window i's plat window is destroyed
15//   gp_window_show(i, show)      before window i is hidden or shown again
16//   gp_shutdown()                window_main's end
17//   sc_gpu(app, args)            the script command `gpu` (below)
18//   Ui::gpu_output(win)          what a window shows through (status and counters for tests)
19//
20// Stages of a window: Cpu (plat::win_present: GDI, as without a GPU) -> Job (its swapchain is
21// being built on a worker thread, the CPU path goes on) -> Switch (the whole framebuffer uploaded
22// and presented on the hidden child, then the child shown) -> Gpu.  Back to Cpu: the window's size
23// changed (also the modal size loop: every frame of a resize is a CPU frame, drawn before
24// SetWindowPos returns, so nothing lags behind the frame; the swapchain is rebuilt when the size
25// has settled), or an error.  Going back shows the framebuffer through the CPU path first, then
26// hides the child: no gap, no wrong frame.  The first frames of a program are CPU frames: the
27// instance, device and swapchain (hundreds of ms the first time) are made on a worker thread that
28// starts after the first frame, so the first frame never waits for them.
29//
30// A hidden window goes back to Cpu too: a swapchain's image does not come back with its window (white
31// until the next present, measured on Windows, and a frame that did not change presents nothing).  What
32// the CPU path holds does come back, the swapchain follows as after a resize; nothing is built or
33// switched for a window nobody sees (unseen: the UI hid it, or the system with its minimised owner).
34//
35// The swapchain lives on a child window (plat win_gpuout.rs): once a swapchain has presented on a
36// window GDI stays invisible there, also after the swapchain is gone.
37//
38// Linux: the same code.  A Swapchain there is a set of dma-bufs lib/gpu makes itself (vk_dmabuf.rs,
39// no VkSurface: that would want libwayland or libxcb), plat attaches them with its own protocol code
40// (linux_gpuout.rs); shared memory and dma-bufs take turns on one wl_surface or X window, so there is
41// no child and win_gpu_child only says what the display accepts.  The differences this code sees: a
42// frame reaches the window after the GPU has finished it (flush, busy: the loop looks every ms while
43// one is on its way), the next one starts when the display has answered the last (next_look: no
44// polling, the answer ends the wait; the frame stays in the texture and its newest state is drawn
45// then), the compositor gets the frame's rectangles (damage), a worker may not ask the window system
46// (Surface::track_size).  What cannot be read back is said by the tests: no client reads a Wayland
47// window, Xwayland's GetImage of a flipped window lags one frame.
48//
49// Android: the same code again.  The host's View with its Bitmap is the CPU path and stays below; the
50// swapchain lives on a SurfaceView above it (plat android_gpu.rs), the analogue of Windows' child
51// window: the surface exists while the View is visible, shows nothing until its first present and
52// then covers the Bitmap.  Going back: the frame to the Bitmap, the SurfaceView away, and with it the
53// surface and swapchain (win_gpu_child_show).  The system can also take the surface (the activity
54// stops): gp_surface_gone, before the host's callback returns.  win_gpu_child says None for a while
55// (the surface is on its way, a tool window or popup shows on the canvas below): win_gpu_child_later,
56// the host wakes the loop.
57//
58// The browser: the same code once more, through WebGPU (lib/gpu web_*.rs, web/gpu.js).  The UI runs in a
59// worker in all three bundles and WebGPU's objects stay on that thread: there are no job threads, the jobs
60// run on this thread when the browser has answered (S::web_start: requestAdapter and requestDevice
61// answer later, the app goes on through the CPU path meanwhile).  The page's #screen is the CPU path and
62// stays below; a canvas above it, an OffscreenCanvas here, is the analogue of the SurfaceView: it shows
63// nothing until its first frame, then covers #screen; each period on the GPU has a new one (plat
64// web_gpu.rs).  Before a device is used, a small picture through all three pipelines is read back and
65// compared: a browser whose WebGPU draws wrongly never shows a frame (S::web_probe).  After the switch the
66// page looks at what reached the canvas (watchGpuCanvas in web/loader.js): nothing there, and the CPU path
67// takes over for good (plat::win_gpu_child_blank).  Reading back comes
68// later here: jfgui_GPU_VERIFY compares when the browser has answered (verify_poll).  The shaders go as
69// WGSL (gpu_out.wgsl), no SPIR-V in the bundle.  Runs with ?agent=1 stay on the CPU path like scripts,
70// unless ?gpu=1 asks; no script command here.
71//
72// Presents never wait: Swapchain::try_acquire.  A frame without a free image stays in the texture
73// and is shown by gp_tick or with the next frame (gp_deadline: 2 ms); so is a frame less than 4 ms
74// after the last present (nothing shows faster, the GPU need not draw it).  Nothing runs while the
75// app is idle: no thread, no timer, no frame.  Minimised windows upload nothing; they get the whole
76// frame when they come back.
77//
78// Popups (plat::popup: small, short-lived windows with a window region) and tooltips stay on the
79// CPU path: a swapchain takes tens of ms, longer than many of them live.
80//
81// Layers (G3b).  The framebuffer stays the truth: all layers flattened, byte for byte what the CPU
82// path holds.  What the GPU gets apart: a window's base texture holds the base layer only; each
83// layer above it with commands has a texture of its own that holds, in the 32 x 32 px cells the
84// layer touches, the pixels up to and including that layer, flattened on the CPU over what really
85// lies below (shadows, translucent widgets, subpixel text, RTL: nothing to get wrong, the same
86// kernels on the same pixels).  The render pass draws the base, then these cells opaque in layer
87// order; the GPU blends nothing (blending a premultiplied layer differs from drawing directly by
88// 1 .. 2 levels in about an eighth of its translucent pixels, measured, and subpixel text needs an
89// opaque target; docs/GPU.md).  Beside each texture the pixels below the layer stay in a Store,
90// cell by cell, so a layer that moves or closes puts them back in the framebuffer: nothing below
91// it is drawn again or uploaded; closing costs no upload at all.  dirty.rs tells per cell which
92// layer changed first (W::through): gp_split_draw restores below that layer, then draws and uploads
93// only from there on.  Something changing below an open layer draws what lies above it in those
94// cells, as ever.  Textures and stores exist while their layer has content and LINGER_US longer
95// (a menu closed and another opened need no new memory).
96//
97// Images (G3b).  A big opaque Cmd::Image of the base layer that has cost as much in redraws as its
98// upload would (rent or buy: a static image never takes GPU memory) becomes a texture, cached by
99// surface identity and version, under a budget with LRU eviction; it goes up in parts over the
100// frames that draw it (IMAGE_STEP_BYTES, the CPU draws it meanwhile).  In the cells it fills, as the
101// last command there, its pixels are drawn by the GPU with Dc::blit_scaled's integer mapping: the
102// CPU neither draws nor uploads them (Split::owed: the framebuffer lacks them; gp_settle draws them
103// before anybody reads it).  Everything else, also the cells at the image's edge or with something
104// on top of it, goes the usual way.
105//
106// Canvases (G3c, canvas.rs).  While a canvas's view moves and its content stays, its content comes
107// from tiles: TILE x TILE px textures of the content at one scale (a Level), drawn once by
108// CanvasContent::draw_view on this thread (at most FILL_US per frame) and kept under a budget with
109// LRU eviction.  The render pass draws them with the transform: by copy (fs) at the level's scale
110// and whole pixels away, else bilinear (fs_tile).  In the cells the canvas ends up on top of (also
111// those it only partly covers, with the scissor at its edge) the CPU neither draws nor uploads it:
112// its command's hash stands only for where it lies (dirty.rs), Split::owed and Split::stale as for
113// images, gp_settle runs its Draw before anybody reads the framebuffer.  Cells with something over
114// it and cells without their tiles go the CPU's way: its Draw, the overlay, the upload.  SETTLE_US
115// after the last move gp_tick asks for a frame, the canvas is no longer apart, the CPU draws what
116// the view shows: at rest the picture is the CPU path's.  A frame that changes the content (new
117// CanvasDesc::ver, ui.canvas_gpu_damage) ends the gesture at once and costs no tile; a tile is only
118// drawn when a view moves over it.  Nothing of it exists before the first move, and nothing SETTLE
119// + CANVAS_UNUSED_US after the last one.  Right-to-left windows draw canvases on the CPU.
120//
121// Views (G5e1, gpu_view.rs).  A view of GPU content (ui.gpu_view: a 3D scene between widgets) draws its frames
122// on this device into a texture of its size (gp_views_begin, at render time before the cells are hashed: a new
123// ver, size or device draws one, nothing else does).  Where the window is on its swapchain and the view ends up
124// on top it is a quad like an image's (QuadSrc::View, drawn by the shader that copies: `fs` at its place), its
125// command's hash stands for where it lies as a canvas apart's (dirty.canv), and a new frame counts as a canvas's
126// move: its cells owed and stale, the cells with something over it and under a layer drawn again by the CPU,
127// whose Draw reads the frame back (the only way the CPU gets it: a copy to a read-back buffer, waited for).
128// VIEW_REST_US after the last frame gp_tick settles the window once: at rest the framebuffer holds the picture.
129// Anywhere else the view's command hashes its frame's number, so each new frame is drawn by the CPU, which reads
130// it back.  A view's frame is bound to the display (G5e3, docs/GPU.md "3D-Ansicht"): while its window is on its
131// swapchain with a frame not shown yet (Win::pending), a new ver waits; gp_views_wake asks for a render once the
132// window's frame has been shown, at the latest VIEW_HOLD_US after the first frame waited (a display that never
133// answers does not stop the view).  Without this a view drawn as fast as the app can starved the compositor on the
134// same GPU (Intel under Linux: no frame presented at all).  AF_GPU_3D makes the device at level Voll on the fastest
135// adapter; without the flag views work on the Basis device of the frugal adapter.  Without an output in this run (not
136// wanted, never possible) views make a device of their own (gp_view_device).  A lost device detaches every content
137// (gp_views_gone) and attaches it to the next one.
138//
139// A lost device or any other failure: every window back to the CPU path (the framebuffer holds the
140// frame), a new device after 2 s, 4 s, ... 5 min (jfgui_GPU_RETRY_MS: the first wait).  No Vulkan, no
141// usable adapter or surface, a display that takes or imports no dma-bufs: the CPU path for good,
142// silently.  plat::gpu_out_state tells the platform how the first start went (Android keeps it across
143// runs: a driver that killed the program does not get a third chance).
144//
145// Environment: jfgui_GPU=0|1 (off, on; default: DEFAULT_ON, DEFAULT_ON_LINUX, DEFAULT_ON_BROWSER; Android:
146// GalleryActivity always sets it, GPU_DEFAULT there; the browser: ?gpu=0|1), jfgui_GPU_VERIFY=1 (every presented
147// image read back and compared with the framebuffer), jfgui_GPU_TRACE=1 (stderr: the stages and
148// their times), jfgui_GPU_LAYERS=0 (one texture per window: no layers, no images),
149// jfgui_GPU_IMAGES=0 (no images), jfgui_GPU_IMAGE_MB=n (the images' budget), jfgui_GPU_CANVAS=0 (no
150// canvas uses tiles), jfgui_GPU_CANVAS_CPU=a,b (the canvases named so draw on the CPU),
151// jfgui_GPU_CANVAS_PAN=a,b (they only pan from tiles), jfgui_GPU_CANVAS_ZOOM=a,b (they also zoom from
152// tiles, whatever the widget chose), jfgui_GPU_CANVAS_MB=n (the tiles' budget),
153// jfgui_GPU_CANVAS_SETTLE_MS=n (the time until the CPU's picture follows a move), jfgui_GPU_OUTPUT=0 (the
154// windows through the CPU path, the GPU stays for views, which read every frame back), jfgui_GPU_VIEWS=0 (no view
155// is drawn by the output's render pass: every frame read back into the framebuffer);
156// lib/gpu's jfgui_GPU_PRESENT, jfgui_GPU_ADAPTER, jfgui_GPU_VALIDATE, ...
157//
158// Script command `gpu` (headless runs stay on the CPU path without it; on other targets (a static
159// Linux program, the browser), or without a GPU, it prints a SKIP once and does nothing):
160//   gpu on [fast]          a real window per UI window (shown without activation) with
161//                          the output, every presented image compared with the framebuffer (fast:
162//                          not compared, for measurements), a render after every input edge; waits
163//                          for the swapchain; a canvas stays apart until `gpu settle` or `same`
164//   gpu off                windows closed
165//   gpu wait [MS]          until the scoped windows are on the swapchain (after `size`, `gpu lose`)
166//   gpu expect gpu|cpu     the main window's stage
167//   gpu settle             every canvas goes back to the CPU's picture now
168//   gpu expect same        every canvas settled, every window presents once more: the image read
169//                          back = the framebuffer byte for byte (x byte: 255), and no frame differed
170//                          since the last check (a canvas's scaled tiles count as GpuOutput::canvas_diff_*)
171//   gpu expect screen      the composed screen at the window = the framebuffer (another window on
172//                          top of ours is a note, not a failure); X11: what the server holds for the
173//                          window; Wayland: a note, no client can read it
174//   gpu expect window      the window's DC = the framebuffer (the CPU path's pixels); Linux as `screen`
175//   gpu expect upload N    at most N bytes uploaded since the last check
176//   gpu expect base N      at most N bytes of them to the base texture since the last check
177//   gpu expect raster N    at most N pixels of the base layer drawn since the last check
178//   gpu expect layers N    N layers above the base have a texture now
179//   gpu expect images N    N image textures exist
180//   gpu expect apart N     N canvases of the window come from tiles now
181//   gpu expect tiles N     N canvas tiles exist
182//   gpu expect fills N     at most N tiles were drawn since the last check
183//   gpu expect nodiff      no pixel of a canvas's tiles differed from the CPU's since the last
184//                          `same` or `nodiff`
185//   gpu budget BYTES       the images' budget
186//   gpu tilebudget BYTES   the tiles' budget
187//   gpu shot FILE          the image the window presents now (read back: also what a gesture's
188//                          tiles show; `shot` writes the framebuffer)
189//   gpu lose               the device behaves as lost from now on (Device::test_lose)
190//   gpu hold MS            real time passes, the windows stay as they are (a look from outside)
191//   gpu print              the counters
192
193#[cfg(not(target_arch = "wasm32"))]
194use alloc::boxed::Box;
195use alloc::collections::BTreeMap;
196use alloc::format;
197use alloc::rc::{Rc, Weak};
198use alloc::string::String;
199use alloc::sync::Arc;
200use alloc::vec;
201use alloc::vec::Vec;
202use core::sync::atomic::{AtomicU32, Ordering};
203
204use gpu::{
205    Access, Adapter, BindEntry, BindGroup, BindGroupDesc, BindGroupLayout, Binding, BindingKind, Buffer, BufferDesc, BufferUsage,
206    ColorAttachment, ColorTarget, Cull, DataLayout, Device, DeviceDesc, Format, GpuError, Instance, Layout, Level, Load, MemoryKind,
207    PipelineLayoutDesc, PresentMode, RenderPassDesc, RenderPipeline, RenderPipelineDesc, Resource, ShaderModule, ShaderStage, Stages,
208    Swapchain, SwapchainDesc, Texture, TextureDesc, TextureRegion, TextureUsage, Topology, TryAcquire,
209};
210use rt::sync::SpinLock;
211
212use super::auto::{next_word, skip_ws, word_eq};
213use super::canvas::{CanvasGpu, CanvasView};
214use super::cmd::{render_layer, Cmd, DrawRef, Gfx};
215use super::consts::*;
216use super::dirty::TOP_BLOCKED;
217use super::gpu_view::{Entry, GpuContent, Own, Target, ViewTex, GPU_VIEW_FORMAT};
218use super::{App, GpuOutput, Mods, Ui};
219use gfx::{Dc, Surface, FMT_A8, FMT_XRGB};
220use text::Text;
221
222/// the output without jfgui_GPU in the environment: on, jfgui_GPU=0 turns it off (docs/GPU.md
223/// "Ausgabe der UI" says what it costs)
224const DEFAULT_ON: bool = true;
225/// the same on Linux, measured apart: a weak GPU may answer differently (docs/GPU.md "Ausgabe unter
226/// Linux"); this line decides
227const DEFAULT_ON_LINUX: bool = true;
228/// the same in the browser, where the numbers differ again (docs/GPU.md "Ausgabe im Browser"): the one
229/// switch for it.  A run with ?agent=1 stays on the CPU path unless ?gpu=1 asks: such runs read #screen.
230/// Phones and tablets stay there too until one has been tested: web/loader.js sets jfgui_GPU=0
231const DEFAULT_ON_BROWSER: bool = true;
232/// the same on macOS and iOS (Metal, AP6): off, the user's decision of 2026-11-11 after the measurements (docs/APPLE.md
233/// AP6d); jfgui_GPU=1 and a script's `gpu on` turn it on
234const DEFAULT_ON_APPLE: bool = false;
235/// the present mode (FIFO when the surface lacks it; jfgui_GPU_PRESENT overrides it): a burst of
236/// frames shows its newest within a refresh, where FIFO lags 3 .. 5 refreshes behind (docs/GPU.md
237/// "Ausgabe der UI")
238const PRESENT: PresentMode = PresentMode::Mailbox;
239/// swapchain images: as fast as three in every measurement, a window's size less GPU memory
240const IMAGES: u32 = 2;
241/// frames closer together than this wait for gp_tick: 250 per second are plenty for a window (us).
242/// (The browser: every frame waits for the gp_tick of its turn; the next tick may be 33 ms away.)
243const MIN_PRESENT_US: u64 = if cfg!(target_arch = "wasm32") { 0 } else { 4_000 };
244/// a window's size is settled when it did not change for this long (us)
245const STABLE_US: u64 = 100_000;
246/// a frame that found no free image is tried again after this long (us)
247const RETRY_US: u64 = 2_000;
248/// while a job runs the loop looks this often (us)
249const JOB_US: u64 = 8_000;
250/// (a driver compiles shaders on this thread; on Linux a stack has exactly this size, on Windows
251/// this is what is committed at the start: docs/GPU.md "Linux")
252#[cfg(not(target_arch = "wasm32"))]
253const WORKER_STACK: usize = if cfg!(windows) { 2 << 20 } else { 8 << 20 };
254/// the first wait before a new device (ms), its longest
255const RETRY_FIRST_MS: u64 = 2_000;
256const RETRY_MAX_MS: u64 = 300_000;
257/// an upload above this keeps a large staging buffer for a while: gp_tick gives it back
258const BIG_UPLOAD: u64 = 128 << 10;
259const TRIM_US: u64 = 1_200_000;
260/// a Remote Desktop session is looked at again after this long (us)
261const REMOTE_LOOK_US: u64 = 10_000_000;
262/// (scripts) a window whose size differs from its frame for this long stays so (us)
263const KEPT_US: u64 = 500_000;
264/// the layers above the base
265const UPPER: usize = L_COUNT - 1;
266/// dirty.rs's cells: what a layer's texture and store are made of
267const CELL: i32 = 32;
268const CELL_ROW: usize = CELL as usize * 4;
269const CELL_BYTES: usize = CELL as usize * CELL_ROW;
270const NO_BLOCK: u32 = u32::MAX;
271/// a layer's texture: its size is rounded up to this (px), half of the spare room on each side
272/// of what it holds now
273const LAYER_STEP: i32 = 256;
274const LAYER_SPARE: i32 = 128;
275/// a layer's texture this many times larger than needed goes
276const LAYER_SHRINK: i64 = 4;
277/// a layer's texture and store stay this long after its last content (us)
278const LINGER_US: u64 = 1_000_000;
279/// the bytes image textures may take (jfgui_GPU_IMAGE_MB)
280const IMAGE_BUDGET: u64 = 256 << 20;
281/// an image goes up with this many bytes per frame: no frame waits for a whole big one
282const IMAGE_STEP_BYTES: u64 = 4 << 20;
283/// images whose redraws are counted
284const IMAGES_KEPT: usize = 64;
285/// an image texture not drawn for this long goes with the next frame (us)
286const IMAGE_UNUSED_US: u64 = 30_000_000;
287/// a canvas's tile: its content, a texel of its neighbours around it (fs_tile reads them)
288const TILE: i32 = 256;
289const TILE_BORDER: i32 = 1;
290const TILE_TEX: i32 = TILE + 2 * TILE_BORDER;
291const TILE_BYTES: u64 = TILE_TEX as u64 * TILE_TEX as u64 * 4;
292/// the bytes canvas tiles may take (jfgui_GPU_CANVAS_MB)
293const CANVAS_BUDGET: u64 = 128 << 20;
294/// a frame draws tiles for at most this long; what is left goes the CPU's way in this frame (us)
295const FILL_US: u64 = 5_000;
296/// this long after a canvas's last move the CPU draws its picture (us)
297const SETTLE_US: u64 = 180_000;
298/// the tiles go this long after the last move (us)
299const CANVAS_UNUSED_US: u64 = 30_000_000;
300/// a level serves a view scaled by this much at most, then the next one is drawn
301const K_MIN: f64 = 0.55;
302const K_MAX: f64 = 1.9;
303/// whole pixels away, up to this much of a pixel: a copy
304const PHASE_TOL: f64 = 1.0 / 64.0;
305/// the scales kept per canvas
306const LEVELS_MAX: usize = 6;
307/// (scripts) a window shows once more, 4 ms between the tries: longer than a display takes to answer
308const SHOW_TRIES: u32 = 300;
309/// (views) this long after a view's last frame the window is settled: its frame read back into the framebuffer (us)
310const VIEW_REST_US: u64 = 180_000;
311/// (views, G5e3) a new frame of a view waits for its window's last frame to be shown at most this long, then it is
312/// drawn anyway (us): a display that never answers costs the view its rate, not its picture
313const VIEW_HOLD_US: u64 = 100_000;
314
315/// the shaders, compiled from gpu_out.wgsl (uigputest --test shaders compares them with what
316/// wgsl::compile makes of it, --gen-shaders writes them again); the browser takes the WGSL itself
317#[cfg(not(target_arch = "wasm32"))]
318mod spv {
319    include!("gpu_out_spv.rs");
320}
321
322/// the embedded shaders of the output and their source (uigputest: the embedding check)
323pub const GPU_OUT_WGSL: &str = include_str!("gpu_out.wgsl");
324#[cfg(not(target_arch = "wasm32"))]
325pub const GPU_OUT_VS: &[u32] = spv::VS;
326#[cfg(not(target_arch = "wasm32"))]
327pub const GPU_OUT_FS: &[u32] = spv::FS;
328#[cfg(not(target_arch = "wasm32"))]
329pub const GPU_OUT_FS_IMAGE: &[u32] = spv::FS_IMAGE;
330#[cfg(not(target_arch = "wasm32"))]
331pub const GPU_OUT_FS_TILE: &[u32] = spv::FS_TILE;
332
333#[derive(Clone, Copy, PartialEq, Eq, Debug)]
334enum Stage {
335    Cpu,
336    Job,
337    Switch,
338    Gpu,
339}
340
341struct Tex {
342    t: Texture,
343    group: BindGroup,
344    w: i32,
345    h: i32,
346}
347
348/// the pixels below a layer, cell by cell, while the layer touches the cell
349#[derive(Default)]
350struct Store {
351    idx: Vec<u32>,
352    px: Vec<u8>,
353    free: Vec<u32>,
354    used: u32,
355}
356
357impl Store {
358    fn has(&self, i: usize) -> bool {
359        self.idx.get(i).is_some_and(|&b| b != NO_BLOCK)
360    }
361
362    fn get(&self, i: usize) -> &[u8] {
363        &self.px[self.idx[i] as usize * CELL_BYTES..][..CELL_BYTES]
364    }
365
366    /// nothing kept, the memory stays
367    fn empty(&mut self) {
368        self.idx.clear();
369        self.px.clear();
370        self.free.clear();
371        self.used = 0;
372    }
373
374    /// room for n cells more without moving what is kept
375    fn reserve(&mut self, n: usize) {
376        self.px.reserve_exact((n + self.used as usize).saturating_mul(CELL_BYTES).saturating_sub(self.px.len()));
377    }
378
379    /// the cell's block, a new one when it has none (cells: the window's)
380    fn put(&mut self, i: usize, cells: usize) -> &mut [u8] {
381        if self.idx.len() != cells {
382            self.empty();
383            self.idx.resize(cells, NO_BLOCK);
384        }
385        let mut b = self.idx[i];
386        if b == NO_BLOCK {
387            b = self.free.pop().unwrap_or_else(|| {
388                let n = (self.px.len() / CELL_BYTES) as u32;
389                self.px.resize(self.px.len() + CELL_BYTES, 0);
390                n
391            });
392            self.idx[i] = b;
393            self.used += 1;
394        }
395        &mut self.px[b as usize * CELL_BYTES..][..CELL_BYTES]
396    }
397
398    fn drop_cell(&mut self, i: usize) {
399        if self.has(i) {
400            self.free.push(self.idx[i]);
401            self.idx[i] = NO_BLOCK;
402            self.used -= 1;
403        }
404    }
405}
406
407/// a layer's texture: holds the window's rectangle ox, oy, w, h
408struct LayerTex {
409    t: Texture,
410    group: BindGroup,
411    ox: i32,
412    oy: i32,
413    w: i32,
414    h: i32,
415    fresh: bool, // nothing uploaded yet (Layout::Undefined)
416}
417
418/// a layer above the base: its texture, the rectangles of its cells (none: it has no content, the
419/// texture and the store linger), the pixels below it
420#[derive(Default)]
421struct Upper {
422    tex: Option<LayerTex>,
423    rects: Vec<[i32; 4]>,
424    store: Store,
425}
426
427/// an image's texture (Rc: a window's last frame keeps drawing an evicted one until its next)
428struct ImageTex {
429    _t: Texture,
430    group: BindGroup,
431}
432
433/// an image's texture on the way up: the rows that went up so far
434struct ImageLoad {
435    t: Texture,
436    group: BindGroup,
437    row: i32,
438}
439
440/// an image the output has seen: what its redraws cost so far, its texture
441struct Image {
442    key: usize,
443    weak: Weak<Surface>, // holds the address: no other surface comes to this identity
444    ver: u32,
445    bytes: u64,
446    acc: u64,
447    opaque: Option<bool>,
448    tex: Option<Rc<ImageTex>>,
449    load: Option<ImageLoad>,
450    tick: u64,
451    used_us: u64,
452}
453
454/// a tile's texture (Rc: a window's last frame keeps drawing an evicted one until its next)
455struct TileTex {
456    t: Texture,
457    group: BindGroup,
458    fresh: core::cell::Cell<bool>, // nothing uploaded yet (Layout::Undefined)
459}
460
461struct Tile {
462    tex: Rc<TileTex>,
463    tick: u64,
464}
465
466/// a canvas's tiles at one scale: grid pixel g shows the content point (g - f) / scale
467struct TileLevel {
468    scale: f64,
469    fx: f64,
470    fy: f64,
471    tiles: BTreeMap<(i32, i32), Tile>,
472}
473
474/// a canvas the output has seen
475struct Canvas {
476    win: usize,
477    id: u32,
478    ver: u32,
479    /// the view of the last frame and of the one before it
480    view: CanvasView,
481    prev: CanvasView,
482    known: bool,
483    damaged: bool,
484    /// its view comes from tiles, until this time; it moved in this frame
485    apart: bool,
486    until: u64,
487    moved: bool,
488    levels: Vec<TileLevel>,
489}
490
491/// a tile in a window's frame: its texture, its box in the window (x0, y0, x1, y1), the shader's
492/// place
493struct TileDraw {
494    tex: Rc<TileTex>,
495    b: [i32; 4],
496    place: [i32; 4],
497}
498
499/// what a quad draws: an image with its steps (Dc::blit_scaled), or a canvas's tiles (copied or
500/// scaled; loose: a difference from the CPU's picture is no failure) with its command's Draw, its box and
501/// what of it shows (x0, y0, x1, y1: a scrolling parent clips it, the frame's clip stack is gone later)
502enum QuadSrc {
503    Image { tex: Rc<ImageTex>, src: Rc<Surface>, w: i32, h: i32, step: [u32; 2] },
504    Canvas { f: DrawRef, w: i32, h: i32, vis: [i32; 4], tiles: Vec<TileDraw>, copy: bool, loose: bool },
505    /// (G5e1) a view's frame: its target, one to one at the quad's place; its Draw reads the frame back
506    View { tex: Rc<ViewTex>, f: DrawRef, w: i32, h: i32, vis: [i32; 4] },
507}
508
509/// an image or a canvas drawn by the GPU in a window's frame: its place, the rectangles of its cells
510struct Quad {
511    x: i32,
512    y: i32,
513    src: QuadSrc,
514    rects: Vec<[i32; 4]>,
515}
516
517/// what a window on the swapchain keeps apart (the module's header)
518#[derive(Default)]
519struct Split {
520    upper: [Upper; UPPER],
521    /// a layer above the base has cells apart
522    active: bool,
523    /// when the memory of layers without content goes (0: none lingers)
524    linger_at: u64,
525    /// the base texture holds no pixels of a layer above it
526    clean: bool,
527    /// this frame's uploads are done
528    drawn: bool,
529    err: Option<GpuError>,
530    // ---- per cell, this frame: the lowest layer that changed (L_COUNT: none); the layers that
531    // touch the cell (bits); the rectangles of a stage
532    m: Vec<u8>,
533    nr: Vec<u8>,
534    rects: Vec<[i32; 4]>,
535    // ---- per cell: the image on top of it (quad + 1) the GPU draws; the framebuffer lacks its
536    // pixels; the base texture lacks its pixels
537    cols: usize,
538    rows: usize,
539    hole: Vec<u8>,
540    owed: Vec<bool>,
541    stale: Vec<bool>,
542    any_owed: bool,
543    any_stale: bool,
544    quads: Vec<Quad>,
545    // ---- per cell: a canvas covers it only partly (the CPU draws the rest); a canvas moved
546    // there in this frame; a canvas moved in this frame
547    part: Vec<bool>,
548    redraw: Vec<bool>,
549    moved: bool,
550}
551
552impl Split {
553    /// layers, images and whatever hangs on them go; the base texture is not clean then
554    fn reset(&mut self) {
555        *self = Split::default();
556    }
557}
558
559/// a window of the output
560struct Win {
561    pw: i32,
562    stage: Stage,
563    sc: Option<Swapchain>, // also on the CPU path: the next build reuses it (oldSwapchain)
564    surf: Option<gpu::Surface>, // (after sc: it goes first)
565    tex: Option<Tex>,
566    rb: Option<Buffer>, // jfgui_GPU_VERIFY: the presented image
567    #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
568    rb_px: Vec<u8>,
569    rb_keep: Option<Vec<u8>>, // `gpu shot`: the image as presented
570    full: bool,    // the texture lacks pixels: the whole frame goes up next
571    undef: bool,   // the texture is new (Layout::Undefined)
572    pending: bool, // the texture holds pixels that were not presented
573    hidden: bool,  // the UI hid it: no swapchain is built or shown for it
574    test_hold: bool, // (Ui::gpu_test_hold) its frames are not shown, as if the display did not answer
575    apart_before: bool, // the last frame had layers, images or canvases apart
576    last_present: u64,
577    job: u32,      // the job in flight
578    lost: bool,    // (Android) the surface went while the job built on it
579    size: (i32, i32),
580    stable_at: u64,
581    fail_at: u64,  // no new swapchain before this time
582    fail_ms: u64,
583    out: GpuOutput,
584    sp: Split,
585    #[cfg(target_arch = "wasm32")]
586    verify_wait: Vec<Verify>,
587}
588
589/// (the browser) a presented image on its way back, the framebuffer of that frame beside it
590#[cfg(target_arch = "wasm32")]
591struct Verify {
592    rb: Buffer,
593    fb: Surface,
594    loose: Vec<[i32; 4]>,
595    /// (G5e2) where the output's pass drew views: not compared.  Their frame reaches the framebuffer here only when its
596    /// read-back has come, later than the image that shows it (gpu_view.rs); at rest the framebuffer has it, which the
597    /// tests compare with the screen
598    late: Vec<[i32; 4]>,
599}
600
601/// (the browser) a new device's small picture on its way back
602#[cfg(target_arch = "wasm32")]
603struct Probe {
604    job: u32,
605    win: usize,
606    core: Core,
607    built: Built,
608    rb: Buffer,
609    px: Vec<u8>,
610    at: u64,
611}
612
613struct Core {
614    inst: Instance,
615    dev: Device,
616    bgl: BindGroupLayout,
617    pipe: RenderPipeline,
618    pipe_image: RenderPipeline,
619    pipe_tile: RenderPipeline,
620    format: Format,
621    name: String,
622}
623
624enum Dev {
625    None,
626    Starting(u32),
627    Ready(Core),
628    Retry(u64),
629    Never(String),
630}
631
632struct Built {
633    surf: gpu::Surface,
634    sc: Swapchain,
635    tex: Texture,
636    group: BindGroup,
637}
638
639enum Done {
640    Device { job: u32, win: usize, r: gpu::Result<(Core, Built)> },
641    Window { job: u32, win: usize, r: gpu::Result<Built> },
642}
643
644struct Jobs {
645    done: SpinLock<Vec<Done>>,
646    running: AtomicU32,
647}
648
649/// the output's state: one per process (Mods)
650pub(crate) struct S {
651    want: bool,
652    verify: bool,
653    trace: bool,
654    script: bool,
655    skip: bool,
656    dev: Dev,
657    wins: Vec<Option<Win>>,
658    jobs: Arc<Jobs>,
659    threads: Vec<plat::Thread>,
660    next_job: u32,
661    retry_ms: u64,
662    ready_at: u64,
663    trim_at: u64,
664    t0: u64,
665    script_pw: [i32; super::consts::WIN_MAX],
666    script_asked: [(i32, i32); super::consts::WIN_MAX],
667    layers_on: bool,
668    images_on: bool,
669    images: Vec<Image>,
670    image_budget: u64,
671    image_bytes: u64,
672    image_uploads: u64,
673    image_hits: u64,
674    image_evicted: u64,
675    tick: u64,
676    look_at: u64,
677    canvases_on: bool,
678    canvases: Vec<Canvas>,
679    canvas_cpu: String,
680    canvas_pan: String,
681    canvas_zoom: String,
682    canvas_budget: u64,
683    canvas_settle_us: u64,
684    canvas_unused_us: u64,
685    tile_bytes: u64,
686    tile_free: Vec<Rc<TileTex>>,
687    tile_sf: Surface,
688    tile_fills: u64,
689    tile_fill_us: u64,
690    tile_evicted: u64,
691    canvas_settles: u64,
692    canvas_free_at: u64,
693    said_ok: bool, // plat::gpu_out_state
694    tile_marked: u64, // `gpu expect fills`
695    diff_marked: u64, // `gpu expect nodiff`
696    /// (views) counts the devices views can draw on (the output's, their own): a new one takes their contents along
697    dev_gen: u32,
698    /// (views) the output's render pass may draw them (jfgui_GPU_VIEWS); the app shows them (AF_GPU_3D)
699    views_on: bool,
700    want3d: bool,
701    #[cfg(target_arch = "wasm32")]
702    web_inst: Option<Instance>, // adapter and device asked for, on their way
703    #[cfg(target_arch = "wasm32")]
704    probe: Option<Probe>,
705}
706
707impl Default for S {
708    fn default() -> S {
709        S {
710            want: false,
711            verify: false,
712            trace: false,
713            script: false,
714            skip: false,
715            dev: Dev::None,
716            wins: Vec::new(),
717            // (the browser: one thread, S::spawn runs a job where it is called; the Arc is the other targets')
718            #[allow(clippy::arc_with_non_send_sync)]
719            jobs: Arc::new(Jobs { done: SpinLock::new(Vec::new()), running: AtomicU32::new(0) }),
720            threads: Vec::new(),
721            next_job: 1,
722            retry_ms: 0,
723            ready_at: 0,
724            trim_at: 0,
725            t0: 0,
726            script_pw: [-1; super::consts::WIN_MAX],
727            script_asked: [(0, 0); super::consts::WIN_MAX],
728            layers_on: false,
729            images_on: false,
730            images: Vec::new(),
731            image_budget: IMAGE_BUDGET,
732            image_bytes: 0,
733            image_uploads: 0,
734            image_hits: 0,
735            image_evicted: 0,
736            tick: 0,
737            look_at: 0,
738            canvases_on: false,
739            canvases: Vec::new(),
740            canvas_cpu: String::new(),
741            canvas_pan: String::new(),
742            canvas_zoom: String::new(),
743            canvas_budget: CANVAS_BUDGET,
744            canvas_settle_us: SETTLE_US,
745            canvas_unused_us: CANVAS_UNUSED_US,
746            tile_bytes: 0,
747            tile_free: Vec::new(),
748            tile_sf: Surface::default(),
749            tile_fills: 0,
750            tile_fill_us: 0,
751            tile_evicted: 0,
752            canvas_settles: 0,
753            canvas_free_at: 0,
754            said_ok: false,
755            tile_marked: 0,
756            diff_marked: 0,
757            dev_gen: 0,
758            views_on: true,
759            want3d: false,
760            #[cfg(target_arch = "wasm32")]
761            web_inst: None,
762            #[cfg(target_arch = "wasm32")]
763            probe: None,
764        }
765    }
766}
767
768fn env_on(name: &str) -> bool {
769    plat::env(name).is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("on"))
770}
771
772fn wanted() -> bool {
773    let on = match plat::env("jfgui_GPU").as_deref() {
774        Some("0") | Some("off") => false,
775        Some("1") | Some("on") => true,
776        // (a run with ?agent=1 reads #screen, as scripts read the framebuffer of the CPU path)
777        _ if cfg!(target_arch = "wasm32") => DEFAULT_ON_BROWSER && !plat::args().iter().any(|a| a == "--agent"),
778        _ if cfg!(target_vendor = "apple") => DEFAULT_ON_APPLE,
779        _ => if cfg!(windows) { DEFAULT_ON } else { DEFAULT_ON_LINUX }, // (Android: the host says)
780    };
781    // (jfgui_GPU_OUTPUT=0: the windows through the CPU path, the GPU stays for views)
782    let output = !plat::env("jfgui_GPU_OUTPUT").is_some_and(|v| v == "0" || v.eq_ignore_ascii_case("off"));
783    on && output && !plat::win_remote_session()
784}
785
786fn ms(a: u64, b: u64) -> f64 {
787    b.saturating_sub(a) as f64 / 1000.0
788}
789
790impl S {
791    fn setup(&mut self) {
792        self.verify = env_on("jfgui_GPU_VERIFY");
793        self.trace = env_on("jfgui_GPU_TRACE");
794        self.t0 = plat::time_us();
795        self.retry_ms = plat::env("jfgui_GPU_RETRY_MS").and_then(|v| v.parse().ok()).unwrap_or(RETRY_FIRST_MS);
796        let off = |name: &str| plat::env(name).is_some_and(|v| v == "0" || v.eq_ignore_ascii_case("off"));
797        self.layers_on = !off("jfgui_GPU_LAYERS");
798        self.images_on = self.layers_on && !off("jfgui_GPU_IMAGES");
799        if let Some(mb) = plat::env("jfgui_GPU_IMAGE_MB").and_then(|v| v.parse::<u64>().ok()) {
800            self.image_budget = mb << 20;
801        }
802        self.canvases_on = self.layers_on && !off("jfgui_GPU_CANVAS");
803        self.canvas_cpu = plat::env("jfgui_GPU_CANVAS_CPU").unwrap_or_default();
804        self.canvas_pan = plat::env("jfgui_GPU_CANVAS_PAN").unwrap_or_default();
805        self.canvas_zoom = plat::env("jfgui_GPU_CANVAS_ZOOM").unwrap_or_default();
806        if let Some(mb) = plat::env("jfgui_GPU_CANVAS_MB").and_then(|v| v.parse::<u64>().ok()) {
807            self.canvas_budget = mb << 20;
808        }
809        if let Some(ms) = plat::env("jfgui_GPU_CANVAS_SETTLE_MS").and_then(|v| v.parse::<u64>().ok()) {
810            self.canvas_settle_us = ms * 1000;
811        }
812        self.views_on = self.layers_on && !off("jfgui_GPU_VIEWS");
813    }
814
815    /// how this canvas may use the GPU (the variables say what the widget's choice is not)
816    fn canvas_mode(&self, name: &str, gpu: CanvasGpu) -> CanvasGpu {
817        let named = |list: &str| list.split(',').any(|n| n == name);
818        if named(&self.canvas_cpu) {
819            CanvasGpu::Cpu
820        } else if named(&self.canvas_zoom) {
821            CanvasGpu::Zoom
822        } else if named(&self.canvas_pan) {
823            CanvasGpu::Pan
824        } else {
825            gpu
826        }
827    }
828
829    /// every tile goes, with the device or with the time: the textures die when no frame draws them
830    fn canvases_gone(&mut self) {
831        for c in self.canvases.iter_mut() {
832            c.levels.clear();
833            c.apart = false;
834            c.moved = false;
835        }
836        self.tile_free.clear();
837        self.tile_bytes = 0;
838        self.tile_sf = Surface::default();
839        self.canvas_free_at = 0;
840    }
841
842    /// a texture for a new tile: a free one, a new one, the one of the tile drawn longest ago
843    /// (not one of this frame) -> None: the budget is full of this frame's tiles
844    fn tile_texture(&mut self, tick: u64) -> Option<Rc<TileTex>> {
845        if self.tile_bytes + TILE_BYTES <= self.canvas_budget {
846            if let Some(k) = self.tile_free.iter().position(|t| Rc::strong_count(t) == 1) {
847                return Some(self.tile_free.swap_remove(k));
848            }
849        }
850        // (the budget may have become smaller: as many as it takes)
851        while self.tile_bytes + TILE_BYTES > self.canvas_budget {
852            if self.tile_free.pop().is_some() {
853                self.tile_bytes -= TILE_BYTES; // (dies with the frame that still draws it)
854                continue;
855            }
856            let mut old: Option<(usize, usize, (i32, i32), u64)> = None;
857            for (ci, c) in self.canvases.iter().enumerate() {
858                for (li, l) in c.levels.iter().enumerate() {
859                    for (key, t) in l.tiles.iter() {
860                        if t.tick != tick && old.is_none_or(|o| t.tick < o.3) {
861                            old = Some((ci, li, *key, t.tick));
862                        }
863                    }
864                }
865            }
866            let (ci, li, key, _) = old?;
867            self.canvases[ci].levels[li].tiles.remove(&key)?;
868            self.tile_evicted += 1;
869            self.tile_bytes -= TILE_BYTES;
870        }
871        let Dev::Ready(core) = &self.dev else { return None };
872        let t = core.dev.create_texture(&TextureDesc::d2(TILE_TEX as u32, TILE_TEX as u32, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED)).ok()?;
873        let group = core.dev.create_bind_group(&BindGroupDesc {
874            layout: &core.bgl,
875            entries: &[BindEntry { binding: 0, resource: Resource::Texture(t.view()) }],
876        }).ok()?;
877        self.tile_bytes += TILE_BYTES;
878        Some(Rc::new(TileTex { t, group, fresh: core::cell::Cell::new(true) }))
879    }
880
881    /// the tiles of a level go to the free ones
882    fn tiles_free(&mut self, tiles: BTreeMap<(i32, i32), Tile>) {
883        self.tile_free.extend(tiles.into_values().map(|t| t.tex));
884    }
885
886    /// an image's texture goes (a window's last frame may keep drawing it)
887    fn evict(&mut self, k: usize) {
888        let e = &mut self.images[k];
889        if e.tex.take().is_some() | e.load.take().is_some() {
890            self.image_bytes -= e.bytes;
891            self.image_evicted += 1;
892            self.trim_at = plat::time_us() + TRIM_US;
893        }
894    }
895
896    /// every image texture goes: the device with them
897    fn images_gone(&mut self) {
898        for e in self.images.iter_mut() {
899            e.tex = None;
900            e.load = None;
901        }
902        self.image_bytes = 0;
903    }
904
905    fn desc(&self) -> SwapchainDesc {
906        SwapchainDesc { present: PRESENT, copy_src: self.verify, min_images: IMAGES, ..SwapchainDesc::DEFAULT }
907    }
908
909    fn note(&self, what: &str) {
910        if self.trace {
911            plat::write_stderr(format!("gpu: {:9.2} ms  {what}\n", ms(self.t0, plat::time_us())).as_bytes());
912        }
913    }
914
915    /// (views, G5e3) window i is on its swapchain and holds a frame that was not shown yet: a new frame of its views waits
916    /// (what "shown" means per platform: docs/GPU.md "3D-Ansicht", G5e3)
917    fn unshown(&self, i: usize) -> bool {
918        self.wins.get(i).and_then(|w| w.as_ref()).is_some_and(|w| w.stage == Stage::Gpu && w.pending)
919    }
920
921    fn win(&mut self, i: usize) -> Option<&mut Win> {
922        self.wins.get_mut(i).and_then(|w| w.as_mut())
923    }
924
925    /// (the browser) no threads: WebGPU's objects stay on this one, and nothing below waits
926    #[cfg(target_arch = "wasm32")]
927    fn spawn(&mut self, f: impl FnOnce() -> Done + 'static) -> bool {
928        let d = f();
929        self.jobs.done.lock().push(d);
930        true
931    }
932
933    /// (the browser) what S::spawn's job on a thread does elsewhere: adapter and device first, which
934    /// answer later, then the page's canvas, then everything else in one go on this thread, then the small
935    /// picture -> true: nothing more to do for the device in this look
936    #[cfg(target_arch = "wasm32")]
937    fn web_start(&mut self) -> bool {
938        if self.probe.is_some() {
939            self.web_probe();
940            return true;
941        }
942        if !matches!(self.dev, Dev::None | Dev::Starting(WEB_PREPARING)) {
943            return matches!(self.dev, Dev::Starting(_));
944        }
945        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 {
946            return true;
947        };
948        if self.web_inst.is_none() {
949            self.note("start: adapter and device asked for");
950            match Instance::new() {
951                Ok(inst) => {
952                    self.web_inst = Some(inst);
953                    self.dev = Dev::Starting(WEB_PREPARING);
954                }
955                Err(e) => {
956                    self.note(&format!("no device: {e}"));
957                    if let Some(w) = self.win(i) {
958                        w.out.error = Some(format!("{e}"));
959                    }
960                    self.dev = Dev::Never(format!("{e}"));
961                    return true;
962                }
963            }
964        }
965        let inst = self.web_inst.as_ref().map(|x| x.prepare());
966        match inst {
967            Some(Ok(true)) => {}
968            Some(Err(e)) => {
969                self.note(&format!("no device: {e}"));
970                self.web_inst = None;
971                if let Some(w) = self.win(i) {
972                    w.out.error = Some(format!("{e}"));
973                }
974                self.dev = if matches!(e, GpuError::Failed(_)) {
975                    let at = plat::time_us() + self.retry_ms * 1000;
976                    self.retry_ms = (self.retry_ms * 2).min(RETRY_MAX_MS);
977                    Dev::Retry(at)
978                } else {
979                    Dev::Never(format!("{e}")) // no adapter: the CPU path for good
980                };
981                return true;
982            }
983            _ => return true,
984        }
985        // (a tool window on #screen: it can wait; the page's canvas is on its way)
986        let Some(nw) = plat::win_gpu_child(pw) else { return true };
987        let job = self.next_job;
988        self.next_job += 1;
989        let desc = self.desc();
990        let trace = self.trace;
991        let t0 = plat::time_us();
992        let r = build_device(self.web_inst.take(), nw, pw, None, desc, trace, self.want3d);
993        self.note(&format!("device, pipelines, swapchain: {:.2} ms on this thread", ms(t0, plat::time_us())));
994        if let Some(w) = self.win(i) {
995            w.stage = Stage::Job;
996            w.job = job;
997        }
998        self.dev = Dev::Starting(job);
999        match r.and_then(|(core, built)| {
1000            let (rb, px) = probe_start(&core)?;
1001            Ok(Probe { job, win: i, core, built, rb, px, at: plat::time_us() })
1002        }) {
1003            Ok(p) => self.probe = Some(p),
1004            Err(e) => self.jobs.done.lock().push(Done::Device { job, win: i, r: Err(e) }),
1005        }
1006        true
1007    }
1008
1009    /// (the browser) has the small picture come back as it should?
1010    #[cfg(target_arch = "wasm32")]
1011    fn web_probe(&mut self) {
1012        let Some(p) = &self.probe else { return };
1013        let mut got = vec![0u8; p.px.len()];
1014        let r = match p.rb.read(0, &mut got) {
1015            Err(GpuError::NotReady) if plat::time_us() < p.at + PROBE_US => return,
1016            Err(GpuError::NotReady) => Err(GpuError::Timeout),
1017            Err(e) => Err(e),
1018            Ok(()) if got == p.px => Ok(()),
1019            Ok(()) => Err(GpuError::Unsupported("the browser's WebGPU draws other pixels")),
1020        };
1021        let Some(p) = self.probe.take() else { return };
1022        self.note(&format!("the small picture: {:?}, {:.2} ms", r, ms(p.at, plat::time_us())));
1023        let (job, win) = (p.job, p.win);
1024        self.jobs.done.lock().push(Done::Device { job, win, r: r.map(|_| (p.core, p.built)) });
1025    }
1026
1027    /// the worker threads: one per job, joined when none runs (they end by themselves)
1028    #[cfg(not(target_arch = "wasm32"))]
1029    fn spawn(&mut self, f: impl FnOnce() -> Done + Send + 'static) -> bool {
1030        let jobs = self.jobs.clone();
1031        jobs.running.fetch_add(1, Ordering::AcqRel);
1032        let j = jobs.clone();
1033        // (jfgui_GPU_WORKER_KB: to see what a driver needs, docs/GPU.md "Linux")
1034        let stack = plat::env("jfgui_GPU_WORKER_KB").and_then(|v| v.parse::<usize>().ok()).map_or(WORKER_STACK, |k| k << 10);
1035        let t = plat::gpu_thread_spawn(stack, Box::new(move || {
1036            let d = f();
1037            j.done.lock().push(d);
1038            j.running.fetch_sub(1, Ordering::AcqRel);
1039        }));
1040        match t {
1041            Some(t) => {
1042                self.threads.push(t);
1043                true
1044            }
1045            None => {
1046                jobs.running.fetch_sub(1, Ordering::AcqRel);
1047                false
1048            }
1049        }
1050    }
1051}
1052
1053// ------------------------------------------------------------------ the worker's jobs
1054
1055/// (the browser) S::dev while adapter and device are on their way
1056#[cfg(target_arch = "wasm32")]
1057const WEB_PREPARING: u32 = 0;
1058/// (the browser) a new device's small picture comes back within this time, or the device is not used (us)
1059#[cfg(target_arch = "wasm32")]
1060const PROBE_US: u64 = 5_000_000;
1061
1062/// (the browser) a small picture through all three pipelines and the push constants, read back (the
1063/// rows of 256 bytes WebGPU wants) -> the buffer, the bytes it must hold
1064#[cfg(target_arch = "wasm32")]
1065fn probe_start(core: &Core) -> gpu::Result<(Buffer, Vec<u8>)> {
1066    const N: u32 = 8;
1067    const ROW: usize = 256;
1068    let dev = &core.dev;
1069    let src = dev.create_texture(&TextureDesc::d2(N, N, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED))?;
1070    let group = dev.create_bind_group(&BindGroupDesc {
1071        layout: &core.bgl,
1072        entries: &[BindEntry { binding: 0, resource: Resource::Texture(src.view()) }],
1073    })?;
1074    let target = dev.create_texture(&TextureDesc::d2(N, N, core.format, TextureUsage::COLOR_TARGET | TextureUsage::COPY_SRC))?;
1075    let rb = dev.create_buffer(&BufferDesc { size: (ROW * N as usize) as u64, usage: BufferUsage::NONE, memory: MemoryKind::Readback })?;
1076    // texels B, G, R and a fourth byte that means nothing; what must come out of them
1077    let mut px = vec![0u8; (N * N * 4) as usize];
1078    let mut want = vec![0u8; ROW * N as usize];
1079    for y in 0..N as usize {
1080        for x in 0..N as usize {
1081            let t = [(x * 30 + 1) as u8, (y * 30 + 2) as u8, ((x ^ y) * 17 + 3) as u8, 0x55];
1082            px[(y * N as usize + x) * 4..][..4].copy_from_slice(&t);
1083            let rgba = core.format == Format::Rgba8Unorm;
1084            let c = if rgba { [t[2], t[1], t[0], 255] } else { [t[0], t[1], t[2], 255] };
1085            want[y * ROW + x * 4..][..4].copy_from_slice(&c);
1086        }
1087    }
1088    dev.queue().write_texture_rects(&src, &[[0, 0, N as i32, N as i32]], &px, N * 4, Layout::Undefined, Layout::Sampled)?;
1089    let mut e = dev.encoder()?;
1090    e.begin_render(&RenderPassDesc {
1091        color: &[ColorAttachment { view: target.view(), load: Load::Discard, clear: [0.0; 4], store: true, before: Layout::Undefined, after: Layout::CopySrc }],
1092        depth: None,
1093    });
1094    let half = N / 2;
1095    e.set_render_pipeline(&core.pipe);
1096    e.set_bind_group(0, &group);
1097    e.push_constants(0, &[0i32; 4]);
1098    e.set_scissor(0, 0, N, half);
1099    e.draw(3, 1, 0, 0);
1100    e.set_render_pipeline(&core.pipe_image);
1101    e.set_bind_group(0, &group);
1102    e.push_constants(0, &[0i32, 0, 65536, 65536]);
1103    e.set_scissor(0, half, half, half);
1104    e.draw(3, 1, 0, 0);
1105    e.set_render_pipeline(&core.pipe_tile);
1106    e.set_bind_group(0, &group);
1107    e.set_scissor(half, half, half, half);
1108    e.draw(3, 1, 0, 0);
1109    e.end_render();
1110    e.copy_texture_to_buffer(TextureRegion::mip(&target, 0), &rb, DataLayout { offset: 0, bytes_per_row: ROW as u32, rows_per_image: 0 });
1111    dev.queue().submit([e.finish()?])?;
1112    Ok((rb, want))
1113}
1114
1115/// a swapchain (the old one rebuilt, or a new one on the surface) and the window's texture
1116fn build_window(dev: &Device, bgl: &BindGroupLayout, surf: gpu::Surface, old: Option<Swapchain>, desc: &SwapchainDesc) -> gpu::Result<Built> {
1117    let sc = match old {
1118        Some(mut sc) => {
1119            sc.rebuild()?;
1120            sc
1121        }
1122        None => dev.create_swapchain(&surf, desc)?,
1123    };
1124    let (w, h) = sc.size();
1125    if w == 0 || h == 0 {
1126        return Err(GpuError::NotReady); // minimised meanwhile
1127    }
1128    let tex = dev.create_texture(&TextureDesc::d2(w, h, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED))?;
1129    let group = dev.create_bind_group(&BindGroupDesc {
1130        layout: bgl,
1131        entries: &[BindEntry { binding: 0, resource: Resource::Texture(tex.view()) }],
1132    })?;
1133    Ok(Built { surf, sc, tex, group })
1134}
1135
1136/// (AF_GPU_3D) the adapters for windows and 3D views together, the fastest first: discrete before integrated, none
1137/// the output would not take either (a CPU or virtual one; the browser's one unless it is software).  jfgui_GPU_ADAPTER
1138/// picks one as in Instance::adapter
1139fn fast_adapters(inst: &Instance) -> gpu::Result<Vec<Adapter>> {
1140    if plat::env("jfgui_GPU_ADAPTER").is_some_and(|s| !s.is_empty()) {
1141        return inst.adapter().map(|a| vec![a]);
1142    }
1143    let list: Vec<Adapter> = inst.adapters()?.into_iter().filter(|a| a.info().missing.is_none() && gpu::low_power_rank(a.info().kind, false).is_some()).collect();
1144    if list.is_empty() { Err(GpuError::NoAdapter) } else { Ok(list) }
1145}
1146
1147/// instance, the adapter for this window, device, pipeline, the first swapchain.  want3d (AF_GPU_3D): the fastest
1148/// adapter at level Voll (3D views share the device), else the frugal one at level Basis
1149#[allow(clippy::too_many_arguments)]
1150fn build_device(prepared: Option<Instance>, nw: plat::NativeWindow, pw: i32, display: Option<(u32, u32)>, desc: SwapchainDesc, trace: bool, want3d: bool) -> gpu::Result<(Core, Built)> {
1151    let t0 = plat::time_us();
1152    #[cfg(target_os = "android")]
1153    if env_on("jfgui_GPU_TEST_DIE") {
1154        rt::os::exit(3); // (android_gpu.py: a driver that kills the program)
1155    }
1156    let inst = match prepared {
1157        Some(inst) => inst, // (the browser: it has answered)
1158        None => Instance::new()?,
1159    };
1160    let t1 = plat::time_us();
1161    let surf = gpu::Surface::native(&inst, nw, pw)?;
1162    let list: Vec<Adapter> = if want3d { fast_adapters(&inst)? } else { inst.adapters_low_power(display)? };
1163    let adapter = list.into_iter().find(|a| a.can_present(&surf)).ok_or(GpuError::NoAdapter)?;
1164    let t2 = plat::time_us();
1165    let dev = adapter.create_device(&DeviceDesc { max_level: if want3d { Level::Voll } else { Level::Basis } })?;
1166    let t3 = plat::time_us();
1167    let bgl = dev.create_bind_group_layout(&[Binding { binding: 0, kind: BindingKind::Texture, stages: Stages::FRAGMENT }])?;
1168    let built = build_window(&dev, &bgl, surf, None, &desc)?;
1169    let t4 = plat::time_us();
1170    let format = built.sc.format();
1171    #[cfg(not(target_arch = "wasm32"))]
1172    let (vs, fs, fs_image, fs_tile) = (ShaderModule::from_spirv(&dev, spv::VS)?, ShaderModule::from_spirv(&dev, spv::FS)?,
1173                                       ShaderModule::from_spirv(&dev, spv::FS_IMAGE)?, ShaderModule::from_spirv(&dev, spv::FS_TILE)?);
1174    // (the browser takes the WGSL as it is)
1175    #[cfg(target_arch = "wasm32")]
1176    let (vs, fs, fs_image, fs_tile) = {
1177        let m = ShaderModule::from_wgsl(&dev, GPU_OUT_WGSL)?;
1178        (m.clone(), m.clone(), m.clone(), m)
1179    };
1180    let layout = dev.create_pipeline_layout(&PipelineLayoutDesc { groups: &[&bgl], push_constants: 16 })?;
1181    let pipeline = |fragment: &'_ ShaderModule, entry: &'static str| {
1182        dev.create_render_pipeline(&RenderPipelineDesc {
1183            layout: &layout,
1184            vertex: ShaderStage::new(&vs, "vs"),
1185            fragment: Some(ShaderStage::new(fragment, entry)),
1186            buffers: &[],
1187            topology: Topology::Triangles,
1188            cull: Cull::None,
1189            front_cw: false,
1190            targets: &[ColorTarget { format, blend: None, write_mask: 0xF }],
1191            depth: None,
1192        })
1193    };
1194    let pipe = pipeline(&fs, "fs")?;
1195    let pipe_image = pipeline(&fs_image, "fs_image")?;
1196    let pipe_tile = pipeline(&fs_tile, "fs_tile")?;
1197    let t5 = plat::time_us();
1198    if trace {
1199        plat::write_stderr(format!(
1200            "gpu: worker: instance {:.2} ms, adapter {:.2}, device {:.2}, swapchain + texture {:.2}, pipeline {:.2} ({})\n",
1201            ms(t0, t1), ms(t1, t2), ms(t2, t3), ms(t3, t4), ms(t4, t5), adapter.info().name).as_bytes());
1202    }
1203    let name = adapter.info().name.clone();
1204    Ok((Core { inst, dev, bgl, pipe, pipe_image, pipe_tile, format, name }, built))
1205}
1206
1207// ------------------------------------------------------------------ one frame
1208
1209/// the frame's rectangles (None: all of it) into the window's texture
1210fn upload(w: &mut Win, core: &Core, fb: &Surface, rects: Option<&[[i32; 4]]>, trim_at: &mut u64) -> gpu::Result<()> {
1211    let Some(tex) = &w.tex else { return Ok(()) };
1212    let all = [[0, 0, fb.w, fb.h]];
1213    let (rs, before) = match rects {
1214        Some(r) if !w.full && !w.undef => (r, Layout::Sampled),
1215        // the whole frame replaces everything: the old contents do not matter
1216        _ => (&all[..], Layout::Undefined),
1217    };
1218    core.dev.queue().write_texture_rects(&tex.t, rs, &fb.px, fb.stride as u32, before, Layout::Sampled)?;
1219    let bytes: u64 = rs.iter().map(|r| r[2].max(0) as u64 * r[3].max(0) as u64 * 4).sum();
1220    w.out.upload_bytes += bytes;
1221    w.out.upload_base += bytes;
1222    if let Some(sc) = &w.sc {
1223        sc.damage(if rects.is_some() && before == Layout::Sampled { Some(rs) } else { None });
1224    }
1225    if bytes > BIG_UPLOAD {
1226        *trim_at = plat::time_us() + TRIM_US;
1227    }
1228    w.full = false;
1229    w.undef = false;
1230    w.pending = true;
1231    Ok(())
1232}
1233
1234enum Shown {
1235    Done,
1236    /// no image free yet: gp_tick or the next frame
1237    Wait,
1238    NoArea,
1239    Stale,
1240}
1241
1242/// the texture to a swapchain image and on the screen, without waiting
1243fn show(w: &mut Win, core: &Core, fb: &Surface) -> gpu::Result<Shown> {
1244    let (Some(sc), Some(tex)) = (w.sc.as_mut(), w.tex.as_ref()) else { return Ok(Shown::Stale) };
1245    let fr = match sc.try_acquire()? {
1246        TryAcquire::Frame(f) => f,
1247        TryAcquire::NotReady => return Ok(Shown::Wait),
1248        TryAcquire::NoArea => return Ok(Shown::NoArea),
1249        TryAcquire::Stale => return Ok(Shown::Stale),
1250    };
1251    let sc = &*sc;
1252    // (the browser: rows of 256 bytes, a buffer per frame, the answer comes later: verify_poll)
1253    #[cfg(target_arch = "wasm32")]
1254    let rb_frame = match &w.rb {
1255        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 })?),
1256        None => None,
1257    };
1258    #[cfg(target_arch = "wasm32")]
1259    let rb_now = rb_frame.as_ref();
1260    #[cfg(not(target_arch = "wasm32"))]
1261    let rb_now = w.rb.as_ref();
1262    let row_bytes = if cfg!(target_arch = "wasm32") { (fb.w as u32 * 4 + 255) & !255 } else { 0 };
1263    let mut e = core.dev.encoder()?;
1264    let after = if rb_now.is_some() { Layout::CopySrc } else { Layout::Present };
1265    e.begin_render(&RenderPassDesc {
1266        color: &[ColorAttachment { view: sc.view(&fr), load: Load::Discard, clear: [0.0; 4], store: true, before: Layout::Undefined, after }],
1267        depth: None,
1268    });
1269    e.set_render_pipeline(&core.pipe);
1270    e.set_bind_group(0, &tex.group);
1271    e.push_constants(0, &[0i32; 4]);
1272    e.draw(3, 1, 0, 0);
1273    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);
1274    // the images and canvases the CPU left out
1275    if !w.sp.quads.is_empty() {
1276        for q in &w.sp.quads {
1277            match &q.src {
1278                QuadSrc::Image { tex, step, .. } => {
1279                    e.set_render_pipeline(&core.pipe_image);
1280                    e.set_bind_group(0, &tex.group);
1281                    e.push_constants(0, &[q.x, q.y, step[0] as i32, step[1] as i32]);
1282                    for r in &q.rects {
1283                        scissor(&mut e, r);
1284                        e.draw(3, 1, 0, 0);
1285                    }
1286                }
1287                QuadSrc::View { tex, .. } => {
1288                    // (its frame ended in Layout::Sampled; the content's pass made it visible to the shaders)
1289                    let Some(group) = &tex.group else { continue };
1290                    e.set_render_pipeline(&core.pipe);
1291                    e.set_bind_group(0, group);
1292                    e.push_constants(0, &[q.x, q.y, 0, 0]);
1293                    for r in &q.rects {
1294                        scissor(&mut e, r);
1295                        e.draw(3, 1, 0, 0);
1296                    }
1297                }
1298                QuadSrc::Canvas { tiles, copy, .. } => {
1299                    e.set_render_pipeline(if *copy { &core.pipe } else { &core.pipe_tile });
1300                    for t in tiles {
1301                        let mut bound = false;
1302                        for r in &q.rects {
1303                            let (x0, y0) = (t.b[0].max(r[0]), t.b[1].max(r[1]));
1304                            let (x1, y1) = (t.b[2].min(r[0] + r[2]), t.b[3].min(r[1] + r[3]));
1305                            if x0 < x1 && y0 < y1 {
1306                                if !bound {
1307                                    bound = true;
1308                                    e.set_bind_group(0, &t.tex.group);
1309                                    e.push_constants(0, &t.place);
1310                                }
1311                                scissor(&mut e, &[x0, y0, x1 - x0, y1 - y0]);
1312                                e.draw(3, 1, 0, 0);
1313                            }
1314                        }
1315                    }
1316                }
1317            }
1318        }
1319        e.set_render_pipeline(&core.pipe);
1320    }
1321    // the layers above, opaque: each cell holds what is there up to its layer
1322    for u in w.sp.upper.iter().filter(|u| !u.rects.is_empty()) {
1323        if let Some(t) = &u.tex {
1324            e.set_bind_group(0, &t.group);
1325            e.push_constants(0, &[t.ox, t.oy, 0, 0]);
1326            for r in &u.rects {
1327                scissor(&mut e, r);
1328                e.draw(3, 1, 0, 0);
1329            }
1330        }
1331    }
1332    e.end_render();
1333    if let Some(rb) = rb_now {
1334        let img = sc.texture(&fr);
1335        e.copy_texture_to_buffer(TextureRegion::mip(img, 0), rb, DataLayout { bytes_per_row: row_bytes, ..DataLayout::default() });
1336        e.texture_barrier(img, Layout::CopySrc, Layout::Present);
1337        e.barrier(Access::COPY_WRITE, Access::HOST_READ);
1338    }
1339    let cmd = e.finish()?;
1340    let id = sc.present(fr, [cmd])?;
1341    w.pending = false;
1342    w.last_present = plat::time_us();
1343    w.out.frames += 1;
1344    #[cfg(target_arch = "wasm32")]
1345    {
1346        let _ = id;
1347        if let Some(rb) = rb_frame {
1348            let mut snap = Surface::new(fb.w, fb.h, FMT_XRGB);
1349            let n = snap.px.len();
1350            snap.px.copy_from_slice(&fb.px[..n]);
1351            let loose = w.sp.quads.iter().filter(|q| matches!(q.src, QuadSrc::Canvas { loose: true, .. })).flat_map(|q| q.rects.iter().copied()).collect();
1352            let late = w.sp.quads.iter().filter(|q| matches!(q.src, QuadSrc::View { .. })).flat_map(|q| q.rects.iter().copied()).collect();
1353            w.verify_wait.push(Verify { rb, fb: snap, loose, late });
1354        }
1355    }
1356    #[cfg(not(target_arch = "wasm32"))]
1357    if let Some(rb) = &w.rb {
1358        core.dev.wait(id, 5_000_000_000)?;
1359        sc.flush()?;
1360        let n = fb.w as usize * fb.h as usize * 4;
1361        w.rb_px.resize(n, 0);
1362        rb.read(0, &mut w.rb_px[..n])?;
1363        let swap_rb = core.format == Format::Rgba8Unorm;
1364        if let Some(keep) = w.rb_keep.as_mut() {
1365            keep.clear();
1366            keep.extend_from_slice(&w.rb_px[..n]);
1367        }
1368        // what a gesture's tiles may show otherwise: counted, not compared
1369        for q in &w.sp.quads {
1370            if matches!(q.src, QuadSrc::Canvas { loose: true, .. }) {
1371                loose_compare(&mut w.rb_px, fb, &q.rects, swap_rb, &mut w.out);
1372            }
1373        }
1374        let (x, y, bad) = compare(&w.rb_px, fb, swap_rb);
1375        w.out.verified += 1;
1376        if bad != 0 {
1377            w.out.verify_bad += 1;
1378            plat::write_stderr(format!("gpu: the presented image differs from the framebuffer in {bad} pixels, first at {x}, {y}\n").as_bytes());
1379        }
1380    }
1381    Ok(Shown::Done)
1382}
1383
1384/// (the browser) presented images that have come back by now: compared as show compares them elsewhere
1385#[cfg(target_arch = "wasm32")]
1386fn verify_poll(w: &mut Win, core: &Core) {
1387    let swap_rb = core.format == Format::Rgba8Unorm;
1388    while let Some(v) = w.verify_wait.first_mut() {
1389        let row = v.fb.w as usize * 4;
1390        let padded = (row + 255) & !255;
1391        let mut raw = vec![0u8; padded * v.fb.h as usize];
1392        match v.rb.read(0, &mut raw) {
1393            Err(GpuError::NotReady) => return,
1394            Err(_) => {
1395                w.verify_wait.clear();
1396                return;
1397            }
1398            Ok(()) => {}
1399        }
1400        let mut img: Vec<u8> = Vec::with_capacity(row * v.fb.h as usize);
1401        for r in raw.chunks_exact(padded) {
1402            img.extend_from_slice(&r[..row]);
1403        }
1404        loose_compare(&mut img, &v.fb, &v.loose, swap_rb, &mut w.out);
1405        // (a view's frame, which the framebuffer gets later: those pixels as the framebuffer has them)
1406        let mut ignore = GpuOutput::default();
1407        loose_compare(&mut img, &v.fb, &v.late, swap_rb, &mut ignore);
1408        let (x, y, bad) = compare(&img, &v.fb, swap_rb);
1409        w.out.verified += 1;
1410        if bad != 0 {
1411            w.out.verify_bad += 1;
1412            plat::write_stderr(format!("gpu: the presented image differs from the framebuffer in {bad} pixels, first at {x}, {y}\n").as_bytes());
1413        }
1414        w.verify_wait.remove(0);
1415    }
1416}
1417
1418/// the presented image (rows of w * 4 bytes) against the framebuffer: every colour byte equal and
1419/// the fourth byte 255 (the framebuffer's x byte means nothing, the window is opaque) -> first
1420/// x, y and the number of pixels that differ
1421fn compare(img: &[u8], fb: &Surface, swap_rb: bool) -> (i32, i32, u64) {
1422    let (mut fx, mut fy, mut bad) = (0, 0, 0u64);
1423    let row = fb.w as usize * 4;
1424    for y in 0..fb.h as usize {
1425        let a = &img[y * row..y * row + row];
1426        let b = &fb.px[y * fb.stride as usize..y * fb.stride as usize + row];
1427        // the usual case at memcmp speed: fails only for the x byte
1428        for (x, (p, q)) in a.chunks_exact(4).zip(b.chunks_exact(4)).enumerate() {
1429            let same = if swap_rb { p[0] == q[2] && p[1] == q[1] && p[2] == q[0] } else { p[..3] == q[..3] };
1430            if !same || p[3] != 255 {
1431                if bad == 0 {
1432                    fx = x as i32;
1433                    fy = y as i32;
1434                }
1435                bad += 1;
1436            }
1437        }
1438    }
1439    (fx, fy, bad)
1440}
1441
1442// rectangles of the presented image (rows of w * 4 bytes): how far they are from the framebuffer
1443// (GpuOutput::canvas_diff_*), then the framebuffer's pixels in them
1444fn loose_compare(img: &mut [u8], fb: &Surface, rects: &[[i32; 4]], swap_rb: bool, out: &mut GpuOutput) {
1445    let row = fb.w as usize * 4;
1446    let (mut n, mut sum, mut top) = (0u64, 0u64, 0u32);
1447    for r in rects {
1448        for y in r[1] as usize..(r[1] + r[3]) as usize {
1449            let a = &mut img[y * row + r[0] as usize * 4..y * row + (r[0] + r[2]) as usize * 4];
1450            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];
1451            for (p, q) in a.chunks_exact_mut(4).zip(b.chunks_exact(4)) {
1452                let c = if swap_rb { [q[2], q[1], q[0]] } else { [q[0], q[1], q[2]] };
1453                let d = (0..3).map(|i| p[i].abs_diff(c[i]) as u32).max().unwrap_or(0);
1454                if d != 0 {
1455                    n += 1;
1456                    sum += d as u64;
1457                    top = top.max(d);
1458                    p[..3].copy_from_slice(&c);
1459                }
1460            }
1461        }
1462    }
1463    out.canvas_diff_frames += (n != 0) as u64;
1464    out.canvas_diff_px += n;
1465    out.canvas_diff_sum += sum;
1466    out.canvas_diff_max = out.canvas_diff_max.max(top);
1467}
1468
1469// floor and round without libm
1470fn flr(x: f64) -> f64 {
1471    let t = x as i64 as f64;
1472    if t > x { t - 1.0 } else { t }
1473}
1474
1475fn rnd(x: f64) -> f64 {
1476    flr(x + 0.5)
1477}
1478
1479// a level for view v: at its scale whole pixels away, else (a zoom running) the one nearest in scale,
1480// else a new one: for a zoom at the scale of the view before (the content there is drawn already, a
1481// store may hold it) when the tiles can serve this one from there
1482fn level_for(c: &mut Canvas, v: CanvasView, zoom: bool) -> usize {
1483    let zoom = zoom && c.prev.scale != v.scale;
1484    let near = |l: &TileLevel, v: CanvasView| {
1485        let (dx, dy) = (v.ox - l.fx, v.oy - l.fy);
1486        (dx - rnd(dx)).abs() < PHASE_TOL && (dy - rnd(dy)).abs() < PHASE_TOL
1487    };
1488    let mut best: Option<(usize, f64)> = None;
1489    for (n, l) in c.levels.iter().enumerate() {
1490        let k = v.scale / l.scale;
1491        if k == 1.0 {
1492            if near(l, v) {
1493                return n;
1494            }
1495        } else if zoom && (K_MIN..=K_MAX).contains(&k) {
1496            let far = if k < 1.0 { 1.0 / k } else { k };
1497            if best.is_none_or(|b| far < b.1) {
1498                best = Some((n, far));
1499            }
1500        }
1501    }
1502    if let Some(b) = best {
1503        return b.0;
1504    }
1505    let p = c.prev;
1506    let from = if zoom && p.scale > 0.0 && (K_MIN..=K_MAX).contains(&(v.scale / p.scale)) { p } else { v };
1507    c.levels.push(TileLevel { scale: from.scale, fx: from.ox - flr(from.ox), fy: from.oy - flr(from.oy), tiles: BTreeMap::new() });
1508    c.levels.len() - 1
1509}
1510
1511// ------------------------------------------------------------------ cells
1512
1513// the cell's box in the window (x0, y0, x1, y1)
1514fn cell_box(i: usize, cols: usize, w: i32, h: i32) -> [i32; 4] {
1515    let (c, r) = ((i % cols) as i32, (i / cols) as i32);
1516    [c * CELL, r * CELL, ((c + 1) * CELL).min(w), ((r + 1) * CELL).min(h)]
1517}
1518
1519fn cell_save(fb: &Surface, b: [i32; 4], to: &mut [u8]) {
1520    let n = (b[2] - b[0]) as usize * 4;
1521    for y in b[1]..b[3] {
1522        let s = y as usize * fb.stride as usize + b[0] as usize * 4;
1523        let d = (y - b[1]) as usize * CELL_ROW;
1524        to[d..d + n].copy_from_slice(&fb.px[s..s + n]);
1525    }
1526}
1527
1528fn cell_load(fb: &mut Surface, b: [i32; 4], from: &[u8]) {
1529    let n = (b[2] - b[0]) as usize * 4;
1530    for y in b[1]..b[3] {
1531        let d = y as usize * fb.stride as usize + b[0] as usize * 4;
1532        let s = (y - b[1]) as usize * CELL_ROW;
1533        fb.px[d..d + n].copy_from_slice(&from[s..s + n]);
1534    }
1535}
1536
1537// the cells f holds for as rectangles x, y, w, h cut to the w x h window: a run in a row extends a
1538// rectangle of the row above with the same columns
1539fn cell_rects(out: &mut Vec<[i32; 4]>, cols: usize, rows: usize, w: i32, h: i32, f: impl Fn(usize) -> bool) {
1540    out.clear();
1541    for row in 0..rows {
1542        let mut c = 0;
1543        while c < cols {
1544            if !f(row * cols + c) {
1545                c += 1;
1546                continue;
1547            }
1548            let mut e = c + 1;
1549            while e < cols && f(row * cols + e) {
1550                e += 1;
1551            }
1552            let (x, rw, y) = (c as i32 * CELL, (e as i32 * CELL).min(w) - c as i32 * CELL, row as i32 * CELL);
1553            let rh = (y + CELL).min(h) - y;
1554            if let Some(r) = out.iter_mut().find(|r| r[0] == x && r[2] == rw && r[1] + r[3] == y) {
1555                r[3] += rh;
1556            } else {
1557                out.push([x, y, rw, rh]);
1558            }
1559            c = e;
1560        }
1561    }
1562}
1563
1564// an image's or a canvas's pixels in window box b of the framebuffer, as its command draws them there
1565fn quad_blit(fb: &mut Surface, q: &Quad, b: [i32; 4], text: &mut Text) {
1566    let mut dc = Dc::new(fb.w, fb.h);
1567    dc.clip_push(b[0], b[1], b[2] - b[0], b[3] - b[1]);
1568    match &q.src {
1569        QuadSrc::Image { src, w, h, .. } => dc.blit_scaled(fb, src, q.x, q.y, *w, *h),
1570        // (a view's Draw reads its frame back)
1571        QuadSrc::Canvas { f, w, h, vis, .. } | QuadSrc::View { f, w, h, vis, .. } => {
1572            dc.clip_push(vis[0], vis[1], vis[2] - vis[0], vis[3] - vis[1]);
1573            dc.clip_push(q.x, q.y, *w, *h);
1574            f.draw(&mut Gfx { dc: &mut dc, sf: fb, text }, [q.x, q.y, *w, *h]);
1575            dc.clip_pop();
1576            dc.clip_pop();
1577        }
1578    }
1579}
1580
1581// every alpha 255: the image replaces what lies below it
1582fn surface_opaque(sf: &Surface) -> bool {
1583    if sf.fmt == FMT_XRGB {
1584        return true;
1585    }
1586    let (row, stride) = (sf.w as usize * 4, sf.stride as usize);
1587    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))
1588}
1589
1590// the texture of an image, nothing uploaded yet
1591fn image_texture(core: &Core, sf: &Surface) -> gpu::Result<ImageLoad> {
1592    let t = core.dev.create_texture(&TextureDesc::d2(sf.w as u32, sf.h as u32, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED))?;
1593    let group = core.dev.create_bind_group(&BindGroupDesc {
1594        layout: &core.bgl,
1595        entries: &[BindEntry { binding: 0, resource: Resource::Texture(t.view()) }],
1596    })?;
1597    Ok(ImageLoad { t, group, row: 0 })
1598}
1599
1600// a texture for what a layer holds now: bb (x0, y0, x1, y1) with spare room, inside the window
1601fn layer_texture(core: &Core, bb: [i32; 4], fw: i32, fh: i32) -> gpu::Result<LayerTex> {
1602    let size = |n: i32, lim: i32| ((n + LAYER_SPARE + LAYER_STEP - 1) / LAYER_STEP * LAYER_STEP).min(lim);
1603    let (w, h) = (size(bb[2] - bb[0], fw), size(bb[3] - bb[1], fh));
1604    let ox = (bb[0] - (w - (bb[2] - bb[0])) / 2).clamp(0, fw - w);
1605    let oy = (bb[1] - (h - (bb[3] - bb[1])) / 2).clamp(0, fh - h);
1606    let t = core.dev.create_texture(&TextureDesc::d2(w as u32, h as u32, Format::Bgra8Unorm, TextureUsage::COPY_DST | TextureUsage::SAMPLED))?;
1607    let group = core.dev.create_bind_group(&BindGroupDesc {
1608        layout: &core.bgl,
1609        entries: &[BindEntry { binding: 0, resource: Resource::Texture(t.view()) }],
1610    })?;
1611    Ok(LayerTex { t, group, ox, oy, w, h, fresh: true })
1612}
1613
1614// ------------------------------------------------------------------ views (G5e1)
1615
1616/// a view's content leaves its device: detach, the target and its read-back go (what was read back stays)
1617fn view_detach(e: &mut Entry) {
1618    if e.devgen == 0 {
1619        return;
1620    }
1621    if let Some(c) = e.content.upgrade() {
1622        if let Ok(mut c) = c.try_borrow_mut() {
1623            c.detach();
1624        }
1625    }
1626    e.info.detaches += 1;
1627    e.devgen = 0;
1628    e.output = false;
1629    e.refused = 0;
1630    e.tex = None;
1631    e.tried = None;
1632    e.apart = false;
1633    e.moved = false;
1634    e.rest_at = 0;
1635    e.held_at = 0;
1636    e.held_key = None;
1637    e.sh.c.borrow_mut().gpu = None;
1638}
1639
1640/// one frame of view e: its target of this size (new: also the output's bind group for it and the read-back), the
1641/// content's pass into it, submitted.  The target rests in Layout::Sampled between frames: the pass goes from
1642/// there (Undefined the first time) to ColorTarget and back to Sampled at its end, which makes its writes
1643/// visible to the shaders that sample it (the output's pass, later on the same queue) and waits for their
1644/// reads before the next frame writes
1645fn view_frame(e: &mut Entry, content: &Rc<core::cell::RefCell<dyn GpuContent>>, dev: &Device, bgl: Option<&BindGroupLayout>, size: (u32, u32)) -> gpu::Result<()> {
1646    let (w, h) = size;
1647    if e.tex.as_ref().is_none_or(|t| (t.w, t.h) != size) {
1648        e.tex = None;
1649        e.sh.c.borrow_mut().gpu = None;
1650        let t = dev.create_texture(&TextureDesc::d2(w, h, GPU_VIEW_FORMAT, TextureUsage::COLOR_TARGET | TextureUsage::SAMPLED | TextureUsage::COPY_SRC))?;
1651        let group = match bgl {
1652            Some(l) => Some(dev.create_bind_group(&BindGroupDesc { layout: l, entries: &[BindEntry { binding: 0, resource: Resource::Texture(t.view()) }] })?),
1653            None => None,
1654        };
1655        let stride = (w * 4 + 255) & !255;
1656        e.sh.c.borrow_mut().gpu = Some(Target {
1657            dev: dev.clone(),
1658            t: t.clone(),
1659            // (the browser: a buffer per read-back, gpu_view.rs)
1660            #[cfg(not(target_arch = "wasm32"))]
1661            rb: dev.create_buffer(&BufferDesc { size: stride as u64 * h as u64, usage: BufferUsage::NONE, memory: MemoryKind::Readback })?,
1662            stride,
1663            #[cfg(target_arch = "wasm32")]
1664            wait: None,
1665        });
1666        e.tex = Some(Rc::new(ViewTex { t, group, w, h, fresh: core::cell::Cell::new(true) }));
1667    }
1668    let Some(tex) = e.tex.clone() else { return Err(GpuError::Invalid("no target")) };
1669    let fresh = tex.fresh.get();
1670    if let Err(err) = view_draw(&tex, content, dev, size, if fresh { Layout::Undefined } else { Layout::Sampled }) {
1671        // (nothing went to the queue: a new target holds no frame, so neither the output's pass nor a read-back may
1672        // take it; the next frame makes it again)
1673        if fresh {
1674            e.tex = None;
1675            e.sh.c.borrow_mut().gpu = None;
1676        }
1677        return Err(err);
1678    }
1679    tex.fresh.set(false);
1680    e.sh.c.borrow_mut().frame += 1;
1681    Ok(())
1682}
1683
1684/// view_frame's pass: the content's frame into tex, submitted
1685fn view_draw(tex: &ViewTex, content: &Rc<core::cell::RefCell<dyn GpuContent>>, dev: &Device, size: (u32, u32), before: Layout) -> gpu::Result<()> {
1686    let mut c = content.try_borrow_mut().map_err(|_| GpuError::Invalid("the view's content is borrowed"))?;
1687    let mut enc = dev.encoder()?;
1688    let target = ColorAttachment { view: tex.t.view(), load: Load::Clear, clear: [0.0, 0.0, 0.0, 1.0], store: true, before, after: Layout::Sampled };
1689    c.draw(&mut enc, target, size)?;
1690    let id = dev.queue().submit([enc.finish()?])?;
1691    c.submitted(id);
1692    Ok(())
1693}
1694
1695// ------------------------------------------------------------------ the runtime's calls
1696
1697impl Ui {
1698    /// window_main: the output's decision for this run
1699    pub(crate) fn gp_window_mode(&mut self) {
1700        let want3d = self.desc.flags & AF_GPU_3D != 0;
1701        let s = &mut self.m.gout;
1702        s.want = wanted();
1703        s.want3d = want3d;
1704        s.setup();
1705    }
1706
1707    /// uiw_present: window `cur` changed in n rectangles (drects; -1: all) -> the GPU took the frame
1708    pub(crate) fn gp_present(&mut self, cur: u32, pw: i32, n: i32) -> bool {
1709        if !self.m.gout.want {
1710            return false;
1711        }
1712        self.gp_poll();
1713        let t0 = plat::time_us();
1714        let i = cur as usize;
1715        if self.m.gout.verify {
1716            self.gp_settle(i); // the presented image is compared with the framebuffer
1717        }
1718        let s = &mut self.m.gout;
1719        let Dev::Ready(core) = &s.dev else { return false };
1720        let Some(Some(w)) = s.wins.get_mut(i) else { return false };
1721        if w.stage != Stage::Gpu {
1722            return false;
1723        }
1724        if let Some(e) = w.sp.err.take() {
1725            self.gp_failed(i, e);
1726            return true;
1727        }
1728        let fb = &self.wp.rt.fb;
1729        let fits = w.tex.as_ref().is_some_and(|t| (t.w, t.h) == (fb.w, fb.h));
1730        if !fits {
1731            self.gp_leave(i, "the size changed");
1732            return true;
1733        }
1734        if plat::win_state(pw) & plat::WST_MINIMIZED != 0 {
1735            self.gp_flatten(i); // nobody sees it: the whole frame when the window comes back
1736            return true;
1737        }
1738        let rects = if n < 0 { None } else { Some(&self.drects[..n as usize]) };
1739        // (the browser: the worker's turn may hold several presents, the browser shows only the last
1740        // image of a turn: gp_tick draws it, right after this, once)
1741        let soon = (t0 < w.last_present + MIN_PRESENT_US && !s.script) || cfg!(target_arch = "wasm32");
1742        // (what changes on the screen is known without layers, images and canvases)
1743        let apart = w.sp.drawn || w.sp.active || !w.sp.quads.is_empty();
1744        let before = core::mem::replace(&mut w.apart_before, apart);
1745        if apart || before {
1746            if let Some(sc) = &w.sc {
1747                sc.damage(None);
1748            }
1749        }
1750        // (a frame drawn layer by layer has uploaded its parts)
1751        let up = if core::mem::take(&mut w.sp.drawn) { Ok(()) } else { upload(w, core, fb, rects, &mut s.trim_at) };
1752        let r = up.and_then(|_| if soon || w.test_hold { Ok(Shown::Wait) } else { show(w, core, fb) });
1753        w.out.present_us += plat::time_us().saturating_sub(t0);
1754        match r {
1755            Ok(Shown::Done) | Ok(Shown::Wait) => {}
1756            Ok(Shown::NoArea) => {
1757                self.gp_flatten(i);
1758                return true;
1759            }
1760            Ok(Shown::Stale) => {
1761                self.gp_leave(i, "stale");
1762                return true;
1763            }
1764            Err(e) => {
1765                self.gp_failed(i, e);
1766                return true;
1767            }
1768        }
1769        plat::present_account(t0, true);
1770        true
1771    }
1772
1773    /// uiw_present: the frame went through the CPU path
1774    pub(crate) fn gp_cpu_done(&mut self, cur: u32, pw: i32) {
1775        let s = &mut self.m.gout;
1776        if !s.want {
1777            return;
1778        }
1779        let i = cur as usize;
1780        if s.wins.len() <= i {
1781            s.wins.resize_with(i + 1, || None);
1782        }
1783        let now = plat::time_us();
1784        let w = s.wins[i].get_or_insert_with(|| Win {
1785            pw,
1786            stage: Stage::Cpu,
1787            sc: None,
1788            surf: None,
1789            tex: None,
1790            rb: None,
1791            rb_px: Vec::new(),
1792            rb_keep: None,
1793            full: true,
1794            undef: true,
1795            pending: false,
1796            hidden: false,
1797            test_hold: false,
1798            apart_before: false,
1799            last_present: 0,
1800            job: 0,
1801            lost: false,
1802            size: (0, 0),
1803            stable_at: now,
1804            fail_at: 0,
1805            fail_ms: 0,
1806            out: GpuOutput::default(),
1807            sp: Split::default(),
1808            #[cfg(target_arch = "wasm32")]
1809            verify_wait: Vec::new(),
1810        });
1811        w.pw = pw;
1812        w.out.cpu_frames += 1;
1813        let size = (self.wp.rt.fb.w, self.wp.rt.fb.h);
1814        if w.size != size {
1815            w.size = size;
1816            w.stable_at = now;
1817        }
1818    }
1819
1820    /// back to the CPU path, the frame shown first (no gap): window i's size changed or its
1821    /// swapchain went stale; the swapchain is rebuilt when the size has settled
1822    fn gp_leave(&mut self, i: usize, why: &str) {
1823        self.gp_settle(i);
1824        if let Some(p) = self.uiw_parts_mut(i as u32) {
1825            p.dirty.busy = false;
1826        }
1827        let Some(fb) = self.uiw_parts(i as u32).map(|p| &p.rt.fb) else { return };
1828        let size = (fb.w, fb.h);
1829        let Some(Some(w)) = self.m.gout.wins.get(i) else { return };
1830        let pw = w.pw;
1831        let t0 = plat::time_us();
1832        plat::win_present(pw, &Ui::frame_of(fb), None);
1833        plat::win_gpu_child_show(pw, false);
1834        let now = plat::time_us();
1835        let s = &mut self.m.gout;
1836        s.note(&format!("window {i}: back to the CPU path ({why}): {:.2} ms", ms(t0, now)));
1837        let Some(w) = s.win(i) else { return };
1838        w.stage = Stage::Cpu;
1839        w.tex = None;
1840        w.sp.reset();
1841        w.pending = false;
1842        w.size = size;
1843        w.stable_at = now;
1844        w.out.leaves += 1;
1845        // (the browser: its canvas goes with this frame, the next period on the GPU has a new one)
1846        #[cfg(target_arch = "wasm32")]
1847        {
1848            w.sc = None;
1849            w.surf = None;
1850        }
1851        w.out.cpu_frames += 1;
1852    }
1853
1854    /// something failed.  A lost surface takes the window back to the CPU path, anything else
1855    /// every window, with a new device later; every framebuffer holds its frame
1856    fn gp_failed(&mut self, i: usize, e: GpuError) {
1857        plat::log_warn("gpu", &format!("output: {e}"));
1858        self.m.gout.note(&format!("window {i}: {e}"));
1859        if matches!(e, GpuError::SurfaceLost) {
1860            self.gp_leave(i, "surface lost");
1861            let now = plat::time_us();
1862            if let Some(w) = self.m.gout.win(i) {
1863                w.sc = None;
1864                w.surf = None;
1865                w.fail_ms = (w.fail_ms * 2).clamp(RETRY_FIRST_MS, RETRY_MAX_MS);
1866                w.fail_at = now + w.fail_ms * 1000;
1867            }
1868            return;
1869        }
1870        let n = self.m.gout.wins.len();
1871        for k in 0..n {
1872            let on = self.m.gout.wins[k].as_ref().is_some_and(|w| w.stage == Stage::Gpu);
1873            if on {
1874                self.gp_leave(k, "failure");
1875            }
1876            if let Some(w) = self.m.gout.win(k) {
1877                w.stage = Stage::Cpu;
1878                w.sc = None;
1879                w.surf = None;
1880                w.tex = None;
1881                w.rb = None;
1882                w.pending = false;
1883                w.sp.reset();
1884                w.out.error = Some(format!("{e}"));
1885            }
1886        }
1887        // (views: their frames as read back last stay in the framebuffers; the contents leave the device)
1888        if matches!(self.m.gout.dev, Dev::Ready(_)) {
1889            let devgen = self.m.gout.dev_gen;
1890            self.gp_views_gone(Some(devgen));
1891        }
1892        let s = &mut self.m.gout;
1893        s.images_gone();
1894        s.canvases_gone();
1895        let now = plat::time_us();
1896        if matches!(e, GpuError::Invalid(_) | GpuError::Unsupported(_)) {
1897            s.dev = Dev::Never(format!("{e}"));
1898            return;
1899        }
1900        // a device that lives for a minute starts the waits over
1901        if now.saturating_sub(s.ready_at) > 60_000_000 {
1902            s.retry_ms = plat::env("jfgui_GPU_RETRY_MS").and_then(|v| v.parse().ok()).unwrap_or(RETRY_FIRST_MS);
1903        }
1904        s.dev = Dev::Retry(now + s.retry_ms * 1000);
1905        s.retry_ms = (s.retry_ms * 2).min(RETRY_MAX_MS);
1906    }
1907
1908    /// the jobs that finished: a device, a window's swapchain
1909    fn gp_poll(&mut self) {
1910        #[cfg(target_arch = "wasm32")]
1911        {
1912            let s = &mut self.m.gout;
1913            s.web_probe();
1914            if let Dev::Ready(core) = &s.dev {
1915                for w in s.wins.iter_mut().flatten() {
1916                    verify_poll(w, core);
1917                }
1918            }
1919        }
1920        let done = core::mem::take(&mut *self.m.gout.jobs.done.lock());
1921        for d in done {
1922            match d {
1923                Done::Device { job, win, r } => {
1924                    if !matches!(self.m.gout.dev, Dev::Starting(j) if j == job) {
1925                        continue;
1926                    }
1927                    match r {
1928                        Ok((core, built)) => {
1929                            let s = &mut self.m.gout;
1930                            s.note(&format!("device ready: {} ({:?})", core.name, core.dev.level()));
1931                            s.dev = Dev::Ready(core);
1932                            s.dev_gen += 1; // (views: their contents come to it with their next frame)
1933                            s.ready_at = plat::time_us();
1934                            self.gp_built(win, built);
1935                            if !self.m.gview.views.is_empty() {
1936                                self.invalidate(); // (a view at rest gets its frame on the new device)
1937                            }
1938                        }
1939                        Err(e) => {
1940                            let s = &mut self.m.gout;
1941                            s.note(&format!("no device: {e}"));
1942                            match e {
1943                                GpuError::DeviceLost | GpuError::OutOfMemory | GpuError::Timeout | GpuError::Failed(_) | GpuError::SurfaceLost
1944                                | GpuError::NotReady => {
1945                                    s.dev = Dev::Retry(plat::time_us() + s.retry_ms * 1000);
1946                                    s.retry_ms = (s.retry_ms * 2).min(RETRY_MAX_MS);
1947                                    plat::gpu_out_state(plat::GPU_STATE_UNKNOWN);
1948                                }
1949                                _ => {
1950                                    s.dev = Dev::Never(format!("{e}")); // no Vulkan, no usable adapter: the CPU path for good
1951                                    plat::gpu_out_state(plat::GPU_STATE_NEVER);
1952                                }
1953                            }
1954                            if let Some(w) = s.win(win) {
1955                                w.stage = Stage::Cpu;
1956                                w.lost = false;
1957                                w.out.error = Some(format!("{e}"));
1958                                // (the browser: the page's canvas, which never showed anything, goes)
1959                                #[cfg(target_arch = "wasm32")]
1960                                plat::win_gpu_child_show(w.pw, false);
1961                            }
1962                        }
1963                    }
1964                }
1965                Done::Window { job, win, r } => {
1966                    let live = self.m.gout.win(win).is_some_and(|w| w.job == job && w.stage == Stage::Job);
1967                    if !live {
1968                        continue;
1969                    }
1970                    match r {
1971                        Ok(b) => self.gp_built(win, b),
1972                        Err(e) => {
1973                            let now = plat::time_us();
1974                            let s = &mut self.m.gout;
1975                            s.note(&format!("window {win}: no swapchain: {e}"));
1976                            if let Some(w) = s.win(win) {
1977                                w.stage = Stage::Cpu;
1978                                w.stable_at = now;
1979                                if !core::mem::take(&mut w.lost) {
1980                                    w.out.error = Some(format!("{e}"));
1981                                    w.fail_ms = (w.fail_ms * 2).clamp(RETRY_FIRST_MS, RETRY_MAX_MS);
1982                                    w.fail_at = now + w.fail_ms * 1000;
1983                                }
1984                            }
1985                        }
1986                    }
1987                }
1988            }
1989        }
1990        let s = &mut self.m.gout;
1991        if !s.threads.is_empty() && s.jobs.running.load(Ordering::Acquire) == 0 {
1992            for t in s.threads.drain(..) {
1993                t.join();
1994            }
1995        }
1996    }
1997
1998    /// a swapchain and a texture for window i are there: on to the switch
1999    fn gp_built(&mut self, i: usize, b: Built) {
2000        let verify = self.m.gout.verify;
2001        let s = &mut self.m.gout;
2002        let Dev::Ready(core) = &s.dev else { return };
2003        let (dev, format) = (core.dev.clone(), core.format);
2004        let Some(w) = s.win(i) else { return };
2005        if core::mem::take(&mut w.lost) {
2006            w.stage = Stage::Cpu; // (Android: the surface it was built on went meanwhile)
2007            return;
2008        }
2009        if b.sc.format() != format {
2010            w.stage = Stage::Cpu;
2011            w.fail_at = u64::MAX;
2012            return;
2013        }
2014        let (tw, th) = b.sc.size();
2015        let tex = Tex { t: b.tex, group: b.group, w: tw as i32, h: th as i32 };
2016        w.surf = Some(b.surf);
2017        w.sc = Some(b.sc);
2018        w.tex = Some(tex);
2019        w.full = true;
2020        w.undef = true;
2021        w.rb = None;
2022        if verify {
2023            // (the browser: only a sign, show makes a buffer per frame)
2024            let size = if cfg!(target_arch = "wasm32") { 4 } else { tw as u64 * th as u64 * 4 };
2025            w.rb = dev.create_buffer(&BufferDesc { size, usage: BufferUsage::NONE, memory: MemoryKind::Readback }).ok();
2026        }
2027        w.stage = Stage::Switch;
2028        if !unseen(w) {
2029            self.gp_switch(i);
2030        }
2031    }
2032
2033    /// the whole framebuffer on the new swapchain, then the child shown: from now on the GPU
2034    fn gp_switch(&mut self, i: usize) {
2035        let t0 = plat::time_us();
2036        let mut s = core::mem::take(&mut self.m.gout);
2037        let mut failed = None;
2038        let mut again = false;
2039        let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
2040        if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
2041            if w.tex.as_ref().is_none_or(|t| (t.w, t.h) != (fb.w, fb.h)) || !plat::win_gpu_child_alone(w.pw) {
2042                w.stage = Stage::Cpu; // the size changed meanwhile (Android: something else shows now)
2043                w.stable_at = t0;
2044                again = true;
2045            } else {
2046                let ready = w.sc.as_mut().map_or(Ok(true), |sc| sc.prepare());
2047                let shown = match ready {
2048                    Ok(true) => upload(w, core, fb, None, &mut s.trim_at).and_then(|_| show(w, core, fb)),
2049                    Ok(false) => Ok(Shown::Wait), // (Linux: the display has not answered for the images yet)
2050                    Err(e) => Err(e),
2051                };
2052                match shown {
2053                    Ok(Shown::Done) => {
2054                        plat::win_gpu_child_show(w.pw, true);
2055                        plat::win_release_kept(w.pw);
2056                        w.stage = Stage::Gpu;
2057                        w.out.switches += 1;
2058                        w.out.error = None;
2059                        if !core::mem::replace(&mut s.said_ok, true) {
2060                            plat::gpu_out_state(plat::GPU_STATE_OK);
2061                        }
2062                    }
2063                    Ok(Shown::Wait) => w.pending = true,
2064                    Ok(Shown::NoArea) | Ok(Shown::Stale) => {
2065                        w.stage = Stage::Cpu;
2066                        w.stable_at = t0;
2067                    }
2068                    Err(e) => failed = Some(e),
2069                }
2070            }
2071        }
2072        self.m.gout = s;
2073        if let Some(e) = failed {
2074            self.gp_failed(i, e);
2075        } else if !again {
2076            let t1 = plat::time_us();
2077            let on = self.m.gout.wins.get(i).and_then(|w| w.as_ref()).is_some_and(|w| w.stage == Stage::Gpu);
2078            if on {
2079                self.m.gout.note(&format!("window {i}: switched to the swapchain ({:.2} ms)", ms(t0, t1)));
2080            }
2081        }
2082    }
2083
2084    /// the window loop: finished jobs, frames still to show, new jobs
2085    pub(crate) fn gp_tick(&mut self) {
2086        if !self.m.gout.want {
2087            return;
2088        }
2089        self.gp_poll();
2090        // (the browser tells a lost device, or an error of an earlier call, between frames too)
2091        #[cfg(target_arch = "wasm32")]
2092        {
2093            let e = if let Dev::Ready(core) = &self.m.gout.dev { core.dev.poll().err() } else { None };
2094            // (and the page's watcher: the canvas shows nothing, a new one would not either)
2095            let e = e.or(plat::win_gpu_child_blank().then_some(GpuError::Unsupported("the canvas shows nothing")));
2096            if let Some(e) = e {
2097                self.gp_failed(0, e);
2098                return;
2099            }
2100        }
2101        let n = self.m.gout.wins.len();
2102        let now = plat::time_us();
2103        self.gp_canvas_tick(now);
2104        self.gp_views_tick(now);
2105        for w in self.m.gout.wins.iter_mut().flatten() {
2106            if w.sp.linger_at != 0 && now >= w.sp.linger_at {
2107                w.sp.linger_at = 0;
2108                for u in w.sp.upper.iter_mut().filter(|u| u.rects.is_empty()) {
2109                    u.tex = None;
2110                    u.store = Store::default();
2111                }
2112                self.m.gout.trim_at = now + TRIM_US; // (destroyed when the GPU has finished)
2113            }
2114        }
2115        for i in 0..n {
2116            let Some(w) = self.m.gout.wins[i].as_ref() else { continue };
2117            // (Linux: a frame the GPU has finished goes to the window)
2118            if matches!(w.stage, Stage::Gpu | Stage::Switch) && w.sc.as_ref().is_some_and(|sc| sc.busy()) {
2119                if let Some(Err(e)) = w.sc.as_ref().map(|sc| sc.flush()) {
2120                    self.gp_failed(i, e);
2121                    continue;
2122                }
2123            }
2124            let Some(w) = self.m.gout.wins[i].as_ref() else { continue };
2125            match w.stage {
2126                Stage::Switch if !unseen(w) => self.gp_switch(i),
2127                Stage::Gpu if unseen(w) => self.gp_leave(i, "hidden"),
2128                // (Android: a tool window or popup is composed on the canvas, below the surface)
2129                Stage::Gpu if !plat::win_gpu_child_alone(w.pw) => self.gp_leave(i, "something else shows"),
2130                Stage::Gpu if w.pending && !w.test_hold && plat::time_us() >= w.last_present + MIN_PRESENT_US => self.gp_retry(i),
2131                _ => {}
2132            }
2133        }
2134        self.gp_views_wake(plat::time_us());
2135        self.gp_jobs();
2136        let s = &mut self.m.gout;
2137        if s.trim_at != 0 && plat::time_us() >= s.trim_at {
2138            s.trim_at = 0;
2139            if let Dev::Ready(core) = &s.dev {
2140                let _ = core.dev.poll();
2141            }
2142        }
2143    }
2144
2145    /// a frame that was not shown yet: window i's texture to the screen (a minimised window
2146    /// comes back: the whole frame first)
2147    fn gp_retry(&mut self, i: usize) {
2148        if self.m.gout.verify {
2149            self.gp_settle(i);
2150        }
2151        let mut s = core::mem::take(&mut self.m.gout);
2152        let mut r = Ok(Shown::Done);
2153        let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
2154        if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
2155            if plat::win_state(w.pw) & plat::WST_MINIMIZED == 0 {
2156                let t0 = plat::time_us();
2157                r = if w.full { upload(w, core, fb, None, &mut s.trim_at) } else { Ok(()) }.and_then(|_| show(w, core, fb));
2158                plat::present_account(t0, false);
2159            }
2160        }
2161        self.m.gout = s;
2162        match r {
2163            Ok(Shown::Stale) => self.gp_leave(i, "stale"),
2164            Err(e) => self.gp_failed(i, e),
2165            _ => {}
2166        }
2167    }
2168
2169    /// what should run now: the device, the swapchains of windows on the CPU path
2170    fn gp_jobs(&mut self) {
2171        let now = plat::time_us();
2172        let s = &mut self.m.gout;
2173        if plat::win_in_sizemove() {
2174            return;
2175        }
2176        match s.dev {
2177            Dev::None => {}
2178            Dev::Retry(at) if at <= now => s.dev = Dev::None,
2179            _ => {}
2180        }
2181        #[cfg(target_arch = "wasm32")]
2182        if s.web_start() {
2183            return;
2184        }
2185        if matches!(s.dev, Dev::None) {
2186            if plat::win_remote_session() {
2187                s.dev = Dev::Retry(now + REMOTE_LOOK_US); // this session has no use for it (yet)
2188                return;
2189            }
2190            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 {
2191                return;
2192            };
2193            let Some(nw) = plat::win_gpu_child(pw) else {
2194                if !plat::win_gpu_child_later(pw) {
2195                    s.dev = Dev::Never(String::from("no child window"));
2196                }
2197                return;
2198            };
2199            let display = plat::win_display_adapter(pw);
2200            let desc = s.desc();
2201            let job = s.next_job;
2202            s.next_job += 1;
2203            let (trace, want3d) = (s.trace, s.want3d);
2204            s.note("start: instance, device, swapchain on a worker");
2205            plat::gpu_out_state(plat::GPU_STATE_TRYING);
2206            let ok = s.spawn(move || Done::Device { job, win: i, r: build_device(None, nw, pw, display, desc, trace, want3d) });
2207            if ok {
2208                s.dev = Dev::Starting(job);
2209                if let Some(w) = s.win(i) {
2210                    w.stage = Stage::Job;
2211                    w.job = job;
2212                }
2213            } else {
2214                s.dev = Dev::Never(String::from("no thread"));
2215            }
2216            return;
2217        }
2218        let Dev::Ready(core) = &s.dev else { return };
2219        let (dev, bgl, inst) = (core.dev.clone(), core.bgl.clone(), core.inst.clone());
2220        let n = s.wins.len();
2221        let desc = s.desc();
2222        for i in 0..n {
2223            let Some(w) = s.wins[i].as_ref() else { continue };
2224            if w.stage != Stage::Cpu || unseen(w) || now < w.fail_at || now < w.stable_at + STABLE_US {
2225                continue;
2226            }
2227            let pw = w.pw;
2228            if plat::win_state(pw) & plat::WST_MINIMIZED != 0 || plat::win_size(pw) != w.size {
2229                continue;
2230            }
2231            let Some(nw) = plat::win_gpu_child(pw) else { continue };
2232            let job = s.next_job;
2233            s.next_job += 1;
2234            let Some(w) = s.wins[i].as_mut() else { continue };
2235            let old = w.sc.take();
2236            let surf = match w.surf.take() {
2237                Some(x) => {
2238                    x.track_size(); // (Linux: a worker may not ask the window system)
2239                    x
2240                }
2241                None => match gpu::Surface::native(&inst, nw, pw) {
2242                    Ok(x) => x,
2243                    Err(_) => {
2244                        w.fail_at = u64::MAX;
2245                        continue;
2246                    }
2247                },
2248            };
2249            w.stage = Stage::Job;
2250            w.job = job;
2251            #[allow(clippy::redundant_locals)] // each window's job takes a copy
2252            let desc = desc;
2253            let (d2, b2) = (dev.clone(), bgl.clone());
2254            s.note(&format!("window {i}: swapchain on a worker"));
2255            let ok = s.spawn(move || Done::Window { job, win: i, r: build_window(&d2, &b2, surf, old, &desc) });
2256            if !ok {
2257                if let Some(w) = s.win(i) {
2258                    w.stage = Stage::Cpu;
2259                    w.fail_at = u64::MAX;
2260                }
2261            }
2262        }
2263    }
2264
2265    /// when the window loop should look again (plat::time_us; None: not until an event)
2266    pub(crate) fn gp_deadline(&self) -> Option<u64> {
2267        let s = &self.m.gout;
2268        if !s.want || s.wins.is_empty() {
2269            return None;
2270        }
2271        let now = plat::time_us();
2272        let mut d = u64::MAX;
2273        if s.jobs.running.load(Ordering::Acquire) != 0 {
2274            d = now + JOB_US;
2275        }
2276        match s.dev {
2277            Dev::None if s.wins.iter().flatten().next().is_some_and(|w| !plat::win_gpu_child_later(w.pw)) => d = now,
2278            Dev::Retry(at) => d = d.min(at),
2279            _ => {}
2280        }
2281        if s.trim_at != 0 {
2282            d = d.min(s.trim_at);
2283        }
2284        if !s.script {
2285            for c in s.canvases.iter().filter(|c| c.apart) {
2286                d = d.min(c.until);
2287            }
2288        }
2289        if s.canvas_free_at != 0 {
2290            d = d.min(s.canvas_free_at);
2291        }
2292        // (views: the look at rest, once after the last frame; G5e3: a frame that waits for its window, at the latest
2293        // when it has waited long enough, at once when the window's frame has been shown)
2294        for e in self.m.gview.views.iter() {
2295            if e.rest_at != 0 {
2296                d = d.min(e.rest_at);
2297            }
2298            if e.held_at != 0 && !e.woke {
2299                d = d.min(if s.unshown(e.win) { e.held_at + VIEW_HOLD_US } else { now });
2300            }
2301        }
2302        let ready = matches!(s.dev, Dev::Ready(_));
2303        let moving = plat::win_in_sizemove();
2304        for w in s.wins.iter().flatten() {
2305            if w.sp.linger_at != 0 {
2306                d = d.min(w.sp.linger_at);
2307            }
2308            let minimised = plat::win_state(w.pw) & plat::WST_MINIMIZED != 0;
2309            // (Linux: a frame on its way, a display that has not answered yet)
2310            let on_way = w.sc.as_ref().is_some_and(|sc| sc.busy());
2311            match w.stage {
2312                Stage::Switch if !minimised && !unseen(w) => d = d.min(now + RETRY_US),
2313                Stage::Gpu if on_way => d = d.min(now + RETRY_US.min(1000)),
2314                Stage::Gpu if w.pending && !minimised && !w.test_hold => {
2315                    let retry = (w.last_present + MIN_PRESENT_US).max(now + RETRY_US.min(1000));
2316                    d = d.min(w.sc.as_ref().map_or(retry, |sc| sc.next_look(retry)));
2317                }
2318                Stage::Cpu if ready && !minimised && !moving && !unseen(w) && w.fail_at != u64::MAX && !plat::win_gpu_child_later(w.pw) => {
2319                    // (a size the frame has not reached yet: its event is on the way)
2320                    let settle = if plat::win_size(w.pw) == w.size { 1000 } else { 16_000 };
2321                    d = d.min((w.stable_at + STABLE_US).max(w.fail_at).max(now + settle));
2322                }
2323                _ => {}
2324            }
2325        }
2326        if d == u64::MAX { None } else { Some(d) }
2327    }
2328
2329    /// (Android) the system took the surface (the activity stopped; or the output's own hide, then
2330    /// nothing is on it any more): every window back to the CPU path, its swapchain gone before the
2331    /// host's callback returns -> a frame waits to reach the host's Bitmap (the View may be seen
2332    /// again before the next frame)
2333    #[cfg(target_os = "android")]
2334    pub(crate) fn gp_surface_gone(&mut self) -> bool {
2335        if !self.m.gout.want {
2336            return false;
2337        }
2338        let mut waits = false;
2339        for i in 0..self.m.gout.wins.len() {
2340            let Some(stage) = self.m.gout.wins[i].as_ref().map(|w| w.stage) else { continue };
2341            if stage == Stage::Gpu {
2342                self.gp_leave(i, "the surface went");
2343                waits = true;
2344            }
2345            let Some(w) = self.m.gout.win(i) else { continue };
2346            if stage == Stage::Job {
2347                w.lost = true; // (what it built goes when it ends)
2348            } else {
2349                w.stage = Stage::Cpu;
2350            }
2351            w.sc = None;
2352            w.surf = None;
2353            w.tex = None;
2354            w.rb = None;
2355            w.pending = false;
2356            w.sp.reset();
2357        }
2358        waits
2359    }
2360
2361    /// wait for the jobs (the message loop pumps: a driver may send messages to the window while
2362    /// it builds a swapchain), at most ms
2363    fn gp_wait_jobs(&mut self, ms: u64) {
2364        let dl = plat::time_us() + ms * 1000;
2365        while self.m.gout.jobs.running.load(Ordering::Acquire) != 0 && plat::time_us() < dl {
2366            plat::wait(plat::time_us() + 1000);
2367        }
2368        self.gp_poll();
2369    }
2370
2371    /// window i goes away: its swapchain and surface before its plat window
2372    pub(crate) fn gp_window_gone(&mut self, i: u32) {
2373        let i = i as usize;
2374        // (its views: their contents leave the device, the records go)
2375        let views = &mut self.m.gview.views;
2376        for e in views.iter_mut().filter(|e| e.win == i) {
2377            view_detach(e);
2378        }
2379        views.retain(|e| e.win != i);
2380        if self.m.gout.wins.get(i).and_then(|w| w.as_ref()).is_none() {
2381            return;
2382        }
2383        if self.m.gout.jobs.running.load(Ordering::Acquire) != 0 {
2384            self.gp_wait_jobs(3000);
2385        }
2386        if let Some(w) = self.m.gout.wins.get_mut(i) {
2387            *w = None;
2388        }
2389        let s = &mut self.m.gout;
2390        for k in (0..s.canvases.len()).rev() {
2391            if s.canvases[k].win == i {
2392                let c = s.canvases.remove(k);
2393                let n: usize = c.levels.iter().map(|l| l.tiles.len()).sum();
2394                s.tile_bytes -= n as u64 * TILE_BYTES;
2395            }
2396        }
2397    }
2398
2399    /// window i is about to be hidden or shown again: hidden, it goes back to the CPU path (the top of
2400    /// this file), and its swapchain follows when it is back and has settled
2401    pub(crate) fn gp_window_show(&mut self, i: u32, show: bool) {
2402        let i = i as usize;
2403        let Some(stage) = self.m.gout.wins.get(i).and_then(|w| w.as_ref()).map(|w| w.stage) else { return };
2404        if !show && stage == Stage::Gpu {
2405            self.gp_leave(i, "hidden");
2406        }
2407        let now = plat::time_us();
2408        if let Some(w) = self.m.gout.win(i) {
2409            w.hidden = !show;
2410            w.stable_at = now;
2411        }
2412    }
2413
2414    /// the window loop ended: everything of the GPU goes (a job still running after 2 s is left
2415    /// to the process's end)
2416    pub(crate) fn gp_shutdown(&mut self) {
2417        // (views: their contents drop what they built before a device goes, also the views' own)
2418        self.gp_views_gone(None);
2419        self.m.gview.own = Own::None;
2420        if !self.m.gout.want {
2421            return;
2422        }
2423        let t0 = plat::time_us();
2424        self.gp_wait_jobs(2000);
2425        let s = &mut self.m.gout;
2426        // the driver's end of a process takes 100 ms: the windows go first
2427        for w in s.wins.iter().flatten() {
2428            plat::win_show(w.pw, plat::PWS_HIDE);
2429        }
2430        // the process ends now: tearing the device down took 190 ms of the exit (measured). The
2431        // validation layer wants to see every object destroyed
2432        if env_on("jfgui_GPU_VALIDATE") {
2433            s.wins.clear();
2434            s.images.clear();
2435            s.canvases.clear();
2436            s.tile_free.clear();
2437            s.dev = Dev::None;
2438        } else {
2439            core::mem::forget(core::mem::take(&mut s.wins));
2440            core::mem::forget(core::mem::take(&mut s.images));
2441            core::mem::forget(core::mem::take(&mut s.canvases));
2442            core::mem::forget(core::mem::take(&mut s.tile_free));
2443            core::mem::forget(core::mem::replace(&mut s.dev, Dev::None));
2444        }
2445        s.want = false;
2446        s.note(&format!("shut down: {:.2} ms", ms(t0, plat::time_us())));
2447    }
2448
2449    /// what window `win` (a UI window index, 0 = main) shows through
2450    pub fn gpu_output(&self, win: u32) -> GpuOutput {
2451        let s = &self.m.gout;
2452        let mut o = s.wins.get(win as usize).and_then(|w| w.as_ref()).map_or_else(GpuOutput::default, |w| {
2453            let mut o = w.out.clone();
2454            o.on = w.stage == Stage::Gpu;
2455            o.building = matches!(w.stage, Stage::Job | Stage::Switch);
2456            o.pending = w.pending;
2457            for u in &w.sp.upper {
2458                if let Some(t) = &u.tex {
2459                    o.layer_textures += !u.rects.is_empty() as u32;
2460                    o.layer_bytes += t.w as u64 * t.h as u64 * 4;
2461                }
2462                o.layer_store += u.store.px.capacity() as u64;
2463            }
2464            o.image_quads = w.sp.quads.iter().filter(|q| matches!(q.src, QuadSrc::Image { .. })).count() as u32;
2465            o.view_quads = w.sp.quads.iter().filter(|q| matches!(q.src, QuadSrc::View { .. })).count() as u32;
2466            o.canvas_quads = w.sp.quads.len() as u32 - o.image_quads - o.view_quads;
2467            o
2468        });
2469        o.wanted = s.want;
2470        o.image_textures = s.images.iter().filter(|e| e.tex.is_some()).count() as u32;
2471        o.image_bytes = s.image_bytes;
2472        o.image_uploads = s.image_uploads;
2473        o.image_hits = s.image_hits;
2474        o.image_evicted = s.image_evicted;
2475        o.canvas_apart = s.canvases.iter().filter(|c| c.apart && c.win == win as usize).count() as u32;
2476        o.canvas_tiles = s.canvases.iter().flat_map(|c| c.levels.iter()).map(|l| l.tiles.len() as u32).sum();
2477        o.canvas_bytes = s.tile_bytes;
2478        o.canvas_fills = s.tile_fills;
2479        o.canvas_fill_us = s.tile_fill_us;
2480        o.canvas_evicted = s.tile_evicted;
2481        o.canvas_settles = s.canvas_settles;
2482        match &s.dev {
2483            Dev::Ready(core) => {
2484                o.adapter = core.name.clone();
2485                o.reserved = core.dev.memory_reserved();
2486                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() {
2487                    PresentMode::Fifo => "fifo",
2488                    PresentMode::Mailbox => "mailbox",
2489                    PresentMode::Immediate => "immediate",
2490                });
2491            }
2492            Dev::Never(why) => {
2493                o.never = true;
2494                if o.error.is_none() {
2495                    o.error = Some(why.clone());
2496                }
2497            }
2498            _ => {}
2499        }
2500        o
2501    }
2502
2503    /// (test aid) the device behaves as lost from now on: the next frame goes back to the CPU
2504    /// path, a new device follows
2505    pub fn gpu_test_lose(&mut self) {
2506        if let Dev::Ready(core) = &self.m.gout.dev {
2507            core.dev.test_lose();
2508        }
2509    }
2510
2511    /// (test aid, G5e3) window `win`'s frames are not shown while on (as if the display did not answer): they stay in
2512    /// its texture, its views' new frames wait for them; off: the next look shows the newest
2513    pub fn gpu_test_hold(&mut self, win: u32, on: bool) {
2514        if let Some(w) = self.m.gout.win(win as usize) {
2515            w.test_hold = on;
2516        }
2517    }
2518
2519    /// (test aid) the bytes image textures may take
2520    pub fn gpu_test_image_budget(&mut self, bytes: u64) {
2521        self.m.gout.image_budget = bytes;
2522    }
2523
2524    /// (test aid) the bytes canvas tiles may take
2525    pub fn gpu_test_canvas_budget(&mut self, bytes: u64) {
2526        self.m.gout.canvas_budget = bytes;
2527    }
2528
2529    /// (test aid) the tiles go this long (ms) after the last move
2530    pub fn gpu_test_canvas_unused(&mut self, ms: u64) {
2531        self.m.gout.canvas_unused_us = ms * 1000;
2532    }
2533
2534    /// every canvas whose view comes from tiles goes back to the CPU's picture with the next frame
2535    pub fn gpu_canvas_settle(&mut self) {
2536        let mut any = false;
2537        let s = &mut self.m.gout;
2538        for c in s.canvases.iter_mut().filter(|c| c.apart) {
2539            c.apart = false;
2540            any = true;
2541            s.canvas_settles += 1;
2542        }
2543        if any {
2544            self.invalidate();
2545        }
2546    }
2547
2548    /// the pixels of the images the GPU draws into the framebuffers (Ui::fb reads them then)
2549    pub fn gpu_settle(&mut self) {
2550        self.gp_settle_all();
2551    }
2552
2553    // ------------------------------------------------------------------ layers and images
2554
2555    /// render_live: does the live window keep its layers apart this frame?
2556    pub(crate) fn gp_split_wanted(&self) -> bool {
2557        let s = &self.m.gout;
2558        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)
2559    }
2560
2561    /// can window i's frame be drawn apart: the texture fits, it can be seen, no inspector draws
2562    /// over the framebuffer
2563    fn gp_can_split(&self, i: usize) -> bool {
2564        let Some(Some(w)) = self.m.gout.wins.get(i) else { return false };
2565        let fb = &self.wp.rt.fb;
2566        !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
2567    }
2568
2569    /// dirty_compute, after hashing (dirty.rs has W::imgs, W::top; cells: this frame, cells_prev:
2570    /// the last): which images the GPU draws in this frame, in which cells -> anything apart
2571    pub(crate) fn gp_plan_images(&mut self) -> bool {
2572        let i = self.uiw_cur as usize;
2573        if !self.gp_can_split(i) {
2574            return true; // gp_split_begin settles and flattens with the last frame's images
2575        }
2576        let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
2577        let opaque_dc = self.dc.opac == 255;
2578        let Ui { m, wp, .. } = self;
2579        let s = &mut m.gout;
2580        s.tick += 1;
2581        let (tick, now) = (s.tick, plat::time_us());
2582        let d = &wp.dirty;
2583        let (cols, rows) = (d.cols as usize, d.rows as usize);
2584        let total = cols * rows;
2585        let Some(Some(w)) = s.wins.get_mut(i) else { return false };
2586        let sp = &mut w.sp;
2587        if (sp.cols, sp.rows) != (cols, rows) || sp.hole.len() != total {
2588            (sp.cols, sp.rows) = (cols, rows);
2589            sp.hole = vec![0; total];
2590            sp.owed = vec![false; total];
2591            sp.stale = vec![false; total];
2592            sp.part = vec![false; total];
2593            sp.redraw = vec![false; total];
2594            sp.any_owed = false;
2595            sp.any_stale = false;
2596        }
2597        sp.hole.fill(0);
2598        sp.part.fill(false);
2599        let mut quads = core::mem::take(&mut sp.quads);
2600        quads.clear();
2601        // an image gone or drawn in another version takes its texture along
2602        let (cur, prev) = (d.cells.as_deref().unwrap_or(&[]), d.cells_prev.as_deref().unwrap_or(&[]));
2603        let mut k = 0;
2604        while k < s.images.len() {
2605            if s.images[k].weak.strong_count() == 0 {
2606                s.evict(k);
2607                s.images.remove(k);
2608            } else {
2609                k += 1;
2610            }
2611        }
2612        if s.images_on && opaque_dc {
2613            for (tag0, seen) in d.imgs.iter().enumerate() {
2614                let tag = tag0 as u8 + 1;
2615                let Some(Cmd::Image { x, y, w: iw, h: ih, sf, ver }) = wp.cmd.layers[0].get(seen.index as usize) else { continue };
2616                if sf.w <= 0 || sf.h <= 0 || sf.px.is_empty() || sf.fmt == FMT_A8 {
2617                    continue;
2618                }
2619                let key = Rc::as_ptr(sf) as usize;
2620                let vis = seen.vis;
2621                // the cells it is on top of, whole; what is new in this frame
2622                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));
2623                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));
2624                let mut changed = 0u64;
2625                let mut any = false;
2626                for r in r0..r1 {
2627                    for c in c0..c1 {
2628                        let k = r * cols + c;
2629                        if d.top.get(k) == Some(&tag) {
2630                            any = true;
2631                            if cur.get(k) != prev.get(k) {
2632                                changed += CELL_BYTES as u64;
2633                            }
2634                        }
2635                    }
2636                }
2637                if !any {
2638                    continue;
2639                }
2640                // another version of this surface: that one is over
2641                let mut q = 0;
2642                while q < s.images.len() {
2643                    if s.images[q].key == key && s.images[q].ver != *ver {
2644                        s.evict(q);
2645                        s.images.remove(q);
2646                    } else {
2647                        q += 1;
2648                    }
2649                }
2650                let at = match s.images.iter().position(|e| e.key == key) {
2651                    Some(at) => {
2652                        s.images[at].acc += changed;
2653                        at
2654                    }
2655                    None => {
2656                        if s.images.len() >= IMAGES_KEPT {
2657                            let Some(old) = (0..s.images.len()).min_by_key(|&k| s.images[k].tick) else { continue };
2658                            s.evict(old);
2659                            s.images.remove(old);
2660                        }
2661                        s.images.push(Image {
2662                            key,
2663                            weak: Rc::downgrade(sf),
2664                            ver: *ver,
2665                            bytes: sf.w as u64 * sf.h as u64 * 4,
2666                            acc: 0,
2667                            opaque: None,
2668                            tex: None,
2669                            load: None,
2670                            tick,
2671                            used_us: now,
2672                        });
2673                        s.images.len() - 1
2674                    }
2675                };
2676                s.images[at].tick = tick;
2677                s.images[at].used_us = now;
2678                // rent or buy: the redraws have cost what the upload costs
2679                let e = &s.images[at];
2680                if e.tex.is_none() && e.load.is_none() && e.acc >= e.bytes && e.bytes <= s.image_budget {
2681                    if let Dev::Ready(core) = &s.dev {
2682                        let lim = core.dev.limits().max_texture_2d as i32;
2683                        let opaque = e.opaque.unwrap_or_else(|| surface_opaque(sf));
2684                        s.images[at].opaque = Some(opaque);
2685                        if opaque && sf.w <= lim && sf.h <= lim {
2686                            let need = s.images[at].bytes;
2687                            while s.image_bytes + need > s.image_budget {
2688                                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);
2689                                let Some(old) = old else { break };
2690                                s.evict(old);
2691                            }
2692                            if s.image_bytes + need <= s.image_budget {
2693                                if let Dev::Ready(core) = &s.dev {
2694                                    // (a device gone or no memory: the CPU draws it, a failure of the
2695                                    // device shows with the next present)
2696                                    match image_texture(core, sf) {
2697                                        Ok(load) => {
2698                                            s.images[at].load = Some(load);
2699                                            s.image_bytes += need;
2700                                        }
2701                                        Err(_) => s.images[at].opaque = Some(false),
2702                                    }
2703                                }
2704                            }
2705                        }
2706                    }
2707                }
2708                // the next rows of a texture on the way up; the last ones make it usable
2709                if let (Dev::Ready(core), Some(load)) = (&s.dev, s.images[at].load.as_mut()) {
2710                    let t0 = plat::time_us();
2711                    let rows = ((IMAGE_STEP_BYTES / (sf.w as u64 * 4)).max(1) as i32).min(sf.h - load.row);
2712                    let before = if load.row == 0 { Layout::Undefined } else { Layout::Sampled };
2713                    match core.dev.queue().write_texture_rects(&load.t, &[[0, load.row, sf.w, rows]], &sf.px, sf.stride as u32, before, Layout::Sampled) {
2714                        Ok(()) => {
2715                            load.row += rows;
2716                            let done = load.row >= sf.h;
2717                            let bytes = sf.w as u64 * rows as u64 * 4;
2718                            s.trim_at = plat::time_us() + TRIM_US;
2719                            if let Some(Some(w)) = s.wins.get_mut(i) {
2720                                w.out.upload_bytes += bytes;
2721                            }
2722                            if done {
2723                                if let Some(load) = s.images[at].load.take() {
2724                                    s.images[at].tex = Some(Rc::new(ImageTex { _t: load.t, group: load.group }));
2725                                    s.image_uploads += 1;
2726                                }
2727                                s.note(&format!("image {} x {} as a texture, the last part {:.2} ms", sf.w, sf.h, ms(t0, plat::time_us())));
2728                            }
2729                        }
2730                        Err(_) => {
2731                            s.evict(at);
2732                            s.images[at].opaque = Some(false);
2733                        }
2734                    }
2735                }
2736                let Some(tex) = s.images[at].tex.clone() else { continue };
2737                if quads.len() >= TOP_BLOCKED as usize - 1 {
2738                    continue;
2739                }
2740                s.image_hits += 1;
2741                let Some(Some(w)) = s.wins.get_mut(i) else { continue };
2742                let sp = &mut w.sp;
2743                let q = quads.len() as u8 + 1;
2744                for r in r0..r1 {
2745                    for c in c0..c1 {
2746                        let k = r * cols + c;
2747                        if d.top.get(k) == Some(&tag) {
2748                            sp.hole[k] = q;
2749                        }
2750                    }
2751                }
2752                let mut rects = Vec::new();
2753                let hole = &sp.hole;
2754                cell_rects(&mut rects, cols, rows, fw, fh, |k| hole[k] == q);
2755                let step = [(((sf.w as u64) << 16) / *iw as u64) as u32, (((sf.h as u64) << 16) / *ih as u64) as u32];
2756                quads.push(Quad { x: *x, y: *y, src: QuadSrc::Image { tex, src: sf.clone(), w: *iw, h: *ih, step }, rects });
2757            }
2758        }
2759        // what was not drawn for a long time gives its memory back
2760        if now >= s.look_at && !s.images.is_empty() {
2761            s.look_at = now + IMAGE_UNUSED_US / 4;
2762            for k in 0..s.images.len() {
2763                if now.saturating_sub(s.images[k].used_us) > IMAGE_UNUSED_US {
2764                    s.evict(k);
2765                }
2766            }
2767        }
2768        let Some(Some(w)) = s.wins.get_mut(i) else { return false };
2769        w.sp.quads = quads;
2770        !w.sp.quads.is_empty()
2771    }
2772
2773    /// widgets build a CanvasDesc when this says so (canvas.rs)
2774    pub(crate) fn gp_canvas_wanted(&self) -> bool {
2775        let s = &self.m.gout;
2776        s.want && s.canvases_on
2777    }
2778
2779    /// a widget about to record canvas id with this content version: would a move of its view come
2780    /// from tiles in the next frame?
2781    pub(crate) fn gp_canvas_steady(&self, id: u32, ver: u32) -> bool {
2782        let i = self.uiw_cur as usize;
2783        let s = &self.m.gout;
2784        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)
2785    }
2786
2787    /// dirty_compute, before hashing: what the view of each canvas of the frame did since the last
2788    /// frame, and so which of them come from tiles now (W::canv)
2789    pub(crate) fn gp_canvas_begin(&mut self) {
2790        let i = self.uiw_cur as usize;
2791        let can = self.wp.dirty.split && !self.rtl && self.dc.opac == 255 && self.gp_can_split(i);
2792        let now = plat::time_us();
2793        let Ui { m, wp, .. } = self;
2794        let s = &mut m.gout;
2795        let (settle, unused) = (s.canvas_settle_us, s.canvas_unused_us);
2796        wp.dirty.canv.clear();
2797        for rec in wp.canv.recs.iter() {
2798            let d = &rec.d;
2799            let mode = s.canvas_mode(d.name, d.gpu);
2800            if mode == CanvasGpu::Cpu {
2801                continue;
2802            }
2803            let ci = s.canvases.iter().position(|c| c.win == i && c.id == d.id).unwrap_or_else(|| {
2804                s.canvases.push(Canvas {
2805                    win: i,
2806                    id: d.id,
2807                    ver: d.ver,
2808                    view: d.view,
2809                    prev: d.view,
2810                    known: false,
2811                    damaged: false,
2812                    apart: false,
2813                    until: 0,
2814                    moved: false,
2815                    levels: Vec::new(),
2816                });
2817                s.canvases.len() - 1
2818            });
2819            let new_ver = s.canvases[ci].ver != d.ver;
2820            if new_ver {
2821                let levels = core::mem::take(&mut s.canvases[ci].levels);
2822                for l in levels {
2823                    s.tiles_free(l.tiles);
2824                }
2825            }
2826            let c = &mut s.canvases[ci];
2827            let changed = new_ver || c.damaged;
2828            let moved = c.known && c.view != d.view && d.view.scale > 0.0 && d.view.scale < 1.0e6;
2829            let (dx, dy) = (d.view.ox - c.view.ox, d.view.oy - c.view.oy);
2830            let whole = d.view.scale == c.view.scale && (dx - rnd(dx)).abs() < PHASE_TOL && (dy - rnd(dy)).abs() < PHASE_TOL;
2831            c.moved = false;
2832            if changed || !can || rec.layer != 0 {
2833                c.apart = false; // the CPU draws this frame, as without tiles
2834            } else if moved {
2835                c.apart = mode == CanvasGpu::Zoom || whole;
2836                if c.apart {
2837                    c.moved = true;
2838                    c.until = now + settle;
2839                    s.canvas_free_at = c.until + unused;
2840                }
2841            }
2842            let c = &mut s.canvases[ci];
2843            if moved {
2844                c.prev = c.view;
2845            }
2846            c.ver = d.ver;
2847            c.view = d.view;
2848            c.damaged = false;
2849            c.known = true;
2850            if c.apart {
2851                wp.dirty.canv.push((rec.index, d.rect));
2852            }
2853        }
2854    }
2855
2856    /// dirty_compute, after hashing: the tiles each canvas apart needs now (the ones missing are
2857    /// drawn for up to FILL_US), the cells the GPU draws it in -> any canvas apart
2858    pub(crate) fn gp_plan_canvases(&mut self) -> bool {
2859        let i = self.uiw_cur as usize;
2860        if self.wp.dirty.canv.is_empty() {
2861            return false;
2862        }
2863        let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
2864        let Ui { m, wp, text, .. } = self;
2865        let font = text.get_font();
2866        let s = &mut m.gout;
2867        s.tick += 1;
2868        let (tick, t0) = (s.tick, plat::time_us());
2869        let fill_end = if s.script { u64::MAX } else { t0 + FILL_US };
2870        let d = &wp.dirty;
2871        let (cols, rows) = (d.cols as usize, d.rows as usize);
2872        let Some(Some(w)) = s.wins.get_mut(i) else { return false };
2873        let mut quads = core::mem::take(&mut w.sp.quads);
2874        let mut sf = core::mem::take(&mut s.tile_sf);
2875        if sf.px.is_empty() {
2876            sf = Surface::new(TILE_TEX, TILE_TEX, FMT_XRGB);
2877        }
2878        for (tag0, seen) in d.imgs.iter().enumerate() {
2879            let Some(Cmd::Custom { x, y, w: cw, h: ch, f: Some(f), .. }) = wp.cmd.layers[0].get(seen.index as usize) else { continue };
2880            let Some(rec) = wp.canv.recs.iter().find(|r| r.layer == 0 && r.index == seen.index) else { continue };
2881            let Some(ci) = s.canvases.iter().position(|c| c.win == i && c.id == rec.d.id) else { continue };
2882            if quads.len() >= TOP_BLOCKED as usize - 1 {
2883                continue;
2884            }
2885            let (tag, vis, v) = (tag0 as u8 + 1, seen.vis, rec.d.view);
2886            let zoom = s.canvas_mode(rec.d.name, rec.d.gpu) == CanvasGpu::Zoom;
2887            let (moved, li) = {
2888                let c = &mut s.canvases[ci];
2889                (c.moved, level_for(c, v, zoom))
2890            };
2891            let mut li = li;
2892            if s.canvases[ci].levels.len() > LEVELS_MAX {
2893                let old = s.canvases[ci].levels.remove(0);
2894                s.tiles_free(old.tiles);
2895                li -= 1;
2896            }
2897            // ---- the transform: window = o + (grid - f) * k
2898            let (lscale, fx, fy) = {
2899                let l = &s.canvases[ci].levels[li];
2900                (l.scale, l.fx, l.fy)
2901            };
2902            let k = v.scale / lscale;
2903            let (dx, dy) = (v.ox - fx, v.oy - fy);
2904            let (rx, ry) = (rnd(dx), rnd(dy));
2905            let copy = k == 1.0 && (dx - rx).abs() < PHASE_TOL && (dy - ry).abs() < PHASE_TOL;
2906            let t = TILE as f64;
2907            let range = |w0: i32, w1: i32, o: f64, f: f64| -> (i32, i32) {
2908                let g = |w: f64| (w - o) / k + f;
2909                (flr(g(w0 as f64 - 1.0) / t) as i32, flr(g(w1 as f64 + 1.0) / t) as i32)
2910            };
2911            let (ix0, ix1) = range(vis[0], vis[2], v.ox, fx);
2912            let (iy0, iy1) = range(vis[1], vis[3], v.oy, fy);
2913            // the pixel a position at the left or top of belongs to: the first whose centre lies there
2914            let edge = |g: f64, o: f64, f: f64| -> i32 { (flr(o + (g - f) * k - 0.5) + 1.0) as i32 };
2915            let mut tiles = Vec::new();
2916            let mut missing: Vec<[i32; 4]> = Vec::new();
2917            for iy in iy0..=iy1 {
2918                for ix in ix0..=ix1 {
2919                    let (gx, gy) = (ix as f64 * t, iy as f64 * t);
2920                    let (b, place) = if copy {
2921                        let (x0, y0) = (rx as i32 + ix * TILE, ry as i32 + iy * TILE);
2922                        ([x0, y0, x0 + TILE, y0 + TILE], [x0 - TILE_BORDER, y0 - TILE_BORDER, 0, 0])
2923                    } else {
2924                        let b = [edge(gx, v.ox, fx), edge(gy, v.oy, fy), edge(gx + t, v.ox, fx), edge(gy + t, v.oy, fy)];
2925                        let at = |g: f64, o: f64, f: f64| rnd((o + (g - TILE_BORDER as f64 - f) * k) * 256.0) as i32;
2926                        let step = rnd(65536.0 / k) as i32;
2927                        (b, [at(gx, v.ox, fx), at(gy, v.oy, fy), step, step])
2928                    };
2929                    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] {
2930                        continue;
2931                    }
2932                    if let Some(tile) = s.canvases[ci].levels[li].tiles.get_mut(&(ix, iy)) {
2933                        tile.tick = tick;
2934                        tiles.push(TileDraw { tex: tile.tex.clone(), b, place });
2935                        continue;
2936                    }
2937                    let t1 = plat::time_us();
2938                    let tex = if t1 < fill_end { s.tile_texture(tick) } else { None };
2939                    let Some(tex) = tex else {
2940                        missing.push(b);
2941                        continue;
2942                    };
2943                    let tv = CanvasView { scale: lscale, ox: fx - gx + TILE_BORDER as f64, oy: fy - gy + TILE_BORDER as f64 };
2944                    let mut dc = Dc::new(TILE_TEX, TILE_TEX);
2945                    dc.begin(&sf);
2946                    rec.d.content.draw_view(&mut Gfx { dc: &mut dc, sf: &mut sf, text: &mut *text }, tv, [0, 0, TILE_TEX, TILE_TEX]);
2947                    dc.end();
2948                    let Dev::Ready(core) = &s.dev else {
2949                        missing.push(b);
2950                        continue;
2951                    };
2952                    let before = if tex.fresh.replace(false) { Layout::Undefined } else { Layout::Sampled };
2953                    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() {
2954                        missing.push(b); // (a failure of the device shows with the next present)
2955                        continue;
2956                    }
2957                    s.tile_fills += 1;
2958                    s.tile_fill_us += plat::time_us().saturating_sub(t1);
2959                    s.trim_at = plat::time_us() + TRIM_US;
2960                    if let Some(Some(w)) = s.wins.get_mut(i) {
2961                        w.out.upload_bytes += TILE_BYTES;
2962                    }
2963                    tiles.push(TileDraw { tex: tex.clone(), b, place });
2964                    s.canvases[ci].levels[li].tiles.insert((ix, iy), Tile { tex, tick });
2965                }
2966            }
2967            // ---- the cells: on top of them and with its tiles the GPU draws it, else the CPU
2968            let Some(Some(w)) = s.wins.get_mut(i) else { continue };
2969            let sp = &mut w.sp;
2970            if sp.hole.len() != cols * rows {
2971                continue;
2972            }
2973            let q = quads.len() as u8 + 1;
2974            let span = |a: i32, b: i32, n: usize| ((a.max(0) >> 5) as usize, (((b + CELL - 1).max(0) >> 5) as usize).min(n));
2975            let (c0, c1) = span(vis[0], vis[2], cols);
2976            let (r0, r1) = span(vis[1], vis[3], rows);
2977            for r in r0..r1 {
2978                for c in c0..c1 {
2979                    let k = r * cols + c;
2980                    if d.top.get(k) == Some(&tag) {
2981                        let b = cell_box(k, cols, fw, fh);
2982                        sp.hole[k] = q;
2983                        sp.part[k] = b[0] < vis[0] || b[1] < vis[1] || b[2] > vis[2] || b[3] > vis[3];
2984                        if moved {
2985                            sp.owed[k] = true;
2986                            sp.stale[k] = true;
2987                        }
2988                    }
2989                    if moved {
2990                        sp.redraw[k] = true;
2991                    }
2992                }
2993            }
2994            for b in &missing {
2995                let (c0, c1) = span(b[0].max(vis[0]), b[2].min(vis[2]), cols);
2996                let (r0, r1) = span(b[1].max(vis[1]), b[3].min(vis[3]), rows);
2997                for r in r0..r1 {
2998                    for c in c0..c1 {
2999                        let k = r * cols + c;
3000                        if sp.hole[k] == q {
3001                            sp.hole[k] = 0;
3002                            sp.part[k] = false;
3003                        }
3004                    }
3005                }
3006            }
3007            sp.moved |= moved;
3008            w.out.canvas_frames += moved as u64;
3009            let mut rects = Vec::new();
3010            let hole = &sp.hole;
3011            cell_rects(&mut rects, cols, rows, fw, fh, |k| hole[k] == q);
3012            for r in rects.iter_mut() {
3013                let (x0, y0) = (r[0].max(vis[0]), r[1].max(vis[1]));
3014                let (x1, y1) = ((r[0] + r[2]).min(vis[2]), (r[1] + r[3]).min(vis[3]));
3015                *r = [x0, y0, x1 - x0, y1 - y0];
3016            }
3017            rects.retain(|r| r[2] > 0 && r[3] > 0);
3018            if !rects.is_empty() {
3019                let loose = !(rec.d.exact_pan && copy);
3020                quads.push(Quad { x: *x, y: *y, src: QuadSrc::Canvas { f: f.clone(), w: *cw, h: *ch, vis, tiles, copy, loose }, rects });
3021            }
3022        }
3023        s.tile_sf = sf;
3024        text.set_font(font.0, font.1);
3025        if let Some(Some(w)) = s.wins.get_mut(i) {
3026            w.sp.quads = quads;
3027        }
3028        true
3029    }
3030
3031    /// the window loop: gestures over (a frame draws their picture), tiles nobody moved over for a
3032    /// long time
3033    fn gp_canvas_tick(&mut self, now: u64) {
3034        let s = &mut self.m.gout;
3035        if !s.script && s.canvases.iter().any(|c| c.apart && now >= c.until) {
3036            for c in s.canvases.iter_mut().filter(|c| c.apart && now >= c.until) {
3037                c.apart = false;
3038                s.canvas_settles += 1;
3039            }
3040            self.invalidate();
3041        }
3042        let s = &mut self.m.gout;
3043        if s.canvas_free_at != 0 && now >= s.canvas_free_at {
3044            s.canvases_gone();
3045            s.trim_at = now + TRIM_US;
3046        }
3047    }
3048
3049    /// ui.canvas_gpu_damage: the tiles over content rectangle r go, the next frame is the CPU's
3050    pub(crate) fn gp_canvas_damage(&mut self, id: u32, r: [f64; 4]) {
3051        let i = self.uiw_cur as usize;
3052        let S { canvases, tile_free, .. } = &mut self.m.gout;
3053        let Some(c) = canvases.iter_mut().find(|c| c.win == i && c.id == id) else { return };
3054        c.damaged = true;
3055        for l in c.levels.iter_mut() {
3056            let t = TILE as f64;
3057            let cell = |u: f64, f: f64, m: f64| flr((u * l.scale + f + m) / t) as i32;
3058            let (ix0, ix1) = (cell(r[0], l.fx, -2.0), cell(r[2], l.fx, 2.0));
3059            let (iy0, iy1) = (cell(r[1], l.fy, -2.0), cell(r[3], l.fy, 2.0));
3060            l.tiles.retain(|&(ix, iy), tile| {
3061                let hit = ix >= ix0 && ix <= ix1 && iy >= iy0 && iy <= iy1;
3062                if hit {
3063                    tile_free.push(tile.tex.clone());
3064                }
3065                !hit
3066            });
3067        }
3068    }
3069
3070    // ------------------------------------------------------------------ views (G5e1, gpu_view.rs)
3071
3072    /// the device views draw on now: the output's (device, generation, true); else, when this run has no output
3073    /// (not wanted, never possible), their own, made on first use; None while the output's device is on its way
3074    /// or a new one is awaited (a view shows what it showed last)
3075    fn gp_view_device(&mut self, now: u64) -> Option<(Device, u32, bool)> {
3076        let Mods { gout: s, gview, .. } = &mut self.m;
3077        if let (true, Dev::Ready(core)) = (s.want, &s.dev) {
3078            return Some((core.dev.clone(), s.dev_gen, true));
3079        }
3080        if let Own::Ready { dev, devgen, .. } = &gview.own {
3081            return Some((dev.clone(), *devgen, false));
3082        }
3083        if s.want && !matches!(s.dev, Dev::Never(_)) {
3084            return None;
3085        }
3086        match gview.own {
3087            Own::Never(_) => return None,
3088            Own::Retry(at) if now < at => return None,
3089            _ => {}
3090        }
3091        // (the browser: adapter and device answer later, and the views draw on the output's device anyway)
3092        if cfg!(target_arch = "wasm32") {
3093            gview.own = Own::Never(String::from("the browser: views draw on the output's device"));
3094            return None;
3095        }
3096        let t0 = plat::time_us();
3097        let r = Instance::new().and_then(|inst| {
3098            let a = inst.adapter()?;
3099            let dev = a.create_device(&DeviceDesc::DEFAULT)?;
3100            Ok((inst, a.info().name.clone(), dev))
3101        });
3102        match r {
3103            Ok((inst, name, dev)) => {
3104                s.dev_gen += 1;
3105                s.note(&format!("views: a device of their own: {name} ({:?}), {:.2} ms", dev.level(), ms(t0, plat::time_us())));
3106                gview.own = Own::Ready { _inst: inst, dev: dev.clone(), devgen: s.dev_gen };
3107                Some((dev, s.dev_gen, false))
3108            }
3109            Err(e) => {
3110                s.note(&format!("views: no device of their own: {e}"));
3111                gview.own = match e {
3112                    GpuError::DeviceLost | GpuError::OutOfMemory | GpuError::Timeout | GpuError::Failed(_) | GpuError::NotReady => Own::Retry(now + RETRY_FIRST_MS * 1000),
3113                    _ => Own::Never(format!("{e}")), // no Vulkan, jfgui_GPU=0, no usable adapter: they paint
3114                };
3115                None
3116            }
3117        }
3118    }
3119
3120    /// dirty_compute, before hashing: the live window's views of this frame.  The device (a new one takes the
3121    /// content along: detach, attach); a frame where the ver, size or device changed, drawn and submitted now,
3122    /// before anything reads it; whether the output's render pass draws it (dirty.canv: its hash stands for its
3123    /// place then, as a canvas apart's), else its command hashes the frame's number: a new frame, a new picture
3124    pub(crate) fn gp_views_begin(&mut self) {
3125        if self.wp.canv.views.is_empty() {
3126            return;
3127        }
3128        // (contents the app dropped: their records go, and with them what holds a device; also without an output,
3129        // where gp_tick does not look)
3130        self.m.gview.views.retain(|e| e.content.strong_count() > 0);
3131        let i = self.uiw_cur as usize;
3132        let now = plat::time_us();
3133        let dev = self.gp_view_device(now);
3134        let pw = self.uiw_pw(i as u32);
3135        // (nobody sees a minimised or hidden window: no frame for it, the next one when it is back)
3136        let unseen = pw >= 0 && plat::win_state(pw) & (plat::WST_MINIMIZED | plat::WST_HIDDEN) != 0;
3137        let can = self.wp.dirty.split && !self.rtl && self.dc.opac == 255 && self.m.gout.views_on && self.gp_can_split(i);
3138        // (G5e3) the window's last frame is not on the screen yet: a new frame of its views waits for it
3139        let unshown = self.m.gout.unshown(i);
3140        let mut failed: Option<(GpuError, bool, u32)> = None;
3141        let Ui { m, wp, .. } = self;
3142        let Mods { gout: s, gview, .. } = m;
3143        let bgl = match &s.dev {
3144            Dev::Ready(core) if s.want => Some(core.bgl.clone()),
3145            _ => None,
3146        };
3147        let lim = dev.as_ref().map_or(0, |d| d.0.limits().max_texture_2d);
3148        for rec in wp.canv.views.iter_mut() {
3149            rec.apart = false;
3150            let Some(Cmd::Custom { x, y, w, h, ver, .. }) = wp.cmd.layers[rec.layer as usize].get_mut(rec.index as usize) else { continue };
3151            let Some(e) = gview.views.iter_mut().find(|e| e.win == i && e.id == rec.id) else { continue };
3152            let Some(content) = e.content.upgrade() else { continue };
3153            // ---- the device
3154            if let Some((d, devgen, out)) = &dev {
3155                if e.devgen != *devgen && e.refused != *devgen {
3156                    if e.devgen != 0 {
3157                        view_detach(e);
3158                    }
3159                    let r = match content.try_borrow_mut() {
3160                        Ok(mut c) => c.attach(d),
3161                        Err(_) => Err(GpuError::Invalid("the view's content is borrowed")),
3162                    };
3163                    match r {
3164                        Ok(()) => {
3165                            e.devgen = *devgen;
3166                            e.output = *out;
3167                            e.level = Some(d.level());
3168                            e.info.attaches += 1;
3169                            e.info.error = None;
3170                            s.note(&format!("view {}: attached to device {devgen}", e.id));
3171                        }
3172                        Err(err) => {
3173                            e.refused = *devgen;
3174                            e.info.error = Some(format!("{err}"));
3175                        }
3176                    }
3177                }
3178            }
3179            let attached = e.devgen != 0 && dev.as_ref().is_some_and(|d| d.1 == e.devgen);
3180            let size = ((*w).max(0) as u32, (*h).max(0) as u32);
3181            e.info.size = size;
3182            // (the browser: a read-back that came after its Draw goes to the framebuffer as a new frame would)
3183            e.moved = core::mem::take(&mut e.arrived);
3184            if e.moved {
3185                e.rest_at = now + VIEW_REST_US;
3186            }
3187            // ---- a frame
3188            let key = (rec.ver, size, e.devgen);
3189            let due = attached && !unseen && size.0 > 0 && size.1 > 0 && size.0 <= lim && size.1 <= lim && e.tried != Some(key);
3190            // (G5e3) bound to the display: while the window's last frame is not shown, the content is not asked (this
3191            // render takes the view's last frame); gp_views_wake asks for a render once it is shown, or after VIEW_HOLD_US
3192            e.woke = false;
3193            let hold = due && unshown && !(e.held_at != 0 && now >= e.held_at + VIEW_HOLD_US);
3194            if hold {
3195                if e.held_key != Some(key) {
3196                    e.held_key = Some(key);
3197                    e.info.held += 1;
3198                }
3199                if e.held_at == 0 {
3200                    e.held_at = now;
3201                }
3202            } else {
3203                e.held_at = 0;
3204                e.held_key = None;
3205            }
3206            if due && !hold {
3207                e.tried = Some(key);
3208                let (d, _, out) = dev.as_ref().unwrap_or_else(|| unreachable!());
3209                let t0 = plat::time_us();
3210                match view_frame(e, &content, d, if *out { bgl.as_ref() } else { None }, size) {
3211                    Ok(()) => {
3212                        e.moved = true;
3213                        e.rest_at = now + VIEW_REST_US;
3214                        e.info.error = None;
3215                    }
3216                    Err(err) => {
3217                        e.info.error = Some(format!("{err}"));
3218                        if matches!(err, GpuError::DeviceLost | GpuError::OutOfMemory | GpuError::Failed(_) | GpuError::Timeout | GpuError::SurfaceLost) {
3219                            failed = Some((err, *out, e.devgen));
3220                        }
3221                    }
3222                }
3223                e.info.draw_us += plat::time_us().saturating_sub(t0);
3224            }
3225            // ---- the output draws it where it ends up on top (gp_plan_views)
3226            let frame = e.sh.c.borrow().frame;
3227            let fits = e.tex.as_ref().is_some_and(|t| (t.w, t.h) == size && t.group.is_some());
3228            let apart = can && attached && e.output && rec.layer == 0 && fits && frame != 0;
3229            rec.apart = apart;
3230            e.apart = apart;
3231            if apart {
3232                wp.dirty.canv.push((rec.index, [*x, *y, *w, *h]));
3233            }
3234            // ---- the CPU's way: another frame or another painting, its cells drawn again (the browser: also the frame its
3235            // read-back brought, which comes later)
3236            let read = if cfg!(target_arch = "wasm32") { e.sh.c.borrow().image_frame as u32 } else { 0 };
3237            *ver = rec.ver.wrapping_mul(0x9E37_79B9) ^ (frame as u32).wrapping_mul(0x85EB_CA6B) ^ (frame >> 32) as u32 ^ read.wrapping_mul(0xC2B2_AE35);
3238        }
3239        // (their own device, when no view is on it any more)
3240        if let Own::Ready { devgen, .. } = gview.own {
3241            if !gview.views.iter().any(|e| e.devgen == devgen) {
3242                gview.own = Own::None;
3243                s.note("views: their own device goes");
3244            }
3245        }
3246        if let Some((err, out, devgen)) = failed {
3247            if out {
3248                // (every window back on the CPU path now: this frame is hashed and drawn as on it)
3249                self.gp_failed(i, err);
3250                self.wp.dirty.split = false;
3251                self.wp.dirty.canv.clear();
3252                for rec in self.wp.canv.views.iter_mut() {
3253                    rec.apart = false;
3254                }
3255            } else {
3256                self.gp_views_gone(Some(devgen));
3257                self.m.gview.own = Own::Retry(now + RETRY_FIRST_MS * 1000);
3258            }
3259        }
3260    }
3261
3262    /// dirty_compute, after hashing: the cells the output's render pass draws each view apart in (where it ended
3263    /// up on top, as a canvas's), a new frame as a canvas's move: those cells owed and stale, the cells with
3264    /// something over it or under a layer drawn again (their Draw reads the frame back) -> any view drawn so
3265    pub(crate) fn gp_plan_views(&mut self) -> bool {
3266        let i = self.uiw_cur as usize;
3267        if !self.wp.canv.views.iter().any(|v| v.apart) {
3268            return false;
3269        }
3270        let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
3271        let Ui { m, wp, .. } = self;
3272        let Mods { gout: s, gview, .. } = m;
3273        let d = &wp.dirty;
3274        let (cols, rows) = (d.cols as usize, d.rows as usize);
3275        let Some(Some(w)) = s.wins.get_mut(i) else { return false };
3276        let sp = &mut w.sp;
3277        if sp.hole.len() != cols * rows {
3278            return false; // (gp_plan_images sizes them where a window can be drawn apart)
3279        }
3280        let mut quads = core::mem::take(&mut sp.quads);
3281        let span = |a: i32, b: i32, n: usize| ((a.max(0) >> 5) as usize, (((b + CELL - 1).max(0) >> 5) as usize).min(n));
3282        let mut any = false;
3283        for rec in wp.canv.views.iter().filter(|v| v.apart) {
3284            let Some(e) = gview.views.iter().find(|e| e.win == i && e.id == rec.id) else { continue };
3285            let Some(tex) = e.tex.clone() else { continue };
3286            let moved = e.moved;
3287            let Some(Cmd::Custom { x, y, w: cw, h: ch, .. }) = wp.cmd.layers[0].get(rec.index as usize) else { continue };
3288            let seen = d.imgs.iter().position(|s| s.index == rec.index);
3289            let q = quads.len() as u8 + 1;
3290            let Some(tag0) = seen.filter(|_| quads.len() < TOP_BLOCKED as usize - 1) else {
3291                // (not on top anywhere, or too many: the CPU draws its cells, from the frame read back)
3292                if moved {
3293                    let (c0, c1) = span(*x, x + cw, cols);
3294                    let (r0, r1) = span(*y, y + ch, rows);
3295                    for r in r0..r1 {
3296                        sp.redraw[r * cols + c0..r * cols + c1.max(c0)].fill(true);
3297                    }
3298                    sp.moved = true;
3299                }
3300                continue;
3301            };
3302            let (tag, vis) = (tag0 as u8 + 1, d.imgs[tag0].vis);
3303            let (c0, c1) = span(vis[0], vis[2], cols);
3304            let (r0, r1) = span(vis[1], vis[3], rows);
3305            for r in r0..r1 {
3306                for c in c0..c1 {
3307                    let k = r * cols + c;
3308                    if d.top.get(k) == Some(&tag) {
3309                        let b = cell_box(k, cols, fw, fh);
3310                        sp.hole[k] = q;
3311                        sp.part[k] = b[0] < vis[0] || b[1] < vis[1] || b[2] > vis[2] || b[3] > vis[3];
3312                        if moved {
3313                            sp.owed[k] = true;
3314                            sp.stale[k] = true;
3315                        }
3316                    }
3317                    if moved {
3318                        sp.redraw[k] = true;
3319                    }
3320                }
3321            }
3322            sp.moved |= moved;
3323            let mut rects = Vec::new();
3324            let hole = &sp.hole;
3325            cell_rects(&mut rects, cols, rows, fw, fh, |k| hole[k] == q);
3326            for r in rects.iter_mut() {
3327                let (x0, y0) = (r[0].max(vis[0]), r[1].max(vis[1]));
3328                let (x1, y1) = ((r[0] + r[2]).min(vis[2]), (r[1] + r[3]).min(vis[3]));
3329                *r = [x0, y0, x1 - x0, y1 - y0];
3330            }
3331            rects.retain(|r| r[2] > 0 && r[3] > 0);
3332            if !rects.is_empty() {
3333                quads.push(Quad { x: *x, y: *y, src: QuadSrc::View { tex, f: e.draw.clone(), w: *cw, h: *ch, vis }, rects });
3334                any = true;
3335            }
3336        }
3337        w.sp.quads = quads;
3338        any
3339    }
3340
3341    /// the contents of the views on device generation devgen (None: every device) leave it: detach, their targets go
3342    /// (the frames read back last stay with their Draw)
3343    pub(crate) fn gp_views_gone(&mut self, devgen: Option<u32>) {
3344        for e in self.m.gview.views.iter_mut() {
3345            if e.devgen != 0 && devgen.is_none_or(|g| g == e.devgen) {
3346                view_detach(e);
3347            }
3348        }
3349    }
3350
3351    /// the window loop: contents the app dropped; views at rest: their window settled once, the framebuffer holds
3352    /// their last frame again (a lost device later loses nothing of the picture)
3353    fn gp_views_tick(&mut self, now: u64) {
3354        let views = &mut self.m.gview.views;
3355        if views.is_empty() {
3356            return;
3357        }
3358        views.retain(|e| e.content.strong_count() > 0);
3359        // (G5e2, the browser) a read-back answered after the Draw that asked for it: the cells where the output's pass
3360        // draws the view lack the frame now (owed: any settle, by hand or at rest, takes it there), the next render takes
3361        // it to the cells under something (gp_views_begin, as a new frame) and the CPU path's cells (its command's
3362        // hash).  Before, a view at rest kept the frame its last Draw had, an older one
3363        #[cfg(target_arch = "wasm32")]
3364        {
3365            let Mods { gout, gview, .. } = &mut self.m;
3366            let mut came = false;
3367            for e in gview.views.iter_mut() {
3368                let mut c = e.sh.c.borrow_mut();
3369                let Some(g) = c.gpu.as_ref() else { continue };
3370                if g.wait.is_none() {
3371                    // (at rest, with no read-back on its way: the newest frame is asked for here, not only by a Draw,
3372                    // which may not come again; Firefox 159 kept an older frame without this)
3373                    if e.rest_at == 0 && c.image_frame != c.frame && c.error.is_none() {
3374                        c.read();
3375                    }
3376                    continue;
3377                }
3378                let before = c.image_frame;
3379                c.read();
3380                if c.image_frame == before {
3381                    continue;
3382                }
3383                e.arrived = true;
3384                e.rest_at = now + VIEW_REST_US;
3385                came = true;
3386                let Some(Some(w)) = gout.wins.get_mut(e.win) else { continue };
3387                let sp = &mut w.sp;
3388                let mine = Rc::as_ptr(&e.draw) as *const ();
3389                if let Some(q) = sp.quads.iter().position(|q| matches!(&q.src, QuadSrc::View { f, .. } if Rc::as_ptr(f) as *const () == mine)) {
3390                    let tag = q as u8 + 1;
3391                    for (owed, hole) in sp.owed.iter_mut().zip(sp.hole.iter()) {
3392                        if *hole == tag {
3393                            *owed = true;
3394                            sp.any_owed = true;
3395                        }
3396                    }
3397                }
3398            }
3399            if came {
3400                self.invalidate();
3401            }
3402        }
3403        let views = &mut self.m.gview.views;
3404        let mut rest: Vec<usize> = Vec::new();
3405        for e in views.iter_mut().filter(|e| e.rest_at != 0 && now >= e.rest_at) {
3406            e.rest_at = 0;
3407            if !rest.contains(&e.win) {
3408                rest.push(e.win);
3409            }
3410        }
3411        for i in rest {
3412            self.gp_settle(i);
3413        }
3414    }
3415
3416    /// (G5e3) the window loop, after the frames still to show: a view whose new frame waited for its window asks for a
3417    /// render once the window's frame has been shown (or it went back to the CPU path, or is gone), or when it has
3418    /// waited VIEW_HOLD_US; once per wait (woke: the render clears it, gp_views_begin)
3419    fn gp_views_wake(&mut self, now: u64) {
3420        let Mods { gout, gview, .. } = &mut self.m;
3421        let mut wake = false;
3422        for e in gview.views.iter_mut().filter(|e| e.held_at != 0 && !e.woke) {
3423            if !gout.unshown(e.win) || now >= e.held_at + VIEW_HOLD_US {
3424                e.woke = true;
3425                wake = true;
3426            }
3427        }
3428        if wake {
3429            self.invalidate();
3430        }
3431    }
3432
3433    /// render_live, after dirty_compute: is this frame drawn layer by layer?  (Else it goes as
3434    /// without layers: the whole base texture holds the framebuffer's cells then.)
3435    pub(crate) fn gp_split_begin(&mut self, n: i32) -> bool {
3436        if !self.wp.dirty.split {
3437            return false;
3438        }
3439        let i = self.uiw_cur as usize;
3440        let can = self.gp_can_split(i);
3441        let upper_now = self.wp.cmd.layers[1..].iter().any(|l| !l.is_empty());
3442        let Some(w) = self.m.gout.win(i) else { return false };
3443        let sp = &mut w.sp;
3444        let kept = sp.active || !sp.quads.is_empty() || sp.any_stale || sp.moved;
3445        if !can {
3446            if kept {
3447                self.gp_flatten(i);
3448            }
3449            if let Some(w) = self.m.gout.win(i).filter(|_| upper_now) {
3450                w.sp.clean = false; // the layers go into the base texture with this frame
3451            }
3452            self.wp.dirty.busy = false;
3453            return false;
3454        }
3455        if !kept && !upper_now {
3456            sp.clean = true; // what a layer left in the base texture has changed and gone up
3457            self.wp.dirty.busy = false;
3458            return false;
3459        }
3460        let stale = sp.any_stale && sp.stale.iter().zip(sp.hole.iter()).any(|(&a, &h)| a && h == 0);
3461        if n == 0 && !stale && !sp.moved {
3462            return false; // nothing changed
3463        }
3464        if self.wp.rt.present_n == 0 {
3465            self.wp.rt.present_n = -1;
3466        }
3467        true
3468    }
3469
3470    /// render_live: the frame layer by layer (the module's header): from the lowest layer that
3471    /// changed in a cell upwards: back to the pixels below it, save them for each layer that
3472    /// touches the cell, draw the layer, upload the cell to the layer's texture
3473    pub(crate) fn gp_split_draw(&mut self, n: i32) {
3474        let icon_style = (self.style, self.color(T_ACCENT), self.theme_dark, self.color(T_TEXT_DISABLED), self.wp.rt.dpi as u32);
3475        let i = self.uiw_cur as usize;
3476        let Ui { m, wp, dc, text, icon_hook, drects, .. } = self;
3477        let s = &mut m.gout;
3478        let S { dev, wins, trim_at, .. } = s;
3479        let (Dev::Ready(core), Some(Some(w))) = (&*dev, wins.get_mut(i)) else {
3480            for l in wp.cmd.layers.iter() {
3481                render_layer(l, 0, dc, &mut wp.rt.fb, text, icon_hook, icon_style);
3482            }
3483            return;
3484        };
3485        let d = &wp.dirty;
3486        let (cols, rows) = (d.cols as usize, d.rows as usize);
3487        let total = cols * rows;
3488        let fb = &mut wp.rt.fb;
3489        let (fw, fh) = (fb.w, fb.h);
3490        let Win { sp, tex: base, out, pending, .. } = w;
3491        if sp.hole.len() != total {
3492            (sp.cols, sp.rows) = (cols, rows);
3493            sp.hole = vec![0; total];
3494            sp.owed = vec![false; total];
3495            sp.stale = vec![false; total];
3496            sp.part = vec![false; total];
3497            sp.redraw = vec![false; total];
3498        }
3499        let Split { upper, m: lowest, nr, rects, hole, owed, stale, quads, linger_at, part, redraw, .. } = sp;
3500        // ---- per cell: what changed, which layers touch it
3501        lowest.clear();
3502        lowest.resize(total, L_COUNT as u8);
3503        nr.clear();
3504        nr.resize(total, 0);
3505        let all = d.forced || (!sp.active && !sp.clean);
3506        let now: [&[u64]; L_COUNT] = core::array::from_fn(|l| d.through(true, l));
3507        let was: [&[u64]; L_COUNT] = core::array::from_fn(|l| d.through(false, l));
3508        for k in 0..total {
3509            let mut low = L_COUNT;
3510            if all || was[0].len() != total {
3511                low = 0;
3512            } else {
3513                for l in 0..L_COUNT {
3514                    if now[l][k] != was[l][k] {
3515                        low = l;
3516                        break;
3517                    }
3518                }
3519            }
3520            if stale[k] && hole[k] == 0 {
3521                low = 0; // the image the CPU left out is no longer drawn by the GPU
3522            }
3523            for l in 1..L_COUNT {
3524                if now[l][k] != now[l - 1][k] {
3525                    nr[k] |= 1 << l;
3526                }
3527            }
3528            // a canvas moved below: the CPU's way where the GPU does not draw it, and for a layer
3529            // over it, which holds what lies below
3530            if redraw[k] && (hole[k] == 0 || nr[k] != 0) {
3531                low = 0;
3532            }
3533            lowest[k] = low as u8;
3534        }
3535        // explicit damage: its cells from the base up
3536        if n >= 0 {
3537            for r in drects.iter().take(n as usize).skip(d.n0.max(0) as usize) {
3538                let (c0, c1) = ((r[0].max(0) >> 5) as usize, (((r[0] + r[2] + CELL - 1) >> 5).max(0) as usize).min(cols));
3539                let (r0, r1) = ((r[1].max(0) >> 5) as usize, (((r[1] + r[3] + CELL - 1) >> 5).max(0) as usize).min(rows));
3540                for row in r0..r1 {
3541                    lowest[row * cols + c0..row * cols + c1.max(c0)].fill(0);
3542                }
3543            }
3544        }
3545        // ---- the layers' textures: a new one takes all of its cells
3546        let mut err = None;
3547        for (u, l) in upper.iter_mut().zip(1..) {
3548            let bit = 1u8 << l;
3549            let mut bb = [i32::MAX, i32::MAX, i32::MIN, i32::MIN];
3550            for k in (0..total).filter(|&k| nr[k] & bit != 0) {
3551                let b = cell_box(k, cols, fw, fh);
3552                bb = [bb[0].min(b[0]), bb[1].min(b[1]), bb[2].max(b[2]), bb[3].max(b[3])];
3553            }
3554            if bb[0] >= bb[2] {
3555                if u.tex.is_some() && !u.rects.is_empty() {
3556                    *linger_at = plat::time_us() + LINGER_US;
3557                }
3558                continue;
3559            }
3560            let area = (bb[2] - bb[0]) as i64 * (bb[3] - bb[1]) as i64;
3561            let fits = u.tex.as_ref().is_some_and(|t| {
3562                bb[0] >= t.ox && bb[1] >= t.oy && bb[2] <= t.ox + t.w && bb[3] <= t.oy + t.h
3563                    && (t.w as i64 * t.h as i64) <= (area * LAYER_SHRINK).max((LAYER_STEP * LAYER_STEP) as i64)
3564            });
3565            if !fits {
3566                u.tex = None;
3567                match layer_texture(core, bb, fw, fh) {
3568                    Ok(t) => u.tex = Some(t),
3569                    Err(e) => err = Some(e),
3570                }
3571                for k in (0..total).filter(|&k| nr[k] & bit != 0) {
3572                    lowest[k] = lowest[k].min(l);
3573                }
3574            }
3575        }
3576        // ---- stage by stage
3577        let stride = fb.stride as usize;
3578        for l in 0..L_COUNT {
3579            let bit = 1u8 << l;
3580            if l > 0 {
3581                for k in 0..total {
3582                    let low = lowest[k] as usize;
3583                    if low > l {
3584                        continue;
3585                    }
3586                    let b = cell_box(k, cols, fw, fh);
3587                    if low == l {
3588                        // what lay below this layer: kept by the lowest layer that was here, else
3589                        // the framebuffer holds it already
3590                        if let Some(u) = upper[l - 1..].iter().find(|u| u.store.has(k)) {
3591                            cell_load(fb, b, u.store.get(k));
3592                            owed[k] = false;
3593                        }
3594                    }
3595                    if nr[k] & bit != 0 {
3596                        let st = &upper[l - 1].store;
3597                        if !st.has(k) && st.free.is_empty() && st.px.len() + CELL_BYTES > st.px.capacity() {
3598                            // (the layer's other new cells too: no reallocation for each of them)
3599                            let n = (k..total).filter(|&c| nr[c] & bit != 0 && !st.has(c)).count();
3600                            upper[l - 1].store.reserve(n);
3601                        }
3602                        if owed[k] && hole[k] != 0 {
3603                            if let Some(q) = quads.get(hole[k] as usize - 1) {
3604                                quad_blit(fb, q, b, text);
3605                            }
3606                            owed[k] = false;
3607                        }
3608                        cell_save(fb, b, upper[l - 1].store.put(k, total));
3609                    }
3610                }
3611            }
3612            let is_base = l == 0;
3613            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 }));
3614            for r in rects.iter() {
3615                dc.clip_push(r[0], r[1], r[2], r[3]);
3616                render_layer(&wp.cmd.layers[l], 0, dc, fb, text, icon_hook, icon_style);
3617                dc.clip_pop();
3618            }
3619            let bytes: u64 = rects.iter().map(|r| r[2] as u64 * r[3] as u64 * 4).sum();
3620            if is_base {
3621                out.base_px += bytes / 4;
3622                for k in 0..total {
3623                    if lowest[k] == 0 {
3624                        let h = hole[k] != 0;
3625                        owed[k] = h;
3626                        stale[k] = h;
3627                    }
3628                }
3629                if err.is_none() {
3630                    if let Some(t) = base.as_ref() {
3631                        if let Err(e) = core.dev.queue().write_texture_rects(&t.t, rects, &fb.px, stride as u32, Layout::Sampled, Layout::Sampled) {
3632                            err = Some(e);
3633                        }
3634                    }
3635                }
3636                out.upload_base += bytes;
3637            } else {
3638                let u = &mut upper[l - 1];
3639                if err.is_none() {
3640                    if let Some(t) = u.tex.as_mut() {
3641                        let before = if t.fresh { Layout::Undefined } else { Layout::Sampled };
3642                        match core.dev.queue().write_texture_rects_at(&t.t, rects, (t.ox, t.oy), &fb.px, stride as u32, before, Layout::Sampled) {
3643                            Ok(()) => t.fresh = t.fresh && rects.is_empty(),
3644                            Err(e) => err = Some(e),
3645                        }
3646                    }
3647                }
3648                for k in 0..total {
3649                    if nr[k] & bit == 0 {
3650                        u.store.drop_cell(k);
3651                    }
3652                }
3653                if u.store.used == 0 {
3654                    u.store.empty();
3655                }
3656                cell_rects(&mut u.rects, cols, rows, fw, fh, |k| nr[k] & bit != 0);
3657            }
3658            out.upload_bytes += bytes;
3659            if bytes > BIG_UPLOAD {
3660                *trim_at = plat::time_us() + TRIM_US;
3661            }
3662        }
3663        // (a cell a layer has left in this frame got the old view back from its store)
3664        if sp.moved {
3665            for k in 0..total {
3666                if sp.redraw[k] && sp.hole[k] != 0 && sp.nr[k] == 0 {
3667                    sp.owed[k] = true;
3668                }
3669            }
3670        }
3671        sp.any_owed = sp.owed.iter().any(|&v| v);
3672        sp.any_stale = sp.stale.iter().any(|&v| v);
3673        sp.redraw.fill(false);
3674        sp.moved = false;
3675        sp.active = sp.upper.iter().any(|u| !u.rects.is_empty());
3676        sp.clean = true;
3677        sp.drawn = true;
3678        sp.err = err;
3679        *pending = true;
3680        out.split_frames += 1;
3681        wp.dirty.busy = sp.active || !sp.quads.is_empty();
3682    }
3683
3684    /// render_live's end: its time; n: the rectangles a frame without layers drew
3685    pub(crate) fn gp_frame_done(&mut self, t0: u64, n: i32, split: bool) {
3686        let i = self.uiw_cur as usize;
3687        let (fw, fh) = (self.wp.rt.fb.w as u64, self.wp.rt.fb.h as u64);
3688        let px = if split {
3689            0
3690        } else if n < 0 {
3691            fw * fh
3692        } else {
3693            self.drects.iter().take(n as usize).map(|r| r[2].max(0) as u64 * r[3].max(0) as u64).sum()
3694        };
3695        if let Some(w) = self.m.gout.win(i) {
3696            w.out.render_us += plat::time_us().saturating_sub(t0);
3697            w.out.base_px += px;
3698        }
3699    }
3700
3701    /// the images the GPU draws of window i into its framebuffer, where it lacks them
3702    fn gp_settle(&mut self, i: usize) {
3703        let Some(Some(w)) = self.m.gout.wins.get_mut(i) else { return };
3704        if !w.sp.any_owed {
3705            return;
3706        }
3707        let sp = &mut w.sp;
3708        let (mut owed, hole, quads) = (core::mem::take(&mut sp.owed), core::mem::take(&mut sp.hole), core::mem::take(&mut sp.quads));
3709        let (cols, rows) = (sp.cols, sp.rows);
3710        sp.any_owed = false;
3711        let font = self.text.get_font();
3712        let cur = self.uiw_cur as usize;
3713        let Ui { wp, m, text, .. } = self;
3714        let parts = if i == cur { Some(wp) } else { m.win.recs[i & (WIN_MAX - 1)].save.as_deref_mut() };
3715        if let Some(p) = parts {
3716            let fb = &mut p.rt.fb;
3717            let (fw, fh) = (fb.w, fb.h);
3718            let mut rects = Vec::new();
3719            for (q, quad) in quads.iter().enumerate() {
3720                let tag = q as u8 + 1;
3721                cell_rects(&mut rects, cols, rows, fw, fh, |k| owed[k] && hole[k] == tag);
3722                for r in &rects {
3723                    quad_blit(fb, quad, [r[0], r[1], r[0] + r[2], r[1] + r[3]], text);
3724                }
3725            }
3726        }
3727        self.text.set_font(font.0, font.1);
3728        owed.fill(false);
3729        let Some(Some(w)) = self.m.gout.wins.get_mut(i) else { return };
3730        (w.sp.owed, w.sp.hole, w.sp.quads) = (owed, hole, quads);
3731    }
3732
3733    /// every window's framebuffer holds its whole frame
3734    pub(crate) fn gp_settle_all(&mut self) {
3735        for i in 0..self.m.gout.wins.len() {
3736            self.gp_settle(i);
3737        }
3738    }
3739
3740    /// back to one texture: the framebuffer is whole again and goes up with the next look; layers
3741    /// and images come back with the next change
3742    fn gp_flatten(&mut self, i: usize) {
3743        self.gp_settle(i);
3744        let s = &mut self.m.gout;
3745        let trim = plat::time_us() + TRIM_US;
3746        let mut freed = false;
3747        if let Some(Some(w)) = s.wins.get_mut(i) {
3748            freed = w.sp.active;
3749            w.sp.reset();
3750            w.full = true;
3751            w.pending = true;
3752        }
3753        if freed {
3754            s.trim_at = trim;
3755        }
3756        if let Some(p) = self.uiw_parts_mut(i as u32) {
3757            p.dirty.busy = false;
3758        }
3759    }
3760
3761    /// (--verify-dirty) the frame was repaired: it goes up as a whole
3762    pub(crate) fn gp_repaired(&mut self) {
3763        let i = self.uiw_cur as usize;
3764        if self.m.gout.wins.get(i).and_then(|w| w.as_ref()).is_some_and(|w| w.stage == Stage::Gpu) {
3765            self.gp_flatten(i);
3766        }
3767    }
3768
3769    // ------------------------------------------------------------------ scripts
3770
3771    /// a headless run shows its windows through `gpu on`
3772    pub(crate) fn gp_scripted(&self) -> bool {
3773        self.m.gout.script
3774    }
3775
3776    /// the real window of UI window `cur` in a script (made at its first frame, of the
3777    /// framebuffer's size), messages pumped; the script's own events are not queued now (a frame
3778    /// follows process_events)
3779    pub(crate) fn gp_script_pw(&mut self, cur: u32) -> i32 {
3780        let i = cur as usize & (super::consts::WIN_MAX - 1);
3781        let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
3782        let s = &mut self.m.gout;
3783        let mut pw = s.script_pw[i];
3784        if pw < 0 {
3785            let off = 40 + 48 * i as i32;
3786            // (Linux: one window without an owner, the first)
3787            let owner = if cfg!(windows) { plat::PW_NOOWNER } else { s.script_pw.iter().copied().find(|&p| p >= 0).unwrap_or(plat::PW_NOOWNER) };
3788            pw = plat::win_create("gpu output test", 320, 240, plat::PWF_SYSFRAME | plat::PWF_RESIZABLE, owner, off, off);
3789            if pw < 0 {
3790                return -1;
3791            }
3792            s.script_pw[i] = pw;
3793            plat::win_test(plat::WT_NOANIM, pw, [0; 4]);
3794            plat::win_show(pw, plat::PWS_SHOWNA);
3795        }
3796        // (asked once: a window manager that keeps it at another size says so again each time)
3797        if plat::win_size(pw) != (fw, fh) && s.script_asked[i] != (fw, fh) {
3798            s.script_asked[i] = (fw, fh);
3799            plat::win_set_rect(pw, plat::PW_KEEP, plat::PW_KEEP, fw, fh);
3800        }
3801        script_pump();
3802        pw
3803    }
3804
3805    fn gp_scoped(&self) -> usize {
3806        self.uiw_cur as usize
3807    }
3808
3809    /// the script waits: the window loop's work, the real windows' messages
3810    fn gp_script_wait(&mut self, ms: u64) -> bool {
3811        let dl = plat::time_us() + ms * 1000;
3812        loop {
3813            script_pump();
3814            self.gp_tick();
3815            let s = &self.m.gout;
3816            let building = s.jobs.running.load(Ordering::Acquire) != 0;
3817            // (a window the system keeps at another size stays on the CPU path: a narrow tool window
3818            // in a script's real window with a frame, a frame larger than the screen)
3819            let all = s.wins.iter().flatten().all(|w| w.stage == Stage::Gpu || kept_at_other_size(w));
3820            if matches!(s.dev, Dev::Never(_)) {
3821                return false;
3822            }
3823            if all && !building && !s.wins.is_empty() {
3824                return true;
3825            }
3826            if plat::time_us() >= dl {
3827                return false;
3828            }
3829            plat::wait(plat::time_us() + 1000);
3830        }
3831    }
3832
3833    fn gp_skip(&mut self, why: &str) {
3834        self.m.gout.skip = true;
3835        plat::write_stdout(format!("SKIP gpu: {why}\n").as_bytes());
3836    }
3837
3838    /// the script command `gpu` (the module's header)
3839    pub(crate) fn sc_gpu(&mut self, app: &mut dyn App, a: &[u8]) {
3840        if let Some(r) = word_eq(a, "on") {
3841            self.m.gout.script = true;
3842            self.m.gout.setup();
3843            self.m.gout.want = true;
3844            self.m.gout.want3d = self.desc.flags & AF_GPU_3D != 0;
3845            self.m.gout.verify = word_eq(skip_ws(r), "fast").is_none();
3846            self.m.gout.trace = env_on("jfgui_GPU_TRACE");
3847            self.m.gout.skip = false;
3848            self.dirty_force_full();
3849            self.invalidate();
3850            self.run_passes(app);
3851            self.render();
3852            self.present();
3853            if !self.gp_script_wait(20_000) {
3854                let why = match &self.m.gout.dev {
3855                    Dev::Never(w) => w.clone(),
3856                    _ => {
3857                        let stages: Vec<(usize, Stage)> = self.m.gout.wins.iter().enumerate().filter_map(|(i, w)| w.as_ref().map(|w| (i, w.stage))).collect();
3858                        format!("the swapchain did not come: {stages:?}")
3859                    }
3860                };
3861                if matches!(self.m.gout.dev, Dev::Never(_)) {
3862                    self.gp_skip(&why);
3863                } else {
3864                    self.fail(why.as_bytes());
3865                }
3866            }
3867            return;
3868        }
3869        if word_eq(a, "off").is_some() {
3870            self.m.gout.script = false;
3871            self.gp_wait_jobs(3000);
3872            if matches!(self.m.gout.dev, Dev::Ready(_)) {
3873                let devgen = self.m.gout.dev_gen;
3874                self.gp_views_gone(Some(devgen)); // (views: their own device with the next frame)
3875            }
3876            let s = &mut self.m.gout;
3877            s.wins.clear();
3878            s.images.clear();
3879            s.image_bytes = 0;
3880            s.canvases.clear();
3881            s.tile_free.clear();
3882            s.tile_bytes = 0;
3883            s.dev = Dev::None;
3884            s.want = false;
3885            for pw in s.script_pw.iter_mut() {
3886                if *pw >= 0 {
3887                    plat::win_destroy(*pw);
3888                    *pw = -1;
3889                }
3890            }
3891            script_pump();
3892            return;
3893        }
3894        if !self.m.gout.script {
3895            self.fail(b"gpu on first");
3896            return;
3897        }
3898        if word_eq(a, "wait").is_some() {
3899            if self.m.gout.skip {
3900                return;
3901            }
3902            let (t, _) = super::runtime::parse_int(next_word(a));
3903            self.render();
3904            self.present();
3905            if !self.gp_script_wait(if t > 0 { t as u64 } else { 20_000 }) {
3906                self.fail(b"not on the swapchain");
3907            }
3908            return;
3909        }
3910        if word_eq(a, "lose").is_some() {
3911            self.gpu_test_lose();
3912            return;
3913        }
3914        if let Some(n) = word_eq(a, "budget") {
3915            let (v, _) = super::runtime::parse_int(skip_ws(n));
3916            self.gpu_test_image_budget(v.max(0) as u64);
3917            return;
3918        }
3919        if let Some(n) = word_eq(a, "tilebudget") {
3920            let (v, _) = super::runtime::parse_int(skip_ws(n));
3921            self.gpu_test_canvas_budget(v.max(0) as u64);
3922            return;
3923        }
3924        if let Some(r) = word_eq(a, "shot") {
3925            let r = skip_ws(r);
3926            self.gp_shot(&String::from_utf8_lossy(r).into_owned());
3927            return;
3928        }
3929        if word_eq(a, "settle").is_some() {
3930            if !self.m.gout.skip {
3931                self.gpu_canvas_settle();
3932                self.render();
3933                self.present();
3934            }
3935            return;
3936        }
3937        if word_eq(a, "hold").is_some() {
3938            let (t, _) = super::runtime::parse_int(next_word(a));
3939            let dl = plat::time_us() + t.max(0) as u64 * 1000;
3940            while plat::time_us() < dl {
3941                script_pump();
3942                self.gp_tick();
3943                plat::gpu_wait(plat::time_us() + 10_000);
3944            }
3945            return;
3946        }
3947        if word_eq(a, "print").is_some() {
3948            let o = self.gpu_output(self.gp_scoped() as u32);
3949            plat::write_stdout(format!("gpu: {o:?}\n").as_bytes());
3950            return;
3951        }
3952        let Some(r) = word_eq(a, "expect") else {
3953            self.fail(b"unknown gpu command");
3954            return;
3955        };
3956        let r = skip_ws(r);
3957        if self.m.gout.skip {
3958            return;
3959        }
3960        self.render();
3961        self.present();
3962        let i = self.gp_scoped();
3963        // (a window manager that keeps the window smaller than the frame, larger than the screen: the
3964        // window stays on the CPU path; a note, as for a window on top of ours)
3965        let needs_gpu = ["gpu", "apart", "tiles", "layers", "images"].iter().any(|w| word_eq(r, w).is_some());
3966        if needs_gpu && self.m.gout.win(i).is_some_and(|w| w.stage != Stage::Gpu && kept_at_other_size(w)) {
3967            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());
3968            return;
3969        }
3970        if word_eq(r, "gpu").is_some() || word_eq(r, "cpu").is_some() {
3971            let want_gpu = word_eq(r, "gpu").is_some();
3972            let on = self.gpu_output(i as u32).on;
3973            if on != want_gpu {
3974                self.fail(if on { b"on the swapchain" } else { b"on the CPU path" });
3975            }
3976        } else if word_eq(r, "same").is_some() {
3977            self.gp_expect_same();
3978        } else if word_eq(r, "window").is_some() {
3979            self.gp_expect_pixels(i, false);
3980        } else if word_eq(r, "screen").is_some() {
3981            self.gp_expect_pixels(i, true);
3982        } else if let Some(n) = word_eq(r, "upload") {
3983            let (max, _) = super::runtime::parse_int(skip_ws(n));
3984            let bytes = self.m.gout.win(i).map_or(0, |w| core::mem::take(&mut w.out.upload_bytes));
3985            if bytes > max as u64 {
3986                self.fail(format!("{bytes} bytes uploaded").as_bytes());
3987            }
3988        } else if let Some(n) = word_eq(r, "base") {
3989            let (max, _) = super::runtime::parse_int(skip_ws(n));
3990            let bytes = self.m.gout.win(i).map_or(0, |w| core::mem::take(&mut w.out.upload_base));
3991            if bytes > max as u64 {
3992                self.fail(format!("{bytes} bytes uploaded to the base texture").as_bytes());
3993            }
3994        } else if let Some(n) = word_eq(r, "raster") {
3995            let (max, _) = super::runtime::parse_int(skip_ws(n));
3996            let px = self.m.gout.win(i).map_or(0, |w| core::mem::take(&mut w.out.base_px));
3997            if px > max as u64 {
3998                self.fail(format!("{px} pixels of the base layer drawn").as_bytes());
3999            }
4000        } else if let Some(n) = word_eq(r, "layers") {
4001            let (want, _) = super::runtime::parse_int(skip_ws(n));
4002            let have = self.gpu_output(i as u32).layer_textures;
4003            if have != want as u32 {
4004                self.fail(format!("{have} layer textures").as_bytes());
4005            }
4006        } else if let Some(n) = word_eq(r, "images") {
4007            let (want, _) = super::runtime::parse_int(skip_ws(n));
4008            let have = self.gpu_output(i as u32).image_textures;
4009            if have != want as u32 {
4010                self.fail(format!("{have} image textures").as_bytes());
4011            }
4012        } else if let Some(n) = word_eq(r, "apart") {
4013            let (want, _) = super::runtime::parse_int(skip_ws(n));
4014            let have = self.gpu_output(i as u32).canvas_apart;
4015            if have != want as u32 {
4016                self.fail(format!("{have} canvases apart").as_bytes());
4017            }
4018        } else if let Some(n) = word_eq(r, "tiles") {
4019            let (want, _) = super::runtime::parse_int(skip_ws(n));
4020            let have = self.gpu_output(i as u32).canvas_tiles;
4021            if have != want as u32 {
4022                self.fail(format!("{have} canvas tiles").as_bytes());
4023            }
4024        } else if let Some(n) = word_eq(r, "fills") {
4025            let (max, _) = super::runtime::parse_int(skip_ws(n));
4026            let s = &mut self.m.gout;
4027            let fills = s.tile_fills - core::mem::replace(&mut s.tile_marked, s.tile_fills);
4028            if fills > max as u64 {
4029                self.fail(format!("{fills} tiles drawn").as_bytes());
4030            }
4031        } else if word_eq(r, "nodiff").is_some() {
4032            let o = self.gpu_output(i as u32);
4033            let px = o.canvas_diff_px - core::mem::replace(&mut self.m.gout.diff_marked, o.canvas_diff_px);
4034            if px != 0 {
4035                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());
4036            }
4037        } else {
4038            self.fail(b"unknown gpu expectation");
4039        }
4040    }
4041
4042    // `gpu shot`: the window presents once more, the image read back into a file
4043    fn gp_shot(&mut self, file: &str) {
4044        if self.m.gout.skip {
4045            return;
4046        }
4047        self.render();
4048        self.present();
4049        let i = self.gp_scoped();
4050        self.gp_settle_all();
4051        let mut s = core::mem::take(&mut self.m.gout);
4052        let mut raw = None;
4053        let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
4054        let (mut w_px, mut h_px) = (0, 0);
4055        if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
4056            (w_px, h_px) = (fb.w, fb.h);
4057            w.rb_keep = Some(Vec::new());
4058            for _ in 0..SHOW_TRIES {
4059                match show(w, core, fb) {
4060                    Ok(Shown::Wait) => plat::gpu_wait(plat::time_us() + 4_000),
4061                    _ => break,
4062                }
4063            }
4064            raw = w.rb_keep.take().filter(|v| !v.is_empty());
4065            if core.format == Format::Rgba8Unorm {
4066                if let Some(v) = raw.as_mut() {
4067                    for p in v.chunks_exact_mut(4) {
4068                        p.swap(0, 2);
4069                    }
4070                }
4071            }
4072        }
4073        self.m.gout = s;
4074        script_pump();
4075        let saved = raw.is_some_and(|v| {
4076            let mut sf = Surface::new(w_px, h_px, FMT_XRGB);
4077            sf.px.copy_from_slice(&v);
4078            Ui::save_png(&sf, file)
4079        });
4080        if !saved {
4081            self.fail(b"no presented image (gpu on fast reads nothing back)");
4082        }
4083    }
4084
4085    // every window on the GPU: present once more, read back, compare; no frame differed since the
4086    // last check
4087    fn gp_expect_same(&mut self) {
4088        if self.m.gout.canvases.iter().any(|c| c.apart) {
4089            self.gpu_canvas_settle();
4090            self.render();
4091            self.present();
4092        }
4093        self.gp_settle_all();
4094        let mut s = core::mem::take(&mut self.m.gout);
4095        let mut msg = None;
4096        let mut any = false;
4097        for i in 0..s.wins.len() {
4098            let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
4099            if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
4100                if w.stage != Stage::Gpu {
4101                    continue;
4102                }
4103                any = true;
4104                let frames = w.out.verified;
4105                for _ in 0..SHOW_TRIES {
4106                    match show(w, core, fb) {
4107                        Ok(Shown::Wait) => {
4108                            plat::gpu_wait(plat::time_us() + 4_000); // no image free now
4109                        }
4110                        Ok(_) => break,
4111                        Err(e) => {
4112                            msg = Some(format!("{e}"));
4113                            break;
4114                        }
4115                    }
4116                }
4117                if w.out.verified == frames && msg.is_none() {
4118                    msg = Some(format!("window {i}: no frame presented"));
4119                }
4120                if w.out.verify_bad != 0 {
4121                    msg = Some(format!("window {i}: {} of {} presented images differ", w.out.verify_bad, w.out.verified));
4122                    w.out.verify_bad = 0;
4123                }
4124            }
4125        }
4126        if !any && msg.is_none() {
4127            if s.wins.iter().flatten().any(kept_at_other_size) {
4128                plat::write_stdout(b"note: gpu expect same: the system keeps the window at another size (no result)\n");
4129            } else {
4130                msg = Some(String::from("no window on the swapchain"));
4131            }
4132        }
4133        self.m.gout = s;
4134        self.m.gout.diff_marked = self.gpu_output(self.gp_scoped() as u32).canvas_diff_px;
4135        script_pump();
4136        if let Some(m) = msg {
4137            self.fail(m.as_bytes());
4138        }
4139    }
4140
4141    // the window's DC (the CPU path) or the composed screen against the framebuffer
4142    fn gp_expect_pixels(&mut self, i: usize, screen: bool) {
4143        self.gp_settle_all();
4144        let pw = self.m.gout.script_pw[i & (super::consts::WIN_MAX - 1)];
4145        if pw < 0 {
4146            self.fail(b"no window");
4147            return;
4148        }
4149        // (Linux: Xwayland's GetImage on a window flipped to a buffer reads the buffer before the last
4150        // one, seen on KWin 6.6 while its own composition shows the last: one more frame of the same
4151        // content; a Wayland client reads nothing anyway)
4152        if !cfg!(windows) && screen {
4153            let mut s = core::mem::take(&mut self.m.gout);
4154            let fb = self.uiw_parts(i as u32).map(|p| &p.rt.fb);
4155            if let (Dev::Ready(core), Some(Some(w)), Some(fb)) = (&s.dev, s.wins.get_mut(i), fb) {
4156                for _ in 0..SHOW_TRIES {
4157                    if w.stage != Stage::Gpu || !matches!(show(w, core, fb), Ok(Shown::Wait)) {
4158                        break;
4159                    }
4160                    plat::gpu_wait(plat::time_us() + 4_000);
4161                }
4162            }
4163            self.m.gout = s;
4164        }
4165        // (Linux: a frame waits for the display's answer to the last one, or for the GPU)
4166        for _ in 0..SHOW_TRIES {
4167            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()));
4168            if !open {
4169                break;
4170            }
4171            script_pump();
4172            self.gp_tick();
4173            plat::gpu_wait(plat::time_us() + 4_000);
4174        }
4175        // what was presented reaches the screen with the next compositions
4176        for _ in 0..12 {
4177            script_pump();
4178            self.gp_tick();
4179            plat::gpu_wait(plat::time_us() + 8_000);
4180        }
4181        let Some(fb) = self.uiw_parts(i as u32).map(|p| &p.rt.fb) else { return };
4182        let (w, h) = (fb.w, fb.h);
4183        let px = if screen && cfg!(windows) {
4184            plat::win_get_rect(pw).and_then(|r| plat::win_test_screen(r[0], r[1], w, h))
4185        } else {
4186            plat::win_test_image(pw).filter(|x| x.0 == w && x.1 == h).map(|x| x.2)
4187        };
4188        let Some(px) = px else {
4189            if cfg!(windows) {
4190                self.fail(b"no picture");
4191            } else {
4192                // (a Wayland client cannot read a window back; X11: the system keeps it at another size)
4193                plat::write_stdout(b"note: gpu expect screen: no way to read the window back here (no result)\n");
4194            }
4195            return;
4196        };
4197        let mut bad = 0u64;
4198        let mut first = (0, 0);
4199        let mut some = Vec::new(); // of the pixels that differ, to look what lies over them
4200        let row = w as usize * 4;
4201        for y in 0..h as usize {
4202            let a = &px[y * row..y * row + row];
4203            let b = &fb.px[y * fb.stride as usize..y * fb.stride as usize + row];
4204            for (x, (p, q)) in a.chunks_exact(4).zip(b.chunks_exact(4)).enumerate() {
4205                // (the screen: the system rounds the window's corners)
4206                let corner = screen && (x < 8 || x + 8 >= w as usize) && (y < 8 || y + 8 >= h as usize);
4207                if p[..3] != q[..3] && !corner {
4208                    if bad == 0 {
4209                        first = (x, y);
4210                    }
4211                    if bad % 4096 == 0 {
4212                        some.push([x as i32, y as i32, 0, 0]);
4213                    }
4214                    bad += 1;
4215                }
4216            }
4217        }
4218        if bad != 0 {
4219            let total = w as u64 * h as u64;
4220            // (not only the first: it can lie in the shadow of a window above, where hit tests find ours)
4221            if screen && some.iter().any(|&p| plat::win_test(plat::WT_FROM_POINT, pw, p) != 1) {
4222                plat::write_stdout(format!("note: gpu expect screen: {bad} of {total} pixels differ: another window lies on top of ours\n").as_bytes());
4223            } else {
4224                self.fail(format!("{bad} pixels differ, first at {}, {}", first.0, first.1).as_bytes());
4225            }
4226        }
4227    }
4228}
4229
4230/// nobody sees the window: the UI hid it, or the system did (a tool window with its minimised owner)
4231fn unseen(w: &Win) -> bool {
4232    w.hidden || plat::win_state(w.pw) & plat::WST_HIDDEN != 0
4233}
4234
4235/// a script's real window that the system keeps at another size than its frame, for a while now
4236fn kept_at_other_size(w: &Win) -> bool {
4237    plat::win_size(w.pw) != w.size && plat::time_us() >= w.stable_at + KEPT_US
4238}
4239
4240// the real windows of a script: their messages, their events dropped (a wake-up of a task stays)
4241fn script_pump() {
4242    plat::wait(0);
4243    let mut keep: Vec<plat::Event> = vec![];
4244    while let Some(e) = plat::evq_pop() {
4245        if e.typ == plat::E_WAKE {
4246            keep.push(e);
4247        }
4248    }
4249    for e in keep {
4250        plat::evq_push(e);
4251    }
4252}