paavohtl Claude Opus 4.6 (1M context) commited on
Commit
fb071dd
Β·
1 Parent(s): 802630e

refactor: replace untested SpriteBank slot-33 port with panic, trap leaves

Browse files

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
`DisplayBase::sprite_banks[id]` is always null and the bank branch
of slot 33 is unreachable. Confirmed by the panic-test above.

`bitgrid_set_external_buffer` (the helper used only by the bank
path) is removed alongside the body β€” git history preserves it.

2. **Trap the four slot-33 leaf functions on the WA side**:
- `Sprite__GetFrameForBlit` (0x4FAD30)
- `SpriteBank__GetFrameForBlit` (0x4F9710)
- `FrameCache__Allocate` (0x4FA950)
- `DisplayBitGrid::SetExternalBuffer` (0x4F6470)

Each one's only callers (verified via xrefs) were inside the slot
33 dispatch chain, which is now bypassed by the
`vtable_replace!(get_sprite_frame_for_blit => ...)` from the
previous commit. If any of these traps fires in normal play, it
means we missed a still-live caller path and need to investigate.

`Sprite_LZSS_Decode` (0x5B29E0) is intentionally NOT trapped β€” it
has independent callers `IMG_Decode` (0x4F5F80) and `FUN_0048EE32`
reachable from `DDGame__Constructor`, `SoundEmitter`, and
`LoadHudAndWeaponSprites`, all still bridged.

Two new address constants added for the trap targets:
- `FRAME_CACHE_ALLOCATE` in `render::sprite::frame_cache`
- `DISPLAY_BIT_GRID_SET_EXTERNAL_BUFFER` in `bitgrid`

Both re-exported via `address::va`.

