Replaces vtable[0] of DisplayGfx (0x66A218) with a native Rust impl
that mirrors the original DestructorImpl (0x56A010) one-to-one.
The cleanup body lives in the new `display::destructor` module and
ports both helpers along with it:
- `free_layer_sprite_table` — Rust port of 0x56A280: iterates
`sprite_table[1..0x3FF]`, runs `LayerSpriteFrame::Destructor` on
every entry of each sprite's count-prefixed `frame_array`, then
frees the frame array and the LayerSprite itself.
- `layer_sprite_frame_destructor` — Rust port of 0x5732E0: rebinds
the embedded CBitmap vtable and dispatches the surface's
scalar deleting destructor (`vtable[0](1)`) via raw thiscall.
- `tile_bitmap_set_destructor` — Rust port of 0x569BC0 (originally
a usercall on EDI): iterates `bitmap_ptrs[0..count]`, calls
`vtable[0](1)` on each non-null `CBitmap*`, frees `bitmap_ptrs`.
- `free_cbitmap_vec` — the inline CBitmap vec teardown loop:
releases each non-null `CBitmap.surface` via …
feat: define FramePostProcessHook abstract class + ScreenshotHook vtable
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: …
refactor: type FrameCache + Sprite/SpriteBank subframe-cache layouts
Pre-work for porting slot 33 (get_sprite_frame_for_blit) to pure Rust.
All changes are type-only — no logic differences. Verified by all 26
unit tests + 15 headless replay tests passing.
- Rename `SpriteBufferCtrl` -> `FrameCache` with the LRU bookkeeping
fields typed (write_head/wrap_marker/tail_entry/head_entry/entry_count).
Add `FrameCacheEntry` (16-byte header). Note: `frame_idx@+8`,
`owner@+0xc` — the prep-session docstring blueprint had these swapped;
verified from the 0x4FA950 epilog and the eviction call's `MOV ECX,
[EAX+4]` (which loads the owner from `entry+0xc` for the thiscall).
- Rename `SpriteCache::buffer_ctrl` -> `frame_cache: *mut FrameCache`.
- Define `SpriteSubframeCache` (Sprite-side, decoded_ptr@+4) and
`SpriteBankSubframeCache` (bank-side, decoded_ptr@+8) as separate
structs with asserted offsets — sharing one type would corrupt one
of the two paths.
- Define `SpriteBankBboxEntry` (subframe_idx + decoded_offset + bbox).
- `Sprite::secondary_frame_…
refactor: introduce Layer newtype, type layer_contexts and palette_ctx
Adds `Layer(NonZeroU8)` covering the 1..=3 display-layer index used by
`set_layer_color`, `set_active_layer`, `set_layer_visibility`,
`load_sprite`, `load_font`, etc. Vtable signatures still take i32/u32
to match WA's binary contract; conversion happens at function entry,
which both deletes the open-coded `(layer as u32).wrapping_sub(1) >= 3`
checks and statically guarantees array indices are in range.
Bundled with the matching pointer-typing pass:
- `DisplayBase::layer_contexts: [u32; 4]` -> `[*mut PaletteContext; 4]`
- `LayerSprite::palette_ctx: u32` -> `*mut PaletteContext`
- `wa_load_sprite_from_vfs` and the `load_sprite_from_vfs` callback
param retyped from `u32` to `*mut PaletteContext`
Both types are 4 bytes on i686-pc-windows-msvc, so the struct layouts
and bridge calling conventions are unchanged. Replaces every
`if layer_ctx == 0` with `if layer_ctx.is_null()` and deletes the dead
`is_valid_layer` helper.
Verification: cargo test (4 new Layer tests pass), 15/15 headless…
refactor: extract font module, type GfxDir pointers, rename DisplayVtable
Three concurrent cleanups bundled together:
- Extract the font implementation (~830 lines) from display/vtable.rs
into a new display/font.rs module: Font struct (renamed from
FontObject), font_extend, font_set_palette_impl, font_get_info_impl,
font_get_metric_impl, font_set_param_impl, font_load_from_gfx, and
GlyphEntry. vtable.rs is now 870 lines lighter and only contains
vtable definitions and slot wrappers.
- Replace `*mut u8` with typed `*mut GfxDir` across the GfxDir call
graph: LayerSprite.gfx_dir, PCLandscape.{level,water}_gfx_dir,
DDGameWrapper.{primary,secondary}_gfx_dir, slot 31 load_sprite,
slot 34 load_font, slot 37 load_sprite_by_layer,
gfx_dir_find_entry, gfx_resource_create, call_gfx_find_and_load,
call_gfx_load_and_wrap, call_gfx_load_image, plus the matching DLL
trampolines and the inner transmuted vtable[2] cached-load function
pointer in three places. Also renames the misleading local names
`land_layer`/`water_layer` to `land_dir`/`water_d…
refactor: reorganize display/ and render/ into unified render/ module
Merge the display/ and render/ sibling modules into a single render/
module with display/, sprite/, and ddraw/ sub-modules. Pure mechanical
refactor — file moves and import rewrites only, zero logic changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>