From 9247137c593a3549b4d0686e0f4ca0d14f3a1bb1 Mon Sep 17 00:00:00 2001 From: Romain TISSERAND Date: Thu, 10 Sep 2026 14:57:28 +0200 Subject: [PATCH] Share one subcode inflater across the CD codecs cdzl, cdlz and cdfl each carried their own zlib_codec_data for the subcode stream, and each of those allocates an 8376-byte tinfl_decompressor. A CD image compressed with chdman's default trio therefore held four inflaters - cdzl's sector-data one plus three identical subcode ones - where a hunk is decoded by exactly one CD codec and only one subcode inflater is ever in flight. The three now share a single instance owned by the chd_file. Nothing carries across, because zlib_codec_decompress() runs tinfl_init() on entry to every call. The borrowed flag keeps the ownership explicit so only the chd_file frees it. cdzs is untouched: its subcode goes through zstd. Seeding happens before each codec's own init, so nothing is allocated and then discarded, and it is insensitive to ordering - which matters because LOWRAM_TARGET readies codecs lazily, in whatever order the hunks ask for them. The system-zlib path is excluded: there zlib_codec_data holds a z_stream by value, with no pointer to share. Peak heap, massif, same binary either side: cd_default (cdlz+cdzl+cdfl) 183532 -> 166836 -16696, -9.1% every single-codec seed +56 The +56 is the shared zlib_codec_data now living in chd_file even when one CD codec would have sufficed. Note the RAM-budget firmware uses one codec per file, so it cannot see this saving by construction - only the +32 it costs there. Sharing base and subcode inside cdzl was tried before and reverted at 2.6% CPU (see codec_cdzs.h). That does not transfer: those two alternate within a hunk, these three never do. Cachegrind over a real disc, +1089 instructions out of 5.1 billion, and 7689 fewer D1 misses - the shared object has better locality than three. Decoded output is byte-identical across the seed corpus, the v3/v4 fixtures, real CHDv4 images, six real discs and an AVHuff image, in the default build, with CHDR_CD_SCRATCH_BUFFER=OFF, CHDR_LOWRAM_TARGET=ON and WITH_SYSTEM_ZLIB=ON - 144 comparisons, no difference, and no divergence in error codes. Under ASan/UBSan over 820 corrupt and fuzzed images the leak count is identical either side (57 of 220, all pre-existing in chd_open's failure path). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01846EhHuAFk5qvxwEA5Gq6y --- src/codec_zlib.h | 9 ++++++ src/libchdr_chd.c | 69 ++++++++++++++++++++++++++++++++++++++++ src/libchdr_codec_zlib.c | 28 +++++++++++++++- 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/codec_zlib.h b/src/codec_zlib.h index a186daee..f36b430a 100644 --- a/src/codec_zlib.h +++ b/src/codec_zlib.h @@ -51,12 +51,21 @@ struct _zlib_codec_data * against a 254KB peak for a three-codec CD file. Heap-allocated rather * than inline because chd_file embeds every codec's state by value. */ tinfl_decompressor * inflater; + /* Set when `inflater` is owned by the chd_file rather than by this codec. + * The CD codecs' subcode inflaters are one shared object - a hunk is + * decoded by exactly one CD codec, and tinfl_init() runs on entry to + * every decompress, so nothing carries across - and only the owner + * frees it. */ + int borrowed; #endif }; /* zlib compression codec */ chd_error zlib_codec_init(void *codec, uint32_t hunkbytes); void zlib_codec_free(void *codec); +#ifndef CHDR_SYSTEM_ZLIB +void zlib_codec_lend(void *codec, void *owner); +#endif chd_error zlib_codec_decompress(void *codec, const uint8_t *src, uint32_t complen, uint8_t *dest, uint32_t destlen); #endif /* LIBCHDR_CODEC_ZLIB_H */ diff --git a/src/libchdr_chd.c b/src/libchdr_chd.c index 5e7e844a..b5981c25 100644 --- a/src/libchdr_chd.c +++ b/src/libchdr_chd.c @@ -326,6 +326,17 @@ struct _chd_file avhuff_codec_data avhuff; /* avhuff codec data */ } codec_data; +#if WANT_SUBCODE && !defined(CHDR_SYSTEM_ZLIB) + /* One inflater for every CD codec's subcode stream. cdzl, cdlz and cdfl + * each used to allocate their own, but a hunk is decoded by exactly one + * CD codec and zlib_codec_decompress() runs tinfl_init() on entry, so + * nothing carries from one to the next - three tinfl_decompressor + * instances where one does. At CD geometry that is 16752 bytes back on a + * file using all three, 9.3% of its peak. cdzs is not here: its subcode + * goes through zstd. */ + zlib_codec_data subcode_shared; +#endif + uint8_t * file_cache; /* cache of underlying file */ /* Compressed read-ahead. libchdr issues one seek+read per hunk, and @@ -436,6 +447,9 @@ static chd_error header_read(chd_file *chd); #endif #endif +#if WANT_SUBCODE && !defined(CHDR_SYSTEM_ZLIB) +static chd_error cd_lend_subcode_inflater(chd_file *chd, uint32_t tag, void *codec); +#endif static chd_error hunk_read_into_memory(chd_file *chd, uint32_t hunknum, uint8_t *dest); /*------------------------------------------------- @@ -1877,6 +1891,14 @@ static chd_error ensure_codec_ready(chd_file *chd, size_t slot, void *codec) } } +#if WANT_SUBCODE && !defined(CHDR_SYSTEM_ZLIB) + { + chd_error lerr = cd_lend_subcode_inflater(chd, chd->codecintf[slot]->compression, codec); + if (lerr != CHDERR_NONE) + return lerr; + } +#endif + if (chd->codecintf[slot]->init != NULL) { chd_error err = chd->codecintf[slot]->init(codec, chd->header.hunkbytes); @@ -2163,6 +2185,13 @@ CHD_EXPORT chd_error chd_open_core_file_callbacks(const core_file_callbacks *cal if (codec == NULL) EARLY_EXIT(err = CHDERR_UNSUPPORTED_FORMAT); +#if WANT_SUBCODE && !defined(CHDR_SYSTEM_ZLIB) + err = cd_lend_subcode_inflater(newchd, + newchd->header.compression[decompnum], codec); + if (err != CHDERR_NONE) + EARLY_EXIT(err); +#endif + err = newchd->codecintf[decompnum]->init(codec, newchd->header.hunkbytes); if (err != CHDERR_NONE) EARLY_EXIT(err); @@ -2318,6 +2347,12 @@ CHD_EXPORT void chd_close(chd_file *chd) if (chd == NULL || chd->cookie != COOKIE_VALUE) return; +#if WANT_SUBCODE && !defined(CHDR_SYSTEM_ZLIB) + /* The CD codecs only borrowed this one; free it here, once, after they + * have all dropped their pointers to it. */ + zlib_codec_free(&chd->subcode_shared); +#endif + /* deinit the codec */ if (chd->header.version < 5) { @@ -3079,6 +3114,40 @@ static chd_error hunk_read_uncompressed(chd_file *chd, uint64_t offset, size_t s return CHDERR_NONE; } +#if WANT_SUBCODE && !defined(CHDR_SYSTEM_ZLIB) +/*------------------------------------------------- + cd_lend_subcode_inflater - seed a CD codec's + subcode inflater from the shared one +-------------------------------------------------*/ + +/* Seeds before the codec's own init runs: zlib_codec_init() keeps an inflater + * that is already marked borrowed, so nothing is allocated and then thrown + * away. Ordering-proof, which matters because LOWRAM_TARGET readies codecs + * lazily and in whatever order the hunks ask for them. */ +static chd_error cd_lend_subcode_inflater(chd_file *chd, uint32_t tag, void *codec) +{ + void *sub; + + switch (tag) + { + case CHD_CODEC_CD_ZLIB: sub = &((cdzl_codec_data *)codec)->subcode_decompressor; break; + case CHD_CODEC_CD_LZMA: sub = &((cdlz_codec_data *)codec)->subcode_decompressor; break; + case CHD_CODEC_CD_FLAC: sub = &((cdfl_codec_data *)codec)->subcode_decompressor; break; + default: return CHDERR_NONE; + } + + if (chd->subcode_shared.inflater == NULL) + { + chd_error err = zlib_codec_init(&chd->subcode_shared, 0); + if (err != CHDERR_NONE) + return err; + } + + zlib_codec_lend(sub, &chd->subcode_shared); + return CHDERR_NONE; +} +#endif + /*------------------------------------------------- hunk_read_into_memory - read a hunk into memory at the given location diff --git a/src/libchdr_codec_zlib.c b/src/libchdr_codec_zlib.c index f8a160dd..6d299fe3 100644 --- a/src/libchdr_codec_zlib.c +++ b/src/libchdr_codec_zlib.c @@ -18,13 +18,37 @@ static void zlib_allocator_free(voidpf opaque); /* ---- bundled miniz: drive tinfl directly, no 32KB dictionary ---- */ +/*------------------------------------------------- + zlib_codec_lend - point this codec's inflater + at one owned elsewhere +-------------------------------------------------*/ + +void zlib_codec_lend(void *codec, void *owner) +{ + zlib_codec_data *data = (zlib_codec_data *)codec; + zlib_codec_data *src = (zlib_codec_data *)owner; + + data->inflater = src->inflater; + data->borrowed = 1; +} + chd_error zlib_codec_init(void *codec, uint32_t hunkbytes) { zlib_codec_data *data = (zlib_codec_data *)codec; (void)hunkbytes; + /* A borrowed inflater is seeded before init runs; keep it across the + * wipe rather than allocating a second one. */ + tinfl_decompressor *lent = data->borrowed ? data->inflater : NULL; + memset(data, 0, sizeof(zlib_codec_data)); + if (lent != NULL) + { + data->inflater = lent; + data->borrowed = 1; + return CHDERR_NONE; + } data->inflater = (tinfl_decompressor *)malloc(sizeof(tinfl_decompressor)); if (data->inflater == NULL) return CHDERR_OUT_OF_MEMORY; @@ -38,8 +62,10 @@ void zlib_codec_free(void *codec) if (data != NULL && data->inflater != NULL) { - free(data->inflater); + if (!data->borrowed) + free(data->inflater); data->inflater = NULL; + data->borrowed = 0; } }