Fix sprite wire format desync; add device-free damage repaint - #3
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three latent wire-format bugs in
SpriteSurface, plus the missing erasepath. All host-verified; none of it has touched hardware.
Verified against
brilliant_msg7.1.1 —tx_sprite.py,tx_sprite_coords.py,lua/sprite.lua. CLAUDE.md pinned 7.0.0; 7.1.0was explicitly a "True-up against Halo firmware 0.8.8", so the SDK is
tracking the same firmware revision this project reads.
The one that matters
Palette length is load-bearing and was not being enforced.
sprite.luaslices the palette as exactlynum_colors * 3bytes andtreats everything after it as pixel data.
SpriteSurfacesentbytes(palette)whole, regardless ofnum_colors.This works today only by coincidence:
ramp_palette(4)is exactly 12bytes, which happens to match. Any other palette shifts every pixel in
the frame.
It cannot fail host-side —
PILSurfacenever readspalette_data— sothe suite would stay green while the glass showed garbage, and it would
present as a wire-format problem rather than a palette-length one.
The other two
_next_code()derived fromlen(self.ops), andpresent()never clears the log, so codes ranpast
0xFFand wrapped onto sprites still live on the device. Nowcycles within
base_code..0xFF, withreset_codes()for a cleareddisplay.
num_colorscould be a value the packer cannot express.max(2, len(palette) // 3)yields e.g. 5, whichTxSprite.pack()encodes as 4bpp while declaring 5 — the same slice desync. Now rounded
to {2, 4, 16}, rejected above 16.
Nothing erased
blit_coveragewrites ink through a mask, so a re-rendered shorter lineleft the previous tail on the glass. Neither backend recovered:
SpriteSurface.present()is a no-op,PILSurface.present()only clearsdirty.RetainedSurfacebuffers a frame, diffs by value against the lastcommitted frame, and forwards only the damaged regions — filling
background where something was removed, then replaying every op
intersecting the region in draw order. Replaying only the changed op
takes overlapping static content with it.
It wraps either backend and contains no device call, so rule 1 holds.
damage.pyis named apart fromgeometry.pydeliberately: one is theshape of the glass, the other is what changed since last frame.
Also relevant to rule 2 — Halo firmware 0.8.8 registers
display.show()as a no-op with no back buffer, so a clear-and-repaint is a full-field
luminance transient. Differential update is what keeps that off the
glass.
What this does not fix
The x/y origin remains unconfirmed and is not resolvable by reading
the SDK.
sprite_coords.luaonly parses the fields; what they mean isdecided by the app-side Lua calling
frame.display.bitmap, which is1-based on Halo.
TxSpriteCoordsdocuments x as 1..640 — Frame's panel,the same stale bound as
TxPlainText. Needs a device.Tests
251 passing, up from 235. 16 new in
tests/test_retained.py, splitbetween wire-format invariants and retained-mode behaviour over both
backends. The surface-agreement test in
test_glanceable.pystillpasses