paavohtl Claude Opus 4.6 (1M context) commited on
Commit
48dff37
·
1 Parent(s): f360a51

feat: port DisplayGfx::DrawTiledBitmap (slot 11) to pure Rust

Browse files

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 fields from u32 to *mut *mut CBitmap.

Fixes a latent bug in the draw_landscape (slot 23) typed binding:
the previous signature took 3 stack args but the function actually
takes 8 (verified by RET 0x20 + tracing the WA caller in
FUN_00403c60). Never surfaced before because we had no Rust caller.

Verifies (via disassembly stack-offset tracing, not the misleading
Ghidra decompile names) that the TiledBitmapSource layout is +0x08
data ptr, +0x10 row stride, +0x14 bpp dispatch, +0x18 total height.
The decompile incorrectly labels +0x08 as bpp.

Sidesteps porting std::vector::push_back by pre-reserving the
bitmap_vec at first call instead of growing it incrementally - slot
11's populate phase only ever pushes once per landscape height, so
the realloc path is never exercised in practice. This changes WA's
heap allocation pattern slightly (one bigger allocation vs multiple
growing allocations).

Verified by user manual visual inspection of landscape rendering
across multiple turns.

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

crates/openwa-core/src/address.rs CHANGED
@@ -92,9 +92,10 @@ pub mod va {
92
  };
93
  pub use crate::render::sprite::sprite::SPRITE_BANK_VTABLE;
94
  pub use crate::render::sprite::sprite::{
95
- CONSTRUCT_SPRITE, DESTROY_SPRITE, FREE_SPRITE_OBJECT, LOAD_SPRITE_BY_NAME,
96
- LOAD_SPRITE_FROM_VFS, PROCESS_SPRITE, SPRITE_BANK_GET_FRAME_FOR_BLIT, SPRITE_BANK_GET_INFO,
97
- SPRITE_BANK_INIT, SPRITE_GET_FRAME_FOR_BLIT, SPRITE_GET_INFO, SPRITE_VTABLE,
 
98
  };
99
 
100
  crate::define_addresses! {
 
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,
96
+ LOAD_SPRITE_BY_NAME, LOAD_SPRITE_FROM_VFS, PROCESS_SPRITE, SPRITE_BANK_GET_FRAME_FOR_BLIT,
97
+ SPRITE_BANK_GET_INFO, SPRITE_BANK_INIT, SPRITE_GET_FRAME_FOR_BLIT, SPRITE_GET_INFO,
98
+ SPRITE_VTABLE,
99
  };
100
 
101
  crate::define_addresses! {
crates/openwa-core/src/render/display/context.rs CHANGED
@@ -228,13 +228,34 @@ pub struct RenderContextVtable {
228
  this: *mut RenderContext,
229
  result: *mut FastcallResult,
230
  ) -> *mut FastcallResult,
231
- /// draw landscape — blit with clipping and optional color-key transparency (0x5A2790)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  #[slot(23)]
233
  pub draw_landscape: unsafe extern "fastcall" fn(
234
  this: *mut RenderContext,
235
  result: *mut FastcallResult,
236
  surface: *mut u8,
237
- y_offset: i32,
 
 
 
 
 
238
  flags: u32,
239
  ) -> *mut FastcallResult,
240
  /// blit surface to framebuffer — optimized copy with stride alignment (0x5A2A40)
@@ -267,3 +288,68 @@ const _: () = assert!(core::mem::size_of::<RenderContext>() == 0x1C);
267
  // Use the `_raw` variants to avoid LLVM noalias miscompilation.
268
  // Callers must pass a `&mut FastcallResult` buffer (allocated on the stack).
269
  bind_RenderContextVtable!(RenderContext, vtable);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  this: *mut RenderContext,
229
  result: *mut FastcallResult,
230
  ) -> *mut FastcallResult,
231
+ /// draw landscape — blit a `Surface` to the framebuffer with optional
232
+ /// color-key transparency (0x5A2790).
233
+ ///
234
+ /// 8 stack args (verified by RET 0x20 at 0x5a2cba):
235
+ /// - `surface`: source `Surface*` (its vtable's slot 17 = lock-write,
236
+ /// slot 18 = unlock are dispatched internally to read pixel data)
237
+ /// - `dst_x`/`dst_y`: framebuffer destination top-left
238
+ /// - `src_x`/`src_y`: rect origin within the surface to copy from
239
+ /// - `width`/`height`: rect size
240
+ /// - `flags`: bit 1 (`0x2`) = use color-key transparency (skip pixels
241
+ /// equal to the surface's color-key); bit 0 (`0x1`) is set by the
242
+ /// caller in `BlitBitmapClipped` and is reserved/ignored by the
243
+ /// blit loop here.
244
+ ///
245
+ /// Used by `DisplayGfx::DrawTiledBitmap` (slot 11) and
246
+ /// `DisplayGfx::DrawTiledTerrain` (slot 22) via the `CBitmap` blit
247
+ /// helper at `FUN_00403c60`.
248
  #[slot(23)]
249
  pub draw_landscape: unsafe extern "fastcall" fn(
250
  this: *mut RenderContext,
251
  result: *mut FastcallResult,
252
  surface: *mut u8,
253
+ dst_x: i32,
254
+ dst_y: i32,
255
+ src_x: i32,
256
+ src_y: i32,
257
+ width: i32,
258
+ height: i32,
259
  flags: u32,
260
  ) -> *mut FastcallResult,
261
  /// blit surface to framebuffer — optimized copy with stride alignment (0x5A2A40)
 
288
  // Use the `_raw` variants to avoid LLVM noalias miscompilation.
289
  // Callers must pass a `&mut FastcallResult` buffer (allocated on the stack).
290
  bind_RenderContextVtable!(RenderContext, vtable);
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Surface — DDraw/D3D/OpenGL surface object created by alloc_surface (slot 22)
294
+ // ---------------------------------------------------------------------------
295
+
296
+ /// Vtable for `Surface` objects allocated by `RenderContext::alloc_surface`
297
+ /// (slot 22). This is the rendering-backend surface interface — different
298
+ /// backends (CompatRenderer / OpenGLCPU / DDraw) implement different vtables
299
+ /// at the same slot positions.
300
+ ///
301
+ /// Slots typed here are the ones used by `DisplayGfx::DrawTiledBitmap`
302
+ /// (slot 11) and `LoadSpriteByName` (which already uses raw transmutes).
303
+ /// The remaining slots are unmapped — extend as needed.
304
+ ///
305
+ /// All slots are `__fastcall(this, *FastcallResult, ...args)`. The
306
+ /// `FastcallResult.value` field receives a non-zero failure code on
307
+ /// `init_surface` failure (slot 11 retries with bpp=4 if bpp=8 fails).
308
+ #[openwa_core::vtable(size = 8, class = "Surface")]
309
+ pub struct SurfaceVtable {
310
+ /// lock surface for direct pixel access (slot 3).
311
+ ///
312
+ /// Writes the locked surface base pointer to `*out_data` and the row
313
+ /// stride (bytes between rows) to `*out_stride`.
314
+ #[slot(3)]
315
+ pub lock_surface: unsafe extern "fastcall" fn(
316
+ this: *mut Surface,
317
+ result: *mut FastcallResult,
318
+ out_data: *mut *mut u8,
319
+ out_stride: *mut i32,
320
+ ) -> *mut FastcallResult,
321
+ /// unlock surface after editing (slot 4).
322
+ ///
323
+ /// `addr` is the surface base pointer previously obtained from
324
+ /// `lock_surface`'s `out_data`.
325
+ #[slot(4)]
326
+ pub unlock_surface: unsafe extern "fastcall" fn(
327
+ this: *mut Surface,
328
+ result: *mut FastcallResult,
329
+ addr: *mut u8,
330
+ ) -> *mut FastcallResult,
331
+ /// init/recreate surface storage (slot 5).
332
+ ///
333
+ /// `(width, height, bpp)`. Returns 0 in `*result.value` on success;
334
+ /// non-zero on failure.
335
+ #[slot(5)]
336
+ pub init_surface: unsafe extern "fastcall" fn(
337
+ this: *mut Surface,
338
+ result: *mut FastcallResult,
339
+ width: i32,
340
+ height: i32,
341
+ bpp: i32,
342
+ ) -> *mut FastcallResult,
343
+ }
344
+
345
+ /// Backend-specific surface object created by
346
+ /// `RenderContext::alloc_surface` (slot 22). Layout beyond the vtable
347
+ /// pointer is opaque (varies by backend); we only access it through the
348
+ /// `SurfaceVtable` slots.
349
+ #[repr(C)]
350
+ pub struct Surface {
351
+ /// 0x00: vtable pointer (one of several backend vtables)
352
+ pub vtable: *const SurfaceVtable,
353
+ }
354
+
355
+ bind_SurfaceVtable!(Surface, vtable);
crates/openwa-core/src/render/display/gfx.rs CHANGED
@@ -65,20 +65,26 @@ pub struct DisplayGfx {
65
  pub _unknown_357c: u32,
66
 
67
  // =========================================================================
68
- // Bitmap/sprite storage (0x3580 - 0x358B)
69
- // =========================================================================
70
- /// 0x3580: Bitmap vector pointer (init 0)
71
- pub bitmap_ptr: u32,
72
- /// 0x3584: Bitmap vector end (init 0)
73
- pub bitmap_end: u32,
74
- /// 0x3588: Bitmap vector capacity (init 0)
75
- pub bitmap_capacity: u32,
 
 
 
76
 
77
  // =========================================================================
78
  // Palette entry table (0x358C - 0x3D8F)
79
  // =========================================================================
80
- /// 0x358C: Palette entry count or lead byte
81
- pub _unknown_358c: u8,
 
 
 
82
  /// 0x358D - 0x398C: Palette entries (256 × 4 bytes = 0x400 bytes).
83
  /// Each entry is 4 bytes (R, G, B, flags?). Entry 255 (0x3989) = white.
84
  pub palette_entries: [u8; 0x400],
 
65
  pub _unknown_357c: u32,
66
 
67
  // =========================================================================
68
+ // Tile-cache bitmap vector (0x3580 - 0x358B)
69
+ // =========================================================================
70
+ /// 0x3580: Tile-cache `CBitmap*` vector — start pointer (init 0).
71
+ /// Populated by `DisplayGfx::DrawTiledBitmap` (slot 11) on its first
72
+ /// call: one entry per 0x400-row strip of the source landscape.
73
+ pub bitmap_ptr: *mut *mut crate::render::sprite::sprite::CBitmap,
74
+ /// 0x3584: Tile-cache vector end pointer (init 0). `(end - ptr) >> 2`
75
+ /// = current entry count.
76
+ pub bitmap_end: *mut *mut crate::render::sprite::sprite::CBitmap,
77
+ /// 0x3588: Tile-cache vector capacity-end pointer (init 0).
78
+ pub bitmap_capacity: *mut *mut crate::render::sprite::sprite::CBitmap,
79
 
80
  // =========================================================================
81
  // Palette entry table (0x358C - 0x3D8F)
82
  // =========================================================================
83
+ /// 0x358C: Tile-cache populate flag. Set to 0 by `DrawTiledBitmap`'s
84
+ /// allocate phase, set to 1 once the populate phase has filled every
85
+ /// strip's surface. While 0, the next `DrawTiledBitmap` call repopulates
86
+ /// the cached tile bitmaps from the source descriptor.
87
+ pub tile_cache_populated: u8,
88
  /// 0x358D - 0x398C: Palette entries (256 × 4 bytes = 0x400 bytes).
89
  /// Each entry is 4 bytes (R, G, B, flags?). Entry 255 (0x3989) = white.
90
  pub palette_entries: [u8; 0x400],
crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -137,16 +137,15 @@ pub struct DisplayVtable {
137
  /// visible tile × each `0x40`-wide destination column, calls
138
  /// `DisplayGfx__BlitBitmapClipped`.
139
  ///
140
- /// `source` is a sprite-source descriptor with at least these fields:
141
- /// `+0x08` = bpp (8 or 0x40), `+0x10` = source row stride,
142
- /// `+0x14` = (saved early, purpose unconfirmed), `+0x18` = total height.
143
  ///
144
  /// **Reachable at runtime** via `RenderDrawingQueue` case 0xD, fed by
145
  /// `RQ_EnqueueTiledBitmap` (0x541D60). The only known producer is
146
- /// `CTaskLand::RenderLandscape`. Currently bridged to the original WA
147
- /// function — needs porting if/when slot 11 is replaced.
148
  #[slot(11)]
149
- pub draw_tiled_bitmap: fn(this: *mut DisplayGfx, dest_x: u32, dest_y: i32, source: *mut u8),
 
150
  /// draw polyline with camera offset (0x56BCC0, RET 0xC)
151
  ///
152
  /// Transforms point array by camera offset, then draws connected line segments.
@@ -3415,3 +3414,529 @@ pub unsafe extern "thiscall" fn draw_text_on_bitmap(
3415
  font_id_high,
3416
  )
3417
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  /// visible tile × each `0x40`-wide destination column, calls
138
  /// `DisplayGfx__BlitBitmapClipped`.
139
  ///
140
+ /// `source` is a [`TiledBitmapSource`] descriptor — see that struct
141
+ /// for the field layout.
 
142
  ///
143
  /// **Reachable at runtime** via `RenderDrawingQueue` case 0xD, fed by
144
  /// `RQ_EnqueueTiledBitmap` (0x541D60). The only known producer is
145
+ /// `CTaskLand::RenderLandscape`.
 
146
  #[slot(11)]
147
+ pub draw_tiled_bitmap:
148
+ fn(this: *mut DisplayGfx, dest_x: i32, dest_y: i32, source: *const TiledBitmapSource),
149
  /// draw polyline with camera offset (0x56BCC0, RET 0xC)
150
  ///
151
  /// Transforms point array by camera offset, then draws connected line segments.
 
3414
  font_id_high,
3415
  )
3416
  }
