Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ rex-jit = ["cranelift-codegen", "cranelift-frontend", "cranelift-jit", "cranelif
# Use WindowEvent::CursorMoved for mouse motion instead of DeviceEvent::MouseMotion.
# Useful for testing absolute cursor position tracking. May cause doubled moves on some platforms.
mouseabs = []
# CHD disk image support (SCSI HDDs and CD-ROMs) via libchdman-rs.
# Off by default — enable with `cargo build --features chd`.
chd = ["dep:libchdman-rs"]

[dependencies]
clap = { version = "4", features = ["derive"] }
Expand Down Expand Up @@ -58,6 +61,7 @@ cranelift-jit = { version = "0.116", optional = true }
cranelift-module = { version = "0.116", optional = true }
cranelift-native = { version = "0.116", optional = true }
target-lexicon = { version = "0.13", optional = true }
libchdman-rs = { version = "0.287.0-l7", features = ["prebuilt"], optional = true }

[target.'cfg(not(windows))'.dependencies]
libc = "0.2"
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,24 @@ cargo run --release --features jit # enable Cranelift MIPS JIT
cargo run --release --features rex-jit # enable REX3 graphics JIT compiler
cargo run --release --features tlbvmap # enable 8k slot to tlb entry map (increases cache use but may help depending on host cpu arch)
cargo run --release --features ci_clock # synthetic deterministic CP0 Compare clock (CI/snapshot validator only; loses realtime desktop timing)
cargo run --release --features chd # mount .chd disk/CD-ROM images directly (via libchdman-rs); off by default to keep builds light
cargo run --release --features lightning,rex-jit,tlbvmap # recommended for best speed right now
```

### CHD image support (`--features chd`)

Off by default. When enabled, IRIS can mount `.chd` hard-disk and CD-ROM
images directly without first extracting to raw. Compressed parent CHDs
stay untouched — writes go to a MAME-style `.diff.chd` sidecar.

```
cargo build --release --features chd
```

Without this feature, attempting to mount a `.chd` path returns an
`Unsupported` error; raw images and COW overlays continue to work as
before.

See [HELP.md](HELP.md) for the full rundown: serial ports, monitor console,
NVRAM/MAC address setup, disk image prep, and more.

Expand Down
139 changes: 139 additions & 0 deletions src/chd_disk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! CHD-backed disk implementations for the SCSI subsystem.
//!
//! Two flavors:
//! * [`ChdHd`] — hard-disk CHD as a writable block device. Uncompressed CHDs
//! are written in place; compressed CHDs get an uncompressed `.diff.chd`
//! sidecar so the parent stays untouched (MAME's strategy).
//! * [`ChdCd`] — single-track MODE1 CD CHD exposed as a 2048-byte/sector
//! read-only stream via libchdman-rs's `CdCookedReader`.

use std::io::{self, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};

use libchdman_rs::cd::CdCookedReader;
use libchdman_rs::hd::HdImage;
use libchdman_rs::Chd;

fn map_err<E: std::fmt::Debug>(e: E) -> io::Error {
io::Error::new(io::ErrorKind::Other, format!("{:?}", e))
}

/// Writable hard-disk CHD backend.
pub struct ChdHd {
img: HdImage,
sector_size: u32,
total_bytes: u64,
}

// The underlying MAME chd_file holds a raw pointer (`*mut ChdFile`), making it
// !Send by default. We only ever own these from the SCSI worker thread (the
// backend is moved in once and never shared), so transferring ownership across
// threads is safe — we just don't share refs (no Sync).
unsafe impl Send for ChdHd {}
unsafe impl Send for ChdCd {}

impl ChdHd {
pub fn open(path: &str) -> io::Result<Self> {
let p = Path::new(path);
let diff = diff_path_for(p);

// If a diff sidecar already exists, reattach to it (so previously-written
// sectors are preserved across runs, like a COW overlay).
let img = if diff.exists() {
HdImage::reopen_diff(p, &diff).map_err(map_err)?
} else {
// Try in-place first (works for uncompressed CHDs). On failure,
// fall back to creating an uncompressed diff alongside the parent.
match HdImage::open(p) {
Ok(img) => img,
Err(_) => HdImage::open_with_diff(p, &diff).map_err(map_err)?,
}
};

let sector_size = img.sector_size();
let total_bytes = img.sector_count() * u64::from(sector_size);
Ok(Self { img, sector_size, total_bytes })
}

pub fn size(&self) -> u64 {
self.total_bytes
}

pub fn read_blocks(&mut self, lba: u64, count: usize, block_size: u64) -> io::Result<Vec<u8>> {
let ss = u64::from(self.sector_size);
if block_size != ss {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("CHD HD sector size {} != requested block size {}", ss, block_size),
));
}
let mut buf = vec![0u8; count * ss as usize];
for i in 0..count {
let off = i * ss as usize;
self.img
.read_sector(lba + i as u64, &mut buf[off..off + ss as usize])
.map_err(map_err)?;
}
Ok(buf)
}

pub fn write_sectors(&mut self, lba: u64, data: &[u8]) -> io::Result<()> {
let ss = self.sector_size as usize;
if !data.len().is_multiple_of(ss) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("CHD HD write length {} not a multiple of sector {}", data.len(), ss),
));
}
let count = data.len() / ss;
for i in 0..count {
let off = i * ss;
self.img
.write_sector(lba + i as u64, &data[off..off + ss])
.map_err(map_err)?;
}
Ok(())
}
}

/// Read-only CD CHD backend.
pub struct ChdCd {
reader: CdCookedReader,
total_bytes: u64,
}

impl ChdCd {
pub fn open(path: &str) -> io::Result<Self> {
let chd = Chd::open(path, false, None).map_err(map_err)?;
let reader = CdCookedReader::open(chd).map_err(map_err)?;
let total_bytes = reader.len();
Ok(Self { reader, total_bytes })
}

pub fn size(&self) -> u64 {
self.total_bytes
}

pub fn read_blocks(&mut self, lba: u64, count: usize, block_size: u64) -> io::Result<Vec<u8>> {
let byte_offset = lba * block_size;
let byte_count = (count as u64) * block_size;
self.reader.seek(SeekFrom::Start(byte_offset))?;
let mut buf = vec![0u8; byte_count as usize];
self.reader.read_exact(&mut buf)?;
Ok(buf)
}
}

fn diff_path_for(parent: &Path) -> PathBuf {
let mut s = parent.as_os_str().to_owned();
s.push(".diff.chd");
PathBuf::from(s)
}

pub fn is_chd(path: &str) -> bool {
Path::new(path)
.extension()
.and_then(|e| e.to_str())
.map(|e| e.eq_ignore_ascii_case("chd"))
.unwrap_or(false)
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub mod pit8254;
pub mod net;
pub mod seeq8003;
pub mod cow_disk;
#[cfg(feature = "chd")]
pub mod chd_disk;
pub mod scsi;
pub mod wd33c93a;
pub mod hal2;
Expand Down
33 changes: 33 additions & 0 deletions src/scsi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ pub enum DiskBackend {
Direct(File),
/// Copy-on-write: base image is read-only, writes go to overlay file.
Cow(CowDisk),
/// Hard-disk CHD. Writable; compressed parents get an uncompressed
/// `.diff.chd` sidecar (MAME-style), so the parent stays untouched.
#[cfg(feature = "chd")]
ChdHd(crate::chd_disk::ChdHd),
/// CD CHD (single-track MODE1) exposed as a 2048-byte/sector read-only
/// stream. Writes return an error.
#[cfg(feature = "chd")]
ChdCd(crate::chd_disk::ChdCd),
}

impl DiskBackend {
Expand All @@ -86,6 +94,10 @@ impl DiskBackend {
DiskBackend::Cow(cow) => {
cow.read_sectors(lba, count)
}
#[cfg(feature = "chd")]
DiskBackend::ChdHd(hd) => hd.read_blocks(lba, count, block_size),
#[cfg(feature = "chd")]
DiskBackend::ChdCd(cd) => cd.read_blocks(lba, count, block_size),
}
}

Expand All @@ -98,13 +110,24 @@ impl DiskBackend {
Ok(())
}
DiskBackend::Cow(cow) => cow.write_sectors(lba, data),
#[cfg(feature = "chd")]
DiskBackend::ChdHd(hd) => hd.write_sectors(lba, data),
#[cfg(feature = "chd")]
DiskBackend::ChdCd(_) => Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"CD CHD is read-only",
)),
}
}

fn size(&self) -> u64 {
match self {
DiskBackend::Direct(file) => file.metadata().map(|m| m.len()).unwrap_or(0),
DiskBackend::Cow(cow) => cow.size(),
#[cfg(feature = "chd")]
DiskBackend::ChdHd(hd) => hd.size(),
#[cfg(feature = "chd")]
DiskBackend::ChdCd(cd) => cd.size(),
}
}
}
Expand Down Expand Up @@ -156,6 +179,8 @@ impl ScsiDevice {
match &mut self.backend {
DiskBackend::Cow(cow) => cow.commit(),
DiskBackend::Direct(_) => Ok(0),
#[cfg(feature = "chd")]
DiskBackend::ChdHd(_) | DiskBackend::ChdCd(_) => Ok(0),
}
}

Expand All @@ -164,6 +189,8 @@ impl ScsiDevice {
match &mut self.backend {
DiskBackend::Cow(cow) => cow.reset_overlay(),
DiskBackend::Direct(_) => Ok(()),
#[cfg(feature = "chd")]
DiskBackend::ChdHd(_) | DiskBackend::ChdCd(_) => Ok(()),
}
}

Expand All @@ -173,6 +200,8 @@ impl ScsiDevice {
match &mut self.backend {
DiskBackend::Cow(cow) => cow.export_overlay(dest),
DiskBackend::Direct(_) => Ok(Vec::new()),
#[cfg(feature = "chd")]
DiskBackend::ChdHd(_) | DiskBackend::ChdCd(_) => Ok(Vec::new()),
}
}

Expand All @@ -182,6 +211,8 @@ impl ScsiDevice {
match &mut self.backend {
DiskBackend::Cow(cow) => cow.import_overlay(source, dirty),
DiskBackend::Direct(_) => Ok(()),
#[cfg(feature = "chd")]
DiskBackend::ChdHd(_) | DiskBackend::ChdCd(_) => Ok(()),
}
}

Expand All @@ -190,6 +221,8 @@ impl ScsiDevice {
match &self.backend {
DiskBackend::Cow(cow) => cow.dirty_count(),
DiskBackend::Direct(_) => 0,
#[cfg(feature = "chd")]
DiskBackend::ChdHd(_) | DiskBackend::ChdCd(_) => 0,
}
}

Expand Down
34 changes: 33 additions & 1 deletion src/wd33c93a.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,39 @@ impl Wd33c93a {
use crate::cow_disk::CowDisk;
use crate::scsi::DiskBackend;

let (backend, size) = if overlay && !is_cdrom {
#[cfg(feature = "chd")]
let is_chd_path = crate::chd_disk::is_chd(path);
#[cfg(not(feature = "chd"))]
let is_chd_path = {
let p = path.to_ascii_lowercase();
p.ends_with(".chd")
};

let (backend, size) = if is_chd_path {
#[cfg(feature = "chd")]
{
use crate::chd_disk::{ChdCd, ChdHd};
if is_cdrom {
let cd = ChdCd::open(path)?;
let sz = cd.size();
(DiskBackend::ChdCd(cd), sz)
} else {
// HD CHDs are inherently writable via the libchdman-rs HdImage
// surface (in-place for uncompressed, diff sidecar for
// compressed), so the `overlay` flag is not applicable here.
let hd = ChdHd::open(path)?;
let sz = hd.size();
(DiskBackend::ChdHd(hd), sz)
}
}
#[cfg(not(feature = "chd"))]
{
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"CHD image support not compiled in (rebuild with --features chd)",
));
}
} else if overlay && !is_cdrom {
let overlay_path = overlay_path_override
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{}.overlay", path));
Expand Down