Skip to content

[experiment] perf: pack icons into one flash blob with table-driven lookup (-4KB flash) - #154

Merged
thomasjball merged 2 commits into
mainfrom
humanapp/icon-blob
Sep 16, 2026
Merged

thomasjball merged 2 commits into
mainfrom
humanapp/icon-blob

Conversation

@humanapp

Copy link
Copy Markdown
Contributor

Pack icons into one flash blob with table-driven lookup (-4KB flash)

What this change is

Icon storage and lookup are restructured. Before: ~140 icons each lived in their own export const x = bmp... in assets.ts, and icons.get resolved ids through a ~150-branch if/else chain. After: all mapped icons' pixel data is packed into a single flash buffer (ICON_BLOB) in a new generated file, icon-table.g.ts; lookup is a flat 256-byte tid -> icon index table plus a small name table; and Bitmap objects are materialized lazily from blob slices and cached. The mapping and the pixel data live in scripts/icon-map.json and scripts/icon-literals.json, and scripts/genicontable.js regenerates the whole thing.

Result: 479,676 -> 475,700 bytes (-3,976) on the hex image.

Where the savings come from

Not from the images (they're not RLE'd yet) -- flash holds the same bytes of bitmap data before and after. The savings are deleted per-icon code:

Component Before After Delta
icons.get if/else compare chain 2,486 B 476 B -2,010
Per-icon global init code in <main> (share of 11,344 B) -1,930
New lookup machinery -- 418 B +418
Literals (blob + tables vs 111 hexlits) 68,360 B 68,320 B -40

Each if (name == Tid.X) return icondb.y compiled to ~16-20 bytes; each icon const also cost ~17 bytes of startup code and a 9-byte buffer object header. The mapping now costs ~3 bytes of data per icon instead of ~35 bytes of code. (Interesting finding: A first attempt that kept the consts and added a lookup array came out 1.5KB LARGER: pxt constructs array literals element by element at runtime, so code references are the expensive thing, not data.)

Editing or adding an icon

Mapping lives in scripts/icon-map.json; pixels live in scripts/icon-literals.json. A bmp literal still present in assets.ts wins over the store. To add/edit an icon:

  1. Draw it in the Arcade pixel editor, copy the img\... output.
  2. Paste the body rows into scripts/icon-literals.json under a new name, quoting each row (the spaces the editor puts between characters are fine to keep).
  3. Add the mapping in scripts/icon-map.json -- "<tid>": "your_icon" in tidMap, or an entry in nameMap for string-keyed icons.
  4. node scripts/genicontable.js and rebuild.

Tradeoffs

  • Icons now copy into RAM on first display (~136B typical, ~14.5KB worst case if every icon is shown) where they were flash-referenced before. The cache never evicts; if RAM gets tight, add LRU eviction in iconByIndex.

Where RLE would plug in

Every stored icon now flows through one line in iconByIndex:

`bmp = bitmaps.ofBuffer(ICON_BLOB.slice(start, size))`

To add compression: (1) in the generator, RLE-encode each icon's payload before packing, keeping the 8-byte header readable; (2) in iconByIndex, decode into a RAM buffer and pass that to ofBuffer -- RAM cost is unchanged, since the slice already copies today. Measured on this icon set, nibble RLE (4-bit color + 4-bit run length per byte) shrinks the pixel data ~47%, i.e. another ~6.8KB, for a one-time ~150-byte decoder. Encoding choice matters: a naive 2-byte-per-run RLE saves almost nothing on these small, detailed icons.

Other potential size wins:

  • RLE the icons: ~-6.8KB.
  • Move the 10 sample-gallery bmp literals inside the TD_NOOP extraSamples body: unreachable on hardware, yet ~5.5KB of every hex.
  • RLE encode wordLogo / editorBackground images.

@thomasjball

thomasjball commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Move the 10 sample-gallery bmp literals inside the TD_NOOP extraSamples body: unreachable on hardware, yet ~5.5KB of every hex.

Not quite sure what you mean here; the literals are included even if not referenced?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It changes core icon lookup/storage behavior and introduces generated flash-packed assets with lazy caching, which should be validated via full build + on-device/sim UI smoke testing.

Pull request overview

This PR refactors MicroCode’s icon storage/lookup to reduce flash usage by replacing many per-icon bitmap constants and a large if/else dispatch chain with a generated, table-driven lookup over a single packed flash blob (icon-table.g.ts). This fits into the repo’s asset pipeline by moving most icon literals out of assets.ts and into generated data while keeping icons.get() as the stable public entry point.

Changes:

  • Add generator + source-of-truth mapping files (scripts/genicontable.js, scripts/icon-map.json) to produce icon-table.g.ts with packed icon data and lookup tables.
  • Update microcode.icons.get() to resolve numeric TIDs via a 256-entry table and string icon names via a compact name table, materializing Bitmaps lazily and caching them.
  • Register the generated file in pxt.json so it is compiled into the MakeCode build.
File summaries
File Description
scripts/icon-map.json Defines tid -> icon symbol and name -> icon symbol mappings used by the generator.
scripts/icon-blob-syms.json Generated registry/order of icon symbols used to build index-based lookup.
scripts/genicontable.js Node generator that dedupes/encodes bmp literals, packs the blob, and emits icon-table.g.ts.
pxt.json Adds icon-table.g.ts to the compiled file list.
icon-table.g.ts Generated packed icon blob + offset tables + lookup helpers (iconByIndex, name/tid tables).
assets.ts Replaces the large icon if/else chain with table-driven lookup and keeps runtime-drawn operator icons.
Review details
  • Files reviewed: 6/7 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@humanapp

Copy link
Copy Markdown
Contributor Author

Move the 10 sample-gallery bmp literals inside the TD_NOOP extraSamples body: unreachable on hardware, yet ~5.5KB of every hex.

Not quite sure what you mean here.

I confirmed that most of the sample program icons in fact appear in the hex. The extraSamples function itself is correctly excluded, but the icons it references are emitted. This is because even though functions tagged with TD_NOOP don't emit into native code, that is a filter that happens after the treeshake walk. Treeshake still traverses them and marks what they reference! The actual issue is the fact that extraSamples is referenced by icons.get, so the chain of custody for the sample icons maps to a node above extraSamples. A quick workaround would be to inline the sample images into the body of extraSamples rather than referencing them through icondb.

@humanapp
humanapp marked this pull request as ready for review September 16, 2026 22:55
@thomasjball
thomasjball merged commit b22ffbb into main Sep 16, 2026
2 checks passed
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.

3 participants