paavohtl commited on
Commit
1b3fdc4
·
1 Parent(s): e162b4f

refactor: propagate type info through display vtable.rs and friends

Browse files

A series of small type-safety improvements that compound to delete a
lot of raw byte arithmetic and ad-hoc transmutes:

- `LayerSpriteFrame.bitmap_vtable` retyped from `u32` to
`*const c_void`, and `LayerSpriteFrame.surface` from `u32` to
`*mut Surface`. The two existing consumers (the slot 0 destructor's
`layer_sprite_frame_destructor` and the `load_sprite_by_name` /
`free_layer_sprite` pair) now access these via named fields
instead of raw `elem.add(0x08)` / `elem.add(0x0C)` arithmetic.

- `SurfaceVtable::set_color_key` (slot 7) added as a typed binding,
matching the existing `lock_surface` / `unlock_surface` /
`init_surface` / `release` slots. `load_sprite_by_name`'s per-frame
surface init loop now uses the typed binding for slots 3, 4, 5, 6,
7 instead of hand-rolled raw transmutes — drops ~50 lines.

- `RenderContextVtable::lock_surface_write`'s `out_data` retyped
from `*mut u32` to `*mut *mut u8`, and `unlock_surface_write`'s
`p2: u32` renamed to `data: *mut u8`. The acquire/flush helpers
in `vtable.rs` propagate the typed pointer through into
`(*layer_0).data` without the `as *mut u8` cast.

- `font_extend`'s `char_map` and slot 35 `load_font_extension`'s
`char_map` retyped from `*const u8` to `*const c_char` (the
original is a null-terminated C-string passed via the .fex file
format). Similarly `font_draw_text_impl`'s `msg` is now
`*const c_char` to match the slot 7 caller signature, with one
internal cast to `*const u8` at function entry.

- `palette_commit_bridge` now takes `*mut DisplayGfx` instead of
`*mut u8`.

- `LayerSprite` field reads in `load_sprite_by_name` now use
`&raw mut (*sprite).field_name` instead of `sp.add(0x60)` style
raw byte offsets — eight call sites cleaned up.

- Hand-derived counted-array allocation in `load_sprite_by_name` now
pulls the per-element size from `size_of::<LayerSpriteFrame>()`
via a local const, removing the bare `0x14` magic numbers from
the loops.

- New named constants: `MAX_FONT_ID = 31`,
`SPRITE_LOAD_ALREADY_DONE = 0x0080_0000`, plus a new
`is_valid_font_id` helper. Replaces seven scattered
`(1..=31).contains(&font_id)` and two `id & 0x80_0000` magic
checks across `load_font` / `load_font_extension` / `get_font_info`
/ `get_font_metric` / `set_font_param` / `draw_text_on_bitmap` /
`load_sprite` / `load_sprite_by_layer`.

- `set_camera_offset`'s vtable docstring updated to reference
`camera_x` / `camera_y` instead of the now-stale offsets
`+0x3560` / `+0x3564`.

No behavior changes — just type-system tightening. Headless replay
tests pass; one parallel run had a 2/15 spurious flake but the
serial re-run was 15/15.

crates/openwa-core/src/engine/ddgame_load_fonts.rs CHANGED
@@ -115,7 +115,7 @@ unsafe fn load_font_extension(wrapper: *mut DDGameWrapper, font_id: u32) {
115
  display,
116
  font_id as i32,
117
  path.as_ptr() as *const c_char,
118
- char_map.as_ptr(),
119
  palette_value,
120
  0,
121
  );
 
115
  display,
116
  font_id as i32,
117
  path.as_ptr() as *const c_char,
118
+ char_map.as_ptr() as *const c_char,
119
  palette_value,
120
  0,
121
  );
crates/openwa-core/src/render/display/context.rs CHANGED
@@ -187,15 +187,19 @@ pub struct RenderContextVtable {
187
  pub lock_surface_write: unsafe extern "fastcall" fn(
188
  this: *mut RenderContext,
189
  result: *mut FastcallResult,
190
- out_data: *mut u32,
191
  out_stride: *mut u32,
192
  ) -> *mut FastcallResult,
193
  /// unlock surface after writing (0x5A2C10 — same fn as slot 16, RET 0x4)
 
 
 
 
194
  #[slot(18)]
195
  pub unlock_surface_write: unsafe extern "fastcall" fn(
196
  this: *mut RenderContext,
197
  result: *mut FastcallResult,
198
- p2: u32,
199
  ) -> *mut FastcallResult,
200
  /// fill rectangle — lock, memset rows, unlock (0x5A25C0)
201
  ///
@@ -351,6 +355,18 @@ pub struct SurfaceVtable {
351
  this: *mut Surface,
352
  result: *mut FastcallResult,
353
  ) -> *mut FastcallResult,
 
 
 
 
 
 
 
 
 
 
 
 
354
  }
355
 
356
  /// Backend-specific surface object created by
 
187
  pub lock_surface_write: unsafe extern "fastcall" fn(
188
  this: *mut RenderContext,
189
  result: *mut FastcallResult,
190
+ out_data: *mut *mut u8,
191
  out_stride: *mut u32,
192
  ) -> *mut FastcallResult,
193
  /// unlock surface after writing (0x5A2C10 — same fn as slot 16, RET 0x4)
