ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PCSX2 中的 libchdr:MAME CHD 压缩光盘格式的读取实现深度解析

PCSX2 中的 libchdr:MAME CHD 压缩光盘格式的读取实现深度解析 PCSX2 中的 libchdrMAME CHD 压缩光盘格式的读取实现深度解析【免费下载链接】pcsx2PCSX2 - The Playstation 2 Emulator项目地址: https://gitcode.com/GitHub_Trending/pc/pcsx2导读CHDCompressed Hunks of Data是 MAME 项目中广泛使用的一种面向整盘镜像的高效压缩格式尤其适合 CD/DVD/GD-ROM 光盘镜像的分轨存储与冗余压缩。本文以 PCSX2 仓库内嵌的 libchdr 库为主线梳理 CHD v1–v5 文件格式的磁盘布局、压缩编码器体系、公开 C API 的调用方式并结合 ChdFileReader.cpp 展示 PCSX2 如何将其接入光盘读取流水线。读完本文你将掌握 CHD 的头部结构、map 映射机制、hunk 读取流程以及如何在自己的模拟器或工具项目中集成 libchdr。libchdr 是什么独立于 MAME 的 CHD 读取库在 PCSX2 的第三方依赖目录3rdparty/libchdr中README.md对其定位给出了明确的定义libchdr is a standalone library for reading MAMEs CHDv1-v5 formats.即一个用于读取MAME CHD v1 到 v5 全部版本格式的独立库。它并不负责生成 CHD 文件写路径由 MAME 的 chdman 工具承担而是聚焦于打开、解析、解压和读取 CHD 镜像。其代码来源分两部分见 README.mdCHD v1–v4 读取逻辑移植自 MAME 早期的 C 代码库并移除了所有与操作系统强相关的特性使其可以在 Windows、Linux、macOS 等多平台下编译CHD v5 支持从 MAME 当前的 C 代码库反移植backport而来补齐了 v5 引入的 map 压缩、unit 概念等新机制。libchdr 采用BSD 3-Clause许可见 LICENSE.txt其内部使用的第三方库如 dr_flac、zlib、LZMA、Zstd各自遵循自身许可条款见 deps/。从 CMakeLists.txt 可以看到库本身以纯 C 编写仅链接三个外部压缩库add_library(libchdr include/dr_libs/dr_flac.h include/libchdr/bitstream.h include/libchdr/cdrom.h include/libchdr/chd.h include/libchdr/chdconfig.h include/libchdr/coretypes.h include/libchdr/flac.h include/libchdr/huffman.h src/libchdr_bitstream.c src/libchdr_cdrom.c src/libchdr_chd.c src/libchdr_flac.c src/libchdr_huffman.c ) target_include_directories(libchdr PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_link_libraries(libchdr PRIVATE ZLIB::ZLIB LZMA::LZMA Zstd::Zstd)编译后对外暴露的头文件目录为include/其中libchdr/chd.h是全部公共 API 的入口。CHD 文件格式从 v1 到 v5 的演进CHD 全称 Compressed Hunks of Data其核心思想是把整张光盘/硬盘的原始数据按固定大小的hunk块切分每个 hunk 独立压缩并辅以一张map映射表记录每个 hunk 的压缩类型、压缩后长度与文件偏移。这样随机访问任意 hunk 时无需解压整个镜像这正是模拟器场景最需要的特性。chd.h 中完整保留了各版本头部结构的磁盘布局注释以下按版本梳理。v1 / v2 头部76 / 80 字节v1 与 v2 的头部字段基本一致v2 仅在末尾多出seclen每扇区字节数字段偏移字段说明0tag[8]魔数MComprHD8length头部长度含 tag 与 length 字段12version格式版本号16flags全局标志见下文20compression压缩类型24hunksize每个 hunk 包含的扇区数28totalhunks文件总的 hunk 数32–43cylinders/heads/sectors硬盘几何参数柱面/磁头/扇区44md5[16]原始数据 MD5 校验60parentmd5[16]父文件 MD5 校验76v2seclen每扇区字节数全局标志Flags定义如下见 chd.h#define CHDFLAGS_HAS_PARENT 0x00000001 /* 该镜像有父文件 */ #define CHDFLAGS_IS_WRITEABLE 0x00000002 /* 允许写入 */ #define CHDFLAGS_UNDEFINED 0xfffffffc /* 其余位保留 */v3 / v4 头部120 / 108 字节从 v3 开始格式发生了两个重要变化扇区概念让位于 hunk 字节数v1/v2 用每 hunk 的扇区数 × 固定扇区大小描述块大小v3 起直接以hunkbytes每 hunk 字节数描述并引入 64 位logicalbytes逻辑数据总字节数校验算法从 MD5 升级为 SHA1v3 的头部依次存放md5、parentmd5、sha1、parentsha1v4 进一步细化为sha1rawmeta 联合、parentsha1、rawsha1仅原始数据。v4 中rawsha1的出现意味着元数据TOC 等被独立校验sha1则覆盖 raw 与 meta 的联合摘要这是 v4 校验体系相对 v3 的关键改进。v5 头部124 字节现代 CHD 的基石v5 是当前 libchdr 支持的最终版本头部布局如下chd.h偏移字段说明0tag[8]MComprHD8length头部长度12version格式版本号516compressors[4]使用的 4 个自定义压缩器标识32logicalbytes逻辑数据总字节数40mapoffsetmap 表在文件中的偏移48metaoffset第一个元数据块的偏移56hunkbytes每 hunk 字节数上限 512 KB60unitbyteshunk 内每个 unit 的字节数64rawsha1[20]原始数据 SHA184sha1[20]rawmeta 联合 SHA1104parentsha1[20]父文件联合 SHA1v5 的关键规则头文件注释明确说明若parentsha1非全零则存在父文件——不再需要 v1–v4 的 flags 位来标记 parent 关系若compressors[0] 0则整个文件含 map不压缩unitbytes/unit是 v5 新引入的概念一个 hunk 被划分为若干 unit父文件引用、自引用等均以 unit 为粒度表达这使得跨文件去重父镜像差分成为可能。v5 的 map 分为两种形态未压缩 map每个 entry 仅一个uint32_t offset值为“起始偏移 / hunk 大小”压缩 map头部 16 字节包含length压缩后长度、datastart首个数据块的偏移UINT48、crcmap 的 CRC-16、lengthbits编码 complength 所需位数、hunkbits自引用编码位数、parentunitbits父 unit 引用编码位数等参数条目按位压缩存储。每个展开后的压缩 map entry 固定 12 字节语义[ 0] uint8_t compression; // 压缩类型 [ 1] UINT24 complength; // 压缩后长度 [ 4] UINT48 offset; // 数据偏移 [ 10] uint16_t crc; // 数据 CRC-16头文件为各版本定义了明确的大小常量便于解析器按版本分支#define CHD_HEADER_VERSION 5 #define CHD_V1_HEADER_SIZE 76 #define CHD_V2_HEADER_SIZE 80 #define CHD_V3_HEADER_SIZE 120 #define CHD_V4_HEADER_SIZE 108 #define CHD_V5_HEADER_SIZE 124 #define CHD_MAX_HEADER_SIZE CHD_V5_HEADER_SIZE压缩编码器体系codec 标识与 hunk 条目类型全局压缩类型v1–v4#define CHDCOMPRESSION_NONE 0 /* 不压缩 */ #define CHDCOMPRESSION_ZLIB 1 #define CHDCOMPRESSION_ZLIB_PLUS 2 #define CHDCOMPRESSION_AV 3编码器 tagv5 及 CD 前端v5 使用四个字符 tag 标识实际压缩器libchdr 支持以下 codecchd.h#define CHD_CODEC_NONE 0 #define CHD_CODEC_ZLIB CHD_MAKE_TAG(z,l,i,b) #define CHD_CODEC_LZMA CHD_MAKE_TAG(l,z,m,a) #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) /* 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) #define CHD_CODEC_CD_FLAC CHD_MAKE_TAG(c,d,f,l) #define CHD_CODEC_CD_ZSTD CHD_MAKE_TAG(c,d,z,s)其中cd*系列是带 CD 前端处理的专用编码器CD 轨道数据包含 2352 字节的原始扇区与 96 字节子通道subcode在压缩前需要先做 ECC/EDC 剥离等预处理因此与通用 codec 分开标识。在实现层src/libchdr_chd.c中v5 的 hunk 条目类型枚举进一步细分了“压缩类型”的语义COMPRESSION_TYPE_0 0, /* codec #0 */ COMPRESSION_TYPE_1 1, /* codec #1 */ COMPRESSION_TYPE_2 2, /* codec #2 */ COMPRESSION_TYPE_3 3, /* codec #3 */ COMPRESSION_NONE 4, /* 不压缩长度隐式等于 hunkbytes */ COMPRESSION_SELF 5, /* 引用本文件中的另一块 */ COMPRESSION_PARENT 6, /* 引用父文件中的一整 hunk unit */ /* 以下为压缩 map 编码中使用的伪类型 */ COMPRESSION_RLE_SMALL, /* 4 位长度的 RLE 游程 */ COMPRESSION_RLE_LARGE, /* 8 位长度的 RLE 游程 */ COMPRESSION_SELF_0, /* 同上一个 COMPRESSION_SELF 块 */ COMPRESSION_SELF_1, /* 上一个 SELF 块 1 */ COMPRESSION_PARENT_SELF, /* 父文件中同一位置块 */ COMPRESSION_PARENT_0, /* 同上一个 COMPRESSION_PARENT 块 */与之对应v3/v4 时代使用另一套 7 种 entry 类型V34_MAP_ENTRY_TYPE_*包括COMPRESSED、UNCOMPRESSED、MINI直接把数据塞进 offset 字段、SELF_HUNK、PARENT_HUNK、2ND_COMPRESSED通常为 FLAC CDDA 二次压缩等。这种类型枚举 按位压缩 map的设计让 v5 在存储同一镜像时能获得比 v4 更激进的压缩率相同内容块之间用 SELF 引用去重与父镜像相同的部分用 PARENT 引用彻底省略RLE 伪类型进一步压缩连续重复条目的编码开销。辅助模块Huffman、FLAC 与位流libchdr 自带三个轻量解压/解码组件huffman.h静态 Huffman 解码器支持从直方图重建树、规范编码分配canonical codes与 lookup 表构建用于 map 与部分数据的位级解码flac.h基于dr_libs/dr_flac.h的 FLAC 解码封装负责 CDDA 音频轨道每 hunk 8 帧的解压支持最多 8 路交错流与端序交换bitstream.h位级读取器提供bitstream_read、bitstream_peek、bitstream_remove、bitstream_flush等操作是所有按位压缩数据Huffman、压缩 map的底层基础设施。公开 API打开、读取与元数据chd.h 以CHD_EXPORT宏导出的公共接口可分为四组。文件管理chd_error chd_open_core_file(core_file *file, int mode, chd_file *parent, chd_file **chd); chd_error chd_open_file(FILE *file, int mode, chd_file *parent, chd_file **chd); chd_error chd_open(const char *filename, int mode, chd_file *parent, chd_file **chd); chd_error chd_precache(chd_file *chd); /* 预读底层文件到内存 */ void chd_close(chd_file *chd); core_file *chd_core_file(chd_file *chd); const char *chd_error_string(chd_error err);打开模式仅两种#define CHD_OPEN_READ 1 #define CHD_OPEN_READWRITE 2chd_open_core_file是 PCSX2 实际使用的变体——它接受一个自定义core_file抽象让调用方可以完全控制底层 FILE 句柄的生命周期见下文 PCSX2 集成一节。头部管理const chd_header *chd_get_header(chd_file *chd); chd_error chd_read_header_core_file(core_file *file, chd_header *header); chd_error chd_read_header_file(FILE *file, chd_header *header); chd_error chd_read_header(const char *filename, chd_header *header);chd_header是解析后的内存结构非磁盘原始布局成员包括version、flags、compression[4]、hunkbytes、totalhunks、logicalbytes、metaoffset、mapoffset、md5、sha1、rawsha1、parentsha1、unitbytes、mapentrybytes、rawmap以及标记为 obsolete 的obsolete_cylinders等历史字段。chd_read_header*系列允许在不打开整个 CHD 的情况下仅解析头部——PCSX2 用它快速探测文件是否为合法 CHD 并读取父文件信息。核心数据读取chd_error chd_read(chd_file *chd, uint32_t hunknum, void *buffer);这是随机读取的唯一入口传入 hunk 序号libchdr 依据 map 找到该 hunk 的压缩类型、偏移与长度调用对应 codec 解压到调用方缓冲区。实现中还有chd_precache配合的整文件预读路径见 src/libchdr_chd.c 的PRINTF_MAX_HUNK调试开关与MAP_STACK_ENTRIES栈上 map 缓存优化栈上最多缓存 512 个 map 条目。元数据管理chd_error chd_get_metadata(chd_file *chd, uint32_t searchtag, uint32_t searchindex, void *output, uint32_t outputlen, uint32_t *resultlen, uint32_t *resulttag, uint8_t *resultflags);CHD 的元数据以tag 索引寻址libchdr 预定义了一批标准 tagchd.h/* 硬盘元数据 */ #define HARD_DISK_METADATA_TAG CHD_MAKE_TAG(G,D,D,D) #define HARD_DISK_METADATA_FORMAT CYLS:%d,HEADS:%d,SECS:%d,BPS:%d #define HARD_DISK_IDENT_METADATA_TAG CHD_MAKE_TAG(I,D,N,T) #define HARD_DISK_KEY_METADATA_TAG CHD_MAKE_TAG(K,E,Y, ) /* CD-ROM 轨道元数据 */ #define CDROM_OLD_METADATA_TAG CHD_MAKE_TAG(C,H,C,D) #define CDROM_TRACK_METADATA_TAG CHD_MAKE_TAG(C,H,T,R) #define CDROM_TRACK_METADATA_FORMAT TRACK:%d TYPE:%s SUBTYPE:%s FRAMES:%d #define CDROM_TRACK_METADATA2_TAG CHD_MAKE_TAG(C,H,T,2) #define CDROM_TRACK_METADATA2_FORMAT TRACK:%d TYPE:%s SUBTYPE:%s FRAMES:%d PREGAP:%d PGTYPE:%s PGSUB:%s POSTGAP:%d #define GDROM_OLD_METADATA_TAG CHD_MAKE_TAG(C,H,G,T) #define GDROM_TRACK_METADATA_TAG CHD_MAKE_TAG(C, H, G, D) #define GDROM_TRACK_METADATA_FORMAT TRACK:%d TYPE:%s SUBTYPE:%s FRAMES:%d PAD:%d PREGAP:%d PGTYPE:%s PGSUB:%s POSTGAP:%d /* A/V 元数据 */ #define AV_METADATA_TAG CHD_MAKE_TAG(A,V,A,V) #define AV_METADATA_FORMAT FPS:%d.%06d WIDTH:%d HEIGHT:%d INTERLACED:%d CHANNELS:%d SAMPLERATE:%d #define AV_LD_METADATA_TAG CHD_MAKE_TAG(A,V,L,D)错误码体系libchdr 用统一的chd_error枚举表达所有失败原因从CHDERR_NONE、CHDERR_INVALID_FILE、CHDERR_REQUIRES_PARENT、CHDERR_HUNK_OUT_OF_RANGE、CHDERR_DECOMPRESSION_ERROR直到CHDERR_UNSUPPORTED_FORMAT共 30 余种完整列表见 chd.h 的enum _chd_error。chd_error_string(err)返回对应的可读字符串便于直接写入日志或错误对话框。PCSX2 中的集成ChdFileReader 读取流水线PCSX2 在 pcsx2/CDVD/ChdFileReader.cpp 中实现ChdFileReader把 libchdr 封装为ThreadedFileReader的一个具体子类供 CDVD 光盘读取层统一调用。其核心调用链清晰地展示了上文各 API 的实战用法。打开与父文件处理ChdFileReader定义了一个ChdCoreFileWrapperChdFileReader.cpp实现了 libchdr 的core_file抽象——它把fsize、fread、fclose、fseek四个函数指针指向自己的静态包装函数从而精确控制底层FILE*的释放时机并支持整文件预读缓存。打开流程OpenCHD为用chd_read_header_file解析头部校验是否为合法 CHD若头部存在父文件parentsha1非空尝试按文件名约定xxx.chd的父文件通常为xxx.chd同目录下的原始盘或另一 CHD打开父文件递归调用自身打开父 CHD 得到parent_chd调用chd_open_core_file(core_wrapper-GetCoreFile(), CHD_OPEN_READ, parent_chd, chd)完成打开。PCSX2 还实现了 CHD 头部的哈希缓存s_chd_hash_cache见 ChdFileReader.cpp避免反复打开同一文件时重复解析头部父文件链深度上限为MAX_PARENTS 32。读取参数提取与 TOC 解析打开成功后Open2从chd_get_header提取关键尺寸const chd_header* chd_header chd_get_header(ChdFile); hunk_size chd_header-hunkbytes; // CHD 喜欢用完整的 2448 字节块但保留了源 ISO 的 24 偏移 // PCSX2 其余部分使用 2448 字节缓冲区容纳不下因此裁剪块 m_internalBlockSize chd_header-unitbytes;随后ParseTOC通过chd_get_metadata逐条读取轨道元数据ChdFileReader.cpp优先尝试CDROM_TRACK_METADATA2_TAGv2 轨道格式含 PREGAP/POSTGAP 字段用CDROM_TRACK_METADATA2_FORMAT解析出track_num, type, subtype, frames, pregap_frames, pgtype, pgsub, postgap_frames共 8 个字段失败则回退到旧版CDROM_TRACK_METADATA_TAGCHTR解析 4 个字段由于 PCSX2 目前不支持多轨 CD非 1 号轨道会被跳过并打印警告所有轨道含 pre/postgap的帧数累加得到真实的镜像总帧数——因为 CHD 头部记录的logicalbytes会把每条轨道补齐到 4 帧的整数倍见CD_TRACK_PADDING直接使用会得到偏大的文件大小。// The file size in the header is incorrect, each track gets padded to a // multiple of 4 frames. Instead, we pull the real frame count from the TOC.随机读取与预读ReadChunk是实际数据读取路径ChdFileReader.cppchd_error error chd_read(ChdFile, chunkID, dst); if (error ! CHDERR_NONE) { Console.Error(CDVD: chd_read returned error: %s, chd_error_string(error)); return 0; } return hunk_size;上层通过ChunkForOffset把线性偏移换算为hunk 序号 offset / hunk_size。同时Precache2在内存允许时调用ChdCoreFileWrapper::Precache把整个 CHD含父文件读入内存配合chd_precache语义大幅减少运行时的随机 I/O。关闭与生命周期Close2调用chd_close(ChdFile)释放 libchdr 内部状态由于ChdCoreFileWrapper在chd_open_core_file失败时也会被一并销毁见类注释不会出现句柄泄漏。特性开关chdconfig.hchdconfig.h 是 libchdr 的编译期特性配置PCSX2 构建时全部开启#define WANT_RAW_DATA_SECTOR 1 /* 启用 ECC 校验/生成cdrom.h 中的 ecc_verify/ecc_generate/ecc_clear */ #define WANT_SUBCODE 1 /* 启用 96 字节子通道数据支持 */ #define NEED_CACHE_HUNK 1 /* 启用 hunk 缓存 */ #define VERIFY_BLOCK_CRC 1 /* 读取时校验块 CRC-16 */WANT_RAW_DATA_SECTOR控制 cdrom.h 中ecc_verify、ecc_generate、ecc_clear三个 ECC 工具函数的编译WANT_SUBCODE决定是否保留每条轨道的 subcode 数据VERIFY_BLOCK_CRC开启后每次读取都会校验 map 中记录的 CRC-16发现损坏块立即报错对存档类应用尤为关键。CD-ROM 语义轨道类型与地址换算cdrom.h 定义了 CD 相关常量与工具#define CD_TRACK_PADDING (4) /* 轨道按 4 帧的整数倍补齐 */ #define CD_MAX_TRACKS (99) #define CD_MAX_SECTOR_DATA (2352) /* 原始扇区字节数 */ #define CD_MAX_SUBCODE_DATA (96) /* 子通道字节数 */ #define CD_FRAME_SIZE (CD_MAX_SECTOR_DATA CD_MAX_SUBCODE_DATA) #define CD_FRAMES_PER_HUNK (8) /* 每 hunk 8 帧 */轨道类型枚举覆盖 Mode1、Mode2含 Form1/Form2/MIX、RAW 与红皮书音频CD_TRACK_MODE1, /* mode 1 2048 字节/扇区 */ CD_TRACK_MODE1_RAW, /* mode 1 2352 字节/扇区 */ CD_TRACK_MODE2, /* mode 2 2336 字节/扇区 */ CD_TRACK_MODE2_FORM1, /* mode 2 2048 字节/扇区 */ CD_TRACK_MODE2_FORM2, /* mode 2 2324 字节/扇区 */ CD_TRACK_MODE2_FORM_MIX, /* mode 2 2336 字节/扇区 */ CD_TRACK_MODE2_RAW, /* mode 2 2352 字节/扇区 */ CD_TRACK_AUDIO, /* 红皮书音频 2352 字节/扇区588 采样 */ CD_TRACK_RAW_DONTCARE /* 读取时有什么返回什么的特殊标志 */此外还提供了 MSF分:秒:帧与 LBA逻辑块地址互转的内联函数msf_to_lba、lba_to_msf以及 GD-ROM 相关的CD_FLAG_GDROM/CD_FLAG_GDROMLE遗留小端 CDDA 数据标志为 Dreamcast GD-ROM 镜像提供支持。如何在你的项目中集成 libchdrlibchdr 的集成路径清晰且依赖极少引入源码将3rdparty/libchdr整个目录复制到项目参照 CMakeLists.txt 添加静态库目标或在构建系统中直接编译src/下 5 个.c文件链接依赖需要 zlib、LZMAlzma 解码器见 src/libchdr_chd.c 中的#include LzmaDec.h与 ZstdPCSX2 仓库在 3rdparty/lzma、3rdparty/zstd 相关依赖 等处已有现成版本包含头文件#include libchdr/chd.h即可获得全部 API打开并读取#include libchdr/chd.h chd_file* chd NULL; chd_error err chd_open(game.chd, CHD_OPEN_READ, NULL, chd); if (err ! CHDERR_NONE) { fprintf(stderr, open failed: %s\n, chd_error_string(err)); return -1; } const chd_header* hdr chd_get_header(chd); void* buf malloc(hdr-hunkbytes); uint32_t hunk 0; /* 需要读取的 hunk 序号 */ err chd_read(chd, hunk, buf); if (err ! CHDERR_NONE) { fprintf(stderr, read failed: %s\n, chd_error_string(err)); } chd_close(chd);处理父文件若chd_get_header返回的parentsha1非空需要先打开父 CHD 并把句柄作为chd_open的parent参数传入否则会返回CHDERR_REQUIRES_PARENT。小结libchdr 以纯 C 的轻量实现完整覆盖了 MAME CHD v1–v5 的读取语义从 v1/v2 的硬盘几何模型到 v3/v4 的 SHA1 校验体系再到 v5 的 map 压缩、unit 差分与 4 路 codec 架构。在 PCSX2 中它以core_file抽象接入ChdFileReader配合父文件链、TOC 解析与整文件预读为 CD/DVD 镜像提供了随机读取与校验保障。如果你需要在自己的模拟器或工具中支持 CHD 镜像直接复用 3rdparty/libchdr 是一个经过生产级验证的可靠选择。【免费下载链接】pcsx2PCSX2 - The Playstation 2 Emulator项目地址: https://gitcode.com/GitHub_Trending/pc/pcsx2创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表