paavohtl commited on
Commit
f9aa4a2
Β·
1 Parent(s): e7c9ec5

feat: define FramePostProcessHook abstract class + ScreenshotHook vtable

Browse files

Adds the typed Rust definitions for the per-frame post-processing hook
interface used by `DisplayGfx::DispatchFramePostProcessHooks` (0x56CDB0)
and the slot 0 destructor's hook-vec teardown step:

- `FramePostProcessHook` β€” 4-byte abstract polymorphic class (just a
vtable pointer). Concrete subclasses store their state in globals.
- `FramePostProcessHookVtable` (0x66A2B8, 3 slots): scalar deleting
destructor + `get_capture_request` (returns non-zero if the hook
wants to run this frame) + `capture` (does the per-frame work,
receives `layer_0` as a stack arg).
- `ScreenshotHook` (`type` alias) and `SCREENSHOT_HOOK_VTABLE`
(0x66A2C4) β€” the only concrete subclass shipped in WA, writes the
rendered surface as a numbered PNG.

Calling conventions verified directly from
`DispatchFramePostProcessHooks` disasm:
- vtable[1] is pure thiscall (no stack args, returns i32 in EAX),
dispatched at 0x56CE01-0x56CE08.
- vtable[2] is thiscall + 1 stack arg (`layer_0: *mut DisplayBitGrid`
loaded from `DisplayGfx + 0x3D9C`), dispatched at 0x56CE98-0x56CEA8.

Adds 0xF6 (`TEST byte ptr [mem], imm8`) to the startup-check valid
prologue list β€” `FramePostProcessHook__Destructor` (0x569BF0) starts
with `TEST [ESP+4], 1` instead of a frame setup, which the prologue
sanity check did not previously recognize.

Pre-work for porting DisplayGfx::Destructor (slot 0).

crates/openwa-core/src/address.rs CHANGED
@@ -82,6 +82,11 @@ pub mod va {
82
  };
83
  pub use crate::render::display::base::DISPLAY_BASE_VTABLE;
84
  pub use crate::render::display::context::{RenderContextVtable, RENDER_CONTEXT_VTABLE};
 
 
 
 
 
85
  pub use crate::render::display::gfx::{
86
  DISPLAY_BASE_DESTRUCTOR_IMPL, DISPLAY_GFX_DESTRUCTOR, DISPLAY_GFX_DESTRUCTOR_IMPL,
87
  DISPLAY_GFX_DISPATCH_FRAME_POST_PROCESS_HOOKS, DISPLAY_GFX_FREE_LAYER_SPRITE_TABLE,
 
82
  };
83
  pub use crate::render::display::base::DISPLAY_BASE_VTABLE;
84
  pub use crate::render::display::context::{RenderContextVtable, RENDER_CONTEXT_VTABLE};