Verification:
- 135/135 unit tests pass.
- 15/15 headless replay tests pass (traps installed but headless
doesn't exercise the render path, so this only confirms
install_display succeeds and nothing else broke).
- Headful verification with traps in place is **still required** to
confirm none of the four traps fire during actual frame rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/address.rs CHANGED
@@ -73,7 +73,7 @@ pub mod va {
73
  pub use crate::bitgrid::{
74
  BitGridBaseVtable, BitGridCollisionVtable, BitGridDisplayVtable, BIT_GRID_BASE_VTABLE,
75
  BIT_GRID_COLLISION_VTABLE, BIT_GRID_DISPLAY_VTABLE, BIT_GRID_INIT, BLIT_SPRITE_RECT,
76
- DRAW_LINE_CLIPPED, DRAW_LINE_TWO_COLOR,
77
  };
78
  pub use crate::frontend::map_view::MAP_VIEW_VTABLE;
79
  pub use crate::input::controller::INPUT_CTRL_VTABLE;
@@ -90,6 +90,7 @@ pub mod va {
90
  pub use crate::render::palette::{
91
  PALETTE_CONTEXT_INIT, PALETTE_CONTEXT_INIT_RANGE, PALETTE_CONTEXT_MAP_COLOR,
92
  };
 
93
  pub use crate::render::sprite::sprite::SPRITE_BANK_VTABLE;
94
  pub use crate::render::sprite::sprite::{
95
  CBITMAP_VTABLE_MAYBE, CONSTRUCT_SPRITE, DESTROY_SPRITE, FREE_SPRITE_OBJECT,
 
73
  pub use crate::bitgrid::{
74
  BitGridBaseVtable, BitGridCollisionVtable, BitGridDisplayVtable, BIT_GRID_BASE_VTABLE,
75
  BIT_GRID_COLLISION_VTABLE, BIT_GRID_DISPLAY_VTABLE, BIT_GRID_INIT, BLIT_SPRITE_RECT,
76
+ DISPLAY_BIT_GRID_SET_EXTERNAL_BUFFER, DRAW_LINE_CLIPPED, DRAW_LINE_TWO_COLOR,
77
  };
78
  pub use crate::frontend::map_view::MAP_VIEW_VTABLE;
79
  pub use crate::input::controller::INPUT_CTRL_VTABLE;
 
90
  pub use crate::render::palette::{
91
  PALETTE_CONTEXT_INIT, PALETTE_CONTEXT_INIT_RANGE, PALETTE_CONTEXT_MAP_COLOR,
92
  };
93
+ pub use crate::render::sprite::frame_cache::FRAME_CACHE_ALLOCATE;
94
  pub use crate::render::sprite::sprite::SPRITE_BANK_VTABLE;
95
  pub use crate::render::sprite::sprite::{
96
  CBITMAP_VTABLE_MAYBE, CONSTRUCT_SPRITE, DESTROY_SPRITE, FREE_SPRITE_OBJECT,
crates/openwa-core/src/bitgrid.rs CHANGED
@@ -50,6 +50,12 @@ crate::define_addresses! {
50
  fn DRAW_LINE_CLIPPED = 0x004F_7500;
51
  /// Two-color line draw on 8bpp BitGrid (0x4F7A60)
52
  fn DRAW_LINE_TWO_COLOR = 0x004F_7A60;
 
 
 
 
 
 
53
  }
54
  }
55
 
 
50
  fn DRAW_LINE_CLIPPED = 0x004F_7500;
51
  /// Two-color line draw on 8bpp BitGrid (0x4F7A60)
52
  fn DRAW_LINE_TWO_COLOR = 0x004F_7A60;
53
+ /// `DisplayBitGrid::SetExternalBuffer` (FUN_004F6470) β€” fastcall
54
+ /// `(ECX=height, EDX=width, stack=(bitgrid, data, row_stride))`.
55
+ /// Updates an external-buffer bitgrid's data pointer + dimensions
56
+ /// + clip rect. Only known caller is `SpriteBank__GetFrameForBlit`,
57
+ /// which is itself unreachable in shipping WA β€” see slot 33.
58
+ fn/Fastcall DISPLAY_BIT_GRID_SET_EXTERNAL_BUFFER = 0x004F_6470;
59
  }
60
  }
61
 
crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -317,18 +317,16 @@ pub struct DisplayGfxVtable {
317
  /// | `out_left`/`top`/`right`/`bot` | frame bounding box within the cell |
318
  /// | `out_anim_frac` | sub-frame interpolation value (Fixed16) or 0 |
319
  ///
320
- /// **Ported.** Native Rust impl is [`get_sprite_frame_for_blit`]
321
- /// in this file (Sprite path β†’ [`sprite_get_frame_for_blit`],
322
- /// SpriteBank path β†’ [`sprite_bank_get_frame_for_blit`]). Wired via
323
- /// `vtable_replace!` in `install_display`. With this slot ported,
324
- /// the entire in-game render pipeline of a normal Worms match runs
325
- /// on Rust code with zero bridges into WA's render functions.
326
- ///
327
- /// **SpriteBank path is suspected dead in normal gameplay** β€” adding
328
- /// a `panic!()` to `sprite_bank_get_frame_for_blit` and playing
329
- /// several turns of a normal match did not trigger it. The path is
330
- /// still ported (per the disassembly) as a defensive measure for any
331
- /// obscure game state we haven't sampled.
332
  ///
333
  /// # Porting blueprint (kept for archival reference)
334
  ///
@@ -1664,187 +1662,56 @@ unsafe fn sprite_get_frame_for_blit(
1664
  bitgrid
1665
  }
1666
 
1667
- /// Pure-Rust port of `DisplayBitGrid::SetExternalBuffer` (0x4F6470).
1668
- ///
1669
- /// Sets a `DisplayBitGrid`'s data pointer, dimensions, row stride, and
1670
- /// clip rect (full-rect: `(0, 0, width, height)`). Returns `false` if the
1671
- /// bitgrid's `external_buffer` flag is currently zero (i.e. it owns its
1672
- /// own buffer and the caller has no business swapping it out).
1673
- ///
1674
- /// Used by the SpriteBank path of slot 33 to point the bank's embedded
1675
- /// `frame_bitgrid` (at `bank + 0x130`) at the freshly-decompressed
1676
- /// subframe pixels. Inlined manually in the Sprite path because that one
1677
- /// uses `frame_w` for both `width` and `row_stride`.
1678
- #[inline]
1679
- unsafe fn bitgrid_set_external_buffer(
1680
- bitgrid: *mut DisplayBitGrid,
1681
- data: *mut u8,
1682
- row_stride: u32,
1683
- width: u32,
1684
- height: u32,
1685
- ) -> bool {
1686
- if (*bitgrid).external_buffer == 0 {
1687
- return false;
1688
- }
1689
- (*bitgrid).data = data;
1690
- (*bitgrid).row_stride = row_stride;
1691
- (*bitgrid).width = width;
1692
- (*bitgrid).height = height;
1693
- (*bitgrid).clip_left = 0;
1694
- (*bitgrid).clip_top = 0;
1695
- (*bitgrid).clip_right = width;
1696
- (*bitgrid).clip_bottom = height;
1697
- true
1698
- }
1699
-
1700
- /// Pure-Rust port of `SpriteBank__GetFrameForBlit` (0x4F9710).
1701
- ///
1702
- /// SpriteBank-side counterpart of [`sprite_get_frame_for_blit`]: walks
1703
- /// the bank's `index_table` to map `sprite_id` to a frame in
1704
- /// `frame_table`, resolves the animation value to a sub-frame index in
1705
- /// `bbox_table`, lazily decompresses the corresponding subframe via
1706
- /// [`frame_cache_allocate`] + [`sprite_lzss_decode`], updates the
1707
- /// embedded `frame_bitgrid` (at `bank + 0x130`), and returns a pointer
1708
- /// to it. Returns null on any validation failure (null `frame_table` or
1709
- /// out-of-range `entry_idx`).
1710
- ///
1711
- /// Note the structural differences from the Sprite path:
1712
- /// - Animation metadata lives in [`SpriteBankFrame`]
1713
- /// (`flags/width/height/base_frame_idx/scale_or_count`) instead of
1714
- /// `Sprite::flags`/`max_frames`/`scale_*`.
1715
- /// - Out-`w`/`h` come from the per-frame `width`/`height`, NOT from a
1716
- /// sprite-wide cell dimension.
1717
- /// - The bbox table is a separate array of [`SpriteBankBboxEntry`] at
1718
- /// `bank + 0x18`, with `subframe_idx` at `+0` (low u16 of what would
1719
- /// be `SpriteFrame::bitmap_offset`) and `decoded_offset` at `+2`.
1720
- /// - The subframe cache uses [`SpriteBankSubframeCache`] with
1721
- /// `decoded_ptr` at `+8` (vs `+4` in `SpriteSubframeCache`).
1722
- /// - The LUT for LZSS is the inline 256-byte `palette_lut` at
1723
- /// `bank + 0x30`, not a separately-allocated palette pointer.
1724
- /// - The bitgrid update goes through `bitgrid_set_external_buffer`
1725
- /// instead of inline writes.
1726
- ///
1727
- /// # Safety
1728
- ///
1729
- /// `bank` must point at a valid, fully-parsed [`SpriteBank`]. Output
1730
- /// pointers must be writable.
1731
  #[allow(clippy::too_many_arguments)]
1732
  unsafe fn sprite_bank_get_frame_for_blit(
1733
- bank: *mut SpriteBank,
1734
  sprite_id: u32,
1735
- mut anim_value: u32,
1736
- out_anim_frac: *mut u32,
1737
- out_w: *mut i32,
1738
- out_h: *mut i32,
1739
- out_left: *mut i32,
1740
- out_top: *mut i32,
1741
- out_right: *mut i32,
1742
- out_bottom: *mut i32,
1743
  ) -> *mut DisplayBitGrid {
1744
- // ── Index lookup + bounds check ──────────────────────────────────
1745
- let entry_idx = *(*bank)
1746
- .index_table
1747
- .offset((sprite_id as i32 - (*bank).base_id) as isize);
1748
- if (*bank).frame_table.is_null() || entry_idx < 0 || entry_idx >= (*bank).frame_count {
1749
- return core::ptr::null_mut();
1750
- }
1751
- let frame = (*bank).frame_table.add(entry_idx as usize);
1752
- let frame_flags = (*frame).flags;
1753
-
1754
- // ── Clamp anim_value ─────────────────────────────────────────────
1755
- if frame_flags & 1 != 0 {
1756
- anim_value &= 0xFFFF;
1757
- } else {
1758
- let signed = anim_value as i32;
1759
- if signed < 0 {
1760
- anim_value = 0;
1761
- } else if signed >= 0x10000 {
1762
- anim_value = 0xFFFF;
1763
- }
1764
- }
1765
-
1766
- // ── Ping-pong ────────────────────────────────────────────────────
1767
- if frame_flags & 2 != 0 {
1768
- anim_value &= 0xFFFF;
1769
- anim_value = anim_value.wrapping_mul(2);
1770
- if anim_value >= 0x10000 {
1771
- anim_value = 0x1FFFE - anim_value;
1772
- }
1773
- }
1774
-
1775
- // ── Frame index resolution ───────────────────────────────────────
1776
- let scale_or_count = (*frame).scale_or_count;
1777
- let base_frame_idx = (*frame).base_frame_idx;
1778
-
1779
- let resolved_idx: u32 = if scale_or_count & 0x8000 != 0 {
1780
- // Scaled mode: low/high 7-bit nibbles of `scale_or_count` form a
1781
- // signed Fixed16 pair. The asm extracts both via:
1782
- // `value = (low_or_high_7 << 16) >> 5` (signed)
1783
- // i.e. shift the 7-bit value into the high half, sign-extend
1784
- // from bit 22, then shift right by 5 β†’ effective `<< 11` with
1785
- // sign extension from bit 6 of the original 7-bit value.
1786
- let low7 = scale_or_count & 0x7F;
1787
- let high7 = (scale_or_count >> 8) & 0x7F;
1788
- let low_val = (((low7 as u32) << 16) as i32) >> 5;
1789
- let high_val = (((high7 as u32) << 16) as i32) >> 5;
1790
- // Note: the asm computes `EAX = low - high` (NOT high - low) β€”
1791
- // verified at 0x4F97AD `SUB EAX, EDI` where EAX = low_val and
1792
- // EDI = high_val.
1793
- let diff = low_val.wrapping_sub(high_val);
1794
- let prod = (diff as i64).wrapping_mul((anim_value as i32) as i64);
1795
- let interp = ((prod >> 16) as i32).wrapping_add(high_val);
1796
- *out_anim_frac = interp as u32;
1797
- // Scaled mode uses `base_frame_idx` directly as the bbox index.
1798
- base_frame_idx as u32
1799
- } else {
1800
- // Non-scaled mode: subframe_idx = ((scale_or_count * anim_value) >> 16)
1801
- // + base_frame_idx. *out_anim_frac = 0.
1802
- let prod = (scale_or_count as i32).wrapping_mul(anim_value as i32);
1803
- *out_anim_frac = 0;
1804
- ((prod >> 16) as u32).wrapping_add(base_frame_idx as u32)
1805
- };
1806
-
1807
- // ── Read bbox metadata ───────────────────────────────────────────
1808
- let bbox = (*bank).bbox_table.add(resolved_idx as usize);
1809
- *out_left = (*bbox).start_x as i32;
1810
- *out_top = (*bbox).start_y as i32;
1811
- *out_right = (*bbox).end_x as i32;
1812
- *out_bottom = (*bbox).end_y as i32;
1813
- // out_w/out_h come from the per-frame width/height, NOT the bbox.
1814
- *out_w = (*frame).width as i32;
1815
- *out_h = (*frame).height as i32;
1816
- let frame_w = (*out_right - *out_left) as u32;
1817
- let frame_h = (*out_bottom - *out_top) as u32;
1818
-
1819
- // ── Subframe cache lookup + lazy decompress ──────────────────────
1820
- let subframe_idx = (*bbox).subframe_idx as u32;
1821
- let entry = (*bank).subframe_cache_table.add(subframe_idx as usize);
1822
-
1823
- if (*entry).decoded_ptr.is_null() {
1824
- let context_ptr = (*bank).context_ptr;
1825
- let decoded_size = (*entry).decoded_size;
1826
- let decoded = frame_cache_allocate(
1827
- decoded_size,
1828
- context_ptr,
1829
- bank as *mut core::ffi::c_void,
1830
- subframe_idx,
1831
- );
1832
- (*entry).decoded_ptr = decoded;
1833
- let src = (*bank)
1834
- .bitmap_data_ptr
1835
- .add((*entry).compressed_offset as usize) as *mut u8;
1836
- // Bank LUT is the inline 256-byte palette_lut at bank+0x30.
1837
- let lut = (&raw const (*bank).palette_lut) as *const u8;
1838
- sprite_lzss_decode(decoded, src, lut);
1839
- }
1840
-
1841
- // ── Update embedded bitgrid via SetExternalBuffer ────────────────
1842
- //
1843
- // Surface address = bbox.decoded_offset + entry.decoded_ptr.
1844
- let surface = (*entry).decoded_ptr.add((*bbox).decoded_offset as usize);
1845
- let bitgrid = &raw mut (*bank).frame_bitgrid;
1846
- bitgrid_set_external_buffer(bitgrid, surface, frame_w, frame_w, frame_h);
1847
- bitgrid
1848
  }
1849
 
1850
  /// Port of `DisplayGfx::GetSpriteFrameForBlit` (vtable slot 33, 0x5237C0).
 
317
  /// | `out_left`/`top`/`right`/`bot` | frame bounding box within the cell |
318
  /// | `out_anim_frac` | sub-frame interpolation value (Fixed16) or 0 |
319
  ///
320
+ /// **Ported (Sprite path only).** Native Rust impl is
321
+ /// [`get_sprite_frame_for_blit`] in this file: Sprite path β†’
322
+ /// [`sprite_get_frame_for_blit`], SpriteBank path β†’
323
+ /// [`sprite_bank_get_frame_for_blit`] (a `panic!()` stub β€” see its
324
+ /// docstring for the structural-deadness argument). Wired via
325
+ /// `vtable_replace!` in `install_display`.
326
+ ///
327
+ /// With this slot ported, the entire in-game render pipeline of a
328
+ /// normal Worms match runs on Rust code with zero bridges into WA's
329
+ /// render functions.
 
 
330
  ///
331
  /// # Porting blueprint (kept for archival reference)
332
  ///
 
1662
  bitgrid
1663
  }
1664
 
1665
+ /// Stub for `SpriteBank__GetFrameForBlit` (0x4F9710).
1666
+ ///
1667
+ /// **Structurally unreachable in shipping WA.** The only function that
1668
+ /// constructs a `SpriteBank` object is `SpriteBank__Constructor`
1669
+ /// (0x4F9450), and the only caller of *that* is
1670
+ /// `DisplayGfx::LoadSpriteEx` (slot 30, 0x523310), which has zero
1671
+ /// callers in WA.exe and zero in Rust and is currently trapped via
1672
+ /// `install_trap!("DisplayGfx__LoadSpriteEx", ...)` in `install_display`.
1673
+ ///
1674
+ /// Therefore `DisplayBase::sprite_banks[id]` is always null in shipping
1675
+ /// WA, and the `bank.is_null()` check in [`get_sprite_frame_for_blit`]
1676
+ /// always succeeds. The bank branch of slot 33 is dead code in normal
1677
+ /// gameplay β€” confirmed by adding a `panic!()` here and playing several
1678
+ /// turns of a regular Worms match without triggering it.
1679
+ ///
1680
+ /// A previous revision of this function (commit `973f234`) contained a
1681
+ /// disassembly-derived port of the bank-side logic, but it was untested
1682
+ /// because no replay or live play could reach it. Per the project rule
1683
+ /// against keeping unverified code, the body has been replaced with a
1684
+ /// panic β€” if any future change to WA, mods, or our ports starts
1685
+ /// producing live `SpriteBank` objects, this panic will fire and we'll
1686
+ /// need to revive the port (see git history for the unverified Ghidra
1687
+ /// translation, which would then need bridge-and-compare validation
1688
+ /// against the original WA function at 0x4F9710).
1689
+ ///
1690
+ /// The slot 33 dispatcher still calls this stub when `sprite_banks[id]`
1691
+ /// is non-null so we get a clear, source-located error rather than
1692
+ /// silently returning null (which would just present as missing
1693
+ /// graphics with no diagnostic).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1694
  #[allow(clippy::too_many_arguments)]
1695
  unsafe fn sprite_bank_get_frame_for_blit(
1696
+ _bank: *mut SpriteBank,
1697
  sprite_id: u32,
1698
+ _anim_value: u32,
1699
+ _out_anim_frac: *mut u32,
1700
+ _out_w: *mut i32,
1701
+ _out_h: *mut i32,
1702
+ _out_left: *mut i32,
1703
+ _out_top: *mut i32,
1704
+ _out_right: *mut i32,
1705
+ _out_bottom: *mut i32,
1706
  ) -> *mut DisplayBitGrid {
1707
+ panic!(
1708
+ "SpriteBank::GetFrameForBlit reached for sprite_id={sprite_id}, \
1709
+ but `DisplayBase::sprite_banks` was supposed to be unreachable \
1710
+ (LoadSpriteEx slot 30 is trapped). The bank-side port was \
1711
+ removed because it was unverifiable; revive it from commit \
1712
+ 973f234 if banks have become live and validate against \
1713
+ 0x4F9710 by bridge-and-compare."
1714
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1715
  }
1716
 
1717
  /// Port of `DisplayGfx::GetSpriteFrameForBlit` (vtable slot 33, 0x5237C0).
crates/openwa-core/src/render/sprite/frame_cache.rs CHANGED
@@ -23,6 +23,18 @@ use crate::render::display::base::FrameCache;
23
  use crate::render::display::base::FrameCacheEntry;
24
  use crate::render::SpriteCache;
25
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  /// Eviction callback signature β€” `vtable[1]` of both `Sprite` and
27
  /// `SpriteBank`. Called when the oldest entry is being dropped from the
28
  /// LRU ring buffer; the callback clears the owner's per-subframe
 
23
  use crate::render::display::base::FrameCacheEntry;
24
  use crate::render::SpriteCache;
25
 
26
+ crate::define_addresses! {
27
+ class "FrameCache" {
28
+ /// `FrameCache__Allocate` (FUN_004FA950) β€” usercall LRU
29
+ /// allocator. `EAX = entry_size`, stack args
30
+ /// `(context_ptr, owner, frame_idx)`. Ported to
31
+ /// [`frame_cache_allocate`]; only callers were
32
+ /// `Sprite__GetFrameForBlit` and `SpriteBank__GetFrameForBlit`,
33
+ /// both bypassed by the slot 33 vtable_replace.
34
+ fn/Usercall FRAME_CACHE_ALLOCATE = 0x004F_A950;
35
+ }
36
+ }
37
+
38
  /// Eviction callback signature β€” `vtable[1]` of both `Sprite` and
39
  /// `SpriteBank`. Called when the oldest entry is being dropped from the
40
  /// LRU ring buffer; the callback clears the owner's per-subframe
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -1370,6 +1370,37 @@ fn install_display() -> Result<(), String> {
1370
  // own Rust code (no `bind_DisplayVtable!` invocation, no direct call).
1371
  // Trap it so we'll be alerted the moment any future caller appears.
1372
  hook::install_trap!("DisplayGfx__LoadSpriteEx", va::DISPLAY_GFX_LOAD_SPRITE_EX);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1373
  }
1374
 
1375
  Ok(())
 
1370
  // own Rust code (no `bind_DisplayVtable!` invocation, no direct call).
1371
  // Trap it so we'll be alerted the moment any future caller appears.
1372
  hook::install_trap!("DisplayGfx__LoadSpriteEx", va::DISPLAY_GFX_LOAD_SPRITE_EX);
1373
+
1374
+ // ── Slot 33 leaf functions: trap the WA-side originals ────────
1375
+ //
1376
+ // After `vtable_replace!` swapped slot 33 to our Rust impl, the
1377
+ // original `DisplayGfx::GetSpriteFrameForBlit` (0x5237C0) is no
1378
+ // longer reached, which means its leaves have no remaining
1379
+ // callers in shipping WA either:
1380
+ //
1381
+ // - `Sprite__GetFrameForBlit` (0x4FAD30): only caller was the
1382
+ // original slot 33 dispatcher.
1383
+ // - `SpriteBank__GetFrameForBlit` (0x4F9710): same. Also bank
1384
+ // objects are themselves unreachable (LoadSpriteEx is trapped).
1385
+ // - `FrameCache__Allocate` (0x4FA950): only callers were the
1386
+ // two GetFrameForBlit helpers above.
1387
+ // - `DisplayBitGrid::SetExternalBuffer` (0x4F6470): only caller
1388
+ // was `SpriteBank__GetFrameForBlit`.
1389
+ //
1390
+ // `Sprite_LZSS_Decode` (0x5B29E0) is NOT trapped β€” it has
1391
+ // independent callers (`IMG_Decode`, `FUN_0048EE32`) reachable
1392
+ // from `DDGame__Constructor`/`SoundEmitter`/`LoadHudAndWeaponSprites`,
1393
+ // all still bridged. Trapping it would break those.
1394
+ hook::install_trap!("Sprite__GetFrameForBlit", va::SPRITE_GET_FRAME_FOR_BLIT);
1395
+ hook::install_trap!(
1396
+ "SpriteBank__GetFrameForBlit",
1397
+ va::SPRITE_BANK_GET_FRAME_FOR_BLIT
1398
+ );
1399
+ hook::install_trap!("FrameCache__Allocate", va::FRAME_CACHE_ALLOCATE);
1400
+ hook::install_trap!(
1401
+ "DisplayBitGrid__SetExternalBuffer",
1402
+ va::DISPLAY_BIT_GRID_SET_EXTERNAL_BUFFER
1403
+ );
1404
  }
1405
 
1406
  Ok(())