Skip to main content

ud_emulator/
lib.rs

1//! Pure-Rust 32-bit x86 emulator + PE loader + Video for Windows
2//! host. Lets oxideav delegate decoding (and eventually encoding)
3//! to legitimately-licensed Windows codec DLLs on any platform.
4//!
5//! **Round 1 — "Load + DllMain + clean exit".** The crate ships:
6//!
7//! * [`emulator::mmu`] — flat 4 GiB virtual address space, sparse
8//!   4 KiB pages with R/W/X permission bits.
9//! * [`emulator::regs`], [`emulator::decode`], [`emulator::isa_int`]
10//!   — i386 register file, instruction decoder, executor for the
11//!   integer base ISA.
12//! * [`pe`] — PE32-only loader: DOS + PE header parse, section
13//!   mapping into the MMU, base relocation, IAT resolution against
14//!   the Win32 stub registry, export-by-name lookup.
15//! * [`win32::kernel32`] — minimum stub set to satisfy a
16//!   Cinepak-class DLL: `GetProcessHeap` / `HeapAlloc` /
17//!   `HeapFree` / `HeapReAlloc` / `LocalAlloc` / `LocalFree` /
18//!   `OutputDebugStringA` / `GetTickCount` /
19//!   `InterlockedIncrement` / `InterlockedDecrement` /
20//!   `LoadLibraryA` / `GetProcAddress`.
21//!
22//! **Round 2 — "Decode one Cinepak frame".** Adds:
23//!
24//! * [`Sandbox::call_export`] — generic stdcall guest-call helper.
25//! * [`win32::vfw32`] — `BITMAPINFOHEADER` marshalling, `ICDECOMPRESS`
26//!   layout, and the `IC*` host surface (`ICOpen`, `ICClose`,
27//!   `ICGetInfo`, `ICDecompressBegin`, `ICDecompressQuery`,
28//!   `ICDecompress`, `ICDecompressEnd`) that drives the codec
29//!   DLL's `DriverProc` end-to-end.
30//! * [`Sandbox::install_codec`] / [`Sandbox::ic_open`] etc — the
31//!   ergonomic Rust-side wrappers the integration test uses.
32//!
33//! **Round 3 — "Real-codec smoke against IR32_32.DLL".** Adds:
34//!
35//! * `tests/common/mod.rs` — fixture-discovery helper:
36//!   `OXIDEAV_VFW_FIXTURE_DIR` env var → Wine prefix → Windows
37//!   system32 → on-disk cache → HTTPS fetch from
38//!   `samples.oxideav.org`. CI=true bypasses the cache.
39//! * Round-3 m1 test asserted the exact set of 49 Win32 imports
40//!   (gdi32 / user32 / winmm + 24 extra kernel32) the
41//!   round-1+2 stub registry did not satisfy — round 4's
42//!   concrete dispatch budget. Round 4 closed every gap; the
43//!   m1 test now asserts zero unresolved imports.
44//! * `tests/m2_indeo3_driverproc.rs` retained the
45//!   synthetic-codec walkthrough; a forward-compatible Indeo 3
46//!   `DllMain → ICOpen → ICGetInfo → ICClose` walkthrough that
47//!   activated once round 4 closed the import gaps.
48//!
49//! **Round 4 — "Close the 49 round-3 import gaps".** Adds the
50//! 49 stubs round 3 surfaced:
51//!
52//! * [`win32::gdi32`] — 8 fail-soft stubs for `BitBlt` /
53//!   `CreateCompatibleDC` / `DeleteDC` / `GetDeviceCaps` /
54//!   `GetNearestColor` / `GetObjectA` /
55//!   `GetSystemPaletteEntries` / `SelectObject`.
56//! * [`win32::kernel32`] — 24 round-4 stubs covering the CRT
57//!   init surface (`ExitProcess`, `GetACP` / `GetOEMCP` /
58//!   `GetCPInfo`, `GetCommandLineA` / `GetEnvironmentStrings` /
59//!   `GetFileType`, `GetLastError` / `SetLastError`,
60//!   `GetModuleFileNameA` / `GetModuleHandleA`,
61//!   `GetStartupInfoA` / `GetStdHandle` / `GetSystemInfo` /
62//!   `GetVersion`, `GlobalAlloc` / `GlobalFree` / `GlobalLock`
63//!   / `GlobalUnlock`, `MultiByteToWideChar` /
64//!   `WideCharToMultiByte`, `RtlUnwind`, `VirtualAlloc` /
65//!   `VirtualFree`, `WriteFile`).
66//! * [`win32::user32`] — 16 fail-soft stubs covering the
67//!   dialog / paint surface; `MessageBoxA` logs to stderr +
68//!   `host.message_box_log`; `wsprintfA` is a real cdecl
69//!   variadic implementation.
70//! * [`win32::winmm`] — `DefDriverProc` (returns 0 / DRVCNF_OK).
71//! * [`emulator::mmu::Mmu::unmap`] +
72//!   [`emulator::mmu::Mmu::find_free_range`] for the
73//!   `VirtualAlloc` / `VirtualFree` family.
74//!
75//! With round 4 in place, `IR32_32.DLL` loads cleanly and
76//! `DllMain` runs until it hits the first ISA opcode our integer
77//! interpreter does not yet decode: `ADD AL, imm8` (opcode
78//! `0x04`) at `eip = 0x1000_612A`. That was round-4's hand-off
79//! to round 5.
80//!
81//! **Round 5 — "DllMain + ICOpen + ICGetInfo + ICClose against
82//! Intel IR32_32.DLL".** Adds:
83//!
84//! * The 8-bit primary ALU opcodes (`0x00..=0x05` ADD,
85//!   `0x08..=0x0D` OR, `0x10..=0x15` ADC, …, `0x38..=0x3D` CMP)
86//!   plus `r/m8 imm8` group-1 (`0x80`), `r/m8` group-3 (`0xF6`),
87//!   `r/m8` group-4 (`0xFE`).
88//! * Group-2 `r/m8` shifts (`0xC0/0xD0/0xD2`) plus the
89//!   `r/m32` 1/cl variants (`0xD1/0xD3`).
90//! * `IMUL r32, r/m32, imm32`/`imm8` (`0x69/0x6B`),
91//!   `XCHG r/m, r` (`0x86/0x87`), `SAHF/LAHF` (`0x9E/0x9F`),
92//!   `CMC` (`0xF5`), `PUSHAD/POPAD` (`0x60/0x61`), `ENTER`
93//!   (`0xC8`), the full `MOVS/CMPS/STOS/LODS/SCAS` family with
94//!   REP / REPE / REPNE prefixes.
95//! * `0F 40..4F CMOVcc`, `0F A3 BT`, `0F AB BTS`, `0F A4..A5
96//!   SHLD`, `0F AC..AD SHRD`, `0F BA` group-8 (BT/BTS/BTR/BTC
97//!   imm8), `0F B1 CMPXCHG`, `0F C1 XADD`, `0F C8..CF BSWAP`.
98//! * Per-instruction segment-override prefix routing through
99//!   [`emulator::Cpu::set_fs_base`] / `set_gs_base`. The
100//!   runtime maps a 4 KiB TEB at `0x7FFD_E000`, primes
101//!   `FS:[0]` (SEH chain end-of-list = `-1`) and `FS:[0x18]`
102//!   (TEB self-pointer), and points FS at it.
103//! * Corrected `vfw32::ICM_*` numeric values
104//!   (`ICM_GETINFO = 0x5002`, `ICM_DECOMPRESS = 0x400D`, etc).
105//! * [`win32::vfw32::ic_open`] now stages a real 36-byte
106//!   `ICOPEN` so the codec's `DRV_OPEN` allocates per-instance
107//!   state (round 4 passed NULL).
108//! * [`win32::vfw32::ic_get_info`] falls back to an
109//!   fcc-derived `szName` when the codec leaves it NUL
110//!   (real `vfw32!ICGetInfo` fills it from the registry).
111//! * Bug-fix: round-4's `0xC6` (MOV r/m8, imm8) handler
112//!   fetched the immediate BEFORE resolving the displacement.
113//!
114//! **Round 6 — "Drive the full IC* decode pipeline end-to-end
115//! against Intel IR32_32.DLL".** No new emulator code needed:
116//! round-5's ISA + segment-prefix coverage is sufficient for the
117//! `ICDecompressQuery → ICDecompressBegin → ICDecompress →
118//! ICDecompressEnd` sequence to walk cleanly. The
119//! `tests/m2_indeo3_driverproc.rs::indeo3_decompress_one_keyframe`
120//! integration test exercises the whole sequence against a
121//! synthetic IV31 keyframe (64×48). The codec accepts the input
122//! / output formats, sets up internal state, rejects the
123//! synthetic NULL-data-size sync frame at the bitstream-header
124//! validation step (returns `ICERR_BADIMAGE = -100`), and tears
125//! down cleanly. SPECGAP: the `IV5PLAY` fixture bundle ships
126//! only DLLs, no `.avi` payloads, so round-6 cannot exercise a
127//! real keyframe end-to-end. Round 7+ swaps the synthetic input
128//! for a real keyframe once one becomes available.
129//!
130//! **Round 7 — "Real IV31 keyframe decode through `cubes.mov`,
131//! plus MMX scaffolding".** Twin deliverables:
132//!
133//! * **Part A — Real keyframe decode.** Adds a test-side
134//!   QuickTime / ISO BMFF chunk walker
135//!   (`tests/common/mov_extractor.rs`, ~270 LOC, authored from
136//!   ISO/IEC 14496-12 alone) that locates sample 0 in
137//!   `cubes.mov` (160×120 yuv410p, 121 KB) from
138//!   `samples.oxideav.org/ffmpeg/V-codecs/IV32/`. The new
139//!   `tests/round7_cubes_mov.rs::cubes_mov_first_keyframe_decodes_through_ir32_32_dll`
140//!   feeds the real 3079-byte IV31 keyframe through the IC*
141//!   sequence; `ICDecompress` returns `ICERR_OK` and ~30 K of
142//!   the 57.6 KB RGB24 output is non-zero — the first real
143//!   pixel decode through `IR32_32.DLL`. The bug fix that
144//!   unblocks this:
145//!   `ICM_DECOMPRESS_BEGIN` was at `ICM_USER + 16 = 0x4010`
146//!   (round-5 typo) — an unmapped slot in `IR32_32.DLL`'s
147//!   dispatch table. The canonical vfw.h value is
148//!   `ICM_USER + 12 = 0x400C`. Without the BEGIN handler running,
149//!   `ICDecompress` bailed early at a `[state2_ptr] != 0`
150//!   sentinel check. While here,
151//!   `ICM_DECOMPRESS_GET_FORMAT` corrected from
152//!   `0x4008` → `0x400A`.
153//! * **Part B — MMX scaffolding** for round 8.
154//!   [`emulator::Cpu`] grows an `mmx: [u64; 8]` register file
155//!   (`mm0..mm7`, per Intel SDM Vol. 1 §9.2.1). A new
156//!   [`emulator::Trap::UnimplementedMmx`] variant carries the
157//!   2-byte opcode + EIP + an SDM-derived mnemonic hint
158//!   (`"PADDB MMX"`, `"PXOR MMX"`, `"EMMS"`, …). The
159//!   `0x0F 0x60..0x6F`, `0x0F 0x70..0x7F`, and
160//!   `0x0F 0xD0..0xFF` opcode blocks (per SDM Vol. 2
161//!   Appendix A Table A-3) now route through
162//!   `emulator::isa_int::dispatch_mmx` to the structured trap
163//!   instead of the generic `UndefinedOpcode`. Round 8 reads
164//!   the trap log to land MMX semantics opcode-by-opcode.
165//!
166//! **Round 18 — `trace` Cargo feature.** Resolves the planned
167//! "trace mode" milestone documented in
168//! `docs/winmf/winmf-emulator.md` §"Trace mode". A new feature
169//! gate `trace` (off by default) adds `#[cfg]`'d probe sites at
170//! the four natural choke points: every `dispatch_stub` call
171//! (`kind=win32_call`), every guest memory access overlapping
172//! a registered watchpoint (`kind=mem_write` / `kind=mem_read`),
173//! every trap that bubbles out of the run loop (`kind=trap`),
174//! and — under the `trace-exec` sub-feature plus
175//! `Sandbox::set_exec_trace(true)` — every executed
176//! instruction (`kind=exec`). Output is JSONL on a sink
177//! configured via `OXIDEAV_VFW_TRACE_FILE=<path|2>` or
178//! [`Sandbox::set_trace_sink`]. With the feature off, every
179//! probe compiles away; release builds are bit-identical to
180//! the round-17 baseline. Companion CLI is
181//! `oxideav-tracevfw`.
182//!
183//! Modern codecs (H.264 / HEVC / AV1 / Opus / AAC / …) are decoded
184//! natively elsewhere in the workspace; this crate exists for
185//! **rare/legacy** codecs the project would otherwise permanently
186//! shelve. Codec DLLs never execute on the host CPU; they run
187//! through the bounded-MMU interpreter.
188//!
189//! See `OxideAV/docs/winmf/winmf-emulator.md` (659 lines, 13
190//! sections) for the full design contract.
191
192#![forbid(unsafe_code)]
193// Mirror-from-oxideav-vfw allowances. Proper `Debug` impls and
194// clippy-pedantic cleanup land in follow-up commits once the
195// crate has been integrated with the decompile pipeline; the
196// initial mirror keeps the source verbatim modulo crate-name
197// shims for ease of diffing against the upstream.
198#![allow(missing_debug_implementations)]
199#![allow(clippy::pedantic)]
200#![allow(clippy::all)]
201
202pub mod com;
203pub mod context;
204pub mod coverage;
205pub mod emulator;
206pub mod ffi;
207pub mod pe;
208pub mod runtime;
209pub mod sched;
210#[cfg(feature = "trace")]
211pub mod trace;
212pub mod win32;
213
214pub use context::{
215    Context, FileAccess, FileHandle, OpenKey, RegistryKey, RegistryValue, VirtualFs,
216    VirtualRegistry, HKCR, HKCU, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE,
217    HKEY_USERS, HKLM, HKU,
218};
219pub use coverage::CoverageMap;
220pub use ffi::{CallArgs, Dword, FromRet, Guest};
221
222pub use com::{
223    Guid, GuidParseError, CLSID_MEMORY_ALLOCATOR, IID_IBASEFILTER, IID_ICLASSFACTORY,
224    IID_IENUMPINS, IID_IFILTERGRAPH, IID_IMEDIAFILTER, IID_IMEDIASAMPLE, IID_IMEMALLOCATOR,
225    IID_IMEMINPUTPIN, IID_IPERSIST, IID_IPIN, IID_IUNKNOWN, MSADDS_AUDIO_DECODER_CLSID,
226    MSADDS_AUDIO_PROPERTY_PAGE_CLSID,
227};
228pub use runtime::{Sandbox, DLL_PROCESS_ATTACH};
229#[cfg(feature = "trace")]
230pub use trace::{TraceState, WatchMode, Watchpoint};
231pub use win32::vfw32::Bih;
232
233/// Crate-local error type. Each layer (MMU / decoder / executor /
234/// PE loader / Win32 stub) has its own variant; sublayers nest
235/// their detail enums.
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub enum Error {
238    /// Reserved placeholder. Removed in a later round once every
239    /// caller has migrated to a more specific variant.
240    NotImplemented,
241    /// Guest tripped a CPU trap (memory fault, illegal opcode,
242    /// privileged instruction, division by zero, …).
243    Trap(emulator::Trap),
244    /// PE loader rejected the input bytes — bad signature,
245    /// unsupported PE32+ / .NET / packed binary, malformed
246    /// directory entries, missing import, etc.
247    PeLoader(pe::PeError),
248    /// A Win32 stub was called with an argument the round-1 stub
249    /// surface cannot satisfy (unknown DLL, unknown ordinal,
250    /// invalid heap handle, etc.).
251    Win32(win32::Win32Error),
252}
253
254impl core::fmt::Display for Error {
255    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
256        match self {
257            Error::NotImplemented => f.write_str(
258                "oxideav-vfw: round-1 does not yet implement this code path; \
259                 see crates/oxideav-vfw/README.md for the milestone schedule.",
260            ),
261            Error::Trap(t) => write!(f, "oxideav-vfw emulator trap: {t}"),
262            Error::PeLoader(e) => write!(f, "oxideav-vfw PE loader: {e}"),
263            Error::Win32(e) => write!(f, "oxideav-vfw Win32 stub: {e}"),
264        }
265    }
266}
267
268impl std::error::Error for Error {}
269
270impl From<emulator::Trap> for Error {
271    fn from(t: emulator::Trap) -> Self {
272        Error::Trap(t)
273    }
274}
275
276impl From<pe::PeError> for Error {
277    fn from(e: pe::PeError) -> Self {
278        Error::PeLoader(e)
279    }
280}
281
282impl From<win32::Win32Error> for Error {
283    fn from(e: win32::Win32Error) -> Self {
284        Error::Win32(e)
285    }
286}
287
288/// Crate-local Result alias.
289pub type Result<T> = core::result::Result<T, Error>;