Skip to main content

ud_emulator/
runtime.rs

1//! Top-level [`Sandbox`] — owns the MMU, the CPU, the Win32 stub
2//! registry, and the per-emulator host state, and exposes the
3//! "load this DLL and call its DllMain" workflow that the
4//! integration tests + future codec wrapper layers drive.
5//!
6//! This is the highest-level public entry point in the crate.
7//! Round-1 exposed [`Sandbox::load`] + [`Sandbox::call_dll_main`];
8//! round-2 adds the generic [`Sandbox::call_export`] helper that
9//! the `vfw32` host stubs use to invoke the codec's `DriverProc`
10//! synchronously.
11
12use crate::emulator::{mmu::Perm, Cpu, Mmu};
13use crate::pe::{Image, Loader};
14use crate::win32::{
15    call_guest, run_until_sentinel as run_until_sentinel_free, vfw32, HostState, Registry,
16    DATA_IMPORT_BASE,
17};
18
19/// `DllMain` reason code: process is loading the DLL.
20pub const DLL_PROCESS_ATTACH: u32 = 1;
21/// `DllMain` reason code: process is unloading the DLL.
22pub const DLL_PROCESS_DETACH: u32 = 0;
23
24/// Default region the loader can use as the kernel32 heap arena.
25const HEAP_ARENA_START: u32 = 0x6000_0000;
26const HEAP_ARENA_END: u32 = 0x7000_0000;
27
28/// Const-arena region — read-only canned strings handed back from
29/// `GetCommandLineA` / `GetEnvironmentStrings` etc.
30const CONST_ARENA_START: u32 = 0x7000_0000;
31const CONST_ARENA_END: u32 = 0x7010_0000;
32
33/// Data-import slot region — see [`crate::win32::DATA_IMPORT_BASE`].
34/// Holds 4-byte values backing CRT data imports like
35/// `msvcrt!_adjust_fdiv`. 4 KiB is plenty.
36const DATA_IMPORT_REGION_SIZE: u32 = 0x0000_1000;
37
38/// Default guest stack region — plenty of room above the heap.
39const STACK_BOTTOM: u32 = 0x9000_0000;
40const STACK_SIZE: u32 = 0x0010_0000; // 1 MiB
41const STACK_TOP: u32 = STACK_BOTTOM + STACK_SIZE;
42
43/// Thread-stack arena. `CreateThread` carves a 64 KiB region
44/// out of this pool per spawned thread, walking down from the
45/// top. 0x8000_0000 .. 0x9000_0000 = 256 MiB → ~4096 aux
46/// threads, plenty for codec / installer corpora.
47const THREAD_STACK_POOL_BOTTOM: u32 = 0x8000_0000;
48const THREAD_STACK_POOL_SIZE: u32 = 0x1000_0000; // 256 MiB
49const THREAD_STACK_POOL_TOP: u32 = THREAD_STACK_POOL_BOTTOM + THREAD_STACK_POOL_SIZE;
50
51/// Thread Environment Block — Windows places its TEB at
52/// `0x7FFD_E000` historically. We map a 4 KiB page here and
53/// stage the SEH chain head (`FS:[0]`) to `0xFFFF_FFFF` ("end of
54/// chain"). Real Windows fills many more fields; for the codec
55/// CRT init we only need a writable page so the codec's SEH
56/// `__try` setup can save the prior chain head, write its own,
57/// and restore on exit.
58const TEB_BASE: u32 = 0x7FFD_E000;
59const TEB_SIZE: u32 = 0x0000_1000; // 4 KiB
60/// `EXCEPTION_REGISTRATION_RECORD*` initialiser at FS:[0].
61const SEH_END_OF_CHAIN: u32 = 0xFFFF_FFFF;
62
63/// Per-thread TIB pool. `CreateThread` carves a 4 KiB TIB out
64/// of this pool per spawned thread, walking down from the
65/// top. 256 KiB → 64 aux thread TIBs, well above any plausible
66/// codec / installer thread count.
67const TIB_POOL_BOTTOM: u32 = 0x7FFC_0000;
68const TIB_POOL_SIZE: u32 = 0x0001_E000; // ~120 KiB → 30 TIBs
69const TIB_POOL_TOP: u32 = TIB_POOL_BOTTOM + TIB_POOL_SIZE;
70
71/// Child-process pools. `CreateProcessA` loads each spawned PE
72/// at [`CHILD_IMAGE_BASE_START`] + N * `CHILD_IMAGE_STRIDE` and
73/// gives each child a private 16 MiB heap arena out of
74/// `[CHILD_HEAP_POOL_START, CHILD_HEAP_POOL_END)`. Picked above
75/// the parent's mapped regions but below TEB / stack-pool
76/// addresses to keep guest pointers easy to read in traces.
77const CHILD_IMAGE_BASE_START: u32 = 0x1000_0000;
78const CHILD_HEAP_POOL_START: u32 = 0xA000_0000;
79const CHILD_HEAP_POOL_SIZE: u32 = 0x1000_0000; // 256 MiB → 16 children
80const CHILD_HEAP_POOL_END: u32 = CHILD_HEAP_POOL_START + CHILD_HEAP_POOL_SIZE;
81
82/// One sandbox instance per loaded codec DLL.
83pub struct Sandbox {
84    pub mmu: Mmu,
85    pub cpu: Cpu,
86    pub registry: Registry,
87    pub host: HostState,
88}
89
90impl Default for Sandbox {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl Sandbox {
97    /// Borrow the always-on coverage map populated by the
98    /// interpreter. Records every dispatched instruction's
99    /// entry EIP plus every guest memory write. See
100    /// [`crate::coverage::CoverageMap`] for the consumer
101    /// surface.
102    #[must_use]
103    pub fn coverage(&self) -> &crate::coverage::CoverageMap {
104        &self.mmu.coverage
105    }
106
107    /// Mutable accessor for the coverage map — useful for
108    /// per-export resets (`coverage_mut().clear()`) between
109    /// runs of the same sandbox.
110    pub fn coverage_mut(&mut self) -> &mut crate::coverage::CoverageMap {
111        &mut self.mmu.coverage
112    }
113
114    /// Borrow the emulation-context layer (virtual filesystem,
115    /// virtual registry, future surfaces). Always present;
116    /// the per-surface options decide whether the guest
117    /// observes synthetic state or the fail-soft Win32
118    /// default. See [`crate::context::Context`].
119    #[must_use]
120    pub fn context(&self) -> &crate::context::Context {
121        &self.host.context
122    }
123
124    /// Mutable accessor for the context.
125    pub fn context_mut(&mut self) -> &mut crate::context::Context {
126        &mut self.host.context
127    }
128
129    /// Builder: attach a virtual filesystem so guest file-API
130    /// calls land in-memory instead of fail-soft no-ops. See
131    /// [`crate::VirtualFs`] for the stage-some-files / capture-
132    /// what's-written workflow.
133    #[must_use]
134    pub fn with_vfs(mut self, vfs: crate::context::VirtualFs) -> Self {
135        self.host.context.vfs = Some(vfs);
136        self
137    }
138
139    /// Builder: attach a virtual registry so guest `Reg*` calls
140    /// observe analyst-staged keys and writes land in-memory.
141    /// See [`crate::VirtualRegistry`].
142    #[must_use]
143    pub fn with_registry(mut self, reg: crate::context::VirtualRegistry) -> Self {
144        self.host.context.registry = Some(reg);
145        self
146    }
147
148    /// Create a fresh sandbox with the heap arena and stack
149    /// pre-mapped, the kernel32 stub set registered, and the
150    /// CPU's `esp` pointing at a freshly-allocated stack.
151    pub fn new() -> Self {
152        let mut mmu = Mmu::new();
153        // Heap arena (R+W+X). Old codecs (e.g. Cinepak) ship
154        // architecture-specific inner-loop assembly that they
155        // copy into `malloc`'d memory at init time and then call.
156        // On real Windows, `HeapAlloc(GetProcessHeap, ...)` returns
157        // executable memory by default; modelling the same is
158        // simpler than chasing per-codec `VirtualProtect(PAGE_EXEC)`
159        // calls. Bytes still respect `mmu.write_initializer`'s
160        // perm rules; only the X bit is broader.
161        mmu.map(
162            HEAP_ARENA_START,
163            HEAP_ARENA_END - HEAP_ARENA_START,
164            Perm::R | Perm::W | Perm::X,
165        );
166        // Const-arena for canned strings (R+W mapped; the caller
167        // ABI treats it as R-only — we use write_initializer for
168        // population, then any reads honour the perm bits).
169        mmu.map(
170            CONST_ARENA_START,
171            CONST_ARENA_END - CONST_ARENA_START,
172            Perm::R | Perm::W,
173        );
174        // Data-import slot region (R+W) — holds the 4-byte
175        // values backing CRT data imports like
176        // `msvcrt!_adjust_fdiv`. Seeded with each registered
177        // import's `initial` value.
178        mmu.map(DATA_IMPORT_BASE, DATA_IMPORT_REGION_SIZE, Perm::R | Perm::W);
179        // Stack (R+W)
180        mmu.map(STACK_BOTTOM, STACK_SIZE, Perm::R | Perm::W);
181        // Thread-stack pool (R+W). `CreateThread` carves
182        // 64 KiB stacks out of the top of this pool, walking
183        // down with each thread.
184        mmu.map(
185            THREAD_STACK_POOL_BOTTOM,
186            THREAD_STACK_POOL_SIZE,
187            Perm::R | Perm::W,
188        );
189        // Stub-thunk region (R-only, zeroed). The run loop
190        // detects `eip == thunk_addr` via `Registry::is_thunk`
191        // *before* hitting the MMU, so execution still routes
192        // to the stub regardless of the X bit — but codecs that
193        // *read* a function pointer's bytes (a hot-patch /
194        // forwarder probe; CamStudio does this in DllMain) need
195        // a mapped region behind the address. Zeros pass every
196        // standard "is this byte E9/EB/CC/C3?" introspection.
197        mmu.map(crate::win32::THUNK_BASE, 0x1_0000, Perm::R);
198        // TEB / FS-segment data (R+W). Initialise FS:[0] = -1
199        // (no SEH handler installed) and FS:[0x18] = TEB self
200        // pointer per the Windows TEB ABI used by Win32 CRTs.
201        mmu.map(TEB_BASE, TEB_SIZE, Perm::R | Perm::W);
202        mmu.write_initializer(TEB_BASE, &SEH_END_OF_CHAIN.to_le_bytes())
203            .expect("seed TEB FS:[0]");
204        mmu.write_initializer(TEB_BASE + 0x18, &TEB_BASE.to_le_bytes())
205            .expect("seed TEB FS:[0x18] (self pointer)");
206        // Per-thread TIB pool (R+W). `CreateThread` carves a
207        // fresh TIB out of this pool for each spawned thread,
208        // setting the new thread's FS base to its own TIB.
209        mmu.map(TIB_POOL_BOTTOM, TIB_POOL_SIZE, Perm::R | Perm::W);
210        // Child-process heap pool (R+W+X). Each `CreateProcessA`
211        // carves a 16 MiB heap arena from this region for the
212        // spawned child.
213        mmu.map(
214            CHILD_HEAP_POOL_START,
215            CHILD_HEAP_POOL_SIZE,
216            Perm::R | Perm::W | Perm::X,
217        );
218        // FS:[0x30] would be the PEB pointer — we leave it 0
219        // until a codec actually dereferences it.
220
221        let mut cpu = Cpu::new();
222        cpu.regs.set_esp(STACK_TOP - 0x100); // leave a guard at the top
223        cpu.set_fs_base(TEB_BASE);
224
225        let mut registry = Registry::new();
226        registry.register_all();
227        // Seed data-import slot values into the mapped region.
228        for (_dll, _name, d) in registry.data_imports() {
229            mmu.write_initializer(d.addr, &d.initial.to_le_bytes())
230                .expect("seed data import");
231        }
232
233        let mut host = HostState::new(HEAP_ARENA_START, HEAP_ARENA_END)
234            .with_const_arena(CONST_ARENA_START, CONST_ARENA_END)
235            .with_thread_stack_pool(THREAD_STACK_POOL_BOTTOM, THREAD_STACK_POOL_TOP)
236            .with_tib_pool(TIB_POOL_BOTTOM, TIB_POOL_TOP)
237            .with_child_arena(
238                CHILD_IMAGE_BASE_START,
239                CHILD_HEAP_POOL_START,
240                CHILD_HEAP_POOL_END,
241            );
242        // Bootstrap thread's TIB lives at the runtime-owned
243        // TEB_BASE (already mapped + seeded above) — mirror
244        // its address into ThreadState so SetLastError /
245        // GetLastError can also write through `fs:[0x34]`.
246        if let Some(t) = host.threads.get_mut(&1) {
247            t.tib_addr = TEB_BASE;
248        }
249
250        // Pre-register the system DLLs whose stub registries we
251        // ship as "loaded modules". Real Windows always has these
252        // available, and codec CRTs commonly probe them via
253        // `GetModuleHandleW(L"KERNEL32.DLL")` before walking their
254        // exports (e.g. lagarith's `_CRT_INIT` rolls back its heap
255        // and bails if `KERNEL32.DLL`'s handle comes back NULL).
256        // The handles are synthetic, distinct, non-zero values in
257        // the otherwise-unmapped `0x7800_0000..0x7900_0000` band.
258        // Codecs use these handles for identity comparisons and
259        // as opaque arguments to `GetProcAddress`; the band is
260        // clear of every other mapped region (heap, const arena,
261        // TEB, stack, VirtualAlloc range, thunk space) so a
262        // codec that tries to *walk* the handle as if it were a
263        // PE image gets a clean `MemoryFault` rather than
264        // accidentally hitting some other arena.
265        for (i, dll) in [
266            "kernel32.dll",
267            "user32.dll",
268            "gdi32.dll",
269            "advapi32.dll",
270            "ole32.dll",
271            "shell32.dll",
272            "shlwapi.dll",
273            "comctl32.dll",
274            "winmm.dll",
275            "msvcrt.dll",
276            "msvcr71.dll",
277            "msvcr80.dll",
278            "msvcr90.dll",
279            "pncrt.dll",
280            "mfplat.dll",
281            "version.dll",
282            "vfw32.dll",
283        ]
284        .iter()
285        .enumerate()
286        {
287            let handle = 0x7800_0000u32.wrapping_add((i as u32) * 0x10_0000);
288            host.modules.insert((*dll).to_string(), handle);
289        }
290
291        // Round 35 — pre-register the canonical DirectShow memory
292        // allocator class factory in the in-process class-factory
293        // cache.  Codecs that internally call
294        // `CoCreateInstance(CLSID_MemoryAllocator, NULL, _,
295        // IID_IMemAllocator, &alloc)` (e.g. mpg4ds32 from inside
296        // `IMemInputPin::GetAllocator`) will now hit our host
297        // factory rather than the round-34 baseline
298        // `CLASS_E_CLASSNOTAVAILABLE` (`0x80040111`) miss.  CLSID
299        // value sourced from Windows SDK header `axextend.h`.
300        if let Ok(factory) =
301            crate::com::mint_host_mem_allocator_class_factory(&mut host, &mut mmu, &registry)
302        {
303            host.com
304                .register_class_factory(crate::com::CLSID_MEMORY_ALLOCATOR, factory);
305        }
306
307        Sandbox {
308            mmu,
309            cpu,
310            registry,
311            host,
312        }
313    }
314
315    /// Builder-style seed setter for the `msvcrt!rand` LCG.
316    ///
317    /// PRNG state for `msvcrt!rand` calls from sandboxed codec
318    /// code.  Default `1` matches MSVC's documented "no `srand`
319    /// called yet" initial value.  Set via `with_rand_seed` /
320    /// `set_rand_seed` for reproducible encode output: two
321    /// sandboxes seeded identically produce identical `rand`
322    /// sequences, which makes encode regression tests
323    /// deterministic across runs.
324    ///
325    /// The guest's own `msvcrt!srand(seed)` call writes to the
326    /// same field, so the codec may re-seed at any time; in that
327    /// case [`Self::rand_seed`] will report whatever value the
328    /// codec last installed.
329    ///
330    /// Round 55.
331    pub fn with_rand_seed(mut self, seed: u32) -> Self {
332        self.host.rand_state = seed;
333        self
334    }
335
336    /// Set the `msvcrt!rand` LCG state at runtime.
337    ///
338    /// Same contract as [`Self::with_rand_seed`], but mutates an
339    /// already-constructed sandbox — useful for tests that drive
340    /// multiple encode runs with different seeds, or for fuzzing
341    /// harnesses that want to force the codec into a known state
342    /// before each iteration.
343    ///
344    /// Round 55.
345    pub fn set_rand_seed(&mut self, seed: u32) {
346        self.host.rand_state = seed;
347    }
348
349    /// Override the value `kernel32!GetCommandLineA` returns to
350    /// the guest. The string is stashed (NUL-terminated) in the
351    /// host's const arena and a pointer to it is parked at
352    /// `command_line_ptr`. Installer-class binaries consult
353    /// this to pick up `/quiet`, `/qn`, `/S` and similar
354    /// silent-install flags.
355    pub fn set_command_line(&mut self, cmdline: &str) -> Result<(), crate::Error> {
356        let mut bytes = cmdline.as_bytes().to_vec();
357        bytes.push(0);
358        let addr = self
359            .host
360            .arena_const_alloc(bytes.len() as u32)
361            .map_err(crate::Error::Win32)?;
362        self.mmu.write_initializer(addr, &bytes)?;
363        self.host.command_line_ptr = addr;
364        Ok(())
365    }
366
367    /// Read the current `msvcrt!rand` LCG state.
368    ///
369    /// Reflects whatever the host or the guest last wrote: a
370    /// fresh sandbox returns `1` (MSVC's documented "no `srand`
371    /// called yet" initial value); after host
372    /// [`Self::set_rand_seed`] / [`Self::with_rand_seed`] returns
373    /// that value; after a guest `msvcrt!srand(s)` call returns
374    /// `s`; after any number of `msvcrt!rand` calls returns the
375    /// post-step LCG state.
376    ///
377    /// Round 55.
378    pub fn rand_seed(&self) -> u32 {
379        self.host.rand_state
380    }
381
382    /// Load a PE32 image from `bytes`, mapping it into the
383    /// sandbox's MMU. The returned [`Image`] holds the entry
384    /// point + export table.
385    ///
386    /// Strict-resolution: any IAT entry the
387    /// [`crate::win32::Registry`] doesn't satisfy is a hard
388    /// load-time error. Use [`Sandbox::load_fail_soft`] for
389    /// EXEs whose import list exceeds the codec-class stub
390    /// surface (installers, GUI apps, etc.).
391    pub fn load(&mut self, name: &str, bytes: &[u8]) -> Result<Image, crate::Error> {
392        let mut loader = Loader::new(&mut self.mmu, &mut self.registry, &mut self.host);
393        let img = loader.load(name, bytes)?;
394        // Record primary module base so `GetModuleHandleA(NULL)`
395        // returns the right value.
396        self.host.primary_module_base = img.image_base;
397        // Also record the loaded module under its filename so
398        // `GetModuleHandleA("name.dll")` finds it. Lower-cased,
399        // matching the lookup in `stub_get_module_handle_a` /
400        // `_w`.
401        self.host
402            .modules
403            .insert(name.to_ascii_lowercase(), img.image_base);
404        Ok(img)
405    }
406
407    /// Load a PE32 image in fail-soft import-resolution mode.
408    /// Imports the codec-class stub registry doesn't satisfy
409    /// get a trap-on-call fallback thunk so the load succeeds.
410    /// Returns the loaded [`Image`] plus the list of
411    /// `(dll, name)` pairs that received a fallback — i.e.
412    /// the set of APIs the operator now knows the binary uses
413    /// but we don't yet stub.
414    ///
415    /// Intended for the install-monitor workflow: load
416    /// QuickTimeInstaller.exe with fail-soft, drive the entry
417    /// point, watch the trap stream for the next missing API.
418    pub fn load_fail_soft(
419        &mut self,
420        name: &str,
421        bytes: &[u8],
422    ) -> Result<(Image, Vec<(String, String)>), crate::Error> {
423        let mut options = crate::pe::LoadOptions {
424            imports: crate::pe::imports::ResolveMode::FailSoft,
425            fail_soft_log: Some(Vec::new()),
426            target_image_base: None,
427        };
428        let mut loader = Loader::new(&mut self.mmu, &mut self.registry, &mut self.host);
429        let img = loader.load_with_options(name, bytes, &mut options)?;
430        self.host.primary_module_base = img.image_base;
431        self.host
432            .modules
433            .insert(name.to_ascii_lowercase(), img.image_base);
434        Ok((img, options.fail_soft_log.unwrap_or_default()))
435    }
436
437    /// Synchronously call `DllMain(hModule, fdwReason, lpvReserved)`
438    /// inside the emulator and return the dword `eax` value at
439    /// the point the function returned to the synthetic
440    /// `RET_SENTINEL`.
441    ///
442    /// The DllMain ABI is stdcall (callee-cleanup), so we push
443    /// `lpvReserved` first, then `fdwReason`, then `hModule`,
444    /// then the return-address sentinel. The callee's `RET 12`
445    /// (or equivalent) cleans the args.
446    ///
447    /// Resolution: prefer the `DllMain` named export (Indeo
448    /// codecs); fall back to the PE `AddressOfEntryPoint`
449    /// (mpg4c32.dll and other CRT-startup-driven DLLs that
450    /// don't export `DllMain` by name). Both expose the same
451    /// stdcall (HINSTANCE, DWORD, LPVOID) ABI.
452    pub fn call_dll_main(&mut self, image: &Image, reason: u32) -> Result<u32, crate::Error> {
453        let h_module = image.image_base;
454        let lpv_reserved = 0u32;
455        let target = image.export("DllMain").unwrap_or(image.entry_point);
456        if target == 0 {
457            return Err(crate::Error::Win32(
458                crate::win32::Win32Error::InvalidArgument {
459                    stub: "call_dll_main",
460                    reason: format!(
461                        "no DllMain export and no PE entry point in {:?}",
462                        image.name
463                    ),
464                },
465            ));
466        }
467        call_guest(
468            &mut self.cpu,
469            &mut self.mmu,
470            &self.registry,
471            &mut self.host,
472            target,
473            &[h_module, reason, lpv_reserved],
474        )
475    }
476
477    /// Generic stdcall guest-call helper. Resolves `name` against
478    /// `image`'s export table, pushes `args` right-to-left + the
479    /// `RET_SENTINEL`, and runs until the callee returns.
480    /// Returns `eax`.
481    ///
482    /// Used both internally (by [`Self::call_dll_main`]) and by
483    /// future codec adapter layers that need to drive arbitrary
484    /// codec exports — `DriverProc`, `MyCodecGetVersion`,
485    /// `MyCodecExtraInit`, etc. The round-2 `vfw32::ic_*` host
486    /// surface uses [`crate::win32::call_guest`] directly with
487    /// the codec's `DriverProc` VA.
488    pub fn call_export(
489        &mut self,
490        image: &Image,
491        name: &str,
492        args: &[u32],
493    ) -> Result<u32, crate::Error> {
494        let target = image.export(name).ok_or_else(|| {
495            crate::Error::Win32(crate::win32::Win32Error::InvalidArgument {
496                stub: "call_export",
497                reason: format!("export {name:?} not found in {:?}", image.name),
498            })
499        })?;
500        call_guest(
501            &mut self.cpu,
502            &mut self.mmu,
503            &self.registry,
504            &mut self.host,
505            target,
506            args,
507        )
508    }
509
510    /// Call the image's PE entry point (`AddressOfEntryPoint`).
511    /// For an EXE this is the CRT startup, which expects no
512    /// arguments and never returns under normal Windows
513    /// semantics (it calls `ExitProcess`). Here it runs until
514    /// the runtime returns to the synthetic `RET_SENTINEL` or
515    /// hits a trap (e.g. unresolved import, instruction limit).
516    pub fn call_entry_point(&mut self, image: &Image) -> Result<u32, crate::Error> {
517        if image.entry_point == 0 {
518            return Err(crate::Error::Win32(
519                crate::win32::Win32Error::InvalidArgument {
520                    stub: "call_entry_point",
521                    reason: format!("no PE entry point in {:?}", image.name),
522                },
523            ));
524        }
525        call_guest(
526            &mut self.cpu,
527            &mut self.mmu,
528            &self.registry,
529            &mut self.host,
530            image.entry_point,
531            &[],
532        )
533    }
534
535    /// Drive the CPU until `eip == RET_SENTINEL`, dispatching to
536    /// Win32 stubs whenever `eip` lands on a registered thunk
537    /// address. Thin wrapper over [`crate::win32::run_until_sentinel`]
538    /// kept for API stability.
539    pub fn run_until_sentinel(&mut self) -> Result<(), crate::Error> {
540        run_until_sentinel_free(&mut self.cpu, &mut self.mmu, &self.registry, &mut self.host)
541    }
542
543    // ---- vfw32 IC* convenience wrappers ------------------------------
544
545    /// Mark `image` as the codec the next [`Self::ic_open`] call
546    /// should target.
547    ///
548    /// Round 2 supports a single codec image per sandbox — round 3
549    /// will lift that into a multi-codec registry. The image must
550    /// export `DriverProc`.
551    pub fn install_codec(&mut self, image: &Image) -> Result<(), crate::Error> {
552        let dp = image.export("DriverProc").ok_or_else(|| {
553            crate::Error::Win32(crate::win32::Win32Error::InvalidArgument {
554                stub: "install_codec",
555                reason: format!("DriverProc not exported by {:?}", image.name),
556            })
557        })?;
558        self.host.default_driver_proc = dp;
559        Ok(())
560    }
561
562    /// Open the installed codec (`DRV_OPEN`).
563    pub fn ic_open(
564        &mut self,
565        fcc_type: u32,
566        fcc_handler: u32,
567        mode: u32,
568    ) -> Result<u32, crate::Error> {
569        vfw32::ic_open(
570            &mut self.cpu,
571            &mut self.mmu,
572            &self.registry,
573            &mut self.host,
574            fcc_type,
575            fcc_handler,
576            mode,
577        )
578    }
579
580    /// Close a codec instance (`DRV_CLOSE`).
581    pub fn ic_close(&mut self, hic: u32) -> Result<u32, crate::Error> {
582        vfw32::ic_close(
583            &mut self.cpu,
584            &mut self.mmu,
585            &self.registry,
586            &mut self.host,
587            hic,
588        )
589    }
590
591    /// Read the codec's `ICINFO` block.
592    pub fn ic_get_info(&mut self, hic: u32, cb: u32) -> Result<Vec<u8>, crate::Error> {
593        vfw32::ic_get_info(
594            &mut self.cpu,
595            &mut self.mmu,
596            &self.registry,
597            &mut self.host,
598            hic,
599            cb,
600        )
601    }
602
603    /// `ICDecompressQuery` — does the codec accept this format?
604    pub fn ic_decompress_query(
605        &mut self,
606        hic: u32,
607        input: &vfw32::Bih,
608        output: Option<&vfw32::Bih>,
609    ) -> Result<u32, crate::Error> {
610        vfw32::ic_decompress_query(
611            &mut self.cpu,
612            &mut self.mmu,
613            &self.registry,
614            &mut self.host,
615            hic,
616            input,
617            output,
618        )
619    }
620
621    /// `ICDecompressGetFormat` — ask the codec for the output BIH
622    /// matching `input`. Round 30 uses this to probe stream
623    /// dimensions when `CodecParameters` lacks them.
624    pub fn ic_decompress_get_format(
625        &mut self,
626        hic: u32,
627        input: &vfw32::Bih,
628    ) -> Result<(u32, vfw32::Bih), crate::Error> {
629        vfw32::ic_decompress_get_format(
630            &mut self.cpu,
631            &mut self.mmu,
632            &self.registry,
633            &mut self.host,
634            hic,
635            input,
636        )
637    }
638
639    /// `ICDecompressBegin` — set up the decoder pipeline.
640    pub fn ic_decompress_begin(
641        &mut self,
642        hic: u32,
643        input: &vfw32::Bih,
644        output: &vfw32::Bih,
645    ) -> Result<u32, crate::Error> {
646        vfw32::ic_decompress_begin(
647            &mut self.cpu,
648            &mut self.mmu,
649            &self.registry,
650            &mut self.host,
651            hic,
652            input,
653            output,
654        )
655    }
656
657    /// `ICDecompressEnd` — tear down the decoder pipeline.
658    pub fn ic_decompress_end(&mut self, hic: u32) -> Result<u32, crate::Error> {
659        vfw32::ic_decompress_end(
660            &mut self.cpu,
661            &mut self.mmu,
662            &self.registry,
663            &mut self.host,
664            hic,
665        )
666    }
667
668    // ---- Trace-mode programmatic API (gated on the `trace`
669    // ---- Cargo feature). Documented in
670    // ---- `docs/winmf/winmf-emulator.md` §"Trace mode".
671
672    /// Install a memory watchpoint covering `[addr, addr+size)`.
673    /// Any guest access whose address range intersects the
674    /// watchpoint emits a `kind=mem_write` (or `mem_read`) JSONL
675    /// event to the configured sink. Multiple watchpoints may
676    /// overlap; each fires independently.
677    #[cfg(feature = "trace")]
678    pub fn watch(&mut self, addr: u32, size: u32, mode: crate::trace::WatchMode) {
679        self.mmu.trace.watch(addr, size, mode);
680    }
681
682    /// Remove watchpoints whose `(addr, size)` exactly matches.
683    /// Mode is ignored for the match.
684    #[cfg(feature = "trace")]
685    pub fn unwatch(&mut self, addr: u32, size: u32) {
686        self.mmu.trace.unwatch(addr, size);
687    }
688
689    /// Toggle per-instruction execution trace at runtime. Has no
690    /// effect unless the crate was built with the `trace-exec`
691    /// sub-feature.
692    #[cfg(feature = "trace")]
693    pub fn set_exec_trace(&mut self, on: bool) {
694        self.mmu.trace.exec_on = on;
695    }
696
697    /// Override the trace JSONL sink at runtime. Defaults to
698    /// honouring `OXIDEAV_VFW_TRACE_FILE`.
699    #[cfg(feature = "trace")]
700    pub fn set_trace_sink(&mut self, sink: Box<dyn std::io::Write + Send>) {
701        self.mmu.trace.set_sink(sink);
702    }
703
704    // ---- COM / DirectShow surface (round 25) ------------------------
705
706    /// Drive `DllGetClassObject(rclsid, riid, ppv)` on `image`,
707    /// staging the GUID arguments + the `ppv` out-slot in a
708    /// freshly-allocated heap region inside the sandbox.  On
709    /// success returns the guest pointer the codec wrote into
710    /// `*ppv` — typically a guest-side `IClassFactory`.
711    ///
712    /// When `riid == IID_IClassFactory`, the returned pointer is
713    /// also registered with [`crate::com::ComObjectTable::register_class_factory`]
714    /// keyed under `clsid`, so subsequent
715    /// [`Self::co_create_instance`] calls can resolve `clsid`
716    /// without re-driving `DllGetClassObject`.
717    ///
718    /// MSDN: `HRESULT DllGetClassObject(REFCLSID rclsid, REFIID
719    /// riid, LPVOID *ppv)` — every COM in-process server
720    /// exports it; DirectShow filter binaries (`.ax`) export it
721    /// instead of `DriverProc`.
722    pub fn dll_get_class_object(
723        &mut self,
724        image: &crate::pe::Image,
725        clsid: crate::com::Guid,
726        riid: crate::com::Guid,
727    ) -> Result<u32, crate::Error> {
728        let target = image.export("DllGetClassObject").ok_or_else(|| {
729            crate::Error::Win32(crate::win32::Win32Error::InvalidArgument {
730                stub: "dll_get_class_object",
731                reason: format!("DllGetClassObject not exported by {:?}", image.name),
732            })
733        })?;
734        // Stage the two GUIDs + the out-pointer slot in
735        // contiguous arena memory: 16 + 16 + 4 = 36 bytes.
736        let scratch = self.host.arena_alloc(36).map_err(crate::Error::Win32)?;
737        clsid
738            .stage(&mut self.mmu, scratch)
739            .map_err(crate::Error::Trap)?;
740        riid.stage(&mut self.mmu, scratch + 16)
741            .map_err(crate::Error::Trap)?;
742        // Zero the ppv slot.
743        self.mmu
744            .write_initializer(scratch + 32, &0u32.to_le_bytes())
745            .map_err(crate::Error::Trap)?;
746        let hr = call_guest(
747            &mut self.cpu,
748            &mut self.mmu,
749            &self.registry,
750            &mut self.host,
751            target,
752            &[scratch, scratch + 16, scratch + 32],
753        )?;
754        if hr != crate::com::S_OK {
755            return Err(crate::Error::Win32(
756                crate::win32::Win32Error::InvalidArgument {
757                    stub: "dll_get_class_object",
758                    reason: format!("DllGetClassObject returned HRESULT {hr:#010x}"),
759                },
760            ));
761        }
762        let out_ptr = self.mmu.load32(scratch + 32).map_err(crate::Error::Trap)?;
763        if out_ptr == 0 {
764            return Err(crate::Error::Win32(
765                crate::win32::Win32Error::InvalidArgument {
766                    stub: "dll_get_class_object",
767                    reason: "DllGetClassObject succeeded but *ppv is NULL".into(),
768                },
769            ));
770        }
771        // Bookkeep the new object.  If it is a class factory,
772        // also register it under `clsid` so `CoCreateInstance`
773        // can pick it up.
774        self.host.com.intern(out_ptr, Some(riid));
775        if riid == crate::com::IID_ICLASSFACTORY {
776            self.host.com.register_class_factory(clsid, out_ptr);
777        }
778        Ok(out_ptr)
779    }
780
781    /// Drive `CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER,
782    /// riid, ppv)` against the in-process class-factory cache.
783    /// The CLSID must already be registered (typically by a
784    /// prior [`Self::dll_get_class_object`] call); otherwise
785    /// surfaces `CLASS_E_CLASSNOTAVAILABLE` as an error.
786    pub fn co_create_instance(
787        &mut self,
788        clsid: crate::com::Guid,
789        riid: crate::com::Guid,
790    ) -> Result<u32, crate::Error> {
791        let factory = self.host.com.lookup_class_factory(&clsid).ok_or_else(|| {
792            crate::Error::Win32(crate::win32::Win32Error::InvalidArgument {
793                stub: "co_create_instance",
794                reason: format!(
795                    "CLSID {clsid} not registered; \
796                     call dll_get_class_object first"
797                ),
798            })
799        })?;
800        // Stage IID + out slot.
801        let scratch = self.host.arena_alloc(20).map_err(crate::Error::Win32)?;
802        riid.stage(&mut self.mmu, scratch)
803            .map_err(crate::Error::Trap)?;
804        self.mmu
805            .write_initializer(scratch + 16, &0u32.to_le_bytes())
806            .map_err(crate::Error::Trap)?;
807        let r = crate::com::call::call_method(
808            &mut self.cpu,
809            &mut self.mmu,
810            &self.registry,
811            &mut self.host,
812            factory,
813            crate::com::SLOT_CLASS_FACTORY_CREATE_INSTANCE,
814            &[0, scratch, scratch + 16],
815        )?;
816        if r != crate::com::S_OK {
817            return Err(crate::Error::Win32(
818                crate::win32::Win32Error::InvalidArgument {
819                    stub: "co_create_instance",
820                    reason: format!("CreateInstance returned HRESULT {r:#010x}"),
821                },
822            ));
823        }
824        let out = self.mmu.load32(scratch + 16).map_err(crate::Error::Trap)?;
825        if out != 0 {
826            self.host.com.intern(out, Some(riid));
827        }
828        Ok(out)
829    }
830
831    /// Drive `obj->QueryInterface(riid, ppv)` on a guest COM
832    /// object, staging the IID + out-slot in arena memory.
833    /// Returns the new interface pointer on success, or surfaces
834    /// the HRESULT in an error message.
835    pub fn query_interface(
836        &mut self,
837        obj: u32,
838        riid: crate::com::Guid,
839    ) -> Result<u32, crate::Error> {
840        let scratch = self.host.arena_alloc(20).map_err(crate::Error::Win32)?;
841        riid.stage(&mut self.mmu, scratch)
842            .map_err(crate::Error::Trap)?;
843        self.mmu
844            .write_initializer(scratch + 16, &0u32.to_le_bytes())
845            .map_err(crate::Error::Trap)?;
846        let r = crate::com::call::query_interface(
847            &mut self.cpu,
848            &mut self.mmu,
849            &self.registry,
850            &mut self.host,
851            obj,
852            scratch,
853            scratch + 16,
854        )?;
855        if r != crate::com::S_OK {
856            return Err(crate::Error::Win32(
857                crate::win32::Win32Error::InvalidArgument {
858                    stub: "query_interface",
859                    reason: format!("QueryInterface returned HRESULT {r:#010x}"),
860                },
861            ));
862        }
863        let out = self.mmu.load32(scratch + 16).map_err(crate::Error::Trap)?;
864        if out != 0 {
865            self.host.com.intern(out, Some(riid));
866        }
867        Ok(out)
868    }
869
870    /// Round 27 — mint a host-side `IFilterGraph` stub so the
871    /// codec's `IBaseFilter::JoinFilterGraph(pGraph, pName)` call
872    /// has a non-NULL parent graph to record.  The returned guest
873    /// pointer's vtable function-pointer slots are synthetic
874    /// thunk addresses that route into the host stubs registered
875    /// by [`crate::com::host_iface::register`].
876    ///
877    /// `QueryInterface(IID_IUnknown | IID_IFilterGraph)` →
878    /// `S_OK + *ppv = obj`; every other IID returns
879    /// `E_NOINTERFACE`.  All eight `IFilterGraph` methods return
880    /// `E_NOTIMPL` — none are exercised on the
881    /// `JoinFilterGraph → ReceiveConnection` path the round-27
882    /// probe takes.
883    pub fn mint_host_filter_graph(&mut self) -> Result<u32, crate::Error> {
884        crate::com::mint_host_filter_graph(&mut self.host, &mut self.mmu, &self.registry)
885    }
886
887    /// Round 27 — mint a host-side `IPin` stub that pretends to
888    /// be an OUTPUT pin advertising `amt_addr` (a pointer to a
889    /// staged `AM_MEDIA_TYPE`).  Suitable as the `pConnector`
890    /// argument of `IPin::ReceiveConnection`.
891    ///
892    /// `QueryDirection` reports `PIN_OUTPUT`; `QueryAccept`
893    /// returns `S_OK`; `ConnectionMediaType` copies the staged
894    /// AMT; `EnumMediaTypes` vends an enumerator yielding the
895    /// staged AMT once.
896    pub fn mint_host_output_pin(&mut self, amt_addr: u32) -> Result<u32, crate::Error> {
897        crate::com::host_iface::mint_host_output_pin(
898            &mut self.host,
899            &mut self.mmu,
900            &self.registry,
901            amt_addr,
902        )
903    }
904
905    /// Round 37 — same as [`Self::mint_host_output_pin`] but also
906    /// stamps the codec's input-pin pointer (`connected_pin`) into
907    /// the new pin object so `IPin::ConnectedTo` can return it,
908    /// and synthesizes a parent `HostIBaseFilter` so
909    /// `IPin::QueryPinInfo` can fill in `PIN_INFO::pFilter`.
910    ///
911    /// `connected_pin == 0` falls back to the round-30 behaviour
912    /// where the pin reports `VFW_E_NOT_CONNECTED` from
913    /// `ConnectedTo`.
914    pub fn mint_host_output_pin_with_connection(
915        &mut self,
916        amt_addr: u32,
917        connected_pin: u32,
918    ) -> Result<u32, crate::Error> {
919        crate::com::host_iface::mint_host_output_pin_with_connection(
920            &mut self.host,
921            &mut self.mmu,
922            &self.registry,
923            amt_addr,
924            connected_pin,
925        )
926    }
927
928    /// Round 37 — number of `IPin::QueryPinInfo` calls the codec
929    /// has driven against any host pin during this sandbox's
930    /// lifetime.
931    pub fn query_pin_info_call_count(&self) -> usize {
932        crate::com::host_iface::query_pin_info_call_count(&self.host)
933    }
934
935    /// Round 37 — number of `IBaseFilter::QueryFilterInfo` calls
936    /// the codec has driven against any host filter during this
937    /// sandbox's lifetime.
938    pub fn query_filter_info_call_count(&self) -> usize {
939        crate::com::host_iface::query_filter_info_call_count(&self.host)
940    }
941
942    /// Round 37 — `this` pointers of every `IPin::QueryPinInfo`
943    /// call observed.
944    pub fn query_pin_info_calls(&self) -> Vec<u32> {
945        crate::com::host_iface::query_pin_info_calls(&self.host)
946    }
947
948    /// Round 37 — `this` pointers of every
949    /// `IBaseFilter::QueryFilterInfo` call observed.
950    pub fn query_filter_info_calls(&self) -> Vec<u32> {
951        crate::com::host_iface::query_filter_info_calls(&self.host)
952    }
953
954    /// Round 37 — drop every captured introspection call from this
955    /// sandbox's per-state log.
956    pub fn clear_query_info_log(&self) {
957        crate::com::host_iface::clear_query_info_log(&self.host)
958    }
959
960    /// Round 30 — mint a host-side `IMemAllocator` backed by a
961    /// pool of `pool_size` IMediaSample slots, each carrying a
962    /// fresh `sample_capacity`-byte data region. The returned
963    /// guest pointer is suitable as the `pAllocator` argument of
964    /// `IMemInputPin::NotifyAllocator`.
965    ///
966    /// `media_type_ptr` is returned by every minted sample's
967    /// `IMediaSample::GetMediaType` — pass `0` if no AMT should
968    /// surface there (codecs then fall back to the upstream pin's
969    /// connection media type).
970    pub fn mint_host_mem_allocator(
971        &mut self,
972        pool_size: u32,
973        sample_capacity: u32,
974        media_type_ptr: u32,
975    ) -> Result<u32, crate::Error> {
976        crate::com::mint_host_mem_allocator(
977            &mut self.host,
978            &mut self.mmu,
979            &self.registry,
980            pool_size,
981            sample_capacity,
982            media_type_ptr,
983        )
984    }
985
986    /// Round 35 — mint a host-side `IClassFactory` whose
987    /// `CreateInstance` mints fresh `HostIMemAllocator` instances.
988    ///
989    /// Pre-registered in [`Sandbox::new`] under
990    /// [`crate::com::CLSID_MEMORY_ALLOCATOR`]; this method exists
991    /// for tests that want a raw factory pointer to drive
992    /// `IClassFactory::CreateInstance` directly without going
993    /// through the `ole32!CoCreateInstance` cascade.
994    pub fn mint_host_mem_allocator_class_factory(&mut self) -> Result<u32, crate::Error> {
995        crate::com::mint_host_mem_allocator_class_factory(
996            &mut self.host,
997            &mut self.mmu,
998            &self.registry,
999        )
1000    }
1001
1002    /// Round 30 — mint a single host-side `IMediaSample` wrapping
1003    /// a fresh `data_capacity`-byte data region. Useful for
1004    /// stand-alone tests; production paths typically mint samples
1005    /// implicitly via [`Self::mint_host_mem_allocator`].
1006    pub fn mint_host_media_sample(
1007        &mut self,
1008        data_capacity: u32,
1009        media_type_ptr: u32,
1010    ) -> Result<u32, crate::Error> {
1011        crate::com::mint_host_media_sample(
1012            &mut self.host,
1013            &mut self.mmu,
1014            &self.registry,
1015            data_capacity,
1016            media_type_ptr,
1017        )
1018    }
1019
1020    /// Round 30 — copy a payload into a previously-minted sample
1021    /// + flag whether it is a sync (key) frame.
1022    ///
1023    /// Wraps [`crate::com::media_sample_set_payload`].
1024    pub fn media_sample_set_payload(
1025        &mut self,
1026        sample: u32,
1027        payload: &[u8],
1028        sync_point: bool,
1029    ) -> Result<(), crate::Error> {
1030        crate::com::media_sample_set_payload(&mut self.mmu, sample, payload, sync_point)
1031    }
1032
1033    /// Round 31 — mint a paired downstream `(HostIPin, HostIMemInputPin)`
1034    /// for receiving samples the codec pushes from its output pin.
1035    pub fn host_iface_r31_mint_input_pin_pair(&mut self) -> Result<(u32, u32), crate::Error> {
1036        crate::com::host_iface_r31::mint_host_input_pin_pair(
1037            &mut self.host,
1038            &mut self.mmu,
1039            &self.registry,
1040        )
1041    }
1042
1043    /// Round 31 — mint a minimal HostIBaseFilter exposing
1044    /// `input_pin`.
1045    pub fn host_iface_r31_mint_base_filter(&mut self, input_pin: u32) -> Result<u32, crate::Error> {
1046        crate::com::host_iface_r31::mint_host_base_filter(
1047            &mut self.host,
1048            &mut self.mmu,
1049            &self.registry,
1050            input_pin,
1051        )
1052    }
1053
1054    /// Round 31 — pop the oldest sample captured by the
1055    /// downstream `HostIMemInputPin::Receive` callback.
1056    pub fn pop_received_sample(&self) -> Option<crate::com::host_iface_r31::ReceivedSample> {
1057        crate::com::host_iface_r31::pop_sample(&self.host)
1058    }
1059
1060    /// Round 31 — number of samples currently waiting in the
1061    /// host-side queue.
1062    pub fn received_samples_len(&self) -> usize {
1063        crate::com::host_iface_r31::queue_len(&self.host)
1064    }
1065
1066    /// Round 33 — return the most recent
1067    /// `IMemAllocator::SetProperties` capture observed on this
1068    /// sandbox, or `None` if no codec has called `SetProperties`
1069    /// yet.  See [`crate::com::AllocatorPropertiesCapture`] for
1070    /// the captured field shape.
1071    pub fn last_set_properties(&self) -> Option<crate::com::AllocatorPropertiesCapture> {
1072        crate::com::last_set_properties(&self.host)
1073    }
1074
1075    /// Round 33 — return every `SetProperties` capture observed on
1076    /// this sandbox, in arrival order.
1077    pub fn all_set_properties(&self) -> Vec<crate::com::AllocatorPropertiesCapture> {
1078        crate::com::all_set_properties(&self.host)
1079    }
1080
1081    /// Round 33 — drop every captured `SetProperties` for this
1082    /// sandbox.  Useful for resetting per-test state.
1083    pub fn clear_set_properties_log(&self) {
1084        crate::com::clear_set_properties_log(&self.host)
1085    }
1086
1087    /// Drive `obj->AddRef()`.  Returns the codec-reported new
1088    /// refcount; the host's bookkeeping is updated automatically.
1089    pub fn com_add_ref(&mut self, obj: u32) -> Result<u32, crate::Error> {
1090        crate::com::call::add_ref(
1091            &mut self.cpu,
1092            &mut self.mmu,
1093            &self.registry,
1094            &mut self.host,
1095            obj,
1096        )
1097    }
1098
1099    /// Drive `obj->Release()`.  Returns the codec-reported new
1100    /// refcount.  The host's bookkeeping is updated automatically.
1101    pub fn com_release(&mut self, obj: u32) -> Result<u32, crate::Error> {
1102        crate::com::call::release(
1103            &mut self.cpu,
1104            &mut self.mmu,
1105            &self.registry,
1106            &mut self.host,
1107            obj,
1108        )
1109    }
1110
1111    /// `ICDecompress` — decode one frame.
1112    #[allow(clippy::too_many_arguments)]
1113    pub fn ic_decompress(
1114        &mut self,
1115        hic: u32,
1116        flags: u32,
1117        input_bih: &vfw32::Bih,
1118        input_bytes: &[u8],
1119        output_bih: &vfw32::Bih,
1120        output_capacity: u32,
1121    ) -> Result<(u32, Vec<u8>), crate::Error> {
1122        vfw32::ic_decompress(
1123            &mut self.cpu,
1124            &mut self.mmu,
1125            &self.registry,
1126            &mut self.host,
1127            hic,
1128            flags,
1129            input_bih,
1130            input_bytes,
1131            output_bih,
1132            output_capacity,
1133        )
1134    }
1135
1136    // ---- Round 51: encode (compress) wrappers --------------------------
1137
1138    /// `ICCompressQuery` — does the codec accept this input/output
1139    /// format pair? `output` may be `None` to defer the choice.
1140    pub fn ic_compress_query(
1141        &mut self,
1142        hic: u32,
1143        input: &vfw32::Bih,
1144        output: Option<&vfw32::Bih>,
1145    ) -> Result<u32, crate::Error> {
1146        vfw32::ic_compress_query(
1147            &mut self.cpu,
1148            &mut self.mmu,
1149            &self.registry,
1150            &mut self.host,
1151            hic,
1152            input,
1153            output,
1154        )
1155    }
1156
1157    /// `ICCompressGetFormat` — ask the codec for the output BIH
1158    /// describing what its compressed format looks like for the
1159    /// supplied input.
1160    pub fn ic_compress_get_format(
1161        &mut self,
1162        hic: u32,
1163        input: &vfw32::Bih,
1164    ) -> Result<(u32, vfw32::Bih), crate::Error> {
1165        vfw32::ic_compress_get_format(
1166            &mut self.cpu,
1167            &mut self.mmu,
1168            &self.registry,
1169            &mut self.host,
1170            hic,
1171            input,
1172        )
1173    }
1174
1175    /// `ICCompressGetSize` — max encoded-frame byte count for the
1176    /// supplied input/output BIH pair.
1177    pub fn ic_compress_get_size(
1178        &mut self,
1179        hic: u32,
1180        input: &vfw32::Bih,
1181        output: &vfw32::Bih,
1182    ) -> Result<u32, crate::Error> {
1183        vfw32::ic_compress_get_size(
1184            &mut self.cpu,
1185            &mut self.mmu,
1186            &self.registry,
1187            &mut self.host,
1188            hic,
1189            input,
1190            output,
1191        )
1192    }
1193
1194    /// `ICCompressBegin` — set up the encoder pipeline.
1195    pub fn ic_compress_begin(
1196        &mut self,
1197        hic: u32,
1198        input: &vfw32::Bih,
1199        output: &vfw32::Bih,
1200    ) -> Result<u32, crate::Error> {
1201        vfw32::ic_compress_begin(
1202            &mut self.cpu,
1203            &mut self.mmu,
1204            &self.registry,
1205            &mut self.host,
1206            hic,
1207            input,
1208            output,
1209        )
1210    }
1211
1212    /// `ICCompressEnd` — tear down the encoder pipeline.
1213    pub fn ic_compress_end(&mut self, hic: u32) -> Result<u32, crate::Error> {
1214        vfw32::ic_compress_end(
1215            &mut self.cpu,
1216            &mut self.mmu,
1217            &self.registry,
1218            &mut self.host,
1219            hic,
1220        )
1221    }
1222
1223    /// `ICCompress` — encode one frame. Returns the full encode
1224    /// outcome: codec LRESULT, encoded bytes, the post-call output
1225    /// BIH (whose `biSizeImage` holds the actual encoded byte
1226    /// count), the codec-written `*lpdwFlags` (e.g. whether the
1227    /// codec marked the emitted frame as a keyframe), and the
1228    /// codec-written `*lpckid`.
1229    ///
1230    /// `prev_bih_opt` / `prev_bytes_opt` are the previous
1231    /// reconstructed frame (P-frame encoding). Pass `None` for
1232    /// keyframes.
1233    #[allow(clippy::too_many_arguments)]
1234    pub fn ic_compress(
1235        &mut self,
1236        hic: u32,
1237        flags: u32,
1238        input_bih: &vfw32::Bih,
1239        input_bytes: &[u8],
1240        output_bih: &vfw32::Bih,
1241        output_capacity: u32,
1242        ckid: u32,
1243        frame_num: i32,
1244        frame_size_limit: u32,
1245        quality: u32,
1246        prev_bih_opt: Option<&vfw32::Bih>,
1247        prev_bytes_opt: Option<&[u8]>,
1248    ) -> Result<vfw32::CompressOutcome, crate::Error> {
1249        vfw32::ic_compress(
1250            &mut self.cpu,
1251            &mut self.mmu,
1252            &self.registry,
1253            &mut self.host,
1254            hic,
1255            flags,
1256            input_bih,
1257            input_bytes,
1258            output_bih,
1259            output_capacity,
1260            ckid,
1261            frame_num,
1262            frame_size_limit,
1263            quality,
1264            prev_bih_opt,
1265            prev_bytes_opt,
1266        )
1267    }
1268
1269    /// `ICGetState` — ask the codec to serialise its private
1270    /// per-instance state into `dst_buf`.  Returns the byte count
1271    /// the codec actually wrote.
1272    ///
1273    /// Round 70 — wraps `ICM_GETSTATE` (`0x5009`) per MSDN; required
1274    /// by oxideav-tracevfw to drive the encoder's per-quality knob
1275    /// round-trip alongside [`Self::ic_set_state`].  See MSDN
1276    /// `ICGetState` topic page for the public contract.
1277    pub fn ic_get_state(&mut self, hic: u32, dst_buf: &mut [u8]) -> Result<u32, crate::Error> {
1278        vfw32::ic_get_state(
1279            &mut self.cpu,
1280            &mut self.mmu,
1281            &self.registry,
1282            &mut self.host,
1283            hic,
1284            dst_buf,
1285        )
1286    }
1287
1288    /// `ICSetState` — ask the codec to deserialise `src_buf` into
1289    /// its private per-instance state.  Returns `Ok(())` on
1290    /// `ICERR_OK`, or [`crate::Error`] wrapping the codec's raw
1291    /// `LRESULT` otherwise.
1292    ///
1293    /// Round 70 — wraps `ICM_SETSTATE` (`0x500A`) per MSDN.  See
1294    /// MSDN `ICSetState` topic page for the public contract.
1295    pub fn ic_set_state(&mut self, hic: u32, src_buf: &[u8]) -> Result<(), crate::Error> {
1296        vfw32::ic_set_state(
1297            &mut self.cpu,
1298            &mut self.mmu,
1299            &self.registry,
1300            &mut self.host,
1301            hic,
1302            src_buf,
1303        )
1304    }
1305
1306    /// Round 63 — patch `msadds32.ax`'s `helper_addref` thunk (at
1307    /// RVA `0x5cea`) to unconditionally return `value` (32-bit
1308    /// integer).
1309    ///
1310    /// **Why.** Round-62 forensics
1311    /// (`docs/codec/msadds32-receive-null-0x20.md`) traced the
1312    /// `IMemInputPin::Receive` NULL-deref trap at RVA `0x256a` to
1313    /// a buffer-pool init that's handed a size of zero.  The size
1314    /// is `(h * 10) / size_calc(...)` where `h` is what
1315    /// `helper_addref` returns.  On a fresh codec instance the
1316    /// helper-object field at `helper_90 + 0x3c` (the "initialised"
1317    /// flag the addref checks) is zero, so `helper_addref` returns
1318    /// `0`, the quotient is `0`, `operator new(0)` returns NULL,
1319    /// `buffer_pool_init` fails, and the Receive cleanup branch
1320    /// trips a NULL+0x20 deref.
1321    ///
1322    /// In a real DirectShow host the flag is set during
1323    /// `IFilterGraph::JoinFilterGraph` / `Pause` (the codec stamps
1324    /// the field as part of its run-state machine).  Until we
1325    /// drive that path, the surgical workaround is to short-circuit
1326    /// `helper_addref` to return a fixed non-zero value — which
1327    /// empirically lifts the trap and lets Receive run to
1328    /// completion (with HRESULT `0x8000ffff` from the decode body,
1329    /// which is the next round's investigation surface).
1330    ///
1331    /// **Encoding.** The original function (RVA `0x5cea`, 10 bytes)
1332    /// is:
1333    ///
1334    /// ```text
1335    /// 0x5cea: 83 79 20 00  cmp [ecx+0x20], 0
1336    /// 0x5cee: 74 04        jz  +4
1337    /// 0x5cf0: 8b 41 28     mov eax, [ecx+0x28]
1338    /// 0x5cf3: c3           ret
1339    /// 0x5cf4: 33 c0        xor eax, eax
1340    /// 0x5cf6: c3           ret
1341    /// ```
1342    ///
1343    /// We overwrite the first 6 bytes with:
1344    ///
1345    /// ```text
1346    /// b8 XX XX XX XX  mov eax, imm32
1347    /// c3              ret
1348    /// ```
1349    ///
1350    /// The remaining bytes at `0x5cf0..0x5cf6` are unreachable
1351    /// after the patch (no caller enters there directly), so the
1352    /// dead-code is harmless.
1353    ///
1354    /// `image_base` is the address the codec was loaded at; pass
1355    /// the value returned by [`Self::load`] on `msadds32.ax`.
1356    ///
1357    /// # Reference material (clean-room only)
1358    ///
1359    /// * Intel SDM Vol. 2A — `MOV imm32` (`B8+rd`), `RET` (`C3`).
1360    /// * Raw bytes of `msadds32.ax` from
1361    ///   `docs/video/msmpeg4/reference/binaries/wmpcdcs8-2001/`.
1362    ///
1363    /// No Wine / ReactOS / MinGW / Microsoft DShow source consulted.
1364    pub fn msadds32_patch_helper_addref(
1365        &mut self,
1366        image_base: u32,
1367        value: u32,
1368    ) -> Result<(), crate::Error> {
1369        const RVA_HELPER_ADDREF: u32 = 0x5cea;
1370        let va = image_base.wrapping_add(RVA_HELPER_ADDREF);
1371        let mut patch = [0u8; 6];
1372        patch[0] = 0xb8; // mov eax, imm32
1373        patch[1..5].copy_from_slice(&value.to_le_bytes());
1374        patch[5] = 0xc3; // ret
1375        self.mmu
1376            .write_initializer(va, &patch)
1377            .map_err(crate::Error::Trap)
1378    }
1379}
1380
1381#[cfg(test)]
1382mod tests {
1383    use super::*;
1384    use crate::emulator::isa_int::RET_SENTINEL;
1385    use crate::emulator::regs::Reg32;
1386    use crate::pe::test_image::build_minimal_dll;
1387
1388    #[test]
1389    fn load_synth_dll_and_run_dll_main_returns_to_sentinel() {
1390        let bytes = build_minimal_dll();
1391        let mut sb = Sandbox::new();
1392        let img = sb.load("synth.dll", &bytes).unwrap();
1393        // Pre-set eax = 1 so we can confirm the synth DllMain
1394        // returned without modifying it (it's just `ret 12`).
1395        sb.cpu.regs.set32(Reg32::Eax, 1);
1396        let ret = sb.call_dll_main(&img, DLL_PROCESS_ATTACH).unwrap();
1397        assert_eq!(ret, 1);
1398        assert_eq!(sb.cpu.regs.eip, RET_SENTINEL);
1399    }
1400
1401    #[test]
1402    fn calling_through_iat_thunk_invokes_kernel32_stub() {
1403        // Emulator-only test: fabricate a code block that calls
1404        // a kernel32!GetProcessHeap thunk and rets. Verifies the
1405        // run loop's "is_thunk → dispatch" path.
1406        let mut sb = Sandbox::new();
1407        let thunk = sb
1408            .registry
1409            .resolve("kernel32.dll", "GetProcessHeap")
1410            .unwrap();
1411        // Map a code page at 0x1000.
1412        sb.mmu.map(0x1000, 0x1000, Perm::R | Perm::X);
1413        // call dword [thunk_slot]; ret 0
1414        // Easier: set eip directly to the thunk after pushing
1415        // the synthetic ret-sentinel.
1416        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1417        sb.cpu.regs.eip = thunk;
1418        sb.run_until_sentinel().unwrap();
1419        assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 0xDEAD_BEEF);
1420    }
1421
1422    /// Phase 3c of the scheduler refactor: `WaitForSingleObject`
1423    /// on an unsignalled auto-reset Event parks the calling
1424    /// thread; `SetEvent` from a peer wakes exactly one waiter.
1425    /// We exercise this directly through state mutation rather
1426    /// than spawning guest threads (the kernel32 thunk path is
1427    /// covered by the spawn test below).
1428    #[test]
1429    fn wait_for_single_object_blocks_until_set_event() {
1430        let mut sb = Sandbox::new();
1431        // Mint an auto-reset Event, initially unsignalled.
1432        let h = sb
1433            .host
1434            .scheduler
1435            .insert_object(crate::sched::WaitObject::Event {
1436                signaled: false,
1437                manual_reset: false,
1438            });
1439        // Inject a second thread parked on a wait for the event.
1440        let mut t2 = crate::win32::ThreadState::new(2, 1);
1441        t2.parked_cpu = Some(crate::emulator::Cpu::new());
1442        t2.status = crate::sched::ThreadStatus::Waiting;
1443        t2.wait = Some(crate::sched::WaitCondition::Object {
1444            handle: h,
1445            timeout_after: None,
1446        });
1447        sb.host.threads.insert(2, t2);
1448
1449        // Bootstrap drives SetEvent via the kernel32 thunk.
1450        let set_event = sb.registry.resolve("kernel32.dll", "SetEvent").unwrap();
1451        sb.cpu.push32(&mut sb.mmu, h).unwrap();
1452        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1453        sb.cpu.regs.eip = set_event;
1454        sb.run_until_sentinel().unwrap();
1455
1456        // Thread 2 should be back to Ready with no wait.
1457        let t = sb.host.threads.get(&2).unwrap();
1458        assert!(
1459            matches!(t.status, crate::sched::ThreadStatus::Ready),
1460            "expected thread 2 Ready, got {:?}",
1461            t.status
1462        );
1463        assert!(t.wait.is_none(), "wait condition must be cleared");
1464        // Auto-reset: the signal is consumed by the wake.
1465        match sb.host.scheduler.objects.get(&h).unwrap() {
1466            crate::sched::WaitObject::Event { signaled, .. } => assert!(!*signaled),
1467            _ => panic!(),
1468        }
1469    }
1470
1471    /// Mutex round-trip: contention parks the second caller,
1472    /// `ReleaseMutex` from the holder wakes them and transfers
1473    /// ownership.
1474    #[test]
1475    fn mutex_transfers_ownership_on_release() {
1476        let mut sb = Sandbox::new();
1477        // Mint a Mutex owned by tid 1 (the bootstrap).
1478        let h = sb
1479            .host
1480            .scheduler
1481            .insert_object(crate::sched::WaitObject::Mutex {
1482                owner: Some(1),
1483                recursion: 1,
1484            });
1485        // Inject thread 2 waiting on the mutex.
1486        let mut t2 = crate::win32::ThreadState::new(2, 1);
1487        t2.parked_cpu = Some(crate::emulator::Cpu::new());
1488        t2.status = crate::sched::ThreadStatus::Waiting;
1489        t2.wait = Some(crate::sched::WaitCondition::Object {
1490            handle: h,
1491            timeout_after: None,
1492        });
1493        sb.host.threads.insert(2, t2);
1494
1495        // Bootstrap calls ReleaseMutex.
1496        let release = sb.registry.resolve("kernel32.dll", "ReleaseMutex").unwrap();
1497        sb.cpu.push32(&mut sb.mmu, h).unwrap();
1498        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1499        sb.cpu.regs.eip = release;
1500        sb.run_until_sentinel().unwrap();
1501        assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1, "release succeeds");
1502
1503        // Ownership transferred to thread 2; thread 2 Ready.
1504        match sb.host.scheduler.objects.get(&h).unwrap() {
1505            crate::sched::WaitObject::Mutex { owner, recursion } => {
1506                assert_eq!(*owner, Some(2));
1507                assert_eq!(*recursion, 1);
1508            }
1509            _ => panic!(),
1510        }
1511        let t = sb.host.threads.get(&2).unwrap();
1512        assert!(matches!(t.status, crate::sched::ThreadStatus::Ready));
1513    }
1514
1515    /// Semaphore round-trip: `ReleaseSemaphore(N)` wakes up to
1516    /// N waiters and bumps the count.
1517    #[test]
1518    fn semaphore_release_wakes_waiters_and_bumps_count() {
1519        let mut sb = Sandbox::new();
1520        let h = sb
1521            .host
1522            .scheduler
1523            .insert_object(crate::sched::WaitObject::Semaphore { count: 0, max: 10 });
1524        // Two waiters.
1525        for tid in [2u32, 3u32] {
1526            let mut t = crate::win32::ThreadState::new(tid, 1);
1527            t.parked_cpu = Some(crate::emulator::Cpu::new());
1528            t.status = crate::sched::ThreadStatus::Waiting;
1529            t.wait = Some(crate::sched::WaitCondition::Object {
1530                handle: h,
1531                timeout_after: None,
1532            });
1533            sb.host.threads.insert(tid, t);
1534        }
1535        // Bootstrap releases 2.
1536        let release = sb
1537            .registry
1538            .resolve("kernel32.dll", "ReleaseSemaphore")
1539            .unwrap();
1540        sb.mmu.map(0x500_0000, 0x1000, Perm::R | Perm::W);
1541        let p_prev = 0x500_0000u32;
1542        sb.cpu.push32(&mut sb.mmu, p_prev).unwrap();
1543        sb.cpu.push32(&mut sb.mmu, 2u32).unwrap();
1544        sb.cpu.push32(&mut sb.mmu, h).unwrap();
1545        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1546        sb.cpu.regs.eip = release;
1547        sb.run_until_sentinel().unwrap();
1548        assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1549        // Previous count was 0.
1550        assert_eq!(sb.mmu.load32(p_prev).unwrap(), 0);
1551        // Both waiters woke; each consumed one count → net count = 0.
1552        match sb.host.scheduler.objects.get(&h).unwrap() {
1553            crate::sched::WaitObject::Semaphore { count, .. } => {
1554                assert_eq!(*count, 0, "both waiters consumed their signals");
1555            }
1556            _ => panic!(),
1557        }
1558        for tid in [2u32, 3u32] {
1559            let t = sb.host.threads.get(&tid).unwrap();
1560            assert!(matches!(t.status, crate::sched::ThreadStatus::Ready));
1561        }
1562    }
1563
1564    /// Phase 5c: when the target EXE is staged in the
1565    /// VirtualFs, `CreateProcessA` actually loads it at a
1566    /// fresh image base and mints a Ready primary thread the
1567    /// scheduler can run alongside the parent. The PE used
1568    /// here is the synthetic-DLL fixture — it has a real PE
1569    /// header and the only import (`kernel32!ExitProcess`) is
1570    /// satisfied by our stub registry.
1571    #[test]
1572    fn create_process_a_loads_real_child_pe_from_vfs() {
1573        use crate::pe::test_image::build_minimal_dll;
1574        let mut sb = Sandbox::default();
1575        // Stage a child binary in the VFS.
1576        let dll_bytes = build_minimal_dll();
1577        let child_path = "c:\\setup\\helper.exe";
1578        let mut vfs = crate::context::VirtualFs::new();
1579        vfs.insert(child_path, dll_bytes);
1580        sb.host.context.vfs = Some(vfs);
1581
1582        // Stage a PROCESS_INFORMATION scratch region.
1583        sb.mmu.map(0x500_0000, 0x1000, Perm::R | Perm::W);
1584        let pi = 0x500_0000u32;
1585        // Stage the lpApplicationName string.
1586        sb.mmu.map(0x500_1000, 0x1000, Perm::R | Perm::W);
1587        let app_ptr = 0x500_1000u32;
1588        sb.mmu
1589            .write_initializer(app_ptr, child_path.as_bytes())
1590            .unwrap();
1591        sb.mmu.store8(app_ptr + child_path.len() as u32, 0).unwrap();
1592
1593        let create_proc = sb
1594            .registry
1595            .resolve("kernel32.dll", "CreateProcessA")
1596            .unwrap();
1597        for &a in [app_ptr, 0u32, 0, 0, 0, 0, 0, 0, 0, pi].iter().rev() {
1598            sb.cpu.push32(&mut sb.mmu, a).unwrap();
1599        }
1600        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1601        sb.cpu.regs.eip = create_proc;
1602        sb.run_until_sentinel().unwrap();
1603        assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1, "CreateProcessA TRUE");
1604
1605        // The new process should be in the table, distinct PID,
1606        // and its primary thread Ready (not Terminated like the
1607        // fake-child fallback).
1608        let child_pid = sb.mmu.load32(pi + 8).unwrap();
1609        let child_tid = sb.mmu.load32(pi + 12).unwrap();
1610        let p = sb
1611            .host
1612            .processes
1613            .get(&child_pid)
1614            .expect("child process present");
1615        assert_ne!(
1616            p.image_base, 0,
1617            "child PE was rebased into a fresh image slot"
1618        );
1619        assert_eq!(p.parent_pid, 1);
1620        let t = sb
1621            .host
1622            .threads
1623            .get(&child_tid)
1624            .expect("child primary thread present");
1625        assert!(matches!(
1626            t.status,
1627            crate::sched::ThreadStatus::Ready | crate::sched::ThreadStatus::Running
1628        ));
1629        // The minimal-DLL entry just rets, so a subsequent
1630        // run_until_sentinel will Halt it cleanly.
1631    }
1632
1633    /// Phase 3d: `EnterCriticalSection` takes ownership of an
1634    /// implicit Mutex keyed by `LPCRITICAL_SECTION`'s guest
1635    /// address; recursion increments on re-entry by the same
1636    /// thread; `LeaveCriticalSection` decrements and clears
1637    /// ownership when the count reaches zero. Exercised through
1638    /// IPC: `CreatePipe` mints a read/write handle pair;
1639    /// `WriteFile` on the write end stages bytes that
1640    /// `ReadFile` on the read end picks up. The two handles
1641    /// share an in-memory buffer through the scheduler's
1642    /// pipe table; closing the write end drains EOF on the
1643    /// reader.
1644    #[test]
1645    fn create_pipe_write_read_round_trip() {
1646        let mut sb = Sandbox::new();
1647        // Scratch region for handle outputs + data buffers.
1648        sb.mmu.map(0x500_0000, 0x1000, Perm::R | Perm::W);
1649        let p_read_h = 0x500_0000u32;
1650        let p_write_h = 0x500_0004u32;
1651        let p_written = 0x500_0010u32;
1652        let p_data_in = 0x500_0020u32; // bytes to write
1653        let p_data_out = 0x500_0040u32; // bytes to read into
1654        let p_n_read = 0x500_0080u32;
1655
1656        // CreatePipe(read, write, NULL, 0).
1657        let create_pipe = sb.registry.resolve("kernel32.dll", "CreatePipe").unwrap();
1658        for &a in [p_read_h, p_write_h, 0u32, 0u32].iter().rev() {
1659            sb.cpu.push32(&mut sb.mmu, a).unwrap();
1660        }
1661        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1662        sb.cpu.regs.eip = create_pipe;
1663        sb.run_until_sentinel().unwrap();
1664        assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1, "CreatePipe TRUE");
1665        let h_read = sb.mmu.load32(p_read_h).unwrap();
1666        let h_write = sb.mmu.load32(p_write_h).unwrap();
1667        assert!(h_read >= crate::sched::WAIT_OBJECT_HANDLE_BASE);
1668        assert_ne!(h_read, h_write, "distinct ends");
1669
1670        // Stage data in MMU + WriteFile(write_handle, data, 5, &written, NULL).
1671        sb.mmu.write_initializer(p_data_in, b"HELLO").unwrap();
1672        let write_file = sb.registry.resolve("kernel32.dll", "WriteFile").unwrap();
1673        for &a in [h_write, p_data_in, 5u32, p_written, 0u32].iter().rev() {
1674            sb.cpu.push32(&mut sb.mmu, a).unwrap();
1675        }
1676        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1677        sb.cpu.regs.eip = write_file;
1678        sb.run_until_sentinel().unwrap();
1679        assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1680        assert_eq!(sb.mmu.load32(p_written).unwrap(), 5);
1681
1682        // ReadFile(read_handle, out_buf, 5, &n_read, NULL).
1683        let read_file = sb.registry.resolve("kernel32.dll", "ReadFile").unwrap();
1684        for &a in [h_read, p_data_out, 5u32, p_n_read, 0u32].iter().rev() {
1685            sb.cpu.push32(&mut sb.mmu, a).unwrap();
1686        }
1687        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1688        sb.cpu.regs.eip = read_file;
1689        sb.run_until_sentinel().unwrap();
1690        assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1691        assert_eq!(sb.mmu.load32(p_n_read).unwrap(), 5);
1692        // Verify the bytes round-tripped.
1693        let mut got = [0u8; 5];
1694        for i in 0..5 {
1695            got[i] = sb.mmu.load8(p_data_out + i as u32).unwrap();
1696        }
1697        assert_eq!(&got, b"HELLO");
1698    }
1699
1700    /// Named-pipe handshake: a `CreateNamedPipeA(\\.\pipe\X)`
1701    /// server end + a `CreateFileA(\\.\pipe\X)` client end
1702    /// reach each other through the named-object registry
1703    /// and share a buffer for `WriteFile` / `ReadFile`.
1704    #[test]
1705    fn named_pipe_server_and_client_share_buffer() {
1706        let mut sb = Sandbox::new();
1707        sb.mmu.map(0x501_0000, 0x1000, Perm::R | Perm::W);
1708        let p_name = 0x501_0000u32;
1709        let pipe_path = b"\\\\.\\pipe\\test\0";
1710        sb.mmu.write_initializer(p_name, pipe_path).unwrap();
1711        let p_data_in = 0x501_0100u32;
1712        let p_data_out = 0x501_0200u32;
1713        let p_written = 0x501_0300u32;
1714        let p_n_read = 0x501_0310u32;
1715
1716        // CreateNamedPipeA(name, PIPE_ACCESS_OUTBOUND, ...). With
1717        // OUTBOUND, the server holds the write end.
1718        let cnp = sb
1719            .registry
1720            .resolve("kernel32.dll", "CreateNamedPipeA")
1721            .unwrap();
1722        for &a in [p_name, 2u32, 0u32, 1u32, 4096u32, 4096u32, 0u32, 0u32]
1723            .iter()
1724            .rev()
1725        {
1726            sb.cpu.push32(&mut sb.mmu, a).unwrap();
1727        }
1728        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1729        sb.cpu.regs.eip = cnp;
1730        sb.run_until_sentinel().unwrap();
1731        let h_server = sb.cpu.regs.get32(Reg32::Eax);
1732        assert!(h_server >= crate::sched::WAIT_OBJECT_HANDLE_BASE);
1733
1734        // Client: CreateFileA(\\.\pipe\test, GENERIC_READ, ...).
1735        let cfa = sb.registry.resolve("kernel32.dll", "CreateFileA").unwrap();
1736        for &a in [p_name, 0x8000_0000u32, 0u32, 0u32, 3u32, 0u32, 0u32]
1737            .iter()
1738            .rev()
1739        {
1740            sb.cpu.push32(&mut sb.mmu, a).unwrap();
1741        }
1742        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1743        sb.cpu.regs.eip = cfa;
1744        sb.run_until_sentinel().unwrap();
1745        let h_client = sb.cpu.regs.get32(Reg32::Eax);
1746        assert_ne!(h_client, 0xFFFF_FFFF, "client open should succeed");
1747        assert_ne!(h_client, h_server, "distinct handles");
1748
1749        // Server WriteFile.
1750        sb.mmu.write_initializer(p_data_in, b"PING!").unwrap();
1751        let wfile = sb.registry.resolve("kernel32.dll", "WriteFile").unwrap();
1752        for &a in [h_server, p_data_in, 5u32, p_written, 0u32].iter().rev() {
1753            sb.cpu.push32(&mut sb.mmu, a).unwrap();
1754        }
1755        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1756        sb.cpu.regs.eip = wfile;
1757        sb.run_until_sentinel().unwrap();
1758        assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1759        assert_eq!(sb.mmu.load32(p_written).unwrap(), 5);
1760
1761        // Client ReadFile.
1762        let rfile = sb.registry.resolve("kernel32.dll", "ReadFile").unwrap();
1763        for &a in [h_client, p_data_out, 5u32, p_n_read, 0u32].iter().rev() {
1764            sb.cpu.push32(&mut sb.mmu, a).unwrap();
1765        }
1766        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1767        sb.cpu.regs.eip = rfile;
1768        sb.run_until_sentinel().unwrap();
1769        assert_eq!(sb.cpu.regs.get32(Reg32::Eax), 1);
1770        assert_eq!(sb.mmu.load32(p_n_read).unwrap(), 5);
1771        let mut got = [0u8; 5];
1772        for i in 0..5 {
1773            got[i] = sb.mmu.load8(p_data_out + i as u32).unwrap();
1774        }
1775        assert_eq!(&got, b"PING!");
1776    }
1777
1778    /// the kernel32 thunk dispatch.
1779    #[test]
1780    fn enter_leave_critical_section_round_trip() {
1781        let mut sb = Sandbox::new();
1782        // Map a CRITICAL_SECTION scratch region.
1783        sb.mmu.map(0x400_0000, 0x1000, Perm::R | Perm::W);
1784        let cs = 0x400_0000u32;
1785        let enter = sb
1786            .registry
1787            .resolve("kernel32.dll", "EnterCriticalSection")
1788            .unwrap();
1789        let leave = sb
1790            .registry
1791            .resolve("kernel32.dll", "LeaveCriticalSection")
1792            .unwrap();
1793        // First enter: take ownership, recursion=1.
1794        sb.cpu.push32(&mut sb.mmu, cs).unwrap();
1795        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1796        sb.cpu.regs.eip = enter;
1797        sb.run_until_sentinel().unwrap();
1798        let h = sb.host.scheduler.critical_sections[&cs];
1799        match sb.host.scheduler.objects.get(&h).unwrap() {
1800            crate::sched::WaitObject::CriticalSection {
1801                owner, recursion, ..
1802            } => {
1803                assert_eq!(*owner, Some(1));
1804                assert_eq!(*recursion, 1);
1805            }
1806            _ => panic!(),
1807        }
1808        // Re-entry by same thread: recursion=2.
1809        sb.cpu.push32(&mut sb.mmu, cs).unwrap();
1810        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1811        sb.cpu.regs.eip = enter;
1812        sb.run_until_sentinel().unwrap();
1813        match sb.host.scheduler.objects.get(&h).unwrap() {
1814            crate::sched::WaitObject::CriticalSection { recursion, .. } => {
1815                assert_eq!(*recursion, 2);
1816            }
1817            _ => panic!(),
1818        }
1819        // First leave: recursion→1.
1820        sb.cpu.push32(&mut sb.mmu, cs).unwrap();
1821        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1822        sb.cpu.regs.eip = leave;
1823        sb.run_until_sentinel().unwrap();
1824        // Second leave: recursion→0, owner cleared.
1825        sb.cpu.push32(&mut sb.mmu, cs).unwrap();
1826        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1827        sb.cpu.regs.eip = leave;
1828        sb.run_until_sentinel().unwrap();
1829        match sb.host.scheduler.objects.get(&h).unwrap() {
1830            crate::sched::WaitObject::CriticalSection {
1831                owner, recursion, ..
1832            } => {
1833                assert_eq!(*owner, None);
1834                assert_eq!(*recursion, 0);
1835            }
1836            _ => panic!(),
1837        }
1838    }
1839
1840    /// Phase 3b of the scheduler refactor: `CreateThread` mints
1841    /// a Ready thread, the scheduler context-switches into it
1842    /// the first time the bootstrap thread yields, and the new
1843    /// thread runs to its own RET_SENTINEL where it
1844    /// terminates. Verifies both halves of the round trip.
1845    #[test]
1846    fn create_thread_spawns_a_real_runnable_thread() {
1847        let mut sb = Sandbox::new();
1848        // Map a code page for the synthetic thread proc.
1849        sb.mmu.map(0x1000, 0x1000, Perm::R | Perm::X);
1850        // Thread proc: `mov eax, 0x42; ret 4` (stdcall, one arg
1851        // consumed; eax = exit code).
1852        let proc_va = 0x1010u32;
1853        let proc_code: [u8; 8] = [
1854            0xb8, 0x42, 0x00, 0x00, 0x00, // mov eax, 0x42
1855            0xc2, 0x04, 0x00, // ret 4
1856        ];
1857        sb.mmu.write_initializer(proc_va, &proc_code).unwrap();
1858        // Drive CreateThread via the kernel32 thunk so we exercise
1859        // the same code path a real codec would.
1860        let create_thread = sb.registry.resolve("kernel32.dll", "CreateThread").unwrap();
1861        // Push args right-to-left: (attrs, stack, start, param,
1862        // flags, p_tid) = (0, 0, proc_va, 0xCAFE, 0, 0).
1863        for &a in [0u32, 0, proc_va, 0xCAFE, 0, 0].iter().rev() {
1864            sb.cpu.push32(&mut sb.mmu, a).unwrap();
1865        }
1866        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1867        sb.cpu.regs.eip = create_thread;
1868        sb.run_until_sentinel().unwrap();
1869        let thread_handle = sb.cpu.regs.get32(Reg32::Eax);
1870        assert!(thread_handle >= crate::sched::WAIT_OBJECT_HANDLE_BASE);
1871        // The new thread is in the table, Ready, with its CPU
1872        // parked. TID 2 (bootstrap is 1).
1873        let t = sb.host.threads.get(&2).expect("new thread present");
1874        assert!(matches!(t.status, crate::sched::ThreadStatus::Ready));
1875        assert!(t.parked_cpu.is_some());
1876        assert_ne!(
1877            t.parked_cpu.as_ref().unwrap().regs.esp(),
1878            0,
1879            "stack was carved from the pool"
1880        );
1881
1882        // Bootstrap now Sleeps — the scheduler must context
1883        // switch into the new thread, run it to its
1884        // RET_SENTINEL, mark it Terminated, and then come
1885        // back to the bootstrap (which wakes after the sleep
1886        // clock advances).
1887        let sleep_thunk = sb.registry.resolve("kernel32.dll", "Sleep").unwrap();
1888        // Sleep(1ms)
1889        sb.cpu.push32(&mut sb.mmu, 1u32).unwrap();
1890        sb.cpu.push32(&mut sb.mmu, RET_SENTINEL).unwrap();
1891        sb.cpu.regs.eip = sleep_thunk;
1892        sb.run_until_sentinel().unwrap();
1893        // After the run, the new thread should have ran to
1894        // completion (status Terminated, eax = 0x42 on its
1895        // parked CPU if any).
1896        let t = sb.host.threads.get(&2).expect("new thread still present");
1897        assert!(
1898            matches!(t.status, crate::sched::ThreadStatus::Terminated),
1899            "expected thread 2 Terminated, got {:?}",
1900            t.status
1901        );
1902    }
1903}