ADR-007: Canonical visual assets use PNG and a bounded manifest

Date: 2026-07-20

Status: Accepted

Context

v0.2.0 introduces the first visual frontend. The project needs enough real visual assets to try the pet as an application, but it also needs to avoid creating separate source files for every future frontend.

Future targets may want different runtime formats. A desktop frontend may load PNG files. An embedded frontend may eventually need generated C arrays, packed binary frames, or display-specific colour formats. Treating all of those as equal source assets would multiply maintenance work and make visual changes harder to review.

The Core must remain independent from assets. It exposes presentable product facts through PetSnapshot, not file names, image identifiers, animation identifiers, or renderer commands.

There is a second asset category that this release must not forget. Menu graphics, button states, icons, panels, cursor details, fonts, and other application chrome are UI assets rather than pet presentation assets. They are related to the same source-of-truth problem, but they do not map from Core snapshots to pet roles and animations.

Decision

PNG is the canonical raster source format for visual assets in v0.2.0. The first style may be simple and may include pixel art, but the asset contract is visual/raster assets rather than pixel-art-only.

Canonical pet presentation assets live once under assets/pet/. Runtime copies and future embedded binary forms are generated artefacts. They are not the source of truth and must not be edited as the canonical visual state.

A small bounded project manifest maps visual asset identifiers, animations, and presentation roles. The initial manifest is a project-owned contract, not a user modding interface. Parsing stays in the shared components/manifest component, and frontend-neutral asset-set resolution stays in a shared components/assets component. A frontend asks that layer for accepted manifest records or a fallback-required result during initialisation or an explicit reload point, never during ordinary rendering.

The manifest must define:

  • schema version
  • image identifiers and relative paths
  • animation identifiers
  • frame order and duration
  • role-to-animation mappings
  • fallback image, fallback animation, and fallback role

The parser rejects unsupported versions, unknown directives, duplicate identifiers, invalid references, path traversal, excessive lengths, and excessive counts. Stored entries and parsing work are bounded by documented limits.

If the manifest or an external asset is missing or invalid, the frontend uses a calm built-in or procedural fallback presentation. The fallback path is part of normal behaviour, not an error screen.

SDL-specific image decoding and resource ownership stay inside the SDL frontend. The shared asset layer may know that PNG is the canonical source format and may resolve relative paths, but it must not expose SDL types or make SDL a dependency for future embedded frontends.

No frontend-neutral PNG decoder is written for this release, because the pinned SDL already decodes PNG and a second decoder would have no consumer. The decoding of the SDL frontend is therefore kept in one file so that the absence stays cheap to correct: if another frontend or an embedded target later needs the same canonical images without SDL, a shared component such as components/png becomes the decoder and the frontend swaps an implementation rather than a contract.

This manifest covers pet presentation assets only. UI and menu assets should follow the same canonical-source principle when they are introduced, and PNG remains the default source format unless a later ADR supersedes it. They need a separate UI asset contract because their states come from app navigation, focus, pointer hover, selection, text, localisation, and layout rather than from PetSnapshot presentation roles. v0.2.0 records that future layer but does not implement a menu asset pipeline.

Consequences

The project can keep one small canonical visual asset set while still allowing target-specific formats later. A future embedded converter can derive C arrays or binary blobs from the same source without forcing the desktop frontend to use embedded formats.

PNG gives lossless images, alpha, broad authoring tool support, straightforward review, and direct desktop runtime loading through the selected frontend dependency path. It is a source and desktop runtime convenience, not an embedded runtime requirement.

The manifest keeps renderer code from hard-coding Core states to filenames. A snapshot maps to a presentation role, the role resolves to an animation, and the animation selects a frame. New presentable Core states can fall back calmly until the asset set is updated.

The price is a small parser, a frontend-neutral asset loading contract, and one frontend-specific adapter for SDL image resources. Keeping the manifest line-oriented, bounded, and project-owned avoids introducing a general JSON parser or another runtime dependency into C frontend code for this release, while the asset reader boundary keeps desktop filesystem assumptions out of future embedded frontends.

No Python image dependency is required for v0.2.0 runtime or gates. If a later converter uses Pillow or another tool, that dependency must be recorded as tooling, not as a Core or frontend runtime dependency.

Menu and UI work does not get hidden inside the pet manifest. A later menu PBI can reuse the PNG source decision while still designing the extra layer it actually needs: focus state, hit regions, text and font policy, localisation, button state, and navigation state.

Alternatives considered

One source asset per frontend. Rejected because it multiplies maintenance immediately and makes future embedded work diverge from desktop visuals.

BMP as canonical source. Simpler to parse, but rejected because alpha and common art workflow support are worse for this product. BMP may still be a generated target format if a future platform needs it.

Generated .bin as canonical source. Rejected because it is difficult to inspect and edit. It may be a generated embedded artefact later.

JSON manifest parsed in C. Familiar and useful for tooling, but rejected for the initial runtime manifest because the project does not yet have a C JSON dependency and this manifest is small enough for a bounded project grammar. JSON remains appropriate for the composition registry handled by Python and CMake tooling.

Hard-code snapshot states to image paths. Fastest first render, rejected because it couples the renderer to Core enum growth and makes missing future states a code change instead of a mapping or fallback update.

Put menu and chrome assets in the pet manifest. Rejected because menu state is not derived from PetSnapshot. It would make the first manifest look reusable while mixing two different state models.

No external assets, fallback only. Useful for smoke tests, rejected as the whole release shape because the user needs to try real visual assets and the architecture needs to prove the asset contract.

References