vercel-labs / vercel-labs/native
plan stage re-processes unchanged registered canvas images every frame (~1.35 ms per drawn 512px image)
Nobody has claimed this yet.
- Dominant language
- Zig
- Stars
- 7.7k
- Forks
- 314
- Avg merge
- 5h
- Merged PRs (30d)
- 13
Description
Environment
- native-sdk v0.4.2
- macOS aarch64 (dev machine; not yet re-run on 8 GB Apple Silicon baseline)
- Zig 0.16.0
- Renderer: Metal, macOS host,
ReleaseFast
What happens
With a canvas scene holding several registered images drawn every frame, the
plan frame stage costs the same whether or not any image content actually
changed. On a 4×4 grid of 512×512 registered images (16 drawn tiles), plan
p50 is 21.6 ms even on a tick where only unrelated text changed — no
image was re-registered, nothing about the images differs from the prior
frame. That number does not move across batch=0/1/4/16 (0 to 16 MiB/tick of
actual image churn); it only moves with the count of drawn images, not
with how many changed.
By contrast, the present stage (GPU upload) is correctly change-gated:
it costs 0.56 ms when nothing changed and scales up with churned bytes
(~0.5–0.6 ms/MiB) only when images are actually re-registered.
What I expected
Since present already skips re-uploading images whose registered content
hasn't changed, I expected plan to similarly skip re-planning draw packets
for registered images that haven't changed since the last frame — i.e. a
fingerprint/dirty-check ahead of packet planning, not just ahead of GPU
upload.
Measured data
4×4 grid of 512×512 registered canvas images, all at the 1 MiB per-image
cap, all 16 registry slots in use. 16 ms repeating timer re-registers
batch tiles per tick with freshly painted pixels. Measured via in-model
monotonicNanoseconds around fx.registerImage and native automate profile frame-stage timings.
| Scenario | plan p50 | present p50 | interval p50 | ≈fps |
|---|---|---|---|---|
| batch=0 (text-only change), 16 tiles drawn | 21.6 ms | 0.56 ms | 24.2 ms | ~41 |
| batch=1 (1 MiB/tick churn), 16 tiles | 21.5 ms | 7.5 ms | 31.5 ms | ~32 |
| batch=4 (4 MiB/tick), 16 tiles | 21.5 ms | 7.7 ms | 32.8 ms | ~30 |
| batch=16 (16 MiB/tick), 16 tiles | 21.3 ms | 9.3 ms | 38.3 ms | ~26 |
| batch=1, 4 tiles drawn | 5.5 ms | 3.3 ms | 16.7 ms | ~60 |
| paused (no model change) | — | — | no frames | idle |
fx.registerImage itself (1 MiB tile) is cheap and not the bottleneck: avg
0.03 ms, worst 0.18 ms, 0 errors across 9,100+ calls.
Takeaways:
plancost is ~1.35 ms per drawn 512×512 registered image per produced
frame, scaling linearly with drawn-image count, flat across churn
levels (0/1/4/16 batch).presentcost scales with churned bytes and is ~free when nothing
changed — the change-gating pattern we'd like to see applied toplan
too.- Static scenes (no model change) produce no frames at all, so this only
bites during continuous interaction (pan/zoom/drag).
Repro
Repro app: spike/canvas-tiles/ (attached/available on request; not yet
published). Shape:
- 4×4 grid of 512×512 registered canvas images, each at the 1 MiB
per-image cap, using all 16 registry slots. - A 16 ms repeating timer re-registers
batchtiles per tick with freshly
painted pixels (batchconfigurable 0/1/4/8/16). - Toggle for how many tiles are drawn (4 or 16) and a pause control.
- Run with
native build -Dautomation=true, drive withnative automate,
and read frame-stage timings vianative automate profile on. - Compare
plan p50acrossbatch=0vsbatch=16at a fixed drawn-tile
count: cost is flat, which is the core observation.
Suggested direction
We're building an image-optimization app where a comparison canvas
(before/after tiles) is a core interaction, so plan cost sets our
interaction budget more than present does today. Given present already
has a change-gate, it looks like plan's image-packet planning could use
the same fingerprint (e.g. keyed on registered-image generation/version)
to skip re-planning packets for images that haven't changed since the last
produced frame. If that's already on the roadmap or there's a reason
plan can't easily reuse packets across frames (e.g. packet buffers get
reset/invalidated for other reasons), we'd appreciate a pointer — happy to
help verify a fix against this repro.
Related: #102, #103 (same project/spikes)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the available repro in spike/canvas-tiles/ and run it with native build -Dautomation=true, then use native automate profile on to compare plan p50 for batch=0 and batch=16. Done means confirming that unchanged registered images no longer make plan scale with drawn-image count while changed images still produce correct timings.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- macos, zig
- Domain
- computer-graphics, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100