diff --git a/Assets/dll/chd_capi.dll b/Assets/dll/chd_capi.dll new file mode 100644 index 0000000000..68473aa1d3 Binary files /dev/null and b/Assets/dll/chd_capi.dll differ diff --git a/Assets/dll/chdr.dll b/Assets/dll/chdr.dll deleted file mode 100644 index 88b1449b68..0000000000 Binary files a/Assets/dll/chdr.dll and /dev/null differ diff --git a/Assets/dll/libchdr.so b/Assets/dll/libchdr.so deleted file mode 100755 index fb1ec4c16a..0000000000 Binary files a/Assets/dll/libchdr.so and /dev/null differ diff --git a/ExternalProjects/libchd-rs-capi/.gitignore b/ExternalProjects/libchd-rs-capi/.gitignore new file mode 100644 index 0000000000..24d64373c4 --- /dev/null +++ b/ExternalProjects/libchd-rs-capi/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/ExternalProjects/libchd-rs-capi/Cargo.lock b/ExternalProjects/libchd-rs-capi/Cargo.lock new file mode 100644 index 0000000000..22bd31cf9c --- /dev/null +++ b/ExternalProjects/libchd-rs-capi/Cargo.lock @@ -0,0 +1,243 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "bitreader" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd859c9d97f7c468252795b35aeccc412bdbb1e90ee6969c4fa6328272eaeff" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chd" +version = "0.3.1" +source = "git+https://github.com/SnowflakePowered/chd-rs#891b4296dccbe5be383284fd22b7a015956d43e6" +dependencies = [ + "arrayvec", + "bitreader", + "byteorder", + "claxon", + "crc", + "flate2", + "lzma-rs-perf-exp", + "num-derive", + "num-traits", + "ruzstd", + "take_mut", + "text_io", +] + +[[package]] +name = "chd-capi" +version = "0.3.1" +dependencies = [ + "chd", +] + +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "lzma-rs-perf-exp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38435c1305548bb408c98242841c3cf161246323e72a3e4433787f3c05bf18ee" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ruzstd" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5174a470eeb535a721ae9fdd6e291c2411a906b96592182d05217591d5c5cf7b" +dependencies = [ + "byteorder", + "derive_more", + "twox-hash", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "text_io" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f0c8eb2ad70c12a6a69508f499b3051c924f4b1cfeae85bfad96e6bc5bba46" + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/ExternalProjects/libchd-rs-capi/Cargo.toml b/ExternalProjects/libchd-rs-capi/Cargo.toml new file mode 100644 index 0000000000..ddfffbd7e4 --- /dev/null +++ b/ExternalProjects/libchd-rs-capi/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "chd-capi" +version = "0.3.1" +edition = "2021" +authors = ["Ronny Chan "] +description = "libchdr-compatible C API for a Rust implementation of the CHD File Format" +license = "BSD-3-Clause" +repository = "https://github.com/SnowflakePowered/chd-rs" +readme = "README.md" +categories = ["emulators", "compression", "encoding"] +keywords = ["mame", "chd", "decompression"] + +[lib] +doctest = false +crate-type = ["cdylib"] + +[features] +default = [] +chd_precache = [] +verify_block_crc = ["chd/verify_block_crc"] + +[dependencies] +chd = { version = "0.3.1", git = "https://github.com/SnowflakePowered/chd-rs" } diff --git a/ExternalProjects/libchd-rs-capi/README.md b/ExternalProjects/libchd-rs-capi/README.md new file mode 100644 index 0000000000..911275fb50 --- /dev/null +++ b/ExternalProjects/libchd-rs-capi/README.md @@ -0,0 +1,43 @@ +# `chd-capi` + +⚠️*The C API has not been heavily tested. Use at your own risk.* ⚠️ + +chd-rs provides a C API compatible with [chd.h](https://github.com/rtissera/libchdr/blob/6eeb6abc4adc094d489c8ba8cafdcff9ff61251b/include/libchdr/chd.h). +ABI compatibility is detailed below but is untested when compiling as a dynamic library. The intended consumption for this crate is not via cargo, but by vendoring +the [sources of the C API](https://github.com/SnowflakePowered/chd-rs/tree/master/chd-rs-capi) in tree, along with a compatible `libchdcorefile` implementation +for your platform. + +## Features +### `verify_block_crc` +Enables the `verify_block_crc` of the `chd` crate to verify decompressed CHD hunks with their internal hash. + +### `chd_core_file` +Enables `core_file*` and the`chd_open_file`, and `chd_core_file` APIs. This feature requires a `libchdcorefile` implementation, +or the default POSIX compatible implementation (where `core_file*` is `FILE*`) will be used. + +Note that by default, `core_file*` is not an opaque pointer and is a C `FILE*` stream. This allows the underlying +file pointer to be changed unsafely beneath the memory safety guarantees of chd-rs. We strongly encourage using +`chd_open` instead of `chd_open_file`. + +If you need `core_file*` support, chd-capi should have the `chd_core_file` feature enabled, which will wrap +`FILE*` to be usable in Rust with a lightweight wrapper in `libchdcorefile`. If the default implementation +is not suitable, you may need to implement `libchdcorefile` yourself. The `chd_core_file` feature requires +CMake and Clang to be installed. + +### `chd_virtio` +Enables the [virtual I/O](https://github.com/rtissera/libchdr/pull/78) functions `chd_open_core_file`. +Because this C API requires `core_file` to be an opaque pointer, there is no difference between `chd_open_file` and +`chd_open_core_file` unlike libchdr, and `chd_open_core_file` is simply an alias for `chd_open_file`. All functions that +take `core_file*` require a `libchdcorefile` implementation. + +### `chd_precache` +Enables precaching of the underlying file into memory with the `chd_precache_progress` and `chd_precache` functions. + +## ABI compatibility + +chd-rs makes the following ABI-compatibility guarantees compared to libchdr when compiled statically. +* `chd_error` is ABI and API-compatible with [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L258) +* `chd_header` is ABI and API-compatible [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L302) +* `chd_file *` is an opaque pointer. It is **not layout compatible** with [chd.c](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/src/libchdr_chd.c#L265) +* The layout of `core_file *` is user-defined when the `chd_core_file` feature is enabled. +* Freeing any pointer returned by chd-rs with `free` is undefined behaviour. The exception are `chd_file *` pointers which can be safely freed with `chd_close`. diff --git a/ExternalProjects/libchd-rs-capi/build_release.bat b/ExternalProjects/libchd-rs-capi/build_release.bat new file mode 100644 index 0000000000..66864f4529 --- /dev/null +++ b/ExternalProjects/libchd-rs-capi/build_release.bat @@ -0,0 +1,3 @@ +@cargo b --release +@copy target\release\chd_capi.dll ..\..\Assets\dll +@copy target\release\chd_capi.dll ..\..\output\dll diff --git a/ExternalProjects/libchd-rs-capi/build_release.sh b/ExternalProjects/libchd-rs-capi/build_release.sh new file mode 100755 index 0000000000..a71c0cb936 --- /dev/null +++ b/ExternalProjects/libchd-rs-capi/build_release.sh @@ -0,0 +1,9 @@ +#!/bin/sh +if [ -z "$BIZHAWKBUILD_HOME" ]; then export BIZHAWKBUILD_HOME="$(realpath "$(dirname "$0")/../..")"; fi + +cargo b --release + +cp target/release/libchd_capi.so "$BIZHAWKBUILD_HOME/Assets/dll" +if [ -e "$BIZHAWKBUILD_HOME/output" ]; then + cp target/release/libchd_capi.so "$BIZHAWKBUILD_HOME/output/dll" +fi diff --git a/ExternalProjects/libchd-rs-capi/src/header.rs b/ExternalProjects/libchd-rs-capi/src/header.rs new file mode 100644 index 0000000000..3f592adaeb --- /dev/null +++ b/ExternalProjects/libchd-rs-capi/src/header.rs @@ -0,0 +1,168 @@ +use crate::chd_file; +use chd::header::{Header, HeaderV1, HeaderV3, HeaderV4, HeaderV5}; +use chd::map::Map; +use std::mem; + +pub const CHD_MD5_BYTES: usize = 16; +pub const CHD_SHA1_BYTES: usize = 20; + +#[repr(C)] +#[allow(non_camel_case_types)] +/// libchdr-compatible CHD header struct. +/// This struct is ABI-compatible with [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L302) +pub struct chd_header { + length: u32, + version: u32, + flags: u32, + compression: [u32; 4], + hunkbytes: u32, + totalhunks: u32, + logicalbytes: u64, + metaoffset: u64, + mapoffset: u64, + md5: [u8; CHD_MD5_BYTES], + parentmd5: [u8; CHD_MD5_BYTES], + sha1: [u8; CHD_SHA1_BYTES], + rawsha1: [u8; CHD_SHA1_BYTES], + parentsha1: [u8; CHD_SHA1_BYTES], + unitbytes: u32, + unitcount: u64, + hunkcount: u32, + mapentrybytes: u32, + rawmap: *mut u8, + obsolete_cylinders: u32, + obsolete_sectors: u32, + obsolete_heads: u32, + obsolete_hunksize: u32, +} + +impl From<&HeaderV1> for chd_header { + fn from(header: &HeaderV1) -> Self { + chd_header { + length: header.length, + version: header.version as u32, + flags: header.flags, + compression: [header.compression, 0, 0, 0], + hunkbytes: header.hunk_bytes, + totalhunks: header.total_hunks, + logicalbytes: header.logical_bytes, + metaoffset: 0, + mapoffset: 0, + md5: header.md5, + parentmd5: header.parent_md5, + sha1: [0u8; CHD_SHA1_BYTES], + rawsha1: [0u8; CHD_SHA1_BYTES], + parentsha1: [0u8; CHD_SHA1_BYTES], + unitbytes: header.unit_bytes, + unitcount: header.unit_count, + hunkcount: header.total_hunks, + mapentrybytes: 0, + rawmap: std::ptr::null_mut(), + obsolete_cylinders: header.cylinders, + obsolete_sectors: header.sectors, + obsolete_heads: header.heads, + obsolete_hunksize: header.hunk_size, + } + } +} + +impl From<&HeaderV3> for chd_header { + fn from(header: &HeaderV3) -> Self { + chd_header { + length: header.length, + version: header.version as u32, + flags: header.flags, + compression: [header.compression, 0, 0, 0], + hunkbytes: header.hunk_bytes, + totalhunks: header.total_hunks, + logicalbytes: header.logical_bytes, + metaoffset: header.meta_offset, + mapoffset: 0, + md5: header.md5, + parentmd5: header.parent_md5, + sha1: header.sha1, + rawsha1: [0u8; CHD_SHA1_BYTES], + parentsha1: header.parent_sha1, + unitbytes: header.unit_bytes, + unitcount: header.unit_count, + hunkcount: header.total_hunks, + mapentrybytes: 0, + rawmap: std::ptr::null_mut(), + obsolete_cylinders: 0, + obsolete_sectors: 0, + obsolete_heads: 0, + obsolete_hunksize: 0, + } + } +} + +impl From<&HeaderV4> for chd_header { + fn from(header: &HeaderV4) -> Self { + chd_header { + length: header.length, + version: header.version as u32, + flags: header.flags, + compression: [header.compression, 0, 0, 0], + hunkbytes: header.hunk_bytes, + totalhunks: header.total_hunks, + logicalbytes: header.logical_bytes, + metaoffset: header.meta_offset, + mapoffset: 0, + md5: [0u8; CHD_MD5_BYTES], + parentmd5: [0u8; CHD_MD5_BYTES], + sha1: header.sha1, + rawsha1: header.raw_sha1, + parentsha1: header.parent_sha1, + unitbytes: header.unit_bytes, + unitcount: header.unit_count, + hunkcount: header.total_hunks, + mapentrybytes: 0, + rawmap: std::ptr::null_mut(), + obsolete_cylinders: 0, + obsolete_sectors: 0, + obsolete_heads: 0, + obsolete_hunksize: 0, + } + } +} + +pub(crate) fn get_v5_header(chd: &chd_file) -> chd_header { + let header: HeaderV5 = match chd.header() { + Header::V5Header(h) => h.clone(), + _ => unreachable!(), + }; + let mut map_data: Vec = match chd.map() { + Map::V5(map) => map.into(), + _ => unreachable!(), + }; + let version = header.version; + let map_ptr = map_data.as_mut_ptr(); + mem::forget(map_data); + + chd_header { + length: header.length, + version: version as u32, + // libchdr just reads garbage for V5 flags, we will give it as 0. + flags: 0, + compression: header.compression, + hunkbytes: header.hunk_bytes, + totalhunks: header.hunk_count, + logicalbytes: header.logical_bytes, + metaoffset: header.meta_offset, + mapoffset: header.map_offset, + md5: [0u8; CHD_MD5_BYTES], + parentmd5: [0u8; CHD_MD5_BYTES], + sha1: header.sha1, + rawsha1: header.raw_sha1, + parentsha1: header.parent_sha1, + unitbytes: header.unit_bytes, + unitcount: header.unit_count, + hunkcount: header.hunk_count, + mapentrybytes: header.map_entry_bytes, + rawmap: map_ptr, + obsolete_cylinders: 0, + obsolete_sectors: 0, + obsolete_heads: 0, + obsolete_hunksize: 0, + } +} diff --git a/ExternalProjects/libchd-rs-capi/src/lib.rs b/ExternalProjects/libchd-rs-capi/src/lib.rs new file mode 100644 index 0000000000..c85497d85b --- /dev/null +++ b/ExternalProjects/libchd-rs-capi/src/lib.rs @@ -0,0 +1,595 @@ +#![cfg_attr(docsrs, feature(doc_cfg, doc_cfg_hide))] +#![deny(unsafe_op_in_unsafe_fn)] +//! A (mostly) [libchdr](https://github.com/rtissera/libchdr) compatible C-API for [chd-rs](https://crates.io/crates/chd). +//! +//! For Rust consumers, consider using [chd-rs](https://crates.io/crates/chd) instead. +//! +//! The best way to integrate chd-rs in your C or C++ project is to instead vendor the [sources](https://github.com/SnowflakePowered/chd-rs) directly +//! into your project, with a compatible implementation of [libchdcorefile](https://github.com/SnowflakePowered/chd-rs/tree/master/chd-rs-capi/libchdcorefile) +//! for your platform as required. +//! +//! ## ABI compatibility with libchdr +//! +//! chd-rs-capi makes the following ABI-compatibility guarantees compared to libchdr when compiled statically. +//! * `chd_error` is ABI and API-compatible with [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L258) +//! * `chd_header` is ABI and API-compatible [chd.h](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/include/libchdr/chd.h#L302) +//! * `chd_file *` is an opaque pointer. It is **not layout compatible** with [chd.c](https://github.com/rtissera/libchdr/blob/cdcb714235b9ff7d207b703260706a364282b063/src/libchdr_chd.c#L265) +//! * The layout of `core_file *` is user-defined when the `chd_core_file` feature is enabled. +//! * Freeing any pointer returned by chd-rs with `free` is undefined behaviour. The exception are `chd_file *` pointers which can be safely freed with `chd_close`. + +extern crate core; + +mod header; + +#[cfg(feature = "chd_core_file")] +mod chdcorefile; + +#[cfg(feature = "chd_core_file")] +#[allow(non_camel_case_types)] +#[allow(unused)] +mod chdcorefile_sys; + +use crate::header::chd_header; +use chd::header::Header; +use chd::metadata::{KnownMetadata, Metadata, MetadataTag}; +pub use chd::Error as chd_error; +use chd::{Chd, Error}; +use std::any::Any; +use std::ffi::{CStr, CString}; +use std::fs::File; +use std::io::{BufReader, Cursor, Read, Seek}; +use std::mem::MaybeUninit; +use std::os::raw::{c_char, c_int, c_void}; +use std::path::Path; +use std::slice; + +/// Open a CHD for reading. +pub const CHD_OPEN_READ: i32 = 1; +/// Open a CHD for reading and writing. This mode is not supported and will always return an error +/// when passed into a constructor function such as [`chd_open`](crate::chd_open). +pub const CHD_OPEN_READWRITE: i32 = 2; + +/// Trait alias for `Read + Seek + Any`. +#[doc(hidden)] +pub trait SeekRead: Any + Read + Seek { + fn as_any(&self) -> &dyn Any; +} + +impl SeekRead for BufReader { + fn as_any(&self) -> &dyn Any { + self + } +} + +impl SeekRead for Cursor> { + fn as_any(&self) -> &dyn Any { + self + } +} + +#[allow(non_camel_case_types)] +/// An opaque type for an opened CHD file. +pub type chd_file = Chd>; + +fn ffi_takeown_chd(chd: *mut chd_file) -> Box>> { + unsafe { Box::from_raw(chd) } +} + +fn ffi_expose_chd(chd: Box>>) -> *mut chd_file { + Box::into_raw(chd) +} + +fn ffi_open_chd( + filename: *const c_char, + parent: Option>, +) -> Result { + let c_filename = unsafe { CStr::from_ptr(filename) }; + let filename = std::str::from_utf8(c_filename.to_bytes()) + .map(Path::new) + .map_err(|_| chd_error::InvalidParameter)?; + + let file = File::open(filename).map_err(|_| chd_error::FileNotFound)?; + + let bufread = Box::new(BufReader::new(file)) as Box; + Chd::open(bufread, parent) +} + +/// Opens a CHD file by file name, with a layout-undefined backing file pointer owned by +/// the library. +/// +/// The result of passing an object created by this function into [`chd_core_file`](crate::chd_core_file) +/// is strictly undefined. Instead, all `chd_file*` pointers with provenance from `chd_open` should be +/// closed with [`chd_close`](crate::chd_close). +/// +/// # Safety +/// * `filename` is a valid, null-terminated **UTF-8** string. +/// * `parent` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +/// * `out` is aligned and can store a pointer to a `chd_file*`. On success, `out` will point to a valid `chd_file*`. +/// * After this function returns, `parent` is invalid and must not be used, otherwise it will be undefined behaviour. There is no way to retake ownership of `parent`. +#[no_mangle] +pub unsafe extern "C" fn chd_open( + filename: *const c_char, + mode: c_int, + parent: *mut chd_file, + out: *mut *mut chd_file, +) -> chd_error { + // we don't support READWRITE mode + if mode == CHD_OPEN_READWRITE { + return chd_error::FileNotWriteable; + } + + let parent = if parent.is_null() { + None + } else { + Some(ffi_takeown_chd(parent)) + }; + + let chd = match ffi_open_chd(filename, parent) { + Ok(chd) => chd, + Err(e) => return e, + }; + + unsafe { *out = ffi_expose_chd(Box::new(chd)) } + chd_error::None +} + +#[no_mangle] +/// Close a CHD file. +/// +/// # Safety +/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +/// * If `chd` is `NULL`, this does nothing. +pub unsafe extern "C" fn chd_close(chd: *mut chd_file) { + if !chd.is_null() { + unsafe { drop(Box::from_raw(chd)) } + } +} + +#[no_mangle] +/// Returns an error string for the corresponding CHD error. +/// +/// # Safety +/// The returned string is leaked and the memory **should not and can not ever** be validly freed. +/// Attempting to free the returned pointer with `free` is **undefined behaviour**. +pub unsafe extern "C" fn chd_error_string(err: chd_error) -> *const c_char { + // SAFETY: This will leak, but this is much safer than + // potentially allowing the C caller to corrupt internal state + // by returning an internal pointer to an interned string. + let err_string = unsafe { CString::new(err.to_string()).unwrap_unchecked() }; + err_string.into_raw() +} + +fn ffi_chd_get_header(chd: &chd_file) -> chd_header { + match chd.header() { + Header::V5Header(_) => header::get_v5_header(chd), + Header::V1Header(h) | Header::V2Header(h) => h.into(), + Header::V3Header(h) => h.into(), + Header::V4Header(h) => h.into(), + } +} +#[no_mangle] +/// Returns a pointer to the extracted CHD header data. +/// # Safety +/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +/// * If `chd` is `NULL`, returns `NULL`. +/// * The returned pointer is leaked and the memory **should not and can not ever** be validly freed. Attempting to free the returned pointer with `free` is **undefined behaviour**. A non-leaking variant is provided in [`chd_read_header`](crate::chd_read_header). +pub unsafe extern "C" fn chd_get_header(chd: *const chd_file) -> *const chd_header { + match unsafe { chd.as_ref() } { + Some(chd) => { + let header = ffi_chd_get_header(chd); + Box::into_raw(Box::new(header)) + } + None => std::ptr::null(), + } +} + +#[no_mangle] +/// Read a single hunk from the CHD file. +/// +/// # Safety +/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +/// * `buffer` must an aligned pointer to a block of initialized memory of exactly the hunk size for the input `chd_file*` that is valid for both reads and writes. This size can be found with [`chd_get_header`](crate::chd_get_header). +/// * If `chd` is `NULL`, returns `CHDERR_INVALID_PARAMETER`. +pub unsafe extern "C" fn chd_read( + chd: *mut chd_file, + hunknum: u32, + buffer: *mut c_void, +) -> chd_error { + match unsafe { chd.as_mut() } { + None => chd_error::InvalidParameter, + Some(chd) => { + let hunk = chd.hunk(hunknum); + if let Ok(mut hunk) = hunk { + let size = hunk.len(); + let mut comp_buf = Vec::new(); + // SAFETY: The output buffer *must* be initialized and + // have a length of exactly the hunk size. + let output: &mut [u8] = + unsafe { slice::from_raw_parts_mut(buffer as *mut u8, size) }; + let result = hunk.read_hunk_in(&mut comp_buf, output); + match result { + Ok(_) => chd_error::None, + Err(e) => e, + } + } else { + chd_error::HunkOutOfRange + } + } + } +} + +fn find_metadata( + chd: &mut chd_file, + search_tag: u32, + mut search_index: u32, +) -> Result { + for entry in chd.metadata_refs() { + if entry.metatag() == search_tag || entry.metatag() == KnownMetadata::Wildcard.metatag() { + if search_index == 0 { + return entry.read(chd.inner()); + } + search_index -= 1; + } + } + Err(Error::MetadataNotFound) +} +#[no_mangle] +/// Get indexed metadata of the given search tag and index. +/// +/// # Safety +/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +/// * `output` must be an aligned pointer to a block of initialized memory of size exactly `output_len` that is valid for writes. +/// * `result_len` must be either NULL or an aligned pointer to a `uint32_t` that is valid for writes. +/// * `result_tag` must be either NULL or an aligned pointer to a `uint32_t` that is valid for writes. +/// * `result_flags` must be either NULL or an aligned pointer to a `uint8_t` that is valid for writes. +/// * If `chd` is `NULL`, returns `CHDERR_INVALID_PARAMETER`. +pub unsafe extern "C" fn chd_get_metadata( + chd: *mut chd_file, + searchtag: u32, + searchindex: u32, + output: *mut c_void, + output_len: u32, + result_len: *mut u32, + result_tag: *mut u32, + result_flags: *mut u8, +) -> chd_error { + match unsafe { chd.as_mut() } { + Some(chd) => { + let entry = find_metadata(chd, searchtag, searchindex); + match (entry, searchtag) { + (Ok(meta), _) => { + unsafe { + let output_len = std::cmp::min(output_len, meta.value.len() as u32); + std::ptr::copy_nonoverlapping( + meta.value.as_ptr() as *const c_void, + output, + output_len as usize, + ); + + if !result_tag.is_null() { + result_tag.write(meta.metatag) + } + if !result_len.is_null() { + result_len.write(meta.length) + } + if !result_flags.is_null() { + result_flags.write(meta.flags) + } + } + chd_error::None + } + (Err(_), tag) => unsafe { + if (tag == KnownMetadata::HardDisk.metatag() + || tag == KnownMetadata::Wildcard.metatag()) + && searchindex == 0 + { + let header = chd.header(); + if let Header::V1Header(header) = header { + let fake_meta = format!( + "CYLS:{},HEADS:{},SECS:{},BPS:{}", + header.cylinders, + header.heads, + header.sectors, + header.hunk_bytes / header.hunk_size + ); + let cstring = CString::from_vec_unchecked(fake_meta.into_bytes()); + let bytes = cstring.into_bytes_with_nul(); + let len = bytes.len(); + let output_len = std::cmp::min(output_len, len as u32); + + std::ptr::copy_nonoverlapping( + bytes.as_ptr() as *const c_void, + output, + output_len as usize, + ); + if !result_tag.is_null() { + result_tag.write(KnownMetadata::HardDisk.metatag()) + } + if !result_len.is_null() { + result_len.write(len as u32) + } + return chd_error::None; + } + } + chd_error::MetadataNotFound + }, + } + } + None => chd_error::InvalidParameter, + } +} + +#[no_mangle] +/// Set codec internal parameters. +/// +/// This function is not supported and always returns `CHDERR_INVALID_PARAMETER`. +pub extern "C" fn chd_codec_config( + _chd: *const chd_file, + _param: i32, + _config: *mut c_void, +) -> chd_error { + chd_error::InvalidParameter +} + +#[no_mangle] +/// Read CHD header data from the file into the pointed struct. +/// +/// # Safety +/// * `filename` is a valid, null-terminated **UTF-8** string. +/// * `header` is either `NULL`, or an aligned pointer to a possibly uninitialized `chd_header` struct. +/// * If `header` is `NULL`, returns `CHDERR_INVALID_PARAMETER` +pub unsafe extern "C" fn chd_read_header( + filename: *const c_char, + header: *mut MaybeUninit, +) -> chd_error { + let chd = ffi_open_chd(filename, None); + match chd { + Ok(chd) => { + let chd_header = ffi_chd_get_header(&chd); + match unsafe { header.as_mut() } { + None => Error::InvalidParameter, + Some(header) => { + header.write(chd_header); + Error::None + } + } + } + Err(e) => e, + } +} + +#[no_mangle] +#[cfg(feature = "chd_core_file")] +#[cfg_attr(docsrs, doc(cfg(chd_core_file)))] +/// Returns the associated `core_file*`. +/// +/// This method has different semantics than `chd_core_file` in libchdr. +/// +/// The input `chd_file*` will be dropped, and all prior references to +/// to the input `chd_file*` are rendered invalid, with the same semantics as `chd_close`. +/// +/// The provenance of the `chd_file*` is important to keep in mind. +/// +/// If the input `chd_file*` was opened with [`chd_open`](crate::chd_open), the input `chd_file*` will be closed, +/// and the return value should be considered undefined. For now it is `NULL`, but relying on this +/// behaviour is unstable and may change in the future. +/// +/// If the input `chd_file*` was opened with `chd_open_file` and the `chd_core_file` crate feature +/// is enabled, this method will return the same pointer as passed to `chd_input_file`, which may +/// be possible to cast to `FILE*` depending on the implementation of `libchdcorefile` that was +/// linked. +/// +/// # Safety +/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +/// * If `chd` is `NULL`, returns `NULL`. +/// * If `chd` has provenance from [`chd_open`](crate::chd_open), the returned pointer is undefined and must not be used. +/// * `chd` is **no longer valid** upon return of this function, and subsequent reuse of the `chd_file*` pointer is **undefined behaviour**. +pub unsafe extern "C" fn chd_core_file(chd: *mut chd_file) -> *mut chdcorefile_sys::core_file { + if chd.is_null() { + return std::ptr::null_mut(); + } + + let (file, _) = ffi_takeown_chd(chd).into_inner(); + let file_ref = file.as_any(); + + let pointer = match file_ref.downcast_ref::() { + None => std::ptr::null_mut(), + Some(file) => file.0, + }; + std::mem::forget(file); + pointer +} + +#[no_mangle] +#[cfg(feature = "chd_core_file")] +#[cfg_attr(docsrs, doc(cfg(chd_core_file)))] +/// Open an existing CHD file from an opened `core_file` object. +/// +/// Ownership is taken of the `core_file*` object and should not be modified until +/// `chd_core_file` is called to retake ownership of the `core_file*`. +/// +/// # Safety +/// * `file` is a valid pointer to a `core_file` with respect to the implementation of libchdcorefile that was linked. +/// * `parent` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +/// * `out` is aligned and can store a pointer to a `chd_file*`. On success, `out` will point to a valid `chd_file*`. +/// * Until the returned `chd_file*` in `out` is closed with [`chd_close`](crate::chd_close) or [`chd_core_file`](crate::chd_core_file), external mutation of `file` will result in undefined behaviour. +/// * After this function returns, `parent` is invalid and must not be used, otherwise it will be undefined behaviour. There is no way to retake ownership of `parent`. +pub unsafe extern "C" fn chd_open_file( + file: *mut chdcorefile_sys::core_file, + mode: c_int, + parent: *mut chd_file, + out: *mut *mut chd_file, +) -> chd_error { + // we don't support READWRITE mode + if mode == CHD_OPEN_READWRITE { + return chd_error::FileNotWriteable; + } + + let parent = if parent.is_null() { + None + } else { + Some(ffi_takeown_chd(parent)) + }; + + let core_file = Box::new(crate::chdcorefile::CoreFile(file)) as Box; + let chd = match Chd::open(core_file, parent) { + Ok(chd) => chd, + Err(e) => return e, + }; + + unsafe { *out = ffi_expose_chd(Box::new(chd)) } + chd_error::None +} + +#[no_mangle] +#[cfg(feature = "chd_virtio")] +#[cfg_attr(docsrs, doc(cfg(chd_virtio)))] +/// Open an existing CHD file from an opened `core_file` object. +/// +/// Ownership is taken of the `core_file*` object and should not be modified until +/// `chd_core_file` is called to retake ownership of the `core_file*`. +/// +/// # Safety +/// * `file` is a valid pointer to a `core_file` with respect to the implementation of libchdcorefile that was linked. +/// * `parent` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +/// * `out` is aligned and can store a pointer to a `chd_file*`. On success, `out` will point to a valid `chd_file*`. +/// * Until the returned `chd_file*` in `out` is closed with [`chd_close`](crate::chd_close) or [`chd_core_file`](crate::chd_core_file), external mutation of `file` will result in undefined behaviour. +/// * After this function returns, `parent` is invalid and must not be used, otherwise it will be undefined behaviour. There is no way to retake ownership of `parent`. +pub unsafe extern "C" fn chd_open_core_file( + file: *mut chdcorefile_sys::core_file, + mode: c_int, + parent: *mut chd_file, + out: *mut *mut chd_file, +) -> chd_error { + chd_open_file(file, mode, parent, out) +} + +#[no_mangle] +/// Get the name of a particular codec. +/// +/// This method always returns the string "Unknown" +pub extern "C" fn chd_get_codec_name(_codec: u32) -> *const c_char { + b"Unknown\0".as_ptr() as *const c_char +} + +#[cfg(feature = "chd_precache")] +use std::io::SeekFrom; + +#[cfg(feature = "chd_precache")] +#[cfg_attr(docsrs, doc(cfg(chd_precache)))] +/// The chunk size to read when pre-caching the underlying file stream into memory. +pub const PRECACHE_CHUNK_SIZE: usize = 16 * 1024 * 1024; + +#[no_mangle] +#[cfg(feature = "chd_precache")] +#[cfg_attr(docsrs, doc(cfg(chd_precache)))] +/// Precache the underlying file into memory with an optional callback to report progress. +/// +/// The underlying stream of the input `chd_file` is swapped with a layout-undefined in-memory stream. +/// +/// If the provenance of the original `chd_file` is from [`chd_open`](crate::chd_open), then the underlying +/// stream is safely dropped. +/// +/// If instead the underlying stream is a `core_file` opened from [`chd_open_file`](crate::chd_open_file), +/// or [`chd_open_core_file`](crate::chd_open_core_file), then the same semantics of calling [`chd_core_file`](crate::chd_core_file) +/// applies, and ownership of the underlying stream is released to the caller. +/// +/// After precaching, the input `chd_file` no longer returns a valid inner stream when passed to [`chd_core_file`](crate::chd_core_file), +/// and should be treated as having the same provenance as being from [`chd_open`](crate::chd_open). +/// +/// # Safety +/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +pub unsafe extern "C" fn chd_precache_progress( + chd: *mut chd_file, + progress: Option, + param: *mut c_void, +) -> chd_error { + let chd_file = if let Some(chd) = unsafe { chd.as_mut() } { + chd + } else { + return chd_error::InvalidParameter; + }; + + // if the inner is already a cursor over Vec, then it's already cached. + if chd_file.inner().as_any().is::>>() { + return chd_error::None; + } + + let file = chd_file.inner(); + let length = if let Ok(length) = file.seek(SeekFrom::End(0)) { + length as usize + } else { + return chd_error::ReadError; + }; + + let mut buffer = Vec::new(); + if let Err(_) = buffer.try_reserve_exact(length as usize) { + return chd_error::OutOfMemory; + } + let mut done: usize = 0; + let mut last_update_done: usize = 0; + let update_interval: usize = (length + 99) / 100; + + if let Err(_) = file.seek(SeekFrom::Start(0)) { + return chd_error::ReadError; + } + + while done < length { + let req_count = std::cmp::max(length - done, PRECACHE_CHUNK_SIZE); + + // todo: this is kind of sus... + if let Err(_) = file.read_exact(&mut buffer[done..req_count]) { + return chd_error::ReadError; + } + + done += req_count; + if let Some(progress) = progress { + if (done - last_update_done) >= update_interval && done != length { + last_update_done = done; + unsafe { + progress(done, length, param); + } + } + } + } + + // replace underlying stream of chd_file + let stream = Box::new(Cursor::new(buffer)) as Box; + + // take ownership of the existing chd file + let chd_file = ffi_takeown_chd(chd); + let (_file, parent) = chd_file.into_inner(); + + let buffered_chd = match Chd::open(stream, parent) { + Err(e) => return e, + Ok(chd) => Box::new(chd), + }; + + let buffered_chd = ffi_expose_chd(buffered_chd); + unsafe { chd.swap(buffered_chd) }; + + chd_error::None +} + +#[no_mangle] +#[cfg(feature = "chd_precache")] +#[cfg_attr(docsrs, doc(cfg(chd_precache)))] +/// Precache the underlying file into memory. +/// +/// The underlying stream of the input `chd_file` is swapped with a layout-undefined in-memory stream. +/// +/// If the provenance of the original `chd_file` is from [`chd_open`](crate::chd_open), then the underlying +/// stream is safely dropped. +/// +/// If instead the underlying stream is a `core_file` opened from [`chd_open_file`](crate::chd_open_file), +/// or [`chd_open_core_file`](crate::chd_open_core_file), then the same semantics of calling [`chd_core_file`](crate::chd_core_file) +/// applies, and ownership of the underlying stream is released to the caller. +/// +/// After precaching, the input `chd_file` no longer returns a valid inner stream when passed to [`chd_core_file`](crate::chd_core_file), +/// and should be treated as having the same provenance as being from [`chd_open`](crate::chd_open). +/// +/// # Safety +/// * `chd` is either `NULL` or a valid pointer to a `chd_file` obtained from [`chd_open`](crate::chd_open), [`chd_open_file`](crate::chd_open_file), or [`chd_open_core_file`](crate::chd_open_core_file). +pub unsafe extern "C" fn chd_precache(chd: *mut chd_file) -> chd_error { + chd_precache_progress(chd, None, std::ptr::null_mut()) +} diff --git a/src/BizHawk.Emulation.DiscSystem/DiscFormats/Blobs/Blob_CHD.cs b/src/BizHawk.Emulation.DiscSystem/DiscFormats/Blobs/Blob_CHD.cs index 70232deb68..15394dcf6a 100644 --- a/src/BizHawk.Emulation.DiscSystem/DiscFormats/Blobs/Blob_CHD.cs +++ b/src/BizHawk.Emulation.DiscSystem/DiscFormats/Blobs/Blob_CHD.cs @@ -5,16 +5,14 @@ namespace BizHawk.Emulation.DiscSystem { internal class Blob_CHD : IBlob { - private LibChdr.CoreFileStreamWrapper _coreFile; private IntPtr _chdFile; private readonly uint _hunkSize; private readonly byte[] _hunkCache; private int _currentHunk; - public Blob_CHD(LibChdr.CoreFileStreamWrapper coreFile, IntPtr chdFile, uint hunkSize) + public Blob_CHD(IntPtr chdFile, uint hunkSize) { - _coreFile = coreFile; _chdFile = chdFile; _hunkSize = hunkSize; _hunkCache = new byte[hunkSize]; @@ -25,12 +23,9 @@ namespace BizHawk.Emulation.DiscSystem { if (_chdFile != IntPtr.Zero) { - LibChdr.chd_close(_chdFile); + LibChd.chd_close(_chdFile); _chdFile = IntPtr.Zero; } - - _coreFile?.Dispose(); - _coreFile = null; } public int Read(long byte_pos, byte[] buffer, int offset, int count) @@ -41,8 +36,8 @@ namespace BizHawk.Emulation.DiscSystem var targetHunk = (uint)(byte_pos / _hunkSize); if (targetHunk != _currentHunk) { - var err = LibChdr.chd_read(_chdFile, targetHunk, _hunkCache); - if (err != LibChdr.chd_error.CHDERR_NONE) + var err = LibChd.chd_read(_chdFile, targetHunk, _hunkCache); + if (err != LibChd.chd_error.CHDERR_NONE) { // shouldn't ever happen in practice, unless something has gone terribly wrong throw new IOException($"CHD read failed with error {err}"); diff --git a/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs index 6b81a8dcef..43340ab6a8 100644 --- a/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs +++ b/src/BizHawk.Emulation.DiscSystem/DiscFormats/CHD_format.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; @@ -11,7 +12,7 @@ using BizHawk.Emulation.DiscSystem.CUE; #pragma warning disable BHI1005 -// MAME CHD images, using the standard libchdr for reading +// MAME CHD images, using chd-rs for reading // helpful reference: https://problemkaputt.de/psxspx-cdrom-disk-images-chd-mame.htm namespace BizHawk.Emulation.DiscSystem @@ -20,24 +21,19 @@ namespace BizHawk.Emulation.DiscSystem { /// /// Represents a CHD file. - /// This isn't particularly faithful to the format, but rather it just wraps libchdr's chd_file + /// This isn't particularly faithful to the format, but rather it just wraps a chd_file /// public class CHDFile { /// - /// Wrapper of a C# stream to a chd_core_file - /// - public LibChdr.CoreFileStreamWrapper CoreFile; - - /// - /// chd_file* to be used for libchdr functions + /// chd_file* to be used for chd_ functions /// public IntPtr ChdFile; /// - /// CHD header, interpreted by libchdr + /// CHD header, interpreted by chd-rs /// - public LibChdr.chd_header Header; + public LibChd.chd_header Header; /// /// CHD CD metadata for each track @@ -65,12 +61,12 @@ namespace BizHawk.Emulation.DiscSystem /// /// Track type /// - public LibChdr.chd_track_type TrackType; + public LibChd.chd_track_type TrackType; /// /// Subcode type /// - public LibChdr.chd_sub_type SubType; + public LibChd.chd_sub_type SubType; /// /// Size of each sector @@ -104,12 +100,12 @@ namespace BizHawk.Emulation.DiscSystem /// /// Pregap track type /// - public LibChdr.chd_track_type PregapTrackType; + public LibChd.chd_track_type PregapTrackType; /// /// Pregap subcode type /// - public LibChdr.chd_sub_type PregapSubType; + public LibChd.chd_sub_type PregapSubType; /// /// Indicates whether pregap is in the CHD @@ -129,30 +125,30 @@ namespace BizHawk.Emulation.DiscSystem public CHDParseException(string message, Exception ex) : base(message, ex) { } } - private static LibChdr.chd_track_type GetTrackType(string type) + private static LibChd.chd_track_type GetTrackType(string type) { return type switch { - "MODE1" => LibChdr.chd_track_type.CD_TRACK_MODE1, - "MODE1/2048" => LibChdr.chd_track_type.CD_TRACK_MODE1, - "MODE1_RAW" => LibChdr.chd_track_type.CD_TRACK_MODE1_RAW, - "MODE1/2352" => LibChdr.chd_track_type.CD_TRACK_MODE1_RAW, - "MODE2" => LibChdr.chd_track_type.CD_TRACK_MODE2, - "MODE2/2336" => LibChdr.chd_track_type.CD_TRACK_MODE2, - "MODE2_FORM1" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1, - "MODE2/2048" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1, - "MODE2_FORM2" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2, - "MODE2/2324" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2, - "MODE2_FORM_MIX" => LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX, - "MODE2_RAW" => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW, - "MODE2/2352" => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW, - "CDI/2352" => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW, - "AUDIO" => LibChdr.chd_track_type.CD_TRACK_AUDIO, + "MODE1" => LibChd.chd_track_type.CD_TRACK_MODE1, + "MODE1/2048" => LibChd.chd_track_type.CD_TRACK_MODE1, + "MODE1_RAW" => LibChd.chd_track_type.CD_TRACK_MODE1_RAW, + "MODE1/2352" => LibChd.chd_track_type.CD_TRACK_MODE1_RAW, + "MODE2" => LibChd.chd_track_type.CD_TRACK_MODE2, + "MODE2/2336" => LibChd.chd_track_type.CD_TRACK_MODE2, + "MODE2_FORM1" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM1, + "MODE2/2048" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM1, + "MODE2_FORM2" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM2, + "MODE2/2324" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM2, + "MODE2_FORM_MIX" => LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX, + "MODE2_RAW" => LibChd.chd_track_type.CD_TRACK_MODE2_RAW, + "MODE2/2352" => LibChd.chd_track_type.CD_TRACK_MODE2_RAW, + "CDI/2352" => LibChd.chd_track_type.CD_TRACK_MODE2_RAW, + "AUDIO" => LibChd.chd_track_type.CD_TRACK_AUDIO, _ => throw new CHDParseException("Malformed CHD format: Invalid track type!"), }; } - private static (LibChdr.chd_track_type TrackType, bool ChdContainsPregap) GetTrackTypeForPregap(string type) + private static (LibChd.chd_track_type TrackType, bool ChdContainsPregap) GetTrackTypeForPregap(string type) { if (type.Length > 0 && type[0] == 'V') { @@ -162,40 +158,40 @@ namespace BizHawk.Emulation.DiscSystem return (GetTrackType(type), false); } - private static uint GetSectorSize(LibChdr.chd_track_type type) + private static uint GetSectorSize(LibChd.chd_track_type type) { return type switch { - LibChdr.chd_track_type.CD_TRACK_MODE1 => 2048, - LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => 2352, - LibChdr.chd_track_type.CD_TRACK_MODE2 => 2336, - LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 => 2048, - LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 => 2324, - LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX => 2336, - LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => 2352, - LibChdr.chd_track_type.CD_TRACK_AUDIO => 2352, + LibChd.chd_track_type.CD_TRACK_MODE1 => 2048, + LibChd.chd_track_type.CD_TRACK_MODE1_RAW => 2352, + LibChd.chd_track_type.CD_TRACK_MODE2 => 2336, + LibChd.chd_track_type.CD_TRACK_MODE2_FORM1 => 2048, + LibChd.chd_track_type.CD_TRACK_MODE2_FORM2 => 2324, + LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX => 2336, + LibChd.chd_track_type.CD_TRACK_MODE2_RAW => 2352, + LibChd.chd_track_type.CD_TRACK_AUDIO => 2352, _ => throw new CHDParseException("Malformed CHD format: Invalid track type!"), }; } - private static LibChdr.chd_sub_type GetSubType(string type) + private static LibChd.chd_sub_type GetSubType(string type) { return type switch { - "RW" => LibChdr.chd_sub_type.CD_SUB_NORMAL, - "RW_RAW" => LibChdr.chd_sub_type.CD_SUB_RAW, - "NONE" => LibChdr.chd_sub_type.CD_SUB_NONE, + "RW" => LibChd.chd_sub_type.CD_SUB_NORMAL, + "RW_RAW" => LibChd.chd_sub_type.CD_SUB_RAW, + "NONE" => LibChd.chd_sub_type.CD_SUB_NONE, _ => throw new CHDParseException("Malformed CHD format: Invalid sub type!"), }; } - private static uint GetSubSize(LibChdr.chd_sub_type type) + private static uint GetSubSize(LibChd.chd_sub_type type) { return type switch { - LibChdr.chd_sub_type.CD_SUB_NORMAL => 96, - LibChdr.chd_sub_type.CD_SUB_RAW => 96, - LibChdr.chd_sub_type.CD_SUB_NONE => 0, + LibChd.chd_sub_type.CD_SUB_NORMAL => 96, + LibChd.chd_sub_type.CD_SUB_RAW => 96, + LibChd.chd_sub_type.CD_SUB_NONE => 0, _ => throw new CHDParseException("Malformed CHD format: Invalid sub type!"), }; } @@ -312,8 +308,8 @@ namespace BizHawk.Emulation.DiscSystem }; if (bigEndian) { - cdMetadata.TrackType = (LibChdr.chd_track_type)BinaryPrimitives.ReadUInt32BigEndian(track); - cdMetadata.SubType = (LibChdr.chd_sub_type)BinaryPrimitives.ReadUInt32BigEndian(track[..4]); + cdMetadata.TrackType = (LibChd.chd_track_type)BinaryPrimitives.ReadUInt32BigEndian(track); + cdMetadata.SubType = (LibChd.chd_sub_type)BinaryPrimitives.ReadUInt32BigEndian(track[..4]); cdMetadata.SectorSize = BinaryPrimitives.ReadUInt32BigEndian(track[..8]); cdMetadata.SubSize = BinaryPrimitives.ReadUInt32BigEndian(track[..12]); cdMetadata.Frames = BinaryPrimitives.ReadUInt32BigEndian(track[..16]); @@ -321,8 +317,8 @@ namespace BizHawk.Emulation.DiscSystem } else { - cdMetadata.TrackType = (LibChdr.chd_track_type)BinaryPrimitives.ReadUInt32LittleEndian(track); - cdMetadata.SubType = (LibChdr.chd_sub_type)BinaryPrimitives.ReadUInt32LittleEndian(track[..4]); + cdMetadata.TrackType = (LibChd.chd_track_type)BinaryPrimitives.ReadUInt32LittleEndian(track); + cdMetadata.SubType = (LibChd.chd_sub_type)BinaryPrimitives.ReadUInt32LittleEndian(track[..4]); cdMetadata.SectorSize = BinaryPrimitives.ReadUInt32LittleEndian(track[..8]); cdMetadata.SubSize = BinaryPrimitives.ReadUInt32LittleEndian(track[..12]); cdMetadata.Frames = BinaryPrimitives.ReadUInt32LittleEndian(track[..16]); @@ -350,32 +346,52 @@ namespace BizHawk.Emulation.DiscSystem } /// malformed chd format - public static CHDFile ParseFrom(Stream stream) + public static CHDFile ParseFrom(string path) { var chdf = new CHDFile(); try { - chdf.CoreFile = new(stream); - var err = LibChdr.chd_open_core_file(chdf.CoreFile.CoreFile, LibChdr.CHD_OPEN_READ, IntPtr.Zero, out chdf.ChdFile); - if (err != LibChdr.chd_error.CHDERR_NONE) + // .NET Standard 2.0 doesn't have UnmanagedType.LPUTF8Str :( + // (although .NET Framework has it just fine along with modern .NET) + var nb = Encoding.UTF8.GetMaxByteCount(path.Length); + var ptr = Marshal.AllocCoTaskMem(checked(nb + 1)); + try { - throw new CHDParseException($"Malformed CHD format: Failed to open chd, got error {err}"); + unsafe + { + fixed (char* c = path) + { + var pbMem = (byte*)ptr; + var nbWritten = Encoding.UTF8.GetBytes(c, path.Length, pbMem!, nb); + pbMem[nbWritten] = 0; + } + } + + var err = LibChd.chd_open(ptr, LibChd.CHD_OPEN_READ, IntPtr.Zero, out chdf.ChdFile); + if (err != LibChd.chd_error.CHDERR_NONE) + { + throw new CHDParseException($"Malformed CHD format: Failed to open chd, got error {err}"); + } + + err = LibChd.chd_read_header(ptr, ref chdf.Header); + if (err != LibChd.chd_error.CHDERR_NONE) + { + throw new CHDParseException($"Malformed CHD format: Failed to read chd header, got error {err}"); + } + } + finally + { + Marshal.FreeCoTaskMem(ptr); } - unsafe - { - var header = (LibChdr.chd_header*)LibChdr.chd_get_header(chdf.ChdFile); - chdf.Header = *header; - } - - if (chdf.Header.hunkbytes == 0 || chdf.Header.hunkbytes % LibChdr.CD_FRAME_SIZE != 0) + if (chdf.Header.hunkbytes == 0 || chdf.Header.hunkbytes % LibChd.CD_FRAME_SIZE != 0) { throw new CHDParseException("Malformed CHD format: Invalid hunk size"); } - // libchdr puts the correct value here for older versions of chds which don't have this + // chd-rs puts the correct value here for older versions of chds which don't have this // for newer chds, it is left as is, which might be invalid - if (chdf.Header.unitbytes != LibChdr.CD_FRAME_SIZE) + if (chdf.Header.unitbytes != LibChd.CD_FRAME_SIZE) { throw new CHDParseException("Malformed CHD format: Invalid unit size"); } @@ -383,18 +399,18 @@ namespace BizHawk.Emulation.DiscSystem var metadataOutput = new byte[256]; for (uint i = 0; i < 99; i++) { - err = LibChdr.chd_get_metadata(chdf.ChdFile, LibChdr.CDROM_TRACK_METADATA2_TAG, + var err = LibChd.chd_get_metadata(chdf.ChdFile, LibChd.CDROM_TRACK_METADATA2_TAG, i, metadataOutput, (uint)metadataOutput.Length, out var resultLen, out _, out _); - if (err == LibChdr.chd_error.CHDERR_NONE) + if (err == LibChd.chd_error.CHDERR_NONE) { var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen).TrimEnd('\0'); chdf.CdMetadatas.Add(ParseMetadata2(metadata)); continue; } - err = LibChdr.chd_get_metadata(chdf.ChdFile, LibChdr.CDROM_TRACK_METADATA_TAG, + err = LibChd.chd_get_metadata(chdf.ChdFile, LibChd.CDROM_TRACK_METADATA_TAG, i, metadataOutput, (uint)metadataOutput.Length, out resultLen, out _, out _); - if (err == LibChdr.chd_error.CHDERR_NONE) + if (err == LibChd.chd_error.CHDERR_NONE) { var metadata = Encoding.ASCII.GetString(metadataOutput, 0, (int)resultLen).TrimEnd('\0'); chdf.CdMetadatas.Add(ParseMetadata(metadata)); @@ -415,9 +431,9 @@ namespace BizHawk.Emulation.DiscSystem { // if no metadata was present, we might have "old" metadata instead (which has all track info stored in one entry) metadataOutput = new byte[4 + 24 * 99]; - err = LibChdr.chd_get_metadata(chdf.ChdFile, LibChdr.CDROM_OLD_METADATA_TAG, + var err = LibChd.chd_get_metadata(chdf.ChdFile, LibChd.CDROM_OLD_METADATA_TAG, 0, metadataOutput, (uint)metadataOutput.Length, out var resultLen, out _, out _); - if (err == LibChdr.chd_error.CHDERR_NONE) + if (err == LibChd.chd_error.CHDERR_NONE) { if (resultLen != metadataOutput.Length) { @@ -447,7 +463,7 @@ namespace BizHawk.Emulation.DiscSystem } // pad expected sectors up to the next hunk - var sectorsPerHunk = chdf.Header.hunkbytes / LibChdr.CD_FRAME_SIZE; + var sectorsPerHunk = chdf.Header.hunkbytes / LibChd.CD_FRAME_SIZE; chdExpectedNumSectors = (chdExpectedNumSectors + sectorsPerHunk - 1) / sectorsPerHunk * sectorsPerHunk; var chdActualNumSectors = chdf.Header.hunkcount * sectorsPerHunk; @@ -462,15 +478,9 @@ namespace BizHawk.Emulation.DiscSystem { if (chdf.ChdFile != IntPtr.Zero) { - LibChdr.chd_close(chdf.ChdFile); + LibChd.chd_close(chdf.ChdFile); } - if (chdf.CoreFile == null) - { - stream.Dispose(); - } - - chdf.CoreFile?.Dispose(); throw ex as CHDParseException ?? new("Malformed CHD format: An unknown exception was thrown while parsing", ex); } } @@ -493,8 +503,7 @@ namespace BizHawk.Emulation.DiscSystem { if (!File.Exists(path)) throw new CHDParseException("Malformed CHD format: Nonexistent CHD file!"); - var infCHD = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - ret.ParsedCHDFile = ParseFrom(infCHD); + ret.ParsedCHDFile = ParseFrom(path); ret.Valid = true; } catch (CHDParseException ex) @@ -571,7 +580,7 @@ namespace BizHawk.Emulation.DiscSystem try { var chdf = loadResults.ParsedCHDFile; - IBlob chdBlob = new Blob_CHD(chdf.CoreFile, chdf.ChdFile, chdf.Header.hunkbytes); + IBlob chdBlob = new Blob_CHD(chdf.ChdFile, chdf.Header.hunkbytes); disc.DisposableResources.Add(chdBlob); // chds only support 1 session @@ -584,7 +593,7 @@ namespace BizHawk.Emulation.DiscSystem var q = default(SubchannelQ); //absent some kind of policy for how to set it, this is a safe assumption const byte kADR = 1; - var control = cdMetadata.TrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO + var control = cdMetadata.TrackType != LibChd.chd_track_type.CD_TRACK_AUDIO ? EControlQ.DATA : EControlQ.None; q.SetStatus(kADR, control); @@ -597,36 +606,36 @@ namespace BizHawk.Emulation.DiscSystem return new() { QData = q }; } - static SS_Base CreateSynth(LibChdr.chd_track_type trackType) + static SS_Base CreateSynth(LibChd.chd_track_type trackType) { return trackType switch { - LibChdr.chd_track_type.CD_TRACK_MODE1 => new SS_Mode1_2048(), - LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => new SS_2352(), - LibChdr.chd_track_type.CD_TRACK_MODE2 => new SS_Mode2_2336(), - LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 => new SS_Mode2_Form1_2048(), - LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 => new SS_Mode2_Form2_2324(), - LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX => new SS_Mode2_2336(), - LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => new SS_2352(), - LibChdr.chd_track_type.CD_TRACK_AUDIO => new SS_CHD_Audio(), + LibChd.chd_track_type.CD_TRACK_MODE1 => new SS_Mode1_2048(), + LibChd.chd_track_type.CD_TRACK_MODE1_RAW => new SS_2352(), + LibChd.chd_track_type.CD_TRACK_MODE2 => new SS_Mode2_2336(), + LibChd.chd_track_type.CD_TRACK_MODE2_FORM1 => new SS_Mode2_Form1_2048(), + LibChd.chd_track_type.CD_TRACK_MODE2_FORM2 => new SS_Mode2_Form2_2324(), + LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX => new SS_Mode2_2336(), + LibChd.chd_track_type.CD_TRACK_MODE2_RAW => new SS_2352(), + LibChd.chd_track_type.CD_TRACK_AUDIO => new SS_CHD_Audio(), _ => throw new InvalidOperationException(), }; } - static CueTrackType ToCueTrackType(LibChdr.chd_track_type chdTrackType, bool isCdi) + static CueTrackType ToCueTrackType(LibChd.chd_track_type chdTrackType, bool isCdi) { // rough matches, not too important if these are somewhat wrong (they're just used for generated gaps) return chdTrackType switch { - LibChdr.chd_track_type.CD_TRACK_MODE1 => CueTrackType.Mode1_2048, - LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => CueTrackType.Mode1_2352, - LibChdr.chd_track_type.CD_TRACK_MODE2 => CueTrackType.Mode2_2336, - LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 => CueTrackType.Mode2_2336, - LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 => CueTrackType.Mode2_2336, - LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX => CueTrackType.Mode2_2336, - LibChdr.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => CueTrackType.CDI_2352, - LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => CueTrackType.Mode2_2352, - LibChdr.chd_track_type.CD_TRACK_AUDIO => CueTrackType.Audio, + LibChd.chd_track_type.CD_TRACK_MODE1 => CueTrackType.Mode1_2048, + LibChd.chd_track_type.CD_TRACK_MODE1_RAW => CueTrackType.Mode1_2352, + LibChd.chd_track_type.CD_TRACK_MODE2 => CueTrackType.Mode2_2336, + LibChd.chd_track_type.CD_TRACK_MODE2_FORM1 => CueTrackType.Mode2_2336, + LibChd.chd_track_type.CD_TRACK_MODE2_FORM2 => CueTrackType.Mode2_2336, + LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX => CueTrackType.Mode2_2336, + LibChd.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => CueTrackType.CDI_2352, + LibChd.chd_track_type.CD_TRACK_MODE2_RAW => CueTrackType.Mode2_2352, + LibChd.chd_track_type.CD_TRACK_AUDIO => CueTrackType.Audio, _ => throw new InvalidOperationException(), }; } @@ -657,7 +666,7 @@ namespace BizHawk.Emulation.DiscSystem synth.Policy = IN_DiscMountPolicy; const byte kADR = 1; - var control = cdMetadata.PregapTrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO + var control = cdMetadata.PregapTrackType != LibChd.chd_track_type.CD_TRACK_AUDIO ? EControlQ.DATA : EControlQ.None; synth.sq.SetStatus(kADR, control); @@ -676,14 +685,14 @@ namespace BizHawk.Emulation.DiscSystem // wrap the base synth with our special synth if we have subcode in the chd ISectorSynthJob2448 chdSynth = cdMetadata.PregapSubType switch { - LibChdr.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true), - LibChdr.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false), - LibChdr.chd_sub_type.CD_SUB_NONE => synth, + LibChd.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true), + LibChd.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false), + LibChd.chd_sub_type.CD_SUB_NONE => synth, _ => throw new InvalidOperationException(), }; disc._Sectors.Add(chdSynth); - chdOffset += LibChdr.CD_FRAME_SIZE; + chdOffset += LibChd.CD_FRAME_SIZE; } else { @@ -708,7 +717,7 @@ namespace BizHawk.Emulation.DiscSystem synth.BlobOffset = chdOffset; synth.Policy = IN_DiscMountPolicy; const byte kADR = 1; - var control = cdMetadata.TrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO + var control = cdMetadata.TrackType != LibChd.chd_track_type.CD_TRACK_AUDIO ? EControlQ.DATA : EControlQ.None; synth.sq.SetStatus(kADR, control); @@ -721,17 +730,17 @@ namespace BizHawk.Emulation.DiscSystem synth.Pause = false; ISectorSynthJob2448 chdSynth = cdMetadata.SubType switch { - LibChdr.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true), - LibChdr.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false), - LibChdr.chd_sub_type.CD_SUB_NONE => synth, + LibChd.chd_sub_type.CD_SUB_NORMAL => new SS_CHD_Sub(synth, isInterleaved: true), + LibChd.chd_sub_type.CD_SUB_RAW => new SS_CHD_Sub(synth, isInterleaved: false), + LibChd.chd_sub_type.CD_SUB_NONE => synth, _ => throw new InvalidOperationException(), }; disc._Sectors.Add(chdSynth); - chdOffset += LibChdr.CD_FRAME_SIZE; + chdOffset += LibChd.CD_FRAME_SIZE; relMSF++; } - chdOffset += cdMetadata.Padding * LibChdr.CD_FRAME_SIZE; + chdOffset += cdMetadata.Padding * LibChd.CD_FRAME_SIZE; for (var i = 0; i < cdMetadata.PostGap; i++) { @@ -741,7 +750,7 @@ namespace BizHawk.Emulation.DiscSystem Policy = IN_DiscMountPolicy }; const byte kADR = 1; - var control = cdMetadata.TrackType != LibChdr.chd_track_type.CD_TRACK_AUDIO + var control = cdMetadata.TrackType != LibChd.chd_track_type.CD_TRACK_AUDIO ? EControlQ.DATA : EControlQ.None; synth.sq.SetStatus(kADR, control); @@ -766,11 +775,11 @@ namespace BizHawk.Emulation.DiscSystem return SessionFormat.Type10_CDI; } - if (cdMetadata.TrackType is LibChdr.chd_track_type.CD_TRACK_MODE2 - or LibChdr.chd_track_type.CD_TRACK_MODE2_FORM1 - or LibChdr.chd_track_type.CD_TRACK_MODE2_FORM2 - or LibChdr.chd_track_type.CD_TRACK_MODE2_FORM_MIX - or LibChdr.chd_track_type.CD_TRACK_MODE2_RAW) + if (cdMetadata.TrackType is LibChd.chd_track_type.CD_TRACK_MODE2 + or LibChd.chd_track_type.CD_TRACK_MODE2_FORM1 + or LibChd.chd_track_type.CD_TRACK_MODE2_FORM2 + or LibChd.chd_track_type.CD_TRACK_MODE2_FORM_MIX + or LibChd.chd_track_type.CD_TRACK_MODE2_RAW) { return SessionFormat.Type20_CDXA; } @@ -874,20 +883,20 @@ namespace BizHawk.Emulation.DiscSystem // write header // note CHD header has values in big endian, while BinaryWriter will write in little endian bw.Write(_chdTag); - bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CHD_V5_HEADER_SIZE)); - bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CHD_HEADER_VERSION)); + bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CHD_V5_HEADER_SIZE)); + bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CHD_HEADER_VERSION)); // v5 chd allows for 4 different compression types // we only have 1 implemented here - bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CHD_CODEC_ZSTD)); + bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CHD_CODEC_ZSTD)); bw.Write(0); bw.Write(0); bw.Write(0); bw.Write(0L); // total size of all uncompressed data (written later) bw.Write(0L); // offset to hunk map (written later) bw.Write(0L); // offset to first metadata (written later) - bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK)); // bytes per hunk - bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CD_FRAME_SIZE)); // bytes per sector (always CD_FRAME_SIZE) - var blankSha1 = new byte[LibChdr.CHD_SHA1_BYTES]; + bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK)); // bytes per hunk + bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CD_FRAME_SIZE)); // bytes per sector (always CD_FRAME_SIZE) + var blankSha1 = new byte[LibChd.CHD_SHA1_BYTES]; bw.Write(blankSha1); // SHA1 of raw data (written later) bw.Write(blankSha1); // SHA1 of raw data + metadata (written later) bw.Write(blankSha1); // SHA1 of raw data + metadata for parent (N/A, always 0 for us) @@ -909,12 +918,12 @@ namespace BizHawk.Emulation.DiscSystem IsCDI = track.Mode == 2 && session.TOC.SessionFormat == SessionFormat.Type10_CDI, TrackType = track.Mode switch { - 0 => LibChdr.chd_track_type.CD_TRACK_AUDIO, - 1 => LibChdr.chd_track_type.CD_TRACK_MODE1_RAW, - 2 => LibChdr.chd_track_type.CD_TRACK_MODE2_RAW, + 0 => LibChd.chd_track_type.CD_TRACK_AUDIO, + 1 => LibChd.chd_track_type.CD_TRACK_MODE1_RAW, + 2 => LibChd.chd_track_type.CD_TRACK_MODE2_RAW, _ => throw new InvalidOperationException(), }, - SubType = LibChdr.chd_sub_type.CD_SUB_RAW, + SubType = LibChd.chd_sub_type.CD_SUB_RAW, SectorSize = 2352, SubSize = 96, Frames = (uint)(track.NextTrack.LBA - firstIndexLba), @@ -934,12 +943,12 @@ namespace BizHawk.Emulation.DiscSystem using var sha1Inc = IncrementalHash.CreateHash(HashAlgorithmName.SHA1); using var zstd = new Zstd(); var dsr = new DiscSectorReader(disc) { Policy = { DeinterleavedSubcode = true, DeterministicClearBuffer = true } }; - var sectorBuf = new byte[LibChdr.CD_FRAME_SIZE]; + var sectorBuf = new byte[LibChd.CD_FRAME_SIZE]; var cdLba = 0; uint chdLba = 0, chdPos; - var curHunk = new byte[LibChdr.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK]; + var curHunk = new byte[LibChd.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK]; #if false // TODO: cdzs - const uint COMPRESSION_LEN_BYTES = LibChdr.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK < 65536 ? 2 : 3; + const uint COMPRESSION_LEN_BYTES = LibChd.CD_FRAME_SIZE * CD_FRAMES_PER_HUNK < 65536 ? 2 : 3; const uint ECC_BYTES = (CD_FRAMES_PER_HUNK + 7) / 8; var hunkHeader = new byte[COMPRESSION_LEN_BYTES + ECC_BYTES]; #endif @@ -949,9 +958,6 @@ namespace BizHawk.Emulation.DiscSystem var hunkOffset = bw.BaseStream.Position; // TODO: adjust compression level? - // note: it's fairly important a high compression level is chosen - // libchdr will assume a compressed hunk with not be larger than an uncompressed hunk - // but with low compression levels, this is not necessarily true using (var cstream = zstd.CreateZstdCompressionStream(bw.BaseStream, Zstd.MaxCompressionLevel)) { cstream.Write(curHunk, 0, curHunk.Length); @@ -976,7 +982,7 @@ namespace BizHawk.Emulation.DiscSystem // audio samples are byteswapped, so make sure to account for that var trackType = i < cdMetadata.Pregap ? cdMetadata.PregapTrackType : cdMetadata.TrackType; - if (trackType == LibChdr.chd_track_type.CD_TRACK_AUDIO) + if (trackType == LibChd.chd_track_type.CD_TRACK_AUDIO) { EndiannessUtils.MutatingByteSwap16(sectorBuf.AsSpan()[..2352]); } @@ -986,11 +992,11 @@ namespace BizHawk.Emulation.DiscSystem Buffer.BlockCopy(sectorBuf, 0, curHunk, (int)(2352U * chdPos), 2352); Buffer.BlockCopy(sectorBuf, 2352, curHunk, (int)(2352U * CD_FRAMES_PER_HUNK + 96U * chdPos), 96); #else - Buffer.BlockCopy(sectorBuf, 0, curHunk, (int)(LibChdr.CD_FRAME_SIZE * chdPos), (int)LibChdr.CD_FRAME_SIZE); + Buffer.BlockCopy(sectorBuf, 0, curHunk, (int)(LibChd.CD_FRAME_SIZE * chdPos), (int)LibChd.CD_FRAME_SIZE); #endif if (chdPos == CD_FRAMES_PER_HUNK - 1) { - EndHunk(CD_FRAMES_PER_HUNK * LibChdr.CD_FRAME_SIZE); + EndHunk(CD_FRAMES_PER_HUNK * LibChd.CD_FRAME_SIZE); } cdLba++; @@ -1002,7 +1008,7 @@ namespace BizHawk.Emulation.DiscSystem chdPos = chdLba % CD_FRAMES_PER_HUNK; if (chdPos == CD_FRAMES_PER_HUNK - 1) { - EndHunk(CD_FRAMES_PER_HUNK * LibChdr.CD_FRAME_SIZE); + EndHunk(CD_FRAMES_PER_HUNK * LibChd.CD_FRAME_SIZE); } chdLba++; @@ -1013,18 +1019,18 @@ namespace BizHawk.Emulation.DiscSystem chdPos = chdLba % CD_FRAMES_PER_HUNK; if (chdPos != 0) { - EndHunk(chdPos * LibChdr.CD_FRAME_SIZE); + EndHunk(chdPos * LibChd.CD_FRAME_SIZE); } - static string TrackTypeStr(LibChdr.chd_track_type trackType, bool isCdi) + static string TrackTypeStr(LibChd.chd_track_type trackType, bool isCdi) { // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault return trackType switch { - LibChdr.chd_track_type.CD_TRACK_AUDIO => "AUDIO", - LibChdr.chd_track_type.CD_TRACK_MODE1_RAW => "MODE1_RAW", - LibChdr.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => "CDI/2352", - LibChdr.chd_track_type.CD_TRACK_MODE2_RAW => "MODE2_RAW", + LibChd.chd_track_type.CD_TRACK_AUDIO => "AUDIO", + LibChd.chd_track_type.CD_TRACK_MODE1_RAW => "MODE1_RAW", + LibChd.chd_track_type.CD_TRACK_MODE2_RAW when isCdi => "CDI/2352", + LibChd.chd_track_type.CD_TRACK_MODE2_RAW => "MODE2_RAW", _ => throw new InvalidOperationException(), }; } @@ -1045,8 +1051,8 @@ namespace BizHawk.Emulation.DiscSystem var metadataStr = $"TRACK:{cdMetadata.Track} TYPE:{trackType} SUBTYPE:RW_RAW FRAMES:{cdMetadata.Frames} PREGAP:{cdMetadata.Pregap} PGTYPE:{pgTrackType} PGSUB:RW_RAW POSTGAP:0\0"; var metadataBytes = Encoding.ASCII.GetBytes(metadataStr); - bw.Write(BinaryPrimitives.ReverseEndianness(LibChdr.CDROM_TRACK_METADATA2_TAG)); - bw.Write(LibChdr.CHD_MDFLAGS_CHECKSUM); + bw.Write(BinaryPrimitives.ReverseEndianness(LibChd.CDROM_TRACK_METADATA2_TAG)); + bw.Write(LibChd.CHD_MDFLAGS_CHECKSUM); var chunkDataSize = new byte[3]; // 24 bit integer chunkDataSize[0] = (byte)((metadataBytes.Length >> 16) & 0xFF); chunkDataSize[1] = (byte)((metadataBytes.Length >> 8) & 0xFF); @@ -1162,7 +1168,7 @@ namespace BizHawk.Emulation.DiscSystem bw.Write(BinaryPrimitives.ReverseEndianness((uint)(hunkMapEnd - hunkMapOffset - 16))); bw.BaseStream.Seek(0x20, SeekOrigin.Begin); - bw.Write(BinaryPrimitives.ReverseEndianness(chdLba * (long)LibChdr.CD_FRAME_SIZE)); + bw.Write(BinaryPrimitives.ReverseEndianness(chdLba * (long)LibChd.CD_FRAME_SIZE)); bw.Write(BinaryPrimitives.ReverseEndianness(hunkMapOffset)); bw.Write(BinaryPrimitives.ReverseEndianness(metadataOffset)); @@ -1191,10 +1197,10 @@ namespace BizHawk.Emulation.DiscSystem // tag is hashed alongside the hash // we use the same tag every time, so we can just reuse this array var metadataTag = new byte[4]; - metadataTag[0] = (byte)((LibChdr.CDROM_TRACK_METADATA2_TAG >> 24) & 0xFF); - metadataTag[1] = (byte)((LibChdr.CDROM_TRACK_METADATA2_TAG >> 16) & 0xFF); - metadataTag[2] = (byte)((LibChdr.CDROM_TRACK_METADATA2_TAG >> 8) & 0xFF); - metadataTag[3] = (byte)(LibChdr.CDROM_TRACK_METADATA2_TAG & 0xFF); + metadataTag[0] = (byte)((LibChd.CDROM_TRACK_METADATA2_TAG >> 24) & 0xFF); + metadataTag[1] = (byte)((LibChd.CDROM_TRACK_METADATA2_TAG >> 16) & 0xFF); + metadataTag[2] = (byte)((LibChd.CDROM_TRACK_METADATA2_TAG >> 8) & 0xFF); + metadataTag[3] = (byte)(LibChd.CDROM_TRACK_METADATA2_TAG & 0xFF); foreach (var metadataHash in metadataHashes) { sha1Inc.AppendData(metadataTag); diff --git a/src/BizHawk.Emulation.DiscSystem/LibChdr.cs b/src/BizHawk.Emulation.DiscSystem/LibChd.cs similarity index 56% rename from src/BizHawk.Emulation.DiscSystem/LibChdr.cs rename to src/BizHawk.Emulation.DiscSystem/LibChd.cs index 942d51689b..87ab4e2062 100644 --- a/src/BizHawk.Emulation.DiscSystem/LibChdr.cs +++ b/src/BizHawk.Emulation.DiscSystem/LibChd.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Runtime.InteropServices; #pragma warning disable IDE1006 @@ -10,10 +9,11 @@ using System.Runtime.InteropServices; namespace BizHawk.Emulation.DiscSystem { /// - /// libchdr bindings + /// Bindings matching libchdr's chd.h + /// In practice, we use chd-rs, whose c api matches libchdr's /// TODO: should this be common-ized? chd isn't limited to discs, it could be used for hard disk images (e.g. for MAME) /// - public static class LibChdr + public static class LibChd { public const uint CHD_HEADER_VERSION = 5; public const uint CHD_V5_HEADER_SIZE = 124; @@ -69,148 +69,7 @@ namespace BizHawk.Emulation.DiscSystem CHDERR_UNSUPPORTED_FORMAT } - [StructLayout(LayoutKind.Sequential)] - public struct chd_core_file - { - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate ulong FSizeDelegate(IntPtr file); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate nuint FReadDelegate(IntPtr buffer, nuint size, nuint count, IntPtr file); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate int FCloseDelegate(IntPtr file); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate int FSeekDelegate(IntPtr file, long offset, SeekOrigin origin); - - public IntPtr argp; - [MarshalAs(UnmanagedType.FunctionPtr)] - public FSizeDelegate fsize; - [MarshalAs(UnmanagedType.FunctionPtr)] - public FReadDelegate fread; - [MarshalAs(UnmanagedType.FunctionPtr)] - public FCloseDelegate fclose; - [MarshalAs(UnmanagedType.FunctionPtr)] - public FSeekDelegate fseek; - } - - /// - /// Convenience chd_core_file wrapper against a generic Stream - /// - public class CoreFileStreamWrapper : IDisposable - { - private const uint READ_BUFFER_LEN = 8 * CD_FRAME_SIZE; // 8 frames, usual uncompressed hunk size - private readonly byte[] _readBuffer = new byte[READ_BUFFER_LEN]; - - private Stream _s; - - // ReSharper disable once MemberCanBePrivate.Global - private readonly chd_core_file _coreFile; - public readonly IntPtr CoreFile; - - private ulong FSize(IntPtr file) - { - try - { - return (ulong)_s.Length; - } - catch (Exception e) - { - Console.Error.WriteLine(e); - return unchecked((ulong)-1); - } - } - - private nuint FRead(IntPtr buffer, nuint size, nuint count, IntPtr file) - { - nuint ret = 0; - try - { - // note: size will always be 1, so this should never overflow - var numBytesToRead = (uint)Math.Min(size * (ulong)count, uint.MaxValue); - while (numBytesToRead > 0) - { - var numRead = _s.Read(_readBuffer, 0, (int)Math.Min(READ_BUFFER_LEN, numBytesToRead)); - if (numRead == 0) - { - return ret; - } - - Marshal.Copy(_readBuffer, 0, buffer, numRead); - buffer += numRead; - ret += (uint)numRead; - numBytesToRead -= (uint)numRead; - } - - return ret; - } - catch (Exception e) - { - Console.Error.WriteLine(e); - return ret; - } - } - - private int FClose(IntPtr file) - { - if (_s == null) - { - return -1; - } - - _s.Dispose(); - _s = null; - return 0; - } - - private int FSeek(IntPtr file, long offset, SeekOrigin origin) - { - try - { - _s.Seek(offset, origin); - return 0; - } - catch (Exception ex) - { - Console.Error.WriteLine(ex); - return -1; - } - } - - public CoreFileStreamWrapper(Stream s) - { - if (!s.CanRead || !s.CanSeek) - { - throw new NotSupportedException("The underlying CHD stream must support reading and seeking!"); - } - - _s = s; - _coreFile.fsize = FSize; - _coreFile.fread = FRead; - _coreFile.fclose = FClose; - _coreFile.fseek = FSeek; - // the pointer here must stay alloc'd on the unmanaged size - // as libchdr expects the memory to not move around - CoreFile = Marshal.AllocCoTaskMem(Marshal.SizeOf()); - Marshal.StructureToPtr(_coreFile, CoreFile, fDeleteOld: false); - } - - public void Dispose() - { - Marshal.DestroyStructure(CoreFile); - Marshal.FreeCoTaskMem(CoreFile); - _s?.Dispose(); - } - } - - [DllImport("chdr")] - public static extern chd_error chd_open_core_file(IntPtr file, int mode, IntPtr parent, out IntPtr chd); - - [DllImport("chdr")] - public static extern void chd_close(IntPtr chd); - - // extracted chd header (not the same as the one on disk, but rather an interpreted one by libchdr) + // extracted chd header (not the same as the one on disk, but rather an interpreted one by chd-rs) [StructLayout(LayoutKind.Sequential)] public struct chd_header { @@ -239,12 +98,18 @@ namespace BizHawk.Emulation.DiscSystem public uint obsolete_hunksize; // obsolete field -- do not use! } - [DllImport("chdr")] - public static extern IntPtr chd_get_header(IntPtr chd); + [DllImport("chd_capi")] + public static extern chd_error chd_open(IntPtr filename, int mode, IntPtr parent, out IntPtr chd); - [DllImport("chdr")] + [DllImport("chd_capi")] + public static extern void chd_close(IntPtr chd); + + [DllImport("chd_capi")] public static extern chd_error chd_read(IntPtr chd, uint hunknum, byte[] buffer); + [DllImport("chd_capi")] + public static extern chd_error chd_read_header(IntPtr filename, ref chd_header header); + public enum chd_track_type : uint { CD_TRACK_MODE1 = 0, // mode 1 2048 bytes/sector @@ -267,7 +132,7 @@ namespace BizHawk.Emulation.DiscSystem // hunks should be a multiple of this for cd chds public const uint CD_FRAME_SIZE = 2352 + 96; - [DllImport("chdr")] + [DllImport("chd_capi")] public static extern chd_error chd_get_metadata( IntPtr chd, uint searchtag, uint searchindex, byte[] output, uint outputlen, out uint resultlen, out uint resulttag, out byte resultflags); }