Skip to content

FatFs bridges: build a cluster link map on open - #194

Draft
rtissera wants to merge 1 commit into
masterfrom
fatfs-cluster-link-map
Draft

FatFs bridges: build a cluster link map on open#194
rtissera wants to merge 1 commit into
masterfrom
fatfs-cluster-link-map

Conversation

@rtissera

@rtissera rtissera commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Without a cluster link map, every backward f_lseek walks the FAT chain again from the file's first cluster — and every COMPRESSION_SELF hunk is a backward seek, to an earlier hunk the image references instead of storing twice. The cost grows with how far into the file the reader is, so a short test near the start never shows it.

On an ESP32-P4 a 29%-self-referenced image did not finish in 47 minutes until the benchmark built a map (070341a). The BL616 bridge that an integration copies never got that fix, and TangCore's FatFs is built with FF_USE_FASTSEEK 1 — the capability is there, nothing was using it.

What changes

chd_fatfs_open() builds the map right after f_open, before chd_open, so the header and map reads benefit too. The table needs two words per fragment plus one: it starts at 16 words, grows to exactly what FatFs reports it needs, and past CHD_FATFS_CLMT_MAX runs without a map rather than failing — slower backward seeks, still correct. Compiled out when FF_USE_FASTSEEK is 0.

The close callback frees the table. Since #195, libchdr calls it on a failed open too, so the bridge's error path has nothing left to close.

The RP2350 benchmark builds the same map behind BENCH_LINKMAP, so it can be measured with and without.

Checked

  • Compiles cleanly against TangCore's actual FatFs (bouffalo_sdk/components/fs/fatfs, R0.15) with FF_USE_FASTSEEK at 1 and at 0.
  • RP2350 benchmark builds both ways; the switch is real — +128 bytes of text and three extra f_lseek call sites with it on.

Not yet

Not measured on either board. The measurement that matters is a whole file with a high self-reference rate, map on and off — not a capped run, which is exactly what hid this on the P4.

🤖 Generated with Claude Code

Without one, every backward f_lseek walks the FAT chain again from the file's
first cluster - and every COMPRESSION_SELF hunk is a backward seek, to an
earlier hunk the image references instead of storing twice. The cost grows with
how far into the file the reader is, so a short test near the start never shows
it. On an ESP32-P4 a 29%-self-referenced image did not finish in 47 minutes
until the benchmark built a map (070341a). The BL616 bridge an integration
copies never had that fix, and its FatFs is built with FF_USE_FASTSEEK on.

chd_fatfs_open() now builds the map right after f_open, before chd_open, so the
header and map reads benefit too. The table is two words per fragment plus one;
it starts at 16 words, grows to exactly what FatFs reports it needs, and past
CHD_FATFS_CLMT_MAX runs without a map rather than failing - slower backward
seeks, still correct. Compiled out entirely when FF_USE_FASTSEEK is 0.

The close callback frees the table. libchdr calls it on a failed open too
(#195), so the bridge's error path has nothing left to close.

The RP2350 benchmark builds the same map, behind BENCH_LINKMAP so it can be
measured on and off. Not measured yet on either board.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01846EhHuAFk5qvxwEA5Gq6y
@rtissera
rtissera force-pushed the fatfs-cluster-link-map branch from cf60b8b to 9fcd4bc Compare September 10, 2026 23:04
@rtissera

Copy link
Copy Markdown
Owner Author

Tested this on real BL616 hardware (Sipeed Tang Console 60K, TangCore firmware) and it
breaks chd_fatfs_open() on one image while another is fine, so flagging before merge.

Symptom

image size result
Dungeon Explorer II (USA).chd 371 MB chd_fatfs_openerr=14 CHDERR_DECOMPRESSION_ERROR
Prince of Persia (USA).chd 244 MB err=0, opens and reads fine

DE2 opened cleanly on every build before this change.

Bisect

libchdr src/+include/ held constant at master 8e7b8bd. The only variable was
contrib/tangcore-bl616/chd/chd_fatfs.c:

  • from 9fcd4bc (this PR) → DE2 err=14
  • from master (no CLMT) → DE2 err=0

So the cluster link map is the cause.

Why it looks like wrong seeks

A decompression error at open means the huffman-compressed v5 hunk map decoded from the
wrong bytes — i.e. f_lseek landed somewhere unintended once the CLMT was installed. With
LOWRAM_TARGET=1 the map is read in windows at open (42 seeks / ~156 KB on this image),
so a bad map shows up immediately rather than later during chd_read.

Environment, since it is probably fragmentation-dependent

  • Card is exFAT, 128 KB clusters, 94% full
  • FF_USE_FASTSEEK 1, FF_FS_EXFAT 1, FF_MAX_SS 512, FF_FS_TINY 0
  • DE2 371 MB vs PoP 244 MB — consistent with the larger file being more fragmented
  • filefrag does not work on exFAT here, so I could not measure extent counts directly

What I could not determine

I read the builder and FatFs's side carefully and could not fault the logic:

  • the size negotiation looks right — required items are 2 + 2*nfrag, clmt[0] is set to
    the table size in items, and realloc(need) matches what FatFs reports
  • both fallbacks look right — clmt[0] > CHD_FATFS_CLMT_MAX, and a failed realloc, both
    leave fr != FR_OK so cltbl is cleared and the buffer freed
  • FatFs's get_fat() exFAT paths terminate correctly for a contiguous file
    (stat == 2 synthesises 0x7FFFFFFF at the last cluster) and read the FAT for a
    fragmented one, and clmt_clust() is pure table arithmetic

So this is an empirical result, not a diagnosis. Two things I would look at first:

  1. Whether CREATE_LINKMAP is sound on exFAT specifically — a contiguous exFAT file
    has NoFatChain set and no FAT chain at all, and the builder walks it via get_fat().
  2. Whether exceeding CHD_FATFS_CLMT_MAX (1024 words ≈ 511 fragments) on a heavily
    fragmented 371 MB file can leave a partially-populated table installed rather than
    falling back cleanly.

Happy to run more instrumented builds on this hardware if it would help — I can log
f_lseek's FRESULT, the negotiated clmt[0], and the resulting fragment count from
inside chd_fatfs_map_clusters().

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.

1 participant