154 lines
9.1 KiB
Markdown
154 lines
9.1 KiB
Markdown
# 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<f32>` 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.
|