194
+ ///
195
+ /// `data` is the framebuffer pointer previously returned from
196
+ /// `lock_surface_write`. The original ignores it; we still pass it
197
+ /// through for fidelity.
198
  #[slot(18)]
199
  pub unlock_surface_write: unsafe extern "fastcall" fn(
200
  this: *mut RenderContext,
201
  result: *mut FastcallResult,
202
+ data: *mut u8,
203
  ) -> *mut FastcallResult,
204
  /// fill rectangle — lock, memset rows, unlock (0x5A25C0)
205
  ///
 
355
  this: *mut Surface,
356
  result: *mut FastcallResult,
357
  ) -> *mut FastcallResult,
358
+ /// set color-key (transparency) (slot 7).
359
+ ///
360
+ /// Used by `load_sprite_by_name` after `init_surface` to enable
361
+ /// transparency on the freshly-allocated frame surface (color key
362
+ /// `0`, flag `0x10`). The result code is unused by all known callers.
363
+ #[slot(7)]
364
+ pub set_color_key: unsafe extern "fastcall" fn(
365
+ this: *mut Surface,
366
+ result: *mut FastcallResult,
367
+ color_key: u32,
368
+ flags: u32,
369
+ ) -> *mut FastcallResult,
370
  }
371
 
372
  /// Backend-specific surface object created by
