Define Encoded Sprite (0x08): indexed, palette-swap, and deflate sprite payloads - #242
Draft
SolbiatiAlessandro wants to merge 3 commits into
Draft
Define Encoded Sprite (0x08): indexed, palette-swap, and deflate sprite payloads#242SolbiatiAlessandro wants to merge 3 commits into
SolbiatiAlessandro wants to merge 3 commits into
Conversation
…lette-swap payloads
Define Sprite (0x01) ships every sprite as raw RGBA under Snappy: 4 bytes per
pixel before compression, and Snappy finds little in flat 16-color art. A
748x941 map with 16 colors costs 685 KB per copy on the wire, and a game that
pre-composites five dusk tints of it pays that six times.
This adds one server-to-client message, Define Encoded Sprite (0x08). It has
the fields of Define Sprite plus an encoding byte before the payload length:
0x00 rgba-snappy the legacy payload, unchanged
0x01 rgba-deflate zlib over raw RGBA
0x02 indexed u8 count-1, count*4 RGBA palette, zlib over one index
byte per pixel (sprites with at most 256 colors)
0x03 palette-swap u16 source sprite id, u8 count-1, count*4 RGBA palette;
reuses the index plane of an indexed sprite the client
already holds
Define Sprite keeps working unchanged; a client that has not been updated only
breaks if a server sends 0x08 to it, which is the same rule as for any new
message type.
Server side: addEncodedSprite picks indexed or rgba-deflate per sprite;
addPaletteSwapSprite emits a swap when the new pixels are a per-color
recoloring of the source and falls back to a normal sprite otherwise.
parseSpritePacket returns 0x08 as spkSprite with the encoding and payload;
decodeSprite turns any definition (legacy included) into straight RGBA and
keeps the index plane for later swaps.
Clients: the shared browser clients load client/spritecodec.js (a small
zlib/deflate inflater plus the palette expansion) and parse 0x08 next to 0x01;
the native and wasm global client decode through spriteprotocol.decodeSprite
with zippy. client.nim serves spritecodec.js on the same routes as
snappyjs.min.js.
Tests: tests/test_spriteencoding.nim round-trips every encoding
pixel-identically and covers the swap fallback, message sizes, and malformed
payloads; tests/test_spritecodec_js.nim runs the browser decoder under node
against packets the Nim encoder wrote (skips when node is absent);
tests/test_client.nim pins the new routes and parser cases.
Measured on Heartleaf's director init packet (313 sprites): 7,487,421 bytes
with Define Sprite, 1,083,170 bytes with Define Encoded Sprite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Proposal + prototype: a compressed sprite definition message. Heartleaf's director view ships a 7.5MB init packet for 600KB of source art, because Define Sprite (0x01) carries raw RGBA under snappy and the game pre-composites 20 tinted copies of 4 base sprites. This branch adds Define Encoded Sprite (0x08) and takes that packet to 1.08MB with pixel-identical output.
Message.
0x08= the Define Sprite fields plus au8 encodingbefore the payload length. Encodings:0x00rgba-snappy (legacy payload),0x01rgba-deflate (zlib over RGBA),0x02indexed (≤256 colors: palette + zlib over one index byte per pixel),0x03palette-swap (u16 source sprite id + a palette; the client reuses the source's index plane).0x07was left alone because stag_hunt uses it.Decoders. Browser:
client/spritecodec.js(synchronous, pixel-exact; used by global_client.html and player_client.html). Native/wasm:client/global_client.nimparseMessagecase0x08.parseSpritePacketreturns0x08asspkSpritewith anencodingfield (legacy0x01parses as encoding0x00);spriteMessageBytesknows0x08.Why not PNG. Same inflate work in JS plus chunk/filter parsing, no way to express a palette swap, and browser-native decode is asynchronous and not byte-exact for translucent pixels (canvas premultiplies).
Results on heartleaf's init packet (
tools/init_packet_report.nimon the heartleaf side): 7,487,421 → 1,083,170 bytes. Main map bottom 685KB → 190KB (indexed); its five dusk tints 5×680KB → 5×81 bytes (palette-swap); overhang 164KB → 14KB; forest underlay 1.7MB → 767KB (rgba-deflate; 1629 colors, so no palette). The remaining packet is 71% forest; quantizing it to 4 bits/channel would give ~306KB (a heartleaf art decision).Compatibility.
0x01is unchanged and still tested. Old clients close on0x08under the existing unknown-type rule; games ship their own clients, so a game opts in by emitting0x08. No change to client→server messages, replays, or certification tools (they do not parse sprite packets).Tests.
test_spriteprotocol(existing) passes; newtest_spriteencoding(round trips for every encoding, chained swaps, fallback, sizes, malformed input) passes; newtest_spritecodec_jsruns node againstclient/spritecodec.json 11 sprites covering every encoding plus legacy: 0 failed;test_clientpasses; the native client compiles.Full write-up with the measurement tables and open points (0x07 vs 0x08 numbering, whether to keep rgba-snappy inside 0x08, hand-written inflater vs DecompressionStream, a sprite scale factor for large backgrounds):
docs/compressed_sprites_proposal.mdon this branch.Companion heartleaf branch that emits
0x08: SolbiatiAlessandro/coworld-heartleafcompressed-sprites(pins this branch's commit in nimby.lock).🤖 Generated with Claude Code