Skip to content

plan stage re-processes unchanged registered canvas images every frame (~1.35 ms per drawn 512px image) #101

Description

@jamesslock

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:

  • plan cost 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).
  • present cost scales with churned bytes and is ~free when nothing
    changed — the change-gating pattern we'd like to see applied to plan
    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 batch tiles per tick with freshly
    painted pixels (batch configurable 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 with native automate,
    and read frame-stage timings via native automate profile on.
  • Compare plan p50 across batch=0 vs batch=16 at 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions