paavohtl Claude Opus 4.6 (1M context) commited on
Commit
04b2ceb
·
1 Parent(s): edcdcc5

feat: port slot 19 bitmap-sprite path to pure Rust

Browse files

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_bitmap_clipped_native promoted from `unsafe fn` to `pub` so
the DLL hook can call it directly.

New ports in `vtable.rs`:

- `blit_bitmap_tiled_native` — pure-Rust port of BlitBitmapTiled. The
two-loop alignment idiom (walk x by ±tile_width until x ≤ clip_x1)
was verified against the disassembly at 0x56A7DD..0x56A7F4. Then
loops `blit_bitmap_clipped_native` across `[clip_x1, clip_x2)`.
- `get_bitmap_sprite_info` — pure-Rust port of GetBitmapSpriteInfo.
Forward-mode and ping-pong frame index calculation, returns the
resolved frame's `*mut CBitmap` via `LayerSpriteFrame::bitmap_ptr`.

DLL `blit_sprite` now calls these three native functions directly
in the bitmap-sprite branch. Deletes ~95 lines of asm bridges
(`wa_get_bitmap_sprite_info`, `wa_blit_bitmap_clipped`,
`wa_blit_bitmap_tiled`) plus their three address constants
(`DISPLAY_GFX_GET_BITMAP_SPRITE_INFO`,
`DISPLAY_GFX_BLIT_BITMAP_CLIPPED`, `DISPLAY_GFX_BLIT_BITMAP_TILED`).

Verification: cargo build clean, 15/15 headless replay tests pass,
headful gameplay (HUD, weapon icons, scoreboard) confirmed visually
by user — graphics look perfect, no visible issues.

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