crates/openwa-core/src/render/display/destructor.rs CHANGED
@@ -211,14 +211,16 @@ unsafe fn free_layer_sprite_table(this: *mut DisplayGfx) {
211
  unsafe fn layer_sprite_frame_destructor(frame: *mut LayerSpriteFrame) {
212
  use crate::render::sprite::sprite::CBITMAP_VTABLE_MAYBE;
213
 
214
- (*frame).bitmap_vtable = rb(CBITMAP_VTABLE_MAYBE);
215
- let surface = (*frame).surface as *mut c_void;
216
  if !surface.is_null() {
217
  // surface->vtable[0](this, 1) — scalar deleting destructor.
218
- let vtable = *(surface as *const *const *const c_void);
 
 
219
  let dtor: unsafe extern "thiscall" fn(*mut c_void, u32) -> *mut c_void =
220
  core::mem::transmute(*vtable);
221
- dtor(surface, 1);
222
  }
223
  }
224
 
 
211
  unsafe fn layer_sprite_frame_destructor(frame: *mut LayerSpriteFrame) {
212
  use crate::render::sprite::sprite::CBITMAP_VTABLE_MAYBE;
213
 
214
+ (*frame).bitmap_vtable = rb(CBITMAP_VTABLE_MAYBE) as *const c_void;
215
+ let surface = (*frame).surface;
216
  if !surface.is_null() {
217
  // surface->vtable[0](this, 1) — scalar deleting destructor.
218
+ // Surface's vtable doesn't yet expose the destructor as a typed
219
+ // slot (slots 3-7 are typed; slot 0 is class-specific dtor).
220
+ let vtable = (*surface).vtable as *const usize;
221
  let dtor: unsafe extern "thiscall" fn(*mut c_void, u32) -> *mut c_void =
222
  core::mem::transmute(*vtable);
223
+ dtor(surface as *mut c_void, 1);
224
  }
225
  }
226
 
crates/openwa-core/src/render/display/font.rs CHANGED
@@ -398,7 +398,7 @@ pub unsafe fn font_extend(
398
  font_obj: *mut Font,
399
  palette_ctx: *mut PaletteContext,
400
  filename: *const c_char,
401
- char_map: *const u8,
402
  palette_value: u32,
403
  ) {
404
  use crate::address::va;
@@ -407,6 +407,8 @@ pub unsafe fn font_extend(
407
  use crate::wa_alloc::wa_malloc;
408
  use core::ffi::c_char;
409
 
 
 
410
  // Open the .fex file via WA's CRT _fopen.
411
  let fopen: unsafe extern "cdecl" fn(*const c_char, *const c_char) -> *mut u8 =
412
  core::mem::transmute(rb(va::WA_FOPEN) as usize);
 
398
  font_obj: *mut Font,
399
  palette_ctx: *mut PaletteContext,
400
  filename: *const c_char,
401
+ char_map: *const c_char,
402
  palette_value: u32,
403
  ) {
404
  use crate::address::va;
 
407
  use crate::wa_alloc::wa_malloc;
408
  use core::ffi::c_char;
409
 
410
+ let char_map = char_map as *const u8;
411
+
412
  // Open the .fex file via WA's CRT _fopen.
413
  let fopen: unsafe extern "cdecl" fn(*const c_char, *const c_char) -> *mut u8 =
414
  core::mem::transmute(rb(va::WA_FOPEN) as usize);
crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -255,7 +255,8 @@ pub struct DisplayGfxVtable {
255
  pub flush_render: fn(this: *mut DisplayGfx),
256
  /// set camera offset (0x56CC40, RET 0x8)
257
  ///
258
- /// Fixed-point input; internally `>> 16` to pixel integers stored at +0x3560/+0x3564.
 
259
  #[slot(27)]
260
  pub set_camera_offset: fn(this: *mut DisplayGfx, x: Fixed, y: Fixed),
261
  /// set clip rectangle (0x56CC60, RET 0x10)
@@ -315,7 +316,7 @@ pub struct DisplayGfxVtable {
315
  this: *mut DisplayGfx,
316
  font_id: i32,
317
  path: *const c_char,
318
- char_map: *const u8,
319
  palette_value: u32,
320
  flag: i32,
321
  ) -> u32,
@@ -448,7 +449,7 @@ pub unsafe extern "thiscall" fn set_clip_rect(
448
  unsafe fn flush_render_lock(gfx: *mut DisplayGfx) {
449
  if (*gfx).render_lock != 0 {
450
  let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
451
- let data = (*(*gfx).layer_0).data as u32;
452
  let mut buf = FastcallResult::default();
453
  RenderContext::unlock_surface_write_raw(wrapper, &mut buf, data);
454
  (*gfx).render_lock = 0;
@@ -469,7 +470,7 @@ pub unsafe fn acquire_render_lock(gfx: *mut DisplayGfx) {
469
  let fb_width = dims[0];
470
  let fb_height = dims[1];
471
 
472
- let mut data_ptr: u32 = 0;
473
  let mut stride: u32 = 0;
474
  RenderContext::lock_surface_write_raw(wrapper, &mut buf, &mut data_ptr, &mut stride);
475
 
@@ -477,7 +478,7 @@ pub unsafe fn acquire_render_lock(gfx: *mut DisplayGfx) {
477
  if (*layer).external_buffer != 0 {
478
  (*layer).width = fb_width;
479
  (*layer).height = fb_height;
480
- (*layer).data = data_ptr as *mut u8;
481
  (*layer).row_stride = stride;
482
  (*layer).clip_left = 0;
483
  (*layer).clip_top = 0;
@@ -760,12 +761,28 @@ pub unsafe extern "thiscall" fn is_sprite_loaded(this: *mut DisplayGfx, id: i32)
760
  }
761
  }
762
 
763
- /// Check if a sprite ID is in the valid range [1, 0x3FF].
 
 
 
 
 
 
 
 
 
 
764
  #[inline]
765
  fn is_valid_sprite_id(id: i32) -> bool {
766
  (1..=0x3FF).contains(&id)
767
  }
768
 
 
 
 
 
 
 
769
  /// Port of DisplayGfx::GetSpriteInfo (slot 6, 0x523500).
770
  ///
771
  /// Returns the static "sprite" string (0x664170) on success, or 0 if
@@ -1506,17 +1523,12 @@ pub unsafe extern "thiscall" fn update_palette(
1506
  unsafe fn palette_commit(gfx: *mut DisplayGfx) {
1507
  let dirty_min = (*gfx).palette_dirty_min;
1508
  let dirty_max = (*gfx).palette_dirty_max;
1509
- palette_commit_bridge(
1510
- gfx as *mut u8,
1511
- dirty_min,
1512
- dirty_max,
1513
- crate::rebase::rb(0x0056_CD20),
1514
- );
1515
  }
1516
 
1517
  #[unsafe(naked)]
1518
  unsafe extern "cdecl" fn palette_commit_bridge(
1519
- _gfx: *mut u8,
1520
  _dirty_min: u32,
1521
  _dirty_max: u32,
1522
  _target: u32,
@@ -1589,8 +1601,7 @@ pub unsafe fn load_sprite(
1589
  layer_ctx: *mut PaletteContext,
1590
  ) -> i32,
1591
  ) -> i32 {
1592
- // Bit 23 set = "already loaded" sentinel.
1593
- if id & 0x80_0000 != 0 {
1594
  return 1;
1595
  }
1596
 
@@ -1660,14 +1671,12 @@ pub unsafe fn load_sprite_by_name(
1660
  ) -> i32 {
1661
  use crate::address::va;
1662
  use crate::rebase::rb;
1663
- use crate::render::display::context::{FastcallResult, RenderContext};
1664
  use crate::render::palette::{palette_map_color, remap_pixels_through_lut};
1665
  use crate::render::sprite::gfx_dir::{call_gfx_load_image, GfxDirStream};
1666
 
1667
  use crate::wa_alloc::wa_malloc;
1668
 
1669
- let sp = sprite as *mut u8;
1670
-
1671
  // Copy name into sprite.name (max 0x4F chars + null terminator).
1672
  let name_dest = (*sprite).name.as_mut_ptr();
1673
  let mut i = 0usize;
@@ -1732,38 +1741,38 @@ pub unsafe fn load_sprite_by_name(
1732
 
1733
  // Sprite metadata fields, in the original's read order
1734
  // (field_60 before flags/cell_width/cell_height).
1735
- GfxDirStream::read_raw(stream, sp.add(0x60), 4);
1736
- GfxDirStream::read_raw(stream, sp.add(0x64), 2);
1737
- GfxDirStream::read_raw(stream, sp.add(0x68), 2);
1738
- GfxDirStream::read_raw(stream, sp.add(0x6A), 2);
1739
 
1740
  (*sprite).frame_count = 0;
1741
- GfxDirStream::read_raw(stream, sp.add(0x66), 2);
1742
  let frame_count = (*sprite).frame_count as usize;
1743
 
1744
- // Counted LayerSpriteFrame array: 0x14 bytes per element with a
1745
- // 4-byte count prefix at [-4]. Saturate the allocation size on
1746
  // overflow to match the original's checked-mul behavior.
1747
- let total_elems = frame_count;
1748
- let checked_count = total_elems as u32;
1749
- let checked_size = checked_count.checked_mul(0x14).unwrap_or(u32::MAX);
1750
  let checked_alloc = checked_size.checked_add(4).unwrap_or(u32::MAX);
1751
 
1752
  let array_base = wa_malloc(checked_alloc);
1753
- let frame_array = if !array_base.is_null() {
1754
  *(array_base as *mut u32) = checked_count;
1755
- let arr = array_base.add(4);
1756
- let bitmap_vtable = rb(0x00643F64) as u32; // CBitmap vtable
1757
- for j in 0..total_elems {
1758
- let elem = arr.add(j * 0x14);
1759
- *(elem.add(0x08) as *mut u32) = bitmap_vtable;
1760
- *(elem.add(0x0C) as *mut u32) = 0;
1761
  }
1762
  arr
1763
  } else {
1764
  core::ptr::null_mut()
1765
  };
1766
- (*sprite).frame_array = frame_array as *mut LayerSpriteFrame;
1767
 
1768
  // Skip alignment padding: while (remaining() & 3) != 0, read 1 byte.
1769
  loop {
@@ -1778,13 +1787,13 @@ pub unsafe fn load_sprite_by_name(
1778
  // Frame headers: 4-byte discarded prefix, then start_x/y, end_x/y.
1779
  if frame_count > 0 && !frame_array.is_null() {
1780
  for j in 0..frame_count {
1781
- let elem = frame_array.add(j * 0x14);
1782
  let mut frame_hdr = [0u8; 4];
1783
  GfxDirStream::read_raw(stream, frame_hdr.as_mut_ptr(), 4);
1784
- GfxDirStream::read_raw(stream, elem, 2);
1785
- GfxDirStream::read_raw(stream, elem.add(2), 2);
1786
- GfxDirStream::read_raw(stream, elem.add(4), 2);
1787
- GfxDirStream::read_raw(stream, elem.add(6), 2);
1788
  }
1789
  }
1790
 
@@ -1793,14 +1802,9 @@ pub unsafe fn load_sprite_by_name(
1793
  let render_ctx = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
1794
 
1795
  for j in 0..frame_count {
1796
- let elem = frame_array.add(j * 0x14);
1797
- let start_x = *(elem as *const i16) as i32;
1798
- let start_y = *(elem.add(2) as *const i16) as i32;
1799
- let end_x = *(elem.add(4) as *const i16) as i32;
1800
- let end_y = *(elem.add(6) as *const i16) as i32;
1801
-
1802
- let width = end_x - start_x;
1803
- let height = end_y - start_y;
1804
 
1805
  if width * height == 0 {
1806
  continue;
@@ -1808,84 +1812,41 @@ pub unsafe fn load_sprite_by_name(
1808
 
1809
  // alloc_surface returns the surface pointer in EAX, NOT via
1810
  // the FastcallResult buffer — see feedback_alloc_surface_return.md.
1811
- let surface_ptr = elem.add(0x0C) as *mut u32;
1812
- if *surface_ptr == 0 {
1813
  let mut buf = FastcallResult::default();
1814
  let ret = RenderContext::alloc_surface_raw(render_ctx, &mut buf);
1815
- *surface_ptr = ret as u32;
1816
  }
1817
- let surface = *surface_ptr as *mut u8;
1818
  if surface.is_null() {
1819
  continue;
1820
  }
1821
 
1822
- // surface->vtable[5](width, height, 0) — init storage.
1823
- {
1824
- let vt = *(surface as *const *const u32);
1825
- let init_fn: unsafe extern "fastcall" fn(
1826
- *mut u8,
1827
- *mut FastcallResult,
1828
- u32,
1829
- u32,
1830
- u32,
1831
- ) = core::mem::transmute(*vt.add(5));
1832
- let mut buf = FastcallResult::default();
1833
- init_fn(surface, &mut buf, width as u32, height as u32, 0);
1834
- }
1835
-
1836
- // surface->vtable[7](0, 0x10) — SetColorKey.
1837
- {
1838
- let vt = *(surface as *const *const u32);
1839
- let set_ck_fn: unsafe extern "fastcall" fn(
1840
- *mut u8,
1841
- *mut FastcallResult,
1842
- u32,
1843
- u32,
1844
- ) = core::mem::transmute(*vt.add(7));
1845
- let mut buf = FastcallResult::default();
1846
- set_ck_fn(surface, &mut buf, 0, 0x10);
1847
- }
1848
 
1849
- // surface->vtable[3](&out_data, &out_pitch) — Lock.
1850
- let mut data_ptr: u32 = 0;
1851
- let mut pitch: u32 = 0;
1852
- {
1853
- let vt = *(surface as *const *const u32);
1854
- let lock_fn: unsafe extern "fastcall" fn(
1855
- *mut u8,
1856
- *mut FastcallResult,
1857
- *mut u32,
1858
- *mut u32,
1859
- ) = core::mem::transmute(*vt.add(3));
1860
- let mut buf = FastcallResult::default();
1861
- lock_fn(surface, &mut buf, &mut data_ptr, &mut pitch);
1862
- }
1863
 
1864
- if data_ptr != 0 && pitch != 0 {
1865
- let data = data_ptr as *mut u8;
1866
  for row in 0..height {
1867
- let row_dest = data.add((row as u32 * pitch) as usize);
1868
  GfxDirStream::read_raw(stream, row_dest, width as u32);
1869
  }
1870
 
1871
  let width_dwords = ((width as u32) + 3) / 4;
1872
  remap_pixels_through_lut(
1873
- data,
1874
- pitch,
1875
  palette_lut.as_ptr(),
1876
  width_dwords,
1877
  height as u32,
1878
  );
1879
  }
1880
 
1881
- // surface->vtable[4](data_ptr) — Unlock.
1882
- {
1883
- let vt = *(surface as *const *const u32);
1884
- let unlock_fn: unsafe extern "fastcall" fn(*mut u8, *mut FastcallResult, u32) =
1885
- core::mem::transmute(*vt.add(4));
1886
- let mut buf = FastcallResult::default();
1887
- unlock_fn(surface, &mut buf, data_ptr);
1888
- }
1889
  }
1890
  }
1891
  }
@@ -1898,18 +1859,21 @@ pub unsafe fn load_sprite_by_name(
1898
  pub unsafe fn free_layer_sprite(sprite: *mut LayerSprite) {
1899
  use crate::wa_alloc::wa_free;
1900
 
1901
- let frame_array = (*sprite).frame_array as *mut u8;
1902
  if !frame_array.is_null() {
 
1903
  let count_ptr = (frame_array as *mut u32).sub(1);
1904
  let count = *count_ptr as usize;
1905
 
1906
  // Reverse-order destruction matches eh_vector_destructor_iterator.
1907
  for i in (0..count).rev() {
1908
- let elem = frame_array.add(i * 0x14);
1909
- let surface = *(elem.add(0x0C) as *const u32);
1910
- if surface != 0 {
1911
- let vt = *(surface as *const *const u32);
1912
- let dtor: unsafe extern "thiscall" fn(u32, u32) = core::mem::transmute(*vt);
 
 
1913
  dtor(surface, 1);
1914
  }
1915
  }
@@ -1934,8 +1898,7 @@ pub unsafe fn load_sprite_by_layer(
1934
  ) -> i32 {
1935
  use crate::wa_alloc::wa_malloc_zeroed;
1936
 
1937
- // Bit 23 set = "already loaded" sentinel.
1938
- if id & 0x80_0000 != 0 {
1939
  return 1;
1940
  }
1941
 
@@ -1999,7 +1962,7 @@ pub unsafe fn load_font(
1999
  return 0;
2000
  }
2001
 
2002
- if !(1..=31).contains(&font_id) {
2003
  return 0;
2004
  }
2005
  if !base.font_table[font_id as usize].is_null() {
@@ -2029,13 +1992,13 @@ pub unsafe fn load_font_extension(
2029
  this: *mut DisplayGfx,
2030
  font_id: i32,
2031
  path: *const c_char,
2032
- char_map: *const u8,
2033
  palette_value: u32,
2034
  _flag: i32,
2035
  ) -> u32 {
2036
  use crate::render::palette::palette_context_lookup_entry;
2037
 
2038
- if !(1..=31).contains(&font_id) {
2039
  return 0;
2040
  }
2041
  let base = &mut (*this).base;
@@ -2074,7 +2037,7 @@ pub unsafe extern "thiscall" fn get_font_info(
2074
  out_1: *mut u32,
2075
  out_2: *mut u32,
2076
  ) -> u32 {
2077
- if !(1..=31).contains(&font_id) {
2078
  return 0;
2079
  }
2080
  let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
@@ -2095,7 +2058,7 @@ pub unsafe extern "thiscall" fn get_font_metric(
2095
  out_1: *mut u32,
2096
  out_2: *mut u32,
2097
  ) -> u32 {
2098
- if !(1..=31).contains(&font_id) {
2099
  return 0;
2100
  }
2101
  let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
@@ -2121,7 +2084,7 @@ pub unsafe extern "thiscall" fn set_font_param(
2121
  p4: u32,
2122
  p5: u32,
2123
  ) -> u32 {
2124
- if !(1..=31).contains(&font_id) {
2125
  return 0;
2126
  }
2127
  let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
@@ -2198,11 +2161,12 @@ pub unsafe fn font_draw_text_impl(
2198
  bitmap: *const BitGrid,
2199
  pen_x: i32,
2200
  pen_y: i32,
2201
- msg: *const u8,
2202
  out_pen_x: *mut i32,
2203
  out_width: *mut i32,
2204
  font_id_high: i32,
2205
  ) -> i32 {
 
2206
  let font = &*font_obj;
2207
  let font_width = font.width as i16 as i32;
2208
 
@@ -2339,8 +2303,8 @@ pub unsafe extern "thiscall" fn draw_text_on_bitmap(
2339
  out_pen_x: *mut i32,
2340
  out_width: *mut i32,
2341
  ) -> i32 {
2342
- let font_id_low = (font_id as u32) & 0xFFFF;
2343
- if !(1..=31).contains(&font_id_low) {
2344
  return 0;
2345
  }
2346
  let font_obj = (*this).base.font_table[font_id_low as usize] as *const Font;
@@ -2353,7 +2317,7 @@ pub unsafe extern "thiscall" fn draw_text_on_bitmap(
2353
  bitmap as *const BitGrid,
2354
  pen_x,
2355
  pen_y,
2356
- msg as *const u8,
2357
  out_pen_x,
2358
  out_width,
2359
  font_id_high,
 
255
  pub flush_render: fn(this: *mut DisplayGfx),
256
  /// set camera offset (0x56CC40, RET 0x8)
257
  ///
258
+ /// Fixed-point input; internally `>> 16` to pixel integers stored
259
+ /// in `DisplayGfx::camera_x` / `camera_y`.
260
  #[slot(27)]
261
  pub set_camera_offset: fn(this: *mut DisplayGfx, x: Fixed, y: Fixed),
262
  /// set clip rectangle (0x56CC60, RET 0x10)
 
316
  this: *mut DisplayGfx,
317
  font_id: i32,
318
  path: *const c_char,
319
+ char_map: *const c_char,
320
  palette_value: u32,
321
  flag: i32,
322
  ) -> u32,
 
449
  unsafe fn flush_render_lock(gfx: *mut DisplayGfx) {
450
  if (*gfx).render_lock != 0 {
451
  let wrapper = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
452
+ let data = (*(*gfx).layer_0).data;
453
  let mut buf = FastcallResult::default();
454
  RenderContext::unlock_surface_write_raw(wrapper, &mut buf, data);
455
  (*gfx).render_lock = 0;
 
470
  let fb_width = dims[0];
471
  let fb_height = dims[1];
472
 
473
+ let mut data_ptr: *mut u8 = core::ptr::null_mut();
474
  let mut stride: u32 = 0;
475
  RenderContext::lock_surface_write_raw(wrapper, &mut buf, &mut data_ptr, &mut stride);
476
 
 
478
  if (*layer).external_buffer != 0 {
479
  (*layer).width = fb_width;
480
  (*layer).height = fb_height;
481
+ (*layer).data = data_ptr;
482
  (*layer).row_stride = stride;
483
  (*layer).clip_left = 0;
484
  (*layer).clip_top = 0;
 
761
  }
762
  }
763
 
764
+ /// Maximum valid font slot index — `font_table` has 32 entries, slot 0
765
+ /// is reserved (the original validates `1..=31` everywhere).
766
+ pub const MAX_FONT_ID: i32 = 31;
767
+
768
+ /// Sentinel bit on the `id` parameter of `load_sprite` / `load_sprite_by_layer`
769
+ /// meaning "already loaded — return success without doing any work".
770
+ const SPRITE_LOAD_ALREADY_DONE: u32 = 0x0080_0000;
771
+
772
+ /// Check if a sprite ID is in the valid range `[1, 0x3FF]`. Note that
773
+ /// the slot 33 dispatcher uses a slightly tighter bound (`1..=0x3FE`)
774
+ /// matching the original — see `get_sprite_frame_for_blit`.
775
  #[inline]
776
  fn is_valid_sprite_id(id: i32) -> bool {
777
  (1..=0x3FF).contains(&id)
778
  }
779
 
780
+ /// Check if `font_id` is a valid 1-based index into `font_table`.
781
+ #[inline]
782
+ fn is_valid_font_id(id: i32) -> bool {
783
+ (1..=MAX_FONT_ID).contains(&id)
784
+ }
785
+
786
  /// Port of DisplayGfx::GetSpriteInfo (slot 6, 0x523500).
787
  ///
788
  /// Returns the static "sprite" string (0x664170) on success, or 0 if
 
1523
  unsafe fn palette_commit(gfx: *mut DisplayGfx) {
1524
  let dirty_min = (*gfx).palette_dirty_min;
1525
  let dirty_max = (*gfx).palette_dirty_max;
1526
+ palette_commit_bridge(gfx, dirty_min, dirty_max, crate::rebase::rb(0x0056_CD20));
 
 
 
 
 
1527
  }
1528
 
1529
  #[unsafe(naked)]
1530
  unsafe extern "cdecl" fn palette_commit_bridge(
1531
+ _gfx: *mut DisplayGfx,
1532
  _dirty_min: u32,
1533
  _dirty_max: u32,
1534
  _target: u32,
 
1601
  layer_ctx: *mut PaletteContext,
1602
  ) -> i32,
1603
  ) -> i32 {
1604
+ if id & SPRITE_LOAD_ALREADY_DONE != 0 {
 
1605
  return 1;
1606
  }
1607
 
 
1671
  ) -> i32 {
1672
  use crate::address::va;
1673
  use crate::rebase::rb;
1674
+ use crate::render::display::context::{FastcallResult, RenderContext, Surface};
1675
  use crate::render::palette::{palette_map_color, remap_pixels_through_lut};
1676
  use crate::render::sprite::gfx_dir::{call_gfx_load_image, GfxDirStream};
1677
 
1678
  use crate::wa_alloc::wa_malloc;
1679
 
 
 
1680
  // Copy name into sprite.name (max 0x4F chars + null terminator).
1681
  let name_dest = (*sprite).name.as_mut_ptr();
1682
  let mut i = 0usize;
 
1741
 
1742
  // Sprite metadata fields, in the original's read order
1743
  // (field_60 before flags/cell_width/cell_height).
1744
+ GfxDirStream::read_raw(stream, &raw mut (*sprite).field_60 as *mut u8, 4);
1745
+ GfxDirStream::read_raw(stream, &raw mut (*sprite).flags as *mut u8, 2);
1746
+ GfxDirStream::read_raw(stream, &raw mut (*sprite).cell_width as *mut u8, 2);
1747
+ GfxDirStream::read_raw(stream, &raw mut (*sprite).cell_height as *mut u8, 2);
1748
 
1749
  (*sprite).frame_count = 0;
1750
+ GfxDirStream::read_raw(stream, &raw mut (*sprite).frame_count as *mut u8, 2);
1751
  let frame_count = (*sprite).frame_count as usize;
1752
 
1753
+ // Counted `LayerSpriteFrame` array: `count * 0x14` bytes plus a
1754
+ // 4-byte count prefix at `[-4]`. Saturate the allocation size on
1755
  // overflow to match the original's checked-mul behavior.
1756
+ const LSF_SIZE: u32 = core::mem::size_of::<LayerSpriteFrame>() as u32;
1757
+ let checked_count = frame_count as u32;
1758
+ let checked_size = checked_count.checked_mul(LSF_SIZE).unwrap_or(u32::MAX);
1759
  let checked_alloc = checked_size.checked_add(4).unwrap_or(u32::MAX);
1760
 
1761
  let array_base = wa_malloc(checked_alloc);
1762
+ let frame_array: *mut LayerSpriteFrame = if !array_base.is_null() {
1763
  *(array_base as *mut u32) = checked_count;
1764
+ let arr = array_base.add(4) as *mut LayerSpriteFrame;
1765
+ let cbitmap_vt = rb(va::CBITMAP_VTABLE_MAYBE) as *const core::ffi::c_void;
1766
+ for j in 0..frame_count {
1767
+ let elem = arr.add(j);
1768
+ (*elem).bitmap_vtable = cbitmap_vt;
1769
+ (*elem).surface = core::ptr::null_mut();
1770
  }
1771
  arr
1772
  } else {
1773
  core::ptr::null_mut()
1774
  };
1775
+ (*sprite).frame_array = frame_array;
1776
 
1777
  // Skip alignment padding: while (remaining() & 3) != 0, read 1 byte.
1778
  loop {
 
1787
  // Frame headers: 4-byte discarded prefix, then start_x/y, end_x/y.
1788
  if frame_count > 0 && !frame_array.is_null() {
1789
  for j in 0..frame_count {
1790
+ let frame = frame_array.add(j);
1791
  let mut frame_hdr = [0u8; 4];
1792
  GfxDirStream::read_raw(stream, frame_hdr.as_mut_ptr(), 4);
1793
+ GfxDirStream::read_raw(stream, &raw mut (*frame).start_x as *mut u8, 2);
1794
+ GfxDirStream::read_raw(stream, &raw mut (*frame).start_y as *mut u8, 2);
1795
+ GfxDirStream::read_raw(stream, &raw mut (*frame).end_x as *mut u8, 2);
1796
+ GfxDirStream::read_raw(stream, &raw mut (*frame).end_y as *mut u8, 2);
1797
  }
1798
  }
1799
 
 
1802
  let render_ctx = *(rb(va::G_RENDER_CONTEXT) as *const *mut RenderContext);
1803
 
1804
  for j in 0..frame_count {
1805
+ let frame = frame_array.add(j);
1806
+ let width = ((*frame).end_x - (*frame).start_x) as i32;
1807
+ let height = ((*frame).end_y - (*frame).start_y) as i32;
 
 
 
 
 
1808
 
1809
  if width * height == 0 {
1810
  continue;
 
1812
 
1813
  // alloc_surface returns the surface pointer in EAX, NOT via
1814
  // the FastcallResult buffer — see feedback_alloc_surface_return.md.
1815
+ if (*frame).surface.is_null() {
 
1816
  let mut buf = FastcallResult::default();
1817
  let ret = RenderContext::alloc_surface_raw(render_ctx, &mut buf);
1818
+ (*frame).surface = ret as *mut Surface;
1819
  }
1820
+ let surface = (*frame).surface;
1821
  if surface.is_null() {
1822
  continue;
1823
  }
1824
 
1825
+ let mut buf = FastcallResult::default();
1826
+ Surface::init_surface_raw(surface, &mut buf, width, height, 0);
1827
+ Surface::set_color_key_raw(surface, &mut buf, 0, 0x10);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1828
 
1829
+ let mut data_ptr: *mut u8 = core::ptr::null_mut();
1830
+ let mut pitch: i32 = 0;
1831
+ Surface::lock_surface_raw(surface, &mut buf, &mut data_ptr, &mut pitch);
 
 
 
 
 
 
 
 
 
 
 
1832
 
1833
+ if !data_ptr.is_null() && pitch != 0 {
 
1834
  for row in 0..height {
1835
+ let row_dest = data_ptr.add((row * pitch) as usize);
1836
  GfxDirStream::read_raw(stream, row_dest, width as u32);
1837
  }
1838
 
1839
  let width_dwords = ((width as u32) + 3) / 4;
1840
  remap_pixels_through_lut(
1841
+ data_ptr,
1842
+ pitch as u32,
1843
  palette_lut.as_ptr(),
1844
  width_dwords,
1845
  height as u32,
1846
  );
1847
  }
1848
 
1849
+ Surface::unlock_surface_raw(surface, &mut buf, data_ptr);
 
 
 
 
 
 
 
1850
  }
1851
  }
1852
  }
 
1859
  pub unsafe fn free_layer_sprite(sprite: *mut LayerSprite) {
1860
  use crate::wa_alloc::wa_free;
1861
 
1862
+ let frame_array = (*sprite).frame_array;
1863
  if !frame_array.is_null() {
1864
+ // Counted array: count lives at `frame_array[-4]`.
1865
  let count_ptr = (frame_array as *mut u32).sub(1);
1866
  let count = *count_ptr as usize;
1867
 
1868
  // Reverse-order destruction matches eh_vector_destructor_iterator.
1869
  for i in (0..count).rev() {
1870
+ let frame = frame_array.add(i);
1871
+ let surface = (*frame).surface;
1872
+ if !surface.is_null() {
1873
+ // Surface destructor is vtable[0]; not yet a typed slot.
1874
+ let vt = (*surface).vtable as *const usize;
1875
+ let dtor: unsafe extern "thiscall" fn(*mut Surface, u32) =
1876
+ core::mem::transmute(*vt);
1877
  dtor(surface, 1);
1878
  }
1879
  }
 
1898
  ) -> i32 {
1899
  use crate::wa_alloc::wa_malloc_zeroed;
1900
 
1901
+ if id & SPRITE_LOAD_ALREADY_DONE != 0 {
 
1902
  return 1;
1903
  }
1904
 
 
1962
  return 0;
1963
  }
1964
 
1965
+ if !is_valid_font_id(font_id) {
1966
  return 0;
1967
  }
1968
  if !base.font_table[font_id as usize].is_null() {
 
1992
  this: *mut DisplayGfx,
1993
  font_id: i32,
1994
  path: *const c_char,
1995
+ char_map: *const c_char,
1996
  palette_value: u32,
1997
  _flag: i32,
1998
  ) -> u32 {
1999
  use crate::render::palette::palette_context_lookup_entry;
2000
 
2001
+ if !is_valid_font_id(font_id) {
2002
  return 0;
2003
  }
2004
  let base = &mut (*this).base;
 
2037
  out_1: *mut u32,
2038
  out_2: *mut u32,
2039
  ) -> u32 {
2040
+ if !is_valid_font_id(font_id) {
2041
  return 0;
2042
  }
2043
  let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
 
2058
  out_1: *mut u32,
2059
  out_2: *mut u32,
2060
  ) -> u32 {
2061
+ if !is_valid_font_id(font_id) {
2062
  return 0;
2063
  }
2064
  let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
 
2084
  p4: u32,
2085
  p5: u32,
2086
  ) -> u32 {
2087
+ if !is_valid_font_id(font_id) {
2088
  return 0;
2089
  }
2090
  let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
 
2161
  bitmap: *const BitGrid,
2162
  pen_x: i32,
2163
  pen_y: i32,
2164
+ msg: *const c_char,
2165
  out_pen_x: *mut i32,
2166
  out_width: *mut i32,
2167
  font_id_high: i32,
2168
  ) -> i32 {
2169
+ let msg = msg as *const u8;
2170
  let font = &*font_obj;
2171
  let font_width = font.width as i16 as i32;
2172
 
 
2303
  out_pen_x: *mut i32,
2304
  out_width: *mut i32,
2305
  ) -> i32 {
2306
+ let font_id_low = font_id & 0xFFFF;
2307
+ if !is_valid_font_id(font_id_low) {
2308
  return 0;
2309
  }
2310
  let font_obj = (*this).base.font_table[font_id_low as usize] as *const Font;
 
2317
  bitmap as *const BitGrid,
2318
  pen_x,
2319
  pen_y,
2320
+ msg,
2321
  out_pen_x,
2322
  out_width,
2323
  font_id_high,
crates/openwa-core/src/render/sprite/sprite.rs CHANGED
@@ -473,10 +473,10 @@ pub struct LayerSpriteFrame {
473
  pub end_x: i16,
474
  /// 0x06: Frame end Y coordinate.
475
  pub end_y: i16,
476
- /// 0x08: CBitmap vtable pointer (set by constructor).
477
- pub bitmap_vtable: u32,
478
- /// 0x0C: Surface object pointer (created lazily by alloc_surface).
479
- pub surface: u32,
480
  /// 0x10: Reserved/padding.
481
  pub _pad_10: u32,
482
  }
 
473
  pub end_x: i16,
474
  /// 0x06: Frame end Y coordinate.
475
  pub end_y: i16,
476
+ /// 0x08: CBitmap vtable pointer (`0x643F64`, set by constructor).
477
+ pub bitmap_vtable: *const core::ffi::c_void,
478
+ /// 0x0C: Backend surface object pointer (lazy via `alloc_surface`).
479
+ pub surface: *mut crate::render::display::context::Surface,
480
  /// 0x10: Reserved/padding.
481
  pub _pad_10: u32,
482
  }
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -1284,7 +1284,7 @@ unsafe extern "thiscall" fn load_font_extension(
1284
  this: *mut DisplayGfx,
1285
  font_id: i32,
1286
  path: *const c_char,
1287
- char_map: *const u8,
1288
  palette_value: u32,
1289
  flag: i32,
1290
  ) -> u32 {
 
1284
  this: *mut DisplayGfx,
1285
  font_id: i32,
1286
  path: *const c_char,
1287
+ char_map: *const c_char,
1288
  palette_value: u32,
1289
  flag: i32,
1290
  ) -> u32 {