feat: define FramePostProcessHook abstract class + ScreenshotHook vtable
Browse filesAdds 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).
|
@@ -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,
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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 |
|