From 9fcd4bc637dbf600d9e1a30190b9ce86c5c4771c Mon Sep 17 00:00:00 2001 From: Romain TISSERAND Date: Fri, 11 Sep 2026 00:31:19 +0200 Subject: [PATCH] FatFs bridges: build a cluster link map on open 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) Claude-Session: https://claude.ai/code/session_01846EhHuAFk5qvxwEA5Gq6y --- contrib/rp2350/CMakeLists.txt | 4 ++ contrib/rp2350/benchmark_main.c | 48 ++++++++++++++- contrib/tangcore-bl616/chd/chd_fatfs.c | 83 +++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 5 deletions(-) diff --git a/contrib/rp2350/CMakeLists.txt b/contrib/rp2350/CMakeLists.txt index 8a9bff55..78feea9f 100644 --- a/contrib/rp2350/CMakeLists.txt +++ b/contrib/rp2350/CMakeLists.txt @@ -31,6 +31,9 @@ endif() if(NOT DEFINED BENCH_HUNK_CAP) set(BENCH_HUNK_CAP 0) endif() +if(NOT DEFINED BENCH_LINKMAP) + set(BENCH_LINKMAP 1) +endif() if(NOT DEFINED SD_BAUD_HZ) set(SD_BAUD_HZ 25000000) endif() @@ -131,6 +134,7 @@ target_compile_definitions(rp2350-bench PRIVATE VERIFY_BLOCK_CRC=1 LOWRAM_TARGET=${LOWRAM_TARGET_VAL} BENCH_HUNK_CAP=${BENCH_HUNK_CAP} + BENCH_LINKMAP=${BENCH_LINKMAP} SD_BAUD_HZ=${SD_BAUD_HZ} MINIZ_NO_ARCHIVE_APIS MINIZ_NO_DEFLATE_APIS diff --git a/contrib/rp2350/benchmark_main.c b/contrib/rp2350/benchmark_main.c index 48f03b3a..bb562d9b 100644 --- a/contrib/rp2350/benchmark_main.c +++ b/contrib/rp2350/benchmark_main.c @@ -41,6 +41,14 @@ #define BENCH_CACHE_BUDGET (32 * 1024) #endif +/* Build a FatFs cluster link map on every open (needs FF_USE_FASTSEEK). Without + * one, every backward f_lseek walks the FAT chain from the file's first + * cluster, and every COMPRESSION_SELF hunk is a backward seek. Set 0 to measure + * what the map buys. */ +#ifndef BENCH_LINKMAP +#define BENCH_LINKMAP 1 +#endif + /* ---- files under test; missing entries are skipped, not fatal ---- */ static const char *const g_files[] = { "0:/roms/pcenginecd/Insanity (USA) (Unl).cue.chd", @@ -127,6 +135,35 @@ static const char *basename_of(const char *p) return s ? s + 1 : p; } +#if FF_USE_FASTSEEK +#define BENCH_DROP_MAP(f) ((f)->cltbl = NULL) +#else +#define BENCH_DROP_MAP(f) ((void)(f)) +#endif + +#if BENCH_LINKMAP && FF_USE_FASTSEEK +/* Same sizing as contrib/tangcore-bl616/chd/chd_fatfs.c: start small, grow to + * what FatFs reports it needs, give up past a cap and run without. Returns + * the table's size in words, 0 if there is none. */ +static DWORD *bench_map_clusters(FIL *fil) +{ + DWORD *clmt = malloc(sizeof(DWORD) * 16); + FRESULT fr; + if (!clmt) return NULL; + clmt[0] = 16; + fil->cltbl = clmt; + fr = f_lseek(fil, CREATE_LINKMAP); + if (fr == FR_NOT_ENOUGH_CORE && clmt[0] <= 1024) { + DWORD need = clmt[0]; + DWORD *grown = realloc(clmt, sizeof(DWORD) * need); + if (grown) { clmt = grown; clmt[0] = need; fil->cltbl = clmt; fr = f_lseek(fil, CREATE_LINKMAP); } + } + if (fr != FR_OK) { fil->cltbl = NULL; free(clmt); clmt = NULL; } + f_lseek(fil, 0); + return clmt; +} +#endif + static int run_one(const char *path) { sdfile sf; @@ -138,6 +175,11 @@ static int run_one(const char *path) printf("%-34s SKIP (not on card)\n", basename_of(path)); return 0; } +#if BENCH_LINKMAP && FF_USE_FASTSEEK + DWORD *clmt = bench_map_clusters(&sf.fil); +#else + void *clmt = NULL; +#endif g_base = heap_used(); g_peak = 0; @@ -145,7 +187,7 @@ static int run_one(const char *path) err = chd_open_core_file_callbacks(&sd_callbacks, &sf, CHD_OPEN_READ, NULL, &chd); if (err != CHDERR_NONE) { printf("%-34s OPEN FAILED: %s\n", basename_of(path), chd_error_string(err)); - f_close(&sf.fil); + BENCH_DROP_MAP(&sf.fil); f_close(&sf.fil); free(clmt); return 0; } #if BENCH_CACHE_BUDGET @@ -170,7 +212,7 @@ static int run_one(const char *path) unsigned char *buf = malloc(h->hunkbytes); if (!buf) { printf("%-34s hunk buffer alloc failed (%u B)\n", basename_of(path), h->hunkbytes); - chd_close(chd); f_close(&sf.fil); + chd_close(chd); BENCH_DROP_MAP(&sf.fil); f_close(&sf.fil); free(clmt); return 0; } @@ -202,7 +244,9 @@ static int run_one(const char *path) free(buf); chd_close(chd); + BENCH_DROP_MAP(&sf.fil); f_close(&sf.fil); + free(clmt); if (i != n) return 0; diff --git a/contrib/tangcore-bl616/chd/chd_fatfs.c b/contrib/tangcore-bl616/chd/chd_fatfs.c index b8dc517c..ed47ec49 100644 --- a/contrib/tangcore-bl616/chd/chd_fatfs.c +++ b/contrib/tangcore-bl616/chd/chd_fatfs.c @@ -8,7 +8,8 @@ #include "chd_fatfs.h" -#include /* SEEK_SET / SEEK_CUR / SEEK_END */ +#include /* SEEK_SET / SEEK_CUR / SEEK_END */ +#include /* malloc / realloc / free */ static uint64_t chd_fatfs_fsize(void *argp) { @@ -31,10 +32,22 @@ static size_t chd_fatfs_fread(void *ptr, size_t size, size_t nmemb, void *argp) return (size_t)(br / size); } +/* libchdr calls this once, from chd_close() or from a failed open; the link + * map goes with the file. */ static int chd_fatfs_fclose(void *argp) { FIL *fil = (FIL *)argp; - return (f_close(fil) == FR_OK) ? 0 : -1; + FRESULT fr; +#if FF_USE_FASTSEEK + DWORD *clmt = fil->cltbl; + + fil->cltbl = NULL; +#endif + fr = f_close(fil); +#if FF_USE_FASTSEEK + free(clmt); +#endif + return (fr == FR_OK) ? 0 : -1; } static int chd_fatfs_fseek(void *argp, int64_t offset, int whence) @@ -61,6 +74,64 @@ static int chd_fatfs_fseek(void *argp, int64_t offset, int whence) return (f_lseek(fil, abs_offset) == FR_OK) ? 0 : -1; } +#if FF_USE_FASTSEEK +/* Without a cluster link map, every backward f_lseek walks the FAT chain from + * the file's first cluster again - and every COMPRESSION_SELF hunk is a + * backward seek, to an earlier hunk the file 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 map was built. FF_USE_FASTSEEK + * provides the map, but f_open does not build one - the caller has to. + * + * The table needs two words per fragment plus one. A freshly written card + * holds each file as one fragment, so start small and grow only to what + * FatFs reports it needs, capped so a pathologically fragmented file costs at + * most CHD_FATFS_CLMT_MAX words and otherwise just runs without the map. */ +#ifndef CHD_FATFS_CLMT_FIRST +#define CHD_FATFS_CLMT_FIRST 16 +#endif +#ifndef CHD_FATFS_CLMT_MAX +#define CHD_FATFS_CLMT_MAX 1024 +#endif + +static void chd_fatfs_map_clusters(FIL *fil) +{ + DWORD *clmt = (DWORD *)malloc(sizeof(DWORD) * CHD_FATFS_CLMT_FIRST); + FRESULT fr; + + if (clmt == NULL) + return; + + clmt[0] = CHD_FATFS_CLMT_FIRST; + fil->cltbl = clmt; + fr = f_lseek(fil, CREATE_LINKMAP); + + /* on FR_NOT_ENOUGH_CORE, FatFs leaves the size it needs in clmt[0] */ + if (fr == FR_NOT_ENOUGH_CORE && clmt[0] <= CHD_FATFS_CLMT_MAX) + { + DWORD need = clmt[0]; + DWORD *grown = (DWORD *)realloc(clmt, sizeof(DWORD) * need); + + if (grown != NULL) + { + clmt = grown; + clmt[0] = need; + fil->cltbl = clmt; + fr = f_lseek(fil, CREATE_LINKMAP); + } + } + + if (fr != FR_OK) + { + /* no map: slower backward seeks, but correct */ + fil->cltbl = NULL; + free(clmt); + } + + f_lseek(fil, 0); +} +#endif + const core_file_callbacks chd_fatfs_callbacks = { .fsize = chd_fatfs_fsize, .fread = chd_fatfs_fread, @@ -75,7 +146,13 @@ chd_error chd_fatfs_open(const char *path, FIL *fil, chd_file **chd) if (f_open(fil, path, FA_READ) != FR_OK) return CHDERR_FILE_NOT_FOUND; - /* on failure libchdr has already closed fil through the callback */ +#if FF_USE_FASTSEEK + /* before chd_open, so the header and map reads benefit too */ + chd_fatfs_map_clusters(fil); +#endif + + /* on failure libchdr has already closed fil through the callback, which + * also frees the link map */ err = chd_open_core_file_callbacks(&chd_fatfs_callbacks, fil, CHD_OPEN_READ, NULL, chd); if (err != CHDERR_NONE) return err;