ADR-019: Embedded asset pack version 1 is a flat little-endian artefact compiled from the canonical manifest
Date: 2026-08-07
Status: Accepted
Context
v0.5.0 puts the canonical pet on a board. The device has no filesystem, no manifest parser, and no
PNG decoder, and the canonical asset set is a text manifest and five PNG files that a desktop reads
at run time. Something has to turn the second into the first, and the artefact it produces is the
thing every later reader on the board depends on.
The release has already decided the surrounding shape. Section 8.3 of
the version architecture says the pack is a flat binary carrying a magic
value, a format version, the compiled presentation structure, an entry table, and the frame payloads,
and section 8.4 says the compatibility promise is narrow. ADR-015 decided that the manifest grammar
eventually has one reader and that the reader is the converter.
What was not decided is the layout itself. Field widths, byte order, alignment, the transparency representation, the acceptance rules, and the converter’s own contract are what this ADR fixes, ahead of the code that writes and reads a pack, so that neither program is the definition of the format.
Decision
The pack is one flat binary artefact holding a 28-byte header, four presentation tables, an entry table, the frame payloads, and a trailing CRC-32. Every integer field is unsigned and little-endian, every record and payload starts on a four-byte boundary, and the layout is recorded field by field in the pack contract, which this decision points at rather than repeats.
Six choices carry the decision.
The pack is the manifest compiled, not a second authoring surface. The converter reads
assets/pet/manifest.petasset and writes its images, animations, frames, roles, and fallback into
the pack with image indices where the text carried paths. The canonical sources stay the single
source of truth and nothing about the presentation is authored twice.
The compiled structure materialises into a PetManifest. The reader fills the same structure the
desktop parser fills, so the device runs pet_presentation_select_frame unchanged and role
selection, animation timing, and the fallback chain behave identically on both. The pack carries the
image identifiers it does not strictly need, because that is what makes the two structures comparable
field for field and turns drift into a failing test.
Pixels are RGB565 written high byte first, while every other field is little-endian. The pixel order is the one an ST7789 consumes over SPI, so the classic backend transfers a composed buffer without touching a pixel. The mixed order is declared in the header rather than left implicit, so a panel that disagrees converts in its own backend and the disagreement is visible.
Transparency is a one-bit mask beside the colour payload. A 64 by 64 sprite costs 512 mask bytes and the colour payload stays uniform, aligned, and directly transferable.
Alignment is part of the format. The reader answers a lookup with a pointer into the pack and the frontend reads pixels in place, and the Xtensa core the release targets wants aligned loads, so four-byte alignment is a property of the artefact rather than something a reader arranges afterwards.
The integrity check is the one the project already uses. A trailing CRC-32 as used by IEEE 802.3 over everything before it, which is the placement and the polynomial ADR-013 fixed for the save payload. One project, one way of saying that a binary artefact is undamaged.
The compatibility promise is exact version matching with no migration, which the architecture already recorded and the contract states in full. It is narrower than the save format’s promise because a pack is a build output that ships inside the image that reads it, while a save payload is user data that must outlive the process that wrote it.
The converter contract is decided with the format. It accepts PNG at bit depth 8, colour type 6 or 2, without interlacing, reduces each channel by discarding its low bits, treats an alpha of 128 or more as opaque, is deterministic, depends on nothing outside the Python standard library, and leaves no file behind when it fails.
Consequences
The canonical set compiles to about 43 KiB, which is a little under a quarter of the free application
partition at the close of E33. A larger sprite set is bounded by the partition rather than by the
format.
Adding a field is a format revision rather than an accident, and the header holds no reserved space that would invite a quiet one. That is the intended cost of the narrow promise.
A field-by-field writer and a field-by-field reader must each be reviewed against the contract table
rather than trusted to agree with a structure definition. That review is deliberate work in PBI-118
and PBI-120.
The mask has no consumer until PBI-121 composes from a pack frame. It enters the format now anyway,
because adding it later would be a version change for a device that had already shipped one.
The CRC-32 costs one pass over about 43 KiB when a pack is accepted, computed bitwise so that no lookup table reaches flash. On the target class this is a few milliseconds once at boot, against a lookup table this Core has already chosen not to carry.
Alternatives considered
Pre-composing the sprites onto a background colour. Rejected because it bakes a presentation decision into the artefact. The background belongs to the frontend and to the panel it draws on, and a pack that had already chosen one could not be reused by a second frontend or a second panel.
A chroma key instead of a mask. Rejected because it removes a colour from the palette and produces fringes along the edges the canonical art already antialiases. The mask costs one sixteenth of the colour payload and has neither problem.
An 8-bit alpha channel per pixel. Rejected because it costs half as much again as the colour payload for a blend the frontend does not perform. The composition writes opaque pixels into a frame buffer, so anything beyond opaque or not opaque would be stored and ignored.
Carrying PNG files in the pack and decoding on the device. Rejected because it puts a decoder and an inflate window on a microcontroller to undo work a build machine has already done, and because decoding at frame time is exactly what an embedded presentation cannot afford.
Compressing the pack. Rejected because the reader answers a lookup with a pointer into read-only memory the image already holds. A compressed pack would have to be expanded into RAM the device does not have to spare, and it would trade the in-place property for space in a partition that is not short.
Copying a C structure into the artefact. Rejected for the same reason ADR-013 rejected it for
the save payload. Padding, alignment, enumeration width, and host endianness would become part of the
format, and the artefact would be valid only for the compiler that wrote it.
A generated C array source instead of a binary file. Rejected because it makes the pack a source artefact that a compiler must chew through on every build, and because a binary file is what lets the hosted reader tests read the same bytes the device does.
Text or JSON for the compiled structure with binary payloads beside it. Rejected because it puts a parser back on the device, which is the thing the pack exists to remove.
No integrity check, relying on offset and length validation. Rejected because structural validation cannot see a flipped bit inside a valid payload, and because the project already answered this question for its other binary artefact. Consistency here is worth more than the few milliseconds the check costs.
Per-role packs or one file per image. Rejected because it multiplies the embedding, the validation, and the failure modes to save nothing. One artefact is accepted whole or not at all, which is the same discipline the asset set already follows.
References
- SPEC-FR-025, SPEC-FR-026, SPEC-NFR-002
- Architecture for v0.5.0, sections 5.5, 5.7, 8.3, and 8.4
- The pack contract and the manifest contract
- ADR-007, ADR-013, ADR-015
PBI-117,PBI-118,PBI-119,PBI-120,PBI-121