3417
+
3418
+ // =============================================================================
3419
+ // DrawTiledBitmap (slot 11) and its leaf primitives
3420
+ // =============================================================================
3421
+
3422
+ use crate::render::display::context::Surface;
3423
+ use crate::render::sprite::sprite::CBitmap;
3424
+ use crate::wa_alloc::wa_malloc;
3425
+
3426
+ /// Source descriptor passed to `DisplayGfx::DrawTiledBitmap` (slot 11) as
3427
+ /// the third stack arg. The slot reads four fields out of this struct;
3428
+ /// the rest of the layout is unknown.
3429
+ ///
3430
+ /// Field offsets verified by tracing the disassembly of slot 11
3431
+ /// (`0x56b8c0`): the function reads `[ECX+0x14]` (bpp dispatch),
3432
+ /// `[ECX+0x18]` (total height), `[ECX+0x10]` (row stride), and
3433
+ /// `[ECX+0x08]` (source data base pointer).
3434
+ #[repr(C)]
3435
+ pub struct TiledBitmapSource {
3436
+ /// 0x00..0x07: unknown header bytes
3437
+ pub _unknown_00: [u8; 8],
3438
+ /// 0x08: source pixel data base pointer. Used as the start of the
3439
+ /// blit source; per-strip offset is `data + current_y * row_stride`.
3440
+ pub data: *const u8,
3441
+ /// 0x0C: unknown
3442
+ pub _unknown_0c: u32,
3443
+ /// 0x10: source row stride in bytes (passed as `src_stride` to the
3444
+ /// `0x40`-bpp blit primitive; for the 8bpp path the inner loop ignores
3445
+ /// it and advances 8 bytes per row internally).
3446
+ pub row_stride: i32,
3447
+ /// 0x14: bpp dispatch value. `8` selects the 8-byte-pattern replicator
3448
+ /// ([`blit_64byte_row_pattern`]); `0x40` selects the transparent CLUT
3449
+ /// blit ([`blit_color_table_forward`]); any other value skips the blit.
3450
+ pub bpp: u32,
3451
+ /// 0x18: total source height in rows. The cache is allocated as
3452
+ /// `ceil(source_height / 0x400)` strips of `min(remaining, 0x400)` rows.
3453
+ pub source_height: i32,
3454
+ }
3455
+
3456
+ const _: () = assert!(core::mem::offset_of!(TiledBitmapSource, data) == 0x08);
3457
+ const _: () = assert!(core::mem::offset_of!(TiledBitmapSource, row_stride) == 0x10);
3458
+ const _: () = assert!(core::mem::offset_of!(TiledBitmapSource, bpp) == 0x14);
3459
+ const _: () = assert!(core::mem::offset_of!(TiledBitmapSource, source_height) == 0x18);
3460
+
3461
+ /// Pure-Rust port of `FUN_005B2A5E` (the 8bpp 64-byte row replicator).
3462
+ ///
3463
+ /// For each of `row_count` rows, reads 8 bytes from `src` and writes them
3464
+ /// 8 times consecutively to `dst` (filling 64 bytes per row), then advances
3465
+ /// `dst` by `dst_stride` and `src` by exactly 8 bytes (regardless of any
3466
+ /// caller-side row stride). The original is hand-unrolled in asm with 16
3467
+ /// dword writes per row; the port collapses to one inner loop.
3468
+ ///
3469
+ /// Used by `DisplayGfx::DrawTiledBitmap` (slot 11) for the 8bpp tile
3470
+ /// populate phase. Has only that single caller (verified via
3471
+ /// `get_xrefs_to FUN_005B2A5E`).
3472
+ ///
3473
+ /// # Safety
3474
+ /// `dst` must point to a buffer with at least `(row_count - 1) * dst_stride
3475
+ /// + 64` bytes; `src` must point to a buffer with at least `row_count * 8`
3476
+ /// bytes. `row_count` must be positive (the original loops at least once
3477
+ /// before checking; we mirror that with `>= 1`).
3478
+ unsafe fn blit_64byte_row_pattern(
3479
+ mut dst: *mut u8,
3480
+ dst_stride: i32,
3481
+ mut src: *const u8,
3482
+ row_count: i32,
3483
+ ) {
3484
+ let mut remaining = row_count;
3485
+ while remaining > 0 {
3486
+ // Read 8 source bytes (one row's pattern).
3487
+ let pattern_lo = (src as *const u32).read_unaligned();
3488
+ let pattern_hi = (src.add(4) as *const u32).read_unaligned();
3489
+
3490
+ // Write 16 dwords = 64 bytes by replicating (lo, hi) 8 times.
3491
+ let dst_dw = dst as *mut u32;
3492
+ let mut i = 0;
3493
+ while i < 8 {
3494
+ dst_dw.add(i * 2).write_unaligned(pattern_lo);
3495
+ dst_dw.add(i * 2 + 1).write_unaligned(pattern_hi);
3496
+ i += 1;
3497
+ }
3498
+
3499
+ src = src.add(8);
3500
+ dst = dst.offset(dst_stride as isize);
3501
+ remaining -= 1;
3502
+ }
3503
+ }
3504
+
3505
+ /// Pure-Rust port of `BlitColorTable_Forward` (`0x5B2B5D`).
3506
+ ///
3507
+ /// Transparent byte-level blit: for each pixel in a `width × height` rect,
3508
+ /// if the source byte is non-zero, write it to the destination; otherwise
3509
+ /// leave the destination unchanged. Walks rows with independent
3510
+ /// `dst_stride`/`src_stride`.
3511
+ ///
3512
+ /// Used by `DisplayGfx::DrawTiledBitmap` (slot 11) for the `0x40`-bpp tile
3513
+ /// populate phase. The other listed caller in WA — `BitGrid::BlitSpriteRect`
3514
+ /// at `0x4F6C93` — is already replaced by our pure-Rust `blit_sprite_rect`
3515
+ /// in `sprite_blit.rs`, so this primitive is exclusively reachable through
3516
+ /// slot 11 in our build.
3517
+ ///
3518
+ /// # Safety
3519
+ /// `dst`/`src` must point to buffers large enough for the requested rect
3520
+ /// at the given strides. `width` and `height` must be positive (the
3521
+ /// original loops once before checking, mirrored here).
3522
+ unsafe fn blit_color_table_forward(
3523
+ mut dst: *mut u8,
3524
+ dst_stride: i32,
3525
+ mut src: *const u8,
3526
+ src_stride: i32,
3527
+ width: i32,
3528
+ height: i32,
3529
+ ) {
3530
+ let mut rows_left = height;
3531
+ while rows_left > 0 {
3532
+ let mut col = 0;
3533
+ while col < width as isize {
3534
+ let s = *src.offset(col);
3535
+ if s != 0 {
3536
+ *dst.offset(col) = s;
3537
+ }
3538
+ col += 1;
3539
+ }
3540
+ dst = dst.offset(dst_stride as isize);
3541
+ src = src.offset(src_stride as isize);
3542
+ rows_left -= 1;
3543
+ }
3544
+ }
3545
+
3546
+ /// Pure-Rust port of `FUN_00403c60` — the `CBitmap` blit-via-wrapper helper.
3547
+ ///
3548
+ /// Lazily allocates the backing surface for `cbm` via the render context's
3549
+ /// `alloc_surface` (slot 22) the first time it is called for this
3550
+ /// `CBitmap`, then dispatches the blit through `draw_landscape` (slot 23).
3551
+ ///
3552
+ /// `alloc_surface` returns the surface pointer in EAX (the binding's return
3553
+ /// value), NOT via the `FastcallResult` buffer — see
3554
+ /// `feedback_alloc_surface_return.md`.
3555
+ ///
3556
+ /// # Safety
3557
+ /// `cbm` must be a valid `*mut CBitmap`. `g_RenderContext` must be
3558
+ /// initialized.
3559
+ unsafe fn cbitmap_blit_via_wrapper(
3560
+ cbm: *mut CBitmap,
3561
+ dst_x: i32,
3562
+ dst_y: i32,
3563
+ src_x: i32,
3564
+ src_y: i32,
3565
+ width: i32,
3566
+ height: i32,
3567
+ flags: u32,
3568
+ ) {
3569
+ let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
3570
+
3571
+ // Lazy alloc: if the cbm has no surface yet, ask the wrapper for one.
3572
+ if (*cbm).surface.is_null() {
3573
+ let mut buf = FastcallResult::default();
3574
+ let ret = RenderContext::alloc_surface_raw(wrapper, &mut buf);
3575
+ // alloc_surface returns the surface pointer in EAX (the bound
3576
+ // wrapper's return value).
3577
+ (*cbm).surface = ret as *mut Surface;
3578
+ }
3579
+
3580
+ let mut buf = FastcallResult::default();
3581
+ RenderContext::draw_landscape_raw(
3582
+ wrapper,
3583
+ &mut buf,
3584
+ (*cbm).surface as *mut u8,
3585
+ dst_x,
3586
+ dst_y,
3587
+ src_x,
3588
+ src_y,
3589
+ width,
3590
+ height,
3591
+ flags,
3592
+ );
3593
+ }
3594
+
3595
+ /// Pure-Rust port of `DisplayGfx::BlitBitmapClipped` (`0x56A700`).
3596
+ ///
3597
+ /// Computes the clipped intersection of `(dst_x, dst_y, width, height)`
3598
+ /// against the `DisplayBase` clip rect (`0x3550..0x355C`), flushes the
3599
+ /// render lock if held, and dispatches the actual blit through
3600
+ /// `cbitmap_blit_via_wrapper`.
3601
+ ///
3602
+ /// The original WA function still exists and is called by other slots
3603
+ /// (`DrawTiledTerrain`, `BlitBitmapTiled_Maybe`); we don't replace it
3604
+ /// in-place. This Rust version is only used by our ported slot 11.
3605
+ /// (The earlier `blit_bitmap_clipped` in this file is the asm bridge to
3606
+ /// the original WA function used by slot 22 — kept until it has a typed
3607
+ /// caller to validate against.)
3608
+ ///
3609
+ /// # Safety
3610
+ /// `this` must be a valid `*mut DisplayGfx`. `surface` must be a valid
3611
+ /// `*mut CBitmap`. `g_RenderContext` must be initialized.
3612
+ unsafe fn blit_bitmap_clipped_native(
3613
+ this: *mut DisplayGfx,
3614
+ dst_x: i32,
3615
+ dst_y: i32,
3616
+ width: i32,
3617
+ height: i32,
3618
+ surface: *mut CBitmap,
3619
+ flags: u32,
3620
+ ) {
3621
+ let base = &(*this).base;
3622
+ let cx1 = base.clip_x1;
3623
+ let cy1 = base.clip_y1;
3624
+ let cx2 = base.clip_x2;
3625
+ let cy2 = base.clip_y2;
3626
+
3627
+ let dst_x2 = dst_x + width;
3628
+ let dst_y2 = dst_y + height;
3629
+
3630
+ // Trivial reject — entirely outside the clip rect.
3631
+ if dst_x >= cx2 || dst_x2 <= cx1 || dst_y >= cy2 || dst_y2 <= cy1 {
3632
+ return;
3633
+ }
3634
+
3635
+ let new_left = cx1.max(dst_x);
3636
+ let new_right = cx2.min(dst_x2);
3637
+ let new_top = cy1.max(dst_y);
3638
+ let new_bottom = cy2.min(dst_y2);
3639
+
3640
+ // Degenerate (zero-width or zero-height clipped rect) — original
3641
+ // checks `local_28 != iVar2 && local_24 != iVar1`. Mirror exactly.
3642
+ if new_left == new_right || new_top == new_bottom {
3643
+ return;
3644
+ }
3645
+
3646
+ flush_render_lock(this);
3647
+
3648
+ cbitmap_blit_via_wrapper(
3649
+ surface,
3650
+ new_left,
3651
+ new_top,
3652
+ new_left - dst_x,
3653
+ new_top - dst_y,
3654
+ new_right - new_left,
3655
+ new_bottom - new_top,
3656
+ flags | 1,
3657
+ );
3658
+ }
3659
+
3660
+ /// Pure-Rust port of `DisplayGfx::DrawTiledBitmap` (vtable slot 11,
3661
+ /// `0x56B8C0`).
3662
+ ///
3663
+ /// Three-phase tile-cached landscape blit. See the docstring on
3664
+ /// [`DisplayVtable::draw_tiled_bitmap`] for the high-level semantics.
3665
+ ///
3666
+ /// **Tile-cache vector pre-reservation.** The original allocates and
3667
+ /// `vector::push_back`s each strip's `CBitmap*` into `bitmap_vec`
3668
+ /// (`+0x3580`) one at a time, growing the vector as needed. We sidestep
3669
+ /// porting `std::vector::push_back` (`FUN_00402e90`) by computing the
3670
+ /// final entry count up front (`ceil(source_height / 0x400)`), allocating
3671
+ /// the entire backing buffer in one shot, then writing entries directly
3672
+ /// and bumping `bitmap_end`. This changes WA's heap allocation pattern
3673
+ /// slightly (one bigger allocation vs many growing reallocs) but is
3674
+ /// behaviorally equivalent: each `DisplayGfx` only ever populates the
3675
+ /// vector once.
3676
+ ///
3677
+ /// # Safety
3678
+ /// `this` must be a valid `*mut DisplayGfx`. `source` must be a valid
3679
+ /// `*const TiledBitmapSource` whose fields describe the source landscape
3680
+ /// data. `g_RenderContext` must be initialized.
3681
+ pub unsafe fn draw_tiled_bitmap_impl(
3682
+ this: *mut DisplayGfx,
3683
+ dest_x: i32,
3684
+ dest_y: i32,
3685
+ source: *const TiledBitmapSource,
3686
+ ) {
3687
+ let total_height = (*source).source_height;
3688
+ let row_stride = (*source).row_stride;
3689
+ let bpp = (*source).bpp;
3690
+ let source_data = (*source).data;
3691
+
3692
+ // -------------------------------------------------------------------
3693
+ // Phase 1 — Allocate (only if bitmap_vec is empty)
3694
+ // -------------------------------------------------------------------
3695
+ let vec_empty = (*this).bitmap_ptr.is_null()
3696
+ || ((*this).bitmap_end as usize - (*this).bitmap_ptr as usize) >> 2 == 0;
3697
+
3698
+ if vec_empty {
3699
+ if total_height > 0 {
3700
+ // Pre-reserve the entire bitmap_vec in one allocation. The
3701
+ // original grows the vector incrementally; we don't.
3702
+ let max_tiles = ((total_height + 0x3FF) >> 10) as usize;
3703
+ let vec_buf = wa_malloc((max_tiles * core::mem::size_of::<*mut CBitmap>()) as u32)
3704
+ as *mut *mut CBitmap;
3705
+ if vec_buf.is_null() {
3706
+ return;
3707
+ }
3708
+ (*this).bitmap_ptr = vec_buf;
3709
+ (*this).bitmap_end = vec_buf;
3710
+ (*this).bitmap_capacity = vec_buf.add(max_tiles);
3711
+
3712
+ let cbitmap_vt = rb(va::CBITMAP_VTABLE_MAYBE) as *const core::ffi::c_void;
3713
+ let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
3714
+
3715
+ let mut accum = 0i32;
3716
+ let mut remaining = total_height;
3717
+ while accum < total_height {
3718
+ // Allocate one CBitmap entry.
3719
+ let cbm = wa_malloc(core::mem::size_of::<CBitmap>() as u32) as *mut CBitmap;
3720
+ if !cbm.is_null() {
3721
+ (*cbm).vtable = cbitmap_vt;
3722
+ (*cbm).surface = core::ptr::null_mut();
3723
+ (*cbm)._pad = 0;
3724
+ }
3725
+ // The original assumes the malloc succeeded for the rest
3726
+ // of the loop body (no null check on `puVar6`). Mirror
3727
+ // that exactly.
3728
+
3729
+ let strip_h = remaining.min(0x400);
3730
+
3731
+ // Lazy-alloc the surface (matches the original's
3732
+ // double-checked alloc_surface inside the strip loop).
3733
+ if (*cbm).surface.is_null() {
3734
+ let mut buf = FastcallResult::default();
3735
+ let s = RenderContext::alloc_surface_raw(wrapper, &mut buf);
3736
+ (*cbm).surface = s as *mut Surface;
3737
+ }
3738
+
3739
+ // Init at 0x40 × strip_h × 8bpp.
3740
+ let mut init_buf = FastcallResult::default();
3741
+ Surface::init_surface_raw((*cbm).surface, &mut init_buf, 0x40, strip_h, 8);
3742
+
3743
+ // On failure, retry with bpp=4. The original re-allocates
3744
+ // the surface first (still null-checks), then retries.
3745
+ if init_buf.value != 0 {
3746
+ if (*cbm).surface.is_null() {
3747
+ let mut buf = FastcallResult::default();
3748
+ let s = RenderContext::alloc_surface_raw(wrapper, &mut buf);
3749
+ (*cbm).surface = s as *mut Surface;
3750
+ }
3751
+ let mut init_buf2 = FastcallResult::default();
3752
+ Surface::init_surface_raw((*cbm).surface, &mut init_buf2, 0x40, strip_h, 4);
3753
+ if init_buf2.value != 0 {
3754
+ // Both inits failed — bail (matches the original's
3755
+ // `JNZ 0056bcb4` to the function epilogue).
3756
+ return;
3757
+ }
3758
+ }
3759
+
3760
+ // push_back: write the entry and bump end (capacity is
3761
+ // pre-reserved; this never reallocates).
3762
+ *(*this).bitmap_end = cbm;
3763
+ (*this).bitmap_end = (*this).bitmap_end.add(1);
3764
+
3765
+ accum += 0x400;
3766
+ remaining -= 0x400;
3767
+ }
3768
+ }
3769
+ (*this).tile_cache_populated = 0;
3770
+ }
3771
+
3772
+ // -------------------------------------------------------------------
3773
+ // Phase 2 — Populate (only if tile_cache_populated == 0)
3774
+ // -------------------------------------------------------------------
3775
+ if (*this).tile_cache_populated == 0 {
3776
+ let mut tile_idx: usize = 0;
3777
+ let mut current_y: i32 = 0;
3778
+ loop {
3779
+ let vec_size = if (*this).bitmap_ptr.is_null() {
3780
+ 0
3781
+ } else {
3782
+ ((*this).bitmap_end as usize - (*this).bitmap_ptr as usize) >> 2
3783
+ };
3784
+ if tile_idx >= vec_size {
3785
+ break;
3786
+ }
3787
+
3788
+ let strip_end = total_height.min(current_y + 0x400);
3789
+ let strip_h = strip_end - current_y;
3790
+
3791
+ let cbm = *(*this).bitmap_ptr.add(tile_idx);
3792
+
3793
+ // Lazy-alloc surface (paranoid: should already be non-null
3794
+ // from phase 1, but the original repeats the check).
3795
+ if (*cbm).surface.is_null() {
3796
+ let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
3797
+ let mut buf = FastcallResult::default();
3798
+ let s = RenderContext::alloc_surface_raw(wrapper, &mut buf);
3799
+ (*cbm).surface = s as *mut Surface;
3800
+ }
3801
+
3802
+ // Lock the surface (slot 3): writes data ptr to `surf_data`
3803
+ // and stride to `surf_stride`.
3804
+ let mut surf_data: *mut u8 = core::ptr::null_mut();
3805
+ let mut surf_stride: i32 = 0;
3806
+ {
3807
+ let mut buf = FastcallResult::default();
3808
+ Surface::lock_surface_raw(
3809
+ (*cbm).surface,
3810
+ &mut buf,
3811
+ &mut surf_data,
3812
+ &mut surf_stride,
3813
+ );
3814
+ }
3815
+
3816
+ // Compute the source row pointer for this strip.
3817
+ let src_row = source_data.offset((current_y as isize) * (row_stride as isize));
3818
+
3819
+ if bpp == 8 {
3820
+ blit_64byte_row_pattern(surf_data, surf_stride, src_row, strip_h);
3821
+ } else if bpp == 0x40 {
3822
+ blit_color_table_forward(
3823
+ surf_data,
3824
+ surf_stride,
3825
+ src_row,
3826
+ row_stride,
3827
+ 0x40,
3828
+ strip_h,
3829
+ );
3830
+ }
3831
+ // (other bpp: no blit, just unlock — matches original)
3832
+
3833
+ // Unlock the surface (slot 4) with the locked data ptr.
3834
+ {
3835
+ let mut buf = FastcallResult::default();
3836
+ Surface::unlock_surface_raw((*cbm).surface, &mut buf, surf_data);
3837
+ }
3838
+
3839
+ tile_idx += 1;
3840
+ current_y += 0x400;
3841
+ }
3842
+ (*this).tile_cache_populated = 1;
3843
+ }
3844
+
3845
+ // -------------------------------------------------------------------
3846
+ // Phase 3 — Display
3847
+ // -------------------------------------------------------------------
3848
+ // dest_x: snap to a 0x40 grid by computing (dest_x mod 0x40) - 0x40
3849
+ // (with the mod result in [-0x3f, 0x3f] using sign-preserving
3850
+ // rounding). The result is the X offset of the FIRST visible tile
3851
+ // column relative to the screen origin: in the range [-0x3f, 0],
3852
+ // such that stepping by 0x40 produces tile-aligned column positions
3853
+ // that cover the visible area.
3854
+ let col_x: i32 = {
3855
+ // Reproduce the original's signed mod 0x40:
3856
+ // AND EAX, 0x8000003f ; preserve sign bit + low 6 bits
3857
+ // if negative: DEC, OR 0xffffffc0, INC (sign-extend)
3858
+ let dest_x_u = dest_x as u32;
3859
+ let masked = dest_x_u & 0x8000_003f;
3860
+ let mut v = if (masked as i32) < 0 {
3861
+ // sign-preserve
3862
+ (((masked.wrapping_sub(1)) | 0xffff_ffc0).wrapping_add(1)) as i32
3863
+ } else {
3864
+ masked as i32
3865
+ };
3866
+ // If v > 0, subtract 0x40.
3867
+ if v > 0 {
3868
+ v -= 0x40;
3869
+ }
3870
+ v
3871
+ };
3872
+
3873
+ // Y-tile range: compute first/last visible tile index from
3874
+ // (camera_y + dest_y) and (camera_y + dest_y - display_height)
3875
+ // using the same signed `(v + ((v >> 31) & 0x3FF)) >> 10` rounding
3876
+ // idiom that the original applies (round toward zero).
3877
+ let camera_y = (*this).camera_y;
3878
+ let neg = -(camera_y + dest_y);
3879
+ let display_height = (*this).base.display_height as i32;
3880
+ let first_v = neg + 0x20000;
3881
+ let last_v = display_height + neg + 0x20000;
3882
+
3883
+ let mut y_first = (((first_v + ((first_v >> 31) & 0x3FF)) >> 10) - 0x80) as i32;
3884
+ let mut y_last = (((last_v + ((last_v >> 31) & 0x3FF)) >> 10) - 0x80) as i32;
3885
+
3886
+ // Clamp y_first to >= 0 (matches the SETLE/SUB/AND idiom).
3887
+ if y_first < 0 {
3888
+ y_first = 0;
3889
+ }
3890
+
3891
+ let vec_size = if (*this).bitmap_ptr.is_null() {
3892
+ 0i32
3893
+ } else {
3894
+ (((*this).bitmap_end as usize - (*this).bitmap_ptr as usize) >> 2) as i32
3895
+ };
3896
+ let max_idx = vec_size - 1;
3897
+ if max_idx <= y_last {
3898
+ y_last = vec_size - 1;
3899
+ }
3900
+
3901
+ if y_first > y_last {
3902
+ return;
3903
+ }
3904
+
3905
+ let display_width = (*this).base.display_width as i32;
3906
+ let mut tile_idx_y = y_first;
3907
+ let mut current_strip_y = y_first << 10; // y_first * 0x400
3908
+
3909
+ while tile_idx_y <= y_last {
3910
+ let strip_end = total_height.min(current_strip_y + 0x400);
3911
+ let strip_h = strip_end - current_strip_y;
3912
+
3913
+ // Skip the column loop if the start column is already off-screen.
3914
+ if col_x < display_width {
3915
+ let cbm = *(*this).bitmap_ptr.add(tile_idx_y as usize);
3916
+ let dst_y = camera_y + current_strip_y + dest_y;
3917
+ let mut x = col_x;
3918
+ while x < display_width {
3919
+ blit_bitmap_clipped_native(this, x, dst_y, 0x40, strip_h, cbm, 0);
3920
+ x += 0x40;
3921
+ }
3922
+ }
3923
+
3924
+ tile_idx_y += 1;
3925
+ current_strip_y += 0x400;
3926
+ }
3927
+ }
3928
+
3929
+ /// Thiscall entry point for `DisplayGfx::DrawTiledBitmap` (vtable slot 11).
3930
+ /// Matches the original's signature: `(this, dest_x, dest_y, source)` with
3931
+ /// `RET 0xC` (3 stack args, callee-cleaned).
3932
+ ///
3933
+ /// # Safety
3934
+ /// See [`draw_tiled_bitmap_impl`].
3935
+ pub unsafe extern "thiscall" fn draw_tiled_bitmap(
3936
+ this: *mut DisplayGfx,
3937
+ dest_x: i32,
3938
+ dest_y: i32,
3939
+ source: *const TiledBitmapSource,
3940
+ ) {
3941
+ draw_tiled_bitmap_impl(this, dest_x, dest_y, source);
3942
+ }
crates/openwa-core/src/render/sprite/sprite.rs CHANGED
@@ -212,12 +212,42 @@ pub struct LayerSprite {
212
 
213
  const _: () = assert!(core::mem::size_of::<LayerSprite>() == 0x70);
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  /// Per-frame surface element for LayerSprite (0x14 bytes).
216
  ///
217
  /// Holds bounding box coordinates and a DirectDraw surface pointer.
218
  /// Allocated in counted arrays: `malloc(count * 0x14 + 4)`, count at `[-4]`.
219
  ///
220
- /// Ghidra name: `CBitmap` (unrelated to `BitGrid`/`DisplayBitGrid`).
 
 
 
 
 
221
  /// Constructor: 0x573C30 (sets vtable, zeroes surface).
222
  /// Destructor: 0x5732E0 (releases surface via `surface->vtable[0](1)`).
223
  #[repr(C)]
@@ -241,6 +271,12 @@ pub struct LayerSpriteFrame {
241
  const _: () = assert!(core::mem::size_of::<LayerSpriteFrame>() == 0x14);
242
 
243
  crate::define_addresses! {
 
 
 
 
 
 
244
  class "Sprite" {
245
  /// ConstructSprite — usercall EAX=sprite_ptr, ECX=context_ptr
246
  ctor/Usercall CONSTRUCT_SPRITE = 0x004F_AA30;
 
212
 
213
  const _: () = assert!(core::mem::size_of::<LayerSprite>() == 0x70);
214
 
215
+ /// Standalone `CBitmap` cache entry — 12 bytes, vtable `0x643F64`.
216
+ ///
217
+ /// Allocated by `DisplayGfx::DrawTiledBitmap` (slot 11) as the per-strip
218
+ /// cached tile bitmap entry stored in `DisplayGfx.bitmap_vec`. Not the
219
+ /// same struct as [`LayerSpriteFrame`] (which is 0x14 bytes and uses the
220
+ /// same vtable but with a leading 8-byte bounding box) — but the trailing
221
+ /// (vtable, surface, pad) triple matches.
222
+ ///
223
+ /// The vtable pointer is `0x643F64` (constructor at 0x573C30 sets it).
224
+ /// The `surface` field is lazily allocated via `RenderContext::alloc_surface`
225
+ /// the first time the bitmap is blitted.
226
+ #[repr(C)]
227
+ pub struct CBitmap {
228
+ /// 0x00: Vtable pointer (= `0x643F64`).
229
+ pub vtable: *const core::ffi::c_void,
230
+ /// 0x04: Backend surface object pointer. Lazily allocated by
231
+ /// `cbitmap_blit_via_wrapper` (`FUN_00403c60`); zero until the first
232
+ /// `BlitBitmapClipped` call for this bitmap.
233
+ pub surface: *mut crate::render::display::context::Surface,
234
+ /// 0x08: Padding/reserved (zeroed by constructor).
235
+ pub _pad: u32,
236
+ }
237
+
238
+ const _: () = assert!(core::mem::size_of::<CBitmap>() == 0xC);
239
+
240
  /// Per-frame surface element for LayerSprite (0x14 bytes).
241
  ///
242
  /// Holds bounding box coordinates and a DirectDraw surface pointer.
243
  /// Allocated in counted arrays: `malloc(count * 0x14 + 4)`, count at `[-4]`.
244
  ///
245
+ /// Ghidra name: `CBitmap` (unrelated to `BitGrid`/`DisplayBitGrid`). The
246
+ /// trailing 12 bytes (vtable + surface + pad) are isomorphic to
247
+ /// [`CBitmap`] above and share the same vtable `0x643F64`, but the
248
+ /// container has an 8-byte bounding-box prefix that the standalone form
249
+ /// does not.
250
+ ///
251
  /// Constructor: 0x573C30 (sets vtable, zeroes surface).
252
  /// Destructor: 0x5732E0 (releases surface via `surface->vtable[0](1)`).
253
  #[repr(C)]
 
271
  const _: () = assert!(core::mem::size_of::<LayerSpriteFrame>() == 0x14);
272
 
273
  crate::define_addresses! {
274
+ class "CBitmap" {
275
+ /// Vtable shared by [`CBitmap`] (12 bytes) and [`LayerSpriteFrame`]
276
+ /// (0x14 bytes). Set by the CBitmap constructor at 0x573C30.
277
+ vtable CBITMAP_VTABLE_MAYBE = 0x0064_3F64;
278
+ }
279
+
280
  class "Sprite" {
281
  /// ConstructSprite — usercall EAX=sprite_ptr, ECX=context_ptr
282
  ctor/Usercall CONSTRUCT_SPRITE = 0x004F_AA30;
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -1427,6 +1427,7 @@ fn install_display() -> Result<(), String> {
1427
  set_active_layer => display_vtable::set_active_layer,
1428
  get_sprite_info => display_vtable::get_sprite_info,
1429
  draw_text_on_bitmap => display_vtable::draw_text_on_bitmap,
 
1430
  get_font_info => display_vtable::get_font_info,
1431
  get_font_metric => display_vtable::get_font_metric,
1432
  set_font_param => display_vtable::set_font_param,
@@ -1453,7 +1454,7 @@ fn install_display() -> Result<(), String> {
1453
  load_font => load_font,
1454
  load_font_extension => load_font_extension,
1455
  })?;
1456
- let _ = log_line("[Display] DisplayGfx: patched 31 methods -> Rust");
1457
 
1458
  // DisplayGfx::LoadSpriteEx (vtable slot 30) has zero callers in both
1459
  // WA.exe (no instructions reach vtable[+0x78] on a DisplayGfx) and our
 
1427
  set_active_layer => display_vtable::set_active_layer,
1428
  get_sprite_info => display_vtable::get_sprite_info,
1429
  draw_text_on_bitmap => display_vtable::draw_text_on_bitmap,
1430
+ draw_tiled_bitmap => display_vtable::draw_tiled_bitmap,
1431
  get_font_info => display_vtable::get_font_info,
1432
  get_font_metric => display_vtable::get_font_metric,
1433
  set_font_param => display_vtable::set_font_param,
 
1454
  load_font => load_font,
1455
  load_font_extension => load_font_extension,
1456
  })?;
1457
+ let _ = log_line("[Display] DisplayGfx: patched 32 methods -> Rust");
1458
 
1459
  // DisplayGfx::LoadSpriteEx (vtable slot 30) has zero callers in both
1460
  // WA.exe (no instructions reach vtable[+0x78] on a DisplayGfx) and our