feat: port DisplayGfx::DrawTiledBitmap (slot 11) to pure Rust
Browse filesLast 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 +4 -3
- crates/openwa-core/src/render/display/context.rs +88 -2
- crates/openwa-core/src/render/display/gfx.rs +16 -10
- crates/openwa-core/src/render/display/vtable.rs +531 -6
- crates/openwa-core/src/render/sprite/sprite.rs +37 -1
- crates/openwa-dll/src/replacements/render.rs +2 -1
|
@@ -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,
|
| 96 |
-
LOAD_SPRITE_FROM_VFS, PROCESS_SPRITE, SPRITE_BANK_GET_FRAME_FOR_BLIT,
|
| 97 |
-
SPRITE_BANK_INIT, SPRITE_GET_FRAME_FOR_BLIT, SPRITE_GET_INFO,
|
|
|
|
| 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! {
|
|
@@ -228,13 +228,34 @@ pub struct RenderContextVtable {
|
|
| 228 |
this: *mut RenderContext,
|
| 229 |
result: *mut FastcallResult,
|
| 230 |
) -> *mut FastcallResult,
|
| 231 |
-
/// draw landscape — blit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
#[slot(23)]
|
| 233 |
pub draw_landscape: unsafe extern "fastcall" fn(
|
| 234 |
this: *mut RenderContext,
|
| 235 |
result: *mut FastcallResult,
|
| 236 |
surface: *mut u8,
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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);
|
|
@@ -65,20 +65,26 @@ pub struct DisplayGfx {
|
|
| 65 |
pub _unknown_357c: u32,
|
| 66 |
|
| 67 |
// =========================================================================
|
| 68 |
-
//
|
| 69 |
-
// =========================================================================
|
| 70 |
-
/// 0x3580:
|
| 71 |
-
|
| 72 |
-
///
|
| 73 |
-
pub
|
| 74 |
-
///
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
// =========================================================================
|
| 78 |
// Palette entry table (0x358C - 0x3D8F)
|
| 79 |
// =========================================================================
|
| 80 |
-
/// 0x358C:
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
| 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],
|
|
@@ -137,16 +137,15 @@ pub struct DisplayVtable {
|
|
| 137 |
/// visible tile × each `0x40`-wide destination column, calls
|
| 138 |
/// `DisplayGfx__BlitBitmapClipped`.
|
| 139 |
///
|
| 140 |
-
/// `source` is a
|
| 141 |
-
///
|
| 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`.
|
| 147 |
-
/// function — needs porting if/when slot 11 is replaced.
|
| 148 |
#[slot(11)]
|
| 149 |
-
pub draw_tiled_bitmap:
|
|
|
|
| 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 |
+
}
|
|
@@ -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;
|
|
@@ -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
|
| 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
|