crates/openwa-core/src/address.rs CHANGED
@@ -462,12 +462,6 @@ pub mod va {
462
  fn/Usercall DISPLAY_GFX_INIT = 0x0056_9D00;
463
  /// DisplayGfx vtable slot 19 — blit sprite
464
  fn/Thiscall DISPLAY_GFX_BLIT_SPRITE = 0x0056_B080;
465
- /// DisplayGfx bitmap sprite info lookup — usercall(EAX=bitmap_obj, EDX=palette), RET 0x18
466
- fn/Usercall DISPLAY_GFX_GET_BITMAP_SPRITE_INFO = 0x0057_3C50;
467
- /// DisplayGfx bitmap blit (clipped) — usercall(EAX=this, EDX=width), RET 0x14
468
- fn/Usercall DISPLAY_GFX_BLIT_BITMAP_CLIPPED = 0x0056_A700;
469
- /// DisplayGfx bitmap blit (tiled) — usercall(EAX=initial_x, EDI=tile_width), RET 0x10
470
- fn/Usercall DISPLAY_GFX_BLIT_BITMAP_TILED = 0x0056_A7D0;
471
  /// DisplayGfx flush render lock — releases lock, plain RET
472
  fn DISPLAY_GFX_FLUSH_RENDER_LOCK = 0x0056_A330;
473
  /// Streaming audio constructor
 
462
  fn/Usercall DISPLAY_GFX_INIT = 0x0056_9D00;
463
  /// DisplayGfx vtable slot 19 — blit sprite
464
  fn/Thiscall DISPLAY_GFX_BLIT_SPRITE = 0x0056_B080;
 
 
 
 
 
 
465
  /// DisplayGfx flush render lock — releases lock, plain RET
466
  fn DISPLAY_GFX_FLUSH_RENDER_LOCK = 0x0056_A330;
467
  /// Streaming audio constructor
crates/openwa-core/src/render/display/gfx.rs CHANGED
@@ -1,7 +1,7 @@
1
  use super::base::DisplayBase;
2
  use super::vtable::DisplayGfxVtable;
3
  use crate::bitgrid::DisplayBitGrid;
4
- use crate::render::sprite::sprite::CBitmap;
5
  use core::ffi::c_void;
6
 
7
  /// DisplayGfx — full display/graphics subsystem (derived from DisplayBase).
@@ -133,9 +133,11 @@ pub struct DisplayGfx {
133
  // =========================================================================
134
  // Sprite/bitmap table (0x3DD4 - 0x4DD7)
135
  // =========================================================================
136
- /// 0x3DD4: Sprite table — 1024 DWORD entries, zeroed in DisplayGfx__Init.
137
- /// Used for tracking loaded sprites/bitmaps by ID.
138
- pub sprite_table: [u32; 0x400],
 
 
139
  // =========================================================================
140
  // Tile stream config (0x4DD4 - 0x4DF3)
141
  // =========================================================================
 
1
  use super::base::DisplayBase;
2
  use super::vtable::DisplayGfxVtable;
3
  use crate::bitgrid::DisplayBitGrid;
4
+ use crate::render::sprite::sprite::{CBitmap, LayerSprite};
5
  use core::ffi::c_void;
6
 
7
  /// DisplayGfx — full display/graphics subsystem (derived from DisplayBase).
 
133
  // =========================================================================
134
  // Sprite/bitmap table (0x3DD4 - 0x4DD7)
135
  // =========================================================================
136
+ /// 0x3DD4: Bitmap-sprite table — 1024 entries, zeroed in DisplayGfx__Init.
137
+ /// Each non-null entry is a `LayerSprite` allocated by `load_sprite_by_layer`
138
+ /// (vtable slot 37). Read by `GetBitmapSpriteInfo` (slot's helper, used
139
+ /// by the bitmap-sprite branch of `BlitSprite` / slot 19).
140
+ pub sprite_table: [*mut LayerSprite; 0x400],
141
  // =========================================================================
142
  // Tile stream config (0x4DD4 - 0x4DF3)
143
  // =========================================================================
crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -891,7 +891,7 @@ pub unsafe extern "thiscall" fn is_sprite_loaded(this: *mut DisplayGfx, id: i32)
891
  let base = &(*this).base;
892
  if !base.sprite_ptrs[id as usize].is_null()
893
  || !base.sprite_banks[id as usize].is_null()
894
- || (*this).sprite_table[id as usize] != 0
895
  {
896
  1
897
  } else {
@@ -1772,11 +1772,12 @@ pub unsafe fn load_sprite_by_name(
1772
  }
1773
  }
1774
 
1775
- // Read sprite metadata fields
 
1776
  GfxDirStream::read_raw(stream, sp.add(0x60), 4); // field_60
1777
- GfxDirStream::read_raw(stream, sp.add(0x64), 2); // field_64
1778
- GfxDirStream::read_raw(stream, sp.add(0x68), 2); // field_68
1779
- GfxDirStream::read_raw(stream, sp.add(0x6A), 2); // field_6a
1780
 
1781
  // frame_count: zero first, then read
1782
  (*sprite).frame_count = 0;
@@ -2034,7 +2035,7 @@ pub unsafe fn load_sprite_by_layer(
2034
  }
2035
 
2036
  // Store in sprite_table (DisplayGfx offset 0x3DD4)
2037
- (*this).sprite_table[id as usize] = sprite as u32;
2038
 
2039
  1
2040
  }
@@ -2711,16 +2712,14 @@ unsafe fn cbitmap_blit_via_wrapper(
2711
  /// render lock if held, and dispatches the actual blit through
2712
  /// `cbitmap_blit_via_wrapper`.
2713
  ///
2714
- /// Used by ported slots 11 (`DrawTiledBitmap`) and 22 (`DrawTiledTerrain`).
2715
- /// The original WA function at `0x56A700` still exists and is called by
2716
- /// the slot-19 bitmap-sprite fallback path (`wa_blit_bitmap_clipped`) —
2717
- /// see follow-up plan item 4. The address constant
2718
- /// `DISPLAY_GFX_BLIT_BITMAP_CLIPPED` is kept until that path is ported.
2719
  ///
2720
  /// # Safety
2721
  /// `this` must be a valid `*mut DisplayGfx`. `surface` must be a valid
2722
  /// `*mut CBitmap`. `g_RenderContext` must be initialized.
2723
- unsafe fn blit_bitmap_clipped_native(
2724
  this: *mut DisplayGfx,
2725
  dst_x: i32,
2726
  dst_y: i32,
@@ -2768,6 +2767,122 @@ unsafe fn blit_bitmap_clipped_native(
2768
  );
2769
  }
2770
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2771
  /// Pure-Rust port of `DisplayGfx::DrawTiledBitmap` (vtable slot 11,
2772
  /// `0x56B8C0`).
2773
  ///
 
891
  let base = &(*this).base;
892
  if !base.sprite_ptrs[id as usize].is_null()
893
  || !base.sprite_banks[id as usize].is_null()
894
+ || !(*this).sprite_table[id as usize].is_null()
895
  {
896
  1
897
  } else {
 
1772
  }
1773
  }
1774
 
1775
+ // Read sprite metadata fields (in original WA's read order:
1776
+ // field_60 first, then flags / cell_width / cell_height).
1777
  GfxDirStream::read_raw(stream, sp.add(0x60), 4); // field_60
1778
+ GfxDirStream::read_raw(stream, sp.add(0x64), 2); // flags
1779
+ GfxDirStream::read_raw(stream, sp.add(0x68), 2); // cell_width
1780
+ GfxDirStream::read_raw(stream, sp.add(0x6A), 2); // cell_height
1781
 
1782
  // frame_count: zero first, then read
1783
  (*sprite).frame_count = 0;
 
2035
  }
2036
 
2037
  // Store in sprite_table (DisplayGfx offset 0x3DD4)
2038
+ (*this).sprite_table[id as usize] = sprite;
2039
 
2040
  1
2041
  }
 
2712
  /// render lock if held, and dispatches the actual blit through
2713
  /// `cbitmap_blit_via_wrapper`.
2714
  ///
2715
+ /// Used by ported slots 11 (`DrawTiledBitmap`), 22 (`DrawTiledTerrain`),
2716
+ /// and the bitmap-sprite branch of slot 19 (`BlitSprite`) via the DLL
2717
+ /// `blit_sprite` hook.
 
 
2718
  ///
2719
  /// # Safety
2720
  /// `this` must be a valid `*mut DisplayGfx`. `surface` must be a valid
2721
  /// `*mut CBitmap`. `g_RenderContext` must be initialized.
2722
+ pub unsafe fn blit_bitmap_clipped_native(
2723
  this: *mut DisplayGfx,
2724
  dst_x: i32,
2725
  dst_y: i32,
 
2767
  );
2768
  }
2769
 
2770
+ /// Pure-Rust port of `DisplayGfx::BlitBitmapTiled` (`0x56A7D0`,
2771
+ /// usercall EAX=initial_x, EDI=tile_width).
2772
+ ///
2773
+ /// Tiles `surface` horizontally across the visible clip range
2774
+ /// `[clip_x1, clip_x2)`. The function first walks `initial_x` by
2775
+ /// `±tile_width` until `x ≤ clip_x1`, then blits at `(x, dst_y),
2776
+ /// (x + tile_width, dst_y), …` until past `clip_x2`. Each blit goes
2777
+ /// through `blit_bitmap_clipped_native` with the same fixed `flags = 2`
2778
+ /// the original WA function uses.
2779
+ ///
2780
+ /// Used by the bitmap-sprite branch of slot 19 (`BlitSprite`) when the
2781
+ /// `tiled` mode bit (high_flags bit 16) is set on the sprite.
2782
+ ///
2783
+ /// # Safety
2784
+ /// `this` must be a valid `*mut DisplayGfx` with an initialized clip
2785
+ /// rect. `surface` must be a valid `*mut CBitmap`. `tile_width` must be
2786
+ /// positive (zero or negative would loop forever; matches the original).
2787
+ pub unsafe fn blit_bitmap_tiled_native(
2788
+ this: *mut DisplayGfx,
2789
+ initial_x: i32,
2790
+ tile_width: i32,
2791
+ dst_y: i32,
2792
+ height: i32,
2793
+ surface: *mut CBitmap,
2794
+ ) {
2795
+ let clip_x1 = (*this).base.clip_x1;
2796
+ let clip_x2 = (*this).base.clip_x2;
2797
+
2798
+ // Walk to the largest x ≤ clip_x1 in the arithmetic sequence
2799
+ // {initial_x ± k*tile_width} (matches the original's two-loop pattern
2800
+ // at 0x56A7DD..0x56A7F4).
2801
+ let mut x = initial_x;
2802
+ while x < clip_x1 {
2803
+ x += tile_width;
2804
+ }
2805
+ while x > clip_x1 {
2806
+ x -= tile_width;
2807
+ }
2808
+
2809
+ while x < clip_x2 {
2810
+ blit_bitmap_clipped_native(this, x, dst_y, tile_width, height, surface, 2);
2811
+ x += tile_width;
2812
+ }
2813
+ }
2814
+
2815
+ /// Pure-Rust port of `DisplayGfx::GetBitmapSpriteInfo` (`0x573C50`,
2816
+ /// usercall EAX=bitmap_obj, EDX=palette_or_anim).
2817
+ ///
2818
+ /// Resolves an animation/palette value into a frame index for a
2819
+ /// `LayerSprite`, then writes the selected frame's bounding box and the
2820
+ /// sprite's full cell width/height to the output pointers and returns a
2821
+ /// pointer to the frame's `CBitmap` (the trailing 12 bytes of the
2822
+ /// `LayerSpriteFrame` entry).
2823
+ ///
2824
+ /// `bitmap_obj.flags` (`LayerSprite +0x64`) controls interpretation of
2825
+ /// `palette_or_anim`:
2826
+ /// - bit 0 set: low 16 bits taken as-is (no clamp)
2827
+ /// - bit 0 clear: signed clamp to `[0, 0xFFFF]`
2828
+ /// - bit 1 set: ping-pong (bounce) iteration over `[0, frame_count)`
2829
+ /// - bit 1 clear: forward iteration
2830
+ ///
2831
+ /// Used by the bitmap-sprite branch of slot 19 (`BlitSprite`).
2832
+ ///
2833
+ /// # Safety
2834
+ /// `bitmap_obj` must be a valid `LayerSprite` with a populated
2835
+ /// `frame_array`. The output pointers must be writable.
2836
+ pub unsafe fn get_bitmap_sprite_info(
2837
+ bitmap_obj: *mut LayerSprite,
2838
+ palette_or_anim: u32,
2839
+ out_w: *mut i32,
2840
+ out_h: *mut i32,
2841
+ out_left: *mut i32,
2842
+ out_top: *mut i32,
2843
+ out_right: *mut i32,
2844
+ out_bottom: *mut i32,
2845
+ ) -> *mut CBitmap {
2846
+ let flags = (*bitmap_obj).flags as i32;
2847
+
2848
+ // Step 1: clamp / mask the palette_or_anim value into [0, 0xFFFF].
2849
+ let pal: i32 = if flags & 1 != 0 {
2850
+ (palette_or_anim & 0xFFFF) as i32
2851
+ } else {
2852
+ let p = palette_or_anim as i32;
2853
+ p.max(0).min(0xFFFF)
2854
+ };
2855
+
2856
+ // Step 2: compute frame index from frame_count and pal.
2857
+ let frame_count = (*bitmap_obj).frame_count as i16 as i32;
2858
+ let frame_idx = if flags & 2 != 0 {
2859
+ // Ping-pong: scaled = ((2*frame_count - 1) * pal) >> 16
2860
+ // If scaled >= frame_count, fold back: (2*frame_count - scaled) - 1.
2861
+ let scaled = ((frame_count * 2 - 1) * pal) >> 16;
2862
+ if scaled >= frame_count {
2863
+ (frame_count * 2 - scaled) - 1
2864
+ } else {
2865
+ scaled
2866
+ }
2867
+ } else {
2868
+ // Forward: idx = (frame_count * pal) >> 16
2869
+ (frame_count * pal) >> 16
2870
+ };
2871
+
2872
+ // Step 3: read the selected frame entry.
2873
+ let frame = (*bitmap_obj).frame_array.offset(frame_idx as isize);
2874
+
2875
+ *out_left = (*frame).start_x as i32;
2876
+ *out_top = (*frame).start_y as i32;
2877
+ *out_right = (*frame).end_x as i32;
2878
+ *out_bottom = (*frame).end_y as i32;
2879
+
2880
+ *out_w = (*bitmap_obj).cell_width as i32;
2881
+ *out_h = (*bitmap_obj).cell_height as i32;
2882
+
2883
+ LayerSpriteFrame::bitmap_ptr(frame)
2884
+ }
2885
+
2886
  /// Pure-Rust port of `DisplayGfx::DrawTiledBitmap` (vtable slot 11,
2887
  /// `0x56B8C0`).
2888
  ///
crates/openwa-core/src/render/sprite/sprite.rs CHANGED
@@ -184,6 +184,12 @@ pub struct SpriteBankVtable {
184
 
185
  /// Layer sprite — 0x70-byte object used by `load_sprite_by_layer` (vtable slot 37).
186
  ///
 
 
 
 
 
 
187
  /// Unlike `Sprite` (which has a vtable and is created via ConstructSprite), this
188
  /// is a raw allocation with partial init by the caller. Used for background layers
189
  /// (back.spr, layer.spr) which load DirectDraw surfaces directly from `.dir` files.
@@ -200,14 +206,18 @@ pub struct LayerSprite {
200
  pub palette_ctx: *mut PaletteContext,
201
  /// 0x60: Header field from stream (4 bytes).
202
  pub field_60: u32,
203
- /// 0x64: Header field from stream (2 bytes).
204
- pub field_64: u16,
 
 
 
 
205
  /// 0x66: Frame count (zeroed, then read from stream).
206
  pub frame_count: u16,
207
- /// 0x68: Header field from stream (2 bytes).
208
- pub field_68: u16,
209
- /// 0x6A: Header field from stream (2 bytes).
210
- pub field_6a: u16,
211
  /// 0x6C: Pointer to LayerSpriteFrame array (count stored at ptr[-4]).
212
  pub frame_array: *mut LayerSpriteFrame,
213
  }
@@ -272,6 +282,18 @@ pub struct LayerSpriteFrame {
272
 
273
  const _: () = assert!(core::mem::size_of::<LayerSpriteFrame>() == 0x14);
274
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  crate::define_addresses! {
276
  class "CBitmap" {
277
  /// Vtable shared by [`CBitmap`] (12 bytes) and [`LayerSpriteFrame`]
 
184
 
185
  /// Layer sprite — 0x70-byte object used by `load_sprite_by_layer` (vtable slot 37).
186
  ///
187
+ /// Also used for the "bitmap sprite" path of `BlitSprite` (slot 19): the
188
+ /// per-id pointers in `DisplayGfx::sprite_table` (`+0x3DD4`) point at
189
+ /// `LayerSprite` instances. `DisplayGfx::GetBitmapSpriteInfo` reads
190
+ /// `flags`, `frame_count`, `cell_width`, `cell_height`, and `frame_array`
191
+ /// to resolve a frame index → frame metadata + `CBitmap` pointer.
192
+ ///
193
  /// Unlike `Sprite` (which has a vtable and is created via ConstructSprite), this
194
  /// is a raw allocation with partial init by the caller. Used for background layers
195
  /// (back.spr, layer.spr) which load DirectDraw surfaces directly from `.dir` files.
 
206
  pub palette_ctx: *mut PaletteContext,
207
  /// 0x60: Header field from stream (4 bytes).
208
  pub field_60: u32,
209
+ /// 0x64: Animation/playback mode flags. Read by
210
+ /// `GetBitmapSpriteInfo` to decide how to interpret the
211
+ /// palette/anim parameter:
212
+ /// - bit 0: low 16 bits used as-is (no clamp)
213
+ /// - bit 1: ping-pong (bounce) frame iteration
214
+ pub flags: u16,
215
  /// 0x66: Frame count (zeroed, then read from stream).
216
  pub frame_count: u16,
217
+ /// 0x68: Sprite cell width in pixels (used for centering during blit).
218
+ pub cell_width: i16,
219
+ /// 0x6A: Sprite cell height in pixels (used for centering during blit).
220
+ pub cell_height: i16,
221
  /// 0x6C: Pointer to LayerSpriteFrame array (count stored at ptr[-4]).
222
  pub frame_array: *mut LayerSpriteFrame,
223
  }
 
282
 
283
  const _: () = assert!(core::mem::size_of::<LayerSpriteFrame>() == 0x14);
284
 
285
+ impl LayerSpriteFrame {
286
+ /// Returns a pointer to the trailing 12-byte `CBitmap` block
287
+ /// (`bitmap_vtable`/`surface`/`_pad_10`) inside this entry. The
288
+ /// `CBitmap` layout is bit-identical to the trailing 12 bytes of
289
+ /// `LayerSpriteFrame`, so the value can be passed to any function
290
+ /// that takes `*mut CBitmap` (e.g., `blit_bitmap_clipped_native`).
291
+ #[inline]
292
+ pub fn bitmap_ptr(this: *mut Self) -> *mut CBitmap {
293
+ unsafe { (this as *mut u8).add(0x08) as *mut CBitmap }
294
+ }
295
+ }
296
+
297
  crate::define_addresses! {
298
  class "CBitmap" {
299
  /// Vtable shared by [`CBitmap`] (12 bytes) and [`LayerSpriteFrame`]
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -808,19 +808,19 @@ unsafe extern "thiscall" fn blit_sprite(
808
  if arr1 == 0 && arr2 == 0 {
809
  // Bitmap sprite path — sprite is in the bitmap table at 0x3DD4.
810
  let bitmap_obj = (*gfx).sprite_table[sprite_id as usize];
811
- if bitmap_obj == 0 {
812
  return;
813
  }
814
 
815
- // Get frame data and dimensions from bitmap sprite object
816
  let mut sprite_w: i32 = 0;
817
  let mut sprite_h: i32 = 0;
818
  let mut rect_left: i32 = 0;
819
  let mut rect_top: i32 = 0;
820
  let mut rect_right: i32 = 0;
821
  let mut rect_bottom: i32 = 0;
822
- let frame_data = wa_get_bitmap_sprite_info(
823
- bitmap_obj as *mut u8,
824
  pal,
825
  &mut sprite_w,
826
  &mut sprite_h,
@@ -828,7 +828,6 @@ unsafe extern "thiscall" fn blit_sprite(
828
  &mut rect_top,
829
  &mut rect_right,
830
  &mut rect_bottom,
831
- rb(openwa_core::address::va::DISPLAY_GFX_GET_BITMAP_SPRITE_INFO),
832
  );
833
  if frame_data.is_null() {
834
  return;
@@ -839,33 +838,18 @@ unsafe extern "thiscall" fn blit_sprite(
839
  let half_w = sprite_w / 2;
840
  let half_h = sprite_h / 2;
841
  let blit_h = rect_bottom - rect_top;
842
-
843
  let dst_y = (y.0 >> 16) + (camera_y - half_h) + rect_top;
844
 
845
  if (high_flags & 0x0001_0000) == 0 {
846
- // Non-tiled: BlitBitmapClipped
847
- let dst_x = (x.0 >> 16) + (camera_x - half_w) + rect_left;
848
- wa_blit_bitmap_clipped(
849
- this as *mut u8,
850
- sprite_w as u32,
851
- dst_x,
852
- dst_y,
853
- blit_h,
854
- frame_data,
855
- 2,
856
- rb(openwa_core::address::va::DISPLAY_GFX_BLIT_BITMAP_CLIPPED),
857
  );
858
  } else {
859
- // Tiled: BlitBitmapTiled
860
- let dst_x = (x.0 >> 16) + (camera_x - half_w) + rect_left;
861
- wa_blit_bitmap_tiled(
862
- dst_x,
863
- sprite_w,
864
- this as *mut u8,
865
- dst_y,
866
- blit_h,
867
- frame_data,
868
- rb(openwa_core::address::va::DISPLAY_GFX_BLIT_BITMAP_TILED),
869
  );
870
  }
871
  return;
@@ -1136,93 +1120,6 @@ unsafe extern "thiscall" fn blit_sprite(
1136
  );
1137
  }
1138
 
1139
- // =========================================================================
1140
- // Bitmap sprite bridges (naked asm for usercall conventions)
1141
- // =========================================================================
1142
-
1143
- /// Call DisplayGfx__GetBitmapSpriteInfo (0x573C50).
1144
- /// Usercall: EAX=bitmap_obj, EDX=palette, 6 stack params (output ptrs), RET 0x18.
1145
- #[unsafe(naked)]
1146
- unsafe extern "cdecl" fn wa_get_bitmap_sprite_info(
1147
- _bitmap_obj: *mut u8,
1148
- _palette: u32,
1149
- _out_w: *mut i32,
1150
- _out_h: *mut i32,
1151
- _out_left: *mut i32,
1152
- _out_top: *mut i32,
1153
- _out_right: *mut i32,
1154
- _out_bottom: *mut i32,
1155
- _target: u32,
1156
- ) -> *const u8 {
1157
- core::arch::naked_asm!(
1158
- "mov eax, [esp + 4]", // bitmap_obj
1159
- "mov edx, [esp + 8]", // palette
1160
- "mov ecx, [esp + 36]", // target
1161
- "push dword ptr [esp + 32]", // out_bottom
1162
- "push dword ptr [esp + 32]", // out_right
1163
- "push dword ptr [esp + 32]", // out_top
1164
- "push dword ptr [esp + 32]", // out_left
1165
- "push dword ptr [esp + 32]", // out_h
1166
- "push dword ptr [esp + 32]", // out_w
1167
- "call ecx", // RET 0x18 cleans 6 params
1168
- "ret",
1169
- );
1170
- }
1171
-
1172
- /// Call DisplayGfx__BlitBitmapClipped (0x56A700).
1173
- /// Usercall: EAX=this, EDX=width, 5 stack params (dst_x, dst_y, height, frame_data, flags), RET 0x14.
1174
- #[unsafe(naked)]
1175
- unsafe extern "cdecl" fn wa_blit_bitmap_clipped(
1176
- _this: *mut u8,
1177
- _width: u32,
1178
- _dst_x: i32,
1179
- _dst_y: i32,
1180
- _height: i32,
1181
- _frame_data: *const u8,
1182
- _flags: u32,
1183
- _target: u32,
1184
- ) {
1185
- core::arch::naked_asm!(
1186
- "mov eax, [esp + 4]", // this
1187
- "mov edx, [esp + 8]", // width
1188
- "mov ecx, [esp + 32]", // target
1189
- "push dword ptr [esp + 28]", // flags
1190
- "push dword ptr [esp + 28]", // frame_data
1191
- "push dword ptr [esp + 28]", // height
1192
- "push dword ptr [esp + 28]", // dst_y
1193
- "push dword ptr [esp + 28]", // dst_x
1194
- "call ecx", // RET 0x14 cleans 5 params
1195
- "ret",
1196
- );
1197
- }
1198
-
1199
- /// Call DisplayGfx__BlitBitmapTiled (0x56A7D0).
1200
- /// Usercall: EAX=initial_x, EDI=tile_width, 4 stack params (this, dst_y, height, frame_data), RET 0x10.
1201
- #[unsafe(naked)]
1202
- unsafe extern "cdecl" fn wa_blit_bitmap_tiled(
1203
- _initial_x: i32,
1204
- _tile_width: i32,
1205
- _this: *mut u8,
1206
- _dst_y: i32,
1207
- _height: i32,
1208
- _frame_data: *const u8,
1209
- _target: u32,
1210
- ) {
1211
- core::arch::naked_asm!(
1212
- "push edi",
1213
- "mov eax, [esp + 8]", // initial_x
1214
- "mov edi, [esp + 12]", // tile_width
1215
- "mov ecx, [esp + 32]", // target (offset +4 from push edi)
1216
- "push dword ptr [esp + 28]", // frame_data
1217
- "push dword ptr [esp + 28]", // height
1218
- "push dword ptr [esp + 28]", // dst_y
1219
- "push dword ptr [esp + 28]", // this
1220
- "call ecx", // RET 0x10 cleans 4 params
1221
- "pop edi",
1222
- "ret",
1223
- );
1224
- }
1225
-
1226
  /// Thiscall wrapper for DisplayGfx::DrawScaledSprite (slot 20).
1227
  ///
1228
  /// Computes coordinates in core, then dispatches the blit via blit_impl.
 
808
  if arr1 == 0 && arr2 == 0 {
809
  // Bitmap sprite path — sprite is in the bitmap table at 0x3DD4.
810
  let bitmap_obj = (*gfx).sprite_table[sprite_id as usize];
811
+ if bitmap_obj.is_null() {
812
  return;
813
  }
814
 
815
+ // Get frame data and dimensions from bitmap sprite object.
816
  let mut sprite_w: i32 = 0;
817
  let mut sprite_h: i32 = 0;
818
  let mut rect_left: i32 = 0;
819
  let mut rect_top: i32 = 0;
820
  let mut rect_right: i32 = 0;
821
  let mut rect_bottom: i32 = 0;
822
+ let frame_data = display_vtable::get_bitmap_sprite_info(
823
+ bitmap_obj,
824
  pal,
825
  &mut sprite_w,
826
  &mut sprite_h,
 
828
  &mut rect_top,
829
  &mut rect_right,
830
  &mut rect_bottom,
 
831
  );
832
  if frame_data.is_null() {
833
  return;
 
838
  let half_w = sprite_w / 2;
839
  let half_h = sprite_h / 2;
840
  let blit_h = rect_bottom - rect_top;
841
+ let dst_x = (x.0 >> 16) + (camera_x - half_w) + rect_left;
842
  let dst_y = (y.0 >> 16) + (camera_y - half_h) + rect_top;
843
 
844
  if (high_flags & 0x0001_0000) == 0 {
845
+ // Non-tiled: clipped blit at (dst_x, dst_y).
846
+ display_vtable::blit_bitmap_clipped_native(
847
+ gfx, dst_x, dst_y, sprite_w, blit_h, frame_data, 2,
 
 
 
 
 
 
 
 
848
  );
849
  } else {
850
+ // Tiled: horizontal tile across the clip rect.
851
+ display_vtable::blit_bitmap_tiled_native(
852
+ gfx, dst_x, sprite_w, dst_y, blit_h, frame_data,
 
 
 
 
 
 
 
853
  );
854
  }
855
  return;
 
1120
  );
1121
  }
1122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1123
  /// Thiscall wrapper for DisplayGfx::DrawScaledSprite (slot 20).
1124
  ///
1125
  /// Computes coordinates in core, then dispatches the blit via blit_impl.