DDGameWrapper had an off-by-16 size assertion (0x6F00 vs actual allocation
0x6F10); the 0x20-byte tail is unzeroed unknown padding. Fixed struct field
and size assertion.
WABox never had a Drop implementation so it was purely a leak-by-design
wrapper with no safety value. Remove it entirely and replace all uses:
- WABox::alloc(...).leak() → wa_malloc_struct_zeroed::<T>()
- WABox::from_value(val).leak() → wa_malloc_struct::<T>() + ptr::write(p, val)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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: rename mis-labeled DisplayGfx slot 33 to GetSpriteFrameForBlit
Slot 33 (0x5237C0) was historically called LoadSpriteComplex everywhere —
in the vtable definition, the address registry, the inner-helper names
(SPRITE_COMPLEX_SPRITE_PATH / SPRITE_COMPLEX_BANK_PATH), the doc strings
on SpriteBank/DisplayBase, and the "NOT ported because complex usercall"
rationale. It is not a load function. It's the per-blit frame lookup that
BlitSprite (slot 19) calls on every sprite render: clamps an animation
value, finds the matching frame entry in the sprite's frame table,
lazily decompresses the frame surface, and returns *DisplayBitGrid +
bounding box.
Cascade renames:
vtable slot load_sprite_complex → get_sprite_frame_for_blit
Ghidra fn LoadSpriteComplex → DisplayGfx__GetSpriteFrameForBlit
Ghidra fn FUN_004FAD30 (alias → Sprite__GetFrameForBlit
SPRITE_COMPLEX_SPRITE_PATH)
Ghidra fn FUN_004F9710 (alias → SpriteBank__GetFrameForBlit
SPRITE_COMPLEX_BANK_PATH)
Replace render.rs's …
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>