diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb20c9..20a4165 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,6 +99,7 @@ set(CHDR_SOURCES src/libchdr_bitstream.c src/libchdr_cdrom.c src/libchdr_chd.c + src/libchdr_codec_avhuff.c src/libchdr_codec_cdfl.c src/libchdr_codec_cdlz.c src/libchdr_codec_cdzl.c diff --git a/include/libchdr/chd.h b/include/libchdr/chd.h index 6b8b439..c3330b9 100644 --- a/include/libchdr/chd.h +++ b/include/libchdr/chd.h @@ -207,6 +207,7 @@ extern "C" { #define CHD_CODEC_HUFFMAN CHD_MAKE_TAG('h','u','f','f') #define CHD_CODEC_FLAC CHD_MAKE_TAG('f','l','a','c') #define CHD_CODEC_ZSTD CHD_MAKE_TAG('z', 's', 't', 'd') +#define CHD_CODEC_AVHUFF CHD_MAKE_TAG('a','v','h','u') /* general codecs with CD frontend */ #define CHD_CODEC_CD_ZLIB CHD_MAKE_TAG('c','d','z','l') #define CHD_CODEC_CD_LZMA CHD_MAKE_TAG('c','d','l','z') diff --git a/src/codec_avhuff.h b/src/codec_avhuff.h new file mode 100644 index 0000000..774d1f2 --- /dev/null +++ b/src/codec_avhuff.h @@ -0,0 +1,52 @@ +/* license:BSD-3-Clause + * copyright-holders:Aaron Giles + * + * codec_avhuff.h + * + * AVHuff (audio/video) codec decompressor private data. + * Used by CHDv5 'avhu' codec for laserdisc CHDs. + */ + +#ifndef __CODEC_AVHUFF_H__ +#define __CODEC_AVHUFF_H__ + +#include + +#include "../include/libchdr/chd.h" +#include "../include/libchdr/flac.h" + +struct huffman_decoder; + +/* codec-private data for the AVHuff codec */ +typedef struct _avhuff_codec_data avhuff_codec_data; +struct _avhuff_codec_data +{ + /* video delta-RLE decoder state (Y, Cb, Cr) */ + struct huffman_decoder *ycontext; + struct huffman_decoder *cbcontext; + struct huffman_decoder *crcontext; + uint8_t y_prev; + uint8_t cb_prev; + uint8_t cr_prev; + int y_rlecount; + int cb_rlecount; + int cr_rlecount; + + /* audio delta-RLE decoder state (hi byte, lo byte) */ + struct huffman_decoder *audiohi; + struct huffman_decoder *audiolo; + uint8_t ahi_prev; + uint8_t alo_prev; + int ahi_rlecount; + int alo_rlecount; + + /* FLAC decoder (reused per channel when treesize == 0xffff) */ + flac_decoder flac; +}; + +chd_error avhuff_codec_init(void *codec, uint32_t hunkbytes); +void avhuff_codec_free(void *codec); +chd_error avhuff_codec_decompress(void *codec, const uint8_t *src, uint32_t complen, + uint8_t *dest, uint32_t destlen); + +#endif /* __CODEC_AVHUFF_H__ */ diff --git a/src/libchdr_chd.c b/src/libchdr_chd.c index 1158f31..b613c70 100644 --- a/src/libchdr_chd.c +++ b/src/libchdr_chd.c @@ -59,6 +59,7 @@ #include "../include/libchdr/chd.h" #include "../include/libchdr/cdrom.h" +#include "codec_avhuff.h" #include "codec_cdfl.h" #include "codec_cdlz.h" #include "codec_cdzl.h" @@ -231,6 +232,7 @@ struct _chd_file cdlz_codec_data cdlz; /* cdlz codec data */ cdfl_codec_data cdfl; /* cdfl codec data */ cdzs_codec_data cdzs; /* cdzs codec data */ + avhuff_codec_data avhuff; /* avhuff codec data */ } codec_data; uint8_t * file_cache; /* cache of underlying file */ @@ -409,8 +411,27 @@ static const codec_interface codec_interfaces[] = cdzs_codec_free, cdzs_codec_decompress, NULL + }, + /* V5 A/V Huffman (laserdisc) */ + { + CHD_CODEC_AVHUFF, + "A/V Huffman", + FALSE, + avhuff_codec_init, + avhuff_codec_free, + avhuff_codec_decompress, + NULL + }, + /* V3/V4 A/V Huffman (laserdisc) — CHDCOMPRESSION_AV */ + { + CHDCOMPRESSION_AV, + "A/V Huffman (v3/v4)", + FALSE, + avhuff_codec_init, + avhuff_codec_free, + avhuff_codec_decompress, + NULL } - }; /*************************************************************************** @@ -1001,7 +1022,11 @@ CHD_EXPORT chd_error chd_open_core_file_callbacks(const core_file_callbacks *cal /* initialize the codec */ if (newchd->codecintf[0]->init != NULL) { - err = newchd->codecintf[0]->init(&newchd->codec_data.zlib, newchd->header.hunkbytes); + /* v1-v4 codecs that use their own state blob; zlib is the default */ + void* codec = &newchd->codec_data.zlib; + if (newchd->header.compression[0] == CHDCOMPRESSION_AV) + codec = &newchd->codec_data.avhuff; + err = newchd->codecintf[0]->init(codec, newchd->header.hunkbytes); if (err != CHDERR_NONE) EARLY_EXIT(err); } @@ -1081,6 +1106,10 @@ CHD_EXPORT chd_error chd_open_core_file_callbacks(const core_file_callbacks *cal case CHD_CODEC_CD_ZSTD: codec = &newchd->codec_data.cdzs; break; + + case CHD_CODEC_AVHUFF: + codec = &newchd->codec_data.avhuff; + break; } if (codec == NULL) @@ -1177,7 +1206,12 @@ CHD_EXPORT void chd_close(chd_file *chd) if (chd->header.version < 5) { if (chd->codecintf[0] != NULL && chd->codecintf[0]->free != NULL) - chd->codecintf[0]->free(&chd->codec_data.zlib); + { + void *codec = &chd->codec_data.zlib; + if (chd->header.compression[0] == CHDCOMPRESSION_AV) + codec = &chd->codec_data.avhuff; + chd->codecintf[0]->free(codec); + } } else { @@ -1242,6 +1276,10 @@ CHD_EXPORT void chd_close(chd_file *chd) case CHD_CODEC_CD_ZSTD: codec = &chd->codec_data.cdzs; break; + + case CHD_CODEC_AVHUFF: + codec = &chd->codec_data.avhuff; + break; } if (codec) @@ -1833,6 +1871,8 @@ static chd_error hunk_read_into_memory(chd_file *chd, uint32_t hunknum, uint8_t /* now decompress using the codec */ err = CHDERR_NONE; codec = &chd->codec_data.zlib; + if (chd->header.compression[0] == CHDCOMPRESSION_AV) + codec = &chd->codec_data.avhuff; if (chd->codecintf[0]->decompress != NULL) err = chd->codecintf[0]->decompress(codec, compressed_bytes, entry->length, dest, chd->header.hunkbytes); if (err != CHDERR_NONE) @@ -1953,6 +1993,10 @@ static chd_error hunk_read_into_memory(chd_file *chd, uint32_t hunknum, uint8_t case CHD_CODEC_CD_ZSTD: codec = &chd->codec_data.cdzs; break; + + case CHD_CODEC_AVHUFF: + codec = &chd->codec_data.avhuff; + break; } if (codec==NULL) return CHDERR_CODEC_ERROR; diff --git a/src/libchdr_codec_avhuff.c b/src/libchdr_codec_avhuff.c new file mode 100644 index 0000000..9c0b5f6 --- /dev/null +++ b/src/libchdr_codec_avhuff.c @@ -0,0 +1,482 @@ +/* license:BSD-3-Clause + * copyright-holders:Aaron Giles + * + * libchdr_codec_avhuff.c + * + * AVHuff decompressor for CHDv5 'avhu' hunks and CHDv3/v4 CHDCOMPRESSION_AV + * hunks. Decompression-only port of MAME's src/lib/util/avhuff.cpp with the + * encoder, C++ scaffolding, and emucore dependencies removed. + * + * Reuses libchdr's existing primitives: + * - huffman decoder (src/libchdr_huffman.c) + * - bitstream reader (src/libchdr_bitstream.c) + * - FLAC via dr_flac wrapper (src/libchdr_flac.c) + */ + +#include "codec_avhuff.h" + +#include +#include +#include + +#include "../include/libchdr/bitstream.h" +#include "../include/libchdr/huffman.h" + +/*************************************************************************** + CONSTANTS +***************************************************************************/ + +#define AVHUFF_NUMCODES (256 + 16) /* 256 byte values + 16 RLE run codes */ +#define AVHUFF_MAXBITS 16 + +/*************************************************************************** + HELPERS +***************************************************************************/ + +static uint16_t get_u16be(const uint8_t *p) +{ + return ((uint16_t)p[0] << 8) | p[1]; +} + +static int16_t get_s16be(const uint8_t *p) +{ + return (int16_t)get_u16be(p); +} + +static void put_u16be(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)(v >> 8); + p[1] = (uint8_t)v; +} + +/* RLE run length for an escape code (matches MAME avhuff.cpp code_to_rlecount) */ +static int code_to_rlecount(int code) +{ + if (code == 0x00) + return 1; + if (code <= 0x107) + return 8 + (code - 0x100); + return 16 << (code - 0x108); +} + +/* Decode one byte from a delta-RLE huffman stream, maintaining prev + rlecount + * at the caller's level. */ +static uint8_t deltarle_decode_one(struct huffman_decoder *dec, struct bitstream *bitbuf, + uint8_t *prev, int *rlecount) +{ + uint32_t data; + + if (*rlecount != 0) + { + (*rlecount)--; + return *prev; + } + + data = huffman_decode_one(dec, bitbuf); + if (data < 0x100) + { + *prev = (uint8_t)(*prev + (uint8_t)data); + return *prev; + } + + *rlecount = code_to_rlecount((int)data); + (*rlecount)--; + return *prev; +} + +static void deltarle_reset(uint8_t *prev, int *rlecount) +{ + *prev = 0; + *rlecount = 0; +} + +static void deltarle_flush(int *rlecount) +{ + *rlecount = 0; +} + +/*************************************************************************** + CODEC INIT / FREE +***************************************************************************/ + +chd_error avhuff_codec_init(void *codec, uint32_t hunkbytes) +{ + avhuff_codec_data *avhuff = (avhuff_codec_data *)codec; + + (void)hunkbytes; + + memset(avhuff, 0, sizeof(*avhuff)); + + /* Y/Cb/Cr decoders are required for every AVHuff hunk; allocate eagerly. + * audiohi/audiolo are only used by the huffman audio sub-codec (treesize + * non-zero and non-0xffff). Modern chdman emits FLAC audio (treesize= + * 0xffff) almost exclusively, so defer those 256 KiB until first use. */ + avhuff->ycontext = create_huffman_decoder(AVHUFF_NUMCODES, AVHUFF_MAXBITS); + avhuff->cbcontext = create_huffman_decoder(AVHUFF_NUMCODES, AVHUFF_MAXBITS); + avhuff->crcontext = create_huffman_decoder(AVHUFF_NUMCODES, AVHUFF_MAXBITS); + + if (avhuff->ycontext == NULL || avhuff->cbcontext == NULL || + avhuff->crcontext == NULL) + { + avhuff_codec_free(codec); + return CHDERR_OUT_OF_MEMORY; + } + + if (flac_decoder_init(&avhuff->flac) != 0) + { + avhuff_codec_free(codec); + return CHDERR_OUT_OF_MEMORY; + } + + return CHDERR_NONE; +} + +void avhuff_codec_free(void *codec) +{ + avhuff_codec_data *avhuff = (avhuff_codec_data *)codec; + + if (avhuff->ycontext != NULL) delete_huffman_decoder(avhuff->ycontext); + if (avhuff->cbcontext != NULL) delete_huffman_decoder(avhuff->cbcontext); + if (avhuff->crcontext != NULL) delete_huffman_decoder(avhuff->crcontext); + if (avhuff->audiohi != NULL) delete_huffman_decoder(avhuff->audiohi); + if (avhuff->audiolo != NULL) delete_huffman_decoder(avhuff->audiolo); + flac_decoder_free(&avhuff->flac); + + avhuff->ycontext = avhuff->cbcontext = avhuff->crcontext = NULL; + avhuff->audiohi = avhuff->audiolo = NULL; +} + +/*************************************************************************** + AUDIO DECODE +***************************************************************************/ + +static chd_error decode_audio_flac(avhuff_codec_data *avhuff, uint32_t channels, + uint32_t samples, const uint8_t *source, + uint8_t **audiostart, const uint8_t *sizes) +{ + uint32_t chnum; + + /* CHD raw hunks: destination is always big-endian, dxor = 0. + * flac_decoder_decode_interleaved writes in native byte order; pass + * swap_endian=1 so the output is byte-swapped to BE on LE hosts. + * detect_native_endian() returns 1 on LE, 0 on BE — that's exactly + * the swap value we need. */ + int swap_endian = flac_decoder_detect_native_endian(); + + for (chnum = 0; chnum < channels; chnum++) + { + uint16_t size = get_u16be(&sizes[chnum * 2 + 2]); + uint8_t *curdest = audiostart[chnum]; + + if (curdest != NULL) + { + if (!flac_decoder_reset(&avhuff->flac, 48000, 1, samples, source, size)) + return CHDERR_DECOMPRESSION_ERROR; + if (!flac_decoder_decode_interleaved(&avhuff->flac, + (int16_t *)curdest, + samples, swap_endian)) + return CHDERR_DECOMPRESSION_ERROR; + flac_decoder_finish(&avhuff->flac); + } + + source += size; + } + + return CHDERR_NONE; +} + +static chd_error decode_audio(avhuff_codec_data *avhuff, uint32_t channels, + uint32_t samples, const uint8_t *source, + uint8_t **audiostart, const uint8_t *sizes) +{ + uint16_t treesize = get_u16be(&sizes[0]); + uint32_t chnum, sampnum; + struct bitstream *bitbuf; + + if (treesize == 0xffff) + return decode_audio_flac(avhuff, channels, samples, source, audiostart, sizes); + + /* If treesize > 0, import both hi/lo huffman trees from the first + * treesize bytes of the audio region. */ + if (treesize != 0) + { + enum huffman_error hufferr; + + /* lazy-allocate the audio huffman decoders on first huffman-audio + * hunk; reused for the lifetime of the codec instance */ + if (avhuff->audiohi == NULL) + { + avhuff->audiohi = create_huffman_decoder(AVHUFF_NUMCODES, AVHUFF_MAXBITS); + if (avhuff->audiohi == NULL) + return CHDERR_OUT_OF_MEMORY; + } + if (avhuff->audiolo == NULL) + { + avhuff->audiolo = create_huffman_decoder(AVHUFF_NUMCODES, AVHUFF_MAXBITS); + if (avhuff->audiolo == NULL) + return CHDERR_OUT_OF_MEMORY; + } + + bitbuf = create_bitstream(source, treesize); + if (bitbuf == NULL) + return CHDERR_OUT_OF_MEMORY; + hufferr = huffman_import_tree_rle(avhuff->audiohi, bitbuf); + if (hufferr != HUFFERR_NONE) { free(bitbuf); return CHDERR_INVALID_DATA; } + bitstream_flush(bitbuf); + hufferr = huffman_import_tree_rle(avhuff->audiolo, bitbuf); + if (hufferr != HUFFERR_NONE) { free(bitbuf); return CHDERR_INVALID_DATA; } + if (bitstream_flush(bitbuf) != treesize) { free(bitbuf); return CHDERR_INVALID_DATA; } + free(bitbuf); + + source += treesize; + } + + for (chnum = 0; chnum < channels; chnum++) + { + uint16_t size = get_u16be(&sizes[chnum * 2 + 2]); + uint8_t *curdest = audiostart[chnum]; + + if (curdest != NULL) + { + int16_t prevsample = 0; + + if (treesize == 0) + { + /* raw big-endian s16 deltas */ + const uint8_t *cur = source; + for (sampnum = 0; sampnum < samples; sampnum++) + { + int16_t delta = get_s16be(cur); + int16_t newsample; + cur += 2; + newsample = (int16_t)(prevsample + delta); + prevsample = newsample; + curdest[0] = (uint8_t)(newsample >> 8); + curdest[1] = (uint8_t)newsample; + curdest += 2; + } + } + else + { + /* huffman-coded deltas, hi/lo byte streams share the same bitbuf */ + bitbuf = create_bitstream(source, size); + if (bitbuf == NULL) + return CHDERR_OUT_OF_MEMORY; + /* Reset deltarle state between channels */ + avhuff->ahi_prev = avhuff->alo_prev = 0; + avhuff->ahi_rlecount = avhuff->alo_rlecount = 0; + for (sampnum = 0; sampnum < samples; sampnum++) + { + int16_t delta; + int16_t newsample; + uint8_t hi = deltarle_decode_one(avhuff->audiohi, bitbuf, + &avhuff->ahi_prev, &avhuff->ahi_rlecount); + uint8_t lo = deltarle_decode_one(avhuff->audiolo, bitbuf, + &avhuff->alo_prev, &avhuff->alo_rlecount); + delta = (int16_t)(((uint16_t)hi << 8) | lo); + newsample = (int16_t)(prevsample + delta); + prevsample = newsample; + curdest[0] = (uint8_t)(newsample >> 8); + curdest[1] = (uint8_t)newsample; + curdest += 2; + } + if (bitstream_overflow(bitbuf)) + { + free(bitbuf); + return CHDERR_INVALID_DATA; + } + free(bitbuf); + } + } + + source += size; + } + + return CHDERR_NONE; +} + +/*************************************************************************** + VIDEO DECODE (lossless only — lossy path rejected) +***************************************************************************/ + +static chd_error decode_video_lossless(avhuff_codec_data *avhuff, + uint32_t width, uint32_t height, + const uint8_t *source, uint32_t complen, + uint8_t *dest, uint32_t dstride) +{ + struct bitstream *bitbuf; + enum huffman_error hufferr; + uint32_t dy, dx; + + bitbuf = create_bitstream(source, complen); + if (bitbuf == NULL) + return CHDERR_OUT_OF_MEMORY; + + /* skip the 1-byte flag that gated lossless vs lossy */ + bitstream_read(bitbuf, 8); + + hufferr = huffman_import_tree_rle(avhuff->ycontext, bitbuf); + if (hufferr != HUFFERR_NONE) { free(bitbuf); return CHDERR_INVALID_DATA; } + bitstream_flush(bitbuf); + hufferr = huffman_import_tree_rle(avhuff->cbcontext, bitbuf); + if (hufferr != HUFFERR_NONE) { free(bitbuf); return CHDERR_INVALID_DATA; } + bitstream_flush(bitbuf); + hufferr = huffman_import_tree_rle(avhuff->crcontext, bitbuf); + if (hufferr != HUFFERR_NONE) { free(bitbuf); return CHDERR_INVALID_DATA; } + bitstream_flush(bitbuf); + + /* Reset per-plane deltarle state before decoding rows */ + deltarle_reset(&avhuff->y_prev, &avhuff->y_rlecount); + deltarle_reset(&avhuff->cb_prev, &avhuff->cb_rlecount); + deltarle_reset(&avhuff->cr_prev, &avhuff->cr_rlecount); + + for (dy = 0; dy < height; dy++) + { + uint8_t *row = dest + dy * dstride; + for (dx = 0; dx < width / 2; dx++) + { + row[0] = deltarle_decode_one(avhuff->ycontext, bitbuf, + &avhuff->y_prev, &avhuff->y_rlecount); + row[1] = deltarle_decode_one(avhuff->cbcontext, bitbuf, + &avhuff->cb_prev, &avhuff->cb_rlecount); + row[2] = deltarle_decode_one(avhuff->ycontext, bitbuf, + &avhuff->y_prev, &avhuff->y_rlecount); + row[3] = deltarle_decode_one(avhuff->crcontext, bitbuf, + &avhuff->cr_prev, &avhuff->cr_rlecount); + row += 4; + } + /* flush RLE accumulator between rows (matches MAME) */ + deltarle_flush(&avhuff->y_rlecount); + deltarle_flush(&avhuff->cb_rlecount); + deltarle_flush(&avhuff->cr_rlecount); + } + + if (bitstream_overflow(bitbuf) || bitstream_flush(bitbuf) != complen) + { + free(bitbuf); + return CHDERR_INVALID_DATA; + } + free(bitbuf); + return CHDERR_NONE; +} + +/*************************************************************************** + TOP-LEVEL DECOMPRESS +***************************************************************************/ + +chd_error avhuff_codec_decompress(void *codec, const uint8_t *src, uint32_t complen, + uint8_t *dest, uint32_t destlen) +{ + avhuff_codec_data *avhuff = (avhuff_codec_data *)codec; + uint32_t metasize, channels, samples, width, height; + uint32_t srcoffs, totalsize, treesize; + uint8_t *metastart, *videostart; + uint8_t *audiostart[16]; + uint32_t videostride; + uint32_t chnum; + uint32_t header_bytes; + uint32_t payload_bytes; + uint32_t written; + + if (complen < 8) + return CHDERR_INVALID_DATA; + + metasize = src[0]; + channels = src[1]; + samples = get_u16be(&src[2]); + width = get_u16be(&src[4]); + height = get_u16be(&src[6]); + + if (channels > 16) + return CHDERR_INVALID_DATA; + if (complen < 10u + 2u * channels) + return CHDERR_INVALID_DATA; + + totalsize = 10u + 2u * channels; + treesize = get_u16be(&src[8]); + if (treesize != 0xffff) + totalsize += treesize; + for (chnum = 0; chnum < channels; chnum++) + totalsize += get_u16be(&src[10 + 2 * chnum]); + if (totalsize >= complen) + return CHDERR_INVALID_DATA; + + /* required output size: 12-byte 'chav' header + metadata + audio + video */ + header_bytes = 12u; + payload_bytes = metasize + 2u * channels * samples + 2u * width * height; + if ((uint64_t)header_bytes + (uint64_t)payload_bytes > (uint64_t)destlen) + return CHDERR_DECOMPRESSION_ERROR; + + /* write destination 'chav' header */ + dest[0] = 'c'; + dest[1] = 'h'; + dest[2] = 'a'; + dest[3] = 'v'; + dest[4] = (uint8_t)metasize; + dest[5] = (uint8_t)channels; + put_u16be(&dest[6], (uint16_t)samples); + put_u16be(&dest[8], (uint16_t)width); + put_u16be(&dest[10], (uint16_t)height); + + /* map destination regions */ + metastart = dest + 12; + { + uint8_t *p = metastart + metasize; + for (chnum = 0; chnum < channels; chnum++) + { + audiostart[chnum] = p; + p += 2 * samples; + } + for (; chnum < 16; chnum++) + audiostart[chnum] = NULL; + videostart = p; + videostride = 2 * width; + } + + srcoffs = 10u + 2u * channels; + + /* metadata: raw copy */ + if (metasize > 0) + { + memcpy(metastart, src + srcoffs, metasize); + srcoffs += metasize; + } + + /* audio */ + if (channels > 0) + { + chd_error err = decode_audio(avhuff, channels, samples, src + srcoffs, + audiostart, &src[8]); + if (err != CHDERR_NONE) + return err; + + treesize = get_u16be(&src[8]); + if (treesize != 0xffff) + srcoffs += treesize; + for (chnum = 0; chnum < channels; chnum++) + srcoffs += get_u16be(&src[10 + 2 * chnum]); + } + + /* video (lossless only) */ + if (width > 0 && height > 0) + { + chd_error err; + if (srcoffs >= complen) + return CHDERR_INVALID_DATA; + /* reject non-lossless (MSB of first byte must be set) */ + if (!(src[srcoffs] & 0x80)) + return CHDERR_DECOMPRESSION_ERROR; + err = decode_video_lossless(avhuff, width, height, + src + srcoffs, complen - srcoffs, + videostart, videostride); + if (err != CHDERR_NONE) + return err; + } + + /* zero-pad any trailing space to match hunkbytes */ + written = header_bytes + payload_bytes; + if (written < destlen) + memset(dest + written, 0, destlen - written); + + return CHDERR_NONE; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b4da3ed..550c757 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,9 @@ add_executable(chdr-benchmark benchmark.c) target_link_libraries(chdr-benchmark PRIVATE chdr-static) +add_executable(chdr-avhuff-regression avhuff_regression.c) +target_link_libraries(chdr-avhuff-regression PRIVATE chdr-static) + # fuzzing if(BUILD_FUZZER) add_executable(chdr-fuzz fuzz.c) diff --git a/tests/avhuff_corpus/.gitignore b/tests/avhuff_corpus/.gitignore new file mode 100644 index 0000000..0c06cfe --- /dev/null +++ b/tests/avhuff_corpus/.gitignore @@ -0,0 +1,9 @@ +# Don't commit downloaded test artifacts (large + redistributable from upstream) +*.chd +*.avi +*.bin +*.raw +!.gitignore +!README.md +!fetch.sh +!run_regression.sh diff --git a/tests/avhuff_corpus/README.md b/tests/avhuff_corpus/README.md new file mode 100644 index 0000000..cb811fd --- /dev/null +++ b/tests/avhuff_corpus/README.md @@ -0,0 +1,30 @@ +# AVHuff regression corpus + +Small, redistributable AVHuff CHDs to verify libchdr's AVHuff decoder +produces byte-identical output to MAME's `chdman`. + +The actual `.chd` / `.avi` payloads are git-ignored; pull them with +`./fetch.sh` (needs `curl`, `chdman`, `ffmpeg`). + +Sources: + +* `regtest/` — verbatim copies of MAME's `regtests/chdman/{input,output}/createld_avi_*` + files (BSD-3-Clause / GPL-2). Video-only, 624x176, 6 hunks. + +* `synth/` — synthesized via `chdman createld` from the regtest input AVIs + with a 440 Hz sine track muxed in. Two variants: + - `avhu_only.chd`: `-c avhu`, FLAC audio (chdman 0.264 default) + - `flac_audio.chd`: `-c flac,avhu`, dual-codec hunks + +Run the harness: + + cmake --build build --target chdr-avhuff-regression + ./build/tests/chdr-avhuff-regression \ + tests/avhuff_corpus/regtest/createld_avi_yuv2_3_frames_no_audio/out.chd \ + tests/avhuff_corpus/regtest/createld_avi_uyvy_3_frames_no_audio/out.chd \ + tests/avhuff_corpus/synth/avhu_only.chd \ + tests/avhuff_corpus/synth/flac_audio.chd + +The harness relies on libchdr's built-in CRC16 verification (`VERIFY_BLOCK_CRC=1`, +default). Any byte-level decode error fails the per-hunk CRC and `chd_read` +returns `CHDERR_DECOMPRESSION_ERROR`. diff --git a/tests/avhuff_corpus/fetch.sh b/tests/avhuff_corpus/fetch.sh new file mode 100755 index 0000000..c9a1e6a --- /dev/null +++ b/tests/avhuff_corpus/fetch.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Fetch + synthesize the AVHuff regression corpus. +# See README.md for details. Idempotent; existing files are preserved. + +set -eu + +cd "$(dirname "$0")" + +MAME_RAW="https://raw.githubusercontent.com/mamedev/mame/master/regtests/chdman" + +mkdir -p regtest synth + +for d in createld_avi_yuv2_3_frames_no_audio createld_avi_uyvy_3_frames_no_audio; do + mkdir -p "regtest/$d" + [ -f "regtest/$d/out.chd" ] || \ + curl -fsSL -o "regtest/$d/out.chd" "$MAME_RAW/output/$d/out.chd" + [ -f "regtest/$d/in.avi" ] || \ + curl -fsSL -o "regtest/$d/in.avi" "$MAME_RAW/input/$d/in.avi" +done + +# Synth: take the YUY2 input and mux in a short 48 kHz stereo sine track, +# then encode with chdman. +if [ ! -f synth/in_with_audio.avi ]; then + ffmpeg -hide_banner -loglevel error -y \ + -i regtest/createld_avi_yuv2_3_frames_no_audio/in.avi \ + -f lavfi -i "sine=frequency=440:sample_rate=48000:duration=0.063" \ + -c:v copy -c:a pcm_s16le -ar 48000 -ac 2 -shortest \ + synth/in_with_audio.avi +fi + +[ -f synth/avhu_only.chd ] || \ + chdman createld -i synth/in_with_audio.avi -o synth/avhu_only.chd -c avhu -f +[ -f synth/flac_audio.chd ] || \ + chdman createld -i synth/in_with_audio.avi -o synth/flac_audio.chd -c flac,avhu -f + +echo "corpus ready" +ls -la regtest/*/out.chd synth/*.chd diff --git a/tests/avhuff_regression.c b/tests/avhuff_regression.c new file mode 100644 index 0000000..2a686ee --- /dev/null +++ b/tests/avhuff_regression.c @@ -0,0 +1,83 @@ +/* + * AVHuff regression harness for libchdr. + * + * Decode every hunk of a CHD via libchdr. Built-in CRC16 verification + * (VERIFY_BLOCK_CRC=1) catches any byte-level decode error: a mismatched + * decoded hunk fails CRC and chd_read returns CHDERR_DECOMPRESSION_ERROR. + * + * Usage: avhuff_regression [ ...] + * Exit status: 0 = all hunks decode + CRC clean; 1 = any failure. + */ + +#include +#include +#include + +#include "libchdr/chd.h" + +static int run_one(const char *path) +{ + chd_file *chd = NULL; + const chd_header *hdr; + uint8_t *buf; + uint32_t i; + chd_error err; + + err = chd_open(path, CHD_OPEN_READ, NULL, &chd); + if (err != CHDERR_NONE) { + fprintf(stderr, "[FAIL] open %s: %s\n", path, chd_error_string(err)); + return 1; + } + + hdr = chd_get_header(chd); + if (hdr == NULL || hdr->hunkbytes == 0) { + fprintf(stderr, "[FAIL] %s: no header\n", path); + chd_close(chd); + return 1; + } + + printf("[INFO] %s: v%u, %u hunks of %u bytes, codecs=[0x%08x 0x%08x 0x%08x 0x%08x]\n", + path, hdr->version, hdr->totalhunks, hdr->hunkbytes, + hdr->compression[0], hdr->compression[1], + hdr->compression[2], hdr->compression[3]); + + buf = (uint8_t *)malloc(hdr->hunkbytes); + if (buf == NULL) { + fprintf(stderr, "[FAIL] OOM\n"); + chd_close(chd); + return 1; + } + + for (i = 0; i < hdr->totalhunks; i++) { + err = chd_read(chd, i, buf); + if (err != CHDERR_NONE) { + fprintf(stderr, "[FAIL] %s: hunk %u: %s\n", + path, i, chd_error_string(err)); + free(buf); + chd_close(chd); + return 1; + } + } + + printf("[PASS] %s: %u/%u hunks decoded + CRC verified\n", + path, hdr->totalhunks, hdr->totalhunks); + free(buf); + chd_close(chd); + return 0; +} + +int main(int argc, char **argv) +{ + int rc = 0; + int i; + + if (argc < 2) { + fprintf(stderr, "usage: %s [ ...]\n", argv[0]); + return 2; + } + + for (i = 1; i < argc; i++) + rc |= run_one(argv[i]); + + return rc; +}