Skip to content

Pack the unit sprites into sheets instead of 2,816 files - #229

Draft
Giszmo wants to merge 4 commits into
codex/unit-animation-32-framesfrom
feat/unit-sprite-sheets
Draft

Pack the unit sprites into sheets instead of 2,816 files#229
Giszmo wants to merge 4 commits into
codex/unit-animation-32-framesfrom
feat/unit-sprite-sheets

Conversation

@Giszmo

@Giszmo Giszmo commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Builds on #201. Base branch is codex/unit-animation-32-frames, so the diff below is only this change.

#201 ships the 32-pose animation set as 2,816 PNGs because Sprite::load had no other option: it probed <name><i>.png / <name><i>r.png until both were missing, so one file per pose and layer was the only layout the loader understood. This teaches the loader to read sheets and packs the set into eleven of them.

Format

data/gfx/unit.sheet sits next to the sheets and names them:

# Sprite sheet index for data/gfx/unit, read by GAGCore::Sprite::load.
# One line per sheet: <file> <image|rotated> <first frame> <frames> <tile width> <tile height>
# Tiles are packed row-major; the column count follows from the sheet's width.
unit-explorer-r.png rotated 0 256 32 32
unit-worker-walk-r.png rotated 256 256 38 38
unit-worker-walk.png image 256 256 38 38
...

Sprite::load reads <name>.sheet when it exists and cuts the tiles itself; otherwise it falls back to the per-frame loop unchanged. Terrain, buildings, GUI sprites and any modded data directory keep working exactly as before. Eleven sheets rather than seven because the sets have three different tile sizes (explorer 32, worker 38, warrior 40) and the shadow pass is a separate layer.

Cost

#201 with sheets
files 2,816 11 + index
PNG bytes 3,230,920 2,599,736 (-19.5%)
allocated, ext4 4 KB blocks 11.0 MiB 2.5 MiB (-77%)

The byte saving is one deflate window covering a whole 256-pose animation instead of 256 separate streams. The allocation saving is the median 1.1 KB frame no longer rounding up to a 4 KB block.

Decoded surfaces are unchanged — still 15.9 MiB of source surfaces plus the per-team-colour recolor clones — and the composite cache is untouched. This is a disk-and-startup change, not a memory one.

Validation

  • Every tile is byte-identical to the frame it replaces. All 2,816 frames were cut back out of the sheets and compared to the originals from this branch: 2,816/2,816 exact, alpha included.
  • UnitCompositeCacheTest reports identical numbers loading from sheets and from the per-frame files: mean=5.37923 max=10, bytes=76099904 entries=13172. Same for UnitMotionBlurTest.
  • New test/UnitSpriteSheetCheck.cpp (scons unit-blur-tests): cuts synthetic sheets of known pixels and checks each pixel, checks mixed tile sizes and layer offsets, checks the per-frame fallback still loads when no index exists, checks a sheet too small for its index is rejected rather than half-loaded, and checks the shipped set's 1,792 frames, layers and tile sizes. Inverting the row/column term in the loader makes it fail.
  • test_render.py gains a pack round-trip over the full frame set; inverting the row/column term in the packer makes it fail. All 5 pass.
  • test/TestsRunner: OK (172 tests).
  • Real game, glob2 -test-games under Xvfb for 90 s: units render, recolor and cast shadows as before.

One fix outside the loader

data/gfx/SConscript packed and installed only filenames containing .png, so unit.sheet would have been silently dropped from dist tarballs and scons install — and an installed game would then have found neither an index nor per-frame files. Verified with scons INSTALLDIR=… install.

🤖 Generated with Claude Code

genixpro and others added 4 commits September 7, 2026 23:50
Sprite::load probed <name><i>.png and <name><i>r.png until both were
missing, so the 32-pose set had to ship one file per pose and layer. It
now reads an optional <name>.sheet index first — one line per sheet
giving the layer, first frame, frame count and tile size — and cuts the
tiles out of the packed PNGs itself. Sprites without an index still load
frame by frame, so every other sprite and any modded data directory is
unaffected.

render.py install packs the staged frames into eleven sheets and writes
the index. The tiles are byte-identical to the frames they replace:
3,230,920 bytes in 2,816 files become 2,599,736 in 11, and 11.0 MiB of
allocated ext4 blocks become 2.5 MiB, because a single deflate window
covers a whole animation and the frames stop rounding up to a block
each. Decoded surfaces and the composite cache are unchanged.

data/gfx/SConscript packed and installed only names containing ".png",
which would have dropped the index from dist tarballs and scons install.

Opus 5 helped authoring this commit.
@genixpro
genixpro force-pushed the codex/unit-animation-32-frames branch 2 times, most recently from 271a316 to 22534a7 Compare September 9, 2026 22:48
@Giszmo
Giszmo marked this pull request as draft September 10, 2026 19:10
@Giszmo

Giszmo commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

This is a chore we can do at some point. It's worth it but the target is moving, so ... draft for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants