Skip to content

perf(map): build the overlay frame from the accumulated strip - #9

Merged
sytchi merged 2 commits into
masterfrom
5a23442-map-render-off-loop
Jul 30, 2026
Merged

perf(map): build the overlay frame from the accumulated strip#9
sytchi merged 2 commits into
masterfrom
5a23442-map-render-off-loop

Conversation

@sytchi

@sytchi sytchi commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Two narrow changes in render_overlay, no behaviour change.

  • The overlay frame now starts from a copy of the accumulated vacuumed strip instead of allocating a fresh canvas and compositing the strip onto it. The strip's uncovered pixels are exactly (0, 0, 0, 0), so compositing it onto a transparent canvas was a no-op — the copy skips both the allocation and a full-canvas composite of the supersampled layer, the two most expensive fixed costs of a frame. The invariant is documented at extend_swath_layer and pinned by tests.
  • The zone branch uses the module-level Image.alpha_composite instead of the in-place method. Pillow implements the in-place variant as crop + composite + paste, i.e. three full passes over a 13.9 MB buffer.

The fast path applies only when there is no cleaned-area tint and no zones, which is the usual frame; otherwise the original path runs unchanged.

Verification

  • Byte-identical output. 9 scenarios x 12 frames off-line with zero differing subpixels; on a Home Assistant host the rendered pixel hash matches before and after and the PNG is byte-for-byte equal.
  • Benchmark on the target host (HAOS x86_64, Pillow 12.2), 30 paired renders: 548.2 -> 508.8 ms median, about 44 ms less per frame (-7 to -9%). Measured on the host rather than a dev machine because a Mac renders the same frame roughly 15x faster and would not be representative.
  • Tests: 378 passing, 6 pre-existing failures that also fail on master (brittle asyncio.get_event_loop() under CPython 3.14). Ruff clean.

Scope

This shaves a fixed cost off every frame; it does not eliminate render-related event-loop stalls. A ProcessPoolExecutor was considered and rejected: the renderer keeps ~31 MB of persistent PIL layers between frames, so it would need a dedicated long-lived process with its own lifecycle and crash recovery, which is disproportionate here.

sytchi added 2 commits July 29, 2026 17:07
render_overlay allocated a fresh supersampled RGBA canvas every frame and
composited the accumulated vacuumed strip onto it with the in-place
Image.alpha_composite() method. On a 200x271 map at scale 4 that canvas is
1600x2168 (3.5 Mpx), and the in-place method is implemented as crop +
composite + paste, so a single strip composite made three full passes over
it plus two allocations.

Nothing sits under the strip unless a cleaned-area tint or a target zone is
being drawn, and compositing over a fully transparent canvas is the identity
(extend_swath_layer leaves uncovered pixels at exactly (0,0,0,0)). So the
usual frame now starts from a copy of the strip, and the built-up path uses
the module-level alpha_composite(), which runs the same C routine in one
pass. Output is unchanged in every branch.

Measured on the target host (HAOS x86_64, 4 cores, Pillow 12.2, live map and
live accumulators, 30 back-to-back render pairs): 548 ms -> 509 ms median,
44 ms saved per frame, decoded-pixel hashes identical.
Map render: start the overlay frame from a copy of the accumulated vacuumed
strip instead of allocating a canvas and compositing the strip onto it, and
use the module-level Image.alpha_composite in the zone branch instead of the
in-place method (Pillow implements it as crop + composite + paste, three
passes over a 13.9 MB buffer).

Output is byte-identical, verified pixel-for-pixel on real map data. About
44 ms less per rendered frame on a Home Assistant host, roughly -8%.
@sytchi
sytchi merged commit f116823 into master Jul 30, 2026
6 checks passed
@sytchi
sytchi deleted the 5a23442-map-render-off-loop branch July 30, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant