Port the entire .dir archive I/O layer — GfxDir vtable (read, seek,
load_cached, release), GfxDirStream vtable (all 6 slots), LoadImage,
cache slot management, and cleanup — eliminating all WA.exe asm bridges.
- Move gfx_dir module from render/sprite/ to new asset/ module
- Type cache slots as GfxCacheSlot struct, rename _unknown fields
- Convert GfxDirStreamVtable to #[vtable] macro with bind_! wrappers
- Replace call_gfx_load_dir/call_gfx_load_image bridges with direct calls
- Add WA_FSEEK address, GFX_DIR_STREAM_VTABLE address
- Fix GfxDirStream.gfx_dir field type from *mut u8 to *mut GfxDir
- Rename GfxDirEntry._unknown_08 to data_size
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: introduce SpriteOp/SpriteFlags, rename SpriteId to KnownSpriteId
Replace raw u32 sprite_flags with typed SpriteOp (packed sprite index +
transform flags) and SpriteFlags (bitflags for the 13 known blit flags).
Follows the same pattern as SoundId/KnownSoundId in the audio system.
- SpriteId enum renamed to KnownSpriteId (non-exhaustive catalog)
- SpriteOp: #[repr(transparent)] newtype with index()/flags() accessors
- SpriteFlags: bitflags! type replacing raw 0x0080_0000-style masks
- Updated through the full pipeline: DrawSpriteCmd, RenderMessage,
vtable signature, dispatcher, and blit_sprite implementation
- Added bitflags 2 as direct dep (already transitive via windows crate)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: type set_active_layer return + propagate PaletteContext
Cascades a typed `*mut PaletteContext` return through the chain that
starts at `DisplayGfx::set_active_layer` (slot 5) and feeds the GfxDir
loaders.
- Slot 5 vtable signature retyped from `*mut u8` to
`*mut PaletteContext`. Doc-comment promoted from one line to
explain what the result is used for downstream.
- `gfx_resource_create` (`FUN_004F6300` port), `call_gfx_find_and_load`,
and `call_gfx_load_and_wrap` now take `display_ctx: *mut PaletteContext`
instead of `*mut u8`. The single internal `IMG_Decode` call still
casts back to `*mut u8` since `IMG_Decode` is WA-side.
- `compute_complex_gradient` (`render/display/gradient.rs`) takes
`layer3_ctx: *mut PaletteContext` and passes it through to
`call_gfx_find_and_load` without any casts.
- `wa_load_sprites` naked-asm bridge's `_layer_ctx` parameter retyped
to `*mut PaletteContext` to match what its only caller passes after
`set_active_layer`. The naked asm body is unchanged — pointer types
…
refactor: propagate type info through display vtable.rs and friends
A series of small type-safety improvements that compound to delete a
lot of raw byte arithmetic and ad-hoc transmutes:
- `LayerSpriteFrame.bitmap_vtable` retyped from `u32` to
`*const c_void`, and `LayerSpriteFrame.surface` from `u32` to
`*mut Surface`. The two existing consumers (the slot 0 destructor's
`layer_sprite_frame_destructor` and the `load_sprite_by_name` /
`free_layer_sprite` pair) now access these via named fields
instead of raw `elem.add(0x08)` / `elem.add(0x0C)` arithmetic.
- `SurfaceVtable::set_color_key` (slot 7) added as a typed binding,
matching the existing `lock_surface` / `unlock_surface` /
`init_surface` / `release` slots. `load_sprite_by_name`'s per-frame
surface init loop now uses the typed binding for slots 3, 4, 5, 6,
7 instead of hand-rolled raw transmutes — drops ~50 lines.
- `RenderContextVtable::lock_surface_write`'s `out_data` retyped
from `*mut u32` to `*mut *mut u8`, and `unlock_surface_write`'s
`p2: …
refactor: fix DDGameWrapper size to 0x6F10 and remove WABox
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: replace inline thiscall transmutes with typed GfxDirVtable pattern
Add GfxDirVtable via #[vtable] macro (4 slots: read/seek/load_cached/release)
and GfxDirStream::total_size_raw (slot 4, formerly _slot_4). Replace all
inline transmute vtable calls across gfx_dir.rs, ddgame_constructor.rs, and
font.rs with typed _raw methods generated by bind_GfxDirVtable!. Change
call_gfx_load_image return type to *mut GfxDirStream. Rename GfxHandler and
GfxDirStream vtable functions in Ghidra accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor: replace untested SpriteBank slot-33 port with panic, trap leaves
Two related changes addressing the SpriteBank-side concern raised after
the slot 33 port:
1. **Replace `sprite_bank_get_frame_for_blit` body with a panic.** The
port shipped in 973f234 was disassembly-derived but never executed
in any test (a `panic!()` planted there during headful play was
never triggered). Per the project rule against keeping unverified
code, the body has been removed; the function is now a stub that
panics with a clear "should never be reached" message pointing
back at 973f234 for the unverified Ghidra translation if banks
ever become live.
Why we believe the path is structurally dead in shipping WA:
`SpriteBank` objects are constructed only by `SpriteBank__Constructor`
(0x4F9450, verified by xrefs to vtable 0x664180), whose only caller
is `DisplayGfx::LoadSpriteEx` (slot 30, 0x523310), which has zero
callers in WA.exe and zero in Rust and is itself trapped via
`install_trap!("DisplayGfx__LoadSpriteEx", ...)`. Therefore
`D…
feat: port DisplayGfx::GetSpriteFrameForBlit (slot 33) to pure Rust
This is the LAST in-game render bridge. With this slot ported, the
entire render pipeline of a normal Worms match runs on Rust code with
zero bridges into WA's render functions; only the renderer backend
itself (CompatRenderer / DDraw / OpenGL) and the shutdown destructor
(slot 0) remain.
Three function ports + two new leaf modules:
- `render::sprite::lzss::sprite_lzss_decode` — port of
`Sprite_LZSS_Decode` (0x5B29E0). LZSS-style decompressor with
palette LUT remapping. Key detail verified from the disassembly:
short backrefs encode `real_distance - 1` (the asm's `DEC ECX`
between the JZ and the read), but long backrefs encode the real
distance directly (the JZ jumps OVER the DEC). 6 unit tests cover
literal runs, both backref forms, length boundaries, the
terminator, and self-overlap RLE-style cases.
- `render::sprite::frame_cache::frame_cache_allocate` — port of
`FrameCache__Allocate` (0x4FA950). LRU ring-buffer allocator with
eviction notification via `vtable[1]…
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_…
feat: port slot 19 bitmap-sprite path to pure Rust
The slot-19 (BlitSprite) bitmap-sprite branch was the last in-game
caller of three usercall bridges to WA: GetBitmapSpriteInfo (0x573C50),
BlitBitmapClipped (0x56A700), and BlitBitmapTiled (0x56A7D0). After
this change, the only remaining in-game render bridge is slot 33
(GetSpriteFrameForBlit).
Type cleanups (lessons 14, 18: small typing wins are always welcome):
- LayerSprite renames `field_64`/`field_68`/`field_6a` to `flags`,
`cell_width: i16`, and `cell_height: i16`. Field semantics verified
against the GetBitmapSpriteInfo disassembly's reads of [EAX+0x64..0x6A].
- DisplayGfx::sprite_table retyped from `[u32; 0x400]` to
`[*mut LayerSprite; 0x400]`. Each non-null entry is the bitmap-sprite
object allocated by `load_sprite_by_layer` (slot 37).
- LayerSpriteFrame::bitmap_ptr() helper returns the trailing 12-byte
CBitmap block as `*mut CBitmap`. Lets us avoid duplicating the
vtable+surface+pad triple as a nested struct while keeping the
call-site type-safe.
- blit_bitm…
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…
feat: port DisplayGfx::DrawTiledBitmap (slot 11) to pure Rust
Last hot-path slot in the in-game render pipeline. The entire active
rendering pipeline of a normal Worms match now runs on Rust code; only
GetSpriteFrameForBlit (slot 33, called from blit_sprite) and the WA
wrapper backend (DDraw/D3D/OpenGL) remain as bridges.
Three phases per the existing slot docstring: lazy tile-cache
allocation, lazy populate, per-frame clipped tile blits. Bundles in
the leaf primitives blit_64byte_row_pattern (port of FUN_005B2A5E)
and blit_color_table_forward (port of BlitColorTable_Forward), plus
blit_bitmap_clipped_native (port of DisplayGfx::BlitBitmapClipped at
0x56A700) and cbitmap_blit_via_wrapper (port of FUN_00403c60).
Adds a 12-byte CBitmap struct (vtable 0x643F64) to sprite.rs, distinct
from but isomorphic to the trailing 12 bytes of LayerSpriteFrame, and
the SurfaceVtable / Surface stub in context.rs covering the lock /
unlock / init slots used by the populate phase. Renames
DisplayGfx._unknown_358c -> tile_cache_populated and retypes the
bitmap_vec…
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 …
feat: port LoadSpriteByLayer (slot 37) and LoadSpriteByName to pure Rust
Port the layer sprite loading pipeline to Rust, eliminating all usercall
bridges. This enables load_sprite_by_layer (vtable slot 37, 0x56A4C0) to
be fully hooked via vtable_replace.
Key functions ported:
- LoadSpriteByName (FUN_005733b0): reads .spr header, palette, frame pixel
data from GfxDir stream; creates software surfaces via RenderContext
- FreeLayerSprite (FUN_0056a2f0): destroys surfaces and frees the sprite
- RemapPixelsThroughLut (FUN_005b2beb): palette index remapping on pixel buffers
New types:
- LayerSprite (0x70 bytes): background/water layer sprite object
- LayerSpriteFrame (0x14 bytes): per-frame surface with bounding box
- GfxDirStream / GfxDirStreamVtable: typed wrapper for .dir archive reader
Notable findings during porting:
- Ghidra's unaff_ESI/unaff_EBP were decompiler artifacts, not caller-frame
reads — both registers are set within the function
- RenderContext::alloc_surface returns the pointer in EAX, not via the
FastcallResult buffer
- .spr palette en…
feat: port DisplayGfx::LoadSprite (slot 31) and improve Sprite struct
- Port load_sprite vtable method: Rust validation/bookkeeping with
ConstructSprite in pure Rust, delegating VFS loading to WA bridge
- Embed DisplayBitGrid as a proper struct field in Sprite instead of
raw _unknown_38 bytes, with offset assertions
- Type Sprite::context_ptr as *mut SpriteCache instead of *mut u8
- Add construct_sprite() in openwa-core (pure Rust port of 0x4FAA30)
- Update ConstructSprite hook in DLL to use typed bitgrid fields
- Re-export SpriteVtable from sprite module
- Add address constants for sprite loading helper functions
DisplayGfx now at 27/34 non-stub vtable slots ported.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>