use super::{call_guest, HicEntry, HostState, Registry, Win32Error};
use crate::emulator::{Cpu, Mmu};
pub const DRV_LOAD: u32 = 0x0001;
pub const DRV_ENABLE: u32 = 0x0002;
pub const DRV_OPEN: u32 = 0x0003;
pub const DRV_CLOSE: u32 = 0x0004;
pub const DRV_DISABLE: u32 = 0x0005;
pub const DRV_FREE: u32 = 0x0006;
pub const ICM_USER: u32 = 0x4000;
pub const ICM_RESERVED: u32 = 0x5000;
pub const ICM_GETINFO: u32 = ICM_RESERVED + 2;
pub const ICM_GETSTATE: u32 = ICM_RESERVED + 9;
pub const ICM_SETSTATE: u32 = ICM_RESERVED + 10;
pub const ICM_DECOMPRESS_GET_FORMAT: u32 = ICM_USER + 10;
pub const ICM_DECOMPRESS_QUERY: u32 = ICM_USER + 11;
pub const ICM_DECOMPRESS_BEGIN: u32 = ICM_USER + 12;
pub const ICM_DECOMPRESS: u32 = ICM_USER + 13;
pub const ICM_DECOMPRESS_END: u32 = ICM_USER + 14;
pub const ICM_COMPRESS_GET_FORMAT: u32 = ICM_USER + 4;
pub const ICM_COMPRESS_GET_SIZE: u32 = ICM_USER + 5;
pub const ICM_COMPRESS_QUERY: u32 = ICM_USER + 6;
pub const ICM_COMPRESS_BEGIN: u32 = ICM_USER + 7;
pub const ICM_COMPRESS: u32 = ICM_USER + 8;
pub const ICM_COMPRESS_END: u32 = ICM_USER + 9;
pub const ICCOMPRESS_KEYFRAME: u32 = 0x0000_0001;
pub const ICDECOMPRESS_HURRYUP: u32 = 0x80000000;
pub const ICDECOMPRESS_UPDATE: u32 = 0x40000000;
pub const ICDECOMPRESS_PREROL: u32 = 0x20000000;
pub const ICDECOMPRESS_NULLFRAME: u32 = 0x10000000;
pub const ICDECOMPRESS_NOTKEYFRAME: u32 = 0x08000000;
pub const ICERR_OK: i32 = 0;
pub const ICERR_UNSUPPORTED: i32 = -1;
pub const ICERR_BADFORMAT: i32 = -2;
pub const ICINFO_SIZE: u32 = 568;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bih {
pub bi_size: u32,
pub width: i32,
pub height: i32,
pub planes: u16,
pub bit_count: u16,
pub compression: [u8; 4],
pub size_image: u32,
pub x_pels_per_meter: i32,
pub y_pels_per_meter: i32,
pub clr_used: u32,
pub clr_important: u32,
pub tail: Vec<u8>,
}
impl Default for Bih {
fn default() -> Self {
Bih {
bi_size: 40,
width: 0,
height: 0,
planes: 1,
bit_count: 24,
compression: [0; 4],
size_image: 0,
x_pels_per_meter: 0,
y_pels_per_meter: 0,
clr_used: 0,
clr_important: 0,
tail: Vec::new(),
}
}
}
pub const BIH_SIZE: u32 = 40;
pub fn host_bih_to_guest(mmu: &mut Mmu, bih: &Bih, addr: u32) -> Result<(), Win32Error> {
let trap = |t: crate::emulator::Trap| Win32Error::InvalidArgument {
stub: "host_bih_to_guest",
reason: format!("{t}"),
};
mmu.store32(addr, bih.bi_size).map_err(trap)?;
mmu.store32(addr + 4, bih.width as u32).map_err(trap)?;
mmu.store32(addr + 8, bih.height as u32).map_err(trap)?;
mmu.store16(addr + 12, bih.planes).map_err(trap)?;
mmu.store16(addr + 14, bih.bit_count).map_err(trap)?;
mmu.write(addr + 16, &bih.compression).map_err(trap)?;
mmu.store32(addr + 20, bih.size_image).map_err(trap)?;
mmu.store32(addr + 24, bih.x_pels_per_meter as u32)
.map_err(trap)?;
mmu.store32(addr + 28, bih.y_pels_per_meter as u32)
.map_err(trap)?;
mmu.store32(addr + 32, bih.clr_used).map_err(trap)?;
mmu.store32(addr + 36, bih.clr_important).map_err(trap)?;
if !bih.tail.is_empty() {
mmu.write(addr + BIH_SIZE, &bih.tail).map_err(trap)?;
}
Ok(())
}
pub const BIH_TAIL_CAP: u32 = 1024;
pub fn guest_bih_to_host(mmu: &Mmu, addr: u32) -> Result<Bih, Win32Error> {
let trap = |t: crate::emulator::Trap| Win32Error::InvalidArgument {
stub: "guest_bih_to_host",
reason: format!("{t}"),
};
let bi_size = mmu.load32(addr).map_err(trap)?;
let width = mmu.load32(addr + 4).map_err(trap)? as i32;
let height = mmu.load32(addr + 8).map_err(trap)? as i32;
let planes = mmu.load16(addr + 12).map_err(trap)?;
let bit_count = mmu.load16(addr + 14).map_err(trap)?;
let mut compression = [0u8; 4];
for (i, c) in compression.iter_mut().enumerate() {
*c = mmu.load8(addr + 16 + i as u32).map_err(trap)?;
}
let size_image = mmu.load32(addr + 20).map_err(trap)?;
let x_pels_per_meter = mmu.load32(addr + 24).map_err(trap)? as i32;
let y_pels_per_meter = mmu.load32(addr + 28).map_err(trap)? as i32;
let clr_used = mmu.load32(addr + 32).map_err(trap)?;
let clr_important = mmu.load32(addr + 36).map_err(trap)?;
let tail_len = bi_size.saturating_sub(BIH_SIZE).min(BIH_TAIL_CAP);
let mut tail = Vec::with_capacity(tail_len as usize);
for i in 0..tail_len {
tail.push(mmu.load8(addr + BIH_SIZE + i).map_err(trap)?);
}
Ok(Bih {
bi_size,
width,
height,
planes,
bit_count,
compression,
size_image,
x_pels_per_meter,
y_pels_per_meter,
clr_used,
clr_important,
tail,
})
}
pub const ICDECOMPRESS_SIZE: u32 = 24;
pub const ICDECOMPRESSEX_SIZE: u32 = 88;
pub const ICCOMPRESS_SIZE: u32 = 48;
pub fn ic_open(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
fcc_type: u32,
fcc_handler: u32,
mode: u32,
) -> Result<u32, crate::Error> {
let driver_proc = state.default_driver_proc;
if driver_proc == 0 {
return Err(Win32Error::InvalidArgument {
stub: "ICOpen",
reason: "no codec image staged (host-side)".into(),
}
.into());
}
if !state.loaded_drivers.contains(&driver_proc) {
let _ = call_guest(
cpu,
mmu,
registry,
state,
driver_proc,
&[0, 0, DRV_LOAD, 0, 0],
)?;
let _ = call_guest(
cpu,
mmu,
registry,
state,
driver_proc,
&[0, 0, DRV_ENABLE, 0, 0],
)?;
state.loaded_drivers.insert(driver_proc);
}
const ICOPEN_SIZE: u32 = 36;
let icopen = state.arena_alloc(ICOPEN_SIZE)?;
let fcc_type_canon = fourcc_to_lower(fcc_type);
let fcc_handler_canon = fourcc_to_lower(fcc_handler);
let bytes: [u8; 36] = {
let mut b = [0u8; 36];
b[0..4].copy_from_slice(&ICOPEN_SIZE.to_le_bytes());
b[4..8].copy_from_slice(&fcc_type_canon.to_le_bytes());
b[8..12].copy_from_slice(&fcc_handler_canon.to_le_bytes());
b[12..16].copy_from_slice(&0x0001_0000u32.to_le_bytes());
b[16..20].copy_from_slice(&mode.to_le_bytes());
b
};
mmu.write_initializer(icopen, &bytes)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICOpen",
reason: format!("{t}"),
})?;
let driver_id = call_guest(
cpu,
mmu,
registry,
state,
driver_proc,
&[0, 0, DRV_OPEN, 0, icopen],
)?;
if driver_id == 0 {
return Ok(0);
}
let hic = state.next_hic;
state.next_hic = state.next_hic.wrapping_add(1);
state.hics.insert(
hic,
HicEntry {
fcc_type,
fcc_handler,
mode,
driver_proc_va: driver_proc,
driver_id,
},
);
Ok(hic)
}
pub fn ic_close(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
) -> Result<u32, crate::Error> {
let entry = match state.hics.remove(&hic) {
Some(e) => e,
None => {
return Err(Win32Error::InvalidArgument {
stub: "ICClose",
reason: format!("unknown HIC {hic}"),
}
.into());
}
};
call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, DRV_CLOSE, 0, 0],
)
}
fn fourcc_to_lower(fcc: u32) -> u32 {
let bytes = fcc.to_le_bytes();
let lowered = [
bytes[0].to_ascii_lowercase(),
bytes[1].to_ascii_lowercase(),
bytes[2].to_ascii_lowercase(),
bytes[3].to_ascii_lowercase(),
];
u32::from_le_bytes(lowered)
}
fn is_known_short_return_fcc(fcc: u32) -> bool {
matches!(
&fcc.to_le_bytes(),
b"IV31" | b"iv31" | b"IV32" | b"iv32" | b"IV41" | b"iv41" | b"IV50" | b"iv50"
)
}
pub fn ic_get_info(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
cb: u32,
) -> Result<Vec<u8>, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICGetInfo",
reason: format!("unknown HIC {hic}"),
})?;
let scratch = state.arena_alloc(cb)?;
let zeros = vec![0u8; cb as usize];
mmu.write_initializer(scratch, &zeros)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICGetInfo",
reason: format!("{t}"),
})?;
let written = call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, ICM_GETINFO, scratch, cb],
)?;
let n = written.min(cb) as usize;
if n == 0 && is_known_short_return_fcc(entry.fcc_handler) {
let synth_len = (cb as usize).min(568);
let mut out = vec![0u8; synth_len];
if synth_len >= 4 {
out[0..4].copy_from_slice(&cb.to_le_bytes());
}
if synth_len >= 8 {
out[4..8].copy_from_slice(&entry.fcc_type.to_le_bytes());
}
if synth_len >= 12 {
out[8..12].copy_from_slice(&entry.fcc_handler.to_le_bytes());
}
let fcc = entry.fcc_handler.to_le_bytes();
for (i, &c) in fcc.iter().enumerate() {
if 24 + i * 2 + 1 < synth_len {
out[24 + i * 2] = c;
}
}
return Ok(out);
}
let mut out = vec![0u8; n];
for (i, b) in out.iter_mut().enumerate() {
*b = mmu
.load8(scratch + i as u32)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICGetInfo",
reason: format!("{t}"),
})?;
}
if out.len() >= 24 + 32 && out[24..24 + 32].iter().all(|b| *b == 0) {
let fcc = entry.fcc_handler.to_le_bytes();
for (i, &c) in fcc.iter().enumerate() {
if 24 + i * 2 + 1 < out.len() {
out[24 + i * 2] = c;
}
}
}
Ok(out)
}
pub fn ic_decompress_query(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
input: &Bih,
output: Option<&Bih>,
) -> Result<u32, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICDecompressQuery",
reason: format!("unknown HIC {hic}"),
})?;
let in_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, input, in_addr)?;
let out_addr = if let Some(out_bih) = output {
let a = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, out_bih, a)?;
a
} else {
0
};
call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[
entry.driver_id,
hic,
ICM_DECOMPRESS_QUERY,
in_addr,
out_addr,
],
)
}
pub fn ic_decompress_get_format(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
input: &Bih,
) -> Result<(u32, Bih), crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICDecompressGetFormat",
reason: format!("unknown HIC {hic}"),
})?;
let in_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, input, in_addr)?;
let out_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
for i in 0..(BIH_SIZE + BIH_TAIL_CAP) {
mmu.store8(out_addr + i, 0)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICDecompressGetFormat",
reason: format!("{t}"),
})?;
}
let lr = call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[
entry.driver_id,
hic,
ICM_DECOMPRESS_GET_FORMAT,
in_addr,
out_addr,
],
)?;
let out = guest_bih_to_host(mmu, out_addr)?;
Ok((lr, out))
}
pub fn ic_decompress_begin(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
input: &Bih,
output: &Bih,
) -> Result<u32, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICDecompressBegin",
reason: format!("unknown HIC {hic}"),
})?;
msmpeg4_v3_preinit(mmu, state, &entry)?;
let in_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, input, in_addr)?;
let out_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, output, out_addr)?;
call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[
entry.driver_id,
hic,
ICM_DECOMPRESS_BEGIN,
in_addr,
out_addr,
],
)
}
const MSMPEG4_V3_PRIVATE_GUID: [u8; 16] = [
0x30, 0x6e, 0xc6, 0xb4, 0x80, 0x01, 0xd3, 0x11, 0xbb, 0xc6, 0x00, 0x60, 0x08, 0x32, 0x00, 0x64,
];
fn msmpeg4_v3_preinit(
mmu: &mut Mmu,
_state: &mut HostState,
entry: &HicEntry,
) -> Result<(), Win32Error> {
let h = entry.fcc_handler.to_le_bytes();
let is_msmpeg4 = matches!(
&h,
b"MP43" | b"mp43" | b"MP42" | b"mp42" | b"MPG4" | b"mpg4"
);
if !is_msmpeg4 {
return Ok(());
}
let trap = |t: crate::emulator::Trap| Win32Error::InvalidArgument {
stub: "ICDecompressBegin/msmpeg4_v3_preinit",
reason: format!("{t}"),
};
let v3_tag = mmu.load32(entry.driver_id + 0x18).map_err(trap)?;
if v3_tag != 3 {
return Ok(());
}
mmu.store32(entry.driver_id + 0xb4, 1).map_err(trap)?;
mmu.write(entry.driver_id + 0xb8, &MSMPEG4_V3_PRIVATE_GUID)
.map_err(trap)?;
Ok(())
}
pub fn ic_decompress_end(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
) -> Result<u32, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICDecompressEnd",
reason: format!("unknown HIC {hic}"),
})?;
call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, ICM_DECOMPRESS_END, 0, 0],
)
}
#[allow(clippy::too_many_arguments)]
pub fn ic_decompress(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
flags: u32,
input_bih: &Bih,
input_bytes: &[u8],
output_bih: &Bih,
output_capacity: u32,
) -> Result<(u32, Vec<u8>), crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICDecompress",
reason: format!("unknown HIC {hic}"),
})?;
let bi_in = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, input_bih, bi_in)?;
let bi_out = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, output_bih, bi_out)?;
let in_buf = state.arena_alloc(input_bytes.len() as u32)?;
mmu.write_initializer(in_buf, input_bytes)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICDecompress",
reason: format!("{t}"),
})?;
let out_buf = state.arena_alloc(output_capacity)?;
let zeros = vec![0u8; output_capacity as usize];
mmu.write_initializer(out_buf, &zeros)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICDecompress",
reason: format!("{t}"),
})?;
let icd = state.arena_alloc(ICDECOMPRESS_SIZE)?;
let trap = |t: crate::emulator::Trap| Win32Error::InvalidArgument {
stub: "ICDecompress",
reason: format!("{t}"),
};
mmu.store32(icd, flags).map_err(trap)?;
mmu.store32(icd + 4, bi_in).map_err(trap)?;
mmu.store32(icd + 8, in_buf).map_err(trap)?;
mmu.store32(icd + 12, bi_out).map_err(trap)?;
mmu.store32(icd + 16, out_buf).map_err(trap)?;
mmu.store32(icd + 20, 0).map_err(trap)?;
let lresult = call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, ICM_DECOMPRESS, icd, ICDECOMPRESS_SIZE],
)?;
let mut out = vec![0u8; output_capacity as usize];
for (i, b) in out.iter_mut().enumerate() {
*b = mmu
.load8(out_buf + i as u32)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICDecompress",
reason: format!("{t}"),
})?;
}
Ok((lresult, out))
}
pub fn ic_compress_query(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
input: &Bih,
output: Option<&Bih>,
) -> Result<u32, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICCompressQuery",
reason: format!("unknown HIC {hic}"),
})?;
let in_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, input, in_addr)?;
let out_addr = if let Some(out_bih) = output {
let a = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, out_bih, a)?;
a
} else {
0
};
call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, ICM_COMPRESS_QUERY, in_addr, out_addr],
)
}
pub fn ic_compress_get_format(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
input: &Bih,
) -> Result<(u32, Bih), crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICCompressGetFormat",
reason: format!("unknown HIC {hic}"),
})?;
let in_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, input, in_addr)?;
let out_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
for i in 0..(BIH_SIZE + BIH_TAIL_CAP) {
mmu.store8(out_addr + i, 0)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICCompressGetFormat",
reason: format!("{t}"),
})?;
}
let lr = call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[
entry.driver_id,
hic,
ICM_COMPRESS_GET_FORMAT,
in_addr,
out_addr,
],
)?;
let out = guest_bih_to_host(mmu, out_addr)?;
Ok((lr, out))
}
pub fn ic_compress_get_size(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
input: &Bih,
output: &Bih,
) -> Result<u32, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICCompressGetSize",
reason: format!("unknown HIC {hic}"),
})?;
let in_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, input, in_addr)?;
let out_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, output, out_addr)?;
call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[
entry.driver_id,
hic,
ICM_COMPRESS_GET_SIZE,
in_addr,
out_addr,
],
)
}
pub fn ic_compress_begin(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
input: &Bih,
output: &Bih,
) -> Result<u32, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICCompressBegin",
reason: format!("unknown HIC {hic}"),
})?;
msmpeg4_v3_preinit(mmu, state, &entry)?;
let in_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, input, in_addr)?;
let out_addr = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, output, out_addr)?;
call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, ICM_COMPRESS_BEGIN, in_addr, out_addr],
)
}
pub fn ic_compress_end(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
) -> Result<u32, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICCompressEnd",
reason: format!("unknown HIC {hic}"),
})?;
call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, ICM_COMPRESS_END, 0, 0],
)
}
pub fn ic_get_state(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
dst_buf: &mut [u8],
) -> Result<u32, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICGetState",
reason: format!("unknown HIC {hic}"),
})?;
let len = dst_buf.len() as u32;
let buf_va = state.arena_alloc(len.max(1))?;
let trap = |t: crate::emulator::Trap| Win32Error::InvalidArgument {
stub: "ICGetState",
reason: format!("{t}"),
};
if len > 0 {
let zeros = vec![0u8; len as usize];
mmu.write_initializer(buf_va, &zeros).map_err(trap)?;
}
let lresult = call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, ICM_GETSTATE, buf_va, len],
)?;
for (i, b) in dst_buf.iter_mut().enumerate() {
*b = mmu.load8(buf_va + i as u32).map_err(trap)?;
}
Ok(lresult)
}
pub fn ic_set_state(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
src_buf: &[u8],
) -> Result<(), crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICSetState",
reason: format!("unknown HIC {hic}"),
})?;
let len = src_buf.len() as u32;
let buf_va = state.arena_alloc(len.max(1))?;
let trap = |t: crate::emulator::Trap| Win32Error::InvalidArgument {
stub: "ICSetState",
reason: format!("{t}"),
};
if len > 0 {
mmu.write_initializer(buf_va, src_buf).map_err(trap)?;
}
let lresult = call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, ICM_SETSTATE, buf_va, len],
)?;
if lresult as i32 == ICERR_OK {
Ok(())
} else {
Err(crate::Error::Win32(Win32Error::InvalidArgument {
stub: "ICSetState",
reason: format!("codec returned LRESULT {lresult:#010x} (not ICERR_OK)"),
}))
}
}
#[derive(Debug, Clone, Default)]
pub struct CompressOutcome {
pub lresult: u32,
pub bytes: Vec<u8>,
pub output_bih: Bih,
pub returned_flags: u32,
pub ckid: u32,
}
#[allow(clippy::too_many_arguments)]
pub fn ic_compress(
cpu: &mut Cpu,
mmu: &mut Mmu,
registry: &Registry,
state: &mut HostState,
hic: u32,
flags: u32,
input_bih: &Bih,
input_bytes: &[u8],
output_bih: &Bih,
output_capacity: u32,
ckid: u32,
frame_num: i32,
frame_size_limit: u32,
quality: u32,
prev_bih_opt: Option<&Bih>,
prev_bytes_opt: Option<&[u8]>,
) -> Result<CompressOutcome, crate::Error> {
let entry = state
.hics
.get(&hic)
.cloned()
.ok_or_else(|| Win32Error::InvalidArgument {
stub: "ICCompress",
reason: format!("unknown HIC {hic}"),
})?;
let bi_in = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, input_bih, bi_in)?;
let bi_out = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, output_bih, bi_out)?;
let in_buf = state.arena_alloc(input_bytes.len().max(1) as u32)?;
if !input_bytes.is_empty() {
mmu.write_initializer(in_buf, input_bytes)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICCompress",
reason: format!("{t}"),
})?;
}
let out_buf = state.arena_alloc(output_capacity.max(1))?;
let zeros = vec![0u8; output_capacity as usize];
mmu.write_initializer(out_buf, &zeros)
.map_err(|t| Win32Error::InvalidArgument {
stub: "ICCompress",
reason: format!("{t}"),
})?;
let ckid_slot = state.arena_alloc(4)?;
let trap = |t: crate::emulator::Trap| Win32Error::InvalidArgument {
stub: "ICCompress",
reason: format!("{t}"),
};
mmu.store32(ckid_slot, ckid).map_err(trap)?;
let flags_slot = state.arena_alloc(4)?;
mmu.store32(flags_slot, flags).map_err(trap)?;
let (bi_prev, prev_buf) = match (prev_bih_opt, prev_bytes_opt) {
(Some(bih), Some(bytes)) => {
let bp = state.arena_alloc(BIH_SIZE + BIH_TAIL_CAP)?;
host_bih_to_guest(mmu, bih, bp)?;
let pb = state.arena_alloc(bytes.len().max(1) as u32)?;
if !bytes.is_empty() {
mmu.write_initializer(pb, bytes).map_err(trap)?;
}
(bp, pb)
}
_ => (0, 0),
};
let icc = state.arena_alloc(ICCOMPRESS_SIZE)?;
mmu.store32(icc, flags).map_err(trap)?; mmu.store32(icc + 4, bi_out).map_err(trap)?; mmu.store32(icc + 8, out_buf).map_err(trap)?; mmu.store32(icc + 12, bi_in).map_err(trap)?; mmu.store32(icc + 16, in_buf).map_err(trap)?; mmu.store32(icc + 20, ckid_slot).map_err(trap)?; mmu.store32(icc + 24, flags_slot).map_err(trap)?; mmu.store32(icc + 28, frame_num as u32).map_err(trap)?; mmu.store32(icc + 32, frame_size_limit).map_err(trap)?; mmu.store32(icc + 36, quality).map_err(trap)?; mmu.store32(icc + 40, bi_prev).map_err(trap)?; mmu.store32(icc + 44, prev_buf).map_err(trap)?;
let lresult = call_guest(
cpu,
mmu,
registry,
state,
entry.driver_proc_va,
&[entry.driver_id, hic, ICM_COMPRESS, icc, ICCOMPRESS_SIZE],
)?;
let mut bytes = vec![0u8; output_capacity as usize];
for (i, b) in bytes.iter_mut().enumerate() {
*b = mmu.load8(out_buf + i as u32).map_err(trap)?;
}
let output_bih_back = guest_bih_to_host(mmu, bi_out)?;
let returned_flags = mmu.load32(flags_slot).map_err(trap)?;
let ckid_back = mmu.load32(ckid_slot).map_err(trap)?;
let encoded_len = output_bih_back.size_image;
if encoded_len > 0 && encoded_len <= output_capacity {
bytes.truncate(encoded_len as usize);
}
Ok(CompressOutcome {
lresult,
bytes,
output_bih: output_bih_back,
returned_flags,
ckid: ckid_back,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::emulator::{
isa_int::RET_SENTINEL,
mmu::{Mmu, Perm},
regs::Reg32,
Cpu,
};
use crate::win32::{HostState, Registry};
fn install_canned_driver_proc(mmu: &mut Mmu, va: u32, ret_value: u32) {
mmu.map(va & !0xFFF, 0x1000, Perm::R | Perm::W | Perm::X);
let mut code = [0u8; 8];
code[0] = 0xB8;
code[1..5].copy_from_slice(&ret_value.to_le_bytes());
code[5] = 0xC2;
code[6..8].copy_from_slice(&20u16.to_le_bytes());
mmu.write_initializer(va, &code).unwrap();
}
fn make_env() -> (Cpu, Mmu, Registry, HostState) {
let mut mmu = Mmu::new();
mmu.map(0x6000_0000, 0x10_0000, Perm::R | Perm::W);
mmu.map(0x9000_0000, 0x10_0000, Perm::R | Perm::W);
let mut cpu = Cpu::new();
cpu.regs.set_esp(0x9000_0000 + 0x0F_0000);
let mut registry = Registry::new();
registry.register_kernel32();
let state = HostState::new(0x6000_0000, 0x6000_0000 + 0x10_0000);
(cpu, mmu, registry, state)
}
#[test]
fn host_bih_marshal_roundtrip() {
let (mut _cpu, mut mmu, _registry, _state) = make_env();
let bih = Bih {
bi_size: 40,
width: 320,
height: 240,
planes: 1,
bit_count: 24,
compression: *b"cvid",
size_image: 320 * 240 * 3 / 2,
x_pels_per_meter: 0,
y_pels_per_meter: 0,
clr_used: 0,
clr_important: 0,
tail: Vec::new(),
};
host_bih_to_guest(&mut mmu, &bih, 0x6000_0000).unwrap();
let back = guest_bih_to_host(&mmu, 0x6000_0000).unwrap();
assert_eq!(bih, back);
}
#[test]
fn ic_open_with_canned_driver_returns_synthetic_hic() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xC0FFEE);
state.default_driver_proc = dpv;
let fcc_video = u32::from_le_bytes(*b"VIDC");
let fcc_cvid = u32::from_le_bytes(*b"cvid");
let hic = ic_open(
&mut cpu, &mut mmu, ®istry, &mut state, fcc_video, fcc_cvid, 1,
)
.unwrap();
assert_ne!(hic, 0);
let entry = state.hics.get(&hic).unwrap();
assert_eq!(entry.driver_id, 0xC0FFEE);
assert_eq!(entry.driver_proc_va, dpv);
assert_eq!(entry.fcc_type, fcc_video);
assert_eq!(entry.fcc_handler, fcc_cvid);
assert_eq!(cpu.regs.eip, RET_SENTINEL);
}
#[test]
fn ic_open_returning_zero_does_not_install_hic() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
assert_eq!(hic, 0);
assert!(state.hics.is_empty());
}
#[test]
fn ic_close_invokes_driver_proc_and_drops_hic() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xABCD);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
let _ = ic_close(&mut cpu, &mut mmu, ®istry, &mut state, hic).unwrap();
assert!(state.hics.is_empty());
}
#[test]
fn ic_close_unknown_hic_errors() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let r = ic_close(&mut cpu, &mut mmu, ®istry, &mut state, 99);
assert!(r.is_err());
}
#[test]
fn ic_get_info_reads_back_codec_buffer() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 16);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
let bytes = ic_get_info(&mut cpu, &mut mmu, ®istry, &mut state, hic, 32).unwrap();
assert_eq!(bytes.len(), 16);
assert!(bytes.iter().all(|b| *b == 0));
}
#[test]
fn ic_get_info_short_return_synthesises_known_indeo_fcc() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xC0FFEE);
state.default_driver_proc = dpv;
let fcc_video = u32::from_le_bytes(*b"VIDC");
let fcc_iv41 = u32::from_le_bytes(*b"IV41");
let hic = ic_open(
&mut cpu, &mut mmu, ®istry, &mut state, fcc_video, fcc_iv41, 1,
)
.unwrap();
assert_ne!(hic, 0);
install_canned_driver_proc(&mut mmu, dpv, 0);
let cb = 96u32;
let bytes = ic_get_info(&mut cpu, &mut mmu, ®istry, &mut state, hic, cb).unwrap();
assert_eq!(bytes.len(), cb as usize);
assert_eq!(&bytes[0..4], &cb.to_le_bytes());
assert_eq!(&bytes[4..8], &fcc_video.to_le_bytes());
assert_eq!(&bytes[8..12], &fcc_iv41.to_le_bytes());
assert_eq!(bytes[24], b'I');
assert_eq!(bytes[26], b'V');
assert_eq!(bytes[28], b'4');
assert_eq!(bytes[30], b'1');
assert_eq!(bytes[25], 0);
assert_eq!(bytes[27], 0);
assert_eq!(bytes[29], 0);
assert_eq!(bytes[31], 0);
}
#[test]
fn ic_get_info_short_return_unknown_fcc_returns_empty() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xC0FFEE);
state.default_driver_proc = dpv;
let fcc_unknown = u32::from_le_bytes(*b"XXXX");
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, fcc_unknown, 1).unwrap();
install_canned_driver_proc(&mut mmu, dpv, 0);
let bytes = ic_get_info(&mut cpu, &mut mmu, ®istry, &mut state, hic, 64).unwrap();
assert_eq!(bytes.len(), 0);
}
#[test]
fn ic_decompress_round_trip_passes_buffers_through_emulator() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xDEAD_BEEF);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
assert_ne!(hic, 0);
let bih_in = Bih {
width: 16,
height: 16,
bit_count: 24,
compression: *b"cvid",
..Default::default()
};
let bih_out = Bih {
width: 16,
height: 16,
bit_count: 24,
..Default::default()
};
let input = vec![0xAAu8; 64];
let (lr, out) = ic_decompress(
&mut cpu,
&mut mmu,
®istry,
&mut state,
hic,
0,
&bih_in,
&input,
&bih_out,
16 * 16 * 3,
)
.unwrap();
assert_eq!(lr, 0xDEAD_BEEF);
assert_eq!(out.len(), 16 * 16 * 3);
assert_eq!(cpu.regs.get32(Reg32::Eax), 0xDEAD_BEEF);
}
#[test]
fn iccompress_struct_size_matches_vfw_h_definition() {
assert_eq!(ICCOMPRESS_SIZE, 48);
}
#[test]
fn icm_compress_constants_match_vfw_h_offsets() {
assert_eq!(ICM_COMPRESS_GET_FORMAT, 0x4004);
assert_eq!(ICM_COMPRESS_GET_SIZE, 0x4005);
assert_eq!(ICM_COMPRESS_QUERY, 0x4006);
assert_eq!(ICM_COMPRESS_BEGIN, 0x4007);
assert_eq!(ICM_COMPRESS, 0x4008);
assert_eq!(ICM_COMPRESS_END, 0x4009);
assert_eq!(ICCOMPRESS_KEYFRAME, 0x0000_0001);
}
#[test]
fn ic_compress_query_dispatches_to_driver_proc() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xC0FFEE);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
assert_ne!(hic, 0);
install_canned_driver_proc(&mut mmu, dpv, 0);
let bih_in = Bih {
width: 176,
height: 144,
bit_count: 24,
compression: [0; 4],
..Default::default()
};
let bih_out = Bih {
width: 176,
height: 144,
bit_count: 24,
compression: *b"MP43",
..Default::default()
};
let lr = ic_compress_query(
&mut cpu,
&mut mmu,
®istry,
&mut state,
hic,
&bih_in,
Some(&bih_out),
)
.unwrap();
assert_eq!(lr, 0);
}
#[test]
fn ic_compress_round_trip_passes_buffers_through_emulator() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xDEAD_BEEF);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
assert_ne!(hic, 0);
let bih_in = Bih {
width: 16,
height: 16,
bit_count: 24,
compression: [0; 4],
..Default::default()
};
let bih_out = Bih {
width: 16,
height: 16,
bit_count: 24,
compression: *b"MP43",
..Default::default()
};
let input = vec![0xAAu8; 16 * 16 * 3];
let outcome = ic_compress(
&mut cpu,
&mut mmu,
®istry,
&mut state,
hic,
ICCOMPRESS_KEYFRAME,
&bih_in,
&input,
&bih_out,
16 * 16 * 3,
u32::from_le_bytes(*b"00dc"),
0,
0,
5000,
None,
None,
)
.unwrap();
assert_eq!(outcome.lresult, 0xDEAD_BEEF);
assert_eq!(outcome.bytes.len(), 16 * 16 * 3);
assert_eq!(outcome.returned_flags, ICCOMPRESS_KEYFRAME);
}
#[test]
fn icm_state_constants_match_vfw_h_offsets() {
assert_eq!(ICM_GETSTATE, 0x5009);
assert_eq!(ICM_SETSTATE, 0x500A);
}
#[test]
fn ic_get_state_dispatches_to_driver_proc_and_returns_lresult() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xC0FFEE);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
assert_ne!(hic, 0);
install_canned_driver_proc(&mut mmu, dpv, 24);
let mut buf = vec![0u8; 64];
let written = ic_get_state(&mut cpu, &mut mmu, ®istry, &mut state, hic, &mut buf)
.expect("ic_get_state should succeed");
assert_eq!(written, 24);
assert_eq!(cpu.regs.get32(Reg32::Eax), 24);
}
#[test]
fn ic_set_state_dispatches_to_driver_proc_and_returns_ok() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xC0FFEE);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
assert_ne!(hic, 0);
install_canned_driver_proc(&mut mmu, dpv, 0);
let payload = vec![0xAAu8; 32];
ic_set_state(&mut cpu, &mut mmu, ®istry, &mut state, hic, &payload)
.expect("ic_set_state should succeed when codec returns ICERR_OK");
}
#[test]
fn ic_set_state_surfaces_codec_failure_lresult() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xC0FFEE);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
assert_ne!(hic, 0);
install_canned_driver_proc(&mut mmu, dpv, 0xFFFF_FFFE);
let payload = vec![0u8; 4];
let r = ic_set_state(&mut cpu, &mut mmu, ®istry, &mut state, hic, &payload);
match r {
Err(crate::Error::Win32(Win32Error::InvalidArgument { stub, .. })) => {
assert_eq!(stub, "ICSetState");
}
other => panic!("expected Win32Error::InvalidArgument, got {other:?}"),
}
}
#[test]
fn ic_get_state_zero_length_buffer_is_legal_probe_call() {
let (mut cpu, mut mmu, registry, mut state) = make_env();
let dpv = 0x0040_0000;
install_canned_driver_proc(&mut mmu, dpv, 0xC0FFEE);
state.default_driver_proc = dpv;
let hic = ic_open(&mut cpu, &mut mmu, ®istry, &mut state, 0, 0, 1).unwrap();
install_canned_driver_proc(&mut mmu, dpv, 128);
let mut empty: Vec<u8> = Vec::new();
let r = ic_get_state(&mut cpu, &mut mmu, ®istry, &mut state, hic, &mut empty)
.expect("zero-length probe should succeed");
assert_eq!(r, 128);
}
}