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