From 62db689ad690f7b67d795191a916c37db23e8c23 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Tue, 21 Jul 2026 00:16:12 +0300 Subject: [PATCH 01/10] start on winpin, pinball soccer '98 --- Cargo.lock | 9 + Cargo.toml | 1 + out/winpin/.gitignore | 3 + out/winpin/Cargo.toml | 14 + out/winpin/src/lib.rs | 9 + out/winpin/src/main.rs | 8 + runtime/src/fpu.rs | 4 + runtime/src/lib.rs | 26 ++ runtime/src/machine.rs | 6 +- runtime/src/ops/misc.rs | 38 ++- runtime/src/ops/string.rs | 9 + tc/src/codegen/control_flow.rs | 11 +- tc/src/codegen/fpu.rs | 20 ++ tc/src/codegen/misc.rs | 27 +- tc/src/codegen/mod.rs | 50 ++- tc/src/gather.rs | 386 +++++++++++++++++++++--- tc/src/lib.rs | 1 + tc/src/main.rs | 21 +- translate.sh | 13 + web/game | 1 + win32/winapi/src/advapi32.rs | 13 + win32/winapi/src/ddraw/ddraw.rs | 50 ++- win32/winapi/src/ddraw/ddraw1.rs | 295 +++++++++++++++++- win32/winapi/src/dinput.rs | 369 +++++++++++++++++++++++ win32/winapi/src/gdi32/misc.rs | 20 ++ win32/winapi/src/kernel32/env.rs | 12 +- win32/winapi/src/kernel32/event.rs | 30 ++ win32/winapi/src/kernel32/file.rs | 434 ++++++++++++++++++++++++++- win32/winapi/src/kernel32/heap.rs | 36 ++- win32/winapi/src/kernel32/misc.rs | 31 +- win32/winapi/src/kernel32/mod.rs | 2 + win32/winapi/src/kernel32/nls.rs | 183 +++++++++-- win32/winapi/src/kernel32/strings.rs | 118 ++++++++ win32/winapi/src/kernel32/time.rs | 31 ++ win32/winapi/src/lib.rs | 5 +- win32/winapi/src/msacm32.rs | 6 + win32/winapi/src/ole32.rs | 21 ++ win32/winapi/src/user32/dialog.rs | 12 + win32/winapi/src/user32/message.rs | 62 +++- win32/winapi/src/user32/misc.rs | 98 ++++++ win32/winapi/src/user32/rect.rs | 19 ++ win32/winapi/src/user32/window.rs | 62 +++- win32/winapi/src/winmm/mmio.rs | 72 +++++ win32/winapi/src/winmm/mod.rs | 2 + win32/winapi/src/winmm/time.rs | 5 + 45 files changed, 2494 insertions(+), 151 deletions(-) create mode 100644 out/winpin/.gitignore create mode 100644 out/winpin/Cargo.toml create mode 100644 out/winpin/src/lib.rs create mode 100644 out/winpin/src/main.rs create mode 120000 web/game create mode 100644 win32/winapi/src/dinput.rs create mode 100644 win32/winapi/src/kernel32/strings.rs create mode 100644 win32/winapi/src/msacm32.rs create mode 100644 win32/winapi/src/ole32.rs create mode 100644 win32/winapi/src/winmm/mmio.rs diff --git a/Cargo.lock b/Cargo.lock index 56fddf2a..31b1dff5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -666,6 +666,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winpin" +version = "0.1.0" +dependencies = [ + "runtime", + "wasm-bindgen", + "winapi 0.1.0", +] + [[package]] name = "zerocopy" version = "0.8.40" diff --git a/Cargo.toml b/Cargo.toml index f57b5d0c..cb5b4202 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "out/sbaitso/sbtalker", "out/thread", "out/winapi", + "out/winpin", "out/zig_hello", "runtime", "tc", diff --git a/out/winpin/.gitignore b/out/winpin/.gitignore new file mode 100644 index 00000000..03642c0e --- /dev/null +++ b/out/winpin/.gitignore @@ -0,0 +1,3 @@ +# Generated by `translate.sh winpin` from a copy of the game. +/data/ +/src/generated* diff --git a/out/winpin/Cargo.toml b/out/winpin/Cargo.toml new file mode 100644 index 00000000..bf44706f --- /dev/null +++ b/out/winpin/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "winpin" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["lib", "cdylib"] + +[dependencies] +runtime = { workspace = true } +winapi = { workspace = true } + +[target.'cfg(target_family = "wasm")'.dependencies] +wasm-bindgen = { workspace = true } diff --git a/out/winpin/src/lib.rs b/out/winpin/src/lib.rs new file mode 100644 index 00000000..2de7e0f8 --- /dev/null +++ b/out/winpin/src/lib.rs @@ -0,0 +1,9 @@ +#[cfg(target_family = "wasm")] +use wasm_bindgen::prelude::*; + +mod generated; + +#[cfg_attr(target_family = "wasm", wasm_bindgen)] +pub fn main() { + winapi::run(&generated::EXEDATA); +} diff --git a/out/winpin/src/main.rs b/out/winpin/src/main.rs new file mode 100644 index 00000000..08d1d4ab --- /dev/null +++ b/out/winpin/src/main.rs @@ -0,0 +1,8 @@ +pub fn main() { + // The game reads its data files via paths relative to the current directory; + // pass the game directory as the first argument to run from elsewhere. + if let Some(dir) = std::env::args().nth(1) { + std::env::set_current_dir(&dir).unwrap(); + } + winpin::main(); +} diff --git a/runtime/src/fpu.rs b/runtime/src/fpu.rs index 71316d77..c6a40235 100644 --- a/runtime/src/fpu.rs +++ b/runtime/src/fpu.rs @@ -18,6 +18,9 @@ pub struct FPU { pub st_top: usize, /// The result of the last fcmp, used to generate status word. pub cmp: std::cmp::Ordering, + /// Control word, as managed by fldcw/fnstcw. We only round-trip the value; + /// precision/rounding control bits are not honored. + pub control: u16, } impl Default for FPU { @@ -26,6 +29,7 @@ impl Default for FPU { st: [0.; 8], st_top: 8, cmp: std::cmp::Ordering::Equal, + control: 0x037f, } } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index f7c69506..ac403e08 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -58,6 +58,32 @@ pub struct Cont(pub ContFn); pub const RETURN_FROM_X86_ADDR32: u32 = 0xffff_fffe; pub const RETURN_FROM_X86_ADDR16: SegOfs = SegOfs::new(0xffff, 0xfffe); +/// Record a code address the static analysis missed, so it can be fed back +/// into tc via --entry-points-file. Set THESEUS_MISSING_ADDRS to a file path. +pub fn log_missing_addr(addr: u32) { + #[cfg(not(target_family = "wasm"))] + if let Ok(path) = std::env::var("THESEUS_MISSING_ADDRS") { + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path) + { + let _ = writeln!(f, "{addr:x}"); + } + } +} + +/// Called by generated code for a statically-known jump target that the +/// analysis didn't produce a block for. +pub fn unknown_block(addr: u32) -> Cont { + log_missing_addr(addr); + panic!( + "jmp to unknown block {addr:#010x}; \ + re-run tc with --entry-points-file (see THESEUS_MISSING_ADDRS)" + ); +} + impl Context { /// Call an x86 stdcall function, only returning once the function returns. pub fn call32_x86(&mut self, f: Cont, args: Vec) { diff --git a/runtime/src/machine.rs b/runtime/src/machine.rs index 67821980..3e0ed61e 100644 --- a/runtime/src/machine.rs +++ b/runtime/src/machine.rs @@ -50,7 +50,11 @@ impl Context { // TODO: this would be faster as a hash table, or even a perfect hash if we really cared. let Ok(index) = self.blocks.binary_search_by_key(&addr, |(addr, _)| *addr) else { self.dump(); - panic!("jmp to unknown addr {addr:#08x}"); + crate::log_missing_addr(addr); + panic!( + "jmp to unknown addr {addr:#010x}; \ + re-run tc with --entry-points-file (see THESEUS_MISSING_ADDRS)" + ); }; Cont(self.blocks[index].1) } diff --git a/runtime/src/ops/misc.rs b/runtime/src/ops/misc.rs index adf51b17..a4c06e3d 100644 --- a/runtime/src/ops/misc.rs +++ b/runtime/src/ops/misc.rs @@ -65,12 +65,44 @@ impl Context { self.cpu.flags.contains(Flags::ZF) as u8 } + pub fn setne(self: &Context) -> u8 { + !self.cpu.flags.contains(Flags::ZF) as u8 + } + + pub fn setg(self: &Context) -> u8 { + (!self.cpu.flags.contains(Flags::ZF) + && self.cpu.flags.contains(Flags::SF) == self.cpu.flags.contains(Flags::OF)) + as u8 + } + pub fn setge(self: &Context) -> u8 { - (self.cpu.flags.contains(Flags::ZF) == self.cpu.flags.contains(Flags::OF)) as u8 + (self.cpu.flags.contains(Flags::SF) == self.cpu.flags.contains(Flags::OF)) as u8 } - pub fn setne(self: &Context) -> u8 { - !self.cpu.flags.contains(Flags::ZF) as u8 + pub fn setl(self: &Context) -> u8 { + (self.cpu.flags.contains(Flags::SF) != self.cpu.flags.contains(Flags::OF)) as u8 + } + + pub fn setle(self: &Context) -> u8 { + (self.cpu.flags.contains(Flags::ZF) + || self.cpu.flags.contains(Flags::SF) != self.cpu.flags.contains(Flags::OF)) + as u8 + } + + pub fn seta(self: &Context) -> u8 { + (!self.cpu.flags.contains(Flags::CF) && !self.cpu.flags.contains(Flags::ZF)) as u8 + } + + pub fn setae(self: &Context) -> u8 { + !self.cpu.flags.contains(Flags::CF) as u8 + } + + pub fn setb(self: &Context) -> u8 { + self.cpu.flags.contains(Flags::CF) as u8 + } + + pub fn setbe(self: &Context) -> u8 { + (self.cpu.flags.contains(Flags::CF) || self.cpu.flags.contains(Flags::ZF)) as u8 } pub fn sti(&mut self) { diff --git a/runtime/src/ops/string.rs b/runtime/src/ops/string.rs index 66738195..b71984d7 100644 --- a/runtime/src/ops/string.rs +++ b/runtime/src/ops/string.rs @@ -129,6 +129,12 @@ impl Context { pub fn scasb(&mut self) { self.scas::() } + pub fn scasw(&mut self) { + self.scas::() + } + pub fn scasd(&mut self) { + self.scas::() + } fn cmps(&mut self) { let src = self @@ -154,6 +160,9 @@ impl Context { pub fn cmpsw(&mut self) { self.cmps::() } + pub fn cmpsd(&mut self) { + self.cmps::() + } fn movs(&mut self) { let src_addr = self.addr(self.cpu.regs.ds, self.cpu.regs.esi); diff --git a/tc/src/codegen/control_flow.rs b/tc/src/codegen/control_flow.rs index d24a8209..8da66029 100644 --- a/tc/src/codegen/control_flow.rs +++ b/tc/src/codegen/control_flow.rs @@ -6,16 +6,11 @@ use crate::{ impl<'a> CodeGen<'a> { /// Codegen the Cont for a jump to an statically known address. - /// This should always resolve to a real symbol at translation time. - fn resolve_jmp(&self, ip: IP) -> String { - if let Some(block) = self.blocks.get(&ip.to_addr()) { - format!("Cont({})", block.name()) - } else { - format!("todo!(\"static jmp to unknown block {}\")", ip) - } + fn resolve_jmp(&mut self, ip: IP) -> String { + self.resolve_cont(ip.to_addr()) } - fn jmp_target(&self, instr: &Instr) -> (Option, Option, String) { + fn jmp_target(&mut self, instr: &Instr) -> (Option, Option, String) { assert_eq!(instr.iced.op_count(), 1); let mut extra: Option = None; let mut seg: Option = None; diff --git a/tc/src/codegen/fpu.rs b/tc/src/codegen/fpu.rs index 3a3a69de..f35712fa 100644 --- a/tc/src/codegen/fpu.rs +++ b/tc/src/codegen/fpu.rs @@ -207,6 +207,26 @@ impl<'a> CodeGen<'a> { self.line(self.set_op(instr, 0, "ctx.cpu.fpu.status()".into())); } + // We don't model FPU exceptions, so clearing them is a no-op. + Fnclex => {} + + Fninit => { + self.line("ctx.cpu.fpu.control = 0x037f;"); + } + + Fnstcw => { + assert_eq!(instr.op_count(), 1); + self.line(self.set_op(instr, 0, "ctx.cpu.fpu.control".into())); + } + + Fldcw => { + assert_eq!(instr.op_count(), 1); + self.line(format!( + "ctx.cpu.fpu.control = {};", + self.get_op(instr, 0) + )); + } + Fpatan => { self.line("let t = ctx.cpu.fpu.get(0);"); self.line("ctx.cpu.fpu.pop();"); diff --git a/tc/src/codegen/misc.rs b/tc/src/codegen/misc.rs index ad3061b0..827dd83d 100644 --- a/tc/src/codegen/misc.rs +++ b/tc/src/codegen/misc.rs @@ -25,7 +25,7 @@ impl<'a> CodeGen<'a> { Popad => self.line("ctx.popad();"), Mov => self.line(self.set_op(instr, 0, self.get_op(instr, 1))), - Sete | Setg | Setge | Setne => { + Sete | Setne | Setg | Setge | Setl | Setle | Seta | Setae | Setb | Setbe => { self.line(self.set_op(instr, 0, format!("ctx.{}()", instr_name(instr)))) } @@ -74,17 +74,24 @@ impl<'a> CodeGen<'a> { self.line(self.set_op(instr, 1, "t".into())); } Nop => {} + // x87 exception sync; our FPU never raises. + Wait => {} Not => self.line(self.set_op(instr, 0, format!("!{}", self.get_op(instr, 0)))), Int => { assert!(instr.op0_kind() == iced_x86::OpKind::Immediate8); - assert!(self.module.is_dos()); - self.line(format!( - "dos::int(ctx, {:#x}, {:#x})", - instr.next_ip32(), - instr.immediate8() - )); + // A misidentified code pointer can land us on an `int` in + // non-DOS code, where there's nothing to call. + if self.module.is_dos() { + self.line(format!( + "dos::int(ctx, {:#x}, {:#x})", + instr.next_ip32(), + instr.immediate8() + )); + } else { + self.todo(format!("int {:#x}", instr.immediate8())); + } } Int3 | Cmpxchg | Pushfd | Cpuid | Xgetbv | Bt | Div => self.todo(instr_name(instr)), @@ -117,7 +124,11 @@ impl<'a> CodeGen<'a> { Lds | Les | Lfs | Lgs | Lss => { assert_eq!(instr.op_count(), 2); - assert_eq!(self.module.bitness(), 16); + if self.module.bitness() != 16 { + // Usually a junk block from a misidentified code pointer. + self.todo(instr_name(instr)); + return true; + } self.line(format!( "let ptr = {};", get_mem("u32".into(), self.gen_addr(instr)), diff --git a/tc/src/codegen/mod.rs b/tc/src/codegen/mod.rs index a73ba55d..dd22f534 100644 --- a/tc/src/codegen/mod.rs +++ b/tc/src/codegen/mod.rs @@ -201,6 +201,9 @@ pub struct CodeGen<'a> { mem: &'a Memory, blocks: &'a HashMap, trace: bool, + /// Statically-known jump targets with no discovered block; each gets a + /// panicking stub function so the output still compiles and runs. + unknown: std::collections::BTreeSet, /// Output buffer. buf: String, } @@ -212,6 +215,7 @@ impl<'a> CodeGen<'a> { mem: &state.mem, blocks: &state.blocks, trace, + unknown: Default::default(), buf: Default::default(), } } @@ -221,6 +225,19 @@ impl<'a> CodeGen<'a> { self.buf.push('\n'); } + /// Codegen a Cont expression for a jump to a statically known address. + /// Safe to evaluate in argument position: an unknown target resolves to a + /// stub that only panics once actually jumped to. + pub fn resolve_cont(&mut self, addr: u32) -> String { + if let Some(block) = self.blocks.get(&addr) { + format!("Cont({})", block.name()) + } else { + log::warn!("static jmp to unknown block {addr:08x}"); + self.unknown.insert(addr); + format!("Cont(unk_{addr:x})") + } + } + fn todo(&mut self, msg: String) { self.line(format!("todo!({msg:?});")); } @@ -242,9 +259,24 @@ impl<'a> CodeGen<'a> { Module::Windows(_) => todo!(), } } - if let Err(e) = self.gen_instr(instr) { - self.line(format!("panic!({:?});", e.to_string())); - break; + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.gen_instr(instr) + })); + match result { + Ok(Ok(())) => {} + Ok(Err(e)) => { + self.line(format!("panic!({:?});", e.to_string())); + break; + } + Err(_) => { + // Usually a junk block from a misidentified code pointer. + log::warn!("codegen panic at {} {}", instr.ip, instr.iced); + self.line(format!( + "todo!({:?});", + format!("codegen failed: {}", instr.iced) + )); + break; + } } } @@ -252,8 +284,8 @@ impl<'a> CodeGen<'a> { if last.iced.flow_control() == iced_x86::FlowControl::Next || (last.iced.mnemonic() == iced_x86::Mnemonic::Call && last.hint.is_some()) { - let next_block = self.blocks.get(&last.next_ip().to_addr()).unwrap(); - self.line(format!("Cont({})", next_block.name())); + let cont = self.resolve_cont(last.next_ip().to_addr()); + self.line(cont); } self.line("}\n"); @@ -371,6 +403,14 @@ ctx.cpu.regs.esp = {stack_pointer:#x}; self.gen_block(&block); } + for addr in self.unknown.iter().copied().collect::>() { + self.line(format!( + "pub fn unk_{addr:x}(_ctx: &mut Context) -> Cont {{ + runtime::unknown_block({addr:#x}) +}}\n" + )); + } + self.line(format!( "const BLOCKS: [(u32, ContFn); {}] = [\n", addrs.len() + 1, diff --git a/tc/src/gather.rs b/tc/src/gather.rs index aa45b310..70023b21 100644 --- a/tc/src/gather.rs +++ b/tc/src/gather.rs @@ -22,6 +22,29 @@ fn is_abs_memory_ref(instr: &iced_x86::Instruction) -> Option { Some(instr.memory_displacement32()) } +/// If the instruction looks like a switch dispatch +/// jmp/call [reg*4 + table] +/// where table is a constant, return the address of the table. +fn is_jump_table_ref(instr: &iced_x86::Instruction) -> Option { + let iced_x86::OpKind::Memory = instr.op0_kind() else { + return None; + }; + let iced_x86::Register::None = instr.memory_base() else { + return None; + }; + if instr.memory_index() == iced_x86::Register::None { + return None; + } + if instr.memory_index_scale() != 4 { + return None; + } + let table = instr.memory_displacement32(); + if table < 0x1000 { + return None; + } + Some(table) +} + #[derive(Clone, Copy, PartialEq, PartialOrd)] pub enum IP { Flat(u32), @@ -41,12 +64,6 @@ impl From<(u16, u16)> for IP { } impl IP { - /// called by code that hasn't been updated to be segmentation-aware - pub fn todo_segmenting(_addr: u32) -> IP { - todo!(); - //IP::Flat(addr) - } - pub fn seg(&self) -> u16 { match *self { IP::Flat(_) => unreachable!(), @@ -101,6 +118,7 @@ pub enum EntryPoint { pub struct Gather { pub scan_immediates: bool, pub scan_memory: bool, + pub scan_prologues: bool, pub entry_points: Vec, } @@ -138,6 +156,14 @@ struct Traverse<'a> { iat_refs: HashMap, queue: IPQueue, + /// Lower-confidence code addresses (from scans); validated before decoding. + candidates: VecDeque, + /// All addresses ever considered as candidates, to avoid rescanning. + candidate_seen: HashSet, + /// Jump table addresses already scanned. + seen_tables: HashSet, + /// Ranges within code sections that are known to be data (e.g. jump tables). + data_ranges: Vec>, invalid: HashSet, blocks: BTreeMap, } @@ -152,6 +178,10 @@ impl<'a> Traverse<'a> { iat_refs: Default::default(), queue: IPQueue::default(), + candidates: VecDeque::new(), + candidate_seen: HashSet::new(), + seen_tables: HashSet::new(), + data_ranges: Vec::new(), invalid: HashSet::new(), blocks: Default::default(), } @@ -211,36 +241,175 @@ impl<'a> Traverse<'a> { self.scan_for_pointers(); } - while let Some(ip) = self.queue.pop() { - let addr = ip.to_addr(); + self.drain(); + + if self.gather.scan_prologues { + if self.module.segment_addressed() { + log::warn!("--scan-prologues not supported for segmented (DOS) modules"); + } else { + loop { + let added = self.scan_gaps_for_prologues(); + if added == 0 { + break; + } + log::info!("prologue scan: {added} new candidates"); + self.drain(); + } + } + } + + self.report_coverage(); + } + + /// Process the high-confidence queue to exhaustion, interleaved with promoting + /// scanned (lower-confidence) candidates one at a time. + fn drain(&mut self) { + loop { + while let Some(ip) = self.queue.pop() { + self.process(ip); + } + let Some(addr) = self.candidates.pop_front() else { + break; + }; if self.blocks.contains_key(&addr) || self.invalid.contains(&addr) { continue; } + // Never split an existing block based on a mere scan hit; direct + // control flow that reaches the address will do that instead. + if self.find_containing_block(addr).is_some() { + continue; + } + if !self.looks_like_code(addr) { + continue; + } + self.queue.enqueue(self.module.local_addr(addr)); + } + } + + fn process(&mut self, ip: IP) { + let addr = ip.to_addr(); + if self.blocks.contains_key(&addr) || self.invalid.contains(&addr) { + return; + } - // If this ip is contained within an existing block, it means it is a - // jmp within some other code. - // Re-queue the other block for re-parsing after this one so that it can be split. - if let Some((&baddr, block)) = self.blocks.range(0..addr).last() { + // If this ip is contained within an existing block, it means it is a + // jmp within some other code. + // Re-queue the other block for re-parsing after this one so that it can be split. + if let Some(baddr) = self.find_containing_block(addr) { + if let Some(block) = self.blocks.remove(&baddr) { if let BlockType::Instrs(instrs) = &block.ty { - let range = instrs.first().unwrap().ip.to_addr() - ..instrs.last().unwrap().next_ip().to_addr(); - if range.contains(&addr) { - self.queue.enqueue(instrs[0].ip); - self.blocks.remove(&baddr); - } + self.queue.enqueue(instrs[0].ip); } } + } - match self.decode_one(ip) { - Ok(block) => { - self.blocks.insert(addr, block); - } - Err(e) => { - log::warn!("omitting {ip}: {e}"); - self.invalid.insert(addr); - } + match self.decode_one(ip) { + Ok(block) => { + self.blocks.insert(addr, block); + } + Err(e) => { + log::warn!("omitting {ip}: {e}"); + self.invalid.insert(addr); + } + } + } + + /// If addr falls in the middle of an existing block, return that block's address. + fn find_containing_block(&self, addr: u32) -> Option { + let (&baddr, block) = self.blocks.range(..addr).last()?; + if let BlockType::Instrs(instrs) = &block.ty { + let range = + instrs.first().unwrap().ip.to_addr()..instrs.last().unwrap().next_ip().to_addr(); + if range.contains(&addr) { + return Some(baddr); + } + } + None + } + + fn add_candidate(&mut self, addr: u32) -> bool { + if !self.candidate_seen.insert(addr) { + return false; + } + self.candidates.push_back(addr); + true + } + + /// Cheap validation for scanned code address candidates: the bytes must + /// decode as plausible instructions. + fn looks_like_code(&self, addr: u32) -> bool { + if !self.module.code_memory().contains(&addr) { + return false; + } + let data = self.mem.slice_all(addr); + if data.len() < 2 || data[0] == 0 { + return false; + } + let len = data.len().min(64); + let mut decoder = iced_x86::Decoder::with_ip( + self.module.bitness(), + &data[..len], + addr as u64, + iced_x86::DecoderOptions::NONE, + ); + let mut n = 0; + while decoder.can_decode() { + let instr = decoder.decode(); + if instr.is_invalid() { + // Truncated final instruction is fine; garbage is not. + return n > 0 && decoder.position() + 16 > len; + } + n += 1; + use iced_x86::FlowControl::*; + match instr.flow_control() { + Return | UnconditionalBranch | IndirectBranch | Interrupt => break, + _ => {} + } + if n >= 4 { + break; } } + n >= 1 + } + + /// Read a jump table: consecutive pointers into code, stopping at the first + /// entry that doesn't look like one. Returns the number of entries found. + fn scan_jump_table(&mut self, table: u32) -> usize { + if !self.seen_tables.insert(table) { + return 0; + } + let code = self.module.code_memory(); + let mut addr = table; + let mut count = 0; + while count < 2048 { + if addr as usize + 4 > self.mem.bytes.len() { + break; + } + let target = self.mem.read::(addr); + if !code.contains(&target) || !self.looks_like_code(target) { + break; + } + self.queue.enqueue(self.module.local_addr(target)); + addr += 4; + count += 1; + } + if count > 0 { + // Mark the table as data so prologue scanning doesn't look inside it. + self.data_ranges.push(table..addr); + } + count + } + + /// A `call [addr]` through a non-IAT slot: if the slot statically holds a + /// code pointer (e.g. a function pointer variable), queue it. + fn scan_pointer_slot(&mut self, slot: u32) { + if slot as usize + 4 > self.mem.bytes.len() { + return; + } + let target = self.mem.read::(slot); + if self.module.code_memory().contains(&target) { + self.add_candidate(target); + } } fn decode_one(&mut self, block_ip: IP) -> anyhow::Result { @@ -254,6 +423,12 @@ impl<'a> Traverse<'a> { anyhow::bail!("block appears zero-filled"); } + // Code addresses noticed along the way, processed after the decode loop + // (decoding borrows self.mem). + let mut found_tables: Vec = Vec::new(); + let mut found_slots: Vec = Vec::new(); + let mut found_imms: Vec = Vec::new(); + let mut instrs = Vec::new(); let decoder = iced_x86::Decoder::with_ip( self.module.bitness(), @@ -284,8 +459,7 @@ impl<'a> Traverse<'a> { if instr.op_kind(i) == iced_x86::OpKind::Immediate32 { let imm = instr.immediate32(); if self.module.code_memory().contains(&imm) { - log::info!("{imm:x} looks like a code pointer"); - self.queue.enqueue(IP::todo_segmenting(imm)); + found_imms.push(imm); } } } @@ -325,8 +499,16 @@ impl<'a> Traverse<'a> { continue; // don't end block here } } else { + // A call/jmp through a function pointer variable. + if !self.module.segment_addressed() { + found_slots.push(addr); + } log::warn!("{ip} {instr} ; indirect via memory"); } + } else if !self.module.segment_addressed() + && let Some(table) = is_jump_table_ref(&instr) + { + found_tables.push(table); } else { log::warn!("{ip} {instr} ; indirect via memory"); } @@ -353,6 +535,17 @@ impl<'a> Traverse<'a> { break; } + for table in found_tables { + let n = self.scan_jump_table(table); + log::info!("jump table at {table:08x}: {n} entries"); + } + for slot in found_slots { + self.scan_pointer_slot(slot); + } + for imm in found_imms { + self.add_candidate(imm); + } + let info = self.addr_info.get(&block_ip.to_addr()); Ok(Block { name: info.map(|info| info.name.clone()), @@ -361,28 +554,135 @@ impl<'a> Traverse<'a> { } fn scan_for_pointers(&mut self) { - for i in 0..self.mem.mappings.vec().len() { - let mapping = &self.mem.mappings.vec()[i]; - if mapping.addr == 0 || mapping.addr == self.module.code_memory().start { + if self.module.segment_addressed() { + log::warn!("--scan-memory not supported for segmented (DOS) modules"); + return; + } + let code = self.module.code_memory(); + let mut found = Vec::new(); + for mapping in self.mem.mappings.vec() { + if mapping.addr == 0 || mapping.addr == code.start { continue; } log::info!("scanning mapping {:?}", mapping); - let mapping_addr = mapping.addr; - if self.module.segment_addressed() { - todo!(); - } - let data = self.mem.bytes[mapping.addr as usize..][..mapping.size as usize].to_vec(); - for ofs in 0..data.len() - 4 { + let data = self.mem.slice(mapping.addr, mapping.size); + for ofs in 0..data.len().saturating_sub(4) { let value = u32::from_le_bytes([data[ofs], data[ofs + 1], data[ofs + 2], data[ofs + 3]]); - if self.module.code_memory().contains(&value) { - log::info!( - "{addr:08x}: found possible code pointer {value:x}", - addr = mapping_addr + ofs as u32 - ); - self.queue.enqueue(IP::Flat(value)); + if code.contains(&value) { + found.push(value); + } + } + } + for value in found { + self.add_candidate(value); + } + } + + /// Merged, sorted spans of all discovered blocks plus known data ranges. + fn covered_ranges(&self) -> Vec> { + let mut spans: Vec> = Vec::new(); + for block in self.blocks.values() { + if let BlockType::Instrs(instrs) = &block.ty { + spans.push( + instrs.first().unwrap().ip.to_addr()..instrs.last().unwrap().next_ip().to_addr(), + ); + } + } + spans.extend(self.data_ranges.iter().cloned()); + spans.sort_by_key(|r| r.start); + let mut merged: Vec> = Vec::new(); + for span in spans { + match merged.last_mut() { + Some(last) if span.start <= last.end => last.end = last.end.max(span.end), + _ => merged.push(span), + } + } + merged + } + + /// Uncovered ranges within the code section. + fn gaps(&self) -> Vec> { + let code = self.module.code_memory(); + let mut gaps = Vec::new(); + let mut pos = code.start; + for r in self.covered_ranges() { + if r.end <= code.start { + continue; + } + if r.start >= code.end { + break; + } + if r.start > pos { + gaps.push(pos..r.start.min(code.end)); + } + pos = pos.max(r.end); + if pos >= code.end { + break; + } + } + if pos < code.end { + gaps.push(pos..code.end); + } + gaps + } + + /// Search uncovered code ranges for `push ebp; mov ebp, esp` function + /// prologues, adding them as candidates. Returns how many new ones we found. + fn scan_gaps_for_prologues(&mut self) -> usize { + let mut found = Vec::new(); + for gap in self.gaps() { + let data = self.mem.slice(gap.start, gap.end - gap.start); + if data.len() < 3 { + continue; + } + for i in 0..data.len() - 2 { + if data[i] == 0x55 && data[i + 1] == 0x8b && data[i + 2] == 0xec { + found.push(gap.start + i as u32); } } } + let mut added = 0; + for addr in found { + if self.add_candidate(addr) { + added += 1; + } + } + added + } + + fn report_coverage(&self) { + let code = self.module.code_memory(); + let total = code.end - code.start; + if total == 0 { + return; + } + let mut covered = 0u32; + for r in self.covered_ranges() { + let start = r.start.max(code.start); + let end = r.end.min(code.end); + if start < end { + covered += end - start; + } + } + let blocks = self + .blocks + .values() + .filter(|b| matches!(b.ty, BlockType::Instrs(_))) + .count(); + log::info!( + "code coverage: {covered:#x}/{total:#x} bytes ({:.1}%), {blocks} blocks", + covered as f64 / total as f64 * 100.0 + ); + let mut gaps = self.gaps(); + gaps.sort_by_key(|r| std::cmp::Reverse(r.end - r.start)); + for gap in gaps.iter().take(5) { + log::info!( + " uncovered: {:08x}..{:08x} ({:#x} bytes)", + gap.start, + gap.end, + gap.end - gap.start + ); + } } } diff --git a/tc/src/lib.rs b/tc/src/lib.rs index 856efe07..9b81b50f 100644 --- a/tc/src/lib.rs +++ b/tc/src/lib.rs @@ -204,6 +204,7 @@ impl State { for (dll, vtables) in [ ("ddraw", winapi::ddraw::VTABLES.as_slice()), ("dsound", winapi::dsound::VTABLES.as_slice()), + ("dinput", winapi::dinput::VTABLES.as_slice()), ] { if !module.imports.iter().any(|imp| imp.dll == dll) { continue; diff --git a/tc/src/main.rs b/tc/src/main.rs index 444fbdc6..97915e76 100644 --- a/tc/src/main.rs +++ b/tc/src/main.rs @@ -43,10 +43,18 @@ struct Args { #[argh(switch)] scan_immediates: bool, + /// scan unexplored code ranges for function prologues + #[argh(switch)] + scan_prologues: bool, + /// additional addresses to create a block #[argh(option, from_str_fn(parse_ip))] entry_point: Vec, + /// file with additional entry point addresses, one hex address per line + #[argh(option)] + entry_points_file: Option, + /// additional addresses containing pointers to code #[argh(option, from_str_fn(parse_ip_range))] jump_table: Vec>, @@ -114,6 +122,17 @@ fn run() -> anyhow::Result<()> { } entry_points.push(tc::EntryPoint::Single(ip)); } + if let Some(path) = &args.entry_points_file { + for line in std::fs::read_to_string(path)?.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let ip = parse_ip(line) + .map_err(|err| anyhow::anyhow!("{path}: bad address {line:?}: {err}"))?; + entry_points.push(tc::EntryPoint::Single(ip)); + } + } for range in args.jump_table { let mut src = range.start; // TODO while src <= range.end { @@ -154,8 +173,8 @@ fn run() -> anyhow::Result<()> { state.gather(tc::Gather { scan_immediates: args.scan_immediates, scan_memory: args.scan_memory, + scan_prologues: args.scan_prologues, entry_points, - ..Default::default() // todo: entry_points, jump_tables? }); state.generate(args.trace, &args.out) diff --git a/translate.sh b/translate.sh index ef811cc2..359b8b4e 100755 --- a/translate.sh +++ b/translate.sh @@ -72,6 +72,19 @@ elif [[ $1 == "sbaitso-read" ]]; then --entry-point 1072 ) cargo run -p tc -- "${args[@]}" +elif [[ $1 == "winpin" ]]; then + game_dir=~/projects_hobby/pinball-soccer-98/game_run/Soccer98 + args=( + --exe $game_dir/winpin.exe + --out out/winpin + --scan-memory --scan-immediates --scan-prologues + ) + # feedback loop: addresses discovered at runtime (see THESEUS_MISSING_ADDRS) + if [[ -f out/winpin/missing.txt ]]; then + args+=(--entry-points-file out/winpin/missing.txt) + fi + cargo run -p tc -- "${args[@]}" + cargo build --profile fast -p winpin elif [[ $1 == "sbaitso-sbtalker" ]]; then args=( --trace diff --git a/web/game b/web/game new file mode 120000 index 00000000..e206c317 --- /dev/null +++ b/web/game @@ -0,0 +1 @@ +/home/vasis/projects_hobby/pinball-soccer-98/docs/game \ No newline at end of file diff --git a/win32/winapi/src/advapi32.rs b/win32/winapi/src/advapi32.rs index 916aa6fe..4b7cd43b 100644 --- a/win32/winapi/src/advapi32.rs +++ b/win32/winapi/src/advapi32.rs @@ -11,6 +11,19 @@ pub fn RegCloseKey(_ctx: &mut Context, _hKey: HKEY) -> u32 /* WIN32_ERROR */ { stub!(0) } +#[win32_derive::dllexport] +pub fn GetUserNameA(ctx: &mut Context, lpBuffer: crate::Ptr, pcbBuffer: crate::Ptr) -> bool { + let name = b"user"; + let size = pcbBuffer.read(&ctx.memory).unwrap_or(0); + if (size as usize) < name.len() + 1 { + return false; + } + ctx.memory[lpBuffer.addr..][..name.len()].copy_from_slice(name); + ctx.memory.write::(lpBuffer.addr + name.len() as u32, 0); + pcbBuffer.write(&mut ctx.memory, name.len() as u32 + 1); + true +} + #[win32_derive::dllexport] pub fn RegCreateKeyExW( _ctx: &mut Context, diff --git a/win32/winapi/src/ddraw/ddraw.rs b/win32/winapi/src/ddraw/ddraw.rs index fdbde937..a7b9f2f7 100644 --- a/win32/winapi/src/ddraw/ddraw.rs +++ b/win32/winapi/src/ddraw/ddraw.rs @@ -178,9 +178,10 @@ impl Surface { let entries = &palette.entries; for &p in pixels { let entry = &entries[p as usize]; - buf.push(entry.peBlue); - buf.push(entry.peGreen); + // ABGR8888 layout: R,G,B,A in byte order. buf.push(entry.peRed); + buf.push(entry.peGreen); + buf.push(entry.peBlue); buf.push(0); } buf.as_slice() @@ -196,6 +197,51 @@ impl Surface { } } + /// Present this surface's own pixel buffer to the window it targets, used + /// when an app draws directly to the primary surface (via Lock or Blt) + /// instead of flipping. No-op for non-primary surfaces. + pub fn present(&mut self, mem: &mut Memory) { + let Some(addr) = self.pixels else { + return; + }; + let Target::Window(window) = &self.target else { + return; + }; + // We have no texture of our own; borrow the back buffer's. + let Some(back) = self.attached.clone() else { + return; + }; + let size = self.width * self.height * self.bytes_per_pixel; + let pixels = &mem[addr..][..size as usize]; + let mut buf = vec![]; + let pixels32: &[u8] = match self.bytes_per_pixel { + 1 => { + let Some(palette) = self.palette.as_ref() else { + return; + }; + let palette = palette.borrow(); + let entries = &palette.entries; + for &p in pixels { + let entry = &entries[p as usize]; + // ABGR8888 layout: R,G,B,A in byte order. + buf.push(entry.peRed); + buf.push(entry.peGreen); + buf.push(entry.peBlue); + buf.push(0); + } + buf.as_slice() + } + 4 => pixels, + _ => return, + }; + let mut back = back.borrow_mut(); + let Target::Texture(texture) = &mut back.target else { + return; + }; + texture.set_pixels(pixels32, self.width * 4); + window.borrow_mut().host.render(texture); + } + pub fn flip(&mut self, mem: &mut Memory) { // "Flip can be called only for a surface that has the DDSCAPS_FLIP and DDSCAPS_FRONTBUFFER capabilities." let Target::Window(window) = &self.target else { diff --git a/win32/winapi/src/ddraw/ddraw1.rs b/win32/winapi/src/ddraw/ddraw1.rs index 6d1e68a6..96a4998d 100644 --- a/win32/winapi/src/ddraw/ddraw1.rs +++ b/win32/winapi/src/ddraw/ddraw1.rs @@ -4,6 +4,7 @@ use runtime::Context; use zerocopy::{FromBytes, IntoBytes}; use crate::{ + RECT, ddraw::{DD, Palette, get_pixel_format, state, types::*}, heap::Heap, kernel32, stub, @@ -121,8 +122,66 @@ pub mod IDirectDraw { } #[win32_derive::dllexport] - pub fn EnumDisplayModes(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn EnumDisplayModes( + ctx: &mut Context, + _this: u32, + _dwFlags: u32, + lpSurfaceDesc: u32, + lpContext: u32, + lpEnumCallback: u32, + ) -> DD { + if lpSurfaceDesc != 0 { + todo!("EnumDisplayModes with a filter desc"); + } + + // Report the standard display modes; games match these against their + // internal mode tables by width/height/bit count. + const RESOLUTIONS: &[(u32, u32)] = &[(640, 480), (800, 600), (1024, 768)]; + const BIT_DEPTHS: &[u32] = &[8, 16, 24]; + + for &(width, height) in RESOLUTIONS { + for &bpp in BIT_DEPTHS { + let mut desc = DDSURFACEDESC::default(); + desc.dwSize = std::mem::size_of::() as u32; + desc.dwFlags = DDSD::WIDTH | DDSD::HEIGHT | DDSD::PIXELFORMAT | DDSD::PITCH; + desc.dwWidth = width; + desc.dwHeight = height; + desc.lPitch_dwLinearSize = width * bpp.div_ceil(8); + + // DDPF_RGB = 0x40, DDPF_PALETTEINDEXED8 = 0x20. + let (flags, r, g, b) = match bpp { + 8 => (0x40 | 0x20, 0, 0, 0), + 16 => (0x40, 0xF800, 0x07E0, 0x001F), // 5-6-5 + _ => (0x40, 0xFF0000, 0x00FF00, 0x0000FF), // 24/32 + }; + desc.ddpfPixelFormat = DDPIXELFORMAT { + dwSize: std::mem::size_of::() as u32, + dwFlags: flags, + dwFourCC: 0, + dwRGBBitCount: bpp, + dwRBitMask: r, + dwGBitMask: g, + dwBBitMask: b, + dwRGBAlphaBitMask: 0, + }; + + let desc_addr = kernel32::lock() + .process_heap + .alloc(&mut ctx.memory, desc.dwSize); + ctx.memory.write(desc_addr, desc); + let callback = ctx.indirect(lpEnumCallback); + ctx.call32_x86(callback, vec![desc_addr, lpContext]); + let ret = ctx.cpu.regs.eax; + kernel32::lock().process_heap.free(&mut ctx.memory, desc_addr); + + // DDENUMRET_CANCEL (0) means stop enumerating. + if ret == 0 { + return DD::OK; + } + } + } + + DD::OK } #[win32_derive::dllexport] @@ -201,8 +260,8 @@ pub mod IDirectDraw { } #[win32_derive::dllexport] - pub fn WaitForVerticalBlank(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn WaitForVerticalBlank(_ctx: &mut Context, _this: u32, _dwFlags: u32, _hEvent: u32) -> DD { + DD::OK // pretend the vblank already happened } pub static mut VTABLE: u32 = 0; @@ -281,9 +340,133 @@ pub mod IDirectDrawSurface { todo!() } + fn full_rect(width: u32, height: u32) -> RECT { + RECT { + left: 0, + top: 0, + right: width as i32, + bottom: height as i32, + } + } + + fn clip_rect(rect: RECT, width: u32, height: u32) -> RECT { + rect.clip(&full_rect(width, height)) + } + + fn read_rect(ctx: &Context, addr: u32) -> Option { + if addr == 0 { + None + } else { + crate::Ptr::::new(addr).read(&ctx.memory) + } + } + + /// Copy a rect between two surfaces (which may be the same one; the copy + /// stages through a temporary buffer). + fn blit_copy( + ctx: &mut Context, + dst_ptr: u32, + dst_rect: Option, + src_ptr: u32, + src_rect: Option, + ) { + let src_rc = state().surf.borrow_mut().get(&src_ptr).unwrap().clone(); + let dst_rc = state().surf.borrow_mut().get(&dst_ptr).unwrap().clone(); + + let (rows, row_bytes, row_count, bpp) = { + let mut src = src_rc.borrow_mut(); + let addr = src.lock(&mut ctx.memory); + let bpp = src.bytes_per_pixel; + let stride = src.width * bpp; + let rect = clip_rect( + src_rect.unwrap_or_else(|| full_rect(src.width, src.height)), + src.width, + src.height, + ); + let row_bytes = ((rect.right - rect.left).max(0) as u32 * bpp) as usize; + let row_count = (rect.bottom - rect.top).max(0) as usize; + let mut rows = Vec::with_capacity(row_bytes * row_count); + for y in rect.top..rect.bottom { + let start = addr + y as u32 * stride + rect.left as u32 * bpp; + rows.extend_from_slice(&ctx.memory[start..][..row_bytes]); + } + (rows, row_bytes, row_count, bpp) + }; + + let mut dst = dst_rc.borrow_mut(); + if dst.bytes_per_pixel != bpp { + log::warn!("blit between different pixel formats"); + return; + } + let addr = dst.lock(&mut ctx.memory); + let stride = dst.width * bpp; + let rect = clip_rect( + dst_rect.unwrap_or_else(|| full_rect(dst.width, dst.height)), + dst.width, + dst.height, + ); + // No stretching: copy 1:1, clipped to both rects. + let copy_bytes = row_bytes.min(((rect.right - rect.left).max(0) as u32 * bpp) as usize); + let copy_rows = row_count.min((rect.bottom - rect.top).max(0) as usize); + for i in 0..copy_rows { + let dst_start = addr + (rect.top + i as i32) as u32 * stride + rect.left as u32 * bpp; + ctx.memory[dst_start..][..copy_bytes] + .copy_from_slice(&rows[i * row_bytes..][..copy_bytes]); + } + dst.present(&mut ctx.memory); + } + #[win32_derive::dllexport] - pub fn Blt(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn Blt( + ctx: &mut Context, + this: u32, + lpDstRect: u32, + lpDDSrcSurface: u32, + lpSrcRect: u32, + dwFlags: u32, + lpDDBLTFX: u32, + ) -> DD { + const DDBLT_COLORFILL: u32 = 0x0400; + const DDBLT_WAIT: u32 = 0x0100_0000; + const KNOWN: u32 = DDBLT_COLORFILL | DDBLT_WAIT; + if dwFlags & !KNOWN != 0 { + log::warn!("Blt: ignoring flags {:#x}", dwFlags & !KNOWN); + } + + let dst_rect = read_rect(ctx, lpDstRect); + if dwFlags & DDBLT_COLORFILL != 0 { + // DDBLTFX.dwFillColor is at offset 80. + let color = ctx.memory.read::(lpDDBLTFX + 80); + let dst_rc = state().surf.borrow_mut().get(&this).unwrap().clone(); + let mut dst = dst_rc.borrow_mut(); + let bpp = dst.bytes_per_pixel; + let rect = clip_rect( + dst_rect.unwrap_or_else(|| full_rect(dst.width, dst.height)), + dst.width, + dst.height, + ); + let addr = dst.lock(&mut ctx.memory); + let stride = dst.width * bpp; + for y in rect.top..rect.bottom { + let start = addr + y as u32 * stride + rect.left as u32 * bpp; + let width_bytes = ((rect.right - rect.left).max(0) as u32 * bpp) as usize; + match bpp { + 1 => ctx.memory[start..][..width_bytes].fill(color as u8), + 4 => { + for x in 0..(rect.right - rect.left).max(0) as u32 { + ctx.memory.write::(start + x * 4, color); + } + } + _ => todo!("Blt colorfill bpp {bpp}"), + } + } + dst.present(&mut ctx.memory); + return DD::OK; + } + + let src_rect = read_rect(ctx, lpSrcRect); + blit_copy(ctx, this, dst_rect, lpDDSrcSurface, src_rect); + DD::OK } #[win32_derive::dllexport] @@ -292,8 +475,36 @@ pub mod IDirectDrawSurface { } #[win32_derive::dllexport] - pub fn BltFast(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn BltFast( + ctx: &mut Context, + this: u32, + dwX: u32, + dwY: u32, + lpDDSrcSurface: u32, + lpSrcRect: u32, + dwTrans: u32, + ) -> DD { + if dwTrans & !0x10 != 0 { + // e.g. DDBLTFAST_SRCCOLORKEY; transparency not implemented yet. + log::warn!("BltFast: ignoring flags {dwTrans:#x}"); + } + let src_rect = read_rect(ctx, lpSrcRect); + let (w, h) = match &src_rect { + Some(r) => ((r.right - r.left).max(0), (r.bottom - r.top).max(0)), + None => { + let src = state().surf.borrow_mut().get(&lpDDSrcSurface).unwrap().clone(); + let src = src.borrow(); + (src.width as i32, src.height as i32) + } + }; + let dst_rect = RECT { + left: dwX as i32, + top: dwY as i32, + right: dwX as i32 + w, + bottom: dwY as i32 + h, + }; + blit_copy(ctx, this, Some(dst_rect), lpDDSrcSurface, src_rect); + DD::OK } #[win32_derive::dllexport] @@ -346,8 +557,10 @@ pub mod IDirectDrawSurface { } #[win32_derive::dllexport] - pub fn GetCaps(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn GetCaps(ctx: &mut Context, _this: u32, lpDDSCaps: u32) -> DD { + let caps = DDSCAPS::BACKBUFFER | DDSCAPS::COMPLEX | DDSCAPS::FLIP | DDSCAPS::VIDEOMEMORY; + ctx.memory.write::(lpDDSCaps, caps.bits()); + DD::OK } #[win32_derive::dllexport] @@ -387,8 +600,38 @@ pub mod IDirectDrawSurface { } #[win32_derive::dllexport] - pub fn GetSurfaceDesc(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn GetSurfaceDesc(ctx: &mut Context, this: u32, lpDDSurfaceDesc: u32) -> DD { + let desc = { + let surfaces = state().surf.borrow_mut(); + let surface = surfaces.get(&this).unwrap().borrow(); + let bpp = surface.bytes_per_pixel * 8; + let pixel_format = if bpp == 8 { + DDPIXELFORMAT { + dwSize: std::mem::size_of::() as u32, + dwFlags: 0x40 | 0x20, // DDPF_RGB | DDPF_PALETTEINDEXED8 + dwFourCC: 0, + dwRGBBitCount: 8, + dwRBitMask: 0, + dwGBitMask: 0, + dwBBitMask: 0, + dwRGBAlphaBitMask: 0, + } + } else { + get_pixel_format() + }; + DDSURFACEDESC { + dwSize: std::mem::size_of::() as u32, + dwFlags: DDSD::WIDTH | DDSD::HEIGHT | DDSD::PITCH | DDSD::PIXELFORMAT, + dwWidth: surface.width, + dwHeight: surface.height, + lPitch_dwLinearSize: surface.width * surface.bytes_per_pixel, + ddpfPixelFormat: pixel_format, + ..DDSURFACEDESC::default() + } + }; + desc.write_to_prefix(&mut ctx.memory[lpDDSurfaceDesc..]) + .unwrap(); + DD::OK } #[win32_derive::dllexport] @@ -398,7 +641,7 @@ pub mod IDirectDrawSurface { #[win32_derive::dllexport] pub fn IsLost(_ctx: &mut Context, _this: u32) -> DD { - todo!() + DD::OK // our surfaces are never lost } #[win32_derive::dllexport] @@ -466,6 +709,8 @@ pub mod IDirectDrawSurface { let surfaces = state().surf.borrow_mut(); let mut surface = surfaces.get(&this).unwrap().borrow_mut(); surface.unlock(&mut ctx.memory); + // An app drawing straight to the primary surface expects it on screen. + surface.present(&mut ctx.memory); DD::OK } @@ -537,8 +782,28 @@ pub mod IDirectDrawPalette { } #[win32_derive::dllexport] - pub fn SetEntries(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn SetEntries( + ctx: &mut Context, + this: u32, + _dwFlags: u32, + dwStartingEntry: u32, + dwCount: u32, + lpEntries: u32, + ) -> DD { + let new_entries = + <[PALETTEENTRY]>::ref_from_prefix_with_elems(&ctx.memory[lpEntries..], dwCount as usize) + .unwrap() + .0 + .to_vec(); + let palettes = state().palette.borrow_mut(); + let mut palette = palettes.get(&this).unwrap().borrow_mut(); + for (i, entry) in new_entries.into_iter().enumerate() { + let index = dwStartingEntry as usize + i; + if index < palette.entries.len() { + palette.entries[index] = entry; + } + } + DD::OK } pub static mut VTABLE: u32 = 0; diff --git a/win32/winapi/src/dinput.rs b/win32/winapi/src/dinput.rs new file mode 100644 index 00000000..824e6a73 --- /dev/null +++ b/win32/winapi/src/dinput.rs @@ -0,0 +1,369 @@ +//! DirectInput. Devices exist and can be acquired; GetDeviceState/GetDeviceData +//! report no input until the host grows keyboard support (see the retrowin32 +//! fork's dinput for the full reference). + +use std::{cell::OnceCell, collections::HashMap, sync::Mutex}; + +use runtime::Context; + +use crate::{ddraw::GUID, heap::Heap, kernel32}; + +const GUID_SysMouse: GUID = GUID(( + 0x6F1D2B60, + 0xD5A0, + 0x11CF, + [0xBF, 0xC7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00], +)); + +const GUID_SysKeyboard: GUID = GUID(( + 0x6F1D2B61, + 0xD5A0, + 0x11CF, + [0xBF, 0xC7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00], +)); + +const DI_OK: u32 = 0; +const DIERR_DEVICENOTREG: u32 = 0x80040154; + +/// Which physical device a created IDirectInputDevice stands for. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum DeviceKind { + Keyboard, + Mouse, +} + +#[derive(Default)] +pub struct State { + /// Maps an IDirectInputDevice interface pointer to the device it represents. + pub devices: Mutex>, +} + +struct StaticState(OnceCell); +unsafe impl Sync for StaticState {} +static STATE: StaticState = StaticState(OnceCell::new()); + +pub fn state() -> &'static State { + STATE.0.get_or_init(Default::default) +} + +pub const VTABLES: [(&'static str, &[&str]); 2] = [ + ("IDirectInput", IDirectInput::VTABLE_ENTRIES.as_slice()), + ( + "IDirectInputDevice", + IDirectInputDevice::VTABLE_ENTRIES.as_slice(), + ), +]; + +#[win32_derive::dllexport] +pub fn DirectInputCreateA( + ctx: &mut Context, + _hinst: u32, + _dwVersion: u32, + ppDI: u32, + _punkOuter: u32, +) -> u32 { + let mut kernel32 = kernel32::lock(); + let ptr = IDirectInput::new(ctx, &mut kernel32.process_heap); + drop(kernel32); + ctx.memory.write::(ppDI, ptr); + DI_OK +} + +pub mod IDirectInput { + use super::*; + + pub const VTABLE_ENTRIES: [&str; 8] = [ + "QueryInterface", + "AddRef", + "Release", + "CreateDevice", + "EnumDevices", + "GetDeviceStatus", + "RunControlPanel", + "Initialize", + ]; + + pub static mut VTABLE: u32 = 0; + + pub fn new(ctx: &mut Context, heap: &mut Heap) -> u32 { + let addr = heap.alloc(&mut ctx.memory, 4); + ctx.memory.write(addr, unsafe { VTABLE }); + addr + } + + #[win32_derive::dllexport] + pub fn QueryInterface(_ctx: &mut Context, _this: u32, _riid: u32, _ppv: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn AddRef(_ctx: &mut Context, _this: u32) -> u32 { + 1 + } + + #[win32_derive::dllexport] + pub fn Release(_ctx: &mut Context, _this: u32) -> u32 { + 0 + } + + #[win32_derive::dllexport] + pub fn CreateDevice( + ctx: &mut Context, + _this: u32, + lpGUID: u32, + lplpDirectInputDevice: u32, + _pUnkOuter: u32, + ) -> u32 { + let guid = crate::Ptr::::new(lpGUID).read(&ctx.memory).unwrap(); + let kind = if guid == GUID_SysKeyboard { + DeviceKind::Keyboard + } else if guid == GUID_SysMouse { + DeviceKind::Mouse + } else { + log::warn!("CreateDevice: unknown GUID {guid:?}"); + return DIERR_DEVICENOTREG; + }; + let mut kernel32 = kernel32::lock(); + let device = IDirectInputDevice::new(ctx, &mut kernel32.process_heap); + drop(kernel32); + state().devices.lock().unwrap().insert(device, kind); + ctx.memory.write::(lplpDirectInputDevice, device); + DI_OK + } + + #[win32_derive::dllexport] + pub fn EnumDevices( + _ctx: &mut Context, + _this: u32, + _dwDevType: u32, + _callback: u32, + _pvRef: u32, + _dwFlags: u32, + ) -> u32 { + DI_OK + } + + #[win32_derive::dllexport] + pub fn GetDeviceStatus(_ctx: &mut Context, _this: u32, _rguid: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn RunControlPanel(_ctx: &mut Context, _this: u32, _hwnd: u32, _dwFlags: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn Initialize(_ctx: &mut Context, _this: u32, _hinst: u32, _dwVersion: u32) -> u32 { + DI_OK + } +} + +pub mod IDirectInputDevice { + use super::*; + + // IDirectInputDevice2 layout; the game may call Poll() before reading state. + pub const VTABLE_ENTRIES: [&str; 27] = [ + "QueryInterface", + "AddRef", + "Release", + "GetCapabilities", + "EnumObjects", + "GetProperty", + "SetProperty", + "Acquire", + "Unacquire", + "GetDeviceState", + "GetDeviceData", + "SetDataFormat", + "SetEventNotification", + "SetCooperativeLevel", + "GetObjectInfo", + "GetDeviceInfo", + "RunControlPanel", + "Initialize", + "CreateEffect", + "EnumEffects", + "GetEffectInfo", + "GetForceFeedbackState", + "SendForceFeedbackCommand", + "EnumCreatedEffectObjects", + "Escape", + "Poll", + "SendDeviceData", + ]; + + pub static mut VTABLE: u32 = 0; + + pub fn new(ctx: &mut Context, heap: &mut Heap) -> u32 { + let addr = heap.alloc(&mut ctx.memory, 4); + ctx.memory.write(addr, unsafe { VTABLE }); + addr + } + + pub fn device_kind(this: u32) -> DeviceKind { + state() + .devices + .lock() + .unwrap() + .get(&this) + .copied() + .unwrap_or(DeviceKind::Keyboard) + } + + #[win32_derive::dllexport] + pub fn QueryInterface(_ctx: &mut Context, _this: u32, _riid: u32, _ppv: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn AddRef(_ctx: &mut Context, _this: u32) -> u32 { + 1 + } + + #[win32_derive::dllexport] + pub fn Release(_ctx: &mut Context, _this: u32) -> u32 { + state().devices.lock().unwrap().remove(&_this); + 0 + } + + #[win32_derive::dllexport] + pub fn GetCapabilities(_ctx: &mut Context, _this: u32, _lpCaps: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn EnumObjects( + _ctx: &mut Context, + _this: u32, + _lpCallback: u32, + _pvRef: u32, + _dwFlags: u32, + ) -> u32 { + DI_OK + } + + #[win32_derive::dllexport] + pub fn GetProperty(_ctx: &mut Context, _this: u32, _rguidProp: u32, _pdiph: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn SetProperty(_ctx: &mut Context, _this: u32, _rguidProp: u32, _pdiph: u32) -> u32 { + DI_OK + } + + #[win32_derive::dllexport] + pub fn Acquire(_ctx: &mut Context, _this: u32) -> u32 { + DI_OK + } + + #[win32_derive::dllexport] + pub fn Unacquire(_ctx: &mut Context, _this: u32) -> u32 { + DI_OK + } + + /// Keyboard: a byte array indexed by DIK scancode (0x80 = pressed). + /// Mouse: DIMOUSESTATE (lX/lY/lZ relative, then button bytes). + /// No host input plumbing yet, so everything reads as idle. + #[win32_derive::dllexport] + pub fn GetDeviceState(ctx: &mut Context, _this: u32, cbData: u32, lpvData: u32) -> u32 { + ctx.memory[lpvData..][..cbData as usize].fill(0); + DI_OK + } + + #[win32_derive::dllexport] + pub fn GetDeviceData( + ctx: &mut Context, + _this: u32, + _cbObjectData: u32, + _rgdod: u32, + pdwInOut: u32, + _dwFlags: u32, + ) -> u32 { + if pdwInOut != 0 { + ctx.memory.write::(pdwInOut, 0); // no buffered events + } + DI_OK + } + + #[win32_derive::dllexport] + pub fn SetDataFormat(_ctx: &mut Context, _this: u32, _lpdf: u32) -> u32 { + DI_OK + } + + #[win32_derive::dllexport] + pub fn SetEventNotification(_ctx: &mut Context, _this: u32, _hEvent: u32) -> u32 { + DI_OK + } + + #[win32_derive::dllexport] + pub fn SetCooperativeLevel(_ctx: &mut Context, _this: u32, _hwnd: u32, _dwFlags: u32) -> u32 { + DI_OK + } + + #[win32_derive::dllexport] + pub fn GetObjectInfo(_ctx: &mut Context, _this: u32, _pdidoi: u32, _dwObj: u32, _dwHow: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn GetDeviceInfo(_ctx: &mut Context, _this: u32, _pdidi: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn RunControlPanel(_ctx: &mut Context, _this: u32, _hwnd: u32, _dwFlags: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn Initialize(_ctx: &mut Context, _this: u32, _hinst: u32, _dwVersion: u32, _rguid: u32) -> u32 { + DI_OK + } + + #[win32_derive::dllexport] + pub fn CreateEffect(_ctx: &mut Context, _this: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn EnumEffects(_ctx: &mut Context, _this: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn GetEffectInfo(_ctx: &mut Context, _this: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn GetForceFeedbackState(_ctx: &mut Context, _this: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn SendForceFeedbackCommand(_ctx: &mut Context, _this: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn EnumCreatedEffectObjects(_ctx: &mut Context, _this: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn Escape(_ctx: &mut Context, _this: u32) -> u32 { + todo!() + } + + #[win32_derive::dllexport] + pub fn Poll(_ctx: &mut Context, _this: u32) -> u32 { + DI_OK + } + + #[win32_derive::dllexport] + pub fn SendDeviceData(_ctx: &mut Context, _this: u32) -> u32 { + todo!() + } +} diff --git a/win32/winapi/src/gdi32/misc.rs b/win32/winapi/src/gdi32/misc.rs index 0c92c2e9..862c8a24 100644 --- a/win32/winapi/src/gdi32/misc.rs +++ b/win32/winapi/src/gdi32/misc.rs @@ -9,6 +9,26 @@ pub fn DeleteObject(_ctx: &mut Context, _ho: HGDIOBJ) -> bool { stub!(true) } +#[win32_derive::dllexport] +pub fn GetSystemPaletteEntries( + ctx: &mut Context, + _hdc: HDC, + iStart: u32, + cEntries: u32, + pPalEntries: crate::Ptr, +) -> u32 { + // PALETTEENTRY { peRed, peGreen, peBlue, peFlags }: report a gray ramp. + let mut addr = pPalEntries.addr; + for i in iStart..iStart + cEntries { + let level = (i & 0xff) as u8; + for value in [level, level, level, 0] { + ctx.memory.write::(addr, value); + addr += 1; + } + } + cEntries +} + #[derive(Debug, win32_derive::ABIEnum)] pub enum GetDeviceCapsArg { DRIVERVERSION = 0, diff --git a/win32/winapi/src/kernel32/env.rs b/win32/winapi/src/kernel32/env.rs index beeef910..0bc87842 100644 --- a/win32/winapi/src/kernel32/env.rs +++ b/win32/winapi/src/kernel32/env.rs @@ -18,15 +18,17 @@ pub fn GetEnvironmentStrings(_ctx: &mut Context) -> u32 { encode_env(&mut encoder, &state.env); encoder.status().unwrap(); */ - // TODO: if available, this ends up hitting a jmp table when parsing - // stub!(state().environ.get()) - stub!(0) + // An empty environment block: a list of nul-terminated strings, + // terminated by an extra nul. + let mut kernel32 = lock(); + let addr = kernel32.process_heap.alloc(&mut _ctx.memory, 2); + _ctx.memory[addr..][..2].fill(0); + addr } #[win32_derive::dllexport] pub fn GetEnvironmentStringsW(_ctx: &mut Context) -> u32 { - // TODO: if available, this ends up getting parsed with unimplemented nls functions - // stub!(state().environ.get()) + // Returning 0 pushes the CRT towards the ANSI fallback (GetEnvironmentStrings). stub!(0) } diff --git a/win32/winapi/src/kernel32/event.rs b/win32/winapi/src/kernel32/event.rs index 7a074812..f085ffb9 100644 --- a/win32/winapi/src/kernel32/event.rs +++ b/win32/winapi/src/kernel32/event.rs @@ -7,6 +7,9 @@ use crate::{HANDLE, Ptr, kernel32::lock}; pub enum Object { Thread, Event(Arc), + Mutex, + File(std::fs::File), + FindHandle(crate::kernel32::FindHandle), } pub struct Event { @@ -63,6 +66,33 @@ pub fn CreateEventA( kernel32.objects.add(Object::Event(Arc::new(event))) } +#[win32_derive::dllexport] +pub fn CreateMutexA( + _ctx: &mut Context, + _lpMutexAttributes: Ptr<()>, + _bInitialOwner: bool, + _lpName: Ptr, +) -> HANDLE { + // Single x86 thread; no contention possible. + lock().objects.add(Object::Mutex) +} + +#[win32_derive::dllexport] +pub fn ReleaseMutex(_ctx: &mut Context, _hMutex: HANDLE) -> bool { + true +} + +#[win32_derive::dllexport] +pub fn WaitForMultipleObjects( + _ctx: &mut Context, + _nCount: u32, + _lpHandles: Ptr, + _bWaitAll: bool, + _dwMilliseconds: u32, +) -> u32 /* WAIT_EVENT */ { + crate::stub!(0) // WAIT_OBJECT_0 +} + #[win32_derive::dllexport] pub fn SetEvent(_ctx: &mut Context, hEvent: HANDLE) -> bool { let kernel32 = lock(); diff --git a/win32/winapi/src/kernel32/file.rs b/win32/winapi/src/kernel32/file.rs index 80e0b7b4..8acb6070 100644 --- a/win32/winapi/src/kernel32/file.rs +++ b/win32/winapi/src/kernel32/file.rs @@ -1,6 +1,11 @@ +use std::io::{Read, Seek, SeekFrom, Write}; + use runtime::Context; -use crate::Ptr; +use crate::{ + Ptr, + kernel32::{Object, lock}, +}; pub type HANDLE = u32; @@ -8,6 +13,63 @@ const STDIN_HFILE: HANDLE = 0xF11E_0001; const STDOUT_HFILE: HANDLE = 0xF11E_0002; const STDERR_HFILE: HANDLE = 0xF11E_0003; +const INVALID_SET_FILE_POINTER: u32 = 0xFFFF_FFFF; + +const GENERIC_READ: u32 = 0x8000_0000; +const GENERIC_WRITE: u32 = 0x4000_0000; + +const FILE_ATTRIBUTE_NORMAL: u32 = 0x80; +const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10; + +/// The host directory that maps to the root of the C: drive: the process's +/// initial current directory. Captured on first use, before any chdir. +fn initial_cwd() -> &'static std::path::Path { + static CWD: std::sync::OnceLock = std::sync::OnceLock::new(); + CWD.get_or_init(|| std::env::current_dir().unwrap_or_else(|_| ".".into())) +} + +/// Resolve a Windows-style path against the host filesystem, ignoring case +/// (game data files and the paths that reference them often disagree on it). +pub fn resolve_path(path: &str) -> std::path::PathBuf { + let path = path.replace('\\', "/"); + // Strip any drive prefix; the initial cwd acts as the root of C:. + let path = if path.len() >= 2 && path.as_bytes()[1] == b':' { + &path[2..] + } else { + &path[..] + }; + let mut result = if path.starts_with('/') { + initial_cwd().to_path_buf() + } else { + // Absolute base so that ".." components resolve properly. + std::env::current_dir().unwrap_or_else(|_| ".".into()) + }; + 'component: for comp in path.split('/') { + if comp.is_empty() || comp == "." { + continue; + } + if comp == ".." { + result.pop(); + continue; + } + let direct = result.join(comp); + if direct.exists() { + result = direct; + continue; + } + if let Ok(entries) = std::fs::read_dir(&result) { + for entry in entries.flatten() { + if entry.file_name().to_string_lossy().eq_ignore_ascii_case(comp) { + result = entry.path(); + continue 'component; + } + } + } + result = direct; // may not exist, e.g. a file about to be created + } + result +} + #[win32_derive::dllexport] pub fn GetStdHandle(_ctx: &mut Context, nStdHandle: u32) -> u32 { match nStdHandle as i32 { @@ -21,6 +83,92 @@ pub fn GetStdHandle(_ctx: &mut Context, nStdHandle: u32) -> u32 { } } +#[win32_derive::dllexport] +pub fn SetStdHandle(_ctx: &mut Context, _nStdHandle: u32, _hHandle: u32) -> u32 { + crate::stub!(1) +} + +#[win32_derive::dllexport] +pub fn CreateFileA( + ctx: &mut Context, + lpFileName: Ptr, + dwDesiredAccess: u32, + _dwShareMode: u32, + _lpSecurityAttributes: Ptr<()>, + dwCreationDisposition: u32, + _dwFlagsAndAttributes: u32, + _hTemplateFile: u32, +) -> crate::HANDLE { + let name = ctx.memory.read_str(lpFileName.addr).to_owned(); + let path = resolve_path(&name); + let write = dwDesiredAccess & GENERIC_WRITE != 0; + let read = dwDesiredAccess & GENERIC_READ != 0; + let mut opts = std::fs::OpenOptions::new(); + opts.read(read || !write); + if write { + opts.write(true); + } + match dwCreationDisposition { + 1 => { + opts.create_new(true); // CREATE_NEW + } + 2 => { + opts.create(true).truncate(true); // CREATE_ALWAYS + } + 3 => {} // OPEN_EXISTING + 4 => { + opts.create(true); // OPEN_ALWAYS + } + 5 => { + opts.truncate(true); // TRUNCATE_EXISTING + } + disp => log::warn!("CreateFileA({name:?}): unimplemented disposition {disp}"), + } + match opts.open(&path) { + Ok(file) => lock().objects.add(Object::File(file)), + Err(err) => { + log::warn!("CreateFileA({name:?} => {path:?}): {err}"); + crate::HANDLE::invalid() + } + } +} + +#[win32_derive::dllexport] +pub fn ReadFile( + ctx: &mut Context, + hFile: crate::HANDLE, + lpBuffer: Ptr, + nNumberOfBytesToRead: u32, + lpNumberOfBytesRead: Ptr, + lpOverlapped: Ptr<()>, +) -> bool { + assert_eq!(lpOverlapped.addr, 0); + let mut kernel32 = lock(); + let Some(Object::File(file)) = kernel32.objects.get_mut(hFile) else { + log::warn!("ReadFile({hFile:?}): unknown handle"); + return false; + }; + let buf = &mut ctx.memory[lpBuffer.addr..][..nNumberOfBytesToRead as usize]; + let mut total = 0; + while total < buf.len() { + match file.read(&mut buf[total..]) { + Ok(0) => break, + Ok(n) => total += n, + Err(err) => { + log::warn!("ReadFile({hFile:?}): {err}"); + return false; + } + } + } + drop(kernel32); + if lpNumberOfBytesRead.addr != 0 { + lpNumberOfBytesRead + .write(&mut ctx.memory, total as u32) + .unwrap(); + } + true +} + #[win32_derive::dllexport] pub fn WriteFile( ctx: &mut Context, @@ -31,7 +179,7 @@ pub fn WriteFile( lpOverlapped: Ptr<()>, ) -> u32 { assert_eq!(lpOverlapped.addr, 0); - if hFile == 0xf11e_0002 || hFile == 0xf11e_0003 { + if hFile == STDOUT_HFILE || hFile == STDERR_HFILE { let buf = &ctx.memory[lpBuffer.addr..][..nNumberOfBytesToWrite as usize]; host::host().console_write(buf); if lpNumberOfBytesWritten.addr != 0 { @@ -39,25 +187,124 @@ pub fn WriteFile( .write(&mut ctx.memory, nNumberOfBytesToWrite) .unwrap(); } + return 1; + } + + let mut kernel32 = lock(); + let Some(Object::File(file)) = kernel32.objects.get_mut(crate::HANDLE::from_raw(hFile)) else { + log::warn!("WriteFile({hFile:x}): unknown handle"); + return 0; + }; + let buf = &ctx.memory[lpBuffer.addr..][..nNumberOfBytesToWrite as usize]; + match file.write_all(buf) { + Ok(()) => { + drop(kernel32); + if lpNumberOfBytesWritten.addr != 0 { + lpNumberOfBytesWritten + .write(&mut ctx.memory, nNumberOfBytesToWrite) + .unwrap(); + } + 1 + } + Err(err) => { + log::warn!("WriteFile({hFile:x}): {err}"); + 0 + } + } +} + +#[win32_derive::dllexport] +pub fn SetFilePointer( + ctx: &mut Context, + hFile: crate::HANDLE, + lDistanceToMove: i32, + lpDistanceToMoveHigh: Ptr, + dwMoveMethod: u32, +) -> u32 { + let distance = if lpDistanceToMoveHigh.addr != 0 { + let high = lpDistanceToMoveHigh.read(&ctx.memory).unwrap(); + ((high as i64) << 32) | (lDistanceToMove as u32 as i64) } else { - todo!("WriteFile(hFile={hFile:x})"); + lDistanceToMove as i64 + }; + let from = match dwMoveMethod { + 0 => SeekFrom::Start(distance as u64), // FILE_BEGIN + 1 => SeekFrom::Current(distance), // FILE_CURRENT + 2 => SeekFrom::End(distance), // FILE_END + _ => { + log::warn!("SetFilePointer: bad method {dwMoveMethod}"); + return INVALID_SET_FILE_POINTER; + } + }; + let mut kernel32 = lock(); + let Some(Object::File(file)) = kernel32.objects.get_mut(hFile) else { + log::warn!("SetFilePointer({hFile:?}): unknown handle"); + return INVALID_SET_FILE_POINTER; + }; + match file.seek(from) { + Ok(pos) => { + drop(kernel32); + if lpDistanceToMoveHigh.addr != 0 { + lpDistanceToMoveHigh + .write(&mut ctx.memory, (pos >> 32) as i32) + .unwrap(); + } + pos as u32 + } + Err(err) => { + log::warn!("SetFilePointer({hFile:?}): {err}"); + INVALID_SET_FILE_POINTER + } } - return 1; } #[win32_derive::dllexport] -pub fn GetFileType(_ctx: &mut Context, hFile: HANDLE) -> u32 /* FILE_TYPE */ { +pub fn SetEndOfFile(_ctx: &mut Context, hFile: crate::HANDLE) -> bool { + let mut kernel32 = lock(); + let Some(Object::File(file)) = kernel32.objects.get_mut(hFile) else { + log::warn!("SetEndOfFile({hFile:?}): unknown handle"); + return false; + }; + let Ok(pos) = file.stream_position() else { + return false; + }; + file.set_len(pos).is_ok() +} + +#[win32_derive::dllexport] +pub fn FlushFileBuffers(_ctx: &mut Context, hFile: crate::HANDLE) -> bool { + let mut kernel32 = lock(); + let Some(Object::File(file)) = kernel32.objects.get_mut(hFile) else { + return false; + }; + file.sync_all().is_ok() +} + +#[win32_derive::dllexport] +pub fn CloseHandle(_ctx: &mut Context, hObject: crate::HANDLE) -> bool { + // Also called for handles we don't track (stdio etc.); succeed regardless. + lock().objects.remove(hObject); + true +} + +#[win32_derive::dllexport] +pub fn DeleteFileA(ctx: &mut Context, lpFileName: Ptr) -> bool { + let name = ctx.memory.read_str(lpFileName.addr).to_owned(); + std::fs::remove_file(resolve_path(&name)).is_ok() +} + +#[win32_derive::dllexport] +pub fn GetFileType(_ctx: &mut Context, hFile: u32) -> u32 /* FILE_TYPE */ { + let FILE_TYPE_DISK = 0x1; let FILE_TYPE_CHAR = 0x2; let FILE_TYPE_UNKNOWN = 0x8; match hFile { STDIN_HFILE | STDOUT_HFILE | STDERR_HFILE => return FILE_TYPE_CHAR, _ => {} } - /* - if get_state(sys).files.get(hFile).is_some() { - return FILE_TYPE_CHAR; + if let Some(Object::File(_)) = lock().objects.get(crate::HANDLE::from_raw(hFile)) { + return FILE_TYPE_DISK; } - */ log::error!("GetFileType({hFile:?}) unknown handle"); FILE_TYPE_UNKNOWN @@ -68,3 +315,172 @@ pub fn SetHandleCount(_ctx: &mut Context, uNumber: u32) -> u32 { // "For Windows Win32 systems, this API has no effect." uNumber } + +#[win32_derive::dllexport] +pub fn GetCurrentDirectoryA(ctx: &mut Context, nBufferLength: u32, lpBuffer: Ptr) -> u32 { + let cur = std::env::current_dir().unwrap_or_default(); + let rel = cur + .strip_prefix(initial_cwd()) + .unwrap_or(std::path::Path::new("")) + .to_string_lossy() + .replace('/', "\\"); + let path = if rel.is_empty() { + "C:\\".to_string() + } else { + format!("C:\\{rel}") + }; + let bytes = path.as_bytes(); + if (nBufferLength as usize) < bytes.len() + 1 { + return bytes.len() as u32 + 1; + } + ctx.memory[lpBuffer.addr..][..bytes.len()].copy_from_slice(bytes); + ctx.memory.write::(lpBuffer.addr + bytes.len() as u32, 0); + bytes.len() as u32 +} + +#[win32_derive::dllexport] +pub fn SetCurrentDirectoryA(ctx: &mut Context, lpPathName: Ptr) -> bool { + let name = ctx.memory.read_str(lpPathName.addr).to_owned(); + let path = resolve_path(&name); + match std::env::set_current_dir(&path) { + Ok(()) => true, + Err(err) => { + log::warn!("SetCurrentDirectoryA({name:?} => {path:?}): {err}"); + false + } + } +} + +/// State of an in-progress FindFirstFile/FindNextFile iteration. +pub struct FindHandle { + pub entries: Vec, + pub index: usize, +} + +pub struct FindEntry { + pub name: String, + pub size: u64, + pub is_dir: bool, +} + +#[repr(C)] +#[derive(zerocopy::IntoBytes, zerocopy::Immutable)] +pub struct WIN32_FIND_DATAA { + dwFileAttributes: u32, + ftCreationTime: [u32; 2], + ftLastAccessTime: [u32; 2], + ftLastWriteTime: [u32; 2], + nFileSizeHigh: u32, + nFileSizeLow: u32, + dwReserved0: u32, + dwReserved1: u32, + cFileName: [u8; 260], + cAlternateFileName: [u8; 14], + _pad: [u8; 2], +} + +fn find_data(entry: &FindEntry) -> WIN32_FIND_DATAA { + let mut cFileName = [0u8; 260]; + let name = entry.name.as_bytes(); + let len = name.len().min(259); + cFileName[..len].copy_from_slice(&name[..len]); + WIN32_FIND_DATAA { + dwFileAttributes: if entry.is_dir { + FILE_ATTRIBUTE_DIRECTORY + } else { + FILE_ATTRIBUTE_NORMAL + }, + ftCreationTime: [0; 2], + ftLastAccessTime: [0; 2], + ftLastWriteTime: [0; 2], + nFileSizeHigh: (entry.size >> 32) as u32, + nFileSizeLow: entry.size as u32, + dwReserved0: 0, + dwReserved1: 0, + cFileName, + cAlternateFileName: [0; 14], + _pad: [0; 2], + } +} + +/// Match a DOS wildcard pattern (* and ?) against a name, ignoring case. +fn wildcard_match(pattern: &[u8], name: &[u8]) -> bool { + match (pattern.first(), name.first()) { + (None, None) => true, + (Some(b'*'), _) => { + wildcard_match(&pattern[1..], name) + || (!name.is_empty() && wildcard_match(pattern, &name[1..])) + } + (Some(b'?'), Some(_)) => wildcard_match(&pattern[1..], &name[1..]), + (Some(&p), Some(&n)) => { + p.eq_ignore_ascii_case(&n) && wildcard_match(&pattern[1..], &name[1..]) + } + _ => false, + } +} + +#[win32_derive::dllexport] +pub fn FindFirstFileA( + ctx: &mut Context, + lpFileName: Ptr, + lpFindFileData: Ptr, +) -> crate::HANDLE { + let pattern = ctx.memory.read_str(lpFileName.addr).to_owned(); + let pattern = pattern.replace('\\', "/"); + let (dir, file_pattern) = match pattern.rfind('/') { + Some(pos) => (&pattern[..pos], &pattern[pos + 1..]), + None => (".", &pattern[..]), + }; + let dir = resolve_path(dir); + let mut entries = Vec::new(); + if let Ok(dir_entries) = std::fs::read_dir(&dir) { + for entry in dir_entries.flatten() { + let name = entry.file_name().to_string_lossy().into_owned(); + if !wildcard_match(file_pattern.as_bytes(), name.as_bytes()) { + continue; + } + let meta = entry.metadata().ok(); + entries.push(FindEntry { + name, + size: meta.as_ref().map(|m| m.len()).unwrap_or(0), + is_dir: meta.as_ref().map(|m| m.is_dir()).unwrap_or(false), + }); + } + } + entries.sort_by(|a, b| a.name.cmp(&b.name)); + if entries.is_empty() { + return crate::HANDLE::invalid(); + } + lpFindFileData + .write(&mut ctx.memory, find_data(&entries[0])) + .unwrap(); + lock() + .objects + .add(Object::FindHandle(FindHandle { entries, index: 1 })) +} + +#[win32_derive::dllexport] +pub fn FindNextFileA( + ctx: &mut Context, + hFindFile: crate::HANDLE, + lpFindFileData: Ptr, +) -> bool { + let mut kernel32 = lock(); + let Some(Object::FindHandle(find)) = kernel32.objects.get_mut(hFindFile) else { + log::warn!("FindNextFileA({hFindFile:?}): unknown handle"); + return false; + }; + let Some(entry) = find.entries.get(find.index) else { + return false; + }; + let data = find_data(entry); + find.index += 1; + drop(kernel32); + lpFindFileData.write(&mut ctx.memory, data).unwrap(); + true +} + +#[win32_derive::dllexport] +pub fn FindClose(_ctx: &mut Context, hFindFile: crate::HANDLE) -> bool { + lock().objects.remove(hFindFile).is_some() +} diff --git a/win32/winapi/src/kernel32/heap.rs b/win32/winapi/src/kernel32/heap.rs index 5f3f597e..aca97ba9 100644 --- a/win32/winapi/src/kernel32/heap.rs +++ b/win32/winapi/src/kernel32/heap.rs @@ -10,24 +10,22 @@ use crate::{ win32flags! { pub struct HEAP_FLAGS { + const NO_SERIALIZE = 0x01; + const GENERATE_EXCEPTIONS = 0x04; + const ZERO_MEMORY = 0x08; } } #[win32_derive::dllexport] pub fn HeapAlloc(ctx: &mut Context, hHeap: HANDLE, dwFlags: HEAP_FLAGS, dwBytes: u32) -> u32 { - if !dwFlags.is_empty() { - todo!(); - } - let state = kernel32::lock(); let heap = state.heaps.get(&hHeap).unwrap(); - heap.alloc(&mut ctx.memory, dwBytes) - /* - if flags.contains(HeapAllocFlags::HEAP_ZERO_MEMORY) { - memory.mem().sub32_mut(addr, dwBytes).fill(0); - flags.remove(HeapAllocFlags::HEAP_ZERO_MEMORY); + let addr = heap.alloc(&mut ctx.memory, dwBytes); + drop(state); + if addr != 0 && dwFlags.contains(HEAP_FLAGS::ZERO_MEMORY) { + ctx.memory[addr..][..dwBytes as usize].fill(0); } - */ + addr } #[win32_derive::dllexport] @@ -146,3 +144,21 @@ pub fn GlobalFree(ctx: &mut Context, hMem: Ptr<()>) -> u32 { lock().process_heap.free(&mut ctx.memory, hMem.addr); 0 // success } + +// GlobalAlloc only hands out fixed (non-moveable) memory, so handles and +// pointers are the same value. + +#[win32_derive::dllexport] +pub fn GlobalLock(_ctx: &mut Context, hMem: u32) -> u32 { + hMem +} + +#[win32_derive::dllexport] +pub fn GlobalUnlock(_ctx: &mut Context, _hMem: u32) -> bool { + true +} + +#[win32_derive::dllexport] +pub fn GlobalHandle(_ctx: &mut Context, pMem: u32) -> u32 { + pMem +} diff --git a/win32/winapi/src/kernel32/misc.rs b/win32/winapi/src/kernel32/misc.rs index a4218113..d71ae93a 100644 --- a/win32/winapi/src/kernel32/misc.rs +++ b/win32/winapi/src/kernel32/misc.rs @@ -9,6 +9,31 @@ pub fn GetLastError(_ctx: &mut Context) -> u32 { 0 } +#[win32_derive::dllexport] +pub fn GetComputerNameA(ctx: &mut Context, lpBuffer: Ptr, nSize: Ptr) -> bool { + let name = b"THESEUS"; + let size = nSize.read(&ctx.memory).unwrap_or(0); + if (size as usize) < name.len() + 1 { + return false; + } + ctx.memory[lpBuffer.addr..][..name.len()].copy_from_slice(name); + ctx.memory.write::(lpBuffer.addr + name.len() as u32, 0); + nSize.write(&mut ctx.memory, name.len() as u32); + true +} + +#[win32_derive::dllexport] +pub fn SetEnvironmentVariableA(_ctx: &mut Context, _lpName: Ptr, _lpValue: Ptr) -> bool { + stub!(true) +} + +#[win32_derive::dllexport] +pub fn ExitThread(_ctx: &mut Context, dwExitCode: u32) { + // The only x86 thread is the main one. + log::warn!("ExitThread({dwExitCode})"); + std::process::exit(dwExitCode as i32); +} + #[repr(C)] #[derive(Debug, Default, zerocopy::IntoBytes, zerocopy::Immutable)] pub struct STARTUPINFOA { @@ -95,7 +120,11 @@ pub fn VirtualAlloc( _flAllocationType: u32, /* VIRTUAL_ALLOCATION_TYPE */ _flProtect: u32, /* PAGE_PROTECTION_FLAGS */ ) -> u32 { - assert_eq!(lpAddress.addr, 0); + if lpAddress.addr != 0 { + // Committing (or re-protecting) part of an earlier reservation; all our + // memory is always committed, so just say yes. + return lpAddress.addr; + } lock().mappings.alloc("VirtualAlloc".into(), dwSize) /* let memory = sys.memory_mut(); diff --git a/win32/winapi/src/kernel32/mod.rs b/win32/winapi/src/kernel32/mod.rs index c369efda..92a0c4b1 100644 --- a/win32/winapi/src/kernel32/mod.rs +++ b/win32/winapi/src/kernel32/mod.rs @@ -8,6 +8,7 @@ mod nls; mod process; mod resource; mod state; +mod strings; mod thread; mod time; @@ -21,5 +22,6 @@ pub use nls::*; pub use process::*; pub use resource::*; pub use state::{Lock, State, init_state, lock}; +pub use strings::*; pub use thread::*; pub use time::*; diff --git a/win32/winapi/src/kernel32/nls.rs b/win32/winapi/src/kernel32/nls.rs index 31ccbedd..014c5eaf 100644 --- a/win32/winapi/src/kernel32/nls.rs +++ b/win32/winapi/src/kernel32/nls.rs @@ -8,57 +8,184 @@ pub fn GetACP(_ctx: &mut Context) -> u32 { } #[win32_derive::dllexport] -pub fn GetCPInfo(_ctx: &mut Context, _CodePage: u32, _lpCPInfo: Ptr<()>) -> bool { - stub!(false) // fail +pub fn GetCPInfo(ctx: &mut Context, _CodePage: u32, lpCPInfo: Ptr<()>) -> bool { + // CPINFO { MaxCharSize: u32, DefaultChar: [u8; 2], LeadByte: [u8; 12] } + // for a single-byte codepage. + ctx.memory.write::(lpCPInfo.addr, 1); + ctx.memory.write::(lpCPInfo.addr + 4, b'?'); + ctx.memory[lpCPInfo.addr + 5..][..13].fill(0); + true +} + +/// CT_CTYPE1 character classification of an ASCII-ish character. +fn ctype1(c: u32) -> u16 { + if c > 0xff { + return 0x100; // C1_ALPHA, close enough + } + let c = c as u8; + let mut t = 0u16; + if c.is_ascii_uppercase() { + t |= 0x1; // C1_UPPER + } + if c.is_ascii_lowercase() { + t |= 0x2; // C1_LOWER + } + if c.is_ascii_digit() { + t |= 0x4; // C1_DIGIT + } + if c == b' ' || (0x9..=0xd).contains(&c) { + t |= 0x8; // C1_SPACE + } + if c.is_ascii_punctuation() { + t |= 0x10; // C1_PUNCT + } + if c < 0x20 || c == 0x7f { + t |= 0x20; // C1_CNTRL + } + if c == b' ' || c == 0x9 { + t |= 0x40; // C1_BLANK + } + if c.is_ascii_hexdigit() { + t |= 0x80; // C1_XDIGIT + } + if c.is_ascii_alphabetic() || c >= 0x80 { + t |= 0x100; // C1_ALPHA + } + t } #[win32_derive::dllexport] pub fn GetStringTypeA( - _ctx: &mut Context, + ctx: &mut Context, _Locale: u32, - _dwInfoType: u32, - _lpSrcStr: Ptr, - _cchSrc: i32, - _lpCharType: Ptr, + dwInfoType: u32, + lpSrcStr: Ptr, + cchSrc: i32, + lpCharType: Ptr, ) -> bool { - todo!(); + if dwInfoType != 1 { + log::warn!("GetStringTypeA: unimplemented type {dwInfoType}"); + return false; + } + let len = if cchSrc < 0 { + ctx.memory.read_str(lpSrcStr.addr).len() + 1 + } else { + cchSrc as usize + }; + for i in 0..len as u32 { + let c = ctx.memory.read::(lpSrcStr.addr + i); + ctx.memory + .write::(lpCharType.addr + i * 2, ctype1(c as u32)); + } + true } #[win32_derive::dllexport] pub fn GetStringTypeW( - _ctx: &mut Context, - _dwInfoType: u32, - _lpSrcStr: Ptr, - _cchSrc: i32, - _lpCharType: Ptr, + ctx: &mut Context, + dwInfoType: u32, + lpSrcStr: Ptr, + cchSrc: i32, + lpCharType: Ptr, ) -> bool { - todo!(); + if dwInfoType != 1 { + log::warn!("GetStringTypeW: unimplemented type {dwInfoType}"); + return false; + } + let len = if cchSrc < 0 { + let mut n = 0; + while ctx.memory.read::(lpSrcStr.addr + n * 2) != 0 { + n += 1; + } + n + 1 + } else { + cchSrc as u32 + }; + for i in 0..len { + let c = ctx.memory.read::(lpSrcStr.addr + i * 2); + ctx.memory + .write::(lpCharType.addr + i * 2, ctype1(c as u32)); + } + true +} + +/// ASCII-only character mapping for LCMapString*. +fn lcmap_char(c: u32, flags: u32) -> u32 { + const LCMAP_LOWERCASE: u32 = 0x100; + const LCMAP_UPPERCASE: u32 = 0x200; + if c < 0x80 { + if flags & LCMAP_LOWERCASE != 0 { + return (c as u8).to_ascii_lowercase() as u32; + } + if flags & LCMAP_UPPERCASE != 0 { + return (c as u8).to_ascii_uppercase() as u32; + } + } + c } #[win32_derive::dllexport] pub fn LCMapStringA( - _ctx: &mut Context, + ctx: &mut Context, _Locale: u32, - _dwMapFlags: u32, - _lpSrcStr: Ptr, - _cchSrc: i32, - _lpDestStr: Ptr, - _cchDest: i32, + dwMapFlags: u32, + lpSrcStr: Ptr, + cchSrc: i32, + lpDestStr: Ptr, + cchDest: i32, ) -> i32 { - todo!(); + let len = if cchSrc < 0 { + ctx.memory.read_str(lpSrcStr.addr).len() as u32 + 1 + } else { + cchSrc as u32 + }; + if cchDest == 0 { + return len as i32; + } + if (cchDest as u32) < len { + return 0; + } + for i in 0..len { + let c = ctx.memory.read::(lpSrcStr.addr + i); + ctx.memory + .write::(lpDestStr.addr + i, lcmap_char(c as u32, dwMapFlags) as u8); + } + len as i32 } #[win32_derive::dllexport] pub fn LCMapStringW( - _ctx: &mut Context, + ctx: &mut Context, _Locale: u32, - _dwMapFlags: u32, - _lpSrcStr: Ptr, - _cchSrc: i32, - _lpDestStr: Ptr, - _cchDest: i32, + dwMapFlags: u32, + lpSrcStr: Ptr, + cchSrc: i32, + lpDestStr: Ptr, + cchDest: i32, ) -> i32 { - todo!(); + let len = if cchSrc < 0 { + let mut n = 0; + while ctx.memory.read::(lpSrcStr.addr + n * 2) != 0 { + n += 1; + } + n + 1 + } else { + cchSrc as u32 + }; + if cchDest == 0 { + return len as i32; + } + if (cchDest as u32) < len { + return 0; + } + for i in 0..len { + let c = ctx.memory.read::(lpSrcStr.addr + i * 2); + ctx.memory.write::( + lpDestStr.addr + i * 2, + lcmap_char(c as u32, dwMapFlags) as u16, + ); + } + len as i32 } #[win32_derive::dllexport] diff --git a/win32/winapi/src/kernel32/strings.rs b/win32/winapi/src/kernel32/strings.rs new file mode 100644 index 00000000..7d46f7bb --- /dev/null +++ b/win32/winapi/src/kernel32/strings.rs @@ -0,0 +1,118 @@ +//! String functions: lstr*, CompareString*. + +use runtime::Context; + +use crate::Ptr; + +const CSTR_LESS_THAN: i32 = 1; +const CSTR_EQUAL: i32 = 2; +const CSTR_GREATER_THAN: i32 = 3; + +const NORM_IGNORECASE: u32 = 1; + +#[win32_derive::dllexport] +pub fn lstrlenA(ctx: &mut Context, lpString: Ptr) -> i32 { + if lpString.addr == 0 { + return 0; + } + ctx.memory.read_str(lpString.addr).len() as i32 +} + +#[win32_derive::dllexport] +pub fn lstrcpyA(ctx: &mut Context, lpString1: Ptr, lpString2: Ptr) -> u32 { + let src = ctx.memory.read_str(lpString2.addr).to_owned(); + let bytes = src.as_bytes(); + ctx.memory[lpString1.addr..][..bytes.len()].copy_from_slice(bytes); + ctx.memory + .write::(lpString1.addr + bytes.len() as u32, 0); + lpString1.addr +} + +#[win32_derive::dllexport] +pub fn lstrcatA(ctx: &mut Context, lpString1: Ptr, lpString2: Ptr) -> u32 { + let dst_len = ctx.memory.read_str(lpString1.addr).len() as u32; + let src = ctx.memory.read_str(lpString2.addr).to_owned(); + let bytes = src.as_bytes(); + ctx.memory[lpString1.addr + dst_len..][..bytes.len()].copy_from_slice(bytes); + ctx.memory + .write::(lpString1.addr + dst_len + bytes.len() as u32, 0); + lpString1.addr +} + +fn read_counted_a(ctx: &Context, addr: u32, count: i32) -> Vec { + if count < 0 { + ctx.memory.read_str(addr).as_bytes().to_vec() + } else { + ctx.memory[addr..][..count as usize].to_vec() + } +} + +fn read_counted_w(ctx: &Context, addr: u32, count: i32) -> Vec { + let mut out = Vec::new(); + let mut addr = addr; + if count < 0 { + loop { + let c = ctx.memory.read::(addr); + if c == 0 { + break; + } + out.push(c); + addr += 2; + } + } else { + for _ in 0..count { + out.push(ctx.memory.read::(addr)); + addr += 2; + } + } + out +} + +fn compare_ordering(ord: std::cmp::Ordering) -> i32 { + match ord { + std::cmp::Ordering::Less => CSTR_LESS_THAN, + std::cmp::Ordering::Equal => CSTR_EQUAL, + std::cmp::Ordering::Greater => CSTR_GREATER_THAN, + } +} + +#[win32_derive::dllexport] +pub fn CompareStringA( + ctx: &mut Context, + _Locale: u32, + dwCmpFlags: u32, + lpString1: Ptr, + cchCount1: i32, + lpString2: Ptr, + cchCount2: i32, +) -> i32 { + let mut a = read_counted_a(ctx, lpString1.addr, cchCount1); + let mut b = read_counted_a(ctx, lpString2.addr, cchCount2); + if dwCmpFlags & NORM_IGNORECASE != 0 { + a.make_ascii_lowercase(); + b.make_ascii_lowercase(); + } + compare_ordering(a.cmp(&b)) +} + +#[win32_derive::dllexport] +pub fn CompareStringW( + ctx: &mut Context, + _Locale: u32, + dwCmpFlags: u32, + lpString1: Ptr, + cchCount1: i32, + lpString2: Ptr, + cchCount2: i32, +) -> i32 { + let mut a = read_counted_w(ctx, lpString1.addr, cchCount1); + let mut b = read_counted_w(ctx, lpString2.addr, cchCount2); + if dwCmpFlags & NORM_IGNORECASE != 0 { + for c in a.iter_mut().chain(b.iter_mut()) { + if *c < 0x80 { + *c = (*c as u8).to_ascii_lowercase() as u16; + } + } + } + compare_ordering(a.cmp(&b)) +} diff --git a/win32/winapi/src/kernel32/time.rs b/win32/winapi/src/kernel32/time.rs index f98c2ffb..d497f44a 100644 --- a/win32/winapi/src/kernel32/time.rs +++ b/win32/winapi/src/kernel32/time.rs @@ -9,3 +9,34 @@ pub fn GetTickCount(_ctx: &mut Context) -> u32 { pub fn Sleep(_ctx: &mut Context, dwMilliseconds: u32) { std::thread::sleep(std::time::Duration::from_millis(dwMilliseconds as u64)); } + +#[win32_derive::dllexport] +pub fn GetTimeZoneInformation( + ctx: &mut Context, + lpTimeZoneInformation: crate::Ptr, +) -> u32 /* TIME_ZONE_ID */ { + // TIME_ZONE_INFORMATION is 172 bytes; report UTC by zeroing it. + ctx.memory[lpTimeZoneInformation.addr..][..172].fill(0); + 0 // TIME_ZONE_ID_UNKNOWN +} + +#[win32_derive::dllexport] +pub fn FileTimeToLocalFileTime( + ctx: &mut Context, + lpFileTime: crate::Ptr, + lpLocalFileTime: crate::Ptr, +) -> bool { + let time = lpFileTime.read(&ctx.memory).unwrap_or(0); + lpLocalFileTime.write(&mut ctx.memory, time).is_some() +} + +#[win32_derive::dllexport] +pub fn FileTimeToSystemTime( + ctx: &mut Context, + _lpFileTime: crate::Ptr, + lpSystemTime: crate::Ptr, +) -> bool { + // SYSTEMTIME is 16 bytes; the game only shows these values incidentally. + ctx.memory[lpSystemTime.addr..][..16].fill(0); + true +} diff --git a/win32/winapi/src/lib.rs b/win32/winapi/src/lib.rs index a0ac41d0..11988a15 100644 --- a/win32/winapi/src/lib.rs +++ b/win32/winapi/src/lib.rs @@ -5,6 +5,7 @@ pub mod advapi32; pub mod bitmap_format; pub mod ddraw; +pub mod dinput; mod dllexport; pub mod dsound; pub mod gdi32; @@ -12,7 +13,9 @@ mod handle; mod heap; pub mod kernel32; mod locked_state; +pub mod msacm32; pub mod msvcrt; +pub mod ole32; mod point; mod ptr; mod rect; @@ -50,7 +53,7 @@ pub fn load(exe: &EXEData) -> Context { host::init(); crate::trace::init(&thesesus_trace()); - let memory_size = 32 << 20; + let memory_size = 64 << 20; let memory = Memory::leak_new(memory_size); kernel32::init_state(exe.image_base, exe.resources.clone()); diff --git a/win32/winapi/src/msacm32.rs b/win32/winapi/src/msacm32.rs new file mode 100644 index 00000000..ef89b2a5 --- /dev/null +++ b/win32/winapi/src/msacm32.rs @@ -0,0 +1,6 @@ +use runtime::Context; + +#[win32_derive::dllexport] +pub fn acmMetrics(_ctx: &mut Context, _hao: u32, _uMetric: u32, _pMetric: u32) -> u32 { + crate::stub!(1) // MMSYSERR_ERROR: no codecs available +} diff --git a/win32/winapi/src/ole32.rs b/win32/winapi/src/ole32.rs new file mode 100644 index 00000000..134efb2f --- /dev/null +++ b/win32/winapi/src/ole32.rs @@ -0,0 +1,21 @@ +use runtime::Context; + +#[win32_derive::dllexport] +pub fn CoInitialize(_ctx: &mut Context, _pvReserved: u32) -> u32 /* HRESULT */ { + 0 // S_OK +} + +#[win32_derive::dllexport] +pub fn CoUninitialize(_ctx: &mut Context) {} + +#[win32_derive::dllexport] +pub fn CoCreateInstance( + _ctx: &mut Context, + _rclsid: u32, + _pUnkOuter: u32, + _dwClsContext: u32, + _riid: u32, + _ppv: u32, +) -> u32 /* HRESULT */ { + todo!("CoCreateInstance") +} diff --git a/win32/winapi/src/user32/dialog.rs b/win32/winapi/src/user32/dialog.rs index dfc21b85..a93cd68d 100644 --- a/win32/winapi/src/user32/dialog.rs +++ b/win32/winapi/src/user32/dialog.rs @@ -44,6 +44,18 @@ pub fn EndDialog(_ctx: &mut Context, _hDlg: HWND, _nResult: i32) -> bool { todo!() } +#[win32_derive::dllexport] +pub fn SendDlgItemMessageA( + _ctx: &mut Context, + _hDlg: HWND, + _nIDDlgItem: i32, + _Msg: u32, + _wParam: u32, + _lParam: u32, +) -> u32 { + crate::stub!(0) +} + #[win32_derive::dllexport] pub fn IsDlgButtonChecked(_ctx: &mut Context, _hDlg: HWND, _nIDButton: i32) -> u32 { todo!() diff --git a/win32/winapi/src/user32/message.rs b/win32/winapi/src/user32/message.rs index fc74269c..bee3900d 100644 --- a/win32/winapi/src/user32/message.rs +++ b/win32/winapi/src/user32/message.rs @@ -18,8 +18,12 @@ pub type LPARAM = u32; #[derive(win32_derive::ABIEnum, Debug)] pub enum WM { + ACTIVATE = 0x6, + SETFOCUS = 0x7, PAINT = 0xf, QUIT = 0x12, + SHOWWINDOW = 0x18, + ACTIVATEAPP = 0x1c, MOUSEMOVE = 0x200, LBUTTONDOWN = 0x201, LBUTTONUP = 0x202, @@ -108,6 +112,29 @@ fn mouse_msg(wm: WM, hwnd: HWND, message: &host::MouseMessage) -> MSG { } } +/// Post a message to the application's queue (e.g. synthetic activation +/// messages from ShowWindow). +pub fn post_message(hwnd: HWND, message: u32, wParam: WPARAM, lParam: LPARAM) { + let mut queue = state().message_queue.borrow_mut(); + queue.messages.push_back(MSG { + hwnd, + message, + wParam, + lParam, + time: 0, + pt: POINT::default(), + }); +} + +#[win32_derive::dllexport] +pub fn WaitMessage(_ctx: &mut Context) -> bool { + let mut queue = state().message_queue.borrow_mut(); + if queue.peek().is_none() { + queue.wait_host(); + } + true +} + impl MessageQueue { fn paint_msg(&self) -> Option { let window = self.window.as_ref()?.borrow(); @@ -332,19 +359,40 @@ pub fn TranslateAcceleratorW( } #[win32_derive::dllexport] -pub fn PostQuitMessage(_ctx: &mut Context, _nExitCode: i32) { - todo!() +pub fn PostQuitMessage(_ctx: &mut Context, nExitCode: i32) { + let mut queue = state().message_queue.borrow_mut(); + queue.quit = Some(MSG { + hwnd: HWND::null(), + message: WM::QUIT as u32, + wParam: nExitCode as u32, + lParam: 0, + time: 0, + pt: POINT::default(), + }); } #[win32_derive::dllexport] pub fn PostMessageW( _ctx: &mut Context, - _hWnd: HWND, - _Msg: u32, - _wParam: WPARAM, - _lParam: LPARAM, + hWnd: HWND, + Msg: u32, + wParam: WPARAM, + lParam: LPARAM, ) -> bool { - todo!() + post_message(hWnd, Msg, wParam, lParam); + true +} + +#[win32_derive::dllexport] +pub fn PostMessageA( + _ctx: &mut Context, + hWnd: HWND, + Msg: u32, + wParam: WPARAM, + lParam: LPARAM, +) -> bool { + post_message(hWnd, Msg, wParam, lParam); + true } #[win32_derive::dllexport] diff --git a/win32/winapi/src/user32/misc.rs b/win32/winapi/src/user32/misc.rs index 8bf38ebc..51f92188 100644 --- a/win32/winapi/src/user32/misc.rs +++ b/win32/winapi/src/user32/misc.rs @@ -126,3 +126,101 @@ pub fn wsprintfW( ) -> i32 { todo!() } + +#[win32_derive::dllexport] +pub fn wsprintfA(ctx: &mut Context) -> i32 { + // Cdecl varargs: declared with no args so the wrapper leaves the caller's + // stack alone; read everything manually. + // [esp] = return addr, [esp+4] = dst, [esp+8] = fmt, [esp+12...] = args. + let esp = ctx.cpu.regs.esp; + let dst = ctx.memory.read::(esp + 4); + let fmt_addr = ctx.memory.read::(esp + 8); + let fmt = ctx.memory.read_str(fmt_addr).to_owned(); + let mut arg_addr = esp + 12; + + let bytes = fmt.as_bytes(); + let mut out: Vec = Vec::new(); + let mut i = 0; + while i < bytes.len() { + let c = bytes[i]; + i += 1; + if c != b'%' { + out.push(c); + continue; + } + let mut left = false; + let mut zero = false; + loop { + match bytes.get(i) { + Some(b'-') => { + left = true; + i += 1; + } + Some(b'0') => { + zero = true; + i += 1; + } + Some(b'#') => i += 1, + _ => break, + } + } + let mut width = 0usize; + while let Some(&d @ b'1'..=b'9') = bytes.get(i) { + width = width * 10 + (d - b'0') as usize; + i += 1; + while let Some(&d @ b'0'..=b'9') = bytes.get(i) { + width = width * 10 + (d - b'0') as usize; + i += 1; + } + } + if bytes.get(i) == Some(&b'.') { + i += 1; + while matches!(bytes.get(i), Some(b'0'..=b'9')) { + i += 1; + } + } + while matches!(bytes.get(i), Some(b'l') | Some(b'h')) { + i += 1; + } + let spec = *bytes.get(i).unwrap_or(&b'%'); + i += 1; + let mut next_arg = || { + let value = ctx.memory.read::(arg_addr); + arg_addr += 4; + value + }; + let formatted: Vec = match spec { + b'%' => vec![b'%'], + b'd' | b'i' => format!("{}", next_arg() as i32).into_bytes(), + b'u' => format!("{}", next_arg()).into_bytes(), + b'x' => format!("{:x}", next_arg()).into_bytes(), + b'X' => format!("{:X}", next_arg()).into_bytes(), + b'c' => vec![next_arg() as u8], + b's' => { + let addr = next_arg(); + ctx.memory.read_str(addr).as_bytes().to_vec() + } + _ => { + log::warn!("wsprintfA: unhandled %{}", spec as char); + vec![b'%', spec] + } + }; + if formatted.len() < width { + let pad = if zero && !left { b'0' } else { b' ' }; + let padding = std::iter::repeat(pad).take(width - formatted.len()); + if left { + out.extend(formatted); + out.extend(padding); + } else { + out.extend(padding); + out.extend(formatted); + } + } else { + out.extend(formatted); + } + } + + ctx.memory[dst..][..out.len()].copy_from_slice(&out); + ctx.memory.write::(dst + out.len() as u32, 0); + out.len() as i32 +} diff --git a/win32/winapi/src/user32/rect.rs b/win32/winapi/src/user32/rect.rs index df7646d7..e1906885 100644 --- a/win32/winapi/src/user32/rect.rs +++ b/win32/winapi/src/user32/rect.rs @@ -1,7 +1,26 @@ use runtime::Context; +use super::HWND; use crate::{POINT, Ptr, RECT}; +#[win32_derive::dllexport] +pub fn OffsetRect(ctx: &mut Context, lprc: Ptr, dx: i32, dy: i32) -> bool { + let Some(mut rect) = lprc.read(&ctx.memory) else { + return false; + }; + rect.left += dx; + rect.right += dx; + rect.top += dy; + rect.bottom += dy; + lprc.write(&mut ctx.memory, rect).is_some() +} + +#[win32_derive::dllexport] +pub fn ClientToScreen(_ctx: &mut Context, _hWnd: HWND, _lpPoint: Ptr) -> bool { + // The window's client area sits at the screen origin. + true +} + #[win32_derive::dllexport] pub fn PtInRect(ctx: &mut Context, lprc: Ptr, x: i32, y: i32) -> bool { let rect = lprc.read(&ctx.memory).unwrap(); diff --git a/win32/winapi/src/user32/window.rs b/win32/winapi/src/user32/window.rs index 9c74be59..66fd8837 100644 --- a/win32/winapi/src/user32/window.rs +++ b/win32/winapi/src/user32/window.rs @@ -180,16 +180,23 @@ pub fn CreateWindowExW( #[win32_derive::dllexport] pub fn DestroyWindow(_ctx: &mut Context, _hWnd: HWND) -> bool { - todo!() + stub!(true) } #[win32_derive::dllexport] pub fn ShowWindow( _ctx: &mut Context, - _hWnd: HWND, + hWnd: HWND, _nCmdShow: u32, /* SHOW_WINDOW_CMD */ ) -> bool { - stub!(true) + // The window comes up focused; games often wait for activation before + // running their main loop. + use super::message::{WM, post_message}; + post_message(hWnd, WM::SHOWWINDOW as u32, 1, 0); + post_message(hWnd, WM::ACTIVATEAPP as u32, 1, 0); + post_message(hWnd, WM::ACTIVATE as u32, 1, 0); // WA_ACTIVE + post_message(hWnd, WM::SETFOCUS as u32, 0, 0); + true } #[win32_derive::dllexport] @@ -240,7 +247,10 @@ pub fn DefWindowProcW( ) -> u32 { let msg = match msg { Ok(msg) => msg, - Err(n) => todo!("message type {:x}", n), + Err(n) => { + log::warn!("DefWindowProc: unhandled message type {:x}", n); + return 0; + } }; let window = state().window.borrow(); @@ -456,6 +466,50 @@ pub fn GetDesktopWindow(_ctx: &mut Context) -> HWND { stub!(HWND::null()) } +#[win32_derive::dllexport] +pub fn GetClientRect(ctx: &mut Context, _hWnd: HWND, lpRect: Ptr) -> bool { + let rect = { + let window = state().window.borrow(); + let window = window.as_ref().unwrap().borrow(); + window.rect() + }; + lpRect.write(&mut ctx.memory, rect).is_some() +} + +#[win32_derive::dllexport] +pub fn SetWindowPos( + _ctx: &mut Context, + _hWnd: HWND, + _hWndInsertAfter: u32, + _X: i32, + _Y: i32, + _cx: i32, + _cy: i32, + _uFlags: u32, +) -> bool { + stub!(true) +} + +#[win32_derive::dllexport] +pub fn SetWindowTextA(_ctx: &mut Context, _hWnd: HWND, _lpString: Ptr) -> bool { + stub!(true) +} + +#[win32_derive::dllexport] +pub fn EnableWindow(_ctx: &mut Context, _hWnd: HWND, _bEnable: bool) -> bool { + stub!(false) +} + +#[win32_derive::dllexport] +pub fn SetCursor(_ctx: &mut Context, _hCursor: u32) -> u32 { + stub!(0) +} + +#[win32_derive::dllexport] +pub fn SetCursorPos(_ctx: &mut Context, _X: i32, _Y: i32) -> bool { + stub!(true) +} + #[win32_derive::dllexport] pub fn MapWindowPoints( ctx: &mut Context, diff --git a/win32/winapi/src/winmm/mmio.rs b/win32/winapi/src/winmm/mmio.rs new file mode 100644 index 00000000..d7ee398b --- /dev/null +++ b/win32/winapi/src/winmm/mmio.rs @@ -0,0 +1,72 @@ +//! mmio*: RIFF (WAV) file reading, plus MCI. Currently stubs; see the +//! retrowin32 fork's winmm mmio implementation for a reference port. + +use runtime::Context; + +#[win32_derive::dllexport] +pub fn mciSendCommandA( + _ctx: &mut Context, + _mciId: u32, + _uMsg: u32, + _dwParam1: u32, + _dwParam2: u32, +) -> u32 { + // CD audio etc.; pretend success and play nothing. + crate::stub!(0) +} + +#[win32_derive::dllexport] +pub fn mmioOpenA( + _ctx: &mut Context, + _szFilename: u32, + _lpmmioinfo: u32, + _dwOpenFlags: u32, +) -> u32 { + todo!("mmioOpenA") +} + +#[win32_derive::dllexport] +pub fn mmioClose(_ctx: &mut Context, _hmmio: u32, _wFlags: u32) -> u32 { + todo!("mmioClose") +} + +#[win32_derive::dllexport] +pub fn mmioDescend( + _ctx: &mut Context, + _hmmio: u32, + _lpck: u32, + _lpckParent: u32, + _wFlags: u32, +) -> u32 { + todo!("mmioDescend") +} + +#[win32_derive::dllexport] +pub fn mmioAscend(_ctx: &mut Context, _hmmio: u32, _lpck: u32, _wFlags: u32) -> u32 { + todo!("mmioAscend") +} + +#[win32_derive::dllexport] +pub fn mmioRead(_ctx: &mut Context, _hmmio: u32, _pch: u32, _cch: u32) -> i32 { + todo!("mmioRead") +} + +#[win32_derive::dllexport] +pub fn mmioSeek(_ctx: &mut Context, _hmmio: u32, _lOffset: i32, _iOrigin: i32) -> i32 { + todo!("mmioSeek") +} + +#[win32_derive::dllexport] +pub fn mmioGetInfo(_ctx: &mut Context, _hmmio: u32, _lpmmioinfo: u32, _wFlags: u32) -> u32 { + todo!("mmioGetInfo") +} + +#[win32_derive::dllexport] +pub fn mmioSetInfo(_ctx: &mut Context, _hmmio: u32, _lpmmioinfo: u32, _wFlags: u32) -> u32 { + todo!("mmioSetInfo") +} + +#[win32_derive::dllexport] +pub fn mmioAdvance(_ctx: &mut Context, _hmmio: u32, _lpmmioinfo: u32, _wFlags: u32) -> u32 { + todo!("mmioAdvance") +} diff --git a/win32/winapi/src/winmm/mod.rs b/win32/winapi/src/winmm/mod.rs index 610771f2..7c4f410d 100644 --- a/win32/winapi/src/winmm/mod.rs +++ b/win32/winapi/src/winmm/mod.rs @@ -8,6 +8,8 @@ mod wave; pub use wave::*; mod misc; pub use misc::*; +mod mmio; +pub use mmio::*; #[derive(Default)] pub struct State { diff --git a/win32/winapi/src/winmm/time.rs b/win32/winapi/src/winmm/time.rs index 1d24816b..506bd940 100644 --- a/win32/winapi/src/winmm/time.rs +++ b/win32/winapi/src/winmm/time.rs @@ -12,6 +12,11 @@ pub struct Timer { pub user_data: u32, } +#[win32_derive::dllexport] +pub fn timeGetTime(_ctx: &mut Context) -> u32 { + host::host().time() +} + #[derive(Debug)] pub struct TIME { periodic: bool, From b23a8d93d47d9dfca013c8bf7f7ae1e473c4aea1 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Tue, 21 Jul 2026 01:14:43 +0300 Subject: [PATCH 02/10] keyboard, sound, mmio, colorkey; winpin reaches the table --- Cargo.lock | 1 + dos/src/lib.rs | 1 + host/Cargo.toml | 1 + host/src/lib.rs | 18 + host/src/sdl.rs | 153 ++++- runtime/src/lib.rs | 2 +- runtime/src/machine.rs | 70 ++- tc/src/codegen/mod.rs | 11 + tc/src/exe.rs | 1 + tc/src/gather.rs | 114 +++- tc/src/lib.rs | 37 +- win32/winapi/src/ddraw/ddraw.rs | 149 +++-- win32/winapi/src/ddraw/ddraw1.rs | 235 ++++++-- win32/winapi/src/ddraw/ddraw7.rs | 97 ++-- win32/winapi/src/ddraw/types.rs | 21 +- win32/winapi/src/dinput.rs | 183 +++++- win32/winapi/src/dllexport.rs | 7 + win32/winapi/src/dsound.rs | 826 ++++++++++++++++++++------- win32/winapi/src/kernel32/dll.rs | 115 +++- win32/winapi/src/kernel32/process.rs | 4 +- win32/winapi/src/kernel32/thread.rs | 1 + win32/winapi/src/lib.rs | 19 +- win32/winapi/src/msacm32.rs | 39 +- win32/winapi/src/user32/input.rs | 394 +++++++++++++ win32/winapi/src/user32/message.rs | 91 ++- win32/winapi/src/user32/misc.rs | 34 +- win32/winapi/src/user32/mod.rs | 4 + win32/winapi/src/user32/window.rs | 22 +- win32/winapi/src/winmm/mmio.rs | 336 ++++++++++- win32/winapi/src/winmm/mod.rs | 10 + 30 files changed, 2533 insertions(+), 463 deletions(-) create mode 100644 win32/winapi/src/user32/input.rs diff --git a/Cargo.lock b/Cargo.lock index 31b1dff5..96cc141f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,7 @@ dependencies = [ "bitflags", "console_error_panic_hook", "js-sys", + "log", "logger", "sdl3-sys", "wasm-bindgen", diff --git a/dos/src/lib.rs b/dos/src/lib.rs index 60cef6ec..9149f055 100644 --- a/dos/src/lib.rs +++ b/dos/src/lib.rs @@ -159,6 +159,7 @@ pub fn load(exe: &EXEData, command_line: Option<&str>) -> Context { thread_id: 1, memory, blocks: exe.blocks, + block_map: runtime::BlockMap::get_or_init(exe.blocks), recent: [Context::return_from_x86; 4], }; ctx.cpu.real_mode = true; diff --git a/host/Cargo.toml b/host/Cargo.toml index 701152a9..75d67b5b 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] bitflags = { workspace = true } +log = { workspace = true } logger = { workspace = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] diff --git a/host/src/lib.rs b/host/src/lib.rs index ad747f08..40c7a729 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -42,6 +42,22 @@ pub struct MouseMessage { pub buttons: MouseButton, } +/// A key press/release, described the way DOS-era Windows apps expect: by +/// PC/AT scan code (what DirectInput calls DIK_*) plus virtual key code. +pub struct KeyMessage { + /// PC "set 1" scan code, without the 0xe0 prefix of extended keys. + pub scancode: u8, + /// Windows VK_* code. Uses the side-specific code (VK_RSHIFT, not VK_SHIFT) + /// where one exists; user32 widens it for window messages. + pub vkey: u8, + /// Key from the extended (0xe0-prefixed) part of the keyboard: arrows, + /// right ctrl/alt, keypad enter. Distinguishes e.g. arrow up from keypad 8, + /// which share scan code 0x48. + pub extended: bool, + /// Event produced by auto-repeat rather than a fresh press. + pub repeat: bool, +} + pub enum Message { #[cfg(not(target_family = "wasm"))] // no "quit" menu on web Quit, @@ -50,6 +66,8 @@ pub enum Message { MouseDown(MouseMessage), MouseUp(MouseMessage), MouseMove(MouseMessage), + KeyDown(KeyMessage), + KeyUp(KeyMessage), } pub fn init() { diff --git a/host/src/sdl.rs b/host/src/sdl.rs index 647d896b..96c21fcf 100644 --- a/host/src/sdl.rs +++ b/host/src/sdl.rs @@ -49,6 +49,132 @@ fn mouse_buttons_from_sdl(state: sdl::mouse::SDL_MouseButtonFlags) -> host::Mous buttons } +/// SDL scan code -> (PC set-1 scan code, Windows VK_*, extended key). +/// +/// SDL scan codes are USB HID usages, while Windows apps (and DirectInput's +/// DIK_* constants) speak PC/AT set 1, so the two need an explicit table. +/// Keys with no PC/AT equivalent are simply absent. +#[rustfmt::skip] +const KEY_MAP: &[(sdl::scancode::SDL_Scancode, u8, u8, bool)] = { + use sdl::scancode::SDL_Scancode as SC; + &[ + (SC::ESCAPE, 0x01, 0x1b, false), + (SC::_1, 0x02, b'1', false), + (SC::_2, 0x03, b'2', false), + (SC::_3, 0x04, b'3', false), + (SC::_4, 0x05, b'4', false), + (SC::_5, 0x06, b'5', false), + (SC::_6, 0x07, b'6', false), + (SC::_7, 0x08, b'7', false), + (SC::_8, 0x09, b'8', false), + (SC::_9, 0x0a, b'9', false), + (SC::_0, 0x0b, b'0', false), + (SC::MINUS, 0x0c, 0xbd, false), // VK_OEM_MINUS + (SC::EQUALS, 0x0d, 0xbb, false), // VK_OEM_PLUS + (SC::BACKSPACE, 0x0e, 0x08, false), + (SC::TAB, 0x0f, 0x09, false), + (SC::Q, 0x10, b'Q', false), + (SC::W, 0x11, b'W', false), + (SC::E, 0x12, b'E', false), + (SC::R, 0x13, b'R', false), + (SC::T, 0x14, b'T', false), + (SC::Y, 0x15, b'Y', false), + (SC::U, 0x16, b'U', false), + (SC::I, 0x17, b'I', false), + (SC::O, 0x18, b'O', false), + (SC::P, 0x19, b'P', false), + (SC::LEFTBRACKET, 0x1a, 0xdb, false), // VK_OEM_4 + (SC::RIGHTBRACKET, 0x1b, 0xdd, false), // VK_OEM_6 + (SC::RETURN, 0x1c, 0x0d, false), + (SC::LCTRL, 0x1d, 0xa2, false), // VK_LCONTROL + (SC::A, 0x1e, b'A', false), + (SC::S, 0x1f, b'S', false), + (SC::D, 0x20, b'D', false), + (SC::F, 0x21, b'F', false), + (SC::G, 0x22, b'G', false), + (SC::H, 0x23, b'H', false), + (SC::J, 0x24, b'J', false), + (SC::K, 0x25, b'K', false), + (SC::L, 0x26, b'L', false), + (SC::SEMICOLON, 0x27, 0xba, false), // VK_OEM_1 + (SC::APOSTROPHE, 0x28, 0xde, false), // VK_OEM_7 + (SC::GRAVE, 0x29, 0xc0, false), // VK_OEM_3 + (SC::LSHIFT, 0x2a, 0xa0, false), // VK_LSHIFT + (SC::BACKSLASH, 0x2b, 0xdc, false), // VK_OEM_5 + (SC::Z, 0x2c, b'Z', false), + (SC::X, 0x2d, b'X', false), + (SC::C, 0x2e, b'C', false), + (SC::V, 0x2f, b'V', false), + (SC::B, 0x30, b'B', false), + (SC::N, 0x31, b'N', false), + (SC::M, 0x32, b'M', false), + (SC::COMMA, 0x33, 0xbc, false), // VK_OEM_COMMA + (SC::PERIOD, 0x34, 0xbe, false), // VK_OEM_PERIOD + (SC::SLASH, 0x35, 0xbf, false), // VK_OEM_2 + (SC::RSHIFT, 0x36, 0xa1, false), // VK_RSHIFT + (SC::KP_MULTIPLY, 0x37, 0x6a, false), // VK_MULTIPLY + (SC::LALT, 0x38, 0xa4, false), // VK_LMENU + (SC::SPACE, 0x39, 0x20, false), + (SC::CAPSLOCK, 0x3a, 0x14, false), + (SC::F1, 0x3b, 0x70, false), + (SC::F2, 0x3c, 0x71, false), + (SC::F3, 0x3d, 0x72, false), + (SC::F4, 0x3e, 0x73, false), + (SC::F5, 0x3f, 0x74, false), + (SC::F6, 0x40, 0x75, false), + (SC::F7, 0x41, 0x76, false), + (SC::F8, 0x42, 0x77, false), + (SC::F9, 0x43, 0x78, false), + (SC::F10, 0x44, 0x79, false), + (SC::NUMLOCKCLEAR, 0x45, 0x90, false), + (SC::SCROLLLOCK, 0x46, 0x91, false), + (SC::KP_7, 0x47, 0x67, false), + (SC::KP_8, 0x48, 0x68, false), + (SC::KP_9, 0x49, 0x69, false), + (SC::KP_MINUS, 0x4a, 0x6d, false), + (SC::KP_4, 0x4b, 0x64, false), + (SC::KP_5, 0x4c, 0x65, false), + (SC::KP_6, 0x4d, 0x66, false), + (SC::KP_PLUS, 0x4e, 0x6b, false), + (SC::KP_1, 0x4f, 0x61, false), + (SC::KP_2, 0x50, 0x62, false), + (SC::KP_3, 0x51, 0x63, false), + (SC::KP_0, 0x52, 0x60, false), + (SC::KP_PERIOD, 0x53, 0x6e, false), + (SC::F11, 0x57, 0x7a, false), + (SC::F12, 0x58, 0x7b, false), + // Extended keys: same scan code as their non-extended twin, but + // prefixed with 0xe0 on the wire. + (SC::KP_ENTER, 0x1c, 0x0d, true), + (SC::RCTRL, 0x1d, 0xa3, true), // VK_RCONTROL + (SC::KP_DIVIDE, 0x35, 0x6f, true), // VK_DIVIDE + (SC::RALT, 0x38, 0xa5, true), // VK_RMENU + (SC::HOME, 0x47, 0x24, true), + (SC::UP, 0x48, 0x26, true), + (SC::PAGEUP, 0x49, 0x21, true), + (SC::LEFT, 0x4b, 0x25, true), + (SC::RIGHT, 0x4d, 0x27, true), + (SC::END, 0x4f, 0x23, true), + (SC::DOWN, 0x50, 0x28, true), + (SC::PAGEDOWN, 0x51, 0x22, true), + (SC::INSERT, 0x52, 0x2d, true), + (SC::DELETE, 0x53, 0x2e, true), + (SC::LGUI, 0x5b, 0x5b, true), // VK_LWIN + (SC::RGUI, 0x5c, 0x5c, true), // VK_RWIN + (SC::APPLICATION, 0x5d, 0x5d, true), // VK_APPS + ] +}; + +fn key_from_sdl(event: &sdl::events::SDL_KeyboardEvent) -> Option { + let &(_, scancode, vkey, extended) = KEY_MAP.iter().find(|key| key.0 == event.scancode)?; + Some(host::KeyMessage { + scancode, + vkey, + extended, + repeat: event.repeat, + }) +} + fn msg_from_event(event: &sdl::events::SDL_Event) -> Option { unsafe { use sdl::events::SDL_EventType; @@ -84,6 +210,14 @@ fn msg_from_event(event: &sdl::events::SDL_Event) -> Option { return Some(host::Message::MouseUp(message)); } } + SDL_EventType::KEY_DOWN | SDL_EventType::KEY_UP => { + let key = key_from_sdl(&event.key)?; + if typ == SDL_EventType::KEY_DOWN { + return Some(host::Message::KeyDown(key)); + } else { + return Some(host::Message::KeyUp(key)); + } + } SDL_EventType::QUIT => { return Some(host::Message::Quit); } @@ -244,16 +378,26 @@ impl Host { } } +/// An audio output stream. Null when the host has no usable audio device, in +/// which case writes are discarded — a machine without sound shouldn't stop a +/// program from running. pub struct AudioStream(*mut sdl::audio::SDL_AudioStream); unsafe impl Send for AudioStream {} impl AudioStream { pub fn queued_bytes(&self) -> u32 { + if self.0.is_null() { + // Claim the queue is always full so the mixer doesn't spin + // producing audio nobody will hear. + return u32::MAX; + } unsafe { sdl::audio::SDL_GetAudioStreamQueued(self.0) as u32 } } pub fn put_data(&self, data: &[u8]) { - // self.0.get().put_data(data).unwrap(); + if self.0.is_null() { + return; + } unsafe { check(sdl::audio::SDL_PutAudioStreamData( self.0, @@ -264,6 +408,9 @@ impl AudioStream { } pub fn resume(&self) { + if self.0.is_null() { + return; + } unsafe { check(sdl::audio::SDL_ResumeAudioStreamDevice(self.0)); } @@ -293,6 +440,10 @@ impl Host { None, // no callback std::ptr::null_mut(), // no userdata ); + if stream.is_null() { + let err = std::ffi::CStr::from_ptr(sdl::error::SDL_GetError()); + log::warn!("no audio output: {}", err.to_string_lossy()); + } AudioStream(stream) } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ac403e08..b175d2ed 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,7 +10,7 @@ mod registers; pub use exe::EXEData; pub use flags::Flags; -pub use machine::{CPU, Context}; +pub use machine::{BlockMap, CPU, Context}; pub use mapping::{Mapping, Mappings, round_to_page}; pub use memory::Memory; pub use ops::*; diff --git a/runtime/src/machine.rs b/runtime/src/machine.rs index 3e0ed61e..1139f728 100644 --- a/runtime/src/machine.rs +++ b/runtime/src/machine.rs @@ -21,12 +21,77 @@ impl CPU { } } +/// Address -> block lookup. +/// +/// The generated block table is sorted, so a binary search over it works, but +/// indirect jumps are on the hot path of every call through a function pointer +/// or COM vtable. This turns the search's chain of dependent loads into a +/// single probe in the common case. +pub struct BlockMap { + /// Power-of-two sized, holding indices into `blocks`, or EMPTY. + slots: Box<[u32]>, + mask: u32, + blocks: &'static [(u32, ContFn)], +} + +impl BlockMap { + const EMPTY: u32 = u32::MAX; + + /// The lookup table for the program's blocks, built once and shared by + /// every thread. + pub fn get_or_init(blocks: &'static [(u32, ContFn)]) -> &'static BlockMap { + static MAP: std::sync::OnceLock = std::sync::OnceLock::new(); + MAP.get_or_init(|| BlockMap::new(blocks)) + } + + fn new(blocks: &'static [(u32, ContFn)]) -> Self { + // A load factor of at most 1/2 keeps probe chains short. + let capacity = (blocks.len() * 2).next_power_of_two().max(2); + let mask = capacity as u32 - 1; + let mut slots = vec![Self::EMPTY; capacity].into_boxed_slice(); + for (index, &(addr, _)) in blocks.iter().enumerate() { + let mut slot = Self::hash(addr) & mask; + while slots[slot as usize] != Self::EMPTY { + slot = (slot + 1) & mask; + } + slots[slot as usize] = index as u32; + } + BlockMap { + slots, + mask, + blocks, + } + } + + /// Fibonacci hashing. Code addresses are dense and share their high bits, + /// so the multiply is what spreads them across the table. + fn hash(addr: u32) -> u32 { + addr.wrapping_mul(0x9E37_79B9) + } + + pub fn get(&self, addr: u32) -> Option { + let mut slot = Self::hash(addr) & self.mask; + loop { + let index = self.slots[slot as usize]; + if index == Self::EMPTY { + return None; + } + let (block_addr, func) = self.blocks[index as usize]; + if block_addr == addr { + return Some(func); + } + slot = (slot + 1) & self.mask; + } + } +} + pub struct Context { pub cpu: CPU, pub thread_handle: u32, pub thread_id: u32, pub memory: Memory, pub blocks: &'static [(u32, ContFn)], + pub block_map: &'static BlockMap, pub recent: [ContFn; 4], } @@ -47,8 +112,7 @@ impl Context { self.dump(); panic!("jmp to null ptr"); } - // TODO: this would be faster as a hash table, or even a perfect hash if we really cared. - let Ok(index) = self.blocks.binary_search_by_key(&addr, |(addr, _)| *addr) else { + let Some(func) = self.block_map.get(addr) else { self.dump(); crate::log_missing_addr(addr); panic!( @@ -56,7 +120,7 @@ impl Context { re-run tc with --entry-points-file (see THESEUS_MISSING_ADDRS)" ); }; - Cont(self.blocks[index].1) + Cont(func) } pub fn proc_addr(&mut self, func: ContFn) -> u32 { diff --git a/tc/src/codegen/mod.rs b/tc/src/codegen/mod.rs index dd22f534..f9742bc1 100644 --- a/tc/src/codegen/mod.rs +++ b/tc/src/codegen/mod.rs @@ -355,6 +355,17 @@ out.copy_from_slice(bytes);", } self.line("}"); } + for (dll, func) in &module.dynamic_exports { + let addr = module + .imports + .iter() + .find(|imp| imp.dll == *dll && imp.func == *func) + .expect("dynamic export without a reserved address") + .addr; + self.line(format!( + "winapi::kernel32::register_export({dll:?}, {func:?}, {addr:#x});" + )); + } } Module::DOS(module) => { self.line(format!(" diff --git a/tc/src/exe.rs b/tc/src/exe.rs index ef06038d..99d7b771 100644 --- a/tc/src/exe.rs +++ b/tc/src/exe.rs @@ -83,6 +83,7 @@ fn load_pe(mem: &mut Memory, buf: &[u8], f: exe::PE) -> WindowsModule { code_memory: code_range.unwrap(), resources, vtables: Default::default(), + dynamic_exports: Default::default(), } } diff --git a/tc/src/gather.rs b/tc/src/gather.rs index 70023b21..0ea3a48f 100644 --- a/tc/src/gather.rs +++ b/tc/src/gather.rs @@ -24,8 +24,9 @@ fn is_abs_memory_ref(instr: &iced_x86::Instruction) -> Option { /// If the instruction looks like a switch dispatch /// jmp/call [reg*4 + table] -/// where table is a constant, return the address of the table. -fn is_jump_table_ref(instr: &iced_x86::Instruction) -> Option { +/// where table is a constant, return the address of the table and the register +/// indexing it. +fn is_jump_table_ref(instr: &iced_x86::Instruction) -> Option<(u32, iced_x86::Register)> { let iced_x86::OpKind::Memory = instr.op0_kind() else { return None; }; @@ -42,7 +43,33 @@ fn is_jump_table_ref(instr: &iced_x86::Instruction) -> Option { if table < 0x1000 { return None; } - Some(table) + Some((table, instr.memory_index())) +} + +/// If the instruction bounds a register to a small range — `and reg, mask` or +/// `cmp reg, limit` — return how many values it can then hold. Compilers emit +/// one of these right before a switch dispatch, which tells us exactly how long +/// the jump table is. +fn is_index_bound(instr: &iced_x86::Instruction) -> Option<(iced_x86::Register, usize)> { + use iced_x86::Mnemonic::*; + if !matches!(instr.mnemonic(), And | Cmp) { + return None; + } + if instr.op0_kind() != iced_x86::OpKind::Register { + return None; + } + let imm = match instr.op1_kind() { + iced_x86::OpKind::Immediate8 => instr.immediate8() as u32, + iced_x86::OpKind::Immediate8to32 | iced_x86::OpKind::Immediate32 => instr.immediate32(), + _ => return None, + }; + // An `and` masks to 0..=mask, a `cmp` guards indices 0..=limit; both give + // the same count. Anything large is not a switch bound. + let count = (imm as usize).checked_add(1)?; + if count > 1024 { + return None; + } + Some((instr.op0_register().full_register(), count)) } #[derive(Clone, Copy, PartialEq, PartialOrd)] @@ -374,30 +401,77 @@ impl<'a> Traverse<'a> { /// Read a jump table: consecutive pointers into code, stopping at the first /// entry that doesn't look like one. Returns the number of entries found. - fn scan_jump_table(&mut self, table: u32) -> usize { + /// Queue the targets of a switch jump table. + /// + /// `known_len` comes from a bounds check before the dispatch, when there + /// was one. Knowing the length matters: without it we have to stop at the + /// first entry that doesn't look like code, and compilers happily place a + /// table whose first slot is unreachable padding. + fn scan_jump_table(&mut self, table: u32, known_len: Option) -> usize { if !self.seen_tables.insert(table) { return 0; } + // Entries usually run forward from the displacement, but MSVC also + // emits tables indexed by a negative register — `sub ecx, 4; jb ...; + // jmp [ecx*4 + table]` reaches table[-4..-1] — so look both ways. + let forward = self.scan_jump_table_from(table, 1, known_len); + let backward = self.scan_jump_table_from(table, -1, known_len); + forward + backward + } + + /// Read consecutive code pointers from `table`, stepping by `direction` + /// entries. Without a known length, stops at the first value that isn't + /// plausible code; with one, reads exactly that many and skips the rest. + fn scan_jump_table_from( + &mut self, + table: u32, + direction: i32, + known_len: Option, + ) -> usize { let code = self.module.code_memory(); + let limit = known_len.unwrap_or(2048); let mut addr = table; let mut count = 0; - while count < 2048 { + let mut found = 0; + while count < limit { + if direction < 0 { + let Some(prev) = addr.checked_sub(4) else { break }; + addr = prev; + } if addr as usize + 4 > self.mem.bytes.len() { break; } let target = self.mem.read::(addr); - if !code.contains(&target) || !self.looks_like_code(target) { + let valid = code.contains(&target) && self.looks_like_code(target); + if !valid && known_len.is_none() { break; } - self.queue.enqueue(self.module.local_addr(target)); - addr += 4; + if valid { + if direction > 0 { + self.queue.enqueue(self.module.local_addr(target)); + } else { + // Backwards we may be reading the code that precedes a + // normal table, so treat these as candidates: they get + // dropped if they'd land inside a block we already know. + self.add_candidate(target); + } + found += 1; + } + if direction > 0 { + addr += 4; + } count += 1; } - if count > 0 { + if found > 0 { // Mark the table as data so prologue scanning doesn't look inside it. - self.data_ranges.push(table..addr); + let range = if direction > 0 { + table..addr + } else { + addr..table + }; + self.data_ranges.push(range); } - count + found } /// A `call [addr]` through a non-IAT slot: if the slot statically holds a @@ -425,7 +499,10 @@ impl<'a> Traverse<'a> { // Code addresses noticed along the way, processed after the decode loop // (decoding borrows self.mem). - let mut found_tables: Vec = Vec::new(); + // (table address, entry count if a bounds check revealed it) + let mut found_tables: Vec<(u32, Option)> = Vec::new(); + // Index bounds seen so far in this block, keyed by register. + let mut index_bounds: HashMap = HashMap::new(); let mut found_slots: Vec = Vec::new(); let mut found_imms: Vec = Vec::new(); @@ -448,6 +525,10 @@ impl<'a> Traverse<'a> { anyhow::bail!("'out' instruction in non-DOS code"); } + if let Some((reg, count)) = is_index_bound(&instr) { + index_bounds.insert(reg, count); + } + let new_instr = instrs.push_mut(Instr { ip, iced: instr, @@ -506,9 +587,10 @@ impl<'a> Traverse<'a> { log::warn!("{ip} {instr} ; indirect via memory"); } } else if !self.module.segment_addressed() - && let Some(table) = is_jump_table_ref(&instr) + && let Some((table, index)) = is_jump_table_ref(&instr) { - found_tables.push(table); + let count = index_bounds.get(&index.full_register()).copied(); + found_tables.push((table, count)); } else { log::warn!("{ip} {instr} ; indirect via memory"); } @@ -535,8 +617,8 @@ impl<'a> Traverse<'a> { break; } - for table in found_tables { - let n = self.scan_jump_table(table); + for (table, count) in found_tables { + let n = self.scan_jump_table(table, count); log::info!("jump table at {table:08x}: {n} entries"); } for slot in found_slots { diff --git a/tc/src/lib.rs b/tc/src/lib.rs index 9b81b50f..f9b1b749 100644 --- a/tc/src/lib.rs +++ b/tc/src/lib.rs @@ -31,6 +31,8 @@ pub struct WindowsModule { pub resources: Option>, pub imports: Vec, pub vtables: Vec<(String, u32)>, + /// (dll, function) pairs the program may resolve through GetProcAddress. + pub dynamic_exports: Vec<(String, String)>, } #[derive(Debug, Clone)] @@ -195,7 +197,9 @@ impl State { Ok(()) } - /// For any dll used by the module, write its vtables to the executable memory. + /// For any dll used by the module, reserve executable-memory addresses for + /// the things it can't import statically: COM vtable entries, and the + /// functions it may look up through GetProcAddress. fn add_vtables(&mut self) -> u32 { let Module::Windows(module) = &mut self.module else { unreachable!() @@ -227,6 +231,37 @@ impl State { } } } + + for (dll, funcs) in winapi::DYNAMIC_EXPORTS { + if !module.imports.iter().any(|imp| imp.dll == *dll) { + continue; + } + for func in funcs.iter() { + module.dynamic_exports.push((dll.to_string(), func.to_string())); + // A function already imported statically has an address + // already; only the rest need one reserved. + if module + .imports + .iter() + .any(|imp| imp.dll == *dll && imp.func == *func) + { + continue; + } + if addr == 0 { + addr = self.mem.mappings.alloc("vtables".into(), 0x1000); + assert!(addr != 0); + } + module.imports.push(Import { + dll: dll.to_string(), + func: func.to_string(), + iat_addr: addr, + addr: 0, + data: false, + }); + addr += 4; + } + } + addr } diff --git a/win32/winapi/src/ddraw/ddraw.rs b/win32/winapi/src/ddraw/ddraw.rs index a7b9f2f7..0b0ea3d1 100644 --- a/win32/winapi/src/ddraw/ddraw.rs +++ b/win32/winapi/src/ddraw/ddraw.rs @@ -51,11 +51,18 @@ impl DirectDraw { }; drop(window); - let bytes_per_pixel = if is_primary { - self.bytes_per_pixel + // An offscreen surface takes the display mode's format unless the app + // asks for a specific one, which is what lets a palettized game blit + // between its buffers without conversion. + let bytes_per_pixel = if desc.dwFlags.contains(DDSD::PIXELFORMAT) { + let bits = desc.ddpfPixelFormat.dwRGBBitCount; + if bits == 0 { + self.bytes_per_pixel + } else { + bits.div_ceil(8) + } } else { - log::warn!("creating surface assuming 32bpp"); - 4 + self.bytes_per_pixel }; let surface = self.create_one_surface( @@ -68,6 +75,19 @@ impl DirectDraw { }, ); + if desc.dwFlags.contains(DDSD::CKSRCBLT) { + surface.borrow_mut().src_color_key = Some(ColorKey { + low: desc.ddckCKSrcBlt.dwColorSpaceLowValue, + high: desc.ddckCKSrcBlt.dwColorSpaceHighValue, + }); + } + if desc.dwFlags.contains(DDSD::CKDESTBLT) { + surface.borrow_mut().dst_color_key = Some(ColorKey { + low: desc.ddckCKDestBlt.dwColorSpaceLowValue, + high: desc.ddckCKDestBlt.dwColorSpaceHighValue, + }); + } + if let Some(count) = desc.back_buffer_count() { assert_eq!(count, 1); let back = self.create_one_surface( @@ -108,6 +128,8 @@ impl DirectDraw { attached: Default::default(), pixels: None, palette: None, + src_color_key: None, + dst_color_key: None, })); // TODO: move surf to ddraw state().surf.borrow_mut().insert(addr, surf.clone()); @@ -120,6 +142,20 @@ pub enum Target { Texture(host::Surface), } +/// A DDCOLORKEY: the inclusive range of pixel values a blit treats as +/// transparent. +#[derive(Copy, Clone, Debug)] +pub struct ColorKey { + pub low: u32, + pub high: u32, +} + +impl ColorKey { + pub fn matches(&self, pixel: u32) -> bool { + (self.low..=self.high).contains(&pixel) + } +} + pub struct Surface { pub addr: u32, pub width: u32, @@ -138,6 +174,13 @@ pub struct Surface { pub pixels: Option, pub palette: Option>>, + + /// Pixel values that read as transparent when this surface is the source + /// of a blit — how sprites get their transparent background. + pub src_color_key: Option, + /// Pixel values that may be overwritten when this surface is the + /// destination of a blit. + pub dst_color_key: Option, } impl Surface { @@ -156,26 +199,30 @@ impl Surface { } pub fn unlock(&mut self, mem: &mut Memory) { - self.update_texture(mem, &None); + match self.target { + // Writes to the primary surface go straight to the screen. + Target::Window(_) => self.present(mem), + Target::Texture(_) => self.update_texture(mem, &None), + } } - // App can write pixels to back buffer but attach palette to front buffer, - // so take palette as an argument. - fn update_texture(&mut self, mem: &mut Memory, palette: &Option>>) { - let Some(addr) = self.pixels else { - return; - }; + /// Convert this surface's pixels to the RGBA the host wants, using + /// `palette` for palettized formats. Borrows the pixels directly when they + /// are already RGBA. Returns None when there is nothing to show, e.g. an + /// 8-bit surface with no palette attached yet. + fn to_rgba<'a>( + &self, + mem: &'a Memory, + palette: &Option>>, + ) -> Option> { + let addr = self.pixels?; let size = self.width * self.height * self.bytes_per_pixel; let pixels = &mem[addr..][..size as usize]; - let mut buf = vec![]; - let pixels32 = match self.bytes_per_pixel { + Some(match self.bytes_per_pixel { 1 => { - let Some(palette) = palette.as_ref() else { - // e.g. unlock on a back buffer with no palette attached - return; - }; - let palette = palette.borrow(); - let entries = &palette.entries; + let palette = palette.as_ref()?; + let entries = &palette.borrow().entries; + let mut buf = Vec::with_capacity(pixels.len() * 4); for &p in pixels { let entry = &entries[p as usize]; // ABGR8888 layout: R,G,B,A in byte order. @@ -184,15 +231,42 @@ impl Surface { buf.push(entry.peBlue); buf.push(0); } - buf.as_slice() + buf.into() + } + 2 => { + // RGB565, the standard 16-bit display format. + let mut buf = Vec::with_capacity(pixels.len() * 2); + for pixel in pixels.chunks_exact(2) { + let pixel = u16::from_le_bytes([pixel[0], pixel[1]]); + let (r, g, b) = (pixel >> 11, (pixel >> 5) & 0x3f, pixel & 0x1f); + // Replicate the high bits into the low ones so full-scale + // values stay full-scale. + buf.push((r << 3 | r >> 2) as u8); + buf.push((g << 2 | g >> 4) as u8); + buf.push((b << 3 | b >> 2) as u8); + buf.push(0); + } + buf.into() + } + 4 => pixels.into(), + bpp => { + log::warn!("unsupported surface format: {bpp} bytes per pixel"); + return None; } - 4 => pixels, - _ => todo!(), + }) + } + + // App can write pixels to back buffer but attach palette to front buffer, + // so take palette as an argument. + fn update_texture(&mut self, mem: &mut Memory, palette: &Option>>) { + let Some(pixels) = self.to_rgba(mem, palette) else { + return; }; + let width = self.width; match &mut self.target { Target::Window(_) => unreachable!(), Target::Texture(texture) => { - texture.set_pixels(pixels32, self.width * 4); + texture.set_pixels(&pixels, width * 4); } } } @@ -201,9 +275,6 @@ impl Surface { /// when an app draws directly to the primary surface (via Lock or Blt) /// instead of flipping. No-op for non-primary surfaces. pub fn present(&mut self, mem: &mut Memory) { - let Some(addr) = self.pixels else { - return; - }; let Target::Window(window) = &self.target else { return; }; @@ -211,34 +282,14 @@ impl Surface { let Some(back) = self.attached.clone() else { return; }; - let size = self.width * self.height * self.bytes_per_pixel; - let pixels = &mem[addr..][..size as usize]; - let mut buf = vec![]; - let pixels32: &[u8] = match self.bytes_per_pixel { - 1 => { - let Some(palette) = self.palette.as_ref() else { - return; - }; - let palette = palette.borrow(); - let entries = &palette.entries; - for &p in pixels { - let entry = &entries[p as usize]; - // ABGR8888 layout: R,G,B,A in byte order. - buf.push(entry.peRed); - buf.push(entry.peGreen); - buf.push(entry.peBlue); - buf.push(0); - } - buf.as_slice() - } - 4 => pixels, - _ => return, + let Some(pixels) = self.to_rgba(mem, &self.palette) else { + return; }; let mut back = back.borrow_mut(); let Target::Texture(texture) = &mut back.target else { return; }; - texture.set_pixels(pixels32, self.width * 4); + texture.set_pixels(&pixels, self.width * 4); window.borrow_mut().host.render(texture); } diff --git a/win32/winapi/src/ddraw/ddraw1.rs b/win32/winapi/src/ddraw/ddraw1.rs index 96a4998d..6e00636f 100644 --- a/win32/winapi/src/ddraw/ddraw1.rs +++ b/win32/winapi/src/ddraw/ddraw1.rs @@ -5,7 +5,7 @@ use zerocopy::{FromBytes, IntoBytes}; use crate::{ RECT, - ddraw::{DD, Palette, get_pixel_format, state, types::*}, + ddraw::{ColorKey, DD, GUID, Palette, get_pixel_format, state, types::*}, heap::Heap, kernel32, stub, user32::HWND, @@ -41,18 +41,22 @@ pub mod IDirectDraw { ]; #[win32_derive::dllexport] - pub fn QueryInterface(_ctx: &mut Context, _this: u32, _riid: u32, _ppvObject: u32) -> DD { - todo!() + pub fn QueryInterface(ctx: &mut Context, _this: u32, riid: u32, _ppvObject: u32) -> DD { + let iid = crate::Ptr::::new(riid).read(&ctx.memory); + log::warn!("IDirectDraw::QueryInterface({iid:?}): not supported"); + DD::E_NOINTERFACE } #[win32_derive::dllexport] pub fn AddRef(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + // We don't reference count; the single DirectDraw object lives as long + // as the process. + 1 } #[win32_derive::dllexport] pub fn Release(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + 0 } #[win32_derive::dllexport] @@ -230,8 +234,10 @@ pub mod IDirectDraw { } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn Initialize(_ctx: &mut Context, _this: u32, _lpDD: u32, _dwFlags: u32, _lpDDColorTable: u32) -> DD { + // Palettes are fully constructed by CreatePalette. + const DDERR_ALREADYINITIALIZED: DD = DD::OK; + DDERR_ALREADYINITIALIZED } #[win32_derive::dllexport] @@ -316,18 +322,31 @@ pub mod IDirectDrawSurface { ]; #[win32_derive::dllexport] - pub fn QueryInterface(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn QueryInterface(ctx: &mut Context, _this: u32, riid: u32, _ppvObject: u32) -> DD { + let iid = crate::Ptr::::new(riid).read(&ctx.memory); + log::warn!("IDirectDrawSurface::QueryInterface({iid:?}): not supported"); + DD::E_NOINTERFACE } #[win32_derive::dllexport] pub fn AddRef(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + // Surfaces aren't reference counted here: a surface lives until the + // app releases it, and an app that balances AddRef/Release keeps at + // least one reference until then anyway. + 1 } #[win32_derive::dllexport] - pub fn Release(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn Release(ctx: &mut Context, this: u32) -> u32 { + let Some(surface) = state().surf.borrow_mut().remove(&this) else { + return 0; + }; + // Games recreate surfaces when changing screens, so returning the + // pixels keeps the heap from growing without bound. + if let Some(pixels) = surface.borrow_mut().pixels.take() { + kernel32::lock().process_heap.free(&mut ctx.memory, pixels); + } + 0 } #[win32_derive::dllexport] @@ -363,12 +382,16 @@ pub mod IDirectDrawSurface { /// Copy a rect between two surfaces (which may be the same one; the copy /// stages through a temporary buffer). - fn blit_copy( + /// + /// With a `color_key`, source pixels inside its range are left alone in the + /// destination, which is how sprites get transparent backgrounds. + pub fn blit_copy( ctx: &mut Context, dst_ptr: u32, dst_rect: Option, src_ptr: u32, src_rect: Option, + color_key: Option, ) { let src_rc = state().surf.borrow_mut().get(&src_ptr).unwrap().clone(); let dst_rc = state().surf.borrow_mut().get(&dst_ptr).unwrap().clone(); @@ -410,8 +433,24 @@ pub mod IDirectDrawSurface { let copy_rows = row_count.min((rect.bottom - rect.top).max(0) as usize); for i in 0..copy_rows { let dst_start = addr + (rect.top + i as i32) as u32 * stride + rect.left as u32 * bpp; - ctx.memory[dst_start..][..copy_bytes] - .copy_from_slice(&rows[i * row_bytes..][..copy_bytes]); + let row = &rows[i * row_bytes..][..copy_bytes]; + match color_key { + None => ctx.memory[dst_start..][..copy_bytes].copy_from_slice(row), + Some(key) => { + for (x, pixel) in row.chunks_exact(bpp as usize).enumerate() { + let value = match bpp { + 1 => pixel[0] as u32, + 4 => u32::from_le_bytes(pixel.try_into().unwrap()), + _ => unreachable!("blit bpp {bpp}"), + }; + if key.matches(value) { + continue; + } + let at = dst_start + x as u32 * bpp; + ctx.memory[at..][..bpp as usize].copy_from_slice(pixel); + } + } + } } dst.present(&mut ctx.memory); } @@ -427,8 +466,10 @@ pub mod IDirectDrawSurface { lpDDBLTFX: u32, ) -> DD { const DDBLT_COLORFILL: u32 = 0x0400; + const DDBLT_KEYSRC: u32 = 0x8000; + const DDBLT_KEYSRCOVERRIDE: u32 = 0x0001_0000; const DDBLT_WAIT: u32 = 0x0100_0000; - const KNOWN: u32 = DDBLT_COLORFILL | DDBLT_WAIT; + const KNOWN: u32 = DDBLT_COLORFILL | DDBLT_KEYSRC | DDBLT_KEYSRCOVERRIDE | DDBLT_WAIT; if dwFlags & !KNOWN != 0 { log::warn!("Blt: ignoring flags {:#x}", dwFlags & !KNOWN); } @@ -464,11 +505,32 @@ pub mod IDirectDrawSurface { return DD::OK; } + let color_key = if dwFlags & DDBLT_KEYSRCOVERRIDE != 0 { + // DDBLTFX.ddckSrcColorkey is at offset 16. + Some(ColorKey { + low: ctx.memory.read::(lpDDBLTFX + 16), + high: ctx.memory.read::(lpDDBLTFX + 20), + }) + } else if dwFlags & DDBLT_KEYSRC != 0 { + surface_src_color_key(lpDDSrcSurface) + } else { + None + }; + let src_rect = read_rect(ctx, lpSrcRect); - blit_copy(ctx, this, dst_rect, lpDDSrcSurface, src_rect); + blit_copy(ctx, this, dst_rect, lpDDSrcSurface, src_rect, color_key); DD::OK } + fn surface_src_color_key(surface: u32) -> Option { + let surfaces = state().surf.borrow(); + let key = surfaces.get(&surface)?.borrow().src_color_key; + if key.is_none() { + log::warn!("blit asked for a source color key, but none is set"); + } + key + } + #[win32_derive::dllexport] pub fn BltBatch(_ctx: &mut Context, _this: u32) -> DD { todo!() @@ -484,10 +546,24 @@ pub mod IDirectDrawSurface { lpSrcRect: u32, dwTrans: u32, ) -> DD { - if dwTrans & !0x10 != 0 { - // e.g. DDBLTFAST_SRCCOLORKEY; transparency not implemented yet. - log::warn!("BltFast: ignoring flags {dwTrans:#x}"); + const DDBLTFAST_SRCCOLORKEY: u32 = 0x0001; + const DDBLTFAST_DESTCOLORKEY: u32 = 0x0002; + const DDBLTFAST_WAIT: u32 = 0x0010; + const KNOWN: u32 = DDBLTFAST_SRCCOLORKEY | DDBLTFAST_DESTCOLORKEY | DDBLTFAST_WAIT; + if dwTrans & !KNOWN != 0 { + log::warn!("BltFast: ignoring flags {:#x}", dwTrans & !KNOWN); } + if dwTrans & DDBLTFAST_DESTCOLORKEY != 0 { + // Would need to test the destination pixel rather than the source; + // no caller has needed it. + log::warn!("BltFast: destination color key not supported"); + } + let color_key = if dwTrans & DDBLTFAST_SRCCOLORKEY != 0 { + surface_src_color_key(lpDDSrcSurface) + } else { + None + }; + let src_rect = read_rect(ctx, lpSrcRect); let (w, h) = match &src_rect { Some(r) => ((r.right - r.left).max(0), (r.bottom - r.top).max(0)), @@ -503,7 +579,7 @@ pub mod IDirectDrawSurface { right: dwX as i32 + w, bottom: dwY as i32 + h, }; - blit_copy(ctx, this, Some(dst_rect), lpDDSrcSurface, src_rect); + blit_copy(ctx, this, Some(dst_rect), lpDDSrcSurface, src_rect, color_key); DD::OK } @@ -529,9 +605,14 @@ pub mod IDirectDrawSurface { _lpDDSurfaceTargetOverride: u32, _dwFlags: u32, ) -> DD { - let surfaces = state().surf.borrow_mut(); - let mut surface = surfaces.get(&this).unwrap().borrow_mut(); - surface.flip(&mut ctx.memory); + { + let surfaces = state().surf.borrow_mut(); + let mut surface = surfaces.get(&this).unwrap().borrow_mut(); + surface.flip(&mut ctx.memory); + } + // A frame flip is the one thing a game does every frame no matter what + // it's doing, so it's where we keep the audio mixer fed. + crate::dsound::pump(ctx); DD::OK } @@ -569,8 +650,25 @@ pub mod IDirectDrawSurface { } #[win32_derive::dllexport] - pub fn GetColorKey(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn GetColorKey(ctx: &mut Context, this: u32, dwFlags: u32, lpDDColorKey: u32) -> DD { + let key = { + let surfaces = state().surf.borrow(); + let Some(surface) = surfaces.get(&this) else { + return DD::ERR_GENERIC; + }; + let surface = surface.borrow(); + if dwFlags & DDCKEY_DESTBLT != 0 { + surface.dst_color_key + } else { + surface.src_color_key + } + }; + let Some(key) = key else { + return DD::ERR_NOCOLORKEY; + }; + ctx.memory.write::(lpDDColorKey, key.low); + ctx.memory.write::(lpDDColorKey + 4, key.high); + DD::OK } #[win32_derive::dllexport] @@ -635,8 +733,10 @@ pub mod IDirectDrawSurface { } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn Initialize(_ctx: &mut Context, _this: u32, _lpDD: u32, _dwFlags: u32, _lpDDColorTable: u32) -> DD { + // Palettes are fully constructed by CreatePalette. + const DDERR_ALREADYINITIALIZED: DD = DD::OK; + DDERR_ALREADYINITIALIZED } #[win32_derive::dllexport] @@ -684,8 +784,30 @@ pub mod IDirectDrawSurface { } #[win32_derive::dllexport] - pub fn SetColorKey(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn SetColorKey(ctx: &mut Context, this: u32, dwFlags: u32, lpDDColorKey: u32) -> DD { + let key = if lpDDColorKey == 0 { + None + } else { + // DDCOLORKEY: dwColorSpaceLowValue, dwColorSpaceHighValue. + Some(ColorKey { + low: ctx.memory.read::(lpDDColorKey), + high: ctx.memory.read::(lpDDColorKey + 4), + }) + }; + let surfaces = state().surf.borrow(); + let Some(surface) = surfaces.get(&this) else { + return DD::ERR_GENERIC; + }; + let mut surface = surface.borrow_mut(); + if dwFlags & (DDCKEY_SRCOVERLAY | DDCKEY_DESTOVERLAY) != 0 { + log::warn!("SetColorKey: overlays are not supported"); + } + if dwFlags & DDCKEY_DESTBLT != 0 { + surface.dst_color_key = key; + } else { + surface.src_color_key = key; + } + DD::OK } #[win32_derive::dllexport] @@ -752,33 +874,62 @@ pub mod IDirectDrawPalette { ]; #[win32_derive::dllexport] - pub fn QueryInterface(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn QueryInterface(ctx: &mut Context, _this: u32, riid: u32, _ppvObject: u32) -> DD { + let iid = crate::Ptr::::new(riid).read(&ctx.memory); + log::warn!("IDirectDrawPalette::QueryInterface({iid:?}): not supported"); + DD::E_NOINTERFACE } #[win32_derive::dllexport] - pub fn AddRef(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn AddRef(_ctx: &mut Context, _this: u32) -> u32 { + 1 } #[win32_derive::dllexport] - pub fn Release(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn Release(ctx: &mut Context, this: u32) -> u32 { + // Surfaces hold their own reference to the palette, so dropping it from + // the table doesn't disturb anything still displaying it. + state().palette.borrow_mut().remove(&this); + kernel32::lock().process_heap.free(&mut ctx.memory, this); + 0 } #[win32_derive::dllexport] - pub fn GetCaps(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn GetCaps(ctx: &mut Context, _this: u32, lpdwCaps: u32) -> DD { + // We only ever create 8-bit palettes with all 256 entries settable. + let caps = DDPCAPS::_8BIT | DDPCAPS::ALLOW256; + ctx.memory.write::(lpdwCaps, caps.bits()); + DD::OK } #[win32_derive::dllexport] - pub fn GetEntries(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn GetEntries( + ctx: &mut Context, + this: u32, + _dwFlags: u32, + dwBase: u32, + dwNumEntries: u32, + lpEntries: u32, + ) -> DD { + let palettes = state().palette.borrow(); + let Some(palette) = palettes.get(&this) else { + return DD::ERR_GENERIC; + }; + let palette = palette.borrow(); + for i in 0..dwNumEntries { + let Some(entry) = palette.entries.get((dwBase + i) as usize) else { + break; + }; + ctx.memory.write(lpEntries + i * 4, entry.clone()); + } + DD::OK } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32) -> DD { - todo!() + pub fn Initialize(_ctx: &mut Context, _this: u32, _lpDD: u32, _dwFlags: u32, _lpDDColorTable: u32) -> DD { + // Palettes are fully constructed by CreatePalette. + const DDERR_ALREADYINITIALIZED: DD = DD::OK; + DDERR_ALREADYINITIALIZED } #[win32_derive::dllexport] diff --git a/win32/winapi/src/ddraw/ddraw7.rs b/win32/winapi/src/ddraw/ddraw7.rs index f62f3358..223b8946 100644 --- a/win32/winapi/src/ddraw/ddraw7.rs +++ b/win32/winapi/src/ddraw/ddraw7.rs @@ -375,15 +375,25 @@ pub mod IDirectDrawSurface7 { #[win32_derive::dllexport] pub fn Blt( - _ctx: &mut Context, - _this: u32, - _lpDestRect: u32, - _lpDDSrcSurface: u32, - _lpSrcRect: u32, - _dwFlags: u32, - _lpDDBltFx: u32, + ctx: &mut Context, + this: u32, + lpDestRect: u32, + lpDDSrcSurface: u32, + lpSrcRect: u32, + dwFlags: u32, + lpDDBltFx: u32, ) -> DD { - todo!() + // Surfaces are shared between interface versions, so the version 1 + // implementation covers both. + crate::ddraw::ddraw1::IDirectDrawSurface::Blt( + ctx, + this, + lpDestRect, + lpDDSrcSurface, + lpSrcRect, + dwFlags, + lpDDBltFx, + ) } #[win32_derive::dllexport] @@ -405,44 +415,24 @@ pub mod IDirectDrawSurface7 { dwY: u32, lpDDSrcSurface: u32, lpSrcRect: Ptr, - _dwTrans: u32, + dwTrans: u32, ) -> DD { - let surfaces = state().surf.borrow_mut(); - let mut dst_surface = surfaces.get(&this).unwrap().borrow_mut(); - let mut src_surface = surfaces.get(&lpDDSrcSurface).unwrap().borrow_mut(); - - assert_eq!(src_surface.bytes_per_pixel, 4); - assert_eq!(dst_surface.bytes_per_pixel, 4); - let bytes_per_pixel = src_surface.bytes_per_pixel; - let src_stride = src_surface.width * bytes_per_pixel; - let dst_stride = dst_surface.width * bytes_per_pixel; - - let src_rect = lpSrcRect.read(&ctx.memory).unwrap(); - let src_addr = src_surface.lock(&mut ctx.memory); - let dst_addr = dst_surface.lock(&mut ctx.memory); - - let [src_pixels, dst_pixels] = ctx - .memory - .bytes - .get_disjoint_mut([ - src_addr as usize..(src_addr + src_surface.height * src_stride) as usize, - dst_addr as usize..(dst_addr + dst_surface.height * dst_stride) as usize, - ]) - .unwrap(); - - let width = ((src_rect.right - src_rect.left) as u32 * bytes_per_pixel) as usize; - for (sy, dy) in (src_rect.top as u32..src_rect.bottom as u32).zip(dwY..) { - let src_row = &src_pixels[(sy * src_stride) as usize..]; - let dst_row = &mut dst_pixels[(dy * dst_stride) as usize..]; - dst_row[(dwX * bytes_per_pixel) as usize..][..width].copy_from_slice( - &src_row[(src_rect.left as u32 * bytes_per_pixel) as usize..][..width], - ); + let result = crate::ddraw::ddraw1::IDirectDrawSurface::BltFast( + ctx, + this, + dwX, + dwY, + lpDDSrcSurface, + lpSrcRect.addr, + dwTrans, + ); + // Apps on this interface draw to an offscreen surface and expect to see + // the result without flipping, so refresh its texture here. + let surfaces = state().surf.borrow(); + if let Some(surface) = surfaces.get(&this) { + surface.borrow_mut().unlock(&mut ctx.memory); } - - src_surface.unlock(&mut ctx.memory); - dst_surface.unlock(&mut ctx.memory); - - stub!(DD::OK) + result } #[win32_derive::dllexport] @@ -483,9 +473,14 @@ pub mod IDirectDrawSurface7 { _lpDDSurfaceTargetOverride: u32, _dwFlags: u32, ) -> DD { - let surfaces = state().surf.borrow_mut(); - let mut surface = surfaces.get(&this).unwrap().borrow_mut(); - surface.flip(&mut ctx.memory); + { + let surfaces = state().surf.borrow_mut(); + let mut surface = surfaces.get(&this).unwrap().borrow_mut(); + surface.flip(&mut ctx.memory); + } + // A frame flip is the one thing a game does every frame no matter what + // it's doing, so it's where we keep the audio mixer fed. + crate::dsound::pump(ctx); DD::OK } @@ -521,8 +516,8 @@ pub mod IDirectDrawSurface7 { } #[win32_derive::dllexport] - pub fn GetColorKey(_ctx: &mut Context, _this: u32, _dwFlags: u32, _lpDDColorKey: u32) -> DD { - todo!() + pub fn GetColorKey(ctx: &mut Context, this: u32, dwFlags: u32, lpDDColorKey: u32) -> DD { + crate::ddraw::ddraw1::IDirectDrawSurface::GetColorKey(ctx, this, dwFlags, lpDDColorKey) } #[win32_derive::dllexport] @@ -621,8 +616,8 @@ pub mod IDirectDrawSurface7 { } #[win32_derive::dllexport] - pub fn SetColorKey(_ctx: &mut Context, _this: u32, _dwFlags: u32, _lpDDColorKey: u32) -> DD { - todo!() + pub fn SetColorKey(ctx: &mut Context, this: u32, dwFlags: u32, lpDDColorKey: u32) -> DD { + crate::ddraw::ddraw1::IDirectDrawSurface::SetColorKey(ctx, this, dwFlags, lpDDColorKey) } #[win32_derive::dllexport] diff --git a/win32/winapi/src/ddraw/types.rs b/win32/winapi/src/ddraw/types.rs index 9d309344..88417fc4 100644 --- a/win32/winapi/src/ddraw/types.rs +++ b/win32/winapi/src/ddraw/types.rs @@ -10,8 +10,16 @@ pub enum DD { OK = 0, E_NOINTERFACE = 0x80004002, ERR_GENERIC = 0x80004005, + ERR_NOCOLORKEY = 0x887600d4, } +/// DDCKEY_* flags, selecting which of a surface's color keys an operation +/// refers to. +pub const DDCKEY_DESTOVERLAY: u32 = 0x0001; +pub const DDCKEY_DESTBLT: u32 = 0x0002; +pub const DDCKEY_SRCOVERLAY: u32 = 0x0004; +pub const DDCKEY_SRCBLT: u32 = 0x0008; + impl Into for DD { fn into(self) -> ABIReturn { (self as u32).into() @@ -135,8 +143,8 @@ win32flags! { zerocopy::IntoBytes, )] pub struct DDCOLORKEY { - dwColorSpaceLowValue: u32, - dwColorSpaceHighValue: u32, + pub dwColorSpaceLowValue: u32, + pub dwColorSpaceHighValue: u32, } #[repr(C)] @@ -490,7 +498,14 @@ win32flags! { } #[repr(C)] -#[derive(Debug, Clone, zerocopy::FromBytes, zerocopy::Immutable, zerocopy::KnownLayout)] +#[derive( + Debug, + Clone, + zerocopy::FromBytes, + zerocopy::IntoBytes, + zerocopy::Immutable, + zerocopy::KnownLayout, +)] pub struct PALETTEENTRY { pub peRed: u8, pub peGreen: u8, diff --git a/win32/winapi/src/dinput.rs b/win32/winapi/src/dinput.rs index 824e6a73..3fdf422b 100644 --- a/win32/winapi/src/dinput.rs +++ b/win32/winapi/src/dinput.rs @@ -1,12 +1,13 @@ -//! DirectInput. Devices exist and can be acquired; GetDeviceState/GetDeviceData -//! report no input until the host grows keyboard support (see the retrowin32 -//! fork's dinput for the full reference). +//! DirectInput keyboard and mouse. +//! +//! Device state comes from the shared input state in user32, which the host +//! message pump keeps up to date; see user32::input. use std::{cell::OnceCell, collections::HashMap, sync::Mutex}; use runtime::Context; -use crate::{ddraw::GUID, heap::Heap, kernel32}; +use crate::{ddraw::GUID, heap::Heap, kernel32, user32}; const GUID_SysMouse: GUID = GUID(( 0x6F1D2B60, @@ -23,7 +24,20 @@ const GUID_SysKeyboard: GUID = GUID(( )); const DI_OK: u32 = 0; +/// More events were buffered than the app's buffer could hold. +const DI_BUFFEROVERFLOW: u32 = 1; const DIERR_DEVICENOTREG: u32 = 0x80040154; +const DIERR_NOTACQUIRED: u32 = 0x80070015; +const DIERR_INVALIDPARAM: u32 = 0x80070057; + +/// DIGDD_PEEK: leave the returned events in the buffer. +const DIGDD_PEEK: u32 = 0x00000001; + +/// DirectInput property GUIDs are really small integers cast to a GUID pointer +/// (see MAKEDIPROP), so a property is identified by the pointer value itself. +const DIPROP_BUFFERSIZE: u32 = 1; +/// Offset of DIPROPDWORD::dwData, past the DIPROPHEADER. +const DIPROPDWORD_DWDATA: u32 = 16; /// Which physical device a created IDirectInputDevice stands for. #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -32,10 +46,15 @@ pub enum DeviceKind { Mouse, } +pub struct Device { + pub kind: DeviceKind, + pub acquired: bool, +} + #[derive(Default)] pub struct State { /// Maps an IDirectInputDevice interface pointer to the device it represents. - pub devices: Mutex>, + pub devices: Mutex>, } struct StaticState(OnceCell); @@ -126,7 +145,13 @@ pub mod IDirectInput { let mut kernel32 = kernel32::lock(); let device = IDirectInputDevice::new(ctx, &mut kernel32.process_heap); drop(kernel32); - state().devices.lock().unwrap().insert(device, kind); + state().devices.lock().unwrap().insert( + device, + Device { + kind, + acquired: false, + }, + ); ctx.memory.write::(lplpDirectInputDevice, device); DI_OK } @@ -201,14 +226,22 @@ pub mod IDirectInputDevice { addr } - pub fn device_kind(this: u32) -> DeviceKind { + /// The device behind an interface pointer, and whether it's acquired. + /// A device we never created reads as an unacquired keyboard. + pub fn device(this: u32) -> (DeviceKind, bool) { state() .devices .lock() .unwrap() .get(&this) - .copied() - .unwrap_or(DeviceKind::Keyboard) + .map(|device| (device.kind, device.acquired)) + .unwrap_or((DeviceKind::Keyboard, false)) + } + + fn set_acquired(this: u32, acquired: bool) { + if let Some(device) = state().devices.lock().unwrap().get_mut(&this) { + device.acquired = acquired; + } } #[win32_derive::dllexport] @@ -222,8 +255,8 @@ pub mod IDirectInputDevice { } #[win32_derive::dllexport] - pub fn Release(_ctx: &mut Context, _this: u32) -> u32 { - state().devices.lock().unwrap().remove(&_this); + pub fn Release(_ctx: &mut Context, this: u32) -> u32 { + state().devices.lock().unwrap().remove(&this); 0 } @@ -244,47 +277,141 @@ pub mod IDirectInputDevice { } #[win32_derive::dllexport] - pub fn GetProperty(_ctx: &mut Context, _this: u32, _rguidProp: u32, _pdiph: u32) -> u32 { - todo!() + pub fn GetProperty(ctx: &mut Context, this: u32, rguidProp: u32, pdiph: u32) -> u32 { + if rguidProp != DIPROP_BUFFERSIZE { + log::warn!("dinput GetProperty: unhandled property {rguidProp:#x}"); + return DIERR_INVALIDPARAM; + } + let (kind, _) = device(this); + let size = user32::state() + .input + .borrow() + .buffer_size(kind == DeviceKind::Keyboard); + ctx.memory.write::(pdiph + DIPROPDWORD_DWDATA, size as u32); + DI_OK } #[win32_derive::dllexport] - pub fn SetProperty(_ctx: &mut Context, _this: u32, _rguidProp: u32, _pdiph: u32) -> u32 { + pub fn SetProperty(ctx: &mut Context, this: u32, rguidProp: u32, pdiph: u32) -> u32 { + if rguidProp != DIPROP_BUFFERSIZE { + // Axis mode, dead zone and the force feedback properties don't + // apply to the plain keyboard/mouse we emulate. + log::warn!("dinput SetProperty: ignoring property {rguidProp:#x}"); + return DI_OK; + } + let (kind, _) = device(this); + let size = ctx.memory.read::(pdiph + DIPROPDWORD_DWDATA); + user32::state() + .input + .borrow_mut() + .set_buffer_size(kind == DeviceKind::Keyboard, size as usize); DI_OK } #[win32_derive::dllexport] - pub fn Acquire(_ctx: &mut Context, _this: u32) -> u32 { + pub fn Acquire(_ctx: &mut Context, this: u32) -> u32 { + set_acquired(this, true); DI_OK } #[win32_derive::dllexport] - pub fn Unacquire(_ctx: &mut Context, _this: u32) -> u32 { + pub fn Unacquire(_ctx: &mut Context, this: u32) -> u32 { + set_acquired(this, false); DI_OK } - /// Keyboard: a byte array indexed by DIK scancode (0x80 = pressed). - /// Mouse: DIMOUSESTATE (lX/lY/lZ relative, then button bytes). - /// No host input plumbing yet, so everything reads as idle. + /// Read immediate device state into the caller's buffer. + /// + /// Keyboard: a byte array indexed by DIK scan code (0x80 = pressed). + /// Mouse: a DIMOUSESTATE — lX/lY/lZ relative to the last read, then one + /// byte per button. #[win32_derive::dllexport] - pub fn GetDeviceState(ctx: &mut Context, _this: u32, cbData: u32, lpvData: u32) -> u32 { - ctx.memory[lpvData..][..cbData as usize].fill(0); + pub fn GetDeviceState(ctx: &mut Context, this: u32, cbData: u32, lpvData: u32) -> u32 { + let (kind, acquired) = device(this); + if !acquired { + return DIERR_NOTACQUIRED; + } + user32::pump_host_input(); + + let len = cbData as usize; + let mut buf = vec![0u8; len]; + let mut input = user32::state().input.borrow_mut(); + match kind { + DeviceKind::Keyboard => { + let keys = len.min(256); + buf[..keys].copy_from_slice(&input.dik_state()[..keys]); + } + DeviceKind::Mouse => { + let (dx, dy) = input.mouse.take_motion(); + // DIMOUSESTATE: lX, lY, lZ, then rgbButtons. + for (ofs, value) in [(0, dx), (4, dy), (8, 0)] { + if ofs + 4 <= len { + buf[ofs..ofs + 4].copy_from_slice(&value.to_le_bytes()); + } + } + for (index, &button) in input.mouse.buttons.iter().enumerate() { + if 12 + index < len { + buf[12 + index] = button; + } + } + } + } + drop(input); + ctx.memory[lpvData..][..len].copy_from_slice(&buf); DI_OK } + /// Read buffered events into the caller's DIDEVICEOBJECTDATA array. #[win32_derive::dllexport] pub fn GetDeviceData( ctx: &mut Context, - _this: u32, - _cbObjectData: u32, - _rgdod: u32, + this: u32, + cbObjectData: u32, + rgdod: u32, pdwInOut: u32, - _dwFlags: u32, + dwFlags: u32, ) -> u32 { - if pdwInOut != 0 { - ctx.memory.write::(pdwInOut, 0); // no buffered events + if pdwInOut == 0 { + return DIERR_INVALIDPARAM; + } + let (kind, acquired) = device(this); + if !acquired { + return DIERR_NOTACQUIRED; + } + user32::pump_host_input(); + + // A null array means the caller only wants the pending event count. + let capacity = if rgdod == 0 { + usize::MAX + } else { + ctx.memory.read::(pdwInOut) as usize + }; + let peek = rgdod == 0 || dwFlags & DIGDD_PEEK != 0; + let (events, overflowed) = user32::state().input.borrow_mut().take_events( + kind == DeviceKind::Keyboard, + capacity, + peek, + ); + + if rgdod != 0 { + for (i, event) in events.iter().enumerate() { + // DIDEVICEOBJECTDATA: dwOfs, dwData, dwTimeStamp, dwSequence. + // The stride comes from the caller in case it passes the + // larger DirectInput 8 struct. + let base = rgdod + i as u32 * cbObjectData; + ctx.memory.write::(base, event.ofs); + ctx.memory.write::(base + 4, event.data); + ctx.memory.write::(base + 8, event.time); + ctx.memory.write::(base + 12, event.sequence); + } + } + ctx.memory.write::(pdwInOut, events.len() as u32); + + if overflowed { + DI_BUFFEROVERFLOW + } else { + DI_OK } - DI_OK } #[win32_derive::dllexport] diff --git a/win32/winapi/src/dllexport.rs b/win32/winapi/src/dllexport.rs index 11fc7247..5b47090f 100644 --- a/win32/winapi/src/dllexport.rs +++ b/win32/winapi/src/dllexport.rs @@ -24,6 +24,13 @@ impl From for ABIReturn { } } +impl From for ABIReturn { + fn from(value: i16) -> Self { + // Sign-extends, as a function returning SHORT does. + Self(value as i32 as u32) + } +} + impl From for ABIReturn { fn from(value: bool) -> Self { Self(if value { 1 } else { 0 }) diff --git a/win32/winapi/src/dsound.rs b/win32/winapi/src/dsound.rs index 46c0868e..d7a11953 100644 --- a/win32/winapi/src/dsound.rs +++ b/win32/winapi/src/dsound.rs @@ -1,118 +1,160 @@ +//! DirectSound, backed by a software mixer. +//! +//! Apps expect many independent sound buffers playing at once, each with its +//! own format, rate, volume and pan, while the host gives us a single output +//! stream. So buffers here are just PCM in guest memory plus a playback +//! cursor, and [`pump`] mixes whatever is playing into the host stream. +//! +//! Nothing drives the mixer on its own: `pump` is called from the places the +//! app passes through every frame (the message pump and ddraw's presentation), +//! because a game that busy-waits on its own frame limiter never yields to us +//! otherwise. + use std::{collections::HashMap, sync::Mutex}; -use runtime::Context; +use runtime::{Context, Memory}; use zerocopy::FromBytes; -use crate::{dllexport::win32flags, heap::Heap, kernel32, locked_state::LockedState, stub}; +use crate::{dllexport::win32flags, heap::Heap, kernel32, locked_state::LockedState}; -/// When true, write a debug `out.wav` containing the audio data. +/// When true, write a debug `out.wav` of the mixed output. const WRITE_WAV: bool = false; -const fn make_dhsresult(code: u32) -> u32 { +/// Host mix rate, in stereo i16. +const HOST_RATE: u32 = 44100; +/// Stereo frames produced per mixer chunk (~23ms). +const CHUNK_FRAMES: usize = 1024; +/// How far ahead of playback we keep the host queue, in stereo frames (~140ms). +/// Enough slack that playback never runs dry between pumps. +const TARGET_QUEUE_FRAMES: u32 = 6144; +/// Safety cap on chunks produced in a single pump, in case a buffer is +/// misconfigured and the queue never fills. +const MAX_CHUNKS_PER_PUMP: usize = 12; +/// Bytes per stereo i16 frame. +const HOST_FRAME_BYTES: u32 = 4; + +const fn make_dserror(code: u32) -> u32 { (1 << 31) | (0x878 << 16) | code } -#[allow(dead_code)] -const DSERR_NODRIVER: u32 = make_dhsresult(120); - const DS_OK: u32 = 0; +#[allow(dead_code)] +const DSERR_NODRIVER: u32 = make_dserror(120); +const DSERR_INVALIDPARAM: u32 = 0x80070057; -struct WavWrite { - f: std::fs::File, -} +/// DSBVOLUME/DSBPAN are in hundredths of a decibel of attenuation, with 0 as +/// full volume and -10000 as silence. +const DSBVOLUME_MIN: i32 = -10000; -impl WavWrite { - fn new(path: &str) -> Self { - let f = std::fs::File::create(path).unwrap(); - let mut w = Self { f }; - w.write_header().unwrap(); - w +/// Convert an attenuation in hundredths of a decibel to an amplitude factor. +fn gain(hundredths_db: i32) -> f32 { + if hundredths_db <= DSBVOLUME_MIN { + return 0.0; } + 10f32.powf(hundredths_db as f32 / 2000.0) +} - fn write_header(&mut self) -> std::io::Result<()> { - use std::io::Write; - - use zerocopy::IntoBytes; - - #[repr(C)] - #[derive(zerocopy::IntoBytes, zerocopy::Immutable)] - struct Chunk { - id: [u8; 4], - chunk_size: u32, - } +#[derive(Clone, Copy)] +struct WaveFormat { + channels: u32, + bits: u32, + rate: u32, +} - #[repr(C)] - #[derive(zerocopy::IntoBytes, zerocopy::Immutable, Default)] - struct Fmt { - format: u16, - channels: u16, - sample_rate: u32, - byte_per_sec: u32, - byte_per_block: u16, - bits_per_sample: u16, - } - - #[repr(C)] - #[derive(zerocopy::IntoBytes, zerocopy::Immutable)] - struct Header { - file_header: Chunk, - format: [u8; 4], - fmt_header: Chunk, - fmt: Fmt, - data_header: Chunk, +impl Default for WaveFormat { + fn default() -> Self { + WaveFormat { + channels: 2, + bits: 16, + rate: 44100, } + } +} - let mut header = Header { - file_header: Chunk { - id: *b"RIFF", - chunk_size: 0xffff_ffff, - }, - format: *b"WAVE", - fmt_header: Chunk { - id: *b"fmt ", - chunk_size: std::mem::size_of::() as u32, - }, - fmt: Fmt { - format: 1, - channels: 2, - sample_rate: 44100, - bits_per_sample: 16, - ..Default::default() - }, - data_header: Chunk { - id: *b"data", - chunk_size: 0xffff_ffff, +impl WaveFormat { + fn from_wave(fmt: &WAVEFORMATEX) -> Self { + WaveFormat { + channels: fmt.nChannels.max(1) as u32, + bits: if fmt.wBitsPerSample == 0 { + 8 + } else { + fmt.wBitsPerSample as u32 }, - }; - - let fmt = &mut header.fmt; - fmt.byte_per_block = fmt.channels * fmt.bits_per_sample / 8; - fmt.byte_per_sec = fmt.sample_rate * fmt.byte_per_block as u32; - - self.f.write_all(header.as_bytes())?; - - Ok(()) + rate: fmt.nSamplesPerSec.max(1), + } } - fn write(&mut self, data: &[u8]) { - use std::io::Write; - self.f.write_all(data).unwrap(); + fn frame_bytes(&self) -> u32 { + self.channels * (self.bits / 8).max(1) } } struct Buffer { + /// PCM data in guest memory. Zero for the primary buffer, which holds no + /// samples of its own. addr: u32, size: u32, - total_written: u32, - stream: host::AudioStream, - write: Option, + format: WaveFormat, + primary: bool, + caps_flags: DSBCAPS, + playing: bool, + looping: bool, + /// Playback position in source frames; fractional so resampling works. + cursor: f64, + volume: i32, + pan: i32, +} + +impl Buffer { + fn frame_count(&self) -> f64 { + (self.size / self.format.frame_bytes()) as f64 + } + + /// Per-channel amplitude, combining volume and pan. + fn gains(&self) -> (f32, f32) { + let volume = gain(self.volume); + // Panning attenuates the channel you're panning away from. + let left = if self.pan > 0 { gain(-self.pan) } else { 1.0 }; + let right = if self.pan < 0 { gain(self.pan) } else { 1.0 }; + (volume * left, volume * right) + } + + /// The sample pair at the current cursor, in i16 scale. + fn sample(&self, mem: &Memory) -> (i32, i32) { + let base = self.addr + self.cursor as u32 * self.format.frame_bytes(); + let stereo = self.format.channels >= 2; + if self.format.bits >= 16 { + let left = mem.read::(base) as i32; + let right = if stereo { + mem.read::(base + 2) as i32 + } else { + left + }; + (left, right) + } else { + // 8-bit PCM is unsigned, centered on 128. + let left = (mem[base] as i32 - 128) << 8; + let right = if stereo { + (mem[base + 1] as i32 - 128) << 8 + } else { + left + }; + (left, right) + } + } } struct State { buffers: HashMap, + stream: Option, + /// Heap holding the buffers' PCM data. + heap: Option, + write: Option, } + static STATE: Mutex> = Mutex::new(None); type Lock = LockedState; + fn lock() -> Lock { LockedState::from(&STATE) } @@ -122,10 +164,102 @@ fn init() { if state.is_none() { *state = Some(State { buffers: HashMap::default(), + stream: None, + heap: None, + write: if WRITE_WAV { + Some(WavWrite::new("out.wav")) + } else { + None + }, }); } } +impl State { + fn heap(&mut self) -> &Heap { + self.heap.get_or_insert_with(|| { + const HEAP_SIZE: u32 = 16 << 20; + let addr = kernel32::lock() + .mappings + .alloc("dsound buffers".into(), HEAP_SIZE); + Heap::new(addr, HEAP_SIZE) + }) + } + + fn stream(&mut self) -> &host::AudioStream { + self.stream.get_or_insert_with(|| { + let stream = host::host().create_audio_stream(host::AudioSpec { + sample_rate: HOST_RATE, + channels: 2, + }); + stream.resume(); + stream + }) + } + + /// Mix one chunk of every playing buffer and hand it to the host. + fn mix_chunk(&mut self, mem: &Memory) { + let mut mixed = [(0f32, 0f32); CHUNK_FRAMES]; + for buffer in self.buffers.values_mut() { + if !buffer.playing || buffer.primary || buffer.size == 0 { + continue; + } + let frames = buffer.frame_count(); + let step = buffer.format.rate as f64 / HOST_RATE as f64; + let (left_gain, right_gain) = buffer.gains(); + for slot in mixed.iter_mut() { + if buffer.cursor >= frames { + if !buffer.looping { + buffer.playing = false; + break; + } + buffer.cursor %= frames; + } + let (left, right) = buffer.sample(mem); + slot.0 += left as f32 * left_gain; + slot.1 += right as f32 * right_gain; + buffer.cursor += step; + } + } + + let mut bytes = Vec::with_capacity(CHUNK_FRAMES * HOST_FRAME_BYTES as usize); + for (left, right) in mixed { + for sample in [left, right] { + let sample = sample.clamp(i16::MIN as f32, i16::MAX as f32) as i16; + bytes.extend_from_slice(&sample.to_le_bytes()); + } + } + if let Some(write) = self.write.as_mut() { + write.write(&bytes); + } + self.stream().put_data(&bytes); + } +} + +/// Top the host's output queue up to [`TARGET_QUEUE_FRAMES`]. +/// +/// Cheap and safe to call as often as you like: with a full queue it does +/// nothing, and with no sound buffers it returns immediately. +pub fn pump(ctx: &mut Context) { + let Ok(state) = STATE.lock() else { return }; + let idle = match state.as_ref() { + Some(state) => state.buffers.is_empty(), + None => true, + }; + drop(state); + if idle { + return; + } + + let mut state = lock(); + for _ in 0..MAX_CHUNKS_PER_PUMP { + if state.stream().queued_bytes() >= TARGET_QUEUE_FRAMES * HOST_FRAME_BYTES { + break; + } + state.mix_chunk(&ctx.memory); + } +} + #[win32_derive::dllexport] pub fn DirectSoundCreate(ctx: &mut Context, lpGuid: u32, ppDS: u32, pUnkOuter: u32) -> u32 { assert_eq!(lpGuid, 0); @@ -135,6 +269,7 @@ pub fn DirectSoundCreate(ctx: &mut Context, lpGuid: u32, ppDS: u32, pUnkOuter: u let mut kernel32 = kernel32::lock(); let addr = IDirectSound::new(ctx, &mut kernel32.process_heap); + drop(kernel32); ctx.memory.write(ppDS, addr); DS_OK } @@ -144,6 +279,13 @@ pub fn ordinal1(ctx: &mut Context, lpGuid: u32, ppDS: u32, pUnkOuter: u32) -> u3 DirectSoundCreate(ctx, lpGuid, ppDS, pUnkOuter) } +#[win32_derive::dllexport] +pub fn DirectSoundEnumerateA(_ctx: &mut Context, _lpCallback: u32, _lpContext: u32) -> u32 { + // Report no devices to enumerate; apps that care use the default device, + // which DirectSoundCreate always provides. + DS_OK +} + pub mod IDirectSound { use super::*; @@ -162,18 +304,18 @@ pub mod IDirectSound { ]; #[win32_derive::dllexport] - pub fn QueryInterface(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn QueryInterface(_ctx: &mut Context, _this: u32, _riid: u32, _ppv: u32) -> u32 { + DSERR_INVALIDPARAM } #[win32_derive::dllexport] pub fn AddRef(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + 1 } #[win32_derive::dllexport] pub fn Release(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + 0 } #[win32_derive::dllexport] @@ -188,80 +330,139 @@ pub mod IDirectSound { let desc = ::read_from_prefix(&ctx.memory[lpcDSBufferDesc..]) .unwrap() .0; - assert_eq!(desc.dwSize, std::mem::size_of::() as u32); - // log::info!("new buffer flags {:#x?}", desc.dwFlags); let mut kernel32 = kernel32::lock(); let addr = IDirectSoundBuffer::new(ctx, &mut kernel32.process_heap); + drop(kernel32); - if !desc.dwFlags.contains(DSBCAPS::PRIMARYBUFFER) { + let primary = desc.dwFlags.contains(DSBCAPS::PRIMARYBUFFER); + let format = if desc.lpwfxFormat != 0 { let fmt = ::read_from_prefix(&ctx.memory[desc.lpwfxFormat..]) .unwrap() .0; const WAVE_FORMAT_PCM: u16 = 1; - assert_eq!(fmt.wFormatTag, WAVE_FORMAT_PCM); - - let stream = host::host().create_audio_stream(host::AudioSpec { - sample_rate: fmt.nSamplesPerSec as u32, - channels: fmt.nChannels as u32, - }); - - let write = if WRITE_WAV { - Some(WavWrite::new("out.wav")) - } else { - None - }; - - let buffer = Buffer { - addr: kernel32 - .process_heap - .alloc(&mut ctx.memory, desc.dwBufferBytes), - size: desc.dwBufferBytes, - total_written: 0, - stream, - write, - }; + if fmt.wFormatTag != WAVE_FORMAT_PCM { + log::warn!("dsound: non-PCM format {}", fmt.wFormatTag); + } + WaveFormat::from_wave(&fmt) + } else { + WaveFormat::default() + }; - lock().buffers.insert(addr, buffer); - } + let mut state = lock(); + // The primary buffer describes the output mix rather than holding + // samples, so it gets no memory of its own. + let (data, size) = if primary { + (0, 0) + } else { + let size = desc.dwBufferBytes; + (state.heap().alloc(&mut ctx.memory, size), size) + }; + state.buffers.insert( + addr, + Buffer { + addr: data, + size, + format, + primary, + caps_flags: desc.dwFlags, + playing: false, + looping: false, + cursor: 0.0, + volume: 0, + pan: 0, + }, + ); + drop(state); ctx.memory.write(lplpDirectSoundBuffer, addr); DS_OK } #[win32_derive::dllexport] - pub fn GetCaps(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn GetCaps(ctx: &mut Context, _this: u32, lpDSCaps: u32) -> u32 { + if lpDSCaps == 0 { + return DSERR_INVALIDPARAM; + } + // DSCAPS: dwSize, dwFlags, then a long run of counters. Report a + // software-only device with generous limits and zero everything else. + const DSCAPS_PRIMARYSTEREO: u32 = 0x0000_0002; + const DSCAPS_PRIMARY16BIT: u32 = 0x0000_0008; + const DSCAPS_CONTINUOUSRATE: u32 = 0x0000_0010; + let size = ctx.memory.read::(lpDSCaps); + ctx.memory[lpDSCaps + 4..][..(size as usize).saturating_sub(4)].fill(0); + ctx.memory.write::( + lpDSCaps + 4, + DSCAPS_PRIMARYSTEREO | DSCAPS_PRIMARY16BIT | DSCAPS_CONTINUOUSRATE, + ); + ctx.memory.write::(lpDSCaps + 8, 100); // dwMinSecondarySampleRate + ctx.memory.write::(lpDSCaps + 12, 100000); // dwMaxSecondarySampleRate + DS_OK } #[win32_derive::dllexport] - pub fn DuplicateSoundBuffer(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn DuplicateSoundBuffer( + ctx: &mut Context, + _this: u32, + pDSBufferOriginal: u32, + ppDSBufferDuplicate: u32, + ) -> u32 { + let mut kernel32 = kernel32::lock(); + let addr = IDirectSoundBuffer::new(ctx, &mut kernel32.process_heap); + drop(kernel32); + + let mut state = lock(); + let Some(original) = state.buffers.get(&pDSBufferOriginal) else { + return DSERR_INVALIDPARAM; + }; + // A duplicate shares the original's samples but plays independently. + let duplicate = Buffer { + addr: original.addr, + size: original.size, + format: original.format, + primary: original.primary, + caps_flags: original.caps_flags, + playing: false, + looping: false, + cursor: 0.0, + volume: original.volume, + pan: original.pan, + }; + state.buffers.insert(addr, duplicate); + drop(state); + + ctx.memory.write(ppDSBufferDuplicate, addr); + DS_OK } #[win32_derive::dllexport] pub fn SetCooperativeLevel(_ctx: &mut Context, _this: u32, _hwnd: u32, _dwLevel: u32) -> u32 { - stub!(0) + // We always have exclusive use of the host's audio output. + DS_OK } #[win32_derive::dllexport] pub fn Compact(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + DS_OK // nothing to defragment } #[win32_derive::dllexport] - pub fn GetSpeakerConfig(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn GetSpeakerConfig(ctx: &mut Context, _this: u32, lpdwSpeakerConfig: u32) -> u32 { + const DSSPEAKER_STEREO: u32 = 2; + if lpdwSpeakerConfig != 0 { + ctx.memory.write::(lpdwSpeakerConfig, DSSPEAKER_STEREO); + } + DS_OK } #[win32_derive::dllexport] - pub fn SetSpeakerConfig(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn SetSpeakerConfig(_ctx: &mut Context, _this: u32, _dwSpeakerConfig: u32) -> u32 { + DS_OK } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn Initialize(_ctx: &mut Context, _this: u32, _lpGuid: u32) -> u32 { + DS_OK } pub static mut VTABLE: u32 = 0; @@ -301,23 +502,44 @@ pub mod IDirectSoundBuffer { ]; #[win32_derive::dllexport] - pub fn QueryInterface(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn QueryInterface(_ctx: &mut Context, _this: u32, _riid: u32, _ppv: u32) -> u32 { + DSERR_INVALIDPARAM } #[win32_derive::dllexport] pub fn AddRef(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + 1 } #[win32_derive::dllexport] - pub fn Release(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn Release(ctx: &mut Context, this: u32) -> u32 { + let mut state = lock(); + if let Some(buffer) = state.buffers.remove(&this) { + // Duplicates share their original's samples, so only free memory + // that no surviving buffer still points at. + let shared = state.buffers.values().any(|other| other.addr == buffer.addr); + if buffer.addr != 0 && !shared { + state.heap().free(&mut ctx.memory, buffer.addr); + } + } + 0 } #[win32_derive::dllexport] - pub fn GetCaps(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn GetCaps(ctx: &mut Context, this: u32, lpDSBufferCaps: u32) -> u32 { + let state = lock(); + let Some(buffer) = state.buffers.get(&this) else { + return DSERR_INVALIDPARAM; + }; + // DSBCAPS: dwSize, dwFlags, dwBufferBytes, dwUnlockTransferRate, + // dwPlayCpuOverhead. + let (flags, size) = (buffer.caps_flags.bits(), buffer.size); + drop(state); + ctx.memory.write::(lpDSBufferCaps + 4, flags); + ctx.memory.write::(lpDSBufferCaps + 8, size); + ctx.memory.write::(lpDSBufferCaps + 12, 0); + ctx.memory.write::(lpDSBufferCaps + 16, 0); + DS_OK } #[win32_derive::dllexport] @@ -327,50 +549,124 @@ pub mod IDirectSoundBuffer { pdwCurrentPlayCursor: u32, pdwCurrentWriteCursor: u32, ) -> u32 { - let mut lock = lock(); - let buffer = lock.buffers.get_mut(&this).unwrap(); + let state = lock(); + let Some(buffer) = state.buffers.get(&this) else { + return DSERR_INVALIDPARAM; + }; + let frame_bytes = buffer.format.frame_bytes(); + let play = buffer.cursor as u32 * frame_bytes; + drop(state); + if pdwCurrentPlayCursor != 0 { - let unplayed = buffer.stream.queued_bytes(); - let play_cursor = (buffer.total_written - unplayed) % buffer.size; - ctx.memory.write(pdwCurrentPlayCursor, play_cursor); + ctx.memory.write(pdwCurrentPlayCursor, play); } if pdwCurrentWriteCursor != 0 { - let write_cursor = buffer.total_written % buffer.size; - ctx.memory.write(pdwCurrentWriteCursor, write_cursor); + // Real hardware keeps the write cursor a little ahead of playback. + ctx.memory.write(pdwCurrentWriteCursor, play + frame_bytes); } DS_OK } #[win32_derive::dllexport] - pub fn GetFormat(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn GetFormat( + ctx: &mut Context, + this: u32, + lpwfxFormat: u32, + dwSizeAllocated: u32, + lpdwSizeWritten: u32, + ) -> u32 { + let state = lock(); + let Some(buffer) = state.buffers.get(&this) else { + return DSERR_INVALIDPARAM; + }; + let format = buffer.format; + drop(state); + + let size = std::mem::size_of::() as u32; + if lpwfxFormat != 0 { + if dwSizeAllocated < size { + return DSERR_INVALIDPARAM; + } + let block_align = format.frame_bytes() as u16; + ctx.memory.write::(lpwfxFormat, 1); // WAVE_FORMAT_PCM + ctx.memory.write::(lpwfxFormat + 2, format.channels as u16); + ctx.memory.write::(lpwfxFormat + 4, format.rate); + ctx.memory + .write::(lpwfxFormat + 8, format.rate * block_align as u32); + ctx.memory.write::(lpwfxFormat + 12, block_align); + ctx.memory.write::(lpwfxFormat + 14, format.bits as u16); + ctx.memory.write::(lpwfxFormat + 16, 0); // cbSize + } + if lpdwSizeWritten != 0 { + ctx.memory.write::(lpdwSizeWritten, size); + } + DS_OK } #[win32_derive::dllexport] - pub fn GetVolume(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn GetVolume(ctx: &mut Context, this: u32, lplVolume: u32) -> u32 { + let state = lock(); + let Some(buffer) = state.buffers.get(&this) else { + return DSERR_INVALIDPARAM; + }; + let volume = buffer.volume; + drop(state); + ctx.memory.write::(lplVolume, volume); + DS_OK } #[win32_derive::dllexport] - pub fn GetPan(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn GetPan(ctx: &mut Context, this: u32, lplPan: u32) -> u32 { + let state = lock(); + let Some(buffer) = state.buffers.get(&this) else { + return DSERR_INVALIDPARAM; + }; + let pan = buffer.pan; + drop(state); + ctx.memory.write::(lplPan, pan); + DS_OK } #[win32_derive::dllexport] - pub fn GetFrequency(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn GetFrequency(ctx: &mut Context, this: u32, lpdwFrequency: u32) -> u32 { + let state = lock(); + let Some(buffer) = state.buffers.get(&this) else { + return DSERR_INVALIDPARAM; + }; + let rate = buffer.format.rate; + drop(state); + ctx.memory.write::(lpdwFrequency, rate); + DS_OK } #[win32_derive::dllexport] - pub fn GetStatus(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn GetStatus(ctx: &mut Context, this: u32, lpdwStatus: u32) -> u32 { + const DSBSTATUS_PLAYING: u32 = 0x0001; + const DSBSTATUS_LOOPING: u32 = 0x0004; + let state = lock(); + let Some(buffer) = state.buffers.get(&this) else { + return DSERR_INVALIDPARAM; + }; + let mut status = 0; + if buffer.playing { + status |= DSBSTATUS_PLAYING; + if buffer.looping { + status |= DSBSTATUS_LOOPING; + } + } + drop(state); + ctx.memory.write::(lpdwStatus, status); + DS_OK } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn Initialize(_ctx: &mut Context, _this: u32, _lpDirectSound: u32, _lpcDSBufferDesc: u32) -> u32 { + DS_OK } + /// Hand out a pointer straight into the buffer's memory. The app writes + /// samples there and the mixer reads them from the same place, so Unlock + /// has nothing to copy back. #[win32_derive::dllexport] pub fn Lock( ctx: &mut Context, @@ -383,29 +679,44 @@ pub mod IDirectSoundBuffer { pdwAudioBytes2: u32, dwFlags: DSBLOCK, ) -> u32 { - let mut lock = lock(); - let buffer = lock.buffers.get_mut(&this).unwrap(); - - let len = if dwFlags.contains(DSBLOCK::ENTIREBUFFER) { - assert_eq!(dwBytes, 0); - buffer.size - } else { - dwBytes.min(buffer.size - dwOffset) + let state = lock(); + let Some(buffer) = state.buffers.get(&this) else { + return DSERR_INVALIDPARAM; }; - let addr = if len == 0 { - // it appears chillin relies on getting null back in this case - 0 + let (offset, len) = if dwFlags.contains(DSBLOCK::ENTIREBUFFER) { + (0, buffer.size) } else { - buffer.addr + dwOffset + let offset = dwOffset.min(buffer.size); + (offset, dwBytes.min(buffer.size - offset)) }; + // Some callers rely on getting null back for an empty region. + let addr = if len == 0 { 0 } else { buffer.addr + offset }; + drop(state); + ctx.memory.write(ppvAudioPtr1, addr); ctx.memory.write(pdwAudioBytes1, len); + // We never split a locked region, so the second one is always empty. if ppvAudioPtr2 != 0 { ctx.memory.write(ppvAudioPtr2, 0u32); + } + if pdwAudioBytes2 != 0 { ctx.memory.write(pdwAudioBytes2, 0u32); } + DS_OK + } + #[win32_derive::dllexport] + pub fn Unlock( + _ctx: &mut Context, + _this: u32, + _pvAudioPtr1: u32, + _dwAudioBytes1: u32, + _pvAudioPtr2: u32, + _dwAudioBytes2: u32, + ) -> u32 { + // Lock exposed the buffer's real memory, so the app's writes are + // already visible to the mixer. DS_OK } @@ -415,81 +726,98 @@ pub mod IDirectSoundBuffer { this: u32, _dwReserved1: u32, _dwPriority: u32, - _dwFlags: u32, + dwFlags: u32, ) -> u32 { - let mut lock = lock(); - let buffer = lock.buffers.get_mut(&this).unwrap(); - buffer.stream.resume(); + const DSBPLAY_LOOPING: u32 = 0x0001; + let mut state = lock(); + let Some(buffer) = state.buffers.get_mut(&this) else { + return DSERR_INVALIDPARAM; + }; + // Play resumes from wherever the cursor was left, which is the start + // for a fresh buffer and where Stop left off otherwise. + buffer.playing = true; + buffer.looping = dwFlags & DSBPLAY_LOOPING != 0; DS_OK } #[win32_derive::dllexport] - pub fn SetCurrentPosition(_ctx: &mut Context, _this: u32) -> u32 { - todo!() - } - - #[win32_derive::dllexport] - pub fn SetFormat(ctx: &mut Context, _this: u32, pcfxFormat: u32) -> u32 { - let _fmt = ::read_from_prefix(&ctx.memory[pcfxFormat..]) - .unwrap() - .0; - // TODO: verify format is as expected, possibly change format? - // log::warn!("fmt {:#x?}", fmt); - stub!(DS_OK) + pub fn Stop(_ctx: &mut Context, this: u32) -> u32 { + let mut state = lock(); + let Some(buffer) = state.buffers.get_mut(&this) else { + return DSERR_INVALIDPARAM; + }; + // Stop leaves the cursor alone; a later Play resumes from here. + buffer.playing = false; + DS_OK } #[win32_derive::dllexport] - pub fn SetVolume(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn SetCurrentPosition(_ctx: &mut Context, this: u32, dwNewPosition: u32) -> u32 { + let mut state = lock(); + let Some(buffer) = state.buffers.get_mut(&this) else { + return DSERR_INVALIDPARAM; + }; + buffer.cursor = (dwNewPosition / buffer.format.frame_bytes()) as f64; + DS_OK } #[win32_derive::dllexport] - pub fn SetPan(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn SetFormat(ctx: &mut Context, this: u32, pcfxFormat: u32) -> u32 { + let fmt = ::read_from_prefix(&ctx.memory[pcfxFormat..]) + .unwrap() + .0; + let mut state = lock(); + let Some(buffer) = state.buffers.get_mut(&this) else { + return DSERR_INVALIDPARAM; + }; + // Only the primary buffer's format is settable, and since we always mix + // to the host's own format, recording it is all we need to do. + buffer.format = WaveFormat::from_wave(&fmt); + DS_OK } #[win32_derive::dllexport] - pub fn SetFrequency(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn SetVolume(_ctx: &mut Context, this: u32, lVolume: i32) -> u32 { + let mut state = lock(); + let Some(buffer) = state.buffers.get_mut(&this) else { + return DSERR_INVALIDPARAM; + }; + buffer.volume = lVolume.clamp(DSBVOLUME_MIN, 0); + DS_OK } #[win32_derive::dllexport] - pub fn Stop(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + pub fn SetPan(_ctx: &mut Context, this: u32, lPan: i32) -> u32 { + let mut state = lock(); + let Some(buffer) = state.buffers.get_mut(&this) else { + return DSERR_INVALIDPARAM; + }; + buffer.pan = lPan.clamp(DSBVOLUME_MIN, -DSBVOLUME_MIN); + DS_OK } #[win32_derive::dllexport] - pub fn Unlock( - ctx: &mut Context, - this: u32, - pvAudioPtr1: u32, - dwAudioBytes1: u32, - pvAudioPtr2: u32, - dwAudioBytes2: u32, - ) -> u32 { + pub fn SetFrequency(_ctx: &mut Context, this: u32, dwFrequency: u32) -> u32 { let mut state = lock(); - let buffer = state.buffers.get_mut(&this).unwrap(); - assert!(pvAudioPtr2 == 0); - assert!(dwAudioBytes2 == 0); - - if dwAudioBytes1 == 0 { - return DS_OK; + let Some(buffer) = state.buffers.get_mut(&this) else { + return DSERR_INVALIDPARAM; + }; + // Zero means "back to the buffer's original rate", which for us is the + // rate it was created with; we only ever change it from here. + if dwFrequency != 0 { + buffer.format.rate = dwFrequency; } - - let data = &ctx.memory[pvAudioPtr1..][..dwAudioBytes1 as usize]; - buffer.stream.put_data(data); - buffer.write.as_mut().map(|w| w.write(data)); - buffer.total_written += data.len() as u32; - DS_OK } #[win32_derive::dllexport] pub fn Restore(_ctx: &mut Context, _this: u32) -> u32 { - todo!() + // Buffers live in our own memory and are never lost. + DS_OK } pub static mut VTABLE: u32 = 0; + pub fn new(ctx: &mut Context, heap: &mut Heap) -> u32 { let addr = heap.alloc(&mut ctx.memory, 4); ctx.memory.write(addr, unsafe { VTABLE }); @@ -556,3 +884,87 @@ pub const VTABLES: [(&'static str, &[&str]); 2] = [ IDirectSoundBuffer::VTABLE_ENTRIES.as_slice(), ), ]; + +/// Debug helper: dumps the mixed output to a .wav. +struct WavWrite { + f: std::fs::File, +} + +impl WavWrite { + fn new(path: &str) -> Self { + let f = std::fs::File::create(path).unwrap(); + let mut w = Self { f }; + w.write_header().unwrap(); + w + } + + fn write_header(&mut self) -> std::io::Result<()> { + use std::io::Write; + + use zerocopy::IntoBytes; + + #[repr(C)] + #[derive(zerocopy::IntoBytes, zerocopy::Immutable)] + struct Chunk { + id: [u8; 4], + chunk_size: u32, + } + + #[repr(C)] + #[derive(zerocopy::IntoBytes, zerocopy::Immutable, Default)] + struct Fmt { + format: u16, + channels: u16, + sample_rate: u32, + byte_per_sec: u32, + byte_per_block: u16, + bits_per_sample: u16, + } + + #[repr(C)] + #[derive(zerocopy::IntoBytes, zerocopy::Immutable)] + struct Header { + file_header: Chunk, + format: [u8; 4], + fmt_header: Chunk, + fmt: Fmt, + data_header: Chunk, + } + + let mut header = Header { + file_header: Chunk { + id: *b"RIFF", + chunk_size: 0xffff_ffff, + }, + format: *b"WAVE", + fmt_header: Chunk { + id: *b"fmt ", + chunk_size: std::mem::size_of::() as u32, + }, + fmt: Fmt { + format: 1, + channels: 2, + sample_rate: HOST_RATE, + bits_per_sample: 16, + ..Default::default() + }, + data_header: Chunk { + id: *b"data", + chunk_size: 0xffff_ffff, + }, + }; + + let fmt = &mut header.fmt; + fmt.byte_per_block = fmt.channels * fmt.bits_per_sample / 8; + fmt.byte_per_sec = fmt.sample_rate * fmt.byte_per_block as u32; + + self.f.write_all(header.as_bytes())?; + + Ok(()) + } + + fn write(&mut self, data: &[u8]) { + use std::io::Write; + self.f.write_all(data).unwrap(); + } +} diff --git a/win32/winapi/src/kernel32/dll.rs b/win32/winapi/src/kernel32/dll.rs index 9ee487f6..00fe7f01 100644 --- a/win32/winapi/src/kernel32/dll.rs +++ b/win32/winapi/src/kernel32/dll.rs @@ -1,3 +1,5 @@ +use std::sync::Mutex; + use runtime::Context; use crate::{Ptr, kernel32::lock, stub}; @@ -23,6 +25,68 @@ impl DLLLoader for () { } } +/// Functions the translated program can look up by name at runtime. +/// +/// A statically linked import is dispatched by the translator, but a program +/// that goes through LoadLibrary/GetProcAddress needs an actual address to +/// call. The translator picks one per function (the same synthetic addresses it +/// uses for imports, so they resolve through the block table) and registers it +/// here from the generated init code. +#[derive(Default)] +struct Exports { + /// (dll, function) -> address, with dll lowercased and without ".dll". + functions: Vec<(String, String, u32)>, + /// Module handles handed out by LoadLibrary, in registration order. + modules: Vec, +} + +static EXPORTS: Mutex> = Mutex::new(None); + +/// Module handles are synthetic; the value only has to be non-null and +/// distinguishable. +const MODULE_HANDLE_BASE: HMODULE = 0xd11_0000; + +fn normalize_module_name(name: &str) -> String { + let name = name.rsplit(['\\', '/']).next().unwrap_or(name); + name.trim_end_matches(".dll") + .trim_end_matches(".DLL") + .to_ascii_lowercase() +} + +/// Register a function as available through GetProcAddress. Called from +/// generated init code, once per function the translator reserved an address +/// for. +pub fn register_export(dll: &str, func: &str, addr: u32) { + let mut exports = EXPORTS.lock().unwrap(); + let exports = exports.get_or_insert_with(Default::default); + let dll = normalize_module_name(dll); + if !exports.modules.contains(&dll) { + exports.modules.push(dll.clone()); + } + exports.functions.push((dll, func.to_string(), addr)); +} + +fn module_handle(name: &str) -> Option { + let exports = EXPORTS.lock().unwrap(); + let exports = exports.as_ref()?; + let name = normalize_module_name(name); + let index = exports.modules.iter().position(|module| *module == name)?; + Some(MODULE_HANDLE_BASE + index as u32) +} + +fn module_name(handle: HMODULE) -> Option { + let index = handle.checked_sub(MODULE_HANDLE_BASE)? as usize; + let exports = EXPORTS.lock().unwrap(); + exports.as_ref()?.modules.get(index).cloned() +} + +fn proc_address(module: &str, func: &str) -> Option { + let exports = EXPORTS.lock().unwrap(); + exports.as_ref()?.functions.iter().find_map(|(dll, name, addr)| { + (dll == module && name == func).then_some(*addr) + }) +} + #[win32_derive::dllexport] pub fn GetModuleFileNameA( _ctx: &mut Context, @@ -37,36 +101,53 @@ pub fn GetModuleFileNameA( } #[win32_derive::dllexport] -pub fn GetModuleHandleA(_ctx: &mut Context, _lpModuleName: Ptr) -> HMODULE { - stub!(0) - /* - let state = get_state(sys); - let Some(name) = lpModuleName else { - return HMODULE::from_raw(state.image_base); - }; - let name = normalize_module_name(name); - - let Some((&hmodule, _)) = state.modules.iter().find(|(_, dll)| dll.name == name) else { - sys.set_last_error(ERROR::MOD_NOT_FOUND); - return HMODULE::null(); - }; - - hmodule - */ +pub fn GetModuleHandleA(ctx: &mut Context, lpModuleName: Ptr) -> HMODULE { + let Some(name) = (lpModuleName.addr != 0) + .then(|| ctx.memory.read_str(lpModuleName.addr).to_owned()) + else { + // A null name asks for the running executable itself. + return lock().image_base; + }; + match module_handle(&name) { + Some(handle) => handle, + None => { + log::warn!("GetModuleHandleA({name}): not loaded"); + 0 + } + } } #[win32_derive::dllexport] pub fn LoadLibraryA(ctx: &mut Context, lpLibFileName: Ptr) -> HMODULE { - let filename = ctx.memory.read_str(lpLibFileName.addr); + let filename = ctx.memory.read_str(lpLibFileName.addr).to_owned(); + // A DLL we implement is already "loaded"; anything else falls through to + // whatever loader the host installed. + if let Some(handle) = module_handle(&filename) { + return handle; + } lock().dll_loader.load_library(&filename) } +#[win32_derive::dllexport] +pub fn FreeLibrary(_ctx: &mut Context, _hLibModule: HMODULE) -> bool { + // Our modules are always resident. + true +} + #[win32_derive::dllexport] pub fn GetProcAddress(ctx: &mut Context, hModule: HMODULE, lpProcName: Ptr) -> u32 { + // A name below 0x1000 is really an ordinal, per the API's convention. let name = if lpProcName.addr < 0x1000 { format!("ordinal{}", lpProcName.addr) } else { ctx.memory.read_str(lpProcName.addr).to_owned() }; + if let Some(module) = module_name(hModule) { + if let Some(addr) = proc_address(&module, &name) { + return addr; + } + log::warn!("GetProcAddress({module}, {name}): not implemented, returning null"); + return 0; + } lock().dll_loader.get_proc_address(hModule, &name) } diff --git a/win32/winapi/src/kernel32/process.rs b/win32/winapi/src/kernel32/process.rs index 5000a63e..22fa7ae4 100644 --- a/win32/winapi/src/kernel32/process.rs +++ b/win32/winapi/src/kernel32/process.rs @@ -112,7 +112,9 @@ impl kernel32::State { let (peb, _buf) = PEB::mut_from_prefix(buf).unwrap(); peb.ProcessParameters = (params as *const _ as usize - origin) as u32; - let heap_size = 4 << 20; + // Games of this era load whole asset archives into the process heap, + // so give it room; the address space is ours to spend. + let heap_size = 64 << 20; let heap_addr = self.mappings.alloc("process heap".into(), heap_size); let process_heap = Heap::new(heap_addr, heap_size); peb.ProcessHeap = process_heap.addr; diff --git a/win32/winapi/src/kernel32/thread.rs b/win32/winapi/src/kernel32/thread.rs index 2df558e7..12f9cfc1 100644 --- a/win32/winapi/src/kernel32/thread.rs +++ b/win32/winapi/src/kernel32/thread.rs @@ -70,6 +70,7 @@ impl kernel32::State { // See docstring on Memory about the unsafety of sharing memory in this way. memory: ctx.memory.unsafe_clone(), blocks: ctx.blocks, + block_map: ctx.block_map, recent: [Context::return_from_x86; 4], }; self.next_thread_id += 1; diff --git a/win32/winapi/src/lib.rs b/win32/winapi/src/lib.rs index 11988a15..64a1bc1d 100644 --- a/win32/winapi/src/lib.rs +++ b/win32/winapi/src/lib.rs @@ -24,6 +24,20 @@ pub mod trace; pub mod user32; pub mod winmm; +/// Functions a program may resolve at runtime with LoadLibrary/GetProcAddress +/// instead of importing statically. The translator reserves a callable address +/// for each of these, so a call through the returned pointer lands somewhere. +/// +/// Statically imported functions need no entry here; only add a name when a +/// program is seen looking it up by hand. +pub const DYNAMIC_EXPORTS: &[(&str, &[&str])] = &[ + // The Microsoft C runtime loads user32 on demand to report fatal errors. + ( + "user32", + &["MessageBoxA", "GetActiveWindow", "GetLastActivePopup"], + ), +]; + pub use dllexport::{ABIReturn, FromABIParam}; pub use handle::{HANDLE, Handles}; pub use point::POINT; @@ -53,7 +67,9 @@ pub fn load(exe: &EXEData) -> Context { host::init(); crate::trace::init(&thesesus_trace()); - let memory_size = 64 << 20; + // Room for the program's image, its heaps and the flat pool games of this + // era carve out for themselves. + let memory_size = 256 << 20; let memory = Memory::leak_new(memory_size); kernel32::init_state(exe.image_base, exe.resources.clone()); @@ -65,6 +81,7 @@ pub fn load(exe: &EXEData) -> Context { thread_id: 1, memory, blocks: exe.blocks, + block_map: runtime::BlockMap::get_or_init(exe.blocks), recent: [Context::return_from_x86; 4], }; diff --git a/win32/winapi/src/msacm32.rs b/win32/winapi/src/msacm32.rs index ef89b2a5..2ca05bad 100644 --- a/win32/winapi/src/msacm32.rs +++ b/win32/winapi/src/msacm32.rs @@ -1,6 +1,41 @@ +//! Audio Compression Manager. Games probe it to decide whether sound is +//! available; we report a working ACM with no codecs installed, which is all +//! PCM playback needs. + use runtime::Context; +const MMSYSERR_NOERROR: u32 = 0; +const MMSYSERR_NOTSUPPORTED: u32 = 8; + +const ACM_METRIC_COUNT_DRIVERS: u32 = 1; +const ACM_METRIC_COUNT_CODECS: u32 = 2; +const ACM_METRIC_COUNT_CONVERTERS: u32 = 3; +const ACM_METRIC_COUNT_FILTERS: u32 = 4; +const ACM_METRIC_COUNT_DISABLED: u32 = 5; +const ACM_METRIC_MAX_SIZE_FORMAT: u32 = 50; +const ACM_METRIC_MAX_SIZE_FILTER: u32 = 51; + #[win32_derive::dllexport] -pub fn acmMetrics(_ctx: &mut Context, _hao: u32, _uMetric: u32, _pMetric: u32) -> u32 { - crate::stub!(1) // MMSYSERR_ERROR: no codecs available +pub fn acmMetrics(ctx: &mut Context, _hao: u32, uMetric: u32, pMetric: u32) -> u32 { + let value = match uMetric { + // The largest WAVEFORMATEX any driver might describe. Callers size a + // format buffer with this, so it has to cover WAVEFORMATEX plus the + // extra bytes a compressed format would carry. + ACM_METRIC_MAX_SIZE_FORMAT => 64, + ACM_METRIC_MAX_SIZE_FILTER => 64, + // No codecs: PCM needs no conversion. + ACM_METRIC_COUNT_DRIVERS + | ACM_METRIC_COUNT_CODECS + | ACM_METRIC_COUNT_CONVERTERS + | ACM_METRIC_COUNT_FILTERS + | ACM_METRIC_COUNT_DISABLED => 0, + _ => { + log::warn!("acmMetrics: unhandled metric {uMetric}"); + return MMSYSERR_NOTSUPPORTED; + } + }; + if pMetric != 0 { + ctx.memory.write::(pMetric, value); + } + MMSYSERR_NOERROR } diff --git a/win32/winapi/src/user32/input.rs b/win32/winapi/src/user32/input.rs new file mode 100644 index 00000000..c7dc02a5 --- /dev/null +++ b/win32/winapi/src/user32/input.rs @@ -0,0 +1,394 @@ +//! Global input state. +//! +//! There is one source of input events — the host message pump in message.rs — +//! and several consumers: window messages (WM_KEYDOWN etc), the polling APIs +//! (GetKeyState/GetAsyncKeyState), and DirectInput. They all read the state +//! maintained here, so they can never disagree about which keys are down. + +use std::collections::VecDeque; + +use runtime::Context; + +use crate::user32::state; + +/// "Key is down" bit, both in GetKeyState's result and in a DirectInput device +/// state array. +pub const KEY_DOWN: u8 = 0x80; +/// "Key is toggled on" bit of GetKeyState's result (caps lock and friends). +const KEY_TOGGLED: u8 = 0x01; + +/// DirectInput drops the oldest events when a device's buffer fills. Apps ask +/// for a size via DIPROP_BUFFERSIZE; this caps what we'll honor so an app that +/// never drains its buffer can't grow us without bound. +const MAX_BUFFER_SIZE: usize = 1024; + +/// One buffered device event, in the shape IDirectInputDevice::GetDeviceData +/// reports: an offset within the device's data format plus its new value. +#[derive(Clone, Copy)] +pub struct DeviceEvent { + /// Offset of the changed object within the device data format. For a + /// keyboard that is the DirectInput scan code; for a mouse, the axis or + /// button offset within DIMOUSESTATE. + pub ofs: u32, + pub data: u32, + /// Host time in milliseconds. + pub time: u32, + /// DirectInput hands out a monotonic sequence number per event. + pub sequence: u32, +} + +/// Byte offset of a mouse object within the standard c_dfDIMouse data format: +/// lX@0, lY@4, lZ@8, then one byte per button from 12. +const MOUSE_AXIS_X: u32 = 0; +const MOUSE_AXIS_Y: u32 = 4; +const MOUSE_BUTTON_0: u32 = 12; +pub const MOUSE_BUTTONS: usize = 4; + +pub struct Mouse { + /// Cursor position in client coordinates. + pub x: i32, + pub y: i32, + /// Movement accumulated since DirectInput last read it. DirectInput mice + /// report relative motion, so reading resets this. + dx: i32, + dy: i32, + /// Button state, indexed as DIMOUSESTATE::rgbButtons: left, right, middle. + pub buttons: [u8; MOUSE_BUTTONS], + /// False until the first host mouse event, so the initial position doesn't + /// register as a jump. + have_position: bool, +} + +impl Default for Mouse { + fn default() -> Self { + Mouse { + x: 0, + y: 0, + dx: 0, + dy: 0, + buttons: [0; MOUSE_BUTTONS], + have_position: false, + } + } +} + +impl Mouse { + /// Consume the accumulated relative motion, as a read of a relative-axis + /// device does. + pub fn take_motion(&mut self) -> (i32, i32) { + let motion = (self.dx, self.dy); + self.dx = 0; + self.dy = 0; + motion + } + + /// Move the tracked cursor without generating relative motion, so a + /// SetCursorPos warp isn't mistaken for the user moving the mouse. + pub fn warp(&mut self, x: i32, y: i32) { + self.x = x; + self.y = y; + self.have_position = true; + } +} + +/// A device's buffered event queue. Empty `buffer_size` means the app never +/// asked for buffered input, in which case DirectInput buffers nothing. +#[derive(Default)] +struct EventBuffer { + events: VecDeque, + size: usize, + overflowed: bool, +} + +impl EventBuffer { + fn push(&mut self, event: DeviceEvent) { + if self.size == 0 { + return; + } + if self.events.len() >= self.size { + self.events.pop_front(); + self.overflowed = true; + } + self.events.push_back(event); + } + + /// Take up to `max` events, oldest first. `peek` leaves them buffered, as + /// DIGDD_PEEK asks. Returns the events plus whether an overflow happened + /// since the last non-peeking read. + fn take(&mut self, max: usize, peek: bool) -> (Vec, bool) { + let count = max.min(self.events.len()); + let overflowed = self.overflowed; + if peek { + return (self.events.iter().take(count).copied().collect(), overflowed); + } + self.overflowed = false; + (self.events.drain(..count).collect(), overflowed) + } +} + +pub struct Input { + /// Indexed by VK_* code, holding GetKeyState's KEY_DOWN|KEY_TOGGLED bits. + keys: [u8; 256], + /// Indexed by DirectInput scan code, holding KEY_DOWN. + dik: [u8; 256], + keyboard_buffer: EventBuffer, + mouse_buffer: EventBuffer, + next_sequence: u32, + pub mouse: Mouse, +} + +impl Default for Input { + fn default() -> Self { + Input { + keys: [0; 256], + dik: [0; 256], + keyboard_buffer: Default::default(), + mouse_buffer: Default::default(), + next_sequence: 0, + mouse: Default::default(), + } + } +} + +/// Maps a side-specific virtual key (VK_LSHIFT) to the generic one (VK_SHIFT), +/// and to the opposite side's key. Window messages carry the generic code, +/// while the key state array holds both. +fn key_sides(vkey: u8) -> Option<(u8, u8)> { + Some(match vkey { + 0xa0 => (0x10, 0xa1), // VK_LSHIFT -> VK_SHIFT, VK_RSHIFT + 0xa1 => (0x10, 0xa0), + 0xa2 => (0x11, 0xa3), // VK_LCONTROL -> VK_CONTROL, VK_RCONTROL + 0xa3 => (0x11, 0xa2), + 0xa4 => (0x12, 0xa5), // VK_LMENU -> VK_MENU, VK_RMENU + 0xa5 => (0x12, 0xa4), + _ => return None, + }) +} + +/// The virtual key a window message reports for this key: the generic code +/// where there is one. +pub fn message_vkey(vkey: u8) -> u8 { + key_sides(vkey).map_or(vkey, |(generic, _)| generic) +} + +/// Keys that toggle rather than just repeat: caps lock, num lock, scroll lock. +fn is_toggle_key(vkey: u8) -> bool { + matches!(vkey, 0x14 | 0x90 | 0x91) +} + +impl Input { + pub fn key_state(&self, vkey: u8) -> u8 { + self.keys[vkey as usize] + } + + pub fn key_down(&self, vkey: u8) -> bool { + self.keys[vkey as usize] & KEY_DOWN != 0 + } + + /// The 256-byte array a DirectInput keyboard reports. + pub fn dik_state(&self) -> &[u8; 256] { + &self.dik + } + + fn next_sequence(&mut self) -> u32 { + let sequence = self.next_sequence; + self.next_sequence = self.next_sequence.wrapping_add(1); + sequence + } + + fn set_key(&mut self, vkey: u8, down: bool, toggle: bool) { + let state = &mut self.keys[vkey as usize]; + if down { + if toggle && is_toggle_key(vkey) { + *state ^= KEY_TOGGLED; + } + *state |= KEY_DOWN; + } else { + *state &= !KEY_DOWN; + } + } + + pub fn on_key(&mut self, key: &host::KeyMessage, down: bool) { + self.set_key(key.vkey, down, !key.repeat); + if let Some((generic, other)) = key_sides(key.vkey) { + // The generic entry stays down while either side is held. + if down || !self.key_down(other) { + self.set_key(generic, down, false); + } + } + + let dik = key.scancode | if key.extended { 0x80 } else { 0 }; + // DirectInput reports edges, not auto-repeat. + if key.repeat { + return; + } + self.dik[dik as usize] = if down { KEY_DOWN } else { 0 }; + let sequence = self.next_sequence(); + self.keyboard_buffer.push(DeviceEvent { + ofs: dik as u32, + data: if down { KEY_DOWN as u32 } else { 0 }, + time: host::host().time(), + sequence, + }); + } + + pub fn on_mouse(&mut self, mouse: &host::MouseMessage) { + let (x, y) = (mouse.x as i32, mouse.y as i32); + if self.mouse.have_position { + let (dx, dy) = (x - self.mouse.x, y - self.mouse.y); + for (ofs, delta) in [(MOUSE_AXIS_X, dx), (MOUSE_AXIS_Y, dy)] { + if delta == 0 { + continue; + } + let sequence = self.next_sequence(); + self.mouse_buffer.push(DeviceEvent { + ofs, + data: delta as u32, + time: host::host().time(), + sequence, + }); + } + self.mouse.dx += dx; + self.mouse.dy += dy; + } + self.mouse.have_position = true; + self.mouse.x = x; + self.mouse.y = y; + + // DIMOUSESTATE orders buttons left, right, middle. + let buttons = [ + host::MouseButton::Left, + host::MouseButton::Right, + host::MouseButton::Middle, + ]; + for (index, button) in buttons.into_iter().enumerate() { + let down = if mouse.buttons.contains(button) { + KEY_DOWN + } else { + 0 + }; + if self.mouse.buttons[index] == down { + continue; + } + self.mouse.buttons[index] = down; + let sequence = self.next_sequence(); + self.mouse_buffer.push(DeviceEvent { + ofs: MOUSE_BUTTON_0 + index as u32, + data: down as u32, + time: host::host().time(), + sequence, + }); + } + } + + pub fn buffer_size(&self, keyboard: bool) -> usize { + if keyboard { + self.keyboard_buffer.size + } else { + self.mouse_buffer.size + } + } + + pub fn set_buffer_size(&mut self, keyboard: bool, size: usize) { + let buffer = if keyboard { + &mut self.keyboard_buffer + } else { + &mut self.mouse_buffer + }; + buffer.size = size.min(MAX_BUFFER_SIZE); + buffer.events.clear(); + } + + pub fn take_events(&mut self, keyboard: bool, max: usize, peek: bool) -> (Vec, bool) { + let buffer = if keyboard { + &mut self.keyboard_buffer + } else { + &mut self.mouse_buffer + }; + buffer.take(max, peek) + } +} + +/// Drain every host event that is ready, updating input state. +/// +/// Window message dispatch does this as a side effect of pumping messages, but +/// DirectInput reads state without touching the message queue, so it calls +/// this first to avoid reporting a stale keyboard. +pub fn pump_host_input() { + state().message_queue.borrow_mut().poll_host_all(); +} + +fn vkey_to_char(vkey: u8, shift: bool, caps: bool) -> Option { + Some(match vkey { + // Letters are upper case unless exactly one of shift/caps is on. + b'A'..=b'Z' => { + if shift != caps { + vkey + } else { + vkey.to_ascii_lowercase() + } + } + b'0'..=b'9' => { + if shift { + b")!@#$%^&*("[(vkey - b'0') as usize] + } else { + vkey + } + } + 0x08 | 0x09 | 0x1b | 0x20 => vkey, // backspace, tab, escape, space + 0x0d => b'\r', + 0x60..=0x69 => b'0' + (vkey - 0x60), // numpad digits + 0x6a => b'*', + 0x6b => b'+', + 0x6d => b'-', + 0x6e => b'.', + 0x6f => b'/', + 0xba => shifted(shift, b';', b':'), + 0xbb => shifted(shift, b'=', b'+'), + 0xbc => shifted(shift, b',', b'<'), + 0xbd => shifted(shift, b'-', b'_'), + 0xbe => shifted(shift, b'.', b'>'), + 0xbf => shifted(shift, b'/', b'?'), + 0xc0 => shifted(shift, b'`', b'~'), + 0xdb => shifted(shift, b'[', b'{'), + 0xdc => shifted(shift, b'\\', b'|'), + 0xdd => shifted(shift, b']', b'}'), + 0xde => shifted(shift, b'\'', b'"'), + _ => return None, + }) +} + +fn shifted(shift: bool, plain: u8, shifted: u8) -> u8 { + if shift { shifted } else { plain } +} + +/// The character a WM_KEYDOWN translates to, or None for keys that produce no +/// text. Used by TranslateMessage. +pub fn char_for_key(vkey: u8) -> Option { + let input = state().input.borrow(); + let shift = input.key_down(0x10); + let caps = input.key_state(0x14) & KEY_TOGGLED != 0; + vkey_to_char(vkey, shift, caps) +} + +#[win32_derive::dllexport] +pub fn GetKeyState(_ctx: &mut Context, nVirtKey: u32) -> i16 { + let state = state().input.borrow().key_state(nVirtKey as u8); + ((state as i16) << 8) | (state & KEY_TOGGLED) as i16 +} + +#[win32_derive::dllexport] +pub fn GetAsyncKeyState(_ctx: &mut Context, vKey: u32) -> i16 { + pump_host_input(); + let state = state().input.borrow().key_state(vKey as u8); + (state as i16) << 8 +} + +#[win32_derive::dllexport] +pub fn GetKeyboardState(ctx: &mut Context, lpKeyState: u32) -> bool { + let input = state().input.borrow(); + for vkey in 0..256u32 { + ctx.memory[lpKeyState + vkey] = input.key_state(vkey as u8); + } + true +} diff --git a/win32/winapi/src/user32/message.rs b/win32/winapi/src/user32/message.rs index bee3900d..fcb31c4a 100644 --- a/win32/winapi/src/user32/message.rs +++ b/win32/winapi/src/user32/message.rs @@ -6,7 +6,7 @@ use crate::{ POINT, Ptr, dllexport::win32flags, stub, trace, - user32::{HACCEL, HWND, Window, state}, + user32::{HACCEL, HWND, Window, char_for_key, message_vkey, state}, }; /// If THESEUS_TRACE includes "wm", log all Windows messages. @@ -24,6 +24,11 @@ pub enum WM { QUIT = 0x12, SHOWWINDOW = 0x18, ACTIVATEAPP = 0x1c, + KEYDOWN = 0x100, + KEYUP = 0x101, + CHAR = 0x102, + SYSKEYDOWN = 0x104, + SYSKEYUP = 0x105, MOUSEMOVE = 0x200, LBUTTONDOWN = 0x201, LBUTTONUP = 0x202, @@ -112,6 +117,45 @@ fn mouse_msg(wm: WM, hwnd: HWND, message: &host::MouseMessage) -> MSG { } } +fn key_msg(hwnd: HWND, key: &host::KeyMessage, down: bool) -> MSG { + // lParam packs the key's physical details, as documented for WM_KEYDOWN. + let mut lParam = 1; // repeat count; the host reports repeats one at a time + lParam |= (key.scancode as u32) << 16; + if key.extended { + lParam |= 1 << 24; + } + // Bit 29 is set while alt is held, bit 30 holds the previous key state, + // bit 31 marks the release. + let alt = state().input.borrow().key_down(0x12); // VK_MENU + if alt { + lParam |= 1 << 29; + } + if key.repeat || !down { + lParam |= 1 << 30; + } + if !down { + lParam |= 1 << 31; + } + + // Keys pressed with alt held are "system" keys, as is alt itself. + let system = alt || key.vkey == 0xa4 || key.vkey == 0xa5; + let message = match (down, system) { + (true, false) => WM::KEYDOWN, + (false, false) => WM::KEYUP, + (true, true) => WM::SYSKEYDOWN, + (false, true) => WM::SYSKEYUP, + }; + + MSG { + hwnd, + message: message as u32, + wParam: message_vkey(key.vkey) as u32, + lParam, + time: host::host().time(), + pt: POINT::default(), + } +} + /// Post a message to the application's queue (e.g. synthetic activation /// messages from ShowWindow). pub fn post_message(hwnd: HWND, message: u32, wParam: WPARAM, lParam: LPARAM) { @@ -190,6 +234,14 @@ impl MessageQueue { self.enqueue_message(message); } + /// Read every pending host message. DirectInput calls this to refresh + /// input state without going through the window message queue. + pub fn poll_host_all(&mut self) { + while let Some(message) = host::host().poll() { + self.enqueue_message(message); + } + } + /// Wait for a new message to arrive. fn wait_host(&mut self) { let message = host::host().wait(); @@ -205,6 +257,21 @@ impl MessageQueue { return; } + // Every host input event updates the shared input state, whether or not + // the app reads it through the message queue: DirectInput reads the + // same state, and this is the only place host events are consumed. + { + let mut input = state().input.borrow_mut(); + match &msg { + host::Message::KeyDown(key) => input.on_key(key, true), + host::Message::KeyUp(key) => input.on_key(key, false), + host::Message::MouseDown(mouse) + | host::Message::MouseUp(mouse) + | host::Message::MouseMove(mouse) => input.on_mouse(mouse), + _ => {} + } + } + let msg = self.msg_from_message(msg); if *LOG_MESSAGES { log::info!("{:#x?}", msg); @@ -225,6 +292,8 @@ impl MessageQueue { MouseDown(mouse) => mouse_msg(mouse_button_to_wm(true, &mouse), hwnd, &mouse), MouseUp(mouse) => mouse_msg(mouse_button_to_wm(false, &mouse), hwnd, &mouse), MouseMove(mouse) => mouse_msg(WM::MOUSEMOVE, hwnd, &mouse), + KeyDown(key) => key_msg(hwnd, &key, true), + KeyUp(key) => key_msg(hwnd, &key, false), #[cfg(not(target_family = "wasm"))] Paint => unreachable!(), #[cfg(not(target_family = "wasm"))] @@ -260,8 +329,20 @@ pub fn DispatchMessageW(ctx: &mut Context, lpMsg: Ptr) -> u32 { } #[win32_derive::dllexport] -pub fn TranslateMessage(_ctx: &mut Context, _lpMsg: Ptr) -> bool { - false // no translation +pub fn TranslateMessage(ctx: &mut Context, lpMsg: Ptr) -> bool { + let Some(msg) = lpMsg.read(&ctx.memory) else { + return false; + }; + if msg.message != WM::KEYDOWN as u32 { + return false; + } + let Some(ch) = char_for_key(msg.wParam as u8) else { + return false; + }; + // The character message follows the key message in the queue, so the app + // sees it on its next pump. + post_message(msg.hwnd, WM::CHAR as u32, ch as u32, msg.lParam); + true } #[win32_derive::dllexport] @@ -278,6 +359,10 @@ pub fn PeekMessageA( 1 => true, // PM_REMOVE _ => todo!(), // e.g. PM_NOYIELD }; + // Games poll for messages every frame; keep the audio mixer fed from here + // too, in case the app renders without flipping. + crate::dsound::pump(ctx); + let mut queue = state().message_queue.borrow_mut(); queue.poll_host(); let Some(msg) = queue.peek() else { diff --git a/win32/winapi/src/user32/misc.rs b/win32/winapi/src/user32/misc.rs index 51f92188..7a1c316d 100644 --- a/win32/winapi/src/user32/misc.rs +++ b/win32/winapi/src/user32/misc.rs @@ -81,13 +81,39 @@ pub fn KillTimer(_ctx: &mut Context, _hWnd: HWND, _uIDEvent: u32) -> bool { #[win32_derive::dllexport] pub fn MessageBoxA( - _ctx: &mut Context, + ctx: &mut Context, _hWnd: HWND, - _lpText: Ptr, - _lpCaption: Ptr, + lpText: Ptr, + lpCaption: Ptr, _uType: u32, /* MESSAGEBOX_STYLE */ ) -> u32 /* MESSAGEBOX_RESULT */ { - stub!(0) + // We have no dialogs, but the C runtime reports fatal errors this way, so + // the text is worth surfacing. + let read = |ptr: Ptr| { + if ptr.addr == 0 { + String::new() + } else { + ctx.memory.read_str(ptr.addr).to_owned() + } + }; + log::warn!("MessageBox: {} / {}", read(lpCaption), read(lpText)); + const IDOK: u32 = 1; + IDOK +} + +#[win32_derive::dllexport] +pub fn GetActiveWindow(_ctx: &mut Context) -> HWND { + // Only ever one window, and it's always the active one. + match state().window.borrow().as_ref() { + Some(window) => window.borrow().hwnd, + None => HWND::null(), + } +} + +#[win32_derive::dllexport] +pub fn GetLastActivePopup(_ctx: &mut Context, hWnd: HWND) -> HWND { + // No popups, so a window is its own last active popup. + hWnd } #[win32_derive::dllexport] diff --git a/win32/winapi/src/user32/mod.rs b/win32/winapi/src/user32/mod.rs index f95db470..c0ace47a 100644 --- a/win32/winapi/src/user32/mod.rs +++ b/win32/winapi/src/user32/mod.rs @@ -1,4 +1,5 @@ mod dialog; +mod input; mod message; mod misc; mod rect; @@ -11,6 +12,7 @@ use std::{ }; pub use dialog::*; +pub use input::*; pub use message::*; pub use misc::*; pub use rect::*; @@ -30,6 +32,7 @@ pub struct State { pub wndclass: RefCell>, pub window: RefCell>>>, message_queue: RefCell, + pub input: RefCell, } // TODO: reuse locking pattern from kernel32 @@ -44,5 +47,6 @@ pub fn state() -> &'static State { window: Default::default(), wndclass: Default::default(), message_queue: Default::default(), + input: Default::default(), }) } diff --git a/win32/winapi/src/user32/window.rs b/win32/winapi/src/user32/window.rs index 66fd8837..f3e64e4d 100644 --- a/win32/winapi/src/user32/window.rs +++ b/win32/winapi/src/user32/window.rs @@ -506,8 +506,26 @@ pub fn SetCursor(_ctx: &mut Context, _hCursor: u32) -> u32 { } #[win32_derive::dllexport] -pub fn SetCursorPos(_ctx: &mut Context, _X: i32, _Y: i32) -> bool { - stub!(true) +pub fn SetCursorPos(_ctx: &mut Context, X: i32, Y: i32) -> bool { + // We can't warp the host's cursor, but tracking where the app put it keeps + // GetCursorPos and DirectInput's relative motion consistent with each other. + state().input.borrow_mut().mouse.warp(X, Y); + true +} + +#[win32_derive::dllexport] +pub fn GetCursorPos(ctx: &mut Context, lpPoint: Ptr) -> bool { + crate::user32::pump_host_input(); + let mouse = &state().input.borrow().mouse; + lpPoint + .write( + &mut ctx.memory, + POINT { + x: mouse.x, + y: mouse.y, + }, + ) + .is_some() } #[win32_derive::dllexport] diff --git a/win32/winapi/src/winmm/mmio.rs b/win32/winapi/src/winmm/mmio.rs index d7ee398b..6d246e42 100644 --- a/win32/winapi/src/winmm/mmio.rs +++ b/win32/winapi/src/winmm/mmio.rs @@ -1,8 +1,117 @@ -//! mmio*: RIFF (WAV) file reading, plus MCI. Currently stubs; see the -//! retrowin32 fork's winmm mmio implementation for a reference port. +//! mmio*: the multimedia file I/O API, used to walk RIFF files (.wav), plus MCI. +//! +//! Files are read into memory whole on open and served from there, which keeps +//! seeking and the chunk walk trivial. Callers that read through MMIOINFO's +//! direct buffer instead of calling mmioRead get a pointer into a guest-side +//! copy of the same data. + +use std::collections::HashMap; use runtime::Context; +use crate::{heap::Heap, kernel32}; + +const MMSYSERR_NOERROR: u32 = 0; +const MMIOERR_CANNOTOPEN: u32 = 258; +const MMIOERR_CHUNKNOTFOUND: u32 = 261; + +/// mmioRead/mmioSeek report failure as -1. +const MMIO_FAILURE: i32 = -1; + +const MMIO_FINDCHUNK: u32 = 0x0010; +const MMIO_FINDRIFF: u32 = 0x0020; +const MMIO_FINDLIST: u32 = 0x0040; + +const FOURCC_RIFF: u32 = u32::from_le_bytes(*b"RIFF"); +const FOURCC_LIST: u32 = u32::from_le_bytes(*b"LIST"); + +/// MMCKINFO, the chunk descriptor mmioDescend fills in and mmioAscend reads. +#[repr(C)] +#[derive( + Clone, + Copy, + Debug, + Default, + zerocopy::FromBytes, + zerocopy::IntoBytes, + zerocopy::Immutable, + zerocopy::KnownLayout, +)] +struct MMCKINFO { + ckid: u32, + cksize: u32, + fccType: u32, + dwDataOffset: u32, + dwFlags: u32, +} + +/// Field offsets within MMIOINFO. Only the buffer-related fields matter to us, +/// so rather than model the whole 0x48-byte struct we poke the fields we own +/// and leave the rest of the caller's memory untouched. +mod mmioinfo { + pub const DWFLAGS: u32 = 0x00; + pub const CCHBUFFER: u32 = 0x14; + pub const PCHBUFFER: u32 = 0x18; + pub const PCHNEXT: u32 = 0x1c; + pub const PCHENDREAD: u32 = 0x20; + pub const PCHENDWRITE: u32 = 0x24; + pub const LBUFOFFSET: u32 = 0x28; + pub const LDISKOFFSET: u32 = 0x2c; + pub const HMMIO: u32 = 0x44; +} + +struct File { + data: Vec, + pos: usize, + /// Guest-side copy of `data`, allocated on first direct-buffer access. + buffer: u32, +} + +impl File { + fn read_u32(&self, at: usize) -> Option { + let bytes = self.data.get(at..at + 4)?; + Some(u32::from_le_bytes(bytes.try_into().unwrap())) + } +} + +#[derive(Default)] +pub struct State { + files: HashMap, + next_handle: u32, + /// Guest heap backing the direct-access buffers handed out by mmioGetInfo. + heap: Option, +} + +impl State { + /// The heap backing direct-access buffers, created on first use. + fn heap(&mut self) -> &Heap { + self.heap.get_or_insert_with(|| { + const HEAP_SIZE: u32 = 16 << 20; + let addr = kernel32::lock() + .mappings + .alloc("winmm mmio buffers".into(), HEAP_SIZE); + Heap::new(addr, HEAP_SIZE) + }) + } +} + +/// Ensure a guest-side copy of the file exists; returns (buffer, pos, len). +fn ensure_buffer(ctx: &mut Context, hmmio: u32) -> Option<(u32, u32, u32)> { + let mut winmm = super::state(); + let mmio = winmm.mmio(); + let file = mmio.files.get(&hmmio)?; + let (buffer, pos, len) = (file.buffer, file.pos as u32, file.data.len() as u32); + if buffer != 0 { + return Some((buffer, pos, len)); + } + + let addr = mmio.heap().alloc(&mut ctx.memory, len.max(1)); + let file = mmio.files.get_mut(&hmmio)?; + ctx.memory[addr..][..file.data.len()].copy_from_slice(&file.data); + file.buffer = addr; + Some((addr, pos, len)) +} + #[win32_derive::dllexport] pub fn mciSendCommandA( _ctx: &mut Context, @@ -16,57 +125,222 @@ pub fn mciSendCommandA( } #[win32_derive::dllexport] -pub fn mmioOpenA( - _ctx: &mut Context, - _szFilename: u32, - _lpmmioinfo: u32, - _dwOpenFlags: u32, -) -> u32 { - todo!("mmioOpenA") +pub fn mmioOpenA(ctx: &mut Context, szFilename: u32, _lpmmioinfo: u32, _dwOpenFlags: u32) -> u32 { + if szFilename == 0 { + // Opening a memory buffer rather than a file; no caller needs it. + log::warn!("mmioOpenA: no filename"); + return 0; + } + let name = ctx.memory.read_str(szFilename).to_owned(); + let path = kernel32::resolve_path(&name); + let data = match std::fs::read(&path) { + Ok(data) => data, + Err(err) => { + log::warn!("mmioOpenA({name:?}): {err}"); + return 0; // a null HMMIO means the open failed + } + }; + + let mut winmm = super::state(); + let mmio = winmm.mmio(); + // Handle 0 is reserved for failure. + mmio.next_handle += 1; + let handle = mmio.next_handle; + mmio.files.insert( + handle, + File { + data, + pos: 0, + buffer: 0, + }, + ); + handle } #[win32_derive::dllexport] -pub fn mmioClose(_ctx: &mut Context, _hmmio: u32, _wFlags: u32) -> u32 { - todo!("mmioClose") +pub fn mmioClose(ctx: &mut Context, hmmio: u32, _wFlags: u32) -> u32 { + let mut winmm = super::state(); + let mmio = winmm.mmio(); + let Some(file) = mmio.files.remove(&hmmio) else { + return MMIOERR_CANNOTOPEN; + }; + if file.buffer != 0 { + mmio.heap().free(&mut ctx.memory, file.buffer); + } + MMSYSERR_NOERROR } #[win32_derive::dllexport] -pub fn mmioDescend( - _ctx: &mut Context, - _hmmio: u32, - _lpck: u32, - _lpckParent: u32, - _wFlags: u32, -) -> u32 { - todo!("mmioDescend") +pub fn mmioRead(ctx: &mut Context, hmmio: u32, pch: u32, cch: u32) -> i32 { + let mut winmm = super::state(); + let Some(file) = winmm.mmio().files.get_mut(&hmmio) else { + return MMIO_FAILURE; + }; + let end = (file.pos + cch as usize).min(file.data.len()); + let read = end - file.pos; + ctx.memory[pch..][..read].copy_from_slice(&file.data[file.pos..end]); + file.pos = end; + read as i32 } #[win32_derive::dllexport] -pub fn mmioAscend(_ctx: &mut Context, _hmmio: u32, _lpck: u32, _wFlags: u32) -> u32 { - todo!("mmioAscend") +pub fn mmioSeek(_ctx: &mut Context, hmmio: u32, lOffset: i32, iOrigin: i32) -> i32 { + let mut winmm = super::state(); + let Some(file) = winmm.mmio().files.get_mut(&hmmio) else { + return MMIO_FAILURE; + }; + let base = match iOrigin { + 0 => 0, // SEEK_SET + 1 => file.pos as i64, // SEEK_CUR + 2 => file.data.len() as i64, // SEEK_END + _ => return MMIO_FAILURE, + }; + let pos = base + lOffset as i64; + if pos < 0 || pos as usize > file.data.len() { + return MMIO_FAILURE; + } + file.pos = pos as usize; + file.pos as i32 } #[win32_derive::dllexport] -pub fn mmioRead(_ctx: &mut Context, _hmmio: u32, _pch: u32, _cch: u32) -> i32 { - todo!("mmioRead") +pub fn mmioDescend(ctx: &mut Context, hmmio: u32, lpck: u32, lpckParent: u32, wFlags: u32) -> u32 { + let mut want = ctx.memory.read::(lpck); + // A parent chunk bounds the search to its contents. + let parent_end = if lpckParent != 0 { + let parent = ctx.memory.read::(lpckParent); + (parent.dwDataOffset + parent.cksize) as usize + } else { + usize::MAX + }; + + let mut winmm = super::state(); + let Some(file) = winmm.mmio().files.get_mut(&hmmio) else { + return MMIOERR_CANNOTOPEN; + }; + + loop { + let header = file.pos; + if header + 8 > file.data.len() || header + 8 > parent_end { + return MMIOERR_CHUNKNOTFOUND; + } + let ckid = file.read_u32(header).unwrap(); + let cksize = file.read_u32(header + 4).unwrap(); + // RIFF and LIST chunks start with a form type, and the API reports + // dwDataOffset pointing at it. + let body = header + 8; + let container = ckid == FOURCC_RIFF || ckid == FOURCC_LIST; + let fcc_type = if container { + file.read_u32(body).unwrap_or(0) + } else { + 0 + }; + + let matched = if wFlags & MMIO_FINDRIFF != 0 { + ckid == FOURCC_RIFF && fcc_type == want.fccType + } else if wFlags & MMIO_FINDLIST != 0 { + ckid == FOURCC_LIST && fcc_type == want.fccType + } else if wFlags & MMIO_FINDCHUNK != 0 { + ckid == want.ckid + } else { + true // just describe whatever chunk is here + }; + + if matched { + want.ckid = ckid; + want.cksize = cksize; + want.fccType = fcc_type; + want.dwDataOffset = body as u32; + // Descending leaves the file at the chunk's readable contents, + // which for a container is past the form type. + file.pos = if container { body + 4 } else { body }; + ctx.memory.write(lpck, want); + return MMSYSERR_NOERROR; + } + + // On to the next sibling; chunks are word-aligned. + let mut next = body + cksize as usize; + next += next % 2; + if next <= header || next >= file.data.len() || next >= parent_end { + return MMIOERR_CHUNKNOTFOUND; + } + file.pos = next; + } } #[win32_derive::dllexport] -pub fn mmioSeek(_ctx: &mut Context, _hmmio: u32, _lOffset: i32, _iOrigin: i32) -> i32 { - todo!("mmioSeek") +pub fn mmioAscend(ctx: &mut Context, hmmio: u32, lpck: u32, _wFlags: u32) -> u32 { + let chunk = ctx.memory.read::(lpck); + let mut end = (chunk.dwDataOffset + chunk.cksize) as usize; + end += end % 2; + let mut winmm = super::state(); + let Some(file) = winmm.mmio().files.get_mut(&hmmio) else { + return MMIOERR_CANNOTOPEN; + }; + file.pos = end.min(file.data.len()); + MMSYSERR_NOERROR } +/// Expose the file's contents as a buffer the caller can read from directly. #[win32_derive::dllexport] -pub fn mmioGetInfo(_ctx: &mut Context, _hmmio: u32, _lpmmioinfo: u32, _wFlags: u32) -> u32 { - todo!("mmioGetInfo") +pub fn mmioGetInfo(ctx: &mut Context, hmmio: u32, lpmmioinfo: u32, _wFlags: u32) -> u32 { + let Some((buffer, pos, len)) = ensure_buffer(ctx, hmmio) else { + return MMIOERR_CANNOTOPEN; + }; + // MMIO_DIRTY is for writing, which we don't support; report a plain + // readable buffer covering the whole file. + ctx.memory.write::(lpmmioinfo + mmioinfo::DWFLAGS, 0); + ctx.memory.write::(lpmmioinfo + mmioinfo::CCHBUFFER, len); + ctx.memory + .write::(lpmmioinfo + mmioinfo::PCHBUFFER, buffer); + ctx.memory + .write::(lpmmioinfo + mmioinfo::PCHNEXT, buffer + pos); + ctx.memory + .write::(lpmmioinfo + mmioinfo::PCHENDREAD, buffer + len); + ctx.memory + .write::(lpmmioinfo + mmioinfo::PCHENDWRITE, buffer + len); + // The buffer covers the file from its start, so buffer offsets and file + // offsets coincide. + ctx.memory.write::(lpmmioinfo + mmioinfo::LBUFOFFSET, 0); + ctx.memory + .write::(lpmmioinfo + mmioinfo::LDISKOFFSET, len); + ctx.memory.write::(lpmmioinfo + mmioinfo::HMMIO, hmmio); + MMSYSERR_NOERROR } +/// Take back the file position the caller advanced through pchNext. #[win32_derive::dllexport] -pub fn mmioSetInfo(_ctx: &mut Context, _hmmio: u32, _lpmmioinfo: u32, _wFlags: u32) -> u32 { - todo!("mmioSetInfo") +pub fn mmioSetInfo(ctx: &mut Context, hmmio: u32, lpmmioinfo: u32, _wFlags: u32) -> u32 { + let next = ctx.memory.read::(lpmmioinfo + mmioinfo::PCHNEXT); + let buffer = ctx.memory.read::(lpmmioinfo + mmioinfo::PCHBUFFER); + let mut winmm = super::state(); + let Some(file) = winmm.mmio().files.get_mut(&hmmio) else { + return MMIOERR_CANNOTOPEN; + }; + file.pos = (next.saturating_sub(buffer) as usize).min(file.data.len()); + MMSYSERR_NOERROR } +/// Refill the caller's buffer window. The whole file is already resident, so +/// this only syncs the position; pchNext == pchEndRead then signals EOF. #[win32_derive::dllexport] -pub fn mmioAdvance(_ctx: &mut Context, _hmmio: u32, _lpmmioinfo: u32, _wFlags: u32) -> u32 { - todo!("mmioAdvance") +pub fn mmioAdvance(ctx: &mut Context, hmmio: u32, lpmmioinfo: u32, _wFlags: u32) -> u32 { + let Some((buffer, _, len)) = ensure_buffer(ctx, hmmio) else { + return MMIOERR_CANNOTOPEN; + }; + let next = ctx.memory.read::(lpmmioinfo + mmioinfo::PCHNEXT); + let pos = next.saturating_sub(buffer).min(len); + { + let mut winmm = super::state(); + let Some(file) = winmm.mmio().files.get_mut(&hmmio) else { + return MMIOERR_CANNOTOPEN; + }; + file.pos = pos as usize; + } + + ctx.memory + .write::(lpmmioinfo + mmioinfo::PCHNEXT, buffer + pos); + ctx.memory + .write::(lpmmioinfo + mmioinfo::PCHENDREAD, buffer + len); + MMSYSERR_NOERROR } diff --git a/win32/winapi/src/winmm/mod.rs b/win32/winapi/src/winmm/mod.rs index 7c4f410d..94432c92 100644 --- a/win32/winapi/src/winmm/mod.rs +++ b/win32/winapi/src/winmm/mod.rs @@ -15,11 +15,21 @@ pub use mmio::*; pub struct State { timer: Option, wave: Option, + mmio: Option, +} + +impl State { + /// The mmio file table, created on first use (a `static` can't build the + /// map up front). + pub fn mmio(&mut self) -> &mut mmio::State { + self.mmio.get_or_insert_with(Default::default) + } } static STATE: Mutex = Mutex::new(State { timer: None, wave: None, + mmio: None, }); pub fn state() -> MutexGuard<'static, State> { From 7a6fd0f01e12b4ca63a36b2cb613488d2fcea8b1 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Tue, 21 Jul 2026 01:51:23 +0300 Subject: [PATCH 03/10] shard generated code into parts --- host/src/wasm.rs | 33 +++++++--- out/mofo-unpack/src/main.rs | 2 +- tc/src/codegen/mod.rs | 108 ++++++++++++++++++++++++++----- win32/winapi/src/dsound.rs | 13 ++-- win32/winapi/src/kernel32/env.rs | 2 +- win32/winapi/src/kernel32/nls.rs | 2 +- 6 files changed, 127 insertions(+), 33 deletions(-) diff --git a/host/src/wasm.rs b/host/src/wasm.rs index a0e18ad9..db00c36c 100644 --- a/host/src/wasm.rs +++ b/host/src/wasm.rs @@ -64,18 +64,19 @@ impl Window { } } +/// Audio output. Not wired up to the browser yet, so it silently swallows +/// what the mixer produces rather than stopping the program. pub struct AudioStream {} impl AudioStream { pub fn queued_bytes(&self) -> u32 { - todo!() - } - pub fn put_data(&self, _data: &[u8]) { - todo!() + // Reporting a full queue keeps the mixer from producing audio that + // has nowhere to go. + u32::MAX } - pub fn resume(&self) { - todo!() - } + pub fn put_data(&self, _data: &[u8]) {} + + pub fn resume(&self) {} } pub struct Host { @@ -104,7 +105,7 @@ impl Host { } pub fn create_audio_stream(&self, _spec: host::AudioSpec) -> AudioStream { - todo!() + AudioStream {} } pub fn time(&self) -> u32 { @@ -220,6 +221,22 @@ impl WebHostSendChannel { _ => unreachable!(), } } + 5 | 6 => { + // The front end translates the browser's KeyboardEvent.code + // into the PC scan code and VK_* pair the guest expects; see + // the SDL host's key table for the mapping. + let key = host::KeyMessage { + scancode: buf[1] as u8, + vkey: buf[2] as u8, + extended: buf[3] & 1 != 0, + repeat: buf[3] & 2 != 0, + }; + if buf[0] == 5 { + host::Message::KeyDown(key) + } else { + host::Message::KeyUp(key) + } + } msg => todo!("host message {msg}"), }) } diff --git a/out/mofo-unpack/src/main.rs b/out/mofo-unpack/src/main.rs index 53b0bfae..e7dad5fd 100644 --- a/out/mofo-unpack/src/main.rs +++ b/out/mofo-unpack/src/main.rs @@ -133,7 +133,7 @@ pub fn do_unpack(ctx: &mut runtime::Context) { code_memory: 0x40_0000..tc.mem.bytes.len() as u32, resources: None, imports: syms, - vtables: vec![], + ..Default::default() }); tc.init_imports(); diff --git a/tc/src/codegen/mod.rs b/tc/src/codegen/mod.rs index f9742bc1..1a602886 100644 --- a/tc/src/codegen/mod.rs +++ b/tc/src/codegen/mod.rs @@ -10,6 +10,29 @@ mod string; use crate::{Block, BlockType, Instr, Module, State, memory::Memory, write_if_changed}; +/// Preamble shared by the generated module and its parts. +const HEADER: &str = "//! this module was generated by tc + +#![allow(unreachable_code)] +#![allow(unused_parens)] +#![allow(unused_variables)] +#![allow(non_snake_case)] + +use runtime::*; +"; + +/// Preamble for a part file: everything the module file has in scope, +/// including its sibling parts, reaches it through `super`. +const PART_HEADER: &str = "//! this module was generated by tc + +#![allow(unreachable_code)] +#![allow(unused_parens)] +#![allow(unused_variables)] +#![allow(non_snake_case)] + +use super::*; +"; + fn reg_name(r: iced_x86::Register) -> String { format!("{r:?}").to_ascii_lowercase() } @@ -406,28 +429,81 @@ ctx.cpu.regs.esp = {stack_pointer:#x}; self.line(""); } - fn gen_blocks(&mut self) { + /// Emit the block functions, split across `generated/part_NN.rs` files + /// once there are enough of them. + /// + /// rustc's time on one module grows worse than linearly, and a large + /// program's blocks would otherwise land in a single ten-megabyte file that + /// has to be reparsed in full on every rebuild. Returns the part names, or + /// an empty list when everything fit in the module file itself. + fn gen_blocks(&mut self, outdir: &str) -> Result> { + /// Split once a part reaches roughly this size. Small programs stay in + /// a single file, which keeps their generated output easy to read. + const MAX_PART_BYTES: usize = 1 << 20; + let mut addrs = self.blocks.keys().copied().collect::>(); addrs.sort(); + + // Generate every block into one buffer first, remembering where each + // one starts so parts can be cut at function boundaries. + let module_buf = std::mem::take(&mut self.buf); + let mut boundaries = vec![0usize]; for &addr in &addrs { let block = self.blocks.get(&addr).unwrap(); self.gen_block(&block); + boundaries.push(self.buf.len()); } - for addr in self.unknown.iter().copied().collect::>() { self.line(format!( "pub fn unk_{addr:x}(_ctx: &mut Context) -> Cont {{ runtime::unknown_block({addr:#x}) }}\n" )); + boundaries.push(self.buf.len()); + } + let blocks_text = std::mem::replace(&mut self.buf, module_buf); + + if blocks_text.len() <= MAX_PART_BYTES { + self.buf.push_str(&blocks_text); + self.gen_blocks_table(&addrs); + return Ok(Vec::new()); } + // Cut at the first block boundary past each part's size limit. + std::fs::create_dir_all(format!("{outdir}/src/generated"))?; + let mut parts = Vec::new(); + let mut start = 0; + while start < blocks_text.len() { + let target = start + MAX_PART_BYTES; + let end = match boundaries.iter().find(|&&b| b >= target) { + Some(&end) => end, + None => blocks_text.len(), + }; + let name = format!("part_{:02}", parts.len()); + let path = format!("{outdir}/src/generated/{name}.rs"); + let text = format!("{}\n{}", PART_HEADER, &blocks_text[start..end]); + write_if_changed(&path, rustfmt(&text)?.as_bytes()) + .map_err(|err| anyhow!("write {path}: {err}"))?; + parts.push(name); + start = end; + } + + for name in &parts { + self.line(format!("mod {name};")); + self.line(format!("pub use {name}::*;")); + } + self.line(""); + self.gen_blocks_table(&addrs); + Ok(parts) + } + + fn gen_blocks_table(&mut self, addrs: &[u32]) { self.line(format!( "const BLOCKS: [(u32, ContFn); {}] = [\n", addrs.len() + 1, )); - for &addr in &addrs { + for &addr in addrs { let block = self.blocks.get(&addr).unwrap(); self.line(format!("({addr:#x}, {name}),", name = block.name())); } @@ -444,24 +520,14 @@ ctx.cpu.regs.esp = {stack_pointer:#x}; } pub fn gen_file(&mut self, outdir: &str) -> Result<()> { - self.line( - "//! this module was generated by tc - -#![allow(unreachable_code)] -#![allow(unused_parens)] -#![allow(unused_variables)] -#![allow(non_snake_case)] - -use runtime::*; -", - ); + self.line(HEADER); if self.module.is_windows() { self.line("use winapi::*;"); } self.gen_init(); - self.gen_blocks(); + let parts = self.gen_blocks(outdir)?; // TODO: split EXEDATA into DOS/Windows. let resources = match self.module { @@ -492,6 +558,18 @@ use runtime::*; let path = format!("{outdir}/src/generated.rs"); let text = rustfmt(&self.buf)?; write_if_changed(&path, text.as_bytes()).map_err(|err| anyhow!("write {path}: {err}"))?; + + // Drop parts left over from a previous run that produced more of them, + // which would otherwise fail to compile against the current output. + if let Ok(entries) = std::fs::read_dir(format!("{outdir}/src/generated")) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + let stem = name.trim_end_matches(".rs"); + if stem.starts_with("part_") && !parts.iter().any(|part| part == stem) { + std::fs::remove_file(entry.path())?; + } + } + } Ok(()) } } diff --git a/win32/winapi/src/dsound.rs b/win32/winapi/src/dsound.rs index d7a11953..3fda1a97 100644 --- a/win32/winapi/src/dsound.rs +++ b/win32/winapi/src/dsound.rs @@ -17,8 +17,11 @@ use zerocopy::FromBytes; use crate::{dllexport::win32flags, heap::Heap, kernel32, locked_state::LockedState}; -/// When true, write a debug `out.wav` of the mixed output. -const WRITE_WAV: bool = false; +/// Set THESEUS_DSOUND_WAV to a path to dump the mixed output there, for +/// checking what the mixer actually produced. +fn wav_debug_path() -> Option { + std::env::var("THESEUS_DSOUND_WAV").ok() +} /// Host mix rate, in stereo i16. const HOST_RATE: u32 = 44100; @@ -166,11 +169,7 @@ fn init() { buffers: HashMap::default(), stream: None, heap: None, - write: if WRITE_WAV { - Some(WavWrite::new("out.wav")) - } else { - None - }, + write: wav_debug_path().map(|path| WavWrite::new(&path)), }); } } diff --git a/win32/winapi/src/kernel32/env.rs b/win32/winapi/src/kernel32/env.rs index 0bc87842..adee7a61 100644 --- a/win32/winapi/src/kernel32/env.rs +++ b/win32/winapi/src/kernel32/env.rs @@ -20,7 +20,7 @@ pub fn GetEnvironmentStrings(_ctx: &mut Context) -> u32 { */ // An empty environment block: a list of nul-terminated strings, // terminated by an extra nul. - let mut kernel32 = lock(); + let kernel32 = lock(); let addr = kernel32.process_heap.alloc(&mut _ctx.memory, 2); _ctx.memory[addr..][..2].fill(0); addr diff --git a/win32/winapi/src/kernel32/nls.rs b/win32/winapi/src/kernel32/nls.rs index 014c5eaf..53e4356e 100644 --- a/win32/winapi/src/kernel32/nls.rs +++ b/win32/winapi/src/kernel32/nls.rs @@ -1,6 +1,6 @@ use runtime::Context; -use crate::{Ptr, stub}; +use crate::Ptr; #[win32_derive::dllexport] pub fn GetACP(_ctx: &mut Context) -> u32 { From 44180a089ccc50e3dc1315a47ff44f1c73922984 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Tue, 21 Jul 2026 03:45:31 +0300 Subject: [PATCH 04/10] winpin on web --- build-wasm.sh | 4 +- host/src/fs.rs | 406 +++++++++++++++++++++++++++++ host/src/lib.rs | 26 +- host/src/sdl.rs | 27 +- host/src/wasm.rs | 110 +++++++- web/.gitignore | 2 + web/README.md | 23 ++ web/host.ts | 308 ++++++++++++++++++++-- web/static-server.go | 35 ++- web/worker.ts | 78 +++++- win32/winapi/src/kernel32/event.rs | 2 +- win32/winapi/src/kernel32/file.rs | 38 ++- win32/winapi/src/lib.rs | 10 +- win32/winapi/src/winmm/mmio.rs | 2 +- 14 files changed, 1000 insertions(+), 71 deletions(-) create mode 100644 host/src/fs.rs diff --git a/build-wasm.sh b/build-wasm.sh index e7fe01ae..585077fb 100755 --- a/build-wasm.sh +++ b/build-wasm.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash set -e @@ -28,7 +28,7 @@ fi desired="$1" -for package in winapi-exe mine basicdd; do +for package in winapi-exe mine basicdd winpin; do file=$(sed -e 's/-exe//' <<< "$package") if [[ "$desired" != "" && "$desired" != "$file" ]]; then continue diff --git a/host/src/fs.rs b/host/src/fs.rs new file mode 100644 index 00000000..560604e0 --- /dev/null +++ b/host/src/fs.rs @@ -0,0 +1,406 @@ +//! Filesystem access, as the emulated program sees it. +//! +//! Natively this is a thin wrapper over `std::fs`. In the browser there is no +//! filesystem at all, so the wasm build serves an in-memory tree that the page +//! fills in before starting the program (see [`mount`]). + +use std::{ + io::{Read, Result, Seek, Write}, + path::{Path, PathBuf}, +}; + +/// How to open a file, mirroring the subset of `std::fs::OpenOptions` that +/// CreateFile needs. +#[derive(Default, Clone, Copy)] +pub struct OpenOptions { + pub read: bool, + pub write: bool, + pub create: bool, + /// Fail if the file already exists. + pub create_new: bool, + pub truncate: bool, +} + +impl OpenOptions { + pub fn new() -> Self { + Default::default() + } + + pub fn read(&mut self, read: bool) -> &mut Self { + self.read = read; + self + } + pub fn write(&mut self, write: bool) -> &mut Self { + self.write = write; + self + } + pub fn create(&mut self, create: bool) -> &mut Self { + self.create = create; + self + } + pub fn create_new(&mut self, create_new: bool) -> &mut Self { + self.create_new = create_new; + self + } + pub fn truncate(&mut self, truncate: bool) -> &mut Self { + self.truncate = truncate; + self + } + + pub fn open(&self, path: impl AsRef) -> Result { + open(path.as_ref(), *self) + } +} + +/// One entry of a directory listing. +pub struct DirEntry { + pub name: String, + pub is_dir: bool, + pub len: u64, +} + +pub fn read(path: impl AsRef) -> Result> { + let mut buf = Vec::new(); + OpenOptions::new() + .read(true) + .open(path)? + .read_to_end(&mut buf)?; + Ok(buf) +} + +#[cfg(not(target_family = "wasm"))] +mod imp { + use super::*; + + pub struct File(std::fs::File); + + impl File { + pub fn set_len(&mut self, len: u64) -> Result<()> { + self.0.set_len(len) + } + } + + impl Read for File { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.0.read(buf) + } + } + + impl Write for File { + fn write(&mut self, buf: &[u8]) -> Result { + self.0.write(buf) + } + fn flush(&mut self) -> Result<()> { + self.0.flush() + } + } + + impl Seek for File { + fn seek(&mut self, pos: std::io::SeekFrom) -> Result { + self.0.seek(pos) + } + } + + pub fn open(path: &Path, options: OpenOptions) -> Result { + let mut opts = std::fs::OpenOptions::new(); + opts.read(options.read) + .write(options.write) + .create(options.create) + .create_new(options.create_new) + .truncate(options.truncate); + opts.open(path).map(File) + } + + pub fn read_dir(path: &Path) -> Result> { + let mut entries = Vec::new(); + for entry in std::fs::read_dir(path)? { + let entry = entry?; + let meta = entry.metadata().ok(); + entries.push(DirEntry { + name: entry.file_name().to_string_lossy().into_owned(), + is_dir: meta.as_ref().map(|m| m.is_dir()).unwrap_or(false), + len: meta.as_ref().map(|m| m.len()).unwrap_or(0), + }); + } + Ok(entries) + } + + pub fn remove_file(path: &Path) -> Result<()> { + std::fs::remove_file(path) + } + + pub fn exists(path: &Path) -> bool { + path.exists() + } + + pub fn current_dir() -> Result { + std::env::current_dir() + } + + pub fn set_current_dir(path: &Path) -> Result<()> { + std::env::set_current_dir(path) + } +} + +#[cfg(target_family = "wasm")] +mod imp { + use std::{ + collections::BTreeMap, + sync::{Mutex, MutexGuard}, + }; + + use super::*; + + /// An in-memory filesystem, keyed by absolute '/'-separated path. + /// Directories are implied by the paths of the files inside them. + #[derive(Default)] + struct MemFs { + files: BTreeMap>, + cwd: String, + } + + static FS: Mutex> = Mutex::new(None); + + fn fs() -> MutexGuard<'static, Option> { + let mut fs = FS.lock().unwrap(); + if fs.is_none() { + *fs = Some(MemFs { + files: Default::default(), + cwd: "/".into(), + }); + } + fs + } + + fn normalize(path: &Path) -> String { + let path = path.to_string_lossy().replace('\\', "/"); + let mut parts: Vec<&str> = Vec::new(); + if !path.starts_with('/') { + let cwd = fs().as_ref().unwrap().cwd.clone(); + for part in cwd.split('/') { + if !part.is_empty() { + // Leaked to keep the borrow simple; paths are short-lived + // and few, and this only runs in the wasm build. + parts.push(Box::leak(part.to_string().into_boxed_str())); + } + } + } + for part in path.split('/') { + match part { + "" | "." => {} + ".." => { + parts.pop(); + } + part => parts.push(Box::leak(part.to_string().into_boxed_str())), + } + } + format!("/{}", parts.join("/")) + } + + fn not_found() -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::NotFound, "no such file") + } + + /// Add a file to the in-memory filesystem. The page calls this for each + /// file of the program's data before starting it. + pub fn mount(path: &str, data: Vec) { + let path = normalize(Path::new(path)); + fs().as_mut().unwrap().files.insert(path, data); + } + + pub struct File { + path: String, + data: Vec, + pos: u64, + writable: bool, + dirty: bool, + } + + impl File { + pub fn set_len(&mut self, len: u64) -> Result<()> { + self.data.resize(len as usize, 0); + self.dirty = true; + Ok(()) + } + + fn store(&mut self) { + if !self.dirty { + return; + } + self.dirty = false; + fs().as_mut() + .unwrap() + .files + .insert(self.path.clone(), self.data.clone()); + crate::host().persist_file(&self.path, &self.data); + } + } + + impl Drop for File { + fn drop(&mut self) { + self.store(); + } + } + + impl Read for File { + fn read(&mut self, buf: &mut [u8]) -> Result { + let start = (self.pos as usize).min(self.data.len()); + let n = buf.len().min(self.data.len() - start); + buf[..n].copy_from_slice(&self.data[start..start + n]); + self.pos += n as u64; + Ok(n) + } + } + + impl Write for File { + fn write(&mut self, buf: &[u8]) -> Result { + if !self.writable { + return Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "file not open for writing", + )); + } + let start = self.pos as usize; + if self.data.len() < start + buf.len() { + self.data.resize(start + buf.len(), 0); + } + self.data[start..start + buf.len()].copy_from_slice(buf); + self.pos += buf.len() as u64; + self.dirty = true; + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<()> { + self.store(); + Ok(()) + } + } + + impl Seek for File { + fn seek(&mut self, pos: std::io::SeekFrom) -> Result { + use std::io::SeekFrom::*; + let base = match pos { + Start(n) => n as i64, + Current(n) => self.pos as i64 + n, + End(n) => self.data.len() as i64 + n, + }; + if base < 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "seek before start", + )); + } + self.pos = base as u64; + Ok(self.pos) + } + } + + pub fn open(path: &Path, options: OpenOptions) -> Result { + let path = normalize(path); + let existing = fs().as_ref().unwrap().files.get(&path).cloned(); + if existing.is_some() && options.create_new { + return Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + "file exists", + )); + } + let data = match existing { + Some(data) if !options.truncate => data, + Some(_) => Vec::new(), + None if options.create || options.create_new => Vec::new(), + None => return Err(not_found()), + }; + Ok(File { + path, + data, + pos: 0, + writable: options.write, + // A newly created file exists even if nothing is written to it. + dirty: options.create || options.create_new || options.truncate, + }) + } + + pub fn read_dir(path: &Path) -> Result> { + let dir = normalize(path); + let prefix = dir_prefix(&dir); + let mut entries: BTreeMap = BTreeMap::new(); + for (path, data) in &fs().as_ref().unwrap().files { + let Some(rest) = path.strip_prefix(&prefix) else { + continue; + }; + match rest.split_once('/') { + // A file directly in this directory. + None => { + entries.insert( + rest.to_string(), + DirEntry { + name: rest.to_string(), + is_dir: false, + len: data.len() as u64, + }, + ); + } + // Something deeper: the first component is a subdirectory. + Some((name, _)) => { + entries.entry(name.to_string()).or_insert_with(|| DirEntry { + name: name.to_string(), + is_dir: true, + len: 0, + }); + } + } + } + if entries.is_empty() { + return Err(not_found()); + } + Ok(entries.into_values().collect()) + } + + pub fn remove_file(path: &Path) -> Result<()> { + let path = normalize(path); + match fs().as_mut().unwrap().files.remove(&path) { + Some(_) => Ok(()), + None => Err(not_found()), + } + } + + /// The prefix every path inside `dir` starts with, without doubling the + /// slash at the root. + fn dir_prefix(dir: &str) -> String { + if dir.ends_with('/') { + dir.to_string() + } else { + format!("{dir}/") + } + } + + pub fn exists(path: &Path) -> bool { + let path = normalize(path); + // The root is always there, even before anything is mounted. + if path == "/" { + return true; + } + let fs = fs(); + let files = &fs.as_ref().unwrap().files; + // Either a file, or a directory that has something in it. + let prefix = dir_prefix(&path); + files.contains_key(&path) || files.keys().any(|key| key.starts_with(&prefix)) + } + + pub fn current_dir() -> Result { + Ok(PathBuf::from(&fs().as_ref().unwrap().cwd)) + } + + pub fn set_current_dir(path: &Path) -> Result<()> { + let path = normalize(path); + if !exists(Path::new(&path)) { + return Err(not_found()); + } + fs().as_mut().unwrap().cwd = path; + Ok(()) + } +} + +pub use imp::{File, current_dir, exists, open, read_dir, remove_file, set_current_dir}; +#[cfg(target_family = "wasm")] +pub use imp::mount; diff --git a/host/src/lib.rs b/host/src/lib.rs index 40c7a729..d6b41324 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -5,6 +5,7 @@ use std::sync::LazyLock; +pub mod fs; #[cfg(not(target_family = "wasm"))] mod sdl; mod single_thread; @@ -25,7 +26,7 @@ pub struct AudioSpec { } bitflags::bitflags! { - #[derive(Debug, Clone, Copy, Eq, PartialEq)] + #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] pub struct MouseButton: u16 { const Left = 1 << 0; const Middle = 1 << 1; @@ -70,6 +71,29 @@ pub enum Message { KeyUp(KeyMessage), } +/// Which winapi calls to trace, in the syntax `trace::init` parses. +#[cfg(not(target_family = "wasm"))] +pub fn trace_spec() -> String { + std::env::var("THESEUS_TRACE").unwrap_or_default() +} + +/// The web build has no environment to read a spec from, so the page sets one +/// with `set_trace` before starting the program. Tracing every call is slow +/// enough to change how a program behaves, so it is off unless asked for. +#[cfg(target_family = "wasm")] +static TRACE_SPEC: std::sync::Mutex = std::sync::Mutex::new(String::new()); + +#[cfg(target_family = "wasm")] +pub fn trace_spec() -> String { + TRACE_SPEC.lock().unwrap().clone() +} + +#[cfg(target_family = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn set_trace(spec: &str) { + *TRACE_SPEC.lock().unwrap() = spec.to_string(); +} + pub fn init() { logger::init(); LazyLock::force(&HOST); diff --git a/host/src/sdl.rs b/host/src/sdl.rs index 96c21fcf..40611123 100644 --- a/host/src/sdl.rs +++ b/host/src/sdl.rs @@ -165,6 +165,12 @@ const KEY_MAP: &[(sdl::scancode::SDL_Scancode, u8, u8, bool)] = { ] }; +thread_local! { + /// Mouse buttons currently held, maintained as events arrive. + static BUTTONS: std::cell::Cell = + const { std::cell::Cell::new(host::MouseButton::empty()) }; +} + fn key_from_sdl(event: &sdl::events::SDL_KeyboardEvent) -> Option { let &(_, scancode, vkey, extended) = KEY_MAP.iter().find(|key| key.0 == event.scancode)?; Some(host::KeyMessage { @@ -183,6 +189,8 @@ fn msg_from_event(event: &sdl::events::SDL_Event) -> Option { SDL_EventType::WINDOW_EXPOSED => return Some(host::Message::Paint), SDL_EventType::MOUSE_MOTION => { let event = &event.motion; + // Motion events do carry the mask, so resync from them. + BUTTONS.with(|held| held.set(mouse_buttons_from_sdl(event.state))); return Some(host::Message::MouseMove(host::MouseMessage { x: event.x as u32, y: event.y as u32, @@ -198,11 +206,26 @@ fn msg_from_event(event: &sdl::events::SDL_Event) -> Option { sdl::mouse::SDL_BUTTON_RIGHT => host::MouseButton::Right, _ => return None, }; + // `buttons` has to be the state right after this event: a + // release that reported its button as still held would leave + // it stuck down. Button events carry no mask, and the live + // state is the state now rather than when the event happened, + // so track it as events arrive. + let buttons = BUTTONS.with(|held| { + let mut held = held.get(); + if typ == SDL_EventType::MOUSE_BUTTON_DOWN { + held.insert(button); + } else { + held.remove(button); + } + BUTTONS.with(|cell| cell.set(held)); + held + }); let message = host::MouseMessage { x: event.x as u32, y: event.y as u32, - button: button, - buttons: button, + button, + buttons, }; if typ == SDL_EventType::MOUSE_BUTTON_DOWN { return Some(host::Message::MouseDown(message)); diff --git a/host/src/wasm.rs b/host/src/wasm.rs index db00c36c..0b70896a 100644 --- a/host/src/wasm.rs +++ b/host/src/wasm.rs @@ -64,19 +64,32 @@ impl Window { } } -/// Audio output. Not wired up to the browser yet, so it silently swallows -/// what the mixer produces rather than stopping the program. -pub struct AudioStream {} +/// Audio output, played by the page through Web Audio. +pub struct AudioStream { + id: i32, +} + impl AudioStream { + fn new(sample_rate: u32, channels: u32) -> Self { + let id = host::host() + .chan + .lock() + .unwrap() + .create_audio_stream(sample_rate, channels); + AudioStream { id } + } + pub fn queued_bytes(&self) -> u32 { - // Reporting a full queue keeps the mixer from producing audio that - // has nowhere to go. - u32::MAX + host::host().chan.lock().unwrap().audio_queued(self.id) } - pub fn put_data(&self, _data: &[u8]) {} + pub fn put_data(&self, data: &[u8]) { + host::host().chan.lock().unwrap().audio_write(self.id, data); + } - pub fn resume(&self) {} + pub fn resume(&self) { + host::host().chan.lock().unwrap().audio_resume(self.id); + } } pub struct Host { @@ -104,8 +117,8 @@ impl Host { Window::new(title, width, height) } - pub fn create_audio_stream(&self, _spec: host::AudioSpec) -> AudioStream { - AudioStream {} + pub fn create_audio_stream(&self, spec: host::AudioSpec) -> AudioStream { + AudioStream::new(spec.sample_rate, spec.channels) } pub fn time(&self) -> u32 { @@ -119,6 +132,27 @@ impl Host { pub fn console_write(&self, text: &[u8]) { self.chan.lock().unwrap().console_write(text); } + + /// Hand a written file to the page, which keeps it across reloads. + pub fn persist_file(&self, path: &str, data: &[u8]) { + self.chan.lock().unwrap().write_file(path, data); + } +} + +/// Add a file to the program's filesystem. The page calls this for each file +/// of the program's data before starting it. +#[wasm_bindgen] +pub fn mount_file(path: &str, data: &[u8]) { + crate::fs::mount(path, data.to_vec()); +} + +/// Set the directory the program starts in, the equivalent of launching it +/// from that directory natively. +#[wasm_bindgen] +pub fn set_current_dir(path: &str) { + if let Err(err) = crate::fs::set_current_dir(std::path::Path::new(path)) { + log::warn!("set_current_dir({path}): {err}"); + } } struct WebHostSendChannel {} @@ -142,6 +176,13 @@ export interface WasmHost { set_pixels(surface_id: number, ptr: number, len: number): void; + write_file(path: string, ptr: number, len: number): number; + + create_audio_stream(sample_rate: number, channels: number): number; + audio_queued(id: number): number; + audio_write(id: number, ptr: number, len: number): number; + audio_resume(id: number): number; + poll_message(): number[]; wait_message(): Promise; } @@ -194,6 +235,45 @@ impl WebHostSendChannel { self.send_async("render", args); } + pub fn create_audio_stream(&mut self, sample_rate: u32, channels: u32) -> i32 { + let args = js_sys::Array::new(); + args.push(&JsValue::from(sample_rate)); + args.push(&JsValue::from(channels)); + self.send_sync("create_audio_stream", args) + } + + /// Bytes handed to the page but not played yet; the mixer paces itself + /// against this. + pub fn audio_queued(&mut self, id: i32) -> u32 { + let args = js_sys::Array::new(); + args.push(&JsValue::from(id)); + // The page returns one more than the real count so that zero, which + // the synchronization protocol reserves, never comes back. + (self.send_sync("audio_queued", args) - 1) as u32 + } + + pub fn audio_write(&mut self, id: i32, data: &[u8]) { + let args = js_sys::Array::new(); + args.push(&JsValue::from(id)); + args.push(&JsValue::from(data.as_ptr() as u32)); + args.push(&JsValue::from(data.len())); + self.send_sync("audio_write", args); + } + + pub fn audio_resume(&mut self, id: i32) { + let args = js_sys::Array::new(); + args.push(&JsValue::from(id)); + self.send_sync("audio_resume", args); + } + + pub fn write_file(&mut self, path: &str, data: &[u8]) { + let args = js_sys::Array::new(); + args.push(&JsValue::from(path)); + args.push(&JsValue::from(data.as_ptr() as u32)); + args.push(&JsValue::from(data.len())); + self.send_sync("write_file", args); + } + pub fn set_pixels(&mut self, id: i32, pixels: &[u8]) { let args = js_sys::Array::new(); args.push(&JsValue::from(id)); @@ -207,12 +287,16 @@ impl WebHostSendChannel { Some(match buf[0] { -1 => return None, 2 | 3 | 4 => { - let buttons = host::MouseButton::from_bits(buf[3] as u16).unwrap(); + // The button that changed is in the low half, the buttons held + // after the event in the high half. + let button = host::MouseButton::from_bits(buf[3] as u16).unwrap_or_default(); + let buttons = + host::MouseButton::from_bits((buf[3] >> 16) as u16).unwrap_or_default(); let mouse = host::MouseMessage { x: buf[1] as u32, y: buf[2] as u32, - button: buttons, - buttons: buttons, + button, + buttons, }; match buf[0] { 2 => host::Message::MouseDown(mouse), diff --git a/web/.gitignore b/web/.gitignore index 26572b84..0846fe31 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -2,3 +2,5 @@ *.js *.wasm *.map +/game +/node_modules diff --git a/web/README.md b/web/README.md index e0134c58..654e5816 100644 --- a/web/README.md +++ b/web/README.md @@ -2,3 +2,26 @@ This directory contains the hosting implementation for running wasm output. See https://neugierig.org/software/blog/2026/05/theseus-wasm.html for an overview. + +## Running a target + +Build the wasm for a target, then serve this directory: + +``` +bash build-wasm.sh --release winpin +(cd web && go run static-server.go) +``` + +The server sets the cross-origin isolation headers that SharedArrayBuffer +needs, and accepts two POSTs that make a page running under a script +observable: `/log` (messages, mirrored from the program's own log when the URL +carries `?frames=1`) and `/frame` (a PNG of the program's window). + +A program with data files reads them from an in-memory filesystem that the page +fills in before starting it. Point `dataRoot` in host.ts at a directory holding +the files plus a `manifest.json` listing them, and set `cwd` to the directory +the program should start in. Files the program writes are kept in localStorage +and restored on the next load. + +URL parameters: `?frames=1` reports progress and window contents to the server, +`?trace=` traces winapi calls in the same syntax as `THESEUS_TRACE`. diff --git a/web/host.ts b/web/host.ts index dda0692b..26ff1830 100644 --- a/web/host.ts +++ b/web/host.ts @@ -2,6 +2,119 @@ import type * as exe from "./exe/basicdd/basicdd.js"; import type * as worker from "./worker.js"; + +/// Prefix for saved files kept in localStorage. +const SAVE_PREFIX = "theseus:file:"; + +/// KeyboardEvent.code -> [PC set-1 scan code, Windows VK_*, extended key]. +/// Mirrors the SDL host's table in host/src/sdl.rs; keys with no PC/AT +/// equivalent are simply absent. +const KEY_MAP: Record = { + Escape: [0x01, 0x1b, false], + Digit1: [0x02, 0x31, false], + Digit2: [0x03, 0x32, false], + Digit3: [0x04, 0x33, false], + Digit4: [0x05, 0x34, false], + Digit5: [0x06, 0x35, false], + Digit6: [0x07, 0x36, false], + Digit7: [0x08, 0x37, false], + Digit8: [0x09, 0x38, false], + Digit9: [0x0a, 0x39, false], + Digit0: [0x0b, 0x30, false], + Minus: [0x0c, 0xbd, false], + Equal: [0x0d, 0xbb, false], + Backspace: [0x0e, 0x08, false], + Tab: [0x0f, 0x09, false], + KeyQ: [0x10, 0x51, false], + KeyW: [0x11, 0x57, false], + KeyE: [0x12, 0x45, false], + KeyR: [0x13, 0x52, false], + KeyT: [0x14, 0x54, false], + KeyY: [0x15, 0x59, false], + KeyU: [0x16, 0x55, false], + KeyI: [0x17, 0x49, false], + KeyO: [0x18, 0x4f, false], + KeyP: [0x19, 0x50, false], + BracketLeft: [0x1a, 0xdb, false], + BracketRight: [0x1b, 0xdd, false], + Enter: [0x1c, 0x0d, false], + ControlLeft: [0x1d, 0xa2, false], + KeyA: [0x1e, 0x41, false], + KeyS: [0x1f, 0x53, false], + KeyD: [0x20, 0x44, false], + KeyF: [0x21, 0x46, false], + KeyG: [0x22, 0x47, false], + KeyH: [0x23, 0x48, false], + KeyJ: [0x24, 0x4a, false], + KeyK: [0x25, 0x4b, false], + KeyL: [0x26, 0x4c, false], + Semicolon: [0x27, 0xba, false], + Quote: [0x28, 0xde, false], + Backquote: [0x29, 0xc0, false], + ShiftLeft: [0x2a, 0xa0, false], + Backslash: [0x2b, 0xdc, false], + KeyZ: [0x2c, 0x5a, false], + KeyX: [0x2d, 0x58, false], + KeyC: [0x2e, 0x43, false], + KeyV: [0x2f, 0x56, false], + KeyB: [0x30, 0x42, false], + KeyN: [0x31, 0x4e, false], + KeyM: [0x32, 0x4d, false], + Comma: [0x33, 0xbc, false], + Period: [0x34, 0xbe, false], + Slash: [0x35, 0xbf, false], + ShiftRight: [0x36, 0xa1, false], + NumpadMultiply: [0x37, 0x6a, false], + AltLeft: [0x38, 0xa4, false], + Space: [0x39, 0x20, false], + CapsLock: [0x3a, 0x14, false], + F1: [0x3b, 0x70, false], + F2: [0x3c, 0x71, false], + F3: [0x3d, 0x72, false], + F4: [0x3e, 0x73, false], + F5: [0x3f, 0x74, false], + F6: [0x40, 0x75, false], + F7: [0x41, 0x76, false], + F8: [0x42, 0x77, false], + F9: [0x43, 0x78, false], + F10: [0x44, 0x79, false], + NumLock: [0x45, 0x90, false], + ScrollLock: [0x46, 0x91, false], + Numpad7: [0x47, 0x67, false], + Numpad8: [0x48, 0x68, false], + Numpad9: [0x49, 0x69, false], + NumpadSubtract: [0x4a, 0x6d, false], + Numpad4: [0x4b, 0x64, false], + Numpad5: [0x4c, 0x65, false], + Numpad6: [0x4d, 0x66, false], + NumpadAdd: [0x4e, 0x6b, false], + Numpad1: [0x4f, 0x61, false], + Numpad2: [0x50, 0x62, false], + Numpad3: [0x51, 0x63, false], + Numpad0: [0x52, 0x60, false], + NumpadDecimal: [0x53, 0x6e, false], + F11: [0x57, 0x7a, false], + F12: [0x58, 0x7b, false], + // Extended keys: same scan code as their non-extended twin. + NumpadEnter: [0x1c, 0x0d, true], + ControlRight: [0x1d, 0xa3, true], + NumpadDivide: [0x35, 0x6f, true], + AltRight: [0x38, 0xa5, true], + Home: [0x47, 0x24, true], + ArrowUp: [0x48, 0x26, true], + PageUp: [0x49, 0x21, true], + ArrowLeft: [0x4b, 0x25, true], + ArrowRight: [0x4d, 0x27, true], + End: [0x4f, 0x23, true], + ArrowDown: [0x50, 0x28, true], + PageDown: [0x51, 0x22, true], + Insert: [0x52, 0x2d, true], + Delete: [0x53, 0x2e, true], + MetaLeft: [0x5b, 0x5b, true], + MetaRight: [0x5c, 0x5c, true], + ContextMenu: [0x5d, 0x5d, true], +}; + class MessageQueue { private messages: Event[] = []; private waiter: ((value: Event) => void) | undefined; @@ -38,6 +151,54 @@ class MessageQueue { dom.onmouseup = this.enqueue; dom.onmousemove = this.enqueue; dom.oncontextmenu = this.discard; + // Keys go to the document: a canvas only receives them when focused, and + // games expect to be typed at as soon as they are on screen. + document.addEventListener("keydown", this.enqueue); + document.addEventListener("keyup", this.enqueue); + } +} + +/// Plays the samples the program's mixer produces, scheduling them back to +/// back so playback is continuous. +class AudioStream { + private ctx: AudioContext; + /// When the next buffer should start, in the context's timebase. + private nextStart = 0; + + constructor(private sampleRate: number, private channels: number) { + this.ctx = new AudioContext({ sampleRate }); + } + + /// Samples handed over but not played yet, in bytes, which is what the + /// program's mixer paces itself against. + queued(): number { + const ahead = Math.max(0, this.nextStart - this.ctx.currentTime); + return Math.round(ahead * this.sampleRate) * this.channels * 2; + } + + write(samples: Int16Array) { + const frames = samples.length / this.channels; + if (frames === 0) return; + const buffer = this.ctx.createBuffer(this.channels, frames, this.sampleRate); + for (let channel = 0; channel < this.channels; channel++) { + const out = buffer.getChannelData(channel); + for (let i = 0; i < frames; i++) { + out[i] = samples[i * this.channels + channel]! / 32768; + } + } + const source = this.ctx.createBufferSource(); + source.buffer = buffer; + source.connect(this.ctx.destination); + // Never schedule in the past: after a gap, restart from now. + this.nextStart = Math.max(this.nextStart, this.ctx.currentTime); + source.start(this.nextStart); + this.nextStart += buffer.duration; + } + + resume() { + // Browsers only allow audio after a user gesture; retrying on each resume + // means playback starts as soon as the player interacts with the page. + this.ctx.resume().catch(() => {}); } } @@ -48,6 +209,8 @@ class Host implements exe.WasmHost { surfaces: Map = new Map(); nextSurface = 1; + audioStreams: Map = new Map(); + nextAudioStream = 1; messageQueue = new MessageQueue(); constructor(public wasmMemory: WebAssembly.Memory) { @@ -106,6 +269,7 @@ class Host implements exe.WasmHost { this.window_.height = height; document.body.appendChild(this.window_); this.messageQueue.listen(this.window_); + report(`window created ${width}x${height}`); return 1; } @@ -117,6 +281,7 @@ class Host implements exe.WasmHost { render(window_id: number, surface_id: number) { const surface = this.surfaces.get(surface_id)!; this.window_!.getContext("2d")!.drawImage(surface, 0, 0); + reportFrame(this.window_!); } set_pixels(id: number, ptr: number, len: number): number { @@ -127,6 +292,9 @@ class Host implements exe.WasmHost { ptr, len, ).slice(); + // The program's surfaces are opaque: the fourth byte of a pixel is the + // unused X of XRGB, not alpha. Canvas would read it as transparency. + for (let i = 3; i < copy.length; i += 4) copy[i] = 255; const surface = this.surfaces.get(id)!; const imageData = new ImageData(copy, surface.width); surface.getContext("2d")!.putImageData(imageData, 0, 0); @@ -135,19 +303,29 @@ class Host implements exe.WasmHost { private serializeMessage(event: Event): number[] { // see wasm.rs:parse_message - const typeToCode: Record = { - mousedown: 2, - mouseup: 3, - mousemove: 4, - }; - const code = typeToCode[event.type]; - if (code === undefined) throw new Error(); switch (event.type) { case "mousedown": case "mouseup": case "mousemove": { + const typeToCode: Record = { + mousedown: 2, + mouseup: 3, + mousemove: 4, + }; const e = event as MouseEvent; - return [typeToCode[e.type]!, e.offsetX, e.offsetY, 1 << e.button]; + // MouseEvent.button numbers left/middle/right 0/1/2, which lines up + // with our bits, but MouseEvent.buttons orders them left/right/middle. + const held = (e.buttons & 1) | ((e.buttons & 2) << 1) | ((e.buttons & 4) >> 1); + return [typeToCode[e.type]!, e.offsetX, e.offsetY, (1 << e.button) | (held << 16)]; + } + case "keydown": + case "keyup": { + const e = event as KeyboardEvent; + const key = KEY_MAP[e.code]; + if (!key) return []; + const [scancode, vkey, extended] = key; + const flags = (extended ? 1 : 0) | (e.repeat ? 2 : 0); + return [e.type === "keydown" ? 5 : 6, scancode, vkey, flags]; } default: throw new Error(); @@ -155,35 +333,135 @@ class Host implements exe.WasmHost { } poll_message(): number[] { - const event = this.messageQueue.poll(); - return event ? this.serializeMessage(event) : [-1]; + for (;;) { + const event = this.messageQueue.poll(); + if (!event) return [-1]; + const msg = this.serializeMessage(event); + // An empty result means a key we have no mapping for; skip it. + if (msg.length) return msg; + } } async wait_message(): Promise { - const event = await this.messageQueue.wait(); - return this.serializeMessage(event); + for (;;) { + const event = await this.messageQueue.wait(); + const msg = this.serializeMessage(event); + if (msg.length) return msg; + } + } + + create_audio_stream(sample_rate: number, channels: number): number { + const id = this.nextAudioStream++; + this.audioStreams.set(id, new AudioStream(sample_rate, channels)); + return id; + } + + audio_queued(id: number): number { + // Offset by one: the synchronization protocol reserves zero. + return this.audioStreams.get(id)!.queued() + 1; + } + + audio_write(id: number, ptr: number, len: number): number { + const samples = new Int16Array(this.wasmMemory.buffer, ptr, len / 2).slice(); + this.audioStreams.get(id)!.write(samples); + return 1; + } + + audio_resume(id: number): number { + this.audioStreams.get(id)!.resume(); + return 1; + } + + write_file(path: string, ptr: number, len: number): number { + const data = new Uint8Array(this.wasmMemory.buffer, ptr, len); + let binary = ""; + for (const byte of data) binary += String.fromCharCode(byte); + try { + localStorage.setItem(SAVE_PREFIX + path, btoa(binary)); + } catch (e) { + // Quota exceeded, private browsing, ...: losing a save beats crashing. + console.warn("could not save", path, e); + } + return 1; } } +/// Report a message to the server, which logs it. The browser console is not +/// always reachable — a page run from a script, a remote browser — and a game +/// that fails to start otherwise gives no clue why. +function report(message: string) { + navigator.sendBeacon("/log", message); +} + +/// With ?frames=1, post the program's window to the server as it draws, so a +/// script driving the browser can see the output. Driven by the program's own +/// rendering rather than a timer, which browsers throttle in hidden tabs. +const REPORT_FRAMES = location.search.includes("frames"); +let lastFrameReport = 0; +let frameCount = 0; + +function reportFrame(canvas: HTMLCanvasElement) { + frameCount++; + if (!REPORT_FRAMES) return; + const now = Date.now(); + if (now - lastFrameReport < 3000) return; + lastFrameReport = now; + report(`frame ${frameCount}`); + canvas.toBlob((blob) => { + if (!blob) return; + const reader = new FileReader(); + reader.onload = () => { + const url = reader.result as string; + fetch("/frame", { method: "POST", body: url.slice(url.indexOf(",") + 1) }); + }; + reader.readAsDataURL(blob); + }); +} + +window.addEventListener("error", (e) => report(`error: ${e.message}`)); +window.addEventListener("unhandledrejection", (e) => report(`rejected: ${e.reason}`)); + async function main() { if (!window.SharedArrayBuffer) { document.body.innerText = "SharedArrayBuffer is not supported; possibly try reloading"; + report("SharedArrayBuffer is not available"); return; } const memory = new WebAssembly.Memory({ - initial: 32, // in units of 64KB pages, 2mb - maximum: 1024, // 64mb + // In units of 64KB pages. Has to cover the module's own declared minimum, + // which grows with the size of the translated program. + initial: 512, // 32mb + // A translated game reserves a flat address space for itself and keeps its + // data files in memory too, so leave room for both. + maximum: 16384, // 1gb shared: true, }); const host = new Host(memory); const worker = new Worker("./worker.js", { type: "module" }); worker.onmessage = (e) => host.onMessage(e); + + // Files the program wrote in an earlier session. + const saves: Record = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i)!; + if (key.startsWith(SAVE_PREFIX)) { + saves[key.slice(SAVE_PREFIX.length)] = localStorage.getItem(key)!; + } + } + const message: worker.StartMessage = { - module: "./exe/mine/mine.js", + module: "./exe/winpin/winpin.js", memory, + dataRoot: "./game", + cwd: "/Soccer98", + saves, + mirrorConsole: REPORT_FRAMES, + trace: new URLSearchParams(location.search).get("trace") ?? "", }; + worker.onerror = (e) => report(`worker: ${e.message}`); worker.postMessage(message); + report(`started ${message.module}`); } main().catch((e) => console.error(e)); diff --git a/web/static-server.go b/web/static-server.go index e11fd023..908a0820 100644 --- a/web/static-server.go +++ b/web/static-server.go @@ -3,18 +3,49 @@ package main import ( + "encoding/base64" "flag" + "io" "log" "net/http" + "os" ) func main() { addr := flag.String("addr", "localhost:8080", "address to listen on") dir := flag.String("dir", ".", "directory to serve") + frame := flag.String("frame", "frame.png", "where to write frames posted by the page") flag.Parse() - files := http.FileServer(http.Dir(*dir)) - handler := withHeaders(files) + mux := http.NewServeMux() + // The page posts here to report errors, which is the only way to see what + // went wrong when the browser's console isn't at hand. + mux.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + log.Printf("page: %s", body) + w.WriteHeader(http.StatusNoContent) + }) + // The page posts a PNG of its window here when started with ?frames=1, + // which is how a script can see what the program actually drew. + mux.HandleFunc("/frame", func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + png, err := base64.StdEncoding.DecodeString(string(body)) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := os.WriteFile(*frame, png, 0o644); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) + }) + mux.Handle("/", http.FileServer(http.Dir(*dir))) + handler := withHeaders(mux) log.Printf("serving %s at http://%s", *dir, *addr) log.Fatal(http.ListenAndServe(*addr, handler)) diff --git a/web/worker.ts b/web/worker.ts index d5fd884c..61b4ae5a 100644 --- a/web/worker.ts +++ b/web/worker.ts @@ -6,18 +6,86 @@ import type * as exe from "./exe/basicdd/basicdd.js"; self.postMessage(obj); }; -async function run(module: string, memory: WebAssembly.Memory) { - const exe = await import(module); - await exe.default(/* module */ undefined, memory); +/// Load the program's data files into its in-memory filesystem. +/// +/// The program reads files synchronously, so everything it might open has to +/// be resident before it starts; `manifest.json` lists the files to fetch. +async function mountData(exe: any, dataRoot: string, saves: Record) { + const manifest: string[] = await (await fetch(`${dataRoot}/manifest.json`)).json(); + const files = await Promise.all( + manifest.map(async (name) => { + const response = await fetch(`${dataRoot}/${name}`); + if (!response.ok) throw new Error(`${name}: ${response.status}`); + return [name, new Uint8Array(await response.arrayBuffer())] as const; + }), + ); + for (const [name, data] of files) { + exe.mount_file(`/${name}`, data); + } + // Saved files from a previous session win over the shipped copies. + for (const [name, encoded] of Object.entries(saves)) { + const binary = atob(encoded); + const data = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) data[i] = binary.charCodeAt(i); + exe.mount_file(name, data); + } +} + +function report(message: string) { + fetch("/log", { method: "POST", body: message }).catch(() => {}); +} + +/// Mirror the program's log output to the server. The worker's console is out +/// of reach when the browser is driven by a script. +function mirrorConsole() { + for (const level of ["log", "info", "warn", "error"] as const) { + const original = console[level].bind(console); + console[level] = (...args: unknown[]) => { + original(...args); + report(`${level}: ${args.join(" ")}`); + }; + } +} + +async function run(start: StartMessage) { + if (start.mirrorConsole) mirrorConsole(); + report(`worker loading ${start.module}`); + const exe = await import(start.module); + report("module imported"); + await exe.default(/* module */ undefined, start.memory); + if (start.dataRoot) { + report("loading data"); + await mountData(exe, start.dataRoot, start.saves ?? {}); + report("data loaded"); + } + if (start.cwd) { + exe.set_current_dir(start.cwd); + } + if (start.trace) { + exe.set_trace(start.trace); + } + report("starting program"); exe.main(); } export interface StartMessage { module: string; memory: WebAssembly.Memory; + /// URL prefix holding manifest.json and the program's data files. + dataRoot?: string; + /// Directory the program starts in, as a path within the mounted data. + cwd?: string; + /// Previously saved files, base64 encoded, keyed by mounted path. + saves?: Record; + /// Send the program's log output to the server as well as the console. + mirrorConsole?: boolean; + /// Which winapi calls to trace, in THESEUS_TRACE syntax. + trace?: string; } self.onmessage = (e: MessageEvent) => { - const { module, memory } = e.data; - run(module, memory).catch((e) => console.error(e)); + run(e.data).catch((e) => { + report(`worker failed: ${e}\n${e?.stack ?? ""}`); + console.error(e); + }); }; diff --git a/win32/winapi/src/kernel32/event.rs b/win32/winapi/src/kernel32/event.rs index f085ffb9..19a94390 100644 --- a/win32/winapi/src/kernel32/event.rs +++ b/win32/winapi/src/kernel32/event.rs @@ -8,7 +8,7 @@ pub enum Object { Thread, Event(Arc), Mutex, - File(std::fs::File), + File(host::fs::File), FindHandle(crate::kernel32::FindHandle), } diff --git a/win32/winapi/src/kernel32/file.rs b/win32/winapi/src/kernel32/file.rs index 8acb6070..ec9a22a3 100644 --- a/win32/winapi/src/kernel32/file.rs +++ b/win32/winapi/src/kernel32/file.rs @@ -25,7 +25,7 @@ const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10; /// initial current directory. Captured on first use, before any chdir. fn initial_cwd() -> &'static std::path::Path { static CWD: std::sync::OnceLock = std::sync::OnceLock::new(); - CWD.get_or_init(|| std::env::current_dir().unwrap_or_else(|_| ".".into())) + CWD.get_or_init(|| host::fs::current_dir().unwrap_or_else(|_| ".".into())) } /// Resolve a Windows-style path against the host filesystem, ignoring case @@ -42,7 +42,7 @@ pub fn resolve_path(path: &str) -> std::path::PathBuf { initial_cwd().to_path_buf() } else { // Absolute base so that ".." components resolve properly. - std::env::current_dir().unwrap_or_else(|_| ".".into()) + host::fs::current_dir().unwrap_or_else(|_| ".".into()) }; 'component: for comp in path.split('/') { if comp.is_empty() || comp == "." { @@ -53,14 +53,14 @@ pub fn resolve_path(path: &str) -> std::path::PathBuf { continue; } let direct = result.join(comp); - if direct.exists() { + if host::fs::exists(&direct) { result = direct; continue; } - if let Ok(entries) = std::fs::read_dir(&result) { - for entry in entries.flatten() { - if entry.file_name().to_string_lossy().eq_ignore_ascii_case(comp) { - result = entry.path(); + if let Ok(entries) = host::fs::read_dir(&result) { + for entry in entries { + if entry.name.eq_ignore_ascii_case(comp) { + result = result.join(&entry.name); continue 'component; } } @@ -103,7 +103,7 @@ pub fn CreateFileA( let path = resolve_path(&name); let write = dwDesiredAccess & GENERIC_WRITE != 0; let read = dwDesiredAccess & GENERIC_READ != 0; - let mut opts = std::fs::OpenOptions::new(); + let mut opts = host::fs::OpenOptions::new(); opts.read(read || !write); if write { opts.write(true); @@ -277,7 +277,7 @@ pub fn FlushFileBuffers(_ctx: &mut Context, hFile: crate::HANDLE) -> bool { let Some(Object::File(file)) = kernel32.objects.get_mut(hFile) else { return false; }; - file.sync_all().is_ok() + file.flush().is_ok() } #[win32_derive::dllexport] @@ -290,7 +290,7 @@ pub fn CloseHandle(_ctx: &mut Context, hObject: crate::HANDLE) -> bool { #[win32_derive::dllexport] pub fn DeleteFileA(ctx: &mut Context, lpFileName: Ptr) -> bool { let name = ctx.memory.read_str(lpFileName.addr).to_owned(); - std::fs::remove_file(resolve_path(&name)).is_ok() + host::fs::remove_file(&resolve_path(&name)).is_ok() } #[win32_derive::dllexport] @@ -318,7 +318,7 @@ pub fn SetHandleCount(_ctx: &mut Context, uNumber: u32) -> u32 { #[win32_derive::dllexport] pub fn GetCurrentDirectoryA(ctx: &mut Context, nBufferLength: u32, lpBuffer: Ptr) -> u32 { - let cur = std::env::current_dir().unwrap_or_default(); + let cur = host::fs::current_dir().unwrap_or_default(); let rel = cur .strip_prefix(initial_cwd()) .unwrap_or(std::path::Path::new("")) @@ -342,7 +342,7 @@ pub fn GetCurrentDirectoryA(ctx: &mut Context, nBufferLength: u32, lpBuffer: Ptr pub fn SetCurrentDirectoryA(ctx: &mut Context, lpPathName: Ptr) -> bool { let name = ctx.memory.read_str(lpPathName.addr).to_owned(); let path = resolve_path(&name); - match std::env::set_current_dir(&path) { + match host::fs::set_current_dir(&path) { Ok(()) => true, Err(err) => { log::warn!("SetCurrentDirectoryA({name:?} => {path:?}): {err}"); @@ -433,17 +433,15 @@ pub fn FindFirstFileA( }; let dir = resolve_path(dir); let mut entries = Vec::new(); - if let Ok(dir_entries) = std::fs::read_dir(&dir) { - for entry in dir_entries.flatten() { - let name = entry.file_name().to_string_lossy().into_owned(); - if !wildcard_match(file_pattern.as_bytes(), name.as_bytes()) { + if let Ok(dir_entries) = host::fs::read_dir(&dir) { + for entry in dir_entries { + if !wildcard_match(file_pattern.as_bytes(), entry.name.as_bytes()) { continue; } - let meta = entry.metadata().ok(); entries.push(FindEntry { - name, - size: meta.as_ref().map(|m| m.len()).unwrap_or(0), - is_dir: meta.as_ref().map(|m| m.is_dir()).unwrap_or(false), + name: entry.name, + size: entry.len, + is_dir: entry.is_dir, }); } } diff --git a/win32/winapi/src/lib.rs b/win32/winapi/src/lib.rs index 64a1bc1d..be228f43 100644 --- a/win32/winapi/src/lib.rs +++ b/win32/winapi/src/lib.rs @@ -53,19 +53,11 @@ macro_rules! stub { use runtime::{CPU, Context, EXEData, Memory}; pub(crate) use stub; -#[cfg(target_family = "wasm")] -fn thesesus_trace() -> String { - "+".into() -} -#[cfg(not(target_family = "wasm"))] -fn thesesus_trace() -> String { - std::env::var("THESEUS_TRACE").unwrap_or_default() -} pub fn load(exe: &EXEData) -> Context { host::init(); - crate::trace::init(&thesesus_trace()); + crate::trace::init(&host::trace_spec()); // Room for the program's image, its heaps and the flat pool games of this // era carve out for themselves. diff --git a/win32/winapi/src/winmm/mmio.rs b/win32/winapi/src/winmm/mmio.rs index 6d246e42..530ea215 100644 --- a/win32/winapi/src/winmm/mmio.rs +++ b/win32/winapi/src/winmm/mmio.rs @@ -133,7 +133,7 @@ pub fn mmioOpenA(ctx: &mut Context, szFilename: u32, _lpmmioinfo: u32, _dwOpenFl } let name = ctx.memory.read_str(szFilename).to_owned(); let path = kernel32::resolve_path(&name); - let data = match std::fs::read(&path) { + let data = match host::fs::read(&path) { Ok(data) => data, Err(err) => { log::warn!("mmioOpenA({name:?}): {err}"); From 9933070a4e0704fdfbc67e4ef34902dd12b43c04 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Tue, 21 Jul 2026 13:33:27 +0300 Subject: [PATCH 05/10] dsound: rewind one-shots, pump from GetStatus --- host/src/lib.rs | 1 + host/src/sdl.rs | 12 ++++++++++++ win32/winapi/src/dsound.rs | 30 +++++++++++++++++++++++++----- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/host/src/lib.rs b/host/src/lib.rs index d6b41324..935289d1 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -20,6 +20,7 @@ pub use wasm::*; static HOST: LazyLock = LazyLock::new(Host::new); +#[derive(Clone, Copy)] pub struct AudioSpec { pub sample_rate: u32, pub channels: u32, diff --git a/host/src/sdl.rs b/host/src/sdl.rs index 40611123..c914cca8 100644 --- a/host/src/sdl.rs +++ b/host/src/sdl.rs @@ -438,6 +438,18 @@ impl AudioStream { check(sdl::audio::SDL_ResumeAudioStreamDevice(self.0)); } } + +} + +/// Name of the audio backend SDL selected, for diagnostics. +pub fn audio_driver() -> String { + unsafe { + let name = sdl::audio::SDL_GetCurrentAudioDriver(); + if name.is_null() { + return "(none)".into(); + } + std::ffi::CStr::from_ptr(name).to_string_lossy().into_owned() + } } impl Host { diff --git a/win32/winapi/src/dsound.rs b/win32/winapi/src/dsound.rs index 3fda1a97..c84c62d9 100644 --- a/win32/winapi/src/dsound.rs +++ b/win32/winapi/src/dsound.rs @@ -6,9 +6,11 @@ //! cursor, and [`pump`] mixes whatever is playing into the host stream. //! //! Nothing drives the mixer on its own: `pump` is called from the places the -//! app passes through every frame (the message pump and ddraw's presentation), -//! because a game that busy-waits on its own frame limiter never yields to us -//! otherwise. +//! app passes through anyway — the message pump, ddraw's presentation, and the +//! DirectSound calls that report playback progress. That last one matters: +//! a game that waits for a sound to finish by polling GetStatus never pumps +//! messages meanwhile, and would wait forever on playback that only advanced +//! when it moved on. use std::{collections::HashMap, sync::Mutex}; @@ -177,7 +179,9 @@ fn init() { impl State { fn heap(&mut self) -> &Heap { self.heap.get_or_insert_with(|| { - const HEAP_SIZE: u32 = 16 << 20; + // Games allocate a buffer per sound instance and are lax about + // releasing them, so leave plenty of room. + const HEAP_SIZE: u32 = 64 << 20; let addr = kernel32::lock() .mappings .alloc("dsound buffers".into(), HEAP_SIZE); @@ -209,7 +213,11 @@ impl State { for slot in mixed.iter_mut() { if buffer.cursor >= frames { if !buffer.looping { + // Reaching the end stops playback and rewinds, so + // playing the same sound again starts it over rather + // than replaying the silence past its end. buffer.playing = false; + buffer.cursor = 0.0; break; } buffer.cursor %= frames; @@ -548,6 +556,9 @@ pub mod IDirectSoundBuffer { pdwCurrentPlayCursor: u32, pdwCurrentWriteCursor: u32, ) -> u32 { + // Asking where playback is has to move it along first, or an app that + // polls in a tight loop would never see it advance. + pump(ctx); let state = lock(); let Some(buffer) = state.buffers.get(&this) else { return DSERR_INVALIDPARAM; @@ -642,6 +653,10 @@ pub mod IDirectSoundBuffer { pub fn GetStatus(ctx: &mut Context, this: u32, lpdwStatus: u32) -> u32 { const DSBSTATUS_PLAYING: u32 = 0x0001; const DSBSTATUS_LOOPING: u32 = 0x0004; + // Games wait for a sound to finish by polling this, without pumping + // messages meanwhile. Mixing here is what lets that wait end: otherwise + // the app waits for playback that only advances when the app moves on. + pump(ctx); let state = lock(); let Some(buffer) = state.buffers.get(&this) else { return DSERR_INVALIDPARAM; @@ -733,9 +748,14 @@ pub mod IDirectSoundBuffer { return DSERR_INVALIDPARAM; }; // Play resumes from wherever the cursor was left, which is the start - // for a fresh buffer and where Stop left off otherwise. + // for a fresh buffer and where Stop left off otherwise. A buffer + // stopped exactly at its end has nothing left to resume, so start it + // over instead of playing nothing. buffer.playing = true; buffer.looping = dwFlags & DSBPLAY_LOOPING != 0; + if buffer.cursor >= buffer.frame_count() { + buffer.cursor = 0.0; + } DS_OK } From 565480de62b961e70416451e434618b48b811f23 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Tue, 21 Jul 2026 19:29:22 +0300 Subject: [PATCH 06/10] dsound: interpolate when resampling --- web/host.ts | 26 ++++++++++++++++++++++++- win32/winapi/src/dsound.rs | 39 +++++++++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/web/host.ts b/web/host.ts index 26ff1830..3358002d 100644 --- a/web/host.ts +++ b/web/host.ts @@ -158,6 +158,27 @@ class MessageQueue { } } +/// Audio contexts to start once the user interacts with the page. Browsers +/// refuse to play audio before a gesture, and a program that asks for sound +/// while loading — as games do — asks too early, so the request has to be +/// replayed on the first click or keypress. The listeners stay installed +/// because a context can be suspended again later, and resuming one that is +/// already running does nothing. +const audioContexts: Set = new Set(); +let gestureHooked = false; + +function resumeOnGesture(ctx: AudioContext) { + audioContexts.add(ctx); + if (gestureHooked) return; + gestureHooked = true; + const resumeAll = () => { + for (const ctx of audioContexts) ctx.resume().catch(() => {}); + }; + for (const event of ["pointerdown", "keydown", "touchend"]) { + document.addEventListener(event, resumeAll); + } +} + /// Plays the samples the program's mixer produces, scheduling them back to /// back so playback is continuous. class AudioStream { @@ -167,6 +188,7 @@ class AudioStream { constructor(private sampleRate: number, private channels: number) { this.ctx = new AudioContext({ sampleRate }); + resumeOnGesture(this.ctx); } /// Samples handed over but not played yet, in bytes, which is what the @@ -252,10 +274,12 @@ class Host implements exe.WasmHost { } create_surface(width: number, height: number): number { + // Deliberately not added to the document: these are the program's + // offscreen buffers, which reach the screen only when it draws one into + // the window. Attaching them would show every back buffer below the game. const surface = document.createElement("canvas"); surface.width = width; surface.height = height; - document.body.appendChild(surface); const id = this.nextSurface++; this.surfaces.set(id, surface); diff --git a/win32/winapi/src/dsound.rs b/win32/winapi/src/dsound.rs index c84c62d9..126f8bb5 100644 --- a/win32/winapi/src/dsound.rs +++ b/win32/winapi/src/dsound.rs @@ -124,9 +124,9 @@ impl Buffer { (volume * left, volume * right) } - /// The sample pair at the current cursor, in i16 scale. - fn sample(&self, mem: &Memory) -> (i32, i32) { - let base = self.addr + self.cursor as u32 * self.format.frame_bytes(); + /// The sample pair of one whole frame, in i16 scale. + fn frame_at(&self, mem: &Memory, index: u32) -> (i32, i32) { + let base = self.addr + index * self.format.frame_bytes(); let stereo = self.format.channels >= 2; if self.format.bits >= 16 { let left = mem.read::(base) as i32; @@ -147,6 +147,35 @@ impl Buffer { (left, right) } } + + /// The sample pair at the current cursor, in i16 scale, interpolated + /// between the frames it falls between. + /// + /// These sounds are recorded at 11 or 22 kHz and play out at 44.1, so most + /// output samples land between two source frames. Repeating the nearer one + /// instead — the obvious thing — turns each source sample into a little + /// staircase, and those steps are audible as a gritty edge on every sound. + fn sample(&self, mem: &Memory) -> (f32, f32) { + let index = self.cursor.floor(); + let frac = (self.cursor - index) as f32; + let current = self.frame_at(mem, index as u32); + let next_index = index + 1.0; + let next = if next_index < self.frame_count() { + self.frame_at(mem, next_index as u32) + } else if self.looping { + // The frame after the last is the first one again. + self.frame_at(mem, 0) + } else { + // A one-shot ends here; hold rather than interpolate into + // whatever memory follows the buffer. + current + }; + let blend = |a: i32, b: i32| a as f32 + (b - a) as f32 * frac; + ( + blend(current.0, next.0), + blend(current.1, next.1), + ) + } } struct State { @@ -223,8 +252,8 @@ impl State { buffer.cursor %= frames; } let (left, right) = buffer.sample(mem); - slot.0 += left as f32 * left_gain; - slot.1 += right as f32 * right_gain; + slot.0 += left * left_gain; + slot.1 += right * right_gain; buffer.cursor += step; } } From eda36194c9e1e296a3d6df64355405d6a77d1809 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Tue, 21 Jul 2026 20:03:53 +0300 Subject: [PATCH 07/10] fix fninit, blt colorkey offset, dinput not-acquired, LoadStringW handle --- host/src/fs.rs | 2 +- host/src/sdl.rs | 12 ------ tc/src/codegen/fpu.rs | 8 ++-- tc/src/codegen/mod.rs | 4 +- tc/src/gather.rs | 25 +++++++++--- tc/src/lib.rs | 4 +- win32/winapi/src/advapi32.rs | 6 ++- win32/winapi/src/ddraw/ddraw1.rs | 62 ++++++++++++++++++++++------- win32/winapi/src/dinput.rs | 27 ++++++++----- win32/winapi/src/dsound.rs | 23 +++++++---- win32/winapi/src/kernel32/dll.rs | 12 +++--- win32/winapi/src/kernel32/file.rs | 3 +- win32/winapi/src/kernel32/time.rs | 6 +-- win32/winapi/src/lib.rs | 2 - win32/winapi/src/user32/input.rs | 12 +++++- win32/winapi/src/user32/resource.rs | 4 +- win32/winapi/src/winmm/mmio.rs | 6 ++- 17 files changed, 144 insertions(+), 74 deletions(-) diff --git a/host/src/fs.rs b/host/src/fs.rs index 560604e0..1e9b1e62 100644 --- a/host/src/fs.rs +++ b/host/src/fs.rs @@ -401,6 +401,6 @@ mod imp { } } -pub use imp::{File, current_dir, exists, open, read_dir, remove_file, set_current_dir}; #[cfg(target_family = "wasm")] pub use imp::mount; +pub use imp::{File, current_dir, exists, open, read_dir, remove_file, set_current_dir}; diff --git a/host/src/sdl.rs b/host/src/sdl.rs index c914cca8..40611123 100644 --- a/host/src/sdl.rs +++ b/host/src/sdl.rs @@ -438,18 +438,6 @@ impl AudioStream { check(sdl::audio::SDL_ResumeAudioStreamDevice(self.0)); } } - -} - -/// Name of the audio backend SDL selected, for diagnostics. -pub fn audio_driver() -> String { - unsafe { - let name = sdl::audio::SDL_GetCurrentAudioDriver(); - if name.is_null() { - return "(none)".into(); - } - std::ffi::CStr::from_ptr(name).to_string_lossy().into_owned() - } } impl Host { diff --git a/tc/src/codegen/fpu.rs b/tc/src/codegen/fpu.rs index f35712fa..3746d0cc 100644 --- a/tc/src/codegen/fpu.rs +++ b/tc/src/codegen/fpu.rs @@ -211,7 +211,10 @@ impl<'a> CodeGen<'a> { Fnclex => {} Fninit => { + // Also empties the stack, which is how a program recovers from + // leaving values on it. self.line("ctx.cpu.fpu.control = 0x037f;"); + self.line("ctx.cpu.fpu.st_top = 8;"); } Fnstcw => { @@ -221,10 +224,7 @@ impl<'a> CodeGen<'a> { Fldcw => { assert_eq!(instr.op_count(), 1); - self.line(format!( - "ctx.cpu.fpu.control = {};", - self.get_op(instr, 0) - )); + self.line(format!("ctx.cpu.fpu.control = {};", self.get_op(instr, 0))); } Fpatan => { diff --git a/tc/src/codegen/mod.rs b/tc/src/codegen/mod.rs index 1a602886..3034089c 100644 --- a/tc/src/codegen/mod.rs +++ b/tc/src/codegen/mod.rs @@ -564,7 +564,9 @@ ctx.cpu.regs.esp = {stack_pointer:#x}; if let Ok(entries) = std::fs::read_dir(format!("{outdir}/src/generated")) { for entry in entries.flatten() { let name = entry.file_name().to_string_lossy().to_string(); - let stem = name.trim_end_matches(".rs"); + let Some(stem) = name.strip_suffix(".rs") else { + continue; + }; if stem.starts_with("part_") && !parts.iter().any(|part| part == stem) { std::fs::remove_file(entry.path())?; } diff --git a/tc/src/gather.rs b/tc/src/gather.rs index 0ea3a48f..6a0a7e43 100644 --- a/tc/src/gather.rs +++ b/tc/src/gather.rs @@ -69,7 +69,19 @@ fn is_index_bound(instr: &iced_x86::Instruction) -> Option<(iced_x86::Register, if count > 1024 { return None; } - Some((instr.op0_register().full_register(), count)) + // A mask only bounds the index if it is contiguous: `and eax, 0x30` leaves + // values up to 0x30, not 0x31 of them. + if instr.mnemonic() == And && !count.is_power_of_two() { + return None; + } + // Masking a sub-register says nothing about the register the dispatch + // indexes with: `and al, 0xf` leaves the rest of eax untouched. Keyed by + // the full register, which is how the dispatch looks it up. + let reg = instr.op0_register(); + if reg != reg.full_register32() { + return None; + } + Some((reg.full_register(), count)) } #[derive(Clone, Copy, PartialEq, PartialOrd)] @@ -399,9 +411,7 @@ impl<'a> Traverse<'a> { n >= 1 } - /// Read a jump table: consecutive pointers into code, stopping at the first - /// entry that doesn't look like one. Returns the number of entries found. - /// Queue the targets of a switch jump table. + /// Queue the targets of a switch jump table, returning how many it had. /// /// `known_len` comes from a bounds check before the dispatch, when there /// was one. Knowing the length matters: without it we have to stop at the @@ -435,7 +445,9 @@ impl<'a> Traverse<'a> { let mut found = 0; while count < limit { if direction < 0 { - let Some(prev) = addr.checked_sub(4) else { break }; + let Some(prev) = addr.checked_sub(4) else { + break; + }; addr = prev; } if addr as usize + 4 > self.mem.bytes.len() { @@ -667,7 +679,8 @@ impl<'a> Traverse<'a> { for block in self.blocks.values() { if let BlockType::Instrs(instrs) = &block.ty { spans.push( - instrs.first().unwrap().ip.to_addr()..instrs.last().unwrap().next_ip().to_addr(), + instrs.first().unwrap().ip.to_addr() + ..instrs.last().unwrap().next_ip().to_addr(), ); } } diff --git a/tc/src/lib.rs b/tc/src/lib.rs index f9b1b749..0d622b0e 100644 --- a/tc/src/lib.rs +++ b/tc/src/lib.rs @@ -237,7 +237,9 @@ impl State { continue; } for func in funcs.iter() { - module.dynamic_exports.push((dll.to_string(), func.to_string())); + module + .dynamic_exports + .push((dll.to_string(), func.to_string())); // A function already imported statically has an address // already; only the rest need one reserved. if module diff --git a/win32/winapi/src/advapi32.rs b/win32/winapi/src/advapi32.rs index 4b7cd43b..c8b11ff5 100644 --- a/win32/winapi/src/advapi32.rs +++ b/win32/winapi/src/advapi32.rs @@ -12,7 +12,11 @@ pub fn RegCloseKey(_ctx: &mut Context, _hKey: HKEY) -> u32 /* WIN32_ERROR */ { } #[win32_derive::dllexport] -pub fn GetUserNameA(ctx: &mut Context, lpBuffer: crate::Ptr, pcbBuffer: crate::Ptr) -> bool { +pub fn GetUserNameA( + ctx: &mut Context, + lpBuffer: crate::Ptr, + pcbBuffer: crate::Ptr, +) -> bool { let name = b"user"; let size = pcbBuffer.read(&ctx.memory).unwrap_or(0); if (size as usize) < name.len() + 1 { diff --git a/win32/winapi/src/ddraw/ddraw1.rs b/win32/winapi/src/ddraw/ddraw1.rs index 6e00636f..20a391f2 100644 --- a/win32/winapi/src/ddraw/ddraw1.rs +++ b/win32/winapi/src/ddraw/ddraw1.rs @@ -176,7 +176,9 @@ pub mod IDirectDraw { let callback = ctx.indirect(lpEnumCallback); ctx.call32_x86(callback, vec![desc_addr, lpContext]); let ret = ctx.cpu.regs.eax; - kernel32::lock().process_heap.free(&mut ctx.memory, desc_addr); + kernel32::lock() + .process_heap + .free(&mut ctx.memory, desc_addr); // DDENUMRET_CANCEL (0) means stop enumerating. if ret == 0 { @@ -234,7 +236,13 @@ pub mod IDirectDraw { } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32, _lpDD: u32, _dwFlags: u32, _lpDDColorTable: u32) -> DD { + pub fn Initialize( + _ctx: &mut Context, + _this: u32, + _lpDD: u32, + _dwFlags: u32, + _lpDDColorTable: u32, + ) -> DD { // Palettes are fully constructed by CreatePalette. const DDERR_ALREADYINITIALIZED: DD = DD::OK; DDERR_ALREADYINITIALIZED @@ -506,10 +514,10 @@ pub mod IDirectDrawSurface { } let color_key = if dwFlags & DDBLT_KEYSRCOVERRIDE != 0 { - // DDBLTFX.ddckSrcColorkey is at offset 16. + // DDBLTFX.ddckSrcColorkey, past the z-buffer and alpha fields. Some(ColorKey { - low: ctx.memory.read::(lpDDBLTFX + 16), - high: ctx.memory.read::(lpDDBLTFX + 20), + low: ctx.memory.read::(lpDDBLTFX + 92), + high: ctx.memory.read::(lpDDBLTFX + 96), }) } else if dwFlags & DDBLT_KEYSRC != 0 { surface_src_color_key(lpDDSrcSurface) @@ -568,7 +576,12 @@ pub mod IDirectDrawSurface { let (w, h) = match &src_rect { Some(r) => ((r.right - r.left).max(0), (r.bottom - r.top).max(0)), None => { - let src = state().surf.borrow_mut().get(&lpDDSrcSurface).unwrap().clone(); + let src = state() + .surf + .borrow_mut() + .get(&lpDDSrcSurface) + .unwrap() + .clone(); let src = src.borrow(); (src.width as i32, src.height as i32) } @@ -579,7 +592,14 @@ pub mod IDirectDrawSurface { right: dwX as i32 + w, bottom: dwY as i32 + h, }; - blit_copy(ctx, this, Some(dst_rect), lpDDSrcSurface, src_rect, color_key); + blit_copy( + ctx, + this, + Some(dst_rect), + lpDDSrcSurface, + src_rect, + color_key, + ); DD::OK } @@ -733,7 +753,13 @@ pub mod IDirectDrawSurface { } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32, _lpDD: u32, _dwFlags: u32, _lpDDColorTable: u32) -> DD { + pub fn Initialize( + _ctx: &mut Context, + _this: u32, + _lpDD: u32, + _dwFlags: u32, + _lpDDColorTable: u32, + ) -> DD { // Palettes are fully constructed by CreatePalette. const DDERR_ALREADYINITIALIZED: DD = DD::OK; DDERR_ALREADYINITIALIZED @@ -926,7 +952,13 @@ pub mod IDirectDrawPalette { } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32, _lpDD: u32, _dwFlags: u32, _lpDDColorTable: u32) -> DD { + pub fn Initialize( + _ctx: &mut Context, + _this: u32, + _lpDD: u32, + _dwFlags: u32, + _lpDDColorTable: u32, + ) -> DD { // Palettes are fully constructed by CreatePalette. const DDERR_ALREADYINITIALIZED: DD = DD::OK; DDERR_ALREADYINITIALIZED @@ -941,11 +973,13 @@ pub mod IDirectDrawPalette { dwCount: u32, lpEntries: u32, ) -> DD { - let new_entries = - <[PALETTEENTRY]>::ref_from_prefix_with_elems(&ctx.memory[lpEntries..], dwCount as usize) - .unwrap() - .0 - .to_vec(); + let new_entries = <[PALETTEENTRY]>::ref_from_prefix_with_elems( + &ctx.memory[lpEntries..], + dwCount as usize, + ) + .unwrap() + .0 + .to_vec(); let palettes = state().palette.borrow_mut(); let mut palette = palettes.get(&this).unwrap().borrow_mut(); for (i, entry) in new_entries.into_iter().enumerate() { diff --git a/win32/winapi/src/dinput.rs b/win32/winapi/src/dinput.rs index 3fdf422b..eefec778 100644 --- a/win32/winapi/src/dinput.rs +++ b/win32/winapi/src/dinput.rs @@ -27,7 +27,7 @@ const DI_OK: u32 = 0; /// More events were buffered than the app's buffer could hold. const DI_BUFFEROVERFLOW: u32 = 1; const DIERR_DEVICENOTREG: u32 = 0x80040154; -const DIERR_NOTACQUIRED: u32 = 0x80070015; +const DIERR_NOTACQUIRED: u32 = 0x8007000c; const DIERR_INVALIDPARAM: u32 = 0x80070057; /// DIGDD_PEEK: leave the returned events in the buffer. @@ -287,7 +287,8 @@ pub mod IDirectInputDevice { .input .borrow() .buffer_size(kind == DeviceKind::Keyboard); - ctx.memory.write::(pdiph + DIPROPDWORD_DWDATA, size as u32); + ctx.memory + .write::(pdiph + DIPROPDWORD_DWDATA, size as u32); DI_OK } @@ -407,11 +408,7 @@ pub mod IDirectInputDevice { } ctx.memory.write::(pdwInOut, events.len() as u32); - if overflowed { - DI_BUFFEROVERFLOW - } else { - DI_OK - } + if overflowed { DI_BUFFEROVERFLOW } else { DI_OK } } #[win32_derive::dllexport] @@ -430,7 +427,13 @@ pub mod IDirectInputDevice { } #[win32_derive::dllexport] - pub fn GetObjectInfo(_ctx: &mut Context, _this: u32, _pdidoi: u32, _dwObj: u32, _dwHow: u32) -> u32 { + pub fn GetObjectInfo( + _ctx: &mut Context, + _this: u32, + _pdidoi: u32, + _dwObj: u32, + _dwHow: u32, + ) -> u32 { todo!() } @@ -445,7 +448,13 @@ pub mod IDirectInputDevice { } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32, _hinst: u32, _dwVersion: u32, _rguid: u32) -> u32 { + pub fn Initialize( + _ctx: &mut Context, + _this: u32, + _hinst: u32, + _dwVersion: u32, + _rguid: u32, + ) -> u32 { DI_OK } diff --git a/win32/winapi/src/dsound.rs b/win32/winapi/src/dsound.rs index 126f8bb5..c12b5bfa 100644 --- a/win32/winapi/src/dsound.rs +++ b/win32/winapi/src/dsound.rs @@ -171,10 +171,7 @@ impl Buffer { current }; let blend = |a: i32, b: i32| a as f32 + (b - a) as f32 * frac; - ( - blend(current.0, next.0), - blend(current.1, next.1), - ) + (blend(current.0, next.0), blend(current.1, next.1)) } } @@ -553,7 +550,10 @@ pub mod IDirectSoundBuffer { if let Some(buffer) = state.buffers.remove(&this) { // Duplicates share their original's samples, so only free memory // that no surviving buffer still points at. - let shared = state.buffers.values().any(|other| other.addr == buffer.addr); + let shared = state + .buffers + .values() + .any(|other| other.addr == buffer.addr); if buffer.addr != 0 && !shared { state.heap().free(&mut ctx.memory, buffer.addr); } @@ -628,12 +628,14 @@ pub mod IDirectSoundBuffer { } let block_align = format.frame_bytes() as u16; ctx.memory.write::(lpwfxFormat, 1); // WAVE_FORMAT_PCM - ctx.memory.write::(lpwfxFormat + 2, format.channels as u16); + ctx.memory + .write::(lpwfxFormat + 2, format.channels as u16); ctx.memory.write::(lpwfxFormat + 4, format.rate); ctx.memory .write::(lpwfxFormat + 8, format.rate * block_align as u32); ctx.memory.write::(lpwfxFormat + 12, block_align); - ctx.memory.write::(lpwfxFormat + 14, format.bits as u16); + ctx.memory + .write::(lpwfxFormat + 14, format.bits as u16); ctx.memory.write::(lpwfxFormat + 16, 0); // cbSize } if lpdwSizeWritten != 0 { @@ -703,7 +705,12 @@ pub mod IDirectSoundBuffer { } #[win32_derive::dllexport] - pub fn Initialize(_ctx: &mut Context, _this: u32, _lpDirectSound: u32, _lpcDSBufferDesc: u32) -> u32 { + pub fn Initialize( + _ctx: &mut Context, + _this: u32, + _lpDirectSound: u32, + _lpcDSBufferDesc: u32, + ) -> u32 { DS_OK } diff --git a/win32/winapi/src/kernel32/dll.rs b/win32/winapi/src/kernel32/dll.rs index 00fe7f01..dcc73dd9 100644 --- a/win32/winapi/src/kernel32/dll.rs +++ b/win32/winapi/src/kernel32/dll.rs @@ -82,9 +82,11 @@ fn module_name(handle: HMODULE) -> Option { fn proc_address(module: &str, func: &str) -> Option { let exports = EXPORTS.lock().unwrap(); - exports.as_ref()?.functions.iter().find_map(|(dll, name, addr)| { - (dll == module && name == func).then_some(*addr) - }) + exports + .as_ref()? + .functions + .iter() + .find_map(|(dll, name, addr)| (dll == module && name == func).then_some(*addr)) } #[win32_derive::dllexport] @@ -102,8 +104,8 @@ pub fn GetModuleFileNameA( #[win32_derive::dllexport] pub fn GetModuleHandleA(ctx: &mut Context, lpModuleName: Ptr) -> HMODULE { - let Some(name) = (lpModuleName.addr != 0) - .then(|| ctx.memory.read_str(lpModuleName.addr).to_owned()) + let Some(name) = + (lpModuleName.addr != 0).then(|| ctx.memory.read_str(lpModuleName.addr).to_owned()) else { // A null name asks for the running executable itself. return lock().image_base; diff --git a/win32/winapi/src/kernel32/file.rs b/win32/winapi/src/kernel32/file.rs index ec9a22a3..377bcb83 100644 --- a/win32/winapi/src/kernel32/file.rs +++ b/win32/winapi/src/kernel32/file.rs @@ -334,7 +334,8 @@ pub fn GetCurrentDirectoryA(ctx: &mut Context, nBufferLength: u32, lpBuffer: Ptr return bytes.len() as u32 + 1; } ctx.memory[lpBuffer.addr..][..bytes.len()].copy_from_slice(bytes); - ctx.memory.write::(lpBuffer.addr + bytes.len() as u32, 0); + ctx.memory + .write::(lpBuffer.addr + bytes.len() as u32, 0); bytes.len() as u32 } diff --git a/win32/winapi/src/kernel32/time.rs b/win32/winapi/src/kernel32/time.rs index d497f44a..bf020d27 100644 --- a/win32/winapi/src/kernel32/time.rs +++ b/win32/winapi/src/kernel32/time.rs @@ -11,10 +11,8 @@ pub fn Sleep(_ctx: &mut Context, dwMilliseconds: u32) { } #[win32_derive::dllexport] -pub fn GetTimeZoneInformation( - ctx: &mut Context, - lpTimeZoneInformation: crate::Ptr, -) -> u32 /* TIME_ZONE_ID */ { +pub fn GetTimeZoneInformation(ctx: &mut Context, lpTimeZoneInformation: crate::Ptr) -> u32 /* TIME_ZONE_ID */ +{ // TIME_ZONE_INFORMATION is 172 bytes; report UTC by zeroing it. ctx.memory[lpTimeZoneInformation.addr..][..172].fill(0); 0 // TIME_ZONE_ID_UNKNOWN diff --git a/win32/winapi/src/lib.rs b/win32/winapi/src/lib.rs index be228f43..e74640f2 100644 --- a/win32/winapi/src/lib.rs +++ b/win32/winapi/src/lib.rs @@ -53,8 +53,6 @@ macro_rules! stub { use runtime::{CPU, Context, EXEData, Memory}; pub(crate) use stub; - - pub fn load(exe: &EXEData) -> Context { host::init(); crate::trace::init(&host::trace_spec()); diff --git a/win32/winapi/src/user32/input.rs b/win32/winapi/src/user32/input.rs index c7dc02a5..11bbb993 100644 --- a/win32/winapi/src/user32/input.rs +++ b/win32/winapi/src/user32/input.rs @@ -119,7 +119,10 @@ impl EventBuffer { let count = max.min(self.events.len()); let overflowed = self.overflowed; if peek { - return (self.events.iter().take(count).copied().collect(), overflowed); + return ( + self.events.iter().take(count).copied().collect(), + overflowed, + ); } self.overflowed = false; (self.events.drain(..count).collect(), overflowed) @@ -299,7 +302,12 @@ impl Input { buffer.events.clear(); } - pub fn take_events(&mut self, keyboard: bool, max: usize, peek: bool) -> (Vec, bool) { + pub fn take_events( + &mut self, + keyboard: bool, + max: usize, + peek: bool, + ) -> (Vec, bool) { let buffer = if keyboard { &mut self.keyboard_buffer } else { diff --git a/win32/winapi/src/user32/resource.rs b/win32/winapi/src/user32/resource.rs index 5fe483ae..b211ae52 100644 --- a/win32/winapi/src/user32/resource.rs +++ b/win32/winapi/src/user32/resource.rs @@ -142,7 +142,9 @@ pub fn LoadStringW( lpBuffer: Ptr, /* WSTR */ cchBufferMax: i32, ) -> i32 { - assert_eq!(hInstance, 0); + // GetModuleHandle(null) hands back the image base, so a program asking for + // its own resources passes either that or null. + assert!(hInstance == 0 || hInstance == kernel32::lock().image_base); assert!(cchBufferMax > 0); let Some(bytes) = find_string(ctx, uID) else { panic!(); diff --git a/win32/winapi/src/winmm/mmio.rs b/win32/winapi/src/winmm/mmio.rs index 530ea215..1b5155d1 100644 --- a/win32/winapi/src/winmm/mmio.rs +++ b/win32/winapi/src/winmm/mmio.rs @@ -290,7 +290,8 @@ pub fn mmioGetInfo(ctx: &mut Context, hmmio: u32, lpmmioinfo: u32, _wFlags: u32) // MMIO_DIRTY is for writing, which we don't support; report a plain // readable buffer covering the whole file. ctx.memory.write::(lpmmioinfo + mmioinfo::DWFLAGS, 0); - ctx.memory.write::(lpmmioinfo + mmioinfo::CCHBUFFER, len); + ctx.memory + .write::(lpmmioinfo + mmioinfo::CCHBUFFER, len); ctx.memory .write::(lpmmioinfo + mmioinfo::PCHBUFFER, buffer); ctx.memory @@ -301,7 +302,8 @@ pub fn mmioGetInfo(ctx: &mut Context, hmmio: u32, lpmmioinfo: u32, _wFlags: u32) .write::(lpmmioinfo + mmioinfo::PCHENDWRITE, buffer + len); // The buffer covers the file from its start, so buffer offsets and file // offsets coincide. - ctx.memory.write::(lpmmioinfo + mmioinfo::LBUFOFFSET, 0); + ctx.memory + .write::(lpmmioinfo + mmioinfo::LBUFOFFSET, 0); ctx.memory .write::(lpmmioinfo + mmioinfo::LDISKOFFSET, len); ctx.memory.write::(lpmmioinfo + mmioinfo::HMMIO, hmmio); From ea6aca209f7b8e7af0aa669a6bcda8d511b7d064 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Tue, 21 Jul 2026 20:03:57 +0300 Subject: [PATCH 08/10] web: choose program with ?exe=, show loading progress --- web/.gitignore | 1 + web/README.md | 13 +++++++------ web/host.ts | 39 +++++++++++++++++++++++++++++++++++---- web/index.html | 5 +++++ web/worker.ts | 22 +++++++++++++++++++++- 5 files changed, 69 insertions(+), 11 deletions(-) diff --git a/web/.gitignore b/web/.gitignore index 0846fe31..e0853448 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -4,3 +4,4 @@ *.map /game /node_modules +/frame.png diff --git a/web/README.md b/web/README.md index 654e5816..41f79d50 100644 --- a/web/README.md +++ b/web/README.md @@ -18,10 +18,11 @@ observable: `/log` (messages, mirrored from the program's own log when the URL carries `?frames=1`) and `/frame` (a PNG of the program's window). A program with data files reads them from an in-memory filesystem that the page -fills in before starting it. Point `dataRoot` in host.ts at a directory holding -the files plus a `manifest.json` listing them, and set `cwd` to the directory -the program should start in. Files the program writes are kept in localStorage -and restored on the next load. +fills in before starting it. Its entry in `PROGRAMS` in host.ts names a +`dataRoot` directory holding the files plus a `manifest.json` listing them, and +the `cwd` the program should start in. Files the program writes are kept in +localStorage and restored on the next load. -URL parameters: `?frames=1` reports progress and window contents to the server, -`?trace=` traces winapi calls in the same syntax as `THESEUS_TRACE`. +URL parameters: `?exe=` picks a program from `PROGRAMS` (default `mine`), +`?frames=1` reports progress and window contents to the server, `?trace=` +traces winapi calls in the same syntax as `THESEUS_TRACE`. diff --git a/web/host.ts b/web/host.ts index 3358002d..0575bfd0 100644 --- a/web/host.ts +++ b/web/host.ts @@ -235,11 +235,18 @@ class Host implements exe.WasmHost { nextAudioStream = 1; messageQueue = new MessageQueue(); + loadingDom = document.getElementById("loading"); + constructor(public wasmMemory: WebAssembly.Memory) { this.consoleDom.id = "console"; document.body.appendChild(this.consoleDom); } + /// Progress from the worker, which does the slow part of startup. + loading(message: string) { + if (this.loadingDom) this.loadingDom.textContent = message; + } + onMessage(e: MessageEvent) { const msg = e.data; const ret = (this as any)[msg.func](...msg.args); @@ -287,6 +294,9 @@ class Host implements exe.WasmHost { } create_window(title: string, width: number, height: number): number { + // The program is up; whatever the page was waiting on is done. + this.loadingDom?.remove(); + this.loadingDom = null; this.window_ = document.createElement("canvas"); this.window_.className = "window"; this.window_.width = width; @@ -414,6 +424,9 @@ class Host implements exe.WasmHost { /// always reachable — a page run from a script, a remote browser — and a game /// that fails to start otherwise gives no clue why. function report(message: string) { + // Only when a script is watching: a page served from anywhere else has + // nothing listening on /log. + if (!REPORT_FRAMES) return; navigator.sendBeacon("/log", message); } @@ -445,6 +458,18 @@ function reportFrame(canvas: HTMLCanvasElement) { window.addEventListener("error", (e) => report(`error: ${e.message}`)); window.addEventListener("unhandledrejection", (e) => report(`rejected: ${e.reason}`)); +// Programs to run, chosen with `?exe=`. One with data files also needs the +// directory holding them plus a manifest.json, and the directory to start in. +const PROGRAMS: Record> = { + mine: { module: "./exe/mine/mine.js" }, + basicdd: { module: "./exe/basicdd/basicdd.js" }, + winpin: { + module: "./exe/winpin/winpin.js", + dataRoot: "./game", + cwd: "/Soccer98", + }, +}; + async function main() { if (!window.SharedArrayBuffer) { document.body.innerText = "SharedArrayBuffer is not supported; possibly try reloading"; @@ -475,14 +500,20 @@ async function main() { } } + const params = new URLSearchParams(location.search); + const name = params.get("exe") ?? "mine"; + const program = PROGRAMS[name]; + if (!program) { + document.body.innerText = `no such program ${name}`; + return; + } + const message: worker.StartMessage = { - module: "./exe/winpin/winpin.js", + ...program, memory, - dataRoot: "./game", - cwd: "/Soccer98", saves, mirrorConsole: REPORT_FRAMES, - trace: new URLSearchParams(location.search).get("trace") ?? "", + trace: params.get("trace") ?? "", }; worker.onerror = (e) => report(`worker: ${e.message}`); worker.postMessage(message); diff --git a/web/index.html b/web/index.html index ed3df2f5..6d15be4b 100644 --- a/web/index.html +++ b/web/index.html @@ -8,6 +8,11 @@ canvas { image-rendering: crisp-edges; } + #loading { + font: 13px sans-serif; + color: #444; + } +
starting
diff --git a/web/worker.ts b/web/worker.ts index 61b4ae5a..81e17593 100644 --- a/web/worker.ts +++ b/web/worker.ts @@ -12,11 +12,14 @@ import type * as exe from "./exe/basicdd/basicdd.js"; /// be resident before it starts; `manifest.json` lists the files to fetch. async function mountData(exe: any, dataRoot: string, saves: Record) { const manifest: string[] = await (await fetch(`${dataRoot}/manifest.json`)).json(); + let fetched = 0; const files = await Promise.all( manifest.map(async (name) => { const response = await fetch(`${dataRoot}/${name}`); if (!response.ok) throw new Error(`${name}: ${response.status}`); - return [name, new Uint8Array(await response.arrayBuffer())] as const; + const data = new Uint8Array(await response.arrayBuffer()); + progress(`loading data ${++fetched}/${manifest.length}`); + return [name, data] as const; }), ); for (const [name, data] of files) { @@ -31,7 +34,20 @@ async function mountData(exe: any, dataRoot: string, saves: Record {}); } @@ -48,8 +64,10 @@ function mirrorConsole() { } async function run(start: StartMessage) { + reportToServer = start.mirrorConsole ?? false; if (start.mirrorConsole) mirrorConsole(); report(`worker loading ${start.module}`); + progress("loading program"); const exe = await import(start.module); report("module imported"); await exe.default(/* module */ undefined, start.memory); @@ -65,6 +83,7 @@ async function run(start: StartMessage) { exe.set_trace(start.trace); } report("starting program"); + progress("starting"); exe.main(); } @@ -86,6 +105,7 @@ export interface StartMessage { self.onmessage = (e: MessageEvent) => { run(e.data).catch((e) => { report(`worker failed: ${e}\n${e?.stack ?? ""}`); + progress(`failed: ${e}`); console.error(e); }); }; From 38b85d7772b46ab7be3cc4bad5083b291a77a217 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Mon, 27 Jul 2026 11:53:30 +0300 Subject: [PATCH 09/10] runtime: cache indirect jump targets --- dos/src/lib.rs | 2 +- runtime/src/lib.rs | 2 +- runtime/src/machine.rs | 84 ++++++++++++----------------- win32/winapi/src/kernel32/thread.rs | 2 +- win32/winapi/src/lib.rs | 2 +- 5 files changed, 37 insertions(+), 55 deletions(-) diff --git a/dos/src/lib.rs b/dos/src/lib.rs index 9149f055..c4db9580 100644 --- a/dos/src/lib.rs +++ b/dos/src/lib.rs @@ -159,7 +159,7 @@ pub fn load(exe: &EXEData, command_line: Option<&str>) -> Context { thread_id: 1, memory, blocks: exe.blocks, - block_map: runtime::BlockMap::get_or_init(exe.blocks), + cache: Default::default(), recent: [Context::return_from_x86; 4], }; ctx.cpu.real_mode = true; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index b175d2ed..0da5529c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,7 +10,7 @@ mod registers; pub use exe::EXEData; pub use flags::Flags; -pub use machine::{BlockMap, CPU, Context}; +pub use machine::{BlockCache, CPU, Context}; pub use mapping::{Mapping, Mappings, round_to_page}; pub use memory::Memory; pub use ops::*; diff --git a/runtime/src/machine.rs b/runtime/src/machine.rs index 1139f728..0f2fc595 100644 --- a/runtime/src/machine.rs +++ b/runtime/src/machine.rs @@ -21,66 +21,42 @@ impl CPU { } } -/// Address -> block lookup. +/// Cache of recently taken indirect jumps. /// -/// The generated block table is sorted, so a binary search over it works, but -/// indirect jumps are on the hot path of every call through a function pointer -/// or COM vtable. This turns the search's chain of dependent loads into a -/// single probe in the common case. -pub struct BlockMap { - /// Power-of-two sized, holding indices into `blocks`, or EMPTY. - slots: Box<[u32]>, - mask: u32, - blocks: &'static [(u32, ContFn)], +/// A program calls through function pointers and COM vtables constantly, but +/// lands on few distinct targets: one game measured 935 of them across 24k +/// blocks, where these 64 entries answered 99.7% of lookups. That leaves the +/// search below cold enough that its cost stops mattering. +pub struct BlockCache { + /// Direct-mapped on the low bits of the address. A slot stores the address + /// it holds so a collision is caught on lookup. Per-slot cells because + /// `indirect` only has `&self`. + slots: [std::cell::Cell>; BlockCache::SIZE], } -impl BlockMap { - const EMPTY: u32 = u32::MAX; +impl BlockCache { + const SIZE: usize = 64; - /// The lookup table for the program's blocks, built once and shared by - /// every thread. - pub fn get_or_init(blocks: &'static [(u32, ContFn)]) -> &'static BlockMap { - static MAP: std::sync::OnceLock = std::sync::OnceLock::new(); - MAP.get_or_init(|| BlockMap::new(blocks)) + fn slot(addr: u32) -> usize { + addr as usize & (Self::SIZE - 1) } - fn new(blocks: &'static [(u32, ContFn)]) -> Self { - // A load factor of at most 1/2 keeps probe chains short. - let capacity = (blocks.len() * 2).next_power_of_two().max(2); - let mask = capacity as u32 - 1; - let mut slots = vec![Self::EMPTY; capacity].into_boxed_slice(); - for (index, &(addr, _)) in blocks.iter().enumerate() { - let mut slot = Self::hash(addr) & mask; - while slots[slot as usize] != Self::EMPTY { - slot = (slot + 1) & mask; - } - slots[slot as usize] = index as u32; - } - BlockMap { - slots, - mask, - blocks, + fn get(&self, addr: u32) -> Option { + match self.slots[Self::slot(addr)].get() { + Some((cached, func)) if cached == addr => Some(func), + _ => None, } } - /// Fibonacci hashing. Code addresses are dense and share their high bits, - /// so the multiply is what spreads them across the table. - fn hash(addr: u32) -> u32 { - addr.wrapping_mul(0x9E37_79B9) + fn insert(&self, addr: u32, func: ContFn) { + self.slots[Self::slot(addr)].set(Some((addr, func))); } +} - pub fn get(&self, addr: u32) -> Option { - let mut slot = Self::hash(addr) & self.mask; - loop { - let index = self.slots[slot as usize]; - if index == Self::EMPTY { - return None; - } - let (block_addr, func) = self.blocks[index as usize]; - if block_addr == addr { - return Some(func); - } - slot = (slot + 1) & self.mask; +impl Default for BlockCache { + fn default() -> Self { + BlockCache { + slots: std::array::from_fn(|_| Default::default()), } } } @@ -91,7 +67,7 @@ pub struct Context { pub thread_id: u32, pub memory: Memory, pub blocks: &'static [(u32, ContFn)], - pub block_map: &'static BlockMap, + pub cache: BlockCache, pub recent: [ContFn; 4], } @@ -112,7 +88,11 @@ impl Context { self.dump(); panic!("jmp to null ptr"); } - let Some(func) = self.block_map.get(addr) else { + if let Some(func) = self.cache.get(addr) { + return Cont(func); + } + // TODO: this would be faster as a perfect hash if we really cared. + let Ok(index) = self.blocks.binary_search_by_key(&addr, |(addr, _)| *addr) else { self.dump(); crate::log_missing_addr(addr); panic!( @@ -120,6 +100,8 @@ impl Context { re-run tc with --entry-points-file (see THESEUS_MISSING_ADDRS)" ); }; + let func = self.blocks[index].1; + self.cache.insert(addr, func); Cont(func) } diff --git a/win32/winapi/src/kernel32/thread.rs b/win32/winapi/src/kernel32/thread.rs index 12f9cfc1..c86cd165 100644 --- a/win32/winapi/src/kernel32/thread.rs +++ b/win32/winapi/src/kernel32/thread.rs @@ -70,7 +70,7 @@ impl kernel32::State { // See docstring on Memory about the unsafety of sharing memory in this way. memory: ctx.memory.unsafe_clone(), blocks: ctx.blocks, - block_map: ctx.block_map, + cache: Default::default(), recent: [Context::return_from_x86; 4], }; self.next_thread_id += 1; diff --git a/win32/winapi/src/lib.rs b/win32/winapi/src/lib.rs index e74640f2..d8f2f461 100644 --- a/win32/winapi/src/lib.rs +++ b/win32/winapi/src/lib.rs @@ -71,7 +71,7 @@ pub fn load(exe: &EXEData) -> Context { thread_id: 1, memory, blocks: exe.blocks, - block_map: runtime::BlockMap::get_or_init(exe.blocks), + cache: Default::default(), recent: [Context::return_from_x86; 4], }; From 136259a4ba169c27b3e4a6adaf2e31c4458b3db2 Mon Sep 17 00:00:00 2001 From: Vasily Krakovetsky Date: Wed, 29 Jul 2026 06:30:56 +0300 Subject: [PATCH 10/10] tc: avoid unstable push_mut --- tc/src/gather.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tc/src/gather.rs b/tc/src/gather.rs index 6a0a7e43..4ce491a4 100644 --- a/tc/src/gather.rs +++ b/tc/src/gather.rs @@ -541,11 +541,12 @@ impl<'a> Traverse<'a> { index_bounds.insert(reg, count); } - let new_instr = instrs.push_mut(Instr { + instrs.push(Instr { ip, iced: instr, hint: None, }); + let new_instr = instrs.last_mut().unwrap(); if self.gather.scan_immediates { for i in 0..instr.op_count() {