85
+ pub use crate::render::display::frame_hook::{
86
+ FramePostProcessHookVtable, FRAME_POST_PROCESS_HOOK_DESTRUCTOR,
87
+ FRAME_POST_PROCESS_HOOK_VTABLE, SCREENSHOT_HOOK_CAPTURE_TO_PNG,
88
+ SCREENSHOT_HOOK_GET_CAPTURE_REQUEST, SCREENSHOT_HOOK_VTABLE,
89
+ };
90
  pub use crate::render::display::gfx::{
91
  DISPLAY_BASE_DESTRUCTOR_IMPL, DISPLAY_GFX_DESTRUCTOR, DISPLAY_GFX_DESTRUCTOR_IMPL,
92
  DISPLAY_GFX_DISPATCH_FRAME_POST_PROCESS_HOOKS, DISPLAY_GFX_FREE_LAYER_SPRITE_TABLE,
crates/openwa-core/src/render/display/frame_hook.rs ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! `FramePostProcessHook` β€” abstract per-frame post-processing hook.
2
+ //!
3
+ //! 4-byte polymorphic class (just a vtable pointer). Concrete subclasses
4
+ //! override slots 1 and 2 of [`FramePostProcessHookVtable`] to implement
5
+ //! per-frame work that runs after `DisplayGfx` finishes drawing the frame.
6
+ //! Subclasses store their state in globals or are themselves singletons β€”
7
+ //! the textbook "4-byte hook interface" pattern (lesson #34 in the display
8
+ //! porting notes).
9
+ //!
10
+ //! The only concrete subclass shipped in WA is `ScreenshotHook` (vtable
11
+ //! 0x66A2C4) which writes the rendered `layer_0` `DisplayBitGrid` as a
12
+ //! numbered PNG when `g_DDGame->screenshot_pending` is set.
13
+ //!
14
+ //! ## Lifetime
15
+ //!
16
+ //! Lives in a `std::vector<FramePostProcessHook*>` at `DisplayGfx + 0x24DF8`
17
+ //! (see `gfx.rs::hook_vec_proxy/first/last/end`). Inserted during
18
+ //! `DisplayGfx::Init`, dispatched once per frame from `RenderFrame_Maybe`
19
+ //! β†’ `DispatchFramePostProcessHooks` (0x56CDB0), and freed by
20
+ //! `DisplayGfx::DestructorImpl` (slot 0) which iterates the vec and calls
21
+ //! `vtable[0](1)` on every entry.
22
+
23
+ use crate::bitgrid::DisplayBitGrid;
24
+
25
+ // Note: `FRAME_POST_PROCESS_HOOK_VTABLE` is emitted by the
26
+ // `#[vtable(va = ...)]` attribute macro on `FramePostProcessHookVtable`
27
+ // below; it does not need a separate `define_addresses!` entry.
28
+ crate::define_addresses! {
29
+ class "FramePostProcessHook" {
30
+ /// `FramePostProcessHook::Destructor` (0x569BF0) β€” trivial:
31
+ /// rebinds vtable to base + frees if `flags & 1`.
32
+ fn/Thiscall FRAME_POST_PROCESS_HOOK_DESTRUCTOR = 0x0056_9BF0;
33
+ }
34
+
35
+ class "ScreenshotHook" {
36
+ /// Concrete `ScreenshotHook` vtable (0x66A2C4) β€” destructor +
37
+ /// `GetCaptureRequest` + `CaptureToPng`. Has the same shape as
38
+ /// `FramePostProcessHookVtable` (no Rust struct of its own,
39
+ /// since `ScreenshotHook` adds no instance fields).
40
+ vtable SCREENSHOT_HOOK_VTABLE = 0x0066_A2C4;
41
+ /// `ScreenshotHook::GetCaptureRequest` (0x56D170) β€” vtable[1].
42
+ /// Returns 2 if `g_DDGame->screenshot_pending` is set, else 0.
43
+ fn/Thiscall SCREENSHOT_HOOK_GET_CAPTURE_REQUEST = 0x0056_D170;
44
+ /// `ScreenshotHook::CaptureToPng` (0x56D180) β€” vtable[2].
45
+ /// Formats `"%s%06d.png"` and writes the rendered layer_0 surface
46
+ /// to disk via `FUN_0056C6F0`.
47
+ fn/Thiscall SCREENSHOT_HOOK_CAPTURE_TO_PNG = 0x0056_D180;
48
+ }
49
+ }
50
+
51
+ /// `FramePostProcessHook` β€” abstract polymorphic class (just a vtable ptr).
52
+ ///
53
+ /// All instance state lives in the concrete subclass β€” for `ScreenshotHook`
54
+ /// the "state" is `g_DDGame->screenshot_pending` and the running PNG counter
55
+ /// in globals, not fields on the object itself.
56
+ #[repr(C)]
57
+ pub struct FramePostProcessHook {
58
+ pub vtable: *const FramePostProcessHookVtable,
59
+ }
60
+
61
+ const _: () = assert!(core::mem::size_of::<FramePostProcessHook>() == 4);
62
+
63
+ /// Vtable for `FramePostProcessHook` (3 slots, 0x66A2B8).
64
+ ///
65
+ /// Verified call conventions from the dispatcher
66
+ /// `DisplayGfx::DispatchFramePostProcessHooks` (0x56CDB0):
67
+ ///
68
+ /// - `vtable[1]` (`get_capture_request`) at 0x56CE01-0x56CE08: pure
69
+ /// thiscall, no stack args, returns `i32` in EAX.
70
+ /// - `vtable[2]` (`capture`) at 0x56CE98-0x56CEA8: thiscall, one stack
71
+ /// arg `layer_0: *mut DisplayBitGrid` (loaded from
72
+ /// `DisplayGfx + 0x3D9C`).
73
+ /// - `vtable[0]` (destructor) is called from `DisplayGfx::DestructorImpl`
74
+ /// at 0x56A1BF-0x56A1C5 with `flags = 1`.
75
+ ///
76
+ /// Slots 1 and 2 are `_purecall` (pure virtual) on the abstract base vtable;
77
+ /// only the concrete `ScreenshotHook` vtable provides real implementations.
78
+ #[openwa_core::vtable(size = 3, va = 0x0066_A2B8, class = "FramePostProcessHook")]
79
+ pub struct FramePostProcessHookVtable {
80
+ /// scalar deleting destructor (0x569BF0 on the base vtable).
81
+ /// Calls return value is `*mut FramePostProcessHook` per the MSVC ABI.
82
+ #[slot(0)]
83
+ pub destructor: fn(this: *mut FramePostProcessHook, flags: u32) -> *mut FramePostProcessHook,
84
+ /// `get_capture_request` β€” returns non-zero if this hook wants to run
85
+ /// this frame. ScreenshotHook returns 2 when a screenshot is pending,
86
+ /// 0 otherwise. The dispatcher tracks the max return value across all
87
+ /// hooks and only invokes `capture` if any hook returned non-zero.
88
+ #[slot(1)]
89
+ pub get_capture_request: fn(this: *mut FramePostProcessHook) -> i32,
90
+ /// `capture` β€” runs the hook's per-frame work, given the rendered
91
+ /// `layer_0` `DisplayBitGrid` from `DisplayGfx + 0x3D9C`.
92
+ #[slot(2)]
93
+ pub capture: fn(this: *mut FramePostProcessHook, layer_0: *mut DisplayBitGrid),
94
+ }
95
+
96
+ bind_FramePostProcessHookVtable!(FramePostProcessHook, vtable);
97
+
98
+ /// `ScreenshotHook` β€” the only concrete subclass shipped in WA.
99
+ ///
100
+ /// Layout matches `FramePostProcessHook` exactly (no instance fields). The
101
+ /// vtable at 0x66A2C4 differs only in slots 1 and 2: `GetCaptureRequest`
102
+ /// reads `g_DDGame->screenshot_pending`, and `CaptureToPng` writes the
103
+ /// rendered surface to a numbered `.png` file.
104
+ pub type ScreenshotHook = FramePostProcessHook;
crates/openwa-core/src/render/display/mod.rs CHANGED
@@ -1,6 +1,7 @@
1
  pub mod base;
2
  pub mod context;
3
  pub mod font;
 
4
  pub mod gfx;
5
  pub mod gradient;
6
  pub mod layer;
 
1
  pub mod base;
2
  pub mod context;
3
  pub mod font;
4
+ pub mod frame_hook;
5
  pub mod gfx;
6
  pub mod gradient;
7
  pub mod layer;
crates/openwa-dll/src/startup_checks.rs CHANGED
@@ -39,6 +39,7 @@ const VALID_PROLOGUES: &[u8] = &[
39
  0xE8, // call rel32
40
  0xE9, // jmp rel32 (MinHook trampoline β€” function is hooked)
41
  0xEB, // jmp rel8 (short jump)
 
42
  0xFF, // call/jmp [mem]
43
  ];
44
 
 
39
  0xE8, // call rel32
40
  0xE9, // jmp rel32 (MinHook trampoline β€” function is hooked)
41
  0xEB, // jmp rel8 (short jump)
42
+ 0xF6, // test byte ptr [mem], imm8 (e.g. FramePostProcessHook__Destructor's flag check)
43
  0xFF, // call/jmp [mem]
44
  ];
45