diff --git a/harness/experiments/grid-cache-20260925/COLD-PAN.md b/harness/experiments/grid-cache-20260925/COLD-PAN.md new file mode 100644 index 00000000..e529f9eb --- /dev/null +++ b/harness/experiments/grid-cache-20260925/COLD-PAN.md @@ -0,0 +1,153 @@ +# Cold construction and pan alternatives + +Follow-up to [the stationary-camera cache](README.md), for issue 44. Application +source remains `336720559bcec511949caa8a42bee8a55c3822da`, with evidence checkpoint +`2ca84676ee4dc30846208881a27d90a901482085`. These standalone experiments change no +application code, dependencies, event handling or release. Native interaction +remains paused; no result below establishes native FPS. + +## Question and method + +The existing cache removes repeated construction at identical camera inputs but +misses during pan/zoom. Can simpler geometry or renderer-level translation remove +that cost without changing the grid's appearance? Use the same unoptimized Iced +0.14 WGPU dependencies and Windows environment recorded in `context.json`. +Compile the new drivers with the same `rustc`/extern/linker arguments described +in README, replacing the driver filename and output executable. No desktop window +is created and no input is sent. Run `cold_grid_probe` with argument `20`; phase +drivers take no arguments. + +The cold driver compares original circles, one reusable origin circle transformed +for each fill, one path per column, and direct 1.15-unit squares. Every candidate +also constructs one origin-circle template; this small common overhead is retained. +Three warmups precede 20 changing-pan samples. Construction ends before geometry +destruction, rendering and readback. Percentiles use sorted index `ceil((n-1)*p)`. +Fixed candidate ordering and uncontrolled OS load limit small timing comparisons. + +## Cold construction results + +`cold_grid_probe.rs` produces `grid-cold-01.log`. Median milliseconds: + +| Canvas, zoom | Original | Reused circle | Column batch | Square | +| --- | ---: | ---: | ---: | ---: | +| 1900x660, 1.0 | 61.0564 | 55.3975 | 54.3594 | 3.5527 | +| 1900x660, 0.5 | 215.8919 | 162.3073 | 158.5968 | 7.5699 | +| 1440x900, 1.0 | 60.1683 | 43.3860 | 60.8198 | 2.0340 | +| 1440x900, 0.5 | 165.2049 | 133.3420 | 143.1489 | 6.8178 | + +Both circle alternatives exactly match original pixels in all 12 sampled cases +(four integer canvas/zoom pairs, three pan offsets, scale 1). This does not prove +equivalence at fractional zoom, other DPI or other camera inputs. Squares match +eight of those cases but differ at the third pan offset in each canvas/zoom pair: +4,266–13,184 changed pixels, maximum channel difference 7. Their different shape +and area are an explicit appearance change, not a pixel-preserving optimization. + +Reusing circles and column batches leave substantial cold cost; the experiment +does not justify adding them to production as the solution to low FPS. The square +alternative is much faster in this scope, but is not adopted without evaluating +its appearance and the actual interaction result. The previous all-grid single +path experiment remains rejected for its much higher construction cost. + +## Source explanation and independent review + +A fresh read-only critic checked the probes and actual local Iced source. Each +`Path::circle` allocates/builds a path and converts its arc into quadratic segments. +Each WGPU `Frame::fill` allocates a boxed geometry builder and invokes Lyon fill +tessellation. Same-color fills already share a solid mesh buffer: this is not +one GPU draw call per dot. Frame translation transforms paths before tessellation, +so path reuse alone does not reuse triangles. + +Renderer-level `with_translation` can reuse cached triangles. Stock Canvas cannot +attach a separate transform to each returned geometry; application adoption would +need a small background widget or wrapper, with layout, clipping, lifecycle and +interaction checks. Scaling the grid to handle zoom is unsuitable because the +dot radius must remain fixed in screen units. + +The critic also identified two important controls. Original opaque background +geometry must not silently become screenshot-clear color: MSAA/alpha composition +can change quantized pixels. Also, `iced_wgpu::triangle::Storage::prepare` rewrites +cached mesh data when its transform changes. Construction/command-recording time +therefore excludes real preparation/upload costs. The later probe measures a +rendering/readback path separately; neither timer represents native presentation. + +Primary sources: Iced 0.14 `iced_graphics/src/geometry/path{,/builder}.rs`, +`iced_wgpu/src/{geometry,triangle}.rs`, `iced_core/src/renderer.rs`, and +`iced_widget/src/canvas.rs`. The reviewer changed no files and did not execute the +probes; execution and interpretation remain the parent's responsibility. + +## Renderer-translated phase cache + +`phase_grid_probe_v1.rs` / `grid-phase-01.log` cache zero-phase circles once for +each size/spacing pair, translate by `pan.rem_euclid(step)`, and externally clip +to the original viewport. The original nonnegative row/column indices and fixed +screen-space radius are retained. Cache clip bounds include the radius margins. +This first prototype uses screenshot clear color instead of opaque background +geometry; its widespread one-level pixel differences are not accepted as parity. +Its compiler emitted one duplicate Renderer-import warning, retained in the +original driver. Later drivers remove that import and compile with `-D warnings`. + +`phase_grid_opaque_probe.rs` / `grid-phase-opaque-01.log` add an oversized opaque +background to the same cached geometry as the circles. Its bounds extend from +`(-step,-step)` to `(width+2*step,height+2*step)`, covering the viewport at every +positive remainder without adding extra dot rows or columns. This directly +compares against the unchanged legacy painter. A proposed separate-background +control was not executed; no result is claimed for it. + +Of 75 comparisons (five size/zoom pairs, five offsets, three DPI scales), 73 are +byte-exact. All 60 cases at integer canvas sizes and spacing 14 or 24 are exact. +At 301.5x211.25, zoom 0.73, pan (-13.25,9.75), scale 1 changes 12 pixels by at +most one channel level; scale 2 changes one pixel by at most six levels. These +are failures of exact parity and are preserved, not hidden behind a tolerance. + +The targeted `phase_grid_diagnostic.rs` / `grid-phase-diagnostic-01.log` locates +all 12 scale-1 differences at the rightmost physical column x=301. The scale-2 +single-pixel difference does not recur. That diagnostic skips earlier large +cases and their rendering/timing sequence: it does not exonerate the earlier +failure or establish that every residual difference is at an edge. + +The replay driver's first compilation failed with E0689 because the new control's +`Vector::new(-13.25,9.75)` left its numeric type ambiguous before `rem_euclid`. +An explicit `Vector` annotation corrects the experiment harness. No probe +executed during that failed attempt; no application source changed. + +`phase_grid_replay.rs` preserves the full opaque probe's rendering/timing sequence +and adds coordinate diagnostics without extra renders until that sequence ends. +`grid-phase-replay-01.log` reproduces both failures: the 12 right-edge pixels and +one **interior** pixel at physical (149,263), where legacy RGBA is +`[240,243,246,255]` and cached RGBA is `[246,248,251,255]`. The latter cannot be +dismissed as viewport-edge rounding. Afterwards, all 12 same-renderer repeat +controls (three repetitions of old/old and cached/cached at two scales) are +byte-exact; the control cache builds once. This finite control does not establish +universal determinism, but supports investigating differing geometry coverage +instead of treating the reproduced pixel as random readback noise. + +Cold construction plus command recording takes 68.42–173.72ms median for the four +large viewports; cached translation takes 0.0008–0.0011ms. Each per-size/spacing +cache builds exactly once through all pan comparisons and measurements. Zoom +and resize through a single retained cache are not tested by these drivers. +The labels containing `submit` mean renderer command recording, not GPU submission. + +Two selected cases include construction, recording, backend preparation and GPU +readback in a separate timer (two warmups, eight samples, sorted-index median): + +| Canvas, zoom | Original p50 ms | Phase cache p50 ms | +| --- | ---: | ---: | +| 1900x660, 1.0 | 259.9265 | 212.6536 | +| 1440x900, 0.5 | 337.2560 | 170.3780 | + +These support a benefit within the headless path, but retain significant renderer +and readback cost and variable outliers. They are not native frame times. Sample +size, sequential ordering and uncontrolled machine load prohibit a precise +general speedup claim. All driver/log hashes and execution mapping are in +`cold-pan-context.json`. + +The phase candidate removes the measured pan tessellation cost, but does not +meet exact visual preservation. Do not adopt it as an invisible optimization. +The small circle rewrites leave the main cost and squares change appearance. +Further work should investigate the reproduced interior coverage difference or +compare optimized dependency/build profiles before adding a custom widget. +Any adopted rendering candidate must then verify a real mounted scene at a +nonzero widget origin, foreground ordering, viewport clipping, pan/zoom/resize +cache lifecycle and unchanged event behavior. Native interaction/FPS verification +remains a separate required gate. No application implementation is changed by +these experiments. diff --git a/harness/experiments/grid-cache-20260925/README.md b/harness/experiments/grid-cache-20260925/README.md index bdac84b2..c5af665b 100644 --- a/harness/experiments/grid-cache-20260925/README.md +++ b/harness/experiments/grid-cache-20260925/README.md @@ -139,6 +139,11 @@ remains deferred by the user. No published release has been replaced. ## Environment and reproduction +The subsequent [cold-construction and pan experiments](COLD-PAN.md) compare +additional approaches, including a renderer-translated cache. They preserve +their own pixel differences, timing limits and adoption gates; they do not +change the production implementation verified above. + Windows 10.0.26200, Rust 1.98.1, x86_64-pc-windows-gnullvm, development profile with debug information disabled. The exposed CPU is Intel Core i7-10870H with 4 cores/threads; Windows reports VMware SVGA 3D, driver 9.17.11.4. The headless diff --git a/harness/experiments/grid-cache-20260925/cold-pan-context.json b/harness/experiments/grid-cache-20260925/cold-pan-context.json new file mode 100644 index 00000000..ea5e4cda --- /dev/null +++ b/harness/experiments/grid-cache-20260925/cold-pan-context.json @@ -0,0 +1,51 @@ +{ + "application_source": "336720559bcec511949caa8a42bee8a55c3822da", + "base_evidence": "2ca84676ee4dc30846208881a27d90a901482085", + "production_changes": false, + "environment": "Same unoptimized Windows Iced/Tokio artifacts as context.json", + "logs": [ + { + "driver": "cold_grid_probe.rs", + "exit_code": 0, + "log": "grid-cold-01.log" + }, + { + "driver": "phase_grid_probe_v1.rs", + "exit_code": 0, + "compile_warning": "Unused duplicate Renderer trait import; retained in original driver", + "log": "grid-phase-01.log" + }, + { + "driver": "phase_grid_opaque_probe.rs", + "exit_code": 0, + "compile_warnings_denied": true, + "log": "grid-phase-opaque-01.log" + }, + { + "driver": "phase_grid_diagnostic.rs", + "exit_code": 0, + "compile_warnings_denied": true, + "log": "grid-phase-diagnostic-01.log" + }, + { + "driver": "phase_grid_replay.rs", + "log": "grid-phase-replay-01.log", + "exit_code": 0, + "compile_warnings_denied": true, + "initial_compile": "E0689 ambiguous Vector scalar before rem_euclid, fixed with Vector; no execution on failed attempt" + } + ], + "sha256": { + "cold_grid_probe.rs": "a4cc6543229ae4901d54061823a3b05ec6fe3b988f245df12898e013dbb93323", + "phase_grid_probe_v1.rs": "c4293e8bb022fae5bffe67002a6f7c867549f2fa8a865b107f39070655bc57fc", + "phase_grid_opaque_probe.rs": "43ac5be462ca31f9d1c49554291e08b60bac9de38face7138eaa850e4dcc0ab9", + "phase_grid_diagnostic.rs": "8de3c980ac65eef4fcb32b1b121d14aca35b93c704c5b19a440ad9dc7042248c", + "grid-cold-01.log": "ca5cac0be409bafcea2c154d42998cbd622c9e898892574bcd5a2df0c1a70886", + "grid-phase-01.log": "e0b2e2a3e24d642e1b6c2b1b418f24c952399758159de051f7c1ba7efbb39c4a", + "grid-phase-opaque-01.log": "2716cbeb589f4b23a48575680022231c0b94b56ba9c06ec1adbf9e3e6d07f7cd", + "grid-phase-diagnostic-01.log": "1dbb0f745169b88c56a21683dba6797a3dbbe753e8dd742bc9ecc8ca24788886", + "phase_grid_replay.rs": "96e62ba5d08f7e26d9705d3f90e3785151597b526293439345b59ed842de4971", + "grid-phase-replay-01.log": "040034be1b8eb3d4fc010bbe34c5ded47a953b8295bcf2f9440507d1a149da1f" + }, + "native_fps": "unverified" +} diff --git a/harness/experiments/grid-cache-20260925/cold_grid_probe.rs b/harness/experiments/grid-cache-20260925/cold_grid_probe.rs new file mode 100644 index 00000000..ffc038aa --- /dev/null +++ b/harness/experiments/grid-cache-20260925/cold_grid_probe.rs @@ -0,0 +1,142 @@ +//! Experimental cold-grid candidates; no window or input automation. +use iced::advanced::{ + graphics::geometry::Renderer as _, + renderer::{Headless, Renderer as _}, +}; +use iced::{ + Color, Point, Rectangle, Renderer, Size, Vector, + widget::canvas::{Frame, Geometry, Path}, +}; +use std::{hint::black_box, time::Instant}; +const BG: Color = Color::from_rgb8(246, 248, 251); +const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91); + +#[derive(Debug, Clone, Copy)] +enum Candidate { + Original, + Template, + Column, + Square, +} +fn draw(r: &Renderer, size: Size, zoom: f32, pan: Vector, candidate: Candidate) -> Geometry { + let mut f = Frame::new(r, size); + f.fill_rectangle(Point::ORIGIN, size, BG); + let step = (24. * zoom).max(14.); + let ox = pan.x.rem_euclid(step); + let oy = pan.y.rem_euclid(step); + let circle = Path::circle(Point::ORIGIN, 0.65); + for x in 0..(size.width / step) as i32 + 1 { + if matches!(candidate, Candidate::Column) { + let p = Path::new(|p| { + for y in 0..(size.height / step) as i32 + 1 { + p.circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65); + p.close(); + } + }); + f.fill(&p, DOT); + continue; + } + for y in 0..(size.height / step) as i32 + 1 { + let center = Point::new(ox + x as f32 * step, oy + y as f32 * step); + match candidate { + Candidate::Original => f.fill(&Path::circle(center, 0.65), DOT), + Candidate::Template => f.with_save(|f| { + f.translate(Vector::new(center.x, center.y)); + f.fill(&circle, DOT); + }), + Candidate::Square => f.fill_rectangle( + center - Vector::new(0.575, 0.575), + Size::new(1.15, 1.15), + DOT, + ), + Candidate::Column => unreachable!(), + } + } + } + f.into_geometry() +} +fn pixels(r: &mut Renderer, size: Size, g: Geometry) -> Vec { + r.reset(Rectangle::with_size(size)); + r.draw_geometry(g); + Headless::screenshot(r, Size::new(size.width as u32, size.height as u32), 1., BG) +} +fn main() { + let n = std::env::args() + .nth(1) + .map(|s| s.parse::().unwrap()) + .unwrap_or(20) + .max(1); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let mut r = rt + .block_on(::new( + iced::Font::DEFAULT, + 16.into(), + None, + )) + .unwrap(); + println!( + "renderer={} n={n}; construction only, unoptimized linked dependencies; NOT native FPS", + r.name() + ); + for (size, zoom) in [ + (Size::new(1900., 660.), 1.), + (Size::new(1900., 660.), 0.5), + (Size::new(1440., 900.), 1.), + (Size::new(1440., 900.), 0.5), + ] { + println!("size={}x{} zoom={zoom}", size.width, size.height); + for candidate in [ + Candidate::Original, + Candidate::Template, + Candidate::Column, + Candidate::Square, + ] { + for pan in [ + Vector::new(-13.25, 9.75), + Vector::new(0., 0.), + Vector::new(6.125, -8.5), + ] { + let base = draw(&r, size, zoom, pan, Candidate::Original); + let base = pixels(&mut r, size, base); + let test = draw(&r, size, zoom, pan, candidate); + let test = pixels(&mut r, size, test); + let changed = base + .chunks_exact(4) + .zip(test.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count(); + let max = base + .iter() + .zip(&test) + .map(|(a, b)| a.abs_diff(*b)) + .max() + .unwrap_or(0); + println!( + "{candidate:?} pan={},{} changed_pixels={changed} max_channel_delta={max}", + pan.x, pan.y + ); + } + let mut ms = Vec::new(); + for i in 0..n + 3 { + let pan = Vector::new(-13.25 + i as f32 * 0.25, 9.75); + let now = Instant::now(); + let g = black_box(draw(&r, size, zoom, pan, candidate)); + let elapsed = now.elapsed().as_secs_f64() * 1000.; + drop(g); + if i >= 3 { + ms.push(elapsed); + } + } + ms.sort_by(f64::total_cmp); + println!( + "{candidate:?} changing_pan n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}", + ms[((n - 1) as f64 * 0.5).ceil() as usize], + ms[((n - 1) as f64 * 0.95).ceil() as usize], + ms[n - 1] + ); + } + } +} diff --git a/harness/experiments/grid-cache-20260925/grid-cold-01.log b/harness/experiments/grid-cache-20260925/grid-cold-01.log new file mode 100644 index 00000000..8ac931a3 --- /dev/null +++ b/harness/experiments/grid-cache-20260925/grid-cold-01.log @@ -0,0 +1,69 @@ +renderer=wgpu n=20; construction only, unoptimized linked dependencies; NOT native FPS +size=1900x660 zoom=1 +Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Original pan=0,0 changed_pixels=0 max_channel_delta=0 +Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Original changing_pan n=20 p50_ms=61.0564 p95_ms=111.2137 max_ms=111.2137 +Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Template pan=0,0 changed_pixels=0 max_channel_delta=0 +Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Template changing_pan n=20 p50_ms=55.3975 p95_ms=73.8790 max_ms=73.8790 +Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Column pan=0,0 changed_pixels=0 max_channel_delta=0 +Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Column changing_pan n=20 p50_ms=54.3594 p95_ms=69.6607 max_ms=69.6607 +Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Square pan=0,0 changed_pixels=0 max_channel_delta=0 +Square pan=6.125,-8.5 changed_pixels=4266 max_channel_delta=7 +Square changing_pan n=20 p50_ms=3.5527 p95_ms=5.6911 max_ms=5.6911 +size=1900x660 zoom=0.5 +Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Original pan=0,0 changed_pixels=0 max_channel_delta=0 +Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Original changing_pan n=20 p50_ms=215.8919 p95_ms=253.9666 max_ms=253.9666 +Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Template pan=0,0 changed_pixels=0 max_channel_delta=0 +Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Template changing_pan n=20 p50_ms=162.3073 p95_ms=212.2833 max_ms=212.2833 +Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Column pan=0,0 changed_pixels=0 max_channel_delta=0 +Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Column changing_pan n=20 p50_ms=158.5968 p95_ms=449.6066 max_ms=449.6066 +Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Square pan=0,0 changed_pixels=0 max_channel_delta=0 +Square pan=6.125,-8.5 changed_pixels=12784 max_channel_delta=7 +Square changing_pan n=20 p50_ms=7.5699 p95_ms=13.3416 max_ms=13.3416 +size=1440x900 zoom=1 +Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Original pan=0,0 changed_pixels=0 max_channel_delta=0 +Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Original changing_pan n=20 p50_ms=60.1683 p95_ms=88.4967 max_ms=88.4967 +Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Template pan=0,0 changed_pixels=0 max_channel_delta=0 +Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Template changing_pan n=20 p50_ms=43.3860 p95_ms=53.1866 max_ms=53.1866 +Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Column pan=0,0 changed_pixels=0 max_channel_delta=0 +Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Column changing_pan n=20 p50_ms=60.8198 p95_ms=80.8932 max_ms=80.8932 +Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Square pan=0,0 changed_pixels=0 max_channel_delta=0 +Square pan=6.125,-8.5 changed_pixels=4440 max_channel_delta=7 +Square changing_pan n=20 p50_ms=2.0340 p95_ms=4.0620 max_ms=4.0620 +size=1440x900 zoom=0.5 +Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Original pan=0,0 changed_pixels=0 max_channel_delta=0 +Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Original changing_pan n=20 p50_ms=165.2049 p95_ms=203.5331 max_ms=203.5331 +Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Template pan=0,0 changed_pixels=0 max_channel_delta=0 +Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Template changing_pan n=20 p50_ms=133.3420 p95_ms=143.9790 max_ms=143.9790 +Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Column pan=0,0 changed_pixels=0 max_channel_delta=0 +Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +Column changing_pan n=20 p50_ms=143.1489 p95_ms=155.3680 max_ms=155.3680 +Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +Square pan=0,0 changed_pixels=0 max_channel_delta=0 +Square pan=6.125,-8.5 changed_pixels=13184 max_channel_delta=7 +Square changing_pan n=20 p50_ms=6.8178 p95_ms=10.8775 max_ms=10.8775 diff --git a/harness/experiments/grid-cache-20260925/grid-phase-01.log b/harness/experiments/grid-cache-20260925/grid-phase-01.log new file mode 100644 index 00000000..eff17b01 --- /dev/null +++ b/harness/experiments/grid-cache-20260925/grid-phase-01.log @@ -0,0 +1,96 @@ +renderer=wgpu; phase-cache prototype; NOT native FPS +size=1900x660 zoom=1 step=24 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=2212 max_channel_delta=1 +scale=1 pan=6.125,-8.5 changed_pixels=2133 max_channel_delta=1 +scale=1 pan=23.875,23.75 changed_pixels=2133 max_channel_delta=1 +scale=1 pan=-0.125,0.125 changed_pixels=4424 max_channel_delta=1 +scale=1.25 pan=0,0 changed_pixels=8745 max_channel_delta=1 +scale=1.25 pan=-13.25,9.75 changed_pixels=2212 max_channel_delta=1 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=23.875,23.75 changed_pixels=4266 max_channel_delta=1 +scale=1.25 pan=-0.125,0.125 changed_pixels=6557 max_channel_delta=1 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=8848 max_channel_delta=1 +scale=2 pan=6.125,-8.5 changed_pixels=4266 max_channel_delta=1 +scale=2 pan=23.875,23.75 changed_pixels=10665 max_channel_delta=1 +scale=2 pan=-0.125,0.125 changed_pixels=6478 max_channel_delta=1 +cached=false construct_and_submit changing_pan n=20 p50_ms=66.6997 p95_ms=77.3888 max_ms=77.3888 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0010 p95_ms=0.0017 max_ms=0.0017 +cache_builds=1 +size=1900x660 zoom=0.5 step=14 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=6392 max_channel_delta=1 +scale=1 pan=6.125,-8.5 changed_pixels=6392 max_channel_delta=1 +scale=1 pan=13.875,13.75 changed_pixels=6345 max_channel_delta=1 +scale=1 pan=-0.125,0.125 changed_pixels=12960 max_channel_delta=1 +scale=1.25 pan=0,0 changed_pixels=12781 max_channel_delta=1 +scale=1.25 pan=-13.25,9.75 changed_pixels=9588 max_channel_delta=1 +scale=1.25 pan=6.125,-8.5 changed_pixels=11288 max_channel_delta=1 +scale=1.25 pan=13.875,13.75 changed_pixels=9494 max_channel_delta=1 +scale=1.25 pan=-0.125,0.125 changed_pixels=12801 max_channel_delta=1 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=25568 max_channel_delta=1 +scale=2 pan=6.125,-8.5 changed_pixels=12784 max_channel_delta=1 +scale=2 pan=13.875,13.75 changed_pixels=31725 max_channel_delta=1 +scale=2 pan=-0.125,0.125 changed_pixels=19170 max_channel_delta=1 +cached=false construct_and_submit changing_pan n=20 p50_ms=189.4570 p95_ms=221.8240 max_ms=221.8240 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0007 p95_ms=0.0008 max_ms=0.0008 +cache_builds=1 +size=1440x900 zoom=1 step=24 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=2280 max_channel_delta=1 +scale=1 pan=6.125,-8.5 changed_pixels=2220 max_channel_delta=1 +scale=1 pan=23.875,23.75 changed_pixels=2220 max_channel_delta=1 +scale=1 pan=-0.125,0.125 changed_pixels=4522 max_channel_delta=1 +scale=1.25 pan=0,0 changed_pixels=9000 max_channel_delta=1 +scale=1.25 pan=-13.25,9.75 changed_pixels=2280 max_channel_delta=1 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=23.875,23.75 changed_pixels=4403 max_channel_delta=1 +scale=1.25 pan=-0.125,0.125 changed_pixels=6742 max_channel_delta=1 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=9120 max_channel_delta=1 +scale=2 pan=6.125,-8.5 changed_pixels=4440 max_channel_delta=1 +scale=2 pan=23.875,23.75 changed_pixels=11026 max_channel_delta=1 +scale=2 pan=-0.125,0.125 changed_pixels=6683 max_channel_delta=1 +cached=false construct_and_submit changing_pan n=20 p50_ms=66.8644 p95_ms=93.9216 max_ms=93.9216 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008 +cache_builds=1 +size=1440x900 zoom=0.5 step=14 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=6592 max_channel_delta=1 +scale=1 pan=6.125,-8.5 changed_pixels=6592 max_channel_delta=1 +scale=1 pan=13.875,13.75 changed_pixels=6528 max_channel_delta=1 +scale=1 pan=-0.125,0.125 changed_pixels=13260 max_channel_delta=1 +scale=1.25 pan=0,0 changed_pixels=13306 max_channel_delta=1 +scale=1.25 pan=-13.25,9.75 changed_pixels=9952 max_channel_delta=1 +scale=1.25 pan=6.125,-8.5 changed_pixels=11488 max_channel_delta=1 +scale=1.25 pan=13.875,13.75 changed_pixels=9792 max_channel_delta=1 +scale=1.25 pan=-0.125,0.125 changed_pixels=13209 max_channel_delta=1 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=26368 max_channel_delta=1 +scale=2 pan=6.125,-8.5 changed_pixels=13184 max_channel_delta=1 +scale=2 pan=13.875,13.75 changed_pixels=32640 max_channel_delta=1 +scale=2 pan=-0.125,0.125 changed_pixels=19686 max_channel_delta=1 +cached=false construct_and_submit changing_pan n=20 p50_ms=202.5700 p95_ms=482.7433 max_ms=482.7433 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0011 p95_ms=0.0041 max_ms=0.0041 +cache_builds=1 +size=301.5x211.25 zoom=0.73 step=17.52 +scale=1 pan=0,0 changed_pixels=263 max_channel_delta=1 +scale=1 pan=-13.25,9.75 changed_pixels=251 max_channel_delta=1 +scale=1 pan=6.125,-8.5 changed_pixels=250 max_channel_delta=1 +scale=1 pan=17.395,17.27 changed_pixels=238 max_channel_delta=1 +scale=1 pan=-0.125,0.125 changed_pixels=241 max_channel_delta=1 +scale=1.25 pan=0,0 changed_pixels=379 max_channel_delta=1 +scale=1.25 pan=-13.25,9.75 changed_pixels=395 max_channel_delta=1 +scale=1.25 pan=6.125,-8.5 changed_pixels=332 max_channel_delta=1 +scale=1.25 pan=17.395,17.27 changed_pixels=289 max_channel_delta=1 +scale=1.25 pan=-0.125,0.125 changed_pixels=372 max_channel_delta=1 +scale=2 pan=0,0 changed_pixels=759 max_channel_delta=1 +scale=2 pan=-13.25,9.75 changed_pixels=626 max_channel_delta=6 +scale=2 pan=6.125,-8.5 changed_pixels=731 max_channel_delta=1 +scale=2 pan=17.395,17.27 changed_pixels=576 max_channel_delta=1 +scale=2 pan=-0.125,0.125 changed_pixels=784 max_channel_delta=1 +cached=false construct_and_submit changing_pan n=20 p50_ms=6.6260 p95_ms=21.1743 max_ms=21.1743 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0010 p95_ms=0.0012 max_ms=0.0012 +cache_builds=1 diff --git a/harness/experiments/grid-cache-20260925/grid-phase-diagnostic-01.log b/harness/experiments/grid-cache-20260925/grid-phase-diagnostic-01.log new file mode 100644 index 00000000..05b5b768 --- /dev/null +++ b/harness/experiments/grid-cache-20260925/grid-phase-diagnostic-01.log @@ -0,0 +1,32 @@ +renderer=wgpu; phase-cache prototype; NOT native FPS +size=301.5x211.25 zoom=0.73 step=17.52 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +difference_xy=301,26 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,27 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,62 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,80 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,97 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,115 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,132 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,150 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,167 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,184 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,185 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,202 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +scale=1 pan=-13.25,9.75 changed_pixels=12 max_channel_delta=1 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=6.1755 p95_ms=8.4025 max_ms=8.4025 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0011 p95_ms=0.0019 max_ms=0.0019 +cache_builds=1 diff --git a/harness/experiments/grid-cache-20260925/grid-phase-opaque-01.log b/harness/experiments/grid-cache-20260925/grid-phase-opaque-01.log new file mode 100644 index 00000000..03e1a678 --- /dev/null +++ b/harness/experiments/grid-cache-20260925/grid-phase-opaque-01.log @@ -0,0 +1,100 @@ +renderer=wgpu; phase-cache prototype; NOT native FPS +size=1900x660 zoom=1 step=24 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=72.0271 p95_ms=99.9725 max_ms=99.9725 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0011 p95_ms=0.0015 max_ms=0.0015 +cache_builds=1 +cached=false construct_submit_screenshot changing_pan n=8 p50_ms=259.9265 min_ms=253.3506 max_ms=281.4865 +cached=true construct_submit_screenshot changing_pan n=8 p50_ms=212.6536 min_ms=184.5191 max_ms=299.2711 +size=1900x660 zoom=0.5 step=14 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=167.4776 p95_ms=186.8968 max_ms=186.8968 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008 +cache_builds=1 +size=1440x900 zoom=1 step=24 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=68.4232 p95_ms=97.6147 max_ms=97.6147 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0458 max_ms=0.0458 +cache_builds=1 +size=1440x900 zoom=0.5 step=14 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=173.7177 p95_ms=215.3188 max_ms=215.3188 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0009 max_ms=0.0009 +cache_builds=1 +cached=false construct_submit_screenshot changing_pan n=8 p50_ms=337.2560 min_ms=319.0543 max_ms=366.4956 +cached=true construct_submit_screenshot changing_pan n=8 p50_ms=170.3780 min_ms=144.1992 max_ms=188.4337 +size=301.5x211.25 zoom=0.73 step=17.52 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=12 max_channel_delta=1 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=1 max_channel_delta=6 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=5.4797 p95_ms=6.8957 max_ms=6.8957 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0007 p95_ms=0.0008 max_ms=0.0008 +cache_builds=1 diff --git a/harness/experiments/grid-cache-20260925/grid-phase-replay-01.log b/harness/experiments/grid-cache-20260925/grid-phase-replay-01.log new file mode 100644 index 00000000..7d7a1f15 --- /dev/null +++ b/harness/experiments/grid-cache-20260925/grid-phase-replay-01.log @@ -0,0 +1,127 @@ +renderer=wgpu; phase-cache prototype; NOT native FPS +size=1900x660 zoom=1 step=24 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=68.3424 p95_ms=88.0883 max_ms=88.0883 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0010 p95_ms=0.0012 max_ms=0.0012 +cache_builds=1 +cached=false construct_submit_screenshot changing_pan n=8 p50_ms=249.7269 min_ms=216.9378 max_ms=267.1425 +cached=true construct_submit_screenshot changing_pan n=8 p50_ms=215.6865 min_ms=177.7329 max_ms=262.5719 +size=1900x660 zoom=0.5 step=14 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=190.0141 p95_ms=231.4193 max_ms=231.4193 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008 +cache_builds=1 +size=1440x900 zoom=1 step=24 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=72.2449 p95_ms=86.5021 max_ms=86.5021 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008 +cache_builds=1 +size=1440x900 zoom=0.5 step=14 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=209.4728 p95_ms=329.0977 max_ms=329.0977 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0009 max_ms=0.0009 +cache_builds=1 +cached=false construct_submit_screenshot changing_pan n=8 p50_ms=384.7680 min_ms=347.4879 max_ms=420.2894 +cached=true construct_submit_screenshot changing_pan n=8 p50_ms=164.1611 min_ms=150.1276 max_ms=186.4567 +size=301.5x211.25 zoom=0.73 step=17.52 +scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-13.25,9.75 changed_pixels=12 max_channel_delta=1 +difference_xy=301,26 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,27 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,62 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,80 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,97 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,115 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,132 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,150 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,167 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,184 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,185 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +difference_xy=301,202 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255] +scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0 +scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0 +scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-13.25,9.75 changed_pixels=1 max_channel_delta=6 +difference_xy=149,263 expected=[240, 243, 246, 255] actual=[246, 248, 251, 255] +scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0 +scale=2 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0 +scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0 +cached=false construct_and_submit changing_pan n=20 p50_ms=5.5680 p95_ms=7.0634 max_ms=7.0634 +cached=true construct_and_submit changing_pan n=20 p50_ms=0.0007 p95_ms=0.0008 max_ms=0.0008 +cache_builds=1 +original_sequence_complete; beginning same-renderer repeat controls +comparison=old/old repetition=1 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=cached/cached repetition=1 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=old/old repetition=1 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=cached/cached repetition=1 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=old/old repetition=2 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=cached/cached repetition=2 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=old/old repetition=2 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=cached/cached repetition=2 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=old/old repetition=3 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=cached/cached repetition=3 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=old/old repetition=3 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +comparison=cached/cached repetition=3 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0 +control_cache_builds=1 diff --git a/harness/experiments/grid-cache-20260925/phase_grid_diagnostic.rs b/harness/experiments/grid-cache-20260925/phase_grid_diagnostic.rs new file mode 100644 index 00000000..84949b2a --- /dev/null +++ b/harness/experiments/grid-cache-20260925/phase_grid_diagnostic.rs @@ -0,0 +1,209 @@ +//! Renderer-level phase translation experiment, not application code. +use iced::advanced::{Renderer as _, graphics::geometry::Renderer as _, renderer::Headless}; +use iced::{ + Color, Point, Rectangle, Renderer, Size, Vector, + widget::canvas::{Cache, Frame, Geometry, Path}, +}; +use std::{cell::Cell, hint::black_box, time::Instant}; +const BG: Color = Color::from_rgb8(246, 248, 251); +const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91); +fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) { + for x in 0..(size.width / step) as i32 + 1 { + for y in 0..(size.height / step) as i32 + 1 { + f.fill( + &Path::circle( + Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step), + 0.65, + ), + DOT, + ); + } + } +} +fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry { + let mut f = Frame::new(r, size); + f.fill_rectangle(Point::ORIGIN, size, BG); + dots(&mut f, size, step, phase); + f.into_geometry() +} +fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell) -> Geometry { + cache.draw_with_bounds( + r, + Rectangle { + x: -step, + y: -step, + width: size.width + 3. * step, + height: size.height + 3. * step, + }, + |f| { + builds.set(builds.get() + 1); + f.fill_rectangle( + Point::new(-step, -step), + Size::new(size.width + 3. * step, size.height + 3. * step), + BG, + ); + dots(f, size, step, Vector::ZERO); + }, + ) +} +fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) { + r.with_layer(Rectangle::with_size(size), |r| { + r.with_translation(pan, |r| r.draw_geometry(g)) + }); +} +fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec { + Headless::screenshot( + r, + Size::new( + (size.width * scale).ceil() as u32, + (size.height * scale).ceil() as u32, + ), + scale, + BG, + ) +} +fn main() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let mut r = rt + .block_on(::new( + iced::Font::DEFAULT, + 16.into(), + None, + )) + .unwrap(); + println!( + "renderer={}; phase-cache prototype; NOT native FPS", + r.name() + ); + for (size, zoom) in [ + (Size::new(1900., 660.), 1.), + (Size::new(1900., 660.), 0.5), + (Size::new(1440., 900.), 1.), + (Size::new(1440., 900.), 0.5), + (Size::new(301.5, 211.25), 0.73), + ] { + if size.width != 301.5 { + continue; + } + let step = (24f32 * zoom).max(14.); + let cache = Cache::new(); + let builds = Cell::new(0); + println!( + "size={}x{} zoom={zoom} step={step}", + size.width, size.height + ); + for scale in [1., 1.25, 2.] { + for pan in [ + Vector::ZERO, + Vector::new(-13.25, 9.75), + Vector::new(6.125, -8.5), + Vector::new(step - 0.125, step - 0.25), + Vector::new(-0.125, 0.125), + ] { + let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step)); + r.reset(Rectangle::with_size(size)); + let g = old(&r, size, step, offset); + r.draw_geometry(g); + let expected = shot(&mut r, size, scale); + r.reset(Rectangle::with_size(size)); + let g = phase(&r, &cache, size, step, &builds); + submit(&mut r, size, g, offset); + let actual = shot(&mut r, size, scale); + assert_eq!(expected.len(), actual.len()); + for (index, (a, b)) in expected + .chunks_exact(4) + .zip(actual.chunks_exact(4)) + .enumerate() + .filter(|(_, (a, b))| a != b) + .take(20) + { + println!( + "difference_xy={},{} expected={a:?} actual={b:?}", + index % (size.width * scale).ceil() as usize, + index / (size.width * scale).ceil() as usize + ); + } + let changed = expected + .chunks_exact(4) + .zip(actual.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count(); + let max = expected + .iter() + .zip(&actual) + .map(|(a, b)| a.abs_diff(*b)) + .max() + .unwrap_or(0); + println!( + "scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}", + pan.x, pan.y + ); + } + } + assert_eq!(builds.get(), 1); + for cached in [false, true] { + let mut ms = Vec::new(); + for i in 0..23 { + r.reset(Rectangle::with_size(size)); + let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75); + let now = Instant::now(); + let g = if cached { + phase(&r, &cache, size, step, &builds) + } else { + old(&r, size, step, offset) + }; + if cached { + submit(&mut r, size, g, offset) + } else { + r.draw_geometry(g) + }; + let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.); + if i >= 3 { + ms.push(elapsed); + } + } + ms.sort_by(f64::total_cmp); + println!( + "cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}", + ms[10], ms[19], ms[19] + ); + } + println!("cache_builds={}", builds.get()); + assert_eq!(builds.get(), 1); + // Includes backend prepare/upload and readback; still not native presentation. + if size.width == 1900. && zoom == 1. || size.width == 1440. && zoom == 0.5 { + for cached in [false, true] { + let mut ms = Vec::new(); + for i in 0..10 { + r.reset(Rectangle::with_size(size)); + let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75); + let now = Instant::now(); + let g = if cached { + phase(&r, &cache, size, step, &builds) + } else { + old(&r, size, step, offset) + }; + if cached { + submit(&mut r, size, g, offset); + } else { + r.draw_geometry(g); + } + let pixels = black_box(shot(&mut r, size, 1.)); + let elapsed = now.elapsed().as_secs_f64() * 1000.; + drop(pixels); + if i >= 2 { + ms.push(elapsed); + } + } + ms.sort_by(f64::total_cmp); + println!( + "cached={cached} construct_submit_screenshot changing_pan n=8 p50_ms={:.4} min_ms={:.4} max_ms={:.4}", + ms[4], ms[0], ms[7] + ); + } + } + } +} diff --git a/harness/experiments/grid-cache-20260925/phase_grid_opaque_probe.rs b/harness/experiments/grid-cache-20260925/phase_grid_opaque_probe.rs new file mode 100644 index 00000000..520461a4 --- /dev/null +++ b/harness/experiments/grid-cache-20260925/phase_grid_opaque_probe.rs @@ -0,0 +1,193 @@ +//! Renderer-level phase translation experiment, not application code. +use iced::advanced::{Renderer as _, graphics::geometry::Renderer as _, renderer::Headless}; +use iced::{ + Color, Point, Rectangle, Renderer, Size, Vector, + widget::canvas::{Cache, Frame, Geometry, Path}, +}; +use std::{cell::Cell, hint::black_box, time::Instant}; +const BG: Color = Color::from_rgb8(246, 248, 251); +const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91); +fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) { + for x in 0..(size.width / step) as i32 + 1 { + for y in 0..(size.height / step) as i32 + 1 { + f.fill( + &Path::circle( + Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step), + 0.65, + ), + DOT, + ); + } + } +} +fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry { + let mut f = Frame::new(r, size); + f.fill_rectangle(Point::ORIGIN, size, BG); + dots(&mut f, size, step, phase); + f.into_geometry() +} +fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell) -> Geometry { + cache.draw_with_bounds( + r, + Rectangle { + x: -step, + y: -step, + width: size.width + 3. * step, + height: size.height + 3. * step, + }, + |f| { + builds.set(builds.get() + 1); + f.fill_rectangle( + Point::new(-step, -step), + Size::new(size.width + 3. * step, size.height + 3. * step), + BG, + ); + dots(f, size, step, Vector::ZERO); + }, + ) +} +fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) { + r.with_layer(Rectangle::with_size(size), |r| { + r.with_translation(pan, |r| r.draw_geometry(g)) + }); +} +fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec { + Headless::screenshot( + r, + Size::new( + (size.width * scale).ceil() as u32, + (size.height * scale).ceil() as u32, + ), + scale, + BG, + ) +} +fn main() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let mut r = rt + .block_on(::new( + iced::Font::DEFAULT, + 16.into(), + None, + )) + .unwrap(); + println!( + "renderer={}; phase-cache prototype; NOT native FPS", + r.name() + ); + for (size, zoom) in [ + (Size::new(1900., 660.), 1.), + (Size::new(1900., 660.), 0.5), + (Size::new(1440., 900.), 1.), + (Size::new(1440., 900.), 0.5), + (Size::new(301.5, 211.25), 0.73), + ] { + let step = (24f32 * zoom).max(14.); + let cache = Cache::new(); + let builds = Cell::new(0); + println!( + "size={}x{} zoom={zoom} step={step}", + size.width, size.height + ); + for scale in [1., 1.25, 2.] { + for pan in [ + Vector::ZERO, + Vector::new(-13.25, 9.75), + Vector::new(6.125, -8.5), + Vector::new(step - 0.125, step - 0.25), + Vector::new(-0.125, 0.125), + ] { + let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step)); + r.reset(Rectangle::with_size(size)); + let g = old(&r, size, step, offset); + r.draw_geometry(g); + let expected = shot(&mut r, size, scale); + r.reset(Rectangle::with_size(size)); + let g = phase(&r, &cache, size, step, &builds); + submit(&mut r, size, g, offset); + let actual = shot(&mut r, size, scale); + assert_eq!(expected.len(), actual.len()); + let changed = expected + .chunks_exact(4) + .zip(actual.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count(); + let max = expected + .iter() + .zip(&actual) + .map(|(a, b)| a.abs_diff(*b)) + .max() + .unwrap_or(0); + println!( + "scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}", + pan.x, pan.y + ); + } + } + assert_eq!(builds.get(), 1); + for cached in [false, true] { + let mut ms = Vec::new(); + for i in 0..23 { + r.reset(Rectangle::with_size(size)); + let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75); + let now = Instant::now(); + let g = if cached { + phase(&r, &cache, size, step, &builds) + } else { + old(&r, size, step, offset) + }; + if cached { + submit(&mut r, size, g, offset) + } else { + r.draw_geometry(g) + }; + let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.); + if i >= 3 { + ms.push(elapsed); + } + } + ms.sort_by(f64::total_cmp); + println!( + "cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}", + ms[10], ms[19], ms[19] + ); + } + println!("cache_builds={}", builds.get()); + assert_eq!(builds.get(), 1); + // Includes backend prepare/upload and readback; still not native presentation. + if size.width == 1900. && zoom == 1. || size.width == 1440. && zoom == 0.5 { + for cached in [false, true] { + let mut ms = Vec::new(); + for i in 0..10 { + r.reset(Rectangle::with_size(size)); + let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75); + let now = Instant::now(); + let g = if cached { + phase(&r, &cache, size, step, &builds) + } else { + old(&r, size, step, offset) + }; + if cached { + submit(&mut r, size, g, offset); + } else { + r.draw_geometry(g); + } + let pixels = black_box(shot(&mut r, size, 1.)); + let elapsed = now.elapsed().as_secs_f64() * 1000.; + drop(pixels); + if i >= 2 { + ms.push(elapsed); + } + } + ms.sort_by(f64::total_cmp); + println!( + "cached={cached} construct_submit_screenshot changing_pan n=8 p50_ms={:.4} min_ms={:.4} max_ms={:.4}", + ms[4], ms[0], ms[7] + ); + } + } + } +} diff --git a/harness/experiments/grid-cache-20260925/phase_grid_probe_v1.rs b/harness/experiments/grid-cache-20260925/phase_grid_probe_v1.rs new file mode 100644 index 00000000..b7dad4f1 --- /dev/null +++ b/harness/experiments/grid-cache-20260925/phase_grid_probe_v1.rs @@ -0,0 +1,160 @@ +//! Renderer-level phase translation experiment, not application code. +use iced::advanced::{ + Renderer as _, + graphics::geometry::Renderer as _, + renderer::{Headless, Renderer as _}, +}; +use iced::{ + Color, Point, Rectangle, Renderer, Size, Vector, + widget::canvas::{Cache, Frame, Geometry, Path}, +}; +use std::{cell::Cell, hint::black_box, time::Instant}; +const BG: Color = Color::from_rgb8(246, 248, 251); +const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91); +fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) { + for x in 0..(size.width / step) as i32 + 1 { + for y in 0..(size.height / step) as i32 + 1 { + f.fill( + &Path::circle( + Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step), + 0.65, + ), + DOT, + ); + } + } +} +fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry { + let mut f = Frame::new(r, size); + f.fill_rectangle(Point::ORIGIN, size, BG); + dots(&mut f, size, step, phase); + f.into_geometry() +} +fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell) -> Geometry { + cache.draw_with_bounds( + r, + Rectangle { + x: -1., + y: -1., + width: size.width + step + 2., + height: size.height + step + 2., + }, + |f| { + builds.set(builds.get() + 1); + dots(f, size, step, Vector::ZERO); + }, + ) +} +fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) { + r.with_layer(Rectangle::with_size(size), |r| { + r.with_translation(pan, |r| r.draw_geometry(g)) + }); +} +fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec { + Headless::screenshot( + r, + Size::new( + (size.width * scale).ceil() as u32, + (size.height * scale).ceil() as u32, + ), + scale, + BG, + ) +} +fn main() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let mut r = rt + .block_on(::new( + iced::Font::DEFAULT, + 16.into(), + None, + )) + .unwrap(); + println!( + "renderer={}; phase-cache prototype; NOT native FPS", + r.name() + ); + for (size, zoom) in [ + (Size::new(1900., 660.), 1.), + (Size::new(1900., 660.), 0.5), + (Size::new(1440., 900.), 1.), + (Size::new(1440., 900.), 0.5), + (Size::new(301.5, 211.25), 0.73), + ] { + let step = (24f32 * zoom).max(14.); + let cache = Cache::new(); + let builds = Cell::new(0); + println!( + "size={}x{} zoom={zoom} step={step}", + size.width, size.height + ); + for scale in [1., 1.25, 2.] { + for pan in [ + Vector::ZERO, + Vector::new(-13.25, 9.75), + Vector::new(6.125, -8.5), + Vector::new(step - 0.125, step - 0.25), + Vector::new(-0.125, 0.125), + ] { + let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step)); + r.reset(Rectangle::with_size(size)); + let g = old(&r, size, step, offset); + r.draw_geometry(g); + let expected = shot(&mut r, size, scale); + r.reset(Rectangle::with_size(size)); + let g = phase(&r, &cache, size, step, &builds); + submit(&mut r, size, g, offset); + let actual = shot(&mut r, size, scale); + assert_eq!(expected.len(), actual.len()); + let changed = expected + .chunks_exact(4) + .zip(actual.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count(); + let max = expected + .iter() + .zip(&actual) + .map(|(a, b)| a.abs_diff(*b)) + .max() + .unwrap_or(0); + println!( + "scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}", + pan.x, pan.y + ); + } + } + assert_eq!(builds.get(), 1); + for cached in [false, true] { + let mut ms = Vec::new(); + for i in 0..23 { + r.reset(Rectangle::with_size(size)); + let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75); + let now = Instant::now(); + let g = if cached { + phase(&r, &cache, size, step, &builds) + } else { + old(&r, size, step, offset) + }; + if cached { + submit(&mut r, size, g, offset) + } else { + r.draw_geometry(g) + }; + let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.); + if i >= 3 { + ms.push(elapsed); + } + } + ms.sort_by(f64::total_cmp); + println!( + "cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}", + ms[10], ms[19], ms[19] + ); + } + println!("cache_builds={}", builds.get()); + assert_eq!(builds.get(), 1); + } +} diff --git a/harness/experiments/grid-cache-20260925/phase_grid_replay.rs b/harness/experiments/grid-cache-20260925/phase_grid_replay.rs new file mode 100644 index 00000000..b34bcb73 --- /dev/null +++ b/harness/experiments/grid-cache-20260925/phase_grid_replay.rs @@ -0,0 +1,285 @@ +//! Full-sequence opaque phase replay with diagnostics and repeat controls; not application code. +use iced::advanced::{Renderer as _, graphics::geometry::Renderer as _, renderer::Headless}; +use iced::{ + Color, Point, Rectangle, Renderer, Size, Vector, + widget::canvas::{Cache, Frame, Geometry, Path}, +}; +use std::{cell::Cell, hint::black_box, time::Instant}; +const BG: Color = Color::from_rgb8(246, 248, 251); +const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91); +fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) { + for x in 0..(size.width / step) as i32 + 1 { + for y in 0..(size.height / step) as i32 + 1 { + f.fill( + &Path::circle( + Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step), + 0.65, + ), + DOT, + ); + } + } +} +fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry { + let mut f = Frame::new(r, size); + f.fill_rectangle(Point::ORIGIN, size, BG); + dots(&mut f, size, step, phase); + f.into_geometry() +} +fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell) -> Geometry { + cache.draw_with_bounds( + r, + Rectangle { + x: -step, + y: -step, + width: size.width + 3. * step, + height: size.height + 3. * step, + }, + |f| { + builds.set(builds.get() + 1); + f.fill_rectangle( + Point::new(-step, -step), + Size::new(size.width + 3. * step, size.height + 3. * step), + BG, + ); + dots(f, size, step, Vector::ZERO); + }, + ) +} +fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) { + r.with_layer(Rectangle::with_size(size), |r| { + r.with_translation(pan, |r| r.draw_geometry(g)) + }); +} +fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec { + Headless::screenshot( + r, + Size::new( + (size.width * scale).ceil() as u32, + (size.height * scale).ceil() as u32, + ), + scale, + BG, + ) +} +// Diagnostics only: this performs no rendering and does not mutate the renderer. +fn log_coordinates(expected: &[u8], actual: &[u8], size: Size, scale: f32) { + assert_eq!(expected.len(), actual.len()); + let physical_width = (size.width * scale).ceil() as usize; + for (index, (a, b)) in expected + .as_chunks::<4>() + .0 + .iter() + .zip(actual.as_chunks::<4>().0) + .enumerate() + .filter(|(_, (a, b))| a != b) + .take(20) + { + println!( + "difference_xy={},{} expected={a:?} actual={b:?}", + index % physical_width, + index / physical_width + ); + } +} + +fn report_control(label: &str, expected: &[u8], actual: &[u8], size: Size, scale: f32) { + assert_eq!(expected.len(), actual.len()); + let changed = expected + .as_chunks::<4>() + .0 + .iter() + .zip(actual.as_chunks::<4>().0) + .filter(|(a, b)| a != b) + .count(); + let max = expected + .iter() + .zip(actual) + .map(|(a, b)| a.abs_diff(*b)) + .max() + .unwrap_or(0); + println!( + "comparison={label} size={}x{} scale={scale} pan=-13.25,9.75 changed_pixels={changed} max_channel_delta={max}", + size.width, size.height + ); + log_coordinates(expected, actual, size, scale); +} +fn main() { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let mut r = rt + .block_on(::new( + iced::Font::DEFAULT, + 16.into(), + None, + )) + .unwrap(); + println!( + "renderer={}; phase-cache prototype; NOT native FPS", + r.name() + ); + for (size, zoom) in [ + (Size::new(1900., 660.), 1.), + (Size::new(1900., 660.), 0.5), + (Size::new(1440., 900.), 1.), + (Size::new(1440., 900.), 0.5), + (Size::new(301.5, 211.25), 0.73), + ] { + let step = (24f32 * zoom).max(14.); + let cache = Cache::new(); + let builds = Cell::new(0); + println!( + "size={}x{} zoom={zoom} step={step}", + size.width, size.height + ); + for scale in [1., 1.25, 2.] { + for pan in [ + Vector::ZERO, + Vector::new(-13.25, 9.75), + Vector::new(6.125, -8.5), + Vector::new(step - 0.125, step - 0.25), + Vector::new(-0.125, 0.125), + ] { + let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step)); + r.reset(Rectangle::with_size(size)); + let g = old(&r, size, step, offset); + r.draw_geometry(g); + let expected = shot(&mut r, size, scale); + r.reset(Rectangle::with_size(size)); + let g = phase(&r, &cache, size, step, &builds); + submit(&mut r, size, g, offset); + let actual = shot(&mut r, size, scale); + assert_eq!(expected.len(), actual.len()); + let changed = expected + .chunks_exact(4) + .zip(actual.chunks_exact(4)) + .filter(|(a, b)| a != b) + .count(); + let max = expected + .iter() + .zip(&actual) + .map(|(a, b)| a.abs_diff(*b)) + .max() + .unwrap_or(0); + println!( + "scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}", + pan.x, pan.y + ); + // Keep the original render/timing sequence; print diagnostics only. + log_coordinates(&expected, &actual, size, scale); + } + } + assert_eq!(builds.get(), 1); + for cached in [false, true] { + let mut ms = Vec::new(); + for i in 0..23 { + r.reset(Rectangle::with_size(size)); + let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75); + let now = Instant::now(); + let g = if cached { + phase(&r, &cache, size, step, &builds) + } else { + old(&r, size, step, offset) + }; + if cached { + submit(&mut r, size, g, offset) + } else { + r.draw_geometry(g) + }; + let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.); + if i >= 3 { + ms.push(elapsed); + } + } + ms.sort_by(f64::total_cmp); + println!( + "cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}", + ms[10], ms[19], ms[19] + ); + } + println!("cache_builds={}", builds.get()); + assert_eq!(builds.get(), 1); + // Includes backend prepare/upload and readback; still not native presentation. + if size.width == 1900. && zoom == 1. || size.width == 1440. && zoom == 0.5 { + for cached in [false, true] { + let mut ms = Vec::new(); + for i in 0..10 { + r.reset(Rectangle::with_size(size)); + let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75); + let now = Instant::now(); + let g = if cached { + phase(&r, &cache, size, step, &builds) + } else { + old(&r, size, step, offset) + }; + if cached { + submit(&mut r, size, g, offset); + } else { + r.draw_geometry(g); + } + let pixels = black_box(shot(&mut r, size, 1.)); + let elapsed = now.elapsed().as_secs_f64() * 1000.; + drop(pixels); + if i >= 2 { + ms.push(elapsed); + } + } + ms.sort_by(f64::total_cmp); + println!( + "cached={cached} construct_submit_screenshot changing_pan n=8 p50_ms={:.4} min_ms={:.4} max_ms={:.4}", + ms[4], ms[0], ms[7] + ); + } + } + } + // All original cases, timings and screenshots have completed above. + // Reuse the same renderer for controls. This cache is newly constructed, + // then retained across all control repetitions; original per-case caches + // have already gone out of scope, exactly as in the opaque probe. + println!("original_sequence_complete; beginning same-renderer repeat controls"); + let size = Size::new(301.5, 211.25); + let step = (24f32 * 0.73).max(14.); + let pan: Vector = Vector::new(-13.25, 9.75); + let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step)); + let cache = Cache::new(); + let builds = Cell::new(0); + for repetition in 1..=3 { + for scale in [1., 2.] { + r.reset(Rectangle::with_size(size)); + let g = old(&r, size, step, offset); + r.draw_geometry(g); + let first = shot(&mut r, size, scale); + r.reset(Rectangle::with_size(size)); + let g = old(&r, size, step, offset); + r.draw_geometry(g); + let second = shot(&mut r, size, scale); + report_control( + &format!("old/old repetition={repetition}"), + &first, + &second, + size, + scale, + ); + + r.reset(Rectangle::with_size(size)); + let g = phase(&r, &cache, size, step, &builds); + submit(&mut r, size, g, offset); + let first = shot(&mut r, size, scale); + r.reset(Rectangle::with_size(size)); + let g = phase(&r, &cache, size, step, &builds); + submit(&mut r, size, g, offset); + let second = shot(&mut r, size, scale); + report_control( + &format!("cached/cached repetition={repetition}"), + &first, + &second, + size, + scale, + ); + } + } + println!("control_cache_builds={}", builds.get()); + assert_eq!(builds.get(), 1); +}