Skip to content

PoC: introduce packed resources format#596

Closed
ngxson wants to merge 2 commits intocrosspoint-reader:masterfrom
ngxson:xsn/resources_fs
Closed

PoC: introduce packed resources format#596
ngxson wants to merge 2 commits intocrosspoint-reader:masterfrom
ngxson:xsn/resources_fs

Conversation

@ngxson
Copy link
Contributor

@ngxson ngxson commented Jan 28, 2026

Summary

Currently, some features like custom fonts cannot be implemented via SD card because the read speed will be quite slow.

This PR introduce packed resources format that is inspired by some STM32 smartwatch / smartband firmwares. One of my past (hobby) work was also based around this idea: https://github.com/ngxson/amazfit-bip-web

Instead of having a fully functional FS like spiffs or littlefs, we simply pack the resources into one big binary, then have a header table to tell where to find the resource.

Memory layout is simple:

--- header section ---
magic number
file0: name, type, size
file1: name, type, size
...
fileN: name, type, size
--- file1 data ---
...
--- file2 data ---
...
...
--- fileN data ---
...

The key benefit is that it allows using esp_partition_mmap to read the data, eliminate the need of copying it onto system RAM.

Included in this PR

  • The implementation of ResourcesFS
  • A python script to pack multiple files into resources.bin
  • A minimal demo of automatically flashing resources.bin into device's flash, then read a file from it

The expected user flow will be:

  1. The project can provide a list of pre-made resources, similar to https://amazfitbip.ngxson.com
  2. User choose the resources to be packed, and download it to SD card (file name: resources.bin)
  3. Upon boot, resource will automatically be flashed

Alternatively, we can also leverage the web flasher to flash it directly to spiffs partition.

Caveats

The biggest caveat is that the ESP32's MMU only has a limited number of pages to be used for mmap'ing. There are a total of 256 pages (= 16MB), but some are data-only some are instruction-only:

  • Data page can be accessed with byte-aligned access (meaning you can read from everywhere)
  • Instruction page must be accessed with 4-byte alignment, so you cannot access, for example basePtr + 0x1

Currently, with the version on master branch, we have (31 app + 31 inst) = 62 pages available (= ~3.9MB, each page is 64KB). This PR uses an addition of 28 data pages, leaving 3 pages left. This is currently hard-coded, but we can modify such that the number of pages can be control more dynamically.

Additional Context

Please note that I open this PR mostly for discussion (CC @daveallie )

Demo:

  • Create a file named test.txt
  • Run python scripts/make_resources.py -o resources.bin test.txt:FONT_REGULAR
  • Copy resources.bin to SD card
  • Power on the device, observe Serial logs

AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? --> Only for writing python code

@Uri-Tauber
Copy link
Contributor

Hi @ngxson. Can you elaborate more why reading font files from the SD card (Like #455, for example) is too slow?
Thanks.

@ngxson
Copy link
Contributor Author

ngxson commented Jan 28, 2026

@Uri-Tauber I haven't tested the mentioned PR, but it should be pretty understandable why SD card can be slow.

Each time you read even a small byte from SD, the program actually does:

  • Call to the FAT file system
  • The FAT file system figure out where is the block
  • It sends a read command to SD card
  • SD card controller process it, search for the block (1 block = 512 bytes), then respond
  • Read data is copied back into RAM

It is common knowledge that SD card is not optimized for random read/write.

My implementation rely on mmap which is backed by ESP32's MMU, it has the same performance as accessing random data inside the firmware.

@Uri-Tauber
Copy link
Contributor

@ngxson if I’m not mistaken, #455 implements an LRU (Least Recently Used) cache. Since most books are written in a single language (two at most), we can reasonably assume that — for example, as in real @ruby-builds sets it to 30 — having 128 distinct characters is sufficient to render an entire book.

If we assume each character is a 1-bit 16×16 pixel grid, the total cache size would be about 16 KB.

On the other hand, while your implementation is more memory-efficient, it makes things harder for the average user who just wants to add their favorite fonts and doesn’t know — and doesn’t want to know — anything about manual flashing.

@ngxson
Copy link
Contributor Author

ngxson commented Jan 28, 2026

On the other hand, while your implementation is more memory-efficient, it makes things harder for the average user who just wants to add their favorite fonts and doesn’t know — and doesn’t want to know — anything about manual flashing.

I assume you haven't fully read my OP:

The expected user flow will be:
1. The project can provide a list of pre-made resources, similar to https://amazfitbip.ngxson.com
2. User choose the resources to be packed, and download it to SD card (file name: resources.bin)
3. Upon boot, resource will automatically be flashed

@ngxson
Copy link
Contributor Author

ngxson commented Jan 28, 2026

having 128 distinct characters is sufficient to render an entire book.

It's clearly not sufficient at least in my native language, vietnamese. I imagine it will get worse with japanese for example, where the number of grapheme is massive even when compared to chinese

edit: hmm, probably a bad example because all vietnamese and japanese usually has a quite high number of repeatedly used grapheme. I guess this only affect chinese

@Uri-Tauber
Copy link
Contributor

@ngxson Sorry. I missed that.

@Uri-Tauber
Copy link
Contributor

I'm not very familiar with Chinese, but from a quick search, it looks like there are over 20,000 glyphs just in the CJK Unified Ideographs block of Unicode (U+4E00–U+9FFF). How would your approach handle this? Using two or three fonts could easily fill up the entire flash.

@ngxson
Copy link
Contributor Author

ngxson commented Jan 29, 2026

The CJK block covers multiple Asian languages (and even covers obsolete one like Chu Nom). In practice, a human never use the whole CJK, so it will make more sense to separate it by language.

In my experience, most memory-constraint system either support chinese+english, japanese+eng combinations, or easten-only language, I never seen one that support both chinese(simplified) and japanese at the same time.

But yes this may worth considering. I haven't looked specifically into japanese but I think it can be hard to fit the whole katakana+harigana+kanji into flash, it's basically massive. That can be the only blocking point for my approach

edit: in practice, katakana and harigana are pretty small, around 200 graphemes. I've done my research and concluded that ~6000 graphemes are needed to cover japanese's kanji

@ngxson
Copy link
Contributor Author

ngxson commented Feb 1, 2026

After some more consideration, I think my approach is not good as-is. While it works on some other memory-constraint devices, it mostly because they don't have access to a sdcard.

Serving fonts from sdcard has some performance issues as discussed, but I think it worth the trade-off because we don't usually render too many texts at once anyway.

I'm closing this PR as I don't plan to continue it. Will focus my efforts on other parts of the project.

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