Skip to content

Artwork role: back-pressure, timing offset, and lateness reporting#97

Merged
kahrendt merged 9 commits into
mainfrom
artwork-frame-done
Jul 20, 2026
Merged

Artwork role: back-pressure, timing offset, and lateness reporting#97
kahrendt merged 9 commits into
mainfrom
artwork-frame-done

Conversation

@kahrendt

Copy link
Copy Markdown
Contributor

Adds three opt-in artwork role features aimed at smooth cross-fades on ESPHome displays, all backward-compatible (defaults preserve today's decode-and-display-every-frame behavior) beyond a function signature breaking change.

Changes

  • Per-slot frame_done acknowledgment (opt-in back-pressure): Set ImageSlotPreference::require_frame_done and the role delivers at most one un-acked "delivery" (a frame or a clear) at a time for that slot; newer payloads are buffered latest-wins until the consumer calls ArtworkRole::frame_done(slot). A clear supersedes any un-acked frame. Lets a consumer hold the gate open for the duration of a cross-fade animation instead of having buffers reused mid-fade. No timeout by design — the ack is the contract.

  • Per-slot display_offset_ms: Shifts a slot's display deadline (positive fires early, mirroring PlayerRoleConfig::fixed_delay_us) so a cross-fade can straddle the track boundary.

  • Display lateness reporting: on_image_display() now takes a lateness_ms argument reporting how far past the deadline it fired, so a consumer can shorten a fade to still end on schedule, or snap instantly when joining mid-track. lateness_ms == 0 is reserved for the no-connection case; a connected on-time display never reports 0.

New tests/test_artwork_role.cpp covers the gate state machine (hold/supersede/clear/restart/reentrant-ack), the deadline/offset arithmetic, and the lateness mapping.

Breaking change

on_image_display(uint8_t) signature is now on_image_display(uint8_t, uint32_t lateness_ms).

kahrendt added 7 commits July 14, 2026 12:31
Adds a back-pressure gate so a consumer can hold the next artwork
delivery until it finishes presenting the current one (e.g. a
cross-fade that needs both decoded frames alive). Opt-in per slot via
ImageSlotPreference::require_frame_done; acknowledged with the new
ArtworkRole::frame_done(slot).

While a delivery is un-acked, the decode thread parks the newest
notification (latest-wins) instead of decoding it and replays it once
the gate reopens, reusing the existing generation/epoch validation. A
clear counts as a delivery owing its own ack, since it may drive a
fade-out; a stream restart auto-releases a frame that was decoded but
never displayed so the slot cannot wedge. frame_done wakes the decode
thread via a sentinel notification, with the 100ms receive timeout as
fallback.

Default behavior (require_frame_done false) is unchanged.
Lets a consumer fire on_image_display() earlier (or later) than the
server's display timestamp, so a cross-fade can straddle the track
boundary: with a 2 s fade, an offset of 1000 starts the fade 1 s
before the boundary. Positive-equals-earlier mirrors
PlayerRoleConfig::fixed_delay_us.

The offset is applied at the drain_events() deadline check via a pure
display_deadline_reached() helper, keeping the no-connection sentinel
(fire immediately) ahead of the shift and the held timestamps in true
server time. Best-effort: an image that arrives or decodes after the
shifted deadline fires as soon as it is ready, matching existing
past-timestamp handling.
Displays are best-effort: an image that arrives or decodes after its
(offset-shifted) deadline fires as soon as it is ready. Passing the
slip as a lateness_ms parameter lets a consumer compensate, e.g.
shorten a cross-fade by the lateness so it still ends on schedule, or
snap instantly on a huge value such as joining mid-track where the
artwork timestamp is long past.

Replaces the display_deadline_reached() helper with display_overdue_us(),
whose >= 0 result doubles as the reported lateness (0 for the
no-connection fire-immediately path, where no deadline exists).

Breaking listener signature change, made before the v0.7.0 tag freezes
the API: on_image_display(uint8_t) -> on_image_display(uint8_t, uint32_t).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances ArtworkRole (a main-loop scheduled display role with a decode thread) with three opt-in features aimed at smoother UI cross-fades: per-slot back-pressure via an explicit frame_done() acknowledgment, a per-slot display timing offset, and lateness reporting on display callbacks. It also introduces a breaking API change to ArtworkRoleListener::on_image_display() to carry lateness_ms.

Changes:

  • Add an opt-in per-slot ack gate (ImageSlotPreference::require_frame_done) and ArtworkRole::frame_done(slot) to enforce “one un-acked delivery at a time” with latest-wins buffering.
  • Add per-slot display_offset_ms and compute/report lateness_ms to on_image_display(slot, lateness_ms).
  • Add a new test_artwork_role.cpp suite covering the ack gate state machine and display-deadline/lateness arithmetic.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_artwork_role.cpp New unit tests validating ack gate behavior, restart/clear interactions, and offset/lateness pure helpers.
tests/CMakeLists.txt Adds the new artwork role test target source.
src/artwork_role.cpp Implements ack gate, parked-notification replay, deadline offsetting, and lateness reporting; adds frame_done().
src/artwork_role_impl.h Adds ack gate state, parked notification storage, sentinel wake slot, and new helper declarations.
include/sendspin/config.h Extends ImageSlotPreference with require_frame_done and display_offset_ms.
include/sendspin/artwork_role.h Updates listener API to on_image_display(slot, lateness_ms) and documents the ack-gate contract; adds frame_done().
docs/internals.md Updates internals docs for offset/lateness computation and documents the ack-gate state machine.
docs/integration-guide.md Updates integration docs/examples for new on_image_display signature, lateness semantics, and back-pressure usage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/artwork_role.cpp
Comment thread src/artwork_role.cpp
@kahrendt
kahrendt merged commit 254603c into main Jul 20, 2026
5 checks passed
@kahrendt
kahrendt deleted the artwork-frame-done branch July 20, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants