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

refactor: extract font module, type GfxDir pointers, rename DisplayVtable

Browse files

Three concurrent cleanups bundled together:

- Extract the font implementation (~830 lines) from display/vtable.rs
into a new display/font.rs module: Font struct (renamed from
FontObject), font_extend, font_set_palette_impl, font_get_info_impl,
font_get_metric_impl, font_set_param_impl, font_load_from_gfx, and
GlyphEntry. vtable.rs is now 870 lines lighter and only contains
vtable definitions and slot wrappers.

- Replace `*mut u8` with typed `*mut GfxDir` across the GfxDir call
graph: LayerSprite.gfx_dir, PCLandscape.{level,water}_gfx_dir,
DDGameWrapper.{primary,secondary}_gfx_dir, slot 31 load_sprite,
slot 34 load_font, slot 37 load_sprite_by_layer,
gfx_dir_find_entry, gfx_resource_create, call_gfx_find_and_load,
call_gfx_load_and_wrap, call_gfx_load_image, plus the matching DLL
trampolines and the inner transmuted vtable[2] cached-load function
pointer in three places. Also renames the misleading local names
`land_layer`/`water_layer` to `land_dir`/`water_dir` in
ddgame_constructor and gradient.

- Rename `DisplayVtable` -> `DisplayGfxVtable` (and the companion
`bind_DisplayVtable!` -> `bind_DisplayGfxVtable!`) to match the
class name everywhere else uses. Cascades through gfx.rs,
display/mod.rs, render/mod.rs, and the install_display
vtable_replace! invocation.

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

crates/openwa-core/src/engine/ddgame_constructor.rs CHANGED
@@ -310,8 +310,8 @@ unsafe fn wa_init_display_final(display: *mut DisplayGfx) {
310
  /// DDGame__LoadHudAndWeaponSprites (0x53D0E0): load weapon icons and HUD sprites.
311
  /// thiscall(ECX=gfx_dir) + 2 stack(ddgame, secondary_gfx_dir), RET 0x8.
312
  #[cfg(target_arch = "x86")]
313
- unsafe fn wa_load_hud_sprites(gfx_dir: *mut u8, ddgame: *mut DDGame, secondary: *mut u8) {
314
- let f: unsafe extern "thiscall" fn(*mut u8, *mut DDGame, *mut u8) =
315
  core::mem::transmute(rb(va::DDGAME_LOAD_HUD_AND_WEAPON_SPRITES) as usize);
316
  f(gfx_dir, ddgame, secondary);
317
  }
@@ -436,7 +436,7 @@ unsafe fn init_graphics_and_resources(
436
 
437
  // ── GfxDir #1 (primary) ──
438
  let gfx1 = GfxDir::alloc(gfx_dir_vtable);
439
- (*wrapper).primary_gfx_dir = gfx1 as *mut u8;
440
  (*wrapper).secondary_gfx_dir = core::ptr::null_mut();
441
 
442
  // Build path list (order depends on headless + display_flags)
@@ -499,16 +499,18 @@ unsafe fn init_graphics_and_resources(
499
  let game_version = (*game_info).game_version;
500
  let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 };
501
  if game_version < threshold {
 
 
502
  let c_digit = if game_version > -3 { b'2' } else { b'1' };
503
  let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
504
  gfx_c_path[14] = c_digit;
505
 
506
  let gfx2 = GfxDir::alloc(gfx_dir_vtable);
507
- (*wrapper).secondary_gfx_dir = gfx2 as *mut u8;
508
 
509
  let fp = fopen(gfx_c_path.as_ptr().cast(), c"rb".as_ptr());
510
  (*gfx2).file_handle = fp;
511
- let load_addr = crate::render::sprite::gfx_dir::gfx_load_dir_addr();
512
  if fp.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
513
  let fp2 = fopen(c"data\\Gfx\\Gfx.dir".as_ptr(), c"rb".as_ptr());
514
  (*gfx2).file_handle = fp2;
@@ -772,7 +774,7 @@ unsafe fn init_graphics_and_resources(
772
  if !entry.is_null() {
773
  // Try gfx_dir->vtable[2](entry->field_4)
774
  let gfx_vt = *(gfx_dir as *const *const u32);
775
- let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
776
  core::mem::transmute(*gfx_vt.add(2));
777
  let entry_val = *(entry.add(4) as *const u32);
778
  let cached = load_cached(gfx_dir, entry_val);
@@ -823,8 +825,10 @@ unsafe fn init_graphics_and_resources(
823
 
824
  // Arrow GfxDir (conditional on secondary gfxdir)
825
  if !(*ddgame).secondary_gfxdir.is_null() {
826
- let gfx_resource_create: unsafe extern "thiscall" fn(*mut u8, *mut u8) -> *mut u8 =
827
- core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
 
 
828
  (*ddgame).arrow_gfxdirs[i as usize] =
829
  gfx_resource_create(gfx_dir, core::ptr::null_mut());
830
  }
@@ -892,15 +896,15 @@ unsafe fn init_graphics_and_resources(
892
  // 5 stack params (layer, gfx_dir, base_path, data_table, table_size)
893
  {
894
  let landscape_ptr = (*wrapper).landscape;
895
- let water_layer = (*landscape_ptr).water_gfx_dir;
896
- let land_layer = (*landscape_ptr).level_gfx_dir;
897
  let gfx_dir = (*wrapper).primary_gfx_dir;
898
 
899
  let wrapper_vt = *(wrapper as *const *const u32);
900
  let load_resource_list: unsafe extern "thiscall" fn(
901
  *mut DDGameWrapper,
902
  u32,
903
- *mut u8,
904
  *const u8,
905
  *const u8,
906
  u32,
@@ -932,7 +936,7 @@ unsafe fn init_graphics_and_resources(
932
  load_resource_list(
933
  wrapper,
934
  2,
935
- water_layer,
936
  rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8,
937
  rb(va::WATER_RESOURCE_TABLE) as *const u8,
938
  0x2F4,
@@ -945,17 +949,17 @@ unsafe fn init_graphics_and_resources(
945
  // GenerateDebrisParticles (0x546F70) for particle effects, which affects
946
  // the game RNG (DDGame+0x45EC). The original constructor loads them even
947
  // in headless mode. Skipping them causes replay desync.
948
- (*disp).load_sprite_by_layer(3, 0x26D, land_layer, c"back.spr".as_ptr().cast());
949
  // debris.spr must be loaded unconditionally β€” it's used by
950
  // GenerateDebrisParticles (0x546F70) for particle effects, which
951
  // affects the game RNG (DDGame+0x45EC). Skipping it in headless
952
  // mode causes desync (longbow replay checksum mismatch at frame 1350).
953
- (*disp).load_sprite(3, 0x26E, 0, land_layer, c"debris.spr".as_ptr());
954
 
955
  (*disp).load_sprite_by_layer(
956
  2,
957
  0x26C,
958
- water_layer,
959
  c"layer\\layer.spr|layer.spr".as_ptr().cast(),
960
  );
961
 
@@ -968,10 +972,10 @@ unsafe fn init_graphics_and_resources(
968
 
969
  if s_var1 < 0x61 && level_height == 0x2B8 {
970
  // Simple gradient: load gradient.img directly
971
- let gradient = call_gfx_find_and_load(land_layer, c"gradient.img", layer3_ctx);
972
  (*ddgame).gradient_image = gradient;
973
  } else {
974
- compute_complex_gradient(ddgame, land_layer, layer3_ctx, s_var1);
975
  }
976
 
977
  // ── Fill image β†’ fill_pixel (0x7338) ──
@@ -979,7 +983,7 @@ unsafe fn init_graphics_and_resources(
979
  let layer2_ctx = (*(*ddgame).display).set_active_layer(2);
980
  // In the original, fill.img uses piStack_126c which the decompiler
981
  // shows was set from piVar3 (water_layer from landscape+0xB38).
982
- let fill_sprite = call_gfx_find_and_load(water_layer, c"fill.img", layer2_ctx);
983
  if !fill_sprite.is_null() {
984
  // Get pixel value: fill_sprite->vtable[4](0, 0)
985
  let fill_vt = *(fill_sprite as *const *const u32);
@@ -1023,12 +1027,12 @@ unsafe fn init_graphics_and_resources(
1023
  }
1024
 
1025
  // ── Release primary GfxHandler (vtable[3] = release, param 1 = free) ──
1026
- let gfx_dir_4c0 = (*wrapper).primary_gfx_dir;
1027
- if !gfx_dir_4c0.is_null() {
1028
- let gfx_vt = *(gfx_dir_4c0 as *const *const u32);
1029
- let release: unsafe extern "thiscall" fn(*mut u8, u32) =
1030
  core::mem::transmute(*gfx_vt.add(3));
1031
- release(gfx_dir_4c0, 1);
1032
  }
1033
 
1034
  if !is_headless {
 
310
  /// DDGame__LoadHudAndWeaponSprites (0x53D0E0): load weapon icons and HUD sprites.
311
  /// thiscall(ECX=gfx_dir) + 2 stack(ddgame, secondary_gfx_dir), RET 0x8.
312
  #[cfg(target_arch = "x86")]
313
+ unsafe fn wa_load_hud_sprites(gfx_dir: *mut GfxDir, ddgame: *mut DDGame, secondary: *mut GfxDir) {
314
+ let f: unsafe extern "thiscall" fn(*mut GfxDir, *mut DDGame, *mut GfxDir) =
315
  core::mem::transmute(rb(va::DDGAME_LOAD_HUD_AND_WEAPON_SPRITES) as usize);
316
  f(gfx_dir, ddgame, secondary);
317
  }
 
436
 
437
  // ── GfxDir #1 (primary) ──
438
  let gfx1 = GfxDir::alloc(gfx_dir_vtable);
439
+ (*wrapper).primary_gfx_dir = gfx1;
440
  (*wrapper).secondary_gfx_dir = core::ptr::null_mut();
441
 
442
  // Build path list (order depends on headless + display_flags)
 
499
  let game_version = (*game_info).game_version;
500
  let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 };
501
  if game_version < threshold {
502
+ use crate::render::sprite::gfx_dir::gfx_load_dir_addr;
503
+
504
  let c_digit = if game_version > -3 { b'2' } else { b'1' };
505
  let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
506
  gfx_c_path[14] = c_digit;
507
 
508
  let gfx2 = GfxDir::alloc(gfx_dir_vtable);
509
+ (*wrapper).secondary_gfx_dir = gfx2;
510
 
511
  let fp = fopen(gfx_c_path.as_ptr().cast(), c"rb".as_ptr());
512
  (*gfx2).file_handle = fp;
513
+ let load_addr = gfx_load_dir_addr();
514
  if fp.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
515
  let fp2 = fopen(c"data\\Gfx\\Gfx.dir".as_ptr(), c"rb".as_ptr());
516
  (*gfx2).file_handle = fp2;
 
774
  if !entry.is_null() {
775
  // Try gfx_dir->vtable[2](entry->field_4)
776
  let gfx_vt = *(gfx_dir as *const *const u32);
777
+ let load_cached: unsafe extern "thiscall" fn(*mut GfxDir, u32) -> *mut u8 =
778
  core::mem::transmute(*gfx_vt.add(2));
779
  let entry_val = *(entry.add(4) as *const u32);
780
  let cached = load_cached(gfx_dir, entry_val);
 
825
 
826
  // Arrow GfxDir (conditional on secondary gfxdir)
827
  if !(*ddgame).secondary_gfxdir.is_null() {
828
+ let gfx_resource_create: unsafe extern "thiscall" fn(
829
+ *mut GfxDir,
830
+ *mut u8,
831
+ ) -> *mut u8 = core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
832
  (*ddgame).arrow_gfxdirs[i as usize] =
833
  gfx_resource_create(gfx_dir, core::ptr::null_mut());
834
  }
 
896
  // 5 stack params (layer, gfx_dir, base_path, data_table, table_size)
897
  {
898
  let landscape_ptr = (*wrapper).landscape;
899
+ let water_dir = (*landscape_ptr).water_gfx_dir;
900
+ let land_dir = (*landscape_ptr).level_gfx_dir;
901
  let gfx_dir = (*wrapper).primary_gfx_dir;
902
 
903
  let wrapper_vt = *(wrapper as *const *const u32);
904
  let load_resource_list: unsafe extern "thiscall" fn(
905
  *mut DDGameWrapper,
906
  u32,
907
+ *mut GfxDir,
908
  *const u8,
909
  *const u8,
910
  u32,
 
936
  load_resource_list(
937
  wrapper,
938
  2,
939
+ water_dir,
940
  rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8,
941
  rb(va::WATER_RESOURCE_TABLE) as *const u8,
942
  0x2F4,
 
949
  // GenerateDebrisParticles (0x546F70) for particle effects, which affects
950
  // the game RNG (DDGame+0x45EC). The original constructor loads them even
951
  // in headless mode. Skipping them causes replay desync.
952
+ (*disp).load_sprite_by_layer(3, 0x26D, land_dir, c"back.spr".as_ptr().cast());
953
  // debris.spr must be loaded unconditionally β€” it's used by
954
  // GenerateDebrisParticles (0x546F70) for particle effects, which
955
  // affects the game RNG (DDGame+0x45EC). Skipping it in headless
956
  // mode causes desync (longbow replay checksum mismatch at frame 1350).
957
+ (*disp).load_sprite(3, 0x26E, 0, land_dir, c"debris.spr".as_ptr());
958
 
959
  (*disp).load_sprite_by_layer(
960
  2,
961
  0x26C,
962
+ water_dir,
963
  c"layer\\layer.spr|layer.spr".as_ptr().cast(),
964
  );
965
 
 
972
 
973
  if s_var1 < 0x61 && level_height == 0x2B8 {
974
  // Simple gradient: load gradient.img directly
975
+ let gradient = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx);
976
  (*ddgame).gradient_image = gradient;
977
  } else {
978
+ compute_complex_gradient(ddgame, land_dir, layer3_ctx, s_var1);
979
  }
980
 
981
  // ── Fill image β†’ fill_pixel (0x7338) ──
 
983
  let layer2_ctx = (*(*ddgame).display).set_active_layer(2);
984
  // In the original, fill.img uses piStack_126c which the decompiler
985
  // shows was set from piVar3 (water_layer from landscape+0xB38).
986
+ let fill_sprite = call_gfx_find_and_load(water_dir, c"fill.img", layer2_ctx);
987
  if !fill_sprite.is_null() {
988
  // Get pixel value: fill_sprite->vtable[4](0, 0)
989
  let fill_vt = *(fill_sprite as *const *const u32);
 
1027
  }
1028
 
1029
  // ── Release primary GfxHandler (vtable[3] = release, param 1 = free) ──
1030
+ let primary_gfx_dir = (*wrapper).primary_gfx_dir;
1031
+ if !primary_gfx_dir.is_null() {
1032
+ let gfx_vt = *(primary_gfx_dir as *const *const u32);
1033
+ let release: unsafe extern "thiscall" fn(*mut GfxDir, u32) =
1034
  core::mem::transmute(*gfx_vt.add(3));
1035
+ release(primary_gfx_dir, 1);
1036
  }
1037
 
1038
  if !is_headless {
crates/openwa-core/src/engine/ddgame_wrapper.rs CHANGED
@@ -3,6 +3,7 @@ use crate::engine::ddgame::DDGame;
3
  use crate::engine::net_bridge::NetBridge;
4
  use crate::render::display::gfx::DisplayGfx;
5
  use crate::render::landscape::PCLandscape;
 
6
  use crate::FieldRegistry;
7
 
8
  /// Speech name table entry size (0x40 = 64 bytes, null-terminated C string).
@@ -43,9 +44,9 @@ pub struct DDGameWrapper {
43
  /// 0x490-0x4BF: Unknown
44
  pub _unknown_490: [u8; 0x30],
45
  /// 0x4C0: Primary GfxDir β€” main sprite archive (Gfx.dir / Gfx0.dir / Gfx1.dir).
46
- pub primary_gfx_dir: *mut u8,
47
  /// 0x4C4: Secondary GfxDir β€” supplemental sprites (GfxC_3_0.dir), conditional on game version.
48
- pub secondary_gfx_dir: *mut u8,
49
  /// 0x4C8: Graphics mode flag (DWORD index 0x132)
50
  pub gfx_mode: u32,
51
  /// 0x4CC: PCLandscape object pointer (DWORD index 0x133)
 
3
  use crate::engine::net_bridge::NetBridge;
4
  use crate::render::display::gfx::DisplayGfx;
5
  use crate::render::landscape::PCLandscape;
6
+ use crate::render::sprite::gfx_dir::GfxDir;
7
  use crate::FieldRegistry;
8
 
9
  /// Speech name table entry size (0x40 = 64 bytes, null-terminated C string).
 
44
  /// 0x490-0x4BF: Unknown
45
  pub _unknown_490: [u8; 0x30],
46
  /// 0x4C0: Primary GfxDir β€” main sprite archive (Gfx.dir / Gfx0.dir / Gfx1.dir).
47
+ pub primary_gfx_dir: *mut GfxDir,
48
  /// 0x4C4: Secondary GfxDir β€” supplemental sprites (GfxC_3_0.dir), conditional on game version.
49
+ pub secondary_gfx_dir: *mut GfxDir,
50
  /// 0x4C8: Graphics mode flag (DWORD index 0x132)
51
  pub gfx_mode: u32,
52
  /// 0x4CC: PCLandscape object pointer (DWORD index 0x133)
crates/openwa-core/src/render/display/base.rs CHANGED
@@ -23,7 +23,10 @@
23
  /// point to WA's vtable in .rdata and patch individual slots there in-place
24
  /// (via VirtualProtect in `display.rs`).
25
  use crate::{
26
- render::sprite::{Sprite, SpriteBank},
 
 
 
27
  task::base::Vtable,
28
  wa_alloc::{wa_malloc_struct_zeroed, wa_malloc_zeroed},
29
  };
@@ -48,11 +51,19 @@ pub struct DisplayBase<V: Vtable = *const DisplayBaseVtable> {
48
  pub _gap_3008: [u32; 4],
49
  // +0x3018: field at u32 index 0xC06, zeroed by ctor
50
  pub _field_3018: u32,
51
- // +0x301C..0x309C: gap (0x80 bytes, 32 u32s: indices 0xC07..0xC26)
52
- pub _gap_301c: [u32; 32],
 
 
 
 
 
 
 
 
53
  // +0x309C..0x311C: font object pointers (32 entries), zeroed by ctor.
54
  // Indexed by font_id (valid range 1-31). Used by GetFontInfo, GetFontMetric, SetFontParam.
55
- pub font_table: [u32; 32],
56
  // +0x311C..0x312C: layer context pointers (4 entries), zeroed by ctor.
57
  // Indexed by layer (valid range 1-3). Returned by set_active_layer (vtable slot 5).
58
  // Used as palette data input for update_palette (vtable slot 24).
 
23
  /// point to WA's vtable in .rdata and patch individual slots there in-place
24
  /// (via VirtualProtect in `display.rs`).
25
  use crate::{
26
+ render::{
27
+ display::font::Font,
28
+ sprite::{Sprite, SpriteBank},
29
+ },
30
  task::base::Vtable,
31
  wa_alloc::{wa_malloc_struct_zeroed, wa_malloc_zeroed},
32
  };
 
51
  pub _gap_3008: [u32; 4],
52
  // +0x3018: field at u32 index 0xC06, zeroed by ctor
53
  pub _field_3018: u32,
54
+ // +0x301C..0x309C: per-font layer index (0x80 bytes, 32 u32s).
55
+ // Records which `layer_contexts[1..=3]` palette context owns each
56
+ // font's color slots. Written by `load_font` (slot 34) β€” the WA API
57
+ // calls this parameter "mode", but it's the same value space as the
58
+ // layer index used by `set_layer_color` / `set_active_layer` /
59
+ // `update_palette` / `set_layer_visibility`. Read back by
60
+ // `load_font_extension` (slot 35) to recover the owning palette
61
+ // context. Valid values are 1, 2, or 3; only 1 is observed in the
62
+ // shipping game (all 28 fonts go on layer 1).
63
+ pub font_layers: [u32; 32],
64
  // +0x309C..0x311C: font object pointers (32 entries), zeroed by ctor.
65
  // Indexed by font_id (valid range 1-31). Used by GetFontInfo, GetFontMetric, SetFontParam.
66
+ pub font_table: [*mut Font; 32],
67
  // +0x311C..0x312C: layer context pointers (4 entries), zeroed by ctor.
68
  // Indexed by layer (valid range 1-3). Returned by set_active_layer (vtable slot 5).
69
  // Used as palette data input for update_palette (vtable slot 24).
crates/openwa-core/src/render/display/font.rs ADDED
@@ -0,0 +1,843 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // =========================================================================
2
+ // Font loading
3
+ // =========================================================================
4
+
5
+ use std::ffi::c_char;
6
+
7
+ use crate::render::{palette::PaletteContext, sprite::gfx_dir::GfxDir};
8
+
9
+ /// Font object β€” 0x1C bytes.
10
+ ///
11
+ /// Allocated by `load_font` (vtable slot 34) from a `.fnt` file loaded via
12
+ /// GfxDir. Referenced from `DisplayBase::font_table` (offset 0x309C). Read by
13
+ /// `Font__GetInfo` / `Font__GetMetric` / `Font__DrawText` / `Font__SetPalette`
14
+ /// to render bitmap text.
15
+ ///
16
+ /// Field semantics derived from `FUN_004f99d0` (the parser) and `Font__GetInfo`
17
+ /// (0x4fa7d0, which confirms width at +0, width_div_5 at +2, glyph count at +4,
18
+ /// and glyph table at +8 with 12-byte entries whose byte +4 is a max metric).
19
+ #[repr(C)]
20
+ pub struct Font {
21
+ /// 0x00: Font max width in pixels (read from .fnt metadata).
22
+ pub width: u16,
23
+ /// 0x02: `width / 5 + 1` β€” initial max metric seed in `Font__GetInfo`.
24
+ pub width_div_5: u16,
25
+ /// 0x04: Glyph count / font height.
26
+ pub height: u16,
27
+ /// 0x06: Duplicate of height.
28
+ pub _height2: u16,
29
+ /// 0x08: Pointer to glyph table (height entries of 12 bytes each).
30
+ pub glyph_table: *mut GlyphEntry,
31
+ /// 0x0C: Pointer to a 256-byte char→glyph-index lookup table that lives
32
+ /// in the .fnt buffer immediately after the RGB palette triplets.
33
+ /// Each entry is a 1-based glyph index (or 0 if the codepoint has no
34
+ /// glyph) β€” `Font__GetMetric` / `Font__SetParam` use it to map a string
35
+ /// byte to its `glyph_table[index - 1]` entry. `font_extend` writes new
36
+ /// codepoints into this table.
37
+ pub char_to_glyph_idx: *mut u8,
38
+ /// 0x10: Pointer to remapped pixel data in the source buffer.
39
+ pub pixel_data: *mut u8,
40
+ /// 0x14: Zeroed by `load_font`; purpose unknown.
41
+ pub _unknown_14: u32,
42
+ /// 0x18: When non-null, holds the address of an auxiliary allocation
43
+ /// owned by this font (a separate buffer of glyph entries + pixel data
44
+ /// pointed at by individual `GlyphEntry::pixel_offset` deltas). Zeroed
45
+ /// by `load_font`; written by `font_extend` (which appends new glyphs
46
+ /// from a `.fex` file) and `font_set_palette_impl` (which derives the
47
+ /// `.`/`;` glyphs for the digital font). The cleanup path frees this
48
+ /// when the FontObject is destroyed.
49
+ pub aux_alloc: u32,
50
+ }
51
+
52
+ const _: () = assert!(core::mem::size_of::<Font>() == 0x1C);
53
+
54
+ /// Per-glyph metadata in a `.fnt` file (12 bytes).
55
+ ///
56
+ /// Indexed by `FontObject::glyph_table`. Layout matches the loop stride of 0xC
57
+ /// used by the parser, `Font__GetInfo` (0x4fa7d0), and `Font__GetMetric` (0x4fa780).
58
+ ///
59
+ /// `Font__GetMetric` reads `width` (offset +4) as the character advance metric.
60
+ /// `Font__DrawText` uses `pixel_offset` as a byte delta from
61
+ /// `FontObject::pixel_data` to locate the glyph's bitmap rows. The offset can
62
+ /// be negative when `font_extend` adds glyphs from a separate allocation.
63
+ #[repr(C)]
64
+ pub struct GlyphEntry {
65
+ /// 0x00: Glyph bounding-box top-left X within the glyph cell.
66
+ pub start_x: u16,
67
+ /// 0x02: Glyph bounding-box top-left Y within the glyph cell.
68
+ pub start_y: u16,
69
+ /// 0x04: Glyph bounding-box width in pixels (also used as the advance metric).
70
+ pub width: u16,
71
+ /// 0x06: Glyph bounding-box height in pixels.
72
+ pub height: u16,
73
+ /// 0x08: Byte delta from `FontObject::pixel_data` to this glyph's bitmap.
74
+ /// Treated as a signed offset by drawing code so `font_extend` can point
75
+ /// glyphs at pixels that live in a different allocation.
76
+ pub pixel_offset: u32,
77
+ }
78
+
79
+ const _: () = assert!(core::mem::size_of::<GlyphEntry>() == 0xC);
80
+
81
+ /// Fixed prefix of a `.fnt` binary blob (0xC bytes).
82
+ ///
83
+ /// Followed in memory by: packed RGB triplets (`palette_count * 3` bytes),
84
+ /// a 0x100-byte character-width table, `max_width: i16`, `glyph_count: u16`,
85
+ /// 4-byte alignment padding, `GlyphEntry[glyph_count]`, then the packed
86
+ /// glyph pixel data. `font_parse_data` walks this variable-length tail using
87
+ /// byte pointers rather than a single `#[repr(C)]` struct.
88
+ #[repr(C)]
89
+ pub struct FntHeader {
90
+ /// 0x00: Unknown / version.
91
+ pub _unknown_00: u32,
92
+ /// 0x04: Total size of this record in bytes, including this field.
93
+ pub data_size: i32,
94
+ /// 0x08: Unknown u16 (skipped by the parser).
95
+ pub _unknown_08: u16,
96
+ /// 0x0A: Number of palette entries that follow this header.
97
+ pub palette_count: u16,
98
+ // +0x0C: packed RGB triplets (variable length)
99
+ }
100
+
101
+ const _: () = assert!(core::mem::size_of::<FntHeader>() == 0xC);
102
+
103
+ /// Port of `FUN_004f99d0` β€” parses `.fnt` binary data into a `FontObject`.
104
+ ///
105
+ /// Original convention: cdecl with 1 stack arg (buffer), `unaff_EDI = font_obj`.
106
+ /// Ported as a plain Rust function.
107
+ ///
108
+ /// Buffer layout:
109
+ /// ```text
110
+ /// +0x00 u32 (unknown / version)
111
+ /// +0x04 i32 data_size (total bytes in this record, including this field)
112
+ /// +0x08 u16 (unknown)
113
+ /// +0x0A u16 palette_count
114
+ /// +0x0C RGB[palette_count * 3] packed RGB triplets
115
+ /// +... u8[0x100] character-width table (1 byte per codepoint)
116
+ /// +0x100 u16 max_width
117
+ /// +0x102 u16 glyph_count / height
118
+ /// +... align to 4 from buffer start
119
+ /// +... GlyphEntry[glyph_count] 12 bytes each (start_x/start_y/end_x/end_y/...)
120
+ /// +... u8[] packed 4bpp glyph pixels (remapped in place via palette LUT)
121
+ /// ```
122
+ ///
123
+ /// # Safety
124
+ /// `font_obj` must be a valid writable `FontObject`. `header` must point to a
125
+ /// freshly-loaded `.fnt` blob at least `header.data_size` bytes long and
126
+ /// followed in memory by the variable-length tail described above.
127
+ /// `palette_ctx` must be a valid `PaletteContext`.
128
+ pub unsafe fn font_parse_data(
129
+ font_obj: *mut Font,
130
+ header: *mut FntHeader,
131
+ palette_ctx: *mut PaletteContext,
132
+ ) -> u32 {
133
+ use crate::render::palette::{palette_map_color, remap_pixels_through_lut};
134
+
135
+ let data_size = (*header).data_size;
136
+ let palette_count = (*header).palette_count as i32;
137
+ let base_bytes = header as *mut u8;
138
+
139
+ // Cursor starts at the first byte after the fixed header (the RGB triplets).
140
+ let mut cursor = base_bytes.add(core::mem::size_of::<FntHeader>());
141
+
142
+ // Build 256-byte palette LUT. Entry 0 = map_color(0); entries 1..=palette_count
143
+ // come from the RGB triplets. The original reads 4 bytes per triplet but
144
+ // advances by 3 β€” only the low 24 bits matter.
145
+ let mut palette_lut = [0u8; 256];
146
+ palette_lut[0] = palette_map_color(palette_ctx, 0) as u8;
147
+ for i in 0..palette_count {
148
+ let rgb = *(cursor as *const u32);
149
+ palette_lut[(1 + i) as usize] = palette_map_color(palette_ctx, rgb) as u8;
150
+ cursor = cursor.add(3);
151
+ }
152
+
153
+ // char_to_glyph_idx points to the 256-byte char→glyph-index table that
154
+ // sits immediately after the RGB palette triplets in the .fnt buffer.
155
+ (*font_obj).char_to_glyph_idx = cursor;
156
+
157
+ // Read max_width (i16) at cursor + 0x100 β€” the character-width table sits
158
+ // between the palette and the header.
159
+ let max_width = *(cursor.add(0x100) as *const i16) as i32;
160
+ cursor = cursor.add(0x100);
161
+ (*font_obj).width = max_width as u16;
162
+
163
+ // Seed value for Font__GetInfo's max metric: signed (width / 5) + 1,
164
+ // with a +1 correction when the quotient is negative (matches the
165
+ // IMUL+SAR+SHR+LEA sequence in the original).
166
+ let w_div_5 = max_width / 5;
167
+ let seed = w_div_5.wrapping_add((w_div_5 >> 31) & 1).wrapping_add(1);
168
+ (*font_obj).width_div_5 = seed as u16;
169
+
170
+ // Skip max_width, read glyph_count (u16).
171
+ cursor = cursor.add(2);
172
+ let glyph_count = *(cursor as *const u16);
173
+ cursor = cursor.add(2);
174
+ (*font_obj).height = glyph_count;
175
+ (*font_obj)._height2 = glyph_count;
176
+
177
+ // Align cursor to 4 bytes relative to the buffer start.
178
+ let base_addr = base_bytes as usize;
179
+ while (cursor as usize - base_addr) & 3 != 0 {
180
+ cursor = cursor.add(1);
181
+ }
182
+
183
+ // Glyph table: glyph_count entries of 12 bytes each.
184
+ let glyph_table = cursor as *mut GlyphEntry;
185
+ (*font_obj).glyph_table = glyph_table;
186
+ let pixel_data = glyph_table.add(glyph_count as usize) as *mut u8;
187
+ (*font_obj).pixel_data = pixel_data;
188
+
189
+ // Pixel byte count: data_size - (pixel_data - buffer), rounded down to dwords.
190
+ // Original uses CDQ+AND 3+ADD+SAR 2 which rounds toward zero.
191
+ let remaining = data_size - (pixel_data as usize - base_addr) as i32;
192
+ let dword_count = remaining
193
+ .wrapping_add((remaining >> 31) & 3)
194
+ .wrapping_shr(2) as u32;
195
+
196
+ remap_pixels_through_lut(pixel_data, 0, palette_lut.as_ptr(), dword_count, 1);
197
+
198
+ 1
199
+ }
200
+
201
+ /// Port of `FUN_004f9940` β€” loads a font resource by name from a GfxDir and
202
+ /// feeds it into `font_parse_data`.
203
+ ///
204
+ /// Original convention: usercall with `EAX = gfx_dir`, `ECX = font_obj`,
205
+ /// `stack[0] = layer_ctx`, `stack[1] = filename`, `RET 0x8`. The function
206
+ /// first tries `GfxDir__FindEntry` + cached-load via `gfx_dir->vtable[2]`;
207
+ /// on miss it falls back to `GfxDir__LoadImage` + `image->vtable[4]` (get_size)
208
+ /// + `image->vtable[5]` (read into caller-allocated buffer) + `image->vtable[0](1)`
209
+ /// (destroy), then tail-calls the parser.
210
+ ///
211
+ /// Note: `layer_ctx` is only used by `font_parse_data` (as the PaletteContext).
212
+ /// The original never touches it in this function β€” it just forwards it via
213
+ /// ECX to the tail call.
214
+ ///
215
+ /// # Safety
216
+ /// `font_obj` must be a valid writable `FontObject`. `gfx_dir` must be a valid
217
+ /// GfxDir. `palette_ctx` must be a valid PaletteContext. `name` must be a
218
+ /// valid null-terminated C string.
219
+ pub unsafe fn font_load_from_gfx(
220
+ font_obj: *mut Font,
221
+ gfx_dir: *mut GfxDir,
222
+ palette_ctx: *mut PaletteContext,
223
+ name: *const c_char,
224
+ ) -> u32 {
225
+ use crate::render::sprite::gfx_dir::{call_gfx_load_image, gfx_dir_find_entry, GfxDirEntry};
226
+ use crate::wa_alloc::wa_malloc;
227
+
228
+ // 1. Try FindEntry β†’ gfx_dir->vtable[2](entry->value) for cached load.
229
+ let entry = gfx_dir_find_entry(name, gfx_dir);
230
+ if !entry.is_null() {
231
+ let vt = *(gfx_dir as *const *const u32);
232
+ let load_cached: unsafe extern "thiscall" fn(*mut GfxDir, u32) -> *mut FntHeader =
233
+ core::mem::transmute(*vt.add(2));
234
+ let entry_val = (*(entry as *const GfxDirEntry)).value;
235
+ let cached = load_cached(gfx_dir, entry_val);
236
+ if !cached.is_null() {
237
+ return font_parse_data(font_obj, cached, palette_ctx);
238
+ }
239
+ }
240
+
241
+ // 2. Fallback: LoadImage β†’ vtable[4] (size) β†’ wa_malloc β†’ vtable[5] (read) β†’ vtable[0] (destroy)
242
+ let image = call_gfx_load_image(gfx_dir, name);
243
+ if image.is_null() {
244
+ return 0;
245
+ }
246
+
247
+ let image_vt = *(image as *const *const u32);
248
+
249
+ // vtable[4] β€” get size (thiscall, no args)
250
+ let get_size: unsafe extern "thiscall" fn(*mut u8) -> u32 =
251
+ core::mem::transmute(*image_vt.add(4));
252
+ let size = get_size(image);
253
+
254
+ // Match the original's allocation: round size up to 4-byte multiple, add 0x20 guard.
255
+ let alloc_size = ((size + 3) & !3u32).wrapping_add(0x20);
256
+ let buffer = wa_malloc(alloc_size);
257
+ // Original memsets only `size` bytes, not the full allocation.
258
+ if !buffer.is_null() {
259
+ core::ptr::write_bytes(buffer, 0, size as usize);
260
+ }
261
+
262
+ // vtable[5] β€” read into buffer (thiscall with 2 stack args: buffer, size)
263
+ let read_into: unsafe extern "thiscall" fn(*mut u8, *mut u8, u32) =
264
+ core::mem::transmute(*image_vt.add(5));
265
+ read_into(image, buffer, size);
266
+
267
+ // vtable[0] β€” destroy image (thiscall with 1 arg: flags = 1)
268
+ let destroy: unsafe extern "thiscall" fn(*mut u8, u32) = core::mem::transmute(*image_vt);
269
+ destroy(image, 1);
270
+
271
+ font_parse_data(font_obj, buffer as *mut FntHeader, palette_ctx)
272
+ }
273
+
274
+ /// Read a glyph's advance metric from the glyph table. Returns
275
+ /// `glyph_table[idx_1based - 1].width as i32 + 1` (matching the original's
276
+ /// `*(ushort*)(glyph_table - 8 + idx*0xC) + 1`).
277
+ #[inline]
278
+ pub(crate) unsafe fn glyph_advance_metric(font_obj: *const Font, idx_1based: u8) -> i32 {
279
+ let entry = (*font_obj).glyph_table.add(idx_1based as usize - 1);
280
+ (*entry).width as i32 + 1
281
+ }
282
+
283
+ /// Pure-Rust port of `Font__GetInfo` (0x4FA7D0).
284
+ ///
285
+ /// Writes the font's max width to `*out_width` and the longest glyph
286
+ /// advance metric (max of `width_div_5` and `glyph.width + 1` for every
287
+ /// glyph) to `*out_max_metric`. Returns 1 unconditionally.
288
+ ///
289
+ /// # Safety
290
+ /// `font_obj` must be a valid `*const FontObject` with a glyph table of at
291
+ /// least `font_obj.height` entries. The output pointers must be writable.
292
+ pub unsafe fn font_get_info_impl(
293
+ font_obj: *const Font,
294
+ out_max_metric: *mut i32,
295
+ out_width: *mut i32,
296
+ ) -> u32 {
297
+ *out_width = (*font_obj).width as i16 as i32;
298
+ let mut max_metric = (*font_obj).width_div_5 as i16 as i32;
299
+ let height = (*font_obj).height as i32;
300
+ for i in 0..height {
301
+ let entry = (*font_obj).glyph_table.add(i as usize);
302
+ let candidate = (*entry).width as i32 + 1;
303
+ if max_metric < candidate {
304
+ max_metric = candidate;
305
+ }
306
+ }
307
+ *out_max_metric = max_metric;
308
+ 1
309
+ }
310
+
311
+ /// Pure-Rust port of `Font__GetMetric` (0x4FA780).
312
+ ///
313
+ /// Always writes the font's max width to `*out_width`. For space (`0x20`)
314
+ /// and non-breaking space (`0xA0`), writes `width_div_5` to `*out_metric`
315
+ /// and returns 1. Otherwise looks up the codepoint in `char_to_glyph_idx`;
316
+ /// if the codepoint is unmapped (entry is 0) returns 0, else writes
317
+ /// `glyph_table[idx-1].width + 1` to `*out_metric` and returns 1.
318
+ ///
319
+ /// # Safety
320
+ /// `font_obj` must be a valid `*const FontObject`. The output pointers must
321
+ /// be writable.
322
+ pub unsafe fn font_get_metric_impl(
323
+ font_obj: *const Font,
324
+ char_code: u8,
325
+ out_metric: *mut i32,
326
+ out_width: *mut i32,
327
+ ) -> u32 {
328
+ *out_width = (*font_obj).width as i16 as i32;
329
+ if char_code != 0x20 && char_code != 0xA0 {
330
+ let glyph_idx = *(*font_obj).char_to_glyph_idx.add(char_code as usize);
331
+ if glyph_idx == 0 {
332
+ return 0;
333
+ }
334
+ *out_metric = glyph_advance_metric(font_obj, glyph_idx);
335
+ return 1;
336
+ }
337
+ *out_metric = (*font_obj).width_div_5 as i16 as i32;
338
+ 1
339
+ }
340
+
341
+ /// Pure-Rust port of `Font__SetParam` (0x4FA720).
342
+ ///
343
+ /// Walks the null-terminated byte string `text`, accumulating each
344
+ /// codepoint's advance metric (`width_div_5` for unmapped codepoints,
345
+ /// `glyph.width + 1` for mapped ones) into `*out_total`. Always writes the
346
+ /// font's max width to `*out_width`.
347
+ ///
348
+ /// # Safety
349
+ /// `font_obj` must be a valid `*const FontObject`. `text` must be a valid
350
+ /// null-terminated byte string. Output pointers must be writable.
351
+ pub unsafe fn font_set_param_impl(
352
+ font_obj: *const Font,
353
+ text: *const u8,
354
+ out_total: *mut i32,
355
+ out_width: *mut i32,
356
+ ) {
357
+ *out_width = (*font_obj).width as i16 as i32;
358
+ *out_total = 0;
359
+ let mut p = text;
360
+ loop {
361
+ let ch = *p;
362
+ if ch == 0 {
363
+ break;
364
+ }
365
+ let glyph_idx = *(*font_obj).char_to_glyph_idx.add(ch as usize);
366
+ let advance = if glyph_idx == 0 {
367
+ (*font_obj).width_div_5 as i16 as i32
368
+ } else {
369
+ glyph_advance_metric(font_obj, glyph_idx)
370
+ };
371
+ *out_total += advance;
372
+ p = p.add(1);
373
+ }
374
+ }
375
+
376
+ /// Port of `FUN_004f9ad0` β€” extends an existing `FontObject` with new glyphs
377
+ /// loaded from a `.fex` (font extension) file.
378
+ ///
379
+ /// Original convention: usercall with `EAX = filename`, `ESI = font_obj`,
380
+ /// stack args = (`layer_ctx`, `char_map`, `palette_value`), `RET 0xC`.
381
+ ///
382
+ /// The `.fex` file contains raw pixel data for new glyphs (no header). Each
383
+ /// glyph occupies `font.width * stride` bytes (where `stride = font.width`,
384
+ /// or `font.width + 1` for fonts with `width >= 16`). The new bitmap also
385
+ /// gets remapped through a 256-byte LUT built by walking R/G/B in lockstep
386
+ /// with the per-channel steps packed into `palette_value` and using
387
+ /// `palette_find_nearest_cached` to map each composed RGB to a palette index.
388
+ ///
389
+ /// On success the function:
390
+ /// - allocates a single new buffer holding `[old + new glyph entries][new pixels]`
391
+ /// - copies the old glyph entries verbatim into the front of the new buffer
392
+ /// - writes per-codepoint indices into the font's char-width table (the bytes
393
+ /// referenced by `font_obj.char_to_glyph_idx`)
394
+ /// - reads the new pixel rows from disk via `_fread`
395
+ /// - computes a tight bounding box per new glyph and writes it as a new entry
396
+ /// - remaps every new pixel byte through the LUT
397
+ /// - swaps `font_obj.glyph_table` to the new buffer and bumps `font_obj.height`
398
+ /// - records the allocation in `font_obj.aux_alloc`
399
+ ///
400
+ /// The `pixel_offset` field in each new glyph entry stores
401
+ /// `(absolute_pixel_addr - font_obj.pixel_data)` so the existing
402
+ /// `Font__DrawText` lookup `pixel_data + offset` still resolves correctly,
403
+ /// even though the new pixels live in a separate allocation.
404
+ ///
405
+ /// # Safety
406
+ /// `font_obj` must be a fully-parsed `FontObject` (created by `load_font`).
407
+ /// `palette_ctx` must be a valid `PaletteContext`. `filename` and `char_map`
408
+ /// must be valid null-terminated C strings.
409
+ pub unsafe fn font_extend(
410
+ font_obj: *mut Font,
411
+ palette_ctx: *mut PaletteContext,
412
+ filename: *const c_char,
413
+ char_map: *const u8,
414
+ palette_value: u32,
415
+ ) {
416
+ use crate::address::va;
417
+ use crate::rebase::rb;
418
+ use crate::render::palette::{palette_find_nearest_cached, remap_pixels_through_lut};
419
+ use crate::wa_alloc::wa_malloc;
420
+ use core::ffi::c_char;
421
+
422
+ // Open the .fex file via WA's CRT _fopen.
423
+ let fopen: unsafe extern "cdecl" fn(*const c_char, *const c_char) -> *mut u8 =
424
+ core::mem::transmute(rb(va::WA_FOPEN) as usize);
425
+ let fread: unsafe extern "cdecl" fn(*mut u8, u32, u32, *mut u8) -> u32 =
426
+ core::mem::transmute(rb(va::WA_FREAD) as usize);
427
+ let fclose: unsafe extern "cdecl" fn(*mut u8) -> i32 =
428
+ core::mem::transmute(rb(va::WA_FCLOSE) as usize);
429
+
430
+ let file = fopen(filename, c"rb".as_ptr());
431
+ if file.is_null() {
432
+ return;
433
+ }
434
+
435
+ // ── Geometry ──
436
+ let font_width = (*font_obj).width as i32;
437
+ // Per-glyph row stride in the .fex file. Wide fonts use width+1 to allow
438
+ // an extra byte after each row (matches the `if (font.width > 15)` test).
439
+ let row_stride = if font_width > 15 {
440
+ font_width + 1
441
+ } else {
442
+ font_width
443
+ };
444
+
445
+ // strlen(char_map) β€” count of new glyphs.
446
+ let mut new_count: i32 = 0;
447
+ let mut p = char_map;
448
+ while *p != 0 {
449
+ p = p.add(1);
450
+ new_count += 1;
451
+ }
452
+
453
+ let old_height = (*font_obj).height as i32; // existing glyph count
454
+ let total_glyphs = old_height + new_count; // resulting glyph count
455
+ let glyph_entries_bytes = total_glyphs * 12;
456
+ let new_pixels_bytes = new_count * font_width * row_stride;
457
+ let total_size = (glyph_entries_bytes + new_pixels_bytes) as u32;
458
+
459
+ // Allocate (matches WA_MallocMemset alignment + 0x20 guard).
460
+ let alloc = wa_malloc(((total_size + 3) & !3u32).wrapping_add(0x20));
461
+ if alloc.is_null() {
462
+ let _ = fclose(file);
463
+ return;
464
+ }
465
+ core::ptr::write_bytes(alloc, 0, total_size as usize);
466
+
467
+ // pixel data area starts immediately after the glyph entries.
468
+ let new_pixels = alloc.add(glyph_entries_bytes as usize);
469
+
470
+ // Record the allocation so font cleanup paths can free it.
471
+ (*font_obj).aux_alloc = alloc as u32;
472
+
473
+ // ── Copy existing glyph entries verbatim into the new buffer ──
474
+ let old_glyphs = (*font_obj).glyph_table as *const u8;
475
+ if old_height > 0 && !old_glyphs.is_null() {
476
+ core::ptr::copy_nonoverlapping(old_glyphs, alloc, (old_height * 12) as usize);
477
+ }
478
+
479
+ // ── Update the per-codepoint charβ†’glyph-index table ──
480
+ // For each new char in char_map, write the new (1-based) glyph index
481
+ // `(old_height + idx + 1)` into the table at `font_obj.char_to_glyph_idx`.
482
+ let char_to_glyph = (*font_obj).char_to_glyph_idx;
483
+ let mut q = char_map;
484
+ for i in 0..new_count {
485
+ let ch = *q as usize;
486
+ q = q.add(1);
487
+ *char_to_glyph.add(ch) = (old_height + i + 1) as u8;
488
+ }
489
+
490
+ // ── Build 256-byte palette LUT by stepping R/G/B per `palette_value` ──
491
+ // The original walks 256 entries, accumulating signed R/G/B values via the
492
+ // step bytes packed in `palette_value` (low byte=R step, mid=G, high=B),
493
+ // each reduced mod 255 with sign correction (the `IMUL 0x80808081` trick).
494
+ // Each composed RGB is fed to `palette_find_nearest_cached` and the
495
+ // returned palette index becomes lut[i].
496
+ let r_step = (palette_value & 0xff) as i32;
497
+ let g_step = ((palette_value >> 8) & 0xff) as i32;
498
+ let b_step = ((palette_value >> 16) & 0xff) as i32;
499
+
500
+ let mut palette_lut = [0u8; 256];
501
+ let mut acc_r: i32 = 0;
502
+ let mut acc_g: i32 = 0;
503
+ let mut acc_b: i32 = 0;
504
+ for slot in palette_lut.iter_mut() {
505
+ // Reduce each accumulator mod 255 with sign correction. The original
506
+ // uses signed magic division by 255; for 0..255 the result is just
507
+ // `acc - 0` for 0..254 and `acc - 1` for 255. We use rem_euclid for
508
+ // a clean cross-platform equivalent (palette steps are non-negative
509
+ // so this matches the original even on edge cases).
510
+ let r = mod_255_signed(acc_r);
511
+ let g = mod_255_signed(acc_g);
512
+ let b = mod_255_signed(acc_b);
513
+ let composed = (r as u32) | ((g as u32) << 8) | ((b as u32) << 16);
514
+
515
+ let mut distance: i32 = 0;
516
+ let idx = palette_find_nearest_cached(palette_ctx, composed, &mut distance) as u8;
517
+ *slot = idx;
518
+
519
+ acc_r += r_step;
520
+ acc_g += g_step;
521
+ acc_b += b_step;
522
+ }
523
+
524
+ // ── Read the new pixel rows from disk ──
525
+ // fread(new_pixels, new_count, font_width * row_stride, file)
526
+ let _ = fread(
527
+ new_pixels,
528
+ new_count as u32,
529
+ (font_width * row_stride) as u32,
530
+ file,
531
+ );
532
+ let _ = fclose(file);
533
+
534
+ // ── Bounding-box scan for each new glyph + write its glyph entry ──
535
+ let mut row_offset_in_pixels: i32 = 0; // i.e. iVar16 in the original
536
+ let new_glyph_base = alloc as *mut GlyphEntry;
537
+ for new_idx in 0..new_count {
538
+ // Find the LEFTMOST non-empty column (== start_x).
539
+ let mut start_x: u16 = 0;
540
+ let mut found_left = false;
541
+ 'outer_left: for col in 0..font_width as u16 {
542
+ for row in 0..font_width as i32 {
543
+ let addr = new_pixels
544
+ .offset(((row + row_offset_in_pixels) * font_width + col as i32) as isize);
545
+ if *addr != 0 {
546
+ start_x = col;
547
+ found_left = true;
548
+ break 'outer_left;
549
+ }
550
+ }
551
+ }
552
+ // Find the TOPMOST non-empty row (== start_y).
553
+ let mut start_y: u16 = 0;
554
+ if found_left {
555
+ 'outer_top: for row in 0..font_width as u16 {
556
+ for col in 0..font_width as i32 {
557
+ let addr = new_pixels
558
+ .offset((col + (row as i32 + row_offset_in_pixels) * font_width) as isize);
559
+ if *addr != 0 {
560
+ start_y = row;
561
+ break 'outer_top;
562
+ }
563
+ }
564
+ }
565
+ }
566
+ // Find the RIGHTMOST non-empty column (scan right→left from font_width-1).
567
+ let mut right: u16 = (font_width - 1).max(0) as u16;
568
+ if found_left && right != 0 {
569
+ 'outer_right: for col in (1..font_width as u16).rev() {
570
+ let mut empty = true;
571
+ for row in 0..font_width as i32 {
572
+ let addr = new_pixels
573
+ .offset(((row + row_offset_in_pixels) * font_width + col as i32) as isize);
574
+ if *addr != 0 {
575
+ empty = false;
576
+ break;
577
+ }
578
+ }
579
+ if !empty {
580
+ right = col;
581
+ break 'outer_right;
582
+ }
583
+ right = col - 1;
584
+ }
585
+ }
586
+ // Find the BOTTOMMOST non-empty row (scan bottom→top from font_width-1).
587
+ let mut bottom: u16 = (font_width - 1).max(0) as u16;
588
+ if found_left {
589
+ loop {
590
+ let mut empty = true;
591
+ for col in 0..font_width as i32 {
592
+ let addr = new_pixels.offset(
593
+ (col + (bottom as i32 + row_offset_in_pixels) * font_width) as isize,
594
+ );
595
+ if *addr != 0 {
596
+ empty = false;
597
+ break;
598
+ }
599
+ }
600
+ if !empty || bottom == 0 {
601
+ break;
602
+ }
603
+ bottom -= 1;
604
+ }
605
+ }
606
+
607
+ // Write the new glyph entry. The slot index is (old_height + new_idx).
608
+ let entry = new_glyph_base.add((old_height + new_idx) as usize);
609
+ (*entry).start_x = start_x;
610
+ (*entry).start_y = start_y;
611
+ (*entry).width = right.saturating_sub(start_x) + 1;
612
+ (*entry).height = bottom.saturating_sub(start_y) + 1;
613
+ // pixel_offset = absolute address of the glyph's bitmap top-left,
614
+ // expressed as a delta from font_obj.pixel_data so existing draw code
615
+ // can do `pixel_data + offset` and reach the new allocation.
616
+ let abs_addr = new_pixels.offset(
617
+ (start_x as i32 + (start_y as i32 + row_offset_in_pixels) * font_width) as isize,
618
+ );
619
+ (*entry).pixel_offset = (abs_addr as u32).wrapping_sub((*font_obj).pixel_data as u32);
620
+
621
+ // Advance to the next glyph's pixel rows.
622
+ row_offset_in_pixels += row_stride;
623
+ }
624
+
625
+ // ── Remap every new pixel byte through the LUT ──
626
+ let total_pixel_bytes = font_width * new_count * row_stride;
627
+ if total_pixel_bytes > 0 {
628
+ // remap_pixels_through_lut takes (data, pitch, lut, width_dwords, height).
629
+ // The original walks each byte one at a time, so use width_dwords = 1
630
+ // and height = total_bytes / 4 ... actually simpler: do a flat single-row
631
+ // remap with width_dwords = total/4 and height = 1.
632
+ let dword_count = (total_pixel_bytes as u32 + 3) / 4;
633
+ remap_pixels_through_lut(new_pixels, 0, palette_lut.as_ptr(), dword_count, 1);
634
+ }
635
+
636
+ // ── Swap glyph_table and bump glyph count ──
637
+ (*font_obj).glyph_table = alloc as *mut GlyphEntry;
638
+ (*font_obj).height = (old_height + new_count) as u16;
639
+ }
640
+
641
+ /// Helper for `font_extend`'s palette stepping. Reduces a non-negative i32
642
+ /// modulo 255 the way the original signed-magic-division sequence does.
643
+ #[inline]
644
+ fn mod_255_signed(value: i32) -> i32 {
645
+ // For non-negative values this is just `value % 255`.
646
+ // The original uses signed magic-division so we mirror that for safety.
647
+ let q = value / 255;
648
+ value - q * 255
649
+ }
650
+
651
+ /// Pure-Rust port of `Font__SetPalette` (0x4F9F20).
652
+ ///
653
+ /// **The function name is misleading** β€” despite "SetPalette", this routine
654
+ /// extends the font with two new derived glyphs rather than recoloring an
655
+ /// existing palette. It is called once during `DDGame__LoadFonts` for the
656
+ /// digital seven-segment font (`digiwht.fnt`, slot 28), which doesn't ship
657
+ /// with `'.'` or `';'` glyphs and needs them generated at runtime.
658
+ ///
659
+ /// What it does, in order:
660
+ ///
661
+ /// 1. Look up the **`'-'`** (0x2D), **`'8'`** (0x38) and **`':'`** (0x3A)
662
+ /// glyph indices via `char_to_glyph_idx`. If any is missing, return 0.
663
+ /// 2. Sample the foreground-color palette indices of `'-'` and `'8'` by
664
+ /// reading byte +5 within each glyph's pixel data. Save them as
665
+ /// `minus_fg` and `eight_fg`.
666
+ /// 3. Allocate a new buffer sized for `(old_height + 2)` glyph entries
667
+ /// plus enough pixel area for **two** glyphs the size of `':'`.
668
+ /// 4. Copy all existing glyph entries verbatim into the start of the
669
+ /// buffer.
670
+ /// 5. Add two new glyph entries:
671
+ /// - `char_to_glyph_idx[0x2E]` (= `'.'`) β†’ 1-based index `old_height + 1`
672
+ /// - `char_to_glyph_idx[0x3B]` (= `';'`) β†’ 1-based index `old_height + 2`
673
+ ///
674
+ /// Both new entries copy `start_x`/`start_y`/`width`/`height` from the
675
+ /// `':'` glyph. Their `pixel_offset` fields point at consecutive
676
+ /// `colon_height * font_width`-byte regions inside the new buffer.
677
+ /// 6. Initialize the **first** new glyph (`'.'`)'s pixel area:
678
+ /// - `memset(area, palette_value as u8, colon_height * font_width / 2)`
679
+ /// β€” fills approximately the top half with a uniform palette index.
680
+ /// - For rows `colon_height/2 .. colon_height-1`, copy `colon.width`
681
+ /// bytes per row from `':'`'s pixel data into the new buffer at
682
+ /// `font_width`-strided rows.
683
+ /// 7. Initialize the **second** new glyph (`';'`)'s pixel area: for every
684
+ /// `(row, col)` in `(0..colon.height, 0..colon.width)`, copy the
685
+ /// `':'` source pixel; if it equals `eight_fg`, replace it with
686
+ /// `minus_fg` instead.
687
+ /// 8. Update `font_obj.glyph_table` to point at the new buffer, store the
688
+ /// new buffer in `aux_alloc` (so destructors can free it), and bump
689
+ /// `font_obj.height` by 2.
690
+ ///
691
+ /// The original uses `wa_malloc(round_up_to_4(total_size) + 0x20)` and
692
+ /// zero-initializes the body up to `total_size` (the trailing 0x20 padding
693
+ /// is left uninitialized).
694
+ ///
695
+ /// # Safety
696
+ /// `font_obj` must be a valid mutable `FontObject`. `palette_value`'s low
697
+ /// byte is used as a single-byte memset filler. The original's
698
+ /// allocate/copy/update sequence is preserved verbatim.
699
+ pub unsafe fn font_set_palette_impl(font_obj: *mut Font, palette_value: u32) -> u32 {
700
+ use crate::wa_alloc::wa_malloc;
701
+
702
+ let char_to_glyph = (*font_obj).char_to_glyph_idx;
703
+ if char_to_glyph.is_null() {
704
+ return 0;
705
+ }
706
+
707
+ // Step 1: '-' (0x2D) glyph index.
708
+ let minus_idx = *char_to_glyph.add(0x2D);
709
+ if minus_idx == 0 {
710
+ return 0;
711
+ }
712
+
713
+ // Step 2a: read byte +5 of '-' glyph's pixel data.
714
+ let glyph_table = (*font_obj).glyph_table;
715
+ let pixel_data = (*font_obj).pixel_data;
716
+ let minus_glyph = glyph_table.add(minus_idx as usize - 1);
717
+ let minus_fg = *pixel_data.add((*minus_glyph).pixel_offset as usize + 5);
718
+
719
+ // Step 2b: '8' (0x38) glyph index + byte +5.
720
+ let eight_idx = *char_to_glyph.add(0x38);
721
+ if eight_idx == 0 {
722
+ return 0;
723
+ }
724
+ let eight_glyph = glyph_table.add(eight_idx as usize - 1);
725
+ let eight_fg = *pixel_data.add((*eight_glyph).pixel_offset as usize + 5);
726
+
727
+ // Step 1c: ':' (0x3A) β€” required for the template.
728
+ let colon_idx = *char_to_glyph.add(0x3A);
729
+ if colon_idx == 0 {
730
+ return 0;
731
+ }
732
+
733
+ let old_height = (*font_obj).height as i32; // signed via MOVSX in the original
734
+ let new_height = old_height + 2;
735
+ let font_width = (*font_obj).width as i16 as i32;
736
+
737
+ // Step 3: compute total size and allocate.
738
+ // total_size = (colon.height * font_width + new_height * 6) * 2
739
+ // = colon_height * font_width * 2 + new_height * 12
740
+ // alloc = round_up_to_dword(total_size) + 0x20
741
+ let colon_glyph = glyph_table.add(colon_idx as usize - 1);
742
+ let colon_height = (*colon_glyph).height as i32;
743
+ let colon_width = (*colon_glyph).width as i32;
744
+ let colon_pixel_offset = (*colon_glyph).pixel_offset as usize;
745
+
746
+ let total_size = (colon_height * font_width + new_height * 6) * 2;
747
+ let alloc_size = ((total_size as u32 + 3) & !3u32) + 0x20;
748
+ let new_buffer = wa_malloc(alloc_size);
749
+ if new_buffer.is_null() {
750
+ return 0;
751
+ }
752
+ // Zero-init the body (not the trailing 0x20 padding).
753
+ core::ptr::write_bytes(new_buffer, 0, total_size as usize);
754
+
755
+ // _Dst = new_buffer + new_height*12 = where pixel data starts in the new buffer.
756
+ let new_glyph_table = new_buffer as *mut GlyphEntry;
757
+ let new_pixel_area = new_buffer.add((new_height as usize) * 12);
758
+
759
+ // Save the allocation in aux_alloc so cleanup paths can free it
760
+ // (the original writes this *before* the copy loop).
761
+ (*font_obj).aux_alloc = new_buffer as u32;
762
+
763
+ // Step 4: copy existing glyph entries.
764
+ if old_height > 0 {
765
+ core::ptr::copy_nonoverlapping(
766
+ glyph_table as *const u8,
767
+ new_buffer,
768
+ (old_height as usize) * 12,
769
+ );
770
+ }
771
+
772
+ // Step 5: write new char_to_glyph_idx entries for '.' and ';'.
773
+ *char_to_glyph.add(0x2E) = (old_height as u8).wrapping_add(1);
774
+ *char_to_glyph.add(0x3B) = (old_height as u8).wrapping_add(2);
775
+
776
+ // Step 5b: first new glyph (= colon copy with adjusted pixel_offset).
777
+ // pixel_offset = (_Dst - pixel_data)
778
+ let new_first = new_glyph_table.add(old_height as usize);
779
+ (*new_first).start_x = (*colon_glyph).start_x;
780
+ (*new_first).start_y = (*colon_glyph).start_y;
781
+ (*new_first).width = (*colon_glyph).width;
782
+ (*new_first).height = (*colon_glyph).height;
783
+ (*new_first).pixel_offset = (new_pixel_area as u32).wrapping_sub(pixel_data as u32);
784
+
785
+ // Step 5c: second new glyph (= duplicate of first new glyph, but its
786
+ // pixel_offset is bumped by colon_height * font_width so it points to
787
+ // the *next* chunk of new pixel data).
788
+ let new_second = new_first.add(1);
789
+ (*new_second).start_x = (*new_first).start_x;
790
+ (*new_second).start_y = (*new_first).start_y;
791
+ (*new_second).width = (*new_first).width;
792
+ (*new_second).height = (*new_first).height;
793
+ (*new_second).pixel_offset = (*new_first)
794
+ .pixel_offset
795
+ .wrapping_add((colon_height * font_width) as u32);
796
+
797
+ // Step 6a: memset top portion of FIRST new glyph with palette_value's
798
+ // low byte. Length = (colon_height * font_width) with sign-correct /2.
799
+ let memset_len = {
800
+ let v = colon_height.wrapping_mul(font_width);
801
+ // CDQ + SUB EAX,EDX + SAR EAX,1 = signed div by 2 with rounding toward zero
802
+ let v_signed = v;
803
+ let cdq = v_signed >> 31;
804
+ ((v_signed - cdq) >> 1) as usize
805
+ };
806
+ core::ptr::write_bytes(new_pixel_area, palette_value as u8, memset_len);
807
+
808
+ // Step 6b: copy bottom half rows of ':' into FIRST new glyph at
809
+ // font-width row stride.
810
+ let half = (colon_height as u32 >> 1) as i32;
811
+ if half < colon_height {
812
+ for row in half..colon_height {
813
+ let src = pixel_data.add(colon_pixel_offset + (row * colon_width) as usize);
814
+ let dst = new_pixel_area.add((row * font_width) as usize);
815
+ core::ptr::copy_nonoverlapping(src, dst, colon_width as usize);
816
+ }
817
+ }
818
+
819
+ // Step 7: pixel-by-pixel copy for SECOND new glyph with eight→minus
820
+ // foreground substitution. The destination is offset by
821
+ // colon_height * font_width within new_pixel_area.
822
+ if colon_height > 0 {
823
+ for row in 0..colon_height {
824
+ for col in 0..colon_width {
825
+ let src_addr =
826
+ pixel_data.add(colon_pixel_offset + (row * colon_width + col) as usize);
827
+ let dst_addr =
828
+ new_pixel_area.add(((colon_height + row) * font_width + col) as usize);
829
+ let src_byte = *src_addr;
830
+ if src_byte == eight_fg {
831
+ *dst_addr = minus_fg;
832
+ } else {
833
+ *dst_addr = src_byte;
834
+ }
835
+ }
836
+ }
837
+ }
838
+
839
+ // Step 8: update font_obj fields.
840
+ (*font_obj).glyph_table = new_glyph_table;
841
+ (*font_obj).height = new_height as u16;
842
+ 1
843
+ }
crates/openwa-core/src/render/display/gfx.rs CHANGED
@@ -1,5 +1,5 @@
1
  use super::base::DisplayBase;
2
- use super::vtable::DisplayVtable;
3
  use crate::bitgrid::DisplayBitGrid;
4
  use core::ffi::c_void;
5
 
@@ -42,7 +42,7 @@ pub struct DisplayGfx {
42
  // DisplayBase (0x0000 - 0x355F)
43
  // =========================================================================
44
  /// 0x0000: DisplayBase fields (vtable, sprite cache, slot table, etc.)
45
- pub base: DisplayBase<*const DisplayVtable>,
46
 
47
  // =========================================================================
48
  // Display dimensions and clip rect (0x3560 - 0x357F)
 
1
  use super::base::DisplayBase;
2
+ use super::vtable::DisplayGfxVtable;
3
  use crate::bitgrid::DisplayBitGrid;
4
  use core::ffi::c_void;
5
 
 
42
  // DisplayBase (0x0000 - 0x355F)
43
  // =========================================================================
44
  /// 0x0000: DisplayBase fields (vtable, sprite cache, slot table, etc.)
45
+ pub base: DisplayBase<*const DisplayGfxVtable>,
46
 
47
  // =========================================================================
48
  // Display dimensions and clip rect (0x3560 - 0x357F)
crates/openwa-core/src/render/display/gradient.rs CHANGED
@@ -10,6 +10,8 @@ use crate::bitgrid::{BitGrid, BitGridBaseVtable};
10
  use crate::engine::ddgame::DDGame;
11
  use crate::rebase::rb;
12
  use crate::render::sprite::gfx_dir::call_gfx_find_and_load;
 
 
13
  use crate::wa_alloc::{wa_free, wa_malloc};
14
 
15
  /// Palette context for gradient color mapping.
@@ -106,14 +108,14 @@ impl PaletteContext {
106
  #[cfg(target_arch = "x86")]
107
  pub(crate) unsafe fn compute_complex_gradient(
108
  ddgame: *mut DDGame,
109
- land_layer: *mut u8,
110
  layer3_ctx: *mut u8,
111
  sky_height: i16,
112
  ) {
113
  let mut palette = PaletteContext::new();
114
 
115
  // Step 1: Load gradient.img
116
- let gradient_sprite = call_gfx_find_and_load(land_layer, c"gradient.img", layer3_ctx);
117
  if gradient_sprite.is_null() {
118
  return;
119
  }
@@ -142,7 +144,7 @@ pub(crate) unsafe fn compute_complex_gradient(
142
 
143
  // Step 3: If heights match, also set gradient_image_2
144
  if target_rows == gradient_height {
145
- let gradient2 = call_gfx_find_and_load(land_layer, c"gradient.img", layer3_ctx);
146
  (*ddgame).gradient_image_2 = gradient2;
147
  }
148
 
 
10
  use crate::engine::ddgame::DDGame;
11
  use crate::rebase::rb;
12
  use crate::render::sprite::gfx_dir::call_gfx_find_and_load;
13
+ #[cfg(target_arch = "x86")]
14
+ use crate::render::sprite::gfx_dir::GfxDir;
15
  use crate::wa_alloc::{wa_free, wa_malloc};
16
 
17
  /// Palette context for gradient color mapping.
 
108
  #[cfg(target_arch = "x86")]
109
  pub(crate) unsafe fn compute_complex_gradient(
110
  ddgame: *mut DDGame,
111
+ land_dir: *mut GfxDir,
112
  layer3_ctx: *mut u8,
113
  sky_height: i16,
114
  ) {
115
  let mut palette = PaletteContext::new();
116
 
117
  // Step 1: Load gradient.img
118
+ let gradient_sprite = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx);
119
  if gradient_sprite.is_null() {
120
  return;
121
  }
 
144
 
145
  // Step 3: If heights match, also set gradient_image_2
146
  if target_rows == gradient_height {
147
+ let gradient2 = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx);
148
  (*ddgame).gradient_image_2 = gradient2;
149
  }
150
 
crates/openwa-core/src/render/display/mod.rs CHANGED
@@ -1,5 +1,6 @@
1
  pub mod base;
2
  pub mod context;
 
3
  pub mod gfx;
4
  pub mod gradient;
5
  pub mod line_draw;
@@ -11,4 +12,4 @@ pub use base::{DisplayBase, DisplayBaseVtable, SpriteBufferCtrl, SpriteCache};
11
  pub use context::{FastcallResult, RenderContext};
12
  pub use gfx::DisplayGfx;
13
  pub use palette::{Palette, PaletteVtable};
14
- pub use vtable::{DisplayVtable, DrawScaledSpriteResult};
 
1
  pub mod base;
2
  pub mod context;
3
+ pub mod font;
4
  pub mod gfx;
5
  pub mod gradient;
6
  pub mod line_draw;
 
12
  pub use context::{FastcallResult, RenderContext};
13
  pub use gfx::DisplayGfx;
14
  pub use palette::{Palette, PaletteVtable};
15
+ pub use vtable::{DisplayGfxVtable, DrawScaledSpriteResult};
crates/openwa-core/src/render/display/vtable.rs CHANGED
@@ -1,5 +1,14 @@
 
 
 
 
1
  use crate::fixed::Fixed;
 
 
 
 
2
  use crate::render::display::line_draw::Vertex;
 
3
  use crate::render::sprite::sprite::{LayerSprite, LayerSpriteFrame};
4
  use crate::render::SpriteCache;
5
  use crate::wa_alloc::wa_malloc_struct_zeroed;
@@ -35,8 +44,8 @@ use crate::wa_alloc::wa_malloc_struct_zeroed;
35
  /// Coordinate conventions:
36
  /// - Methods that add `camera * 0x10000` take Fixed (16.16) coordinates.
37
  /// - Methods that add `camera` directly take pixel-integer coordinates.
38
- #[openwa_core::vtable(size = 38, va = 0x0066_A218, class = "DisplayGfx")]
39
- pub struct DisplayVtable {
40
  /// destructor (0x569CE0, RET 0x4)
41
  #[slot(0)]
42
  pub destructor: fn(this: *mut DisplayGfx, flags: u8) -> *mut DisplayGfx,
@@ -86,10 +95,10 @@ pub struct DisplayVtable {
86
  pub draw_text_on_bitmap: fn(
87
  this: *mut DisplayGfx,
88
  font_id: i32,
89
- bitmap: *mut crate::bitgrid::BitGrid,
90
  pen_x: i32,
91
  pen_y: i32,
92
- msg: *const core::ffi::c_char,
93
  out_pen_x: *mut i32,
94
  out_width: *mut i32,
95
  ) -> i32,
@@ -245,11 +254,7 @@ pub struct DisplayVtable {
245
  pub set_layer_visibility: fn(this: *mut DisplayGfx, layer: i32, visible: i32),
246
  /// update palette from PaletteContext (0x56A610, RET 0x8)
247
  #[slot(24)]
248
- pub update_palette: fn(
249
- this: *mut DisplayGfx,
250
- palette_ctx: *mut crate::render::palette::PaletteContext,
251
- commit: i32,
252
- ),
253
  // Slot 25: stub (CGameTask__vt19)
254
  /// flush pending render state (0x56A580, plain RET)
255
  ///
@@ -281,8 +286,8 @@ pub struct DisplayVtable {
281
  layer: u32,
282
  id: u32,
283
  flag: u32,
284
- gfx: *mut u8,
285
- name: *const core::ffi::c_char,
286
  ) -> i32,
287
  /// check if a sprite ID is loaded (0x56A480, RET 0x4)
288
  ///
@@ -338,15 +343,15 @@ pub struct DisplayVtable {
338
  this: *mut DisplayGfx,
339
  mode: i32,
340
  font_id: i32,
341
- gfx: *mut u8,
342
- filename: *const core::ffi::c_char,
343
  ) -> u32,
344
  /// load .fex font extension for a font slot (0x523620, RET 0x14)
345
  #[slot(35)]
346
  pub load_font_extension: fn(
347
  this: *mut DisplayGfx,
348
  font_id: i32,
349
- path: *const core::ffi::c_char,
350
  char_map: *const u8,
351
  palette_value: u32,
352
  flag: i32,
@@ -360,13 +365,13 @@ pub struct DisplayVtable {
360
  this: *mut DisplayGfx,
361
  layer: u32,
362
  id: u32,
363
- gfx: *mut u8,
364
- name: *const core::ffi::c_char,
365
  ) -> i32,
366
  }
367
 
368
  // Generate calling wrappers: DisplayGfx::set_layer_color(), etc.
369
- bind_DisplayVtable!(DisplayGfx, base.vtable);
370
 
371
  // =========================================================================
372
  // Ported DisplayGfx vtable methods
@@ -1420,7 +1425,7 @@ pub unsafe extern "thiscall" fn set_layer_color(this: *mut DisplayGfx, layer: i3
1420
  /// The scan area is: `slot_table_guard` (guard=0) + `slot_table[0..255]` + `slot_table_sentinel` (sentinel=-1).
1421
  /// The guard at index 0 is always 0, so allocations start from index 1.
1422
  /// The sentinel (-1) at index 256 terminates the scan with failure.
1423
- unsafe fn palette_slot_alloc(base: &mut DisplayBase<*const DisplayVtable>, count: i32) -> i32 {
1424
  let count = count as usize;
1425
  let table = &base.slot_table_guard as *const u32;
1426
  // Total entries: slot_table_guard(1) + slot_table(255) + slot_table_sentinel(1) = 257
@@ -1661,12 +1666,12 @@ pub unsafe fn load_sprite(
1661
  layer: u32,
1662
  id: u32,
1663
  flag: u32,
1664
- _gfx: *mut u8,
1665
- _name: *const core::ffi::c_char,
1666
  load_sprite_from_vfs: unsafe extern "cdecl" fn(
1667
  sprite: *mut Sprite,
1668
- gfx: *mut u8,
1669
- name: *const core::ffi::c_char,
1670
  layer_ctx: u32,
1671
  ) -> i32,
1672
  ) -> i32 {
@@ -1699,7 +1704,7 @@ pub unsafe fn load_sprite(
1699
 
1700
  // Load from VFS
1701
  let layer_ctx = base.layer_contexts[layer as usize];
1702
- let result = load_sprite_from_vfs(sprite, _gfx, _name, layer_ctx);
1703
  if result == 0 {
1704
  // Load failed β€” destroy sprite via vtable[0]
1705
  if !sprite.is_null() {
@@ -1740,9 +1745,9 @@ pub unsafe fn load_sprite(
1740
  /// All pointers must be valid. `sprite` must be a zeroed 0x70-byte allocation.
1741
  pub unsafe fn load_sprite_by_name(
1742
  sprite: *mut LayerSprite,
1743
- gfx_dir: *mut u8,
1744
  palette_ctx: *mut PaletteContext,
1745
- name: *const core::ffi::c_char,
1746
  ) -> i32 {
1747
  use crate::address::va;
1748
  use crate::rebase::rb;
@@ -2042,8 +2047,8 @@ pub unsafe fn load_sprite_by_layer(
2042
  this: *mut DisplayGfx,
2043
  layer: u32,
2044
  id: u32,
2045
- gfx: *mut u8,
2046
- name: *const core::ffi::c_char,
2047
  ) -> i32 {
2048
  use crate::wa_alloc::wa_malloc_zeroed;
2049
 
@@ -2077,7 +2082,7 @@ pub unsafe fn load_sprite_by_layer(
2077
  (*sprite).gfx_dir = core::ptr::null_mut();
2078
 
2079
  // Load sprite data β€” pure Rust port of FUN_005733b0
2080
- let result = load_sprite_by_name(sprite, gfx, palette_ctx, name);
2081
  if result == 0 {
2082
  free_layer_sprite(sprite);
2083
  return 0;
@@ -2097,281 +2102,17 @@ pub unsafe fn load_sprite_by_layer(
2097
  // that's impractical to bridge directly. Porting requires first porting
2098
  // the FrameCache + decompression chain (FUN_004FA950 β†’ FUN_005B29E0).
2099
 
2100
- // =========================================================================
2101
- // Font loading
2102
- // =========================================================================
2103
-
2104
- /// Font object β€” 0x1C bytes.
2105
- ///
2106
- /// Allocated by `load_font` (vtable slot 34) from a `.fnt` file loaded via
2107
- /// GfxDir. Referenced from `DisplayBase::font_table` (offset 0x309C). Read by
2108
- /// `Font__GetInfo` / `Font__GetMetric` / `Font__DrawText` / `Font__SetPalette`
2109
- /// to render bitmap text.
2110
- ///
2111
- /// Field semantics derived from `FUN_004f99d0` (the parser) and `Font__GetInfo`
2112
- /// (0x4fa7d0, which confirms width at +0, width_div_5 at +2, glyph count at +4,
2113
- /// and glyph table at +8 with 12-byte entries whose byte +4 is a max metric).
2114
- #[repr(C)]
2115
- pub struct FontObject {
2116
- /// 0x00: Font max width in pixels (read from .fnt metadata).
2117
- pub width: u16,
2118
- /// 0x02: `width / 5 + 1` β€” initial max metric seed in `Font__GetInfo`.
2119
- pub width_div_5: u16,
2120
- /// 0x04: Glyph count / font height.
2121
- pub height: u16,
2122
- /// 0x06: Duplicate of height.
2123
- pub _height2: u16,
2124
- /// 0x08: Pointer to glyph table (height entries of 12 bytes each).
2125
- pub glyph_table: *mut GlyphEntry,
2126
- /// 0x0C: Pointer to a 256-byte char→glyph-index lookup table that lives
2127
- /// in the .fnt buffer immediately after the RGB palette triplets.
2128
- /// Each entry is a 1-based glyph index (or 0 if the codepoint has no
2129
- /// glyph) β€” `Font__GetMetric` / `Font__SetParam` use it to map a string
2130
- /// byte to its `glyph_table[index - 1]` entry. `font_extend` writes new
2131
- /// codepoints into this table.
2132
- pub char_to_glyph_idx: *mut u8,
2133
- /// 0x10: Pointer to remapped pixel data in the source buffer.
2134
- pub pixel_data: *mut u8,
2135
- /// 0x14: Zeroed by `load_font`; purpose unknown.
2136
- pub _unknown_14: u32,
2137
- /// 0x18: When non-null, holds the address of an auxiliary allocation
2138
- /// owned by this font (a separate buffer of glyph entries + pixel data
2139
- /// pointed at by individual `GlyphEntry::pixel_offset` deltas). Zeroed
2140
- /// by `load_font`; written by `font_extend` (which appends new glyphs
2141
- /// from a `.fex` file) and `font_set_palette_impl` (which derives the
2142
- /// `.`/`;` glyphs for the digital font). The cleanup path frees this
2143
- /// when the FontObject is destroyed.
2144
- pub aux_alloc: u32,
2145
- }
2146
-
2147
- const _: () = assert!(core::mem::size_of::<FontObject>() == 0x1C);
2148
-
2149
- /// Per-glyph metadata in a `.fnt` file (12 bytes).
2150
- ///
2151
- /// Indexed by `FontObject::glyph_table`. Layout matches the loop stride of 0xC
2152
- /// used by the parser, `Font__GetInfo` (0x4fa7d0), and `Font__GetMetric` (0x4fa780).
2153
- ///
2154
- /// `Font__GetMetric` reads `width` (offset +4) as the character advance metric.
2155
- /// `Font__DrawText` uses `pixel_offset` as a byte delta from
2156
- /// `FontObject::pixel_data` to locate the glyph's bitmap rows. The offset can
2157
- /// be negative when `font_extend` adds glyphs from a separate allocation.
2158
- #[repr(C)]
2159
- pub struct GlyphEntry {
2160
- /// 0x00: Glyph bounding-box top-left X within the glyph cell.
2161
- pub start_x: u16,
2162
- /// 0x02: Glyph bounding-box top-left Y within the glyph cell.
2163
- pub start_y: u16,
2164
- /// 0x04: Glyph bounding-box width in pixels (also used as the advance metric).
2165
- pub width: u16,
2166
- /// 0x06: Glyph bounding-box height in pixels.
2167
- pub height: u16,
2168
- /// 0x08: Byte delta from `FontObject::pixel_data` to this glyph's bitmap.
2169
- /// Treated as a signed offset by drawing code so `font_extend` can point
2170
- /// glyphs at pixels that live in a different allocation.
2171
- pub pixel_offset: u32,
2172
- }
2173
-
2174
- const _: () = assert!(core::mem::size_of::<GlyphEntry>() == 0xC);
2175
-
2176
- /// Fixed prefix of a `.fnt` binary blob (0xC bytes).
2177
- ///
2178
- /// Followed in memory by: packed RGB triplets (`palette_count * 3` bytes),
2179
- /// a 0x100-byte character-width table, `max_width: i16`, `glyph_count: u16`,
2180
- /// 4-byte alignment padding, `GlyphEntry[glyph_count]`, then the packed
2181
- /// glyph pixel data. `font_parse_data` walks this variable-length tail using
2182
- /// byte pointers rather than a single `#[repr(C)]` struct.
2183
- #[repr(C)]
2184
- pub struct FntHeader {
2185
- /// 0x00: Unknown / version.
2186
- pub _unknown_00: u32,
2187
- /// 0x04: Total size of this record in bytes, including this field.
2188
- pub data_size: i32,
2189
- /// 0x08: Unknown u16 (skipped by the parser).
2190
- pub _unknown_08: u16,
2191
- /// 0x0A: Number of palette entries that follow this header.
2192
- pub palette_count: u16,
2193
- // +0x0C: packed RGB triplets (variable length)
2194
- }
2195
-
2196
- const _: () = assert!(core::mem::size_of::<FntHeader>() == 0xC);
2197
-
2198
- /// Port of `FUN_004f99d0` β€” parses `.fnt` binary data into a `FontObject`.
2199
- ///
2200
- /// Original convention: cdecl with 1 stack arg (buffer), `unaff_EDI = font_obj`.
2201
- /// Ported as a plain Rust function.
2202
- ///
2203
- /// Buffer layout:
2204
- /// ```text
2205
- /// +0x00 u32 (unknown / version)
2206
- /// +0x04 i32 data_size (total bytes in this record, including this field)
2207
- /// +0x08 u16 (unknown)
2208
- /// +0x0A u16 palette_count
2209
- /// +0x0C RGB[palette_count * 3] packed RGB triplets
2210
- /// +... u8[0x100] character-width table (1 byte per codepoint)
2211
- /// +0x100 u16 max_width
2212
- /// +0x102 u16 glyph_count / height
2213
- /// +... align to 4 from buffer start
2214
- /// +... GlyphEntry[glyph_count] 12 bytes each (start_x/start_y/end_x/end_y/...)
2215
- /// +... u8[] packed 4bpp glyph pixels (remapped in place via palette LUT)
2216
- /// ```
2217
- ///
2218
- /// # Safety
2219
- /// `font_obj` must be a valid writable `FontObject`. `header` must point to a
2220
- /// freshly-loaded `.fnt` blob at least `header.data_size` bytes long and
2221
- /// followed in memory by the variable-length tail described above.
2222
- /// `palette_ctx` must be a valid `PaletteContext`.
2223
- pub unsafe fn font_parse_data(
2224
- font_obj: *mut FontObject,
2225
- header: *mut FntHeader,
2226
- palette_ctx: *mut PaletteContext,
2227
- ) -> u32 {
2228
- use crate::render::palette::{palette_map_color, remap_pixels_through_lut};
2229
-
2230
- let data_size = (*header).data_size;
2231
- let palette_count = (*header).palette_count as i32;
2232
- let base_bytes = header as *mut u8;
2233
-
2234
- // Cursor starts at the first byte after the fixed header (the RGB triplets).
2235
- let mut cursor = base_bytes.add(core::mem::size_of::<FntHeader>());
2236
-
2237
- // Build 256-byte palette LUT. Entry 0 = map_color(0); entries 1..=palette_count
2238
- // come from the RGB triplets. The original reads 4 bytes per triplet but
2239
- // advances by 3 β€” only the low 24 bits matter.
2240
- let mut palette_lut = [0u8; 256];
2241
- palette_lut[0] = palette_map_color(palette_ctx, 0) as u8;
2242
- for i in 0..palette_count {
2243
- let rgb = *(cursor as *const u32);
2244
- palette_lut[(1 + i) as usize] = palette_map_color(palette_ctx, rgb) as u8;
2245
- cursor = cursor.add(3);
2246
- }
2247
-
2248
- // char_to_glyph_idx points to the 256-byte char→glyph-index table that
2249
- // sits immediately after the RGB palette triplets in the .fnt buffer.
2250
- (*font_obj).char_to_glyph_idx = cursor;
2251
-
2252
- // Read max_width (i16) at cursor + 0x100 β€” the character-width table sits
2253
- // between the palette and the header.
2254
- let max_width = *(cursor.add(0x100) as *const i16) as i32;
2255
- cursor = cursor.add(0x100);
2256
- (*font_obj).width = max_width as u16;
2257
-
2258
- // Seed value for Font__GetInfo's max metric: signed (width / 5) + 1,
2259
- // with a +1 correction when the quotient is negative (matches the
2260
- // IMUL+SAR+SHR+LEA sequence in the original).
2261
- let w_div_5 = max_width / 5;
2262
- let seed = w_div_5.wrapping_add((w_div_5 >> 31) & 1).wrapping_add(1);
2263
- (*font_obj).width_div_5 = seed as u16;
2264
-
2265
- // Skip max_width, read glyph_count (u16).
2266
- cursor = cursor.add(2);
2267
- let glyph_count = *(cursor as *const u16);
2268
- cursor = cursor.add(2);
2269
- (*font_obj).height = glyph_count;
2270
- (*font_obj)._height2 = glyph_count;
2271
-
2272
- // Align cursor to 4 bytes relative to the buffer start.
2273
- let base_addr = base_bytes as usize;
2274
- while (cursor as usize - base_addr) & 3 != 0 {
2275
- cursor = cursor.add(1);
2276
- }
2277
-
2278
- // Glyph table: glyph_count entries of 12 bytes each.
2279
- let glyph_table = cursor as *mut GlyphEntry;
2280
- (*font_obj).glyph_table = glyph_table;
2281
- let pixel_data = glyph_table.add(glyph_count as usize) as *mut u8;
2282
- (*font_obj).pixel_data = pixel_data;
2283
-
2284
- // Pixel byte count: data_size - (pixel_data - buffer), rounded down to dwords.
2285
- // Original uses CDQ+AND 3+ADD+SAR 2 which rounds toward zero.
2286
- let remaining = data_size - (pixel_data as usize - base_addr) as i32;
2287
- let dword_count = remaining
2288
- .wrapping_add((remaining >> 31) & 3)
2289
- .wrapping_shr(2) as u32;
2290
-
2291
- remap_pixels_through_lut(pixel_data, 0, palette_lut.as_ptr(), dword_count, 1);
2292
-
2293
- 1
2294
- }
2295
-
2296
- /// Port of `FUN_004f9940` β€” loads a font resource by name from a GfxDir and
2297
- /// feeds it into `font_parse_data`.
2298
- ///
2299
- /// Original convention: usercall with `EAX = gfx_dir`, `ECX = font_obj`,
2300
- /// `stack[0] = layer_ctx`, `stack[1] = filename`, `RET 0x8`. The function
2301
- /// first tries `GfxDir__FindEntry` + cached-load via `gfx_dir->vtable[2]`;
2302
- /// on miss it falls back to `GfxDir__LoadImage` + `image->vtable[4]` (get_size)
2303
- /// + `image->vtable[5]` (read into caller-allocated buffer) + `image->vtable[0](1)`
2304
- /// (destroy), then tail-calls the parser.
2305
- ///
2306
- /// Note: `layer_ctx` is only used by `font_parse_data` (as the PaletteContext).
2307
- /// The original never touches it in this function β€” it just forwards it via
2308
- /// ECX to the tail call.
2309
- ///
2310
- /// # Safety
2311
- /// `font_obj` must be a valid writable `FontObject`. `gfx_dir` must be a valid
2312
- /// GfxDir. `palette_ctx` must be a valid PaletteContext. `name` must be a
2313
- /// valid null-terminated C string.
2314
- pub unsafe fn font_load_from_gfx(
2315
- font_obj: *mut FontObject,
2316
- gfx_dir: *mut u8,
2317
- palette_ctx: *mut PaletteContext,
2318
- name: *const core::ffi::c_char,
2319
- ) -> u32 {
2320
- use crate::render::sprite::gfx_dir::{call_gfx_load_image, gfx_dir_find_entry, GfxDirEntry};
2321
- use crate::wa_alloc::wa_malloc;
2322
-
2323
- // 1. Try FindEntry β†’ gfx_dir->vtable[2](entry->value) for cached load.
2324
- let entry = gfx_dir_find_entry(name, gfx_dir);
2325
- if !entry.is_null() {
2326
- let vt = *(gfx_dir as *const *const u32);
2327
- let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut FntHeader =
2328
- core::mem::transmute(*vt.add(2));
2329
- let entry_val = (*(entry as *const GfxDirEntry)).value;
2330
- let cached = load_cached(gfx_dir, entry_val);
2331
- if !cached.is_null() {
2332
- return font_parse_data(font_obj, cached, palette_ctx);
2333
- }
2334
- }
2335
-
2336
- // 2. Fallback: LoadImage β†’ vtable[4] (size) β†’ wa_malloc β†’ vtable[5] (read) β†’ vtable[0] (destroy)
2337
- let image = call_gfx_load_image(gfx_dir, name);
2338
- if image.is_null() {
2339
- return 0;
2340
- }
2341
-
2342
- let image_vt = *(image as *const *const u32);
2343
-
2344
- // vtable[4] β€” get size (thiscall, no args)
2345
- let get_size: unsafe extern "thiscall" fn(*mut u8) -> u32 =
2346
- core::mem::transmute(*image_vt.add(4));
2347
- let size = get_size(image);
2348
-
2349
- // Match the original's allocation: round size up to 4-byte multiple, add 0x20 guard.
2350
- let alloc_size = ((size + 3) & !3u32).wrapping_add(0x20);
2351
- let buffer = wa_malloc(alloc_size);
2352
- // Original memsets only `size` bytes, not the full allocation.
2353
- if !buffer.is_null() {
2354
- core::ptr::write_bytes(buffer, 0, size as usize);
2355
- }
2356
-
2357
- // vtable[5] β€” read into buffer (thiscall with 2 stack args: buffer, size)
2358
- let read_into: unsafe extern "thiscall" fn(*mut u8, *mut u8, u32) =
2359
- core::mem::transmute(*image_vt.add(5));
2360
- read_into(image, buffer, size);
2361
-
2362
- // vtable[0] β€” destroy image (thiscall with 1 arg: flags = 1)
2363
- let destroy: unsafe extern "thiscall" fn(*mut u8, u32) = core::mem::transmute(*image_vt);
2364
- destroy(image, 1);
2365
-
2366
- font_parse_data(font_obj, buffer as *mut FntHeader, palette_ctx)
2367
- }
2368
-
2369
  /// Port of `DisplayGfx::LoadFont` (vtable slot 34, 0x523560).
2370
  ///
2371
- /// Validates `mode` (1..=3) and `font_id` (1..=31), then allocates a
2372
  /// zero-initialized 0x1C-byte `FontObject`, loads the named resource via
2373
  /// `font_load_from_gfx`, and stores the object in `DisplayBase::font_table`.
2374
  ///
 
 
 
 
 
2375
  /// On load failure, the partially-initialized font object is leaked to match
2376
  /// the original's behavior (it calls a sprite-bank-style cleanup helper
2377
  /// `FUN_005230c0` which is not exercised here).
@@ -2382,19 +2123,19 @@ pub unsafe fn font_load_from_gfx(
2382
  /// null-terminated C string.
2383
  pub unsafe fn load_font(
2384
  this: *mut DisplayGfx,
2385
- mode: i32,
2386
  font_id: i32,
2387
- _gfx: *mut u8,
2388
- filename: *const core::ffi::c_char,
2389
  ) -> u32 {
2390
  use crate::wa_alloc::wa_malloc_struct_zeroed;
2391
 
2392
- // Validate mode (1..=3) and that the layer context exists.
2393
- if !(1..=3).contains(&mode) {
2394
  return 0;
2395
  }
 
2396
  let base = &mut (*this).base;
2397
- let layer_ctx = base.layer_contexts[mode as usize];
2398
  if layer_ctx == 0 {
2399
  return 0;
2400
  }
@@ -2403,19 +2144,24 @@ pub unsafe fn load_font(
2403
  if !(1..=31).contains(&font_id) {
2404
  return 0;
2405
  }
2406
- if base.font_table[font_id as usize] != 0 {
2407
  return 0;
2408
  }
2409
 
2410
  // Allocate zeroed FontObject. The original uses WA_MallocMemset(0x1C)
2411
  // which only memsets the requested size; wa_malloc_struct_zeroed matches.
2412
- let font_obj = wa_malloc_struct_zeroed::<FontObject>();
2413
  if font_obj.is_null() {
2414
  return 0;
2415
  }
2416
 
2417
  // Load and parse the font data.
2418
- let result = font_load_from_gfx(font_obj, _gfx, layer_ctx as *mut PaletteContext, filename);
 
 
 
 
 
2419
  if result == 0 {
2420
  // Original leaks here on failure (via an unported cleanup helper).
2421
  // We match that behavior rather than introducing a free path that
@@ -2423,291 +2169,16 @@ pub unsafe fn load_font(
2423
  return 0;
2424
  }
2425
 
2426
- // Install into font_table. The original also records which mode owns this
2427
- // font slot at DisplayBase + 0x301C + font_id*4 (the gap region next to
2428
- // font_table), then bumps the layer visibility counter.
2429
- base.font_table[font_id as usize] = font_obj as u32;
2430
- base._gap_301c[font_id as usize] = mode as u32;
2431
- base.layer_visibility[mode as usize] += 1;
2432
 
2433
  1
2434
  }
2435
 
2436
- /// Port of `FUN_004f9ad0` β€” extends an existing `FontObject` with new glyphs
2437
- /// loaded from a `.fex` (font extension) file.
2438
- ///
2439
- /// Original convention: usercall with `EAX = filename`, `ESI = font_obj`,
2440
- /// stack args = (`layer_ctx`, `char_map`, `palette_value`), `RET 0xC`.
2441
- ///
2442
- /// The `.fex` file contains raw pixel data for new glyphs (no header). Each
2443
- /// glyph occupies `font.width * stride` bytes (where `stride = font.width`,
2444
- /// or `font.width + 1` for fonts with `width >= 16`). The new bitmap also
2445
- /// gets remapped through a 256-byte LUT built by walking R/G/B in lockstep
2446
- /// with the per-channel steps packed into `palette_value` and using
2447
- /// `palette_find_nearest_cached` to map each composed RGB to a palette index.
2448
- ///
2449
- /// On success the function:
2450
- /// - allocates a single new buffer holding `[old + new glyph entries][new pixels]`
2451
- /// - copies the old glyph entries verbatim into the front of the new buffer
2452
- /// - writes per-codepoint indices into the font's char-width table (the bytes
2453
- /// referenced by `font_obj.char_to_glyph_idx`)
2454
- /// - reads the new pixel rows from disk via `_fread`
2455
- /// - computes a tight bounding box per new glyph and writes it as a new entry
2456
- /// - remaps every new pixel byte through the LUT
2457
- /// - swaps `font_obj.glyph_table` to the new buffer and bumps `font_obj.height`
2458
- /// - records the allocation in `font_obj.aux_alloc`
2459
- ///
2460
- /// The `pixel_offset` field in each new glyph entry stores
2461
- /// `(absolute_pixel_addr - font_obj.pixel_data)` so the existing
2462
- /// `Font__DrawText` lookup `pixel_data + offset` still resolves correctly,
2463
- /// even though the new pixels live in a separate allocation.
2464
- ///
2465
- /// # Safety
2466
- /// `font_obj` must be a fully-parsed `FontObject` (created by `load_font`).
2467
- /// `palette_ctx` must be a valid `PaletteContext`. `filename` and `char_map`
2468
- /// must be valid null-terminated C strings.
2469
- pub unsafe fn font_extend(
2470
- font_obj: *mut FontObject,
2471
- palette_ctx: *mut PaletteContext,
2472
- filename: *const core::ffi::c_char,
2473
- char_map: *const u8,
2474
- palette_value: u32,
2475
- ) {
2476
- use crate::address::va;
2477
- use crate::rebase::rb;
2478
- use crate::render::palette::{palette_find_nearest_cached, remap_pixels_through_lut};
2479
- use crate::wa_alloc::wa_malloc;
2480
- use core::ffi::c_char;
2481
-
2482
- // Open the .fex file via WA's CRT _fopen.
2483
- let fopen: unsafe extern "cdecl" fn(*const c_char, *const c_char) -> *mut u8 =
2484
- core::mem::transmute(rb(va::WA_FOPEN) as usize);
2485
- let fread: unsafe extern "cdecl" fn(*mut u8, u32, u32, *mut u8) -> u32 =
2486
- core::mem::transmute(rb(va::WA_FREAD) as usize);
2487
- let fclose: unsafe extern "cdecl" fn(*mut u8) -> i32 =
2488
- core::mem::transmute(rb(va::WA_FCLOSE) as usize);
2489
-
2490
- let file = fopen(filename, c"rb".as_ptr());
2491
- if file.is_null() {
2492
- return;
2493
- }
2494
-
2495
- // ── Geometry ──
2496
- let font_width = (*font_obj).width as i32;
2497
- // Per-glyph row stride in the .fex file. Wide fonts use width+1 to allow
2498
- // an extra byte after each row (matches the `if (font.width > 15)` test).
2499
- let row_stride = if font_width > 15 {
2500
- font_width + 1
2501
- } else {
2502
- font_width
2503
- };
2504
-
2505
- // strlen(char_map) β€” count of new glyphs.
2506
- let mut new_count: i32 = 0;
2507
- let mut p = char_map;
2508
- while *p != 0 {
2509
- p = p.add(1);
2510
- new_count += 1;
2511
- }
2512
-
2513
- let old_height = (*font_obj).height as i32; // existing glyph count
2514
- let total_glyphs = old_height + new_count; // resulting glyph count
2515
- let glyph_entries_bytes = total_glyphs * 12;
2516
- let new_pixels_bytes = new_count * font_width * row_stride;
2517
- let total_size = (glyph_entries_bytes + new_pixels_bytes) as u32;
2518
-
2519
- // Allocate (matches WA_MallocMemset alignment + 0x20 guard).
2520
- let alloc = wa_malloc(((total_size + 3) & !3u32).wrapping_add(0x20));
2521
- if alloc.is_null() {
2522
- let _ = fclose(file);
2523
- return;
2524
- }
2525
- core::ptr::write_bytes(alloc, 0, total_size as usize);
2526
-
2527
- // pixel data area starts immediately after the glyph entries.
2528
- let new_pixels = alloc.add(glyph_entries_bytes as usize);
2529
-
2530
- // Record the allocation so font cleanup paths can free it.
2531
- (*font_obj).aux_alloc = alloc as u32;
2532
-
2533
- // ── Copy existing glyph entries verbatim into the new buffer ──
2534
- let old_glyphs = (*font_obj).glyph_table as *const u8;
2535
- if old_height > 0 && !old_glyphs.is_null() {
2536
- core::ptr::copy_nonoverlapping(old_glyphs, alloc, (old_height * 12) as usize);
2537
- }
2538
-
2539
- // ── Update the per-codepoint charβ†’glyph-index table ──
2540
- // For each new char in char_map, write the new (1-based) glyph index
2541
- // `(old_height + idx + 1)` into the table at `font_obj.char_to_glyph_idx`.
2542
- let char_to_glyph = (*font_obj).char_to_glyph_idx;
2543
- let mut q = char_map;
2544
- for i in 0..new_count {
2545
- let ch = *q as usize;
2546
- q = q.add(1);
2547
- *char_to_glyph.add(ch) = (old_height + i + 1) as u8;
2548
- }
2549
-
2550
- // ── Build 256-byte palette LUT by stepping R/G/B per `palette_value` ──
2551
- // The original walks 256 entries, accumulating signed R/G/B values via the
2552
- // step bytes packed in `palette_value` (low byte=R step, mid=G, high=B),
2553
- // each reduced mod 255 with sign correction (the `IMUL 0x80808081` trick).
2554
- // Each composed RGB is fed to `palette_find_nearest_cached` and the
2555
- // returned palette index becomes lut[i].
2556
- let r_step = (palette_value & 0xff) as i32;
2557
- let g_step = ((palette_value >> 8) & 0xff) as i32;
2558
- let b_step = ((palette_value >> 16) & 0xff) as i32;
2559
-
2560
- let mut palette_lut = [0u8; 256];
2561
- let mut acc_r: i32 = 0;
2562
- let mut acc_g: i32 = 0;
2563
- let mut acc_b: i32 = 0;
2564
- for slot in palette_lut.iter_mut() {
2565
- // Reduce each accumulator mod 255 with sign correction. The original
2566
- // uses signed magic division by 255; for 0..255 the result is just
2567
- // `acc - 0` for 0..254 and `acc - 1` for 255. We use rem_euclid for
2568
- // a clean cross-platform equivalent (palette steps are non-negative
2569
- // so this matches the original even on edge cases).
2570
- let r = mod_255_signed(acc_r);
2571
- let g = mod_255_signed(acc_g);
2572
- let b = mod_255_signed(acc_b);
2573
- let composed = (r as u32) | ((g as u32) << 8) | ((b as u32) << 16);
2574
-
2575
- let mut distance: i32 = 0;
2576
- let idx = palette_find_nearest_cached(palette_ctx, composed, &mut distance) as u8;
2577
- *slot = idx;
2578
-
2579
- acc_r += r_step;
2580
- acc_g += g_step;
2581
- acc_b += b_step;
2582
- }
2583
-
2584
- // ── Read the new pixel rows from disk ──
2585
- // fread(new_pixels, new_count, font_width * row_stride, file)
2586
- let _ = fread(
2587
- new_pixels,
2588
- new_count as u32,
2589
- (font_width * row_stride) as u32,
2590
- file,
2591
- );
2592
- let _ = fclose(file);
2593
-
2594
- // ── Bounding-box scan for each new glyph + write its glyph entry ──
2595
- let mut row_offset_in_pixels: i32 = 0; // i.e. iVar16 in the original
2596
- let new_glyph_base = alloc as *mut GlyphEntry;
2597
- for new_idx in 0..new_count {
2598
- // Find the LEFTMOST non-empty column (== start_x).
2599
- let mut start_x: u16 = 0;
2600
- let mut found_left = false;
2601
- 'outer_left: for col in 0..font_width as u16 {
2602
- for row in 0..font_width as i32 {
2603
- let addr = new_pixels
2604
- .offset(((row + row_offset_in_pixels) * font_width + col as i32) as isize);
2605
- if *addr != 0 {
2606
- start_x = col;
2607
- found_left = true;
2608
- break 'outer_left;
2609
- }
2610
- }
2611
- }
2612
- // Find the TOPMOST non-empty row (== start_y).
2613
- let mut start_y: u16 = 0;
2614
- if found_left {
2615
- 'outer_top: for row in 0..font_width as u16 {
2616
- for col in 0..font_width as i32 {
2617
- let addr = new_pixels
2618
- .offset((col + (row as i32 + row_offset_in_pixels) * font_width) as isize);
2619
- if *addr != 0 {
2620
- start_y = row;
2621
- break 'outer_top;
2622
- }
2623
- }
2624
- }
2625
- }
2626
- // Find the RIGHTMOST non-empty column (scan right→left from font_width-1).
2627
- let mut right: u16 = (font_width - 1).max(0) as u16;
2628
- if found_left && right != 0 {
2629
- 'outer_right: for col in (1..font_width as u16).rev() {
2630
- let mut empty = true;
2631
- for row in 0..font_width as i32 {
2632
- let addr = new_pixels
2633
- .offset(((row + row_offset_in_pixels) * font_width + col as i32) as isize);
2634
- if *addr != 0 {
2635
- empty = false;
2636
- break;
2637
- }
2638
- }
2639
- if !empty {
2640
- right = col;
2641
- break 'outer_right;
2642
- }
2643
- right = col - 1;
2644
- }
2645
- }
2646
- // Find the BOTTOMMOST non-empty row (scan bottom→top from font_width-1).
2647
- let mut bottom: u16 = (font_width - 1).max(0) as u16;
2648
- if found_left {
2649
- loop {
2650
- let mut empty = true;
2651
- for col in 0..font_width as i32 {
2652
- let addr = new_pixels.offset(
2653
- (col + (bottom as i32 + row_offset_in_pixels) * font_width) as isize,
2654
- );
2655
- if *addr != 0 {
2656
- empty = false;
2657
- break;
2658
- }
2659
- }
2660
- if !empty || bottom == 0 {
2661
- break;
2662
- }
2663
- bottom -= 1;
2664
- }
2665
- }
2666
-
2667
- // Write the new glyph entry. The slot index is (old_height + new_idx).
2668
- let entry = new_glyph_base.add((old_height + new_idx) as usize);
2669
- (*entry).start_x = start_x;
2670
- (*entry).start_y = start_y;
2671
- (*entry).width = right.saturating_sub(start_x) + 1;
2672
- (*entry).height = bottom.saturating_sub(start_y) + 1;
2673
- // pixel_offset = absolute address of the glyph's bitmap top-left,
2674
- // expressed as a delta from font_obj.pixel_data so existing draw code
2675
- // can do `pixel_data + offset` and reach the new allocation.
2676
- let abs_addr = new_pixels.offset(
2677
- (start_x as i32 + (start_y as i32 + row_offset_in_pixels) * font_width) as isize,
2678
- );
2679
- (*entry).pixel_offset = (abs_addr as u32).wrapping_sub((*font_obj).pixel_data as u32);
2680
-
2681
- // Advance to the next glyph's pixel rows.
2682
- row_offset_in_pixels += row_stride;
2683
- }
2684
-
2685
- // ── Remap every new pixel byte through the LUT ──
2686
- let total_pixel_bytes = font_width * new_count * row_stride;
2687
- if total_pixel_bytes > 0 {
2688
- // remap_pixels_through_lut takes (data, pitch, lut, width_dwords, height).
2689
- // The original walks each byte one at a time, so use width_dwords = 1
2690
- // and height = total_bytes / 4 ... actually simpler: do a flat single-row
2691
- // remap with width_dwords = total/4 and height = 1.
2692
- let dword_count = (total_pixel_bytes as u32 + 3) / 4;
2693
- remap_pixels_through_lut(new_pixels, 0, palette_lut.as_ptr(), dword_count, 1);
2694
- }
2695
-
2696
- // ── Swap glyph_table and bump glyph count ──
2697
- (*font_obj).glyph_table = alloc as *mut GlyphEntry;
2698
- (*font_obj).height = (old_height + new_count) as u16;
2699
- }
2700
-
2701
- /// Helper for `font_extend`'s palette stepping. Reduces a non-negative i32
2702
- /// modulo 255 the way the original signed-magic-division sequence does.
2703
- #[inline]
2704
- fn mod_255_signed(value: i32) -> i32 {
2705
- // For non-negative values this is just `value % 255`.
2706
- // The original uses signed magic-division so we mirror that for safety.
2707
- let q = value / 255;
2708
- value - q * 255
2709
- }
2710
-
2711
  /// Port of `DisplayGfx::LoadFontExtension` (vtable slot 35, 0x523620).
2712
  ///
2713
  /// Validates `font_id`, looks up the existing font object, resolves the
@@ -2721,7 +2192,7 @@ fn mod_255_signed(value: i32) -> i32 {
2721
  pub unsafe fn load_font_extension(
2722
  this: *mut DisplayGfx,
2723
  font_id: i32,
2724
- path: *const core::ffi::c_char,
2725
  char_map: *const u8,
2726
  palette_value: u32,
2727
  _flag: i32,
@@ -2733,10 +2204,10 @@ pub unsafe fn load_font_extension(
2733
  }
2734
  let base = &mut (*this).base;
2735
  let font_obj_addr = base.font_table[font_id as usize];
2736
- if font_obj_addr == 0 {
2737
  return 0;
2738
  }
2739
- let font_obj = font_obj_addr as *mut FontObject;
2740
 
2741
  // Resolve the RGB color via the layer-1 palette context (DisplayBase+0x3120).
2742
  // The original always reads layer_contexts[1] here, regardless of which
@@ -2746,124 +2217,15 @@ pub unsafe fn load_font_extension(
2746
  let mut resolved_rgb: u32 = 0;
2747
  let _ = palette_context_lookup_entry(layer1_ctx, palette_value as i32, &mut resolved_rgb);
2748
 
2749
- // The actual font extension call uses the font's owning mode's palette ctx.
2750
- let mode = base._gap_301c[font_id as usize] as usize;
2751
- let layer_ctx = base.layer_contexts[mode] as *mut PaletteContext;
2752
 
2753
  font_extend(font_obj, layer_ctx, path, char_map, resolved_rgb);
2754
- 1
2755
- }
2756
-
2757
- // =========================================================================
2758
- // Font__GetInfo / Font__GetMetric / Font__SetParam ports
2759
- // =========================================================================
2760
- //
2761
- // These three helpers (originally at 0x4FA7D0, 0x4FA780, 0x4FA720) are pure
2762
- // reads against a `FontObject`. They were the last bridge calls remaining in
2763
- // `DisplayGfx::{GetFontInfo,GetFontMetric,SetFontParam}` (vtable slots 8/9/10).
2764
-
2765
- /// Read a glyph's advance metric from the glyph table. Returns
2766
- /// `glyph_table[idx_1based - 1].width as i32 + 1` (matching the original's
2767
- /// `*(ushort*)(glyph_table - 8 + idx*0xC) + 1`).
2768
- #[inline]
2769
- unsafe fn glyph_advance_metric(font_obj: *const FontObject, idx_1based: u8) -> i32 {
2770
- let entry = (*font_obj).glyph_table.add(idx_1based as usize - 1);
2771
- (*entry).width as i32 + 1
2772
- }
2773
 
2774
- /// Pure-Rust port of `Font__GetInfo` (0x4FA7D0).
2775
- ///
2776
- /// Writes the font's max width to `*out_width` and the longest glyph
2777
- /// advance metric (max of `width_div_5` and `glyph.width + 1` for every
2778
- /// glyph) to `*out_max_metric`. Returns 1 unconditionally.
2779
- ///
2780
- /// # Safety
2781
- /// `font_obj` must be a valid `*const FontObject` with a glyph table of at
2782
- /// least `font_obj.height` entries. The output pointers must be writable.
2783
- pub unsafe fn font_get_info_impl(
2784
- font_obj: *const FontObject,
2785
- out_max_metric: *mut i32,
2786
- out_width: *mut i32,
2787
- ) -> u32 {
2788
- *out_width = (*font_obj).width as i16 as i32;
2789
- let mut max_metric = (*font_obj).width_div_5 as i16 as i32;
2790
- let height = (*font_obj).height as i32;
2791
- for i in 0..height {
2792
- let entry = (*font_obj).glyph_table.add(i as usize);
2793
- let candidate = (*entry).width as i32 + 1;
2794
- if max_metric < candidate {
2795
- max_metric = candidate;
2796
- }
2797
- }
2798
- *out_max_metric = max_metric;
2799
  1
2800
  }
2801
 
2802
- /// Pure-Rust port of `Font__GetMetric` (0x4FA780).
2803
- ///
2804
- /// Always writes the font's max width to `*out_width`. For space (`0x20`)
2805
- /// and non-breaking space (`0xA0`), writes `width_div_5` to `*out_metric`
2806
- /// and returns 1. Otherwise looks up the codepoint in `char_to_glyph_idx`;
2807
- /// if the codepoint is unmapped (entry is 0) returns 0, else writes
2808
- /// `glyph_table[idx-1].width + 1` to `*out_metric` and returns 1.
2809
- ///
2810
- /// # Safety
2811
- /// `font_obj` must be a valid `*const FontObject`. The output pointers must
2812
- /// be writable.
2813
- pub unsafe fn font_get_metric_impl(
2814
- font_obj: *const FontObject,
2815
- char_code: u8,
2816
- out_metric: *mut i32,
2817
- out_width: *mut i32,
2818
- ) -> u32 {
2819
- *out_width = (*font_obj).width as i16 as i32;
2820
- if char_code != 0x20 && char_code != 0xA0 {
2821
- let glyph_idx = *(*font_obj).char_to_glyph_idx.add(char_code as usize);
2822
- if glyph_idx == 0 {
2823
- return 0;
2824
- }
2825
- *out_metric = glyph_advance_metric(font_obj, glyph_idx);
2826
- return 1;
2827
- }
2828
- *out_metric = (*font_obj).width_div_5 as i16 as i32;
2829
- 1
2830
- }
2831
-
2832
- /// Pure-Rust port of `Font__SetParam` (0x4FA720).
2833
- ///
2834
- /// Walks the null-terminated byte string `text`, accumulating each
2835
- /// codepoint's advance metric (`width_div_5` for unmapped codepoints,
2836
- /// `glyph.width + 1` for mapped ones) into `*out_total`. Always writes the
2837
- /// font's max width to `*out_width`.
2838
- ///
2839
- /// # Safety
2840
- /// `font_obj` must be a valid `*const FontObject`. `text` must be a valid
2841
- /// null-terminated byte string. Output pointers must be writable.
2842
- pub unsafe fn font_set_param_impl(
2843
- font_obj: *const FontObject,
2844
- text: *const u8,
2845
- out_total: *mut i32,
2846
- out_width: *mut i32,
2847
- ) {
2848
- *out_width = (*font_obj).width as i16 as i32;
2849
- *out_total = 0;
2850
- let mut p = text;
2851
- loop {
2852
- let ch = *p;
2853
- if ch == 0 {
2854
- break;
2855
- }
2856
- let glyph_idx = *(*font_obj).char_to_glyph_idx.add(ch as usize);
2857
- let advance = if glyph_idx == 0 {
2858
- (*font_obj).width_div_5 as i16 as i32
2859
- } else {
2860
- glyph_advance_metric(font_obj, glyph_idx)
2861
- };
2862
- *out_total += advance;
2863
- p = p.add(1);
2864
- }
2865
- }
2866
-
2867
  /// Port of `DisplayGfx::GetFontInfo` (vtable slot 8, 0x523790).
2868
  ///
2869
  /// Validates `font_id` (must be in `1..=31` with a non-null entry in
@@ -2883,7 +2245,7 @@ pub unsafe extern "thiscall" fn get_font_info(
2883
  if !(1..=31).contains(&font_id) {
2884
  return 0;
2885
  }
2886
- let font_obj = (*this).base.font_table[font_id as usize] as *const FontObject;
2887
  if font_obj.is_null() {
2888
  return 0;
2889
  }
@@ -2911,7 +2273,7 @@ pub unsafe extern "thiscall" fn get_font_metric(
2911
  if !(1..=31).contains(&font_id) {
2912
  return 0;
2913
  }
2914
- let font_obj = (*this).base.font_table[font_id as usize] as *const FontObject;
2915
  if font_obj.is_null() {
2916
  return 0;
2917
  }
@@ -2942,7 +2304,7 @@ pub unsafe extern "thiscall" fn set_font_param(
2942
  if !(1..=31).contains(&font_id) {
2943
  return 0;
2944
  }
2945
- let font_obj = (*this).base.font_table[font_id as usize] as *const FontObject;
2946
  if font_obj.is_null() {
2947
  return 0;
2948
  }
@@ -2950,200 +2312,6 @@ pub unsafe extern "thiscall" fn set_font_param(
2950
  1
2951
  }
2952
 
2953
- /// Pure-Rust port of `Font__SetPalette` (0x4F9F20).
2954
- ///
2955
- /// **The function name is misleading** β€” despite "SetPalette", this routine
2956
- /// extends the font with two new derived glyphs rather than recoloring an
2957
- /// existing palette. It is called once during `DDGame__LoadFonts` for the
2958
- /// digital seven-segment font (`digiwht.fnt`, slot 28), which doesn't ship
2959
- /// with `'.'` or `';'` glyphs and needs them generated at runtime.
2960
- ///
2961
- /// What it does, in order:
2962
- ///
2963
- /// 1. Look up the **`'-'`** (0x2D), **`'8'`** (0x38) and **`':'`** (0x3A)
2964
- /// glyph indices via `char_to_glyph_idx`. If any is missing, return 0.
2965
- /// 2. Sample the foreground-color palette indices of `'-'` and `'8'` by
2966
- /// reading byte +5 within each glyph's pixel data. Save them as
2967
- /// `minus_fg` and `eight_fg`.
2968
- /// 3. Allocate a new buffer sized for `(old_height + 2)` glyph entries
2969
- /// plus enough pixel area for **two** glyphs the size of `':'`.
2970
- /// 4. Copy all existing glyph entries verbatim into the start of the
2971
- /// buffer.
2972
- /// 5. Add two new glyph entries:
2973
- /// - `char_to_glyph_idx[0x2E]` (= `'.'`) β†’ 1-based index `old_height + 1`
2974
- /// - `char_to_glyph_idx[0x3B]` (= `';'`) β†’ 1-based index `old_height + 2`
2975
- ///
2976
- /// Both new entries copy `start_x`/`start_y`/`width`/`height` from the
2977
- /// `':'` glyph. Their `pixel_offset` fields point at consecutive
2978
- /// `colon_height * font_width`-byte regions inside the new buffer.
2979
- /// 6. Initialize the **first** new glyph (`'.'`)'s pixel area:
2980
- /// - `memset(area, palette_value as u8, colon_height * font_width / 2)`
2981
- /// β€” fills approximately the top half with a uniform palette index.
2982
- /// - For rows `colon_height/2 .. colon_height-1`, copy `colon.width`
2983
- /// bytes per row from `':'`'s pixel data into the new buffer at
2984
- /// `font_width`-strided rows.
2985
- /// 7. Initialize the **second** new glyph (`';'`)'s pixel area: for every
2986
- /// `(row, col)` in `(0..colon.height, 0..colon.width)`, copy the
2987
- /// `':'` source pixel; if it equals `eight_fg`, replace it with
2988
- /// `minus_fg` instead.
2989
- /// 8. Update `font_obj.glyph_table` to point at the new buffer, store the
2990
- /// new buffer in `aux_alloc` (so destructors can free it), and bump
2991
- /// `font_obj.height` by 2.
2992
- ///
2993
- /// The original uses `wa_malloc(round_up_to_4(total_size) + 0x20)` and
2994
- /// zero-initializes the body up to `total_size` (the trailing 0x20 padding
2995
- /// is left uninitialized).
2996
- ///
2997
- /// # Safety
2998
- /// `font_obj` must be a valid mutable `FontObject`. `palette_value`'s low
2999
- /// byte is used as a single-byte memset filler. The original's
3000
- /// allocate/copy/update sequence is preserved verbatim.
3001
- pub unsafe fn font_set_palette_impl(font_obj: *mut FontObject, palette_value: u32) -> u32 {
3002
- use crate::wa_alloc::wa_malloc;
3003
-
3004
- let char_to_glyph = (*font_obj).char_to_glyph_idx;
3005
- if char_to_glyph.is_null() {
3006
- return 0;
3007
- }
3008
-
3009
- // Step 1: '-' (0x2D) glyph index.
3010
- let minus_idx = *char_to_glyph.add(0x2D);
3011
- if minus_idx == 0 {
3012
- return 0;
3013
- }
3014
-
3015
- // Step 2a: read byte +5 of '-' glyph's pixel data.
3016
- let glyph_table = (*font_obj).glyph_table;
3017
- let pixel_data = (*font_obj).pixel_data;
3018
- let minus_glyph = glyph_table.add(minus_idx as usize - 1);
3019
- let minus_fg = *pixel_data.add((*minus_glyph).pixel_offset as usize + 5);
3020
-
3021
- // Step 2b: '8' (0x38) glyph index + byte +5.
3022
- let eight_idx = *char_to_glyph.add(0x38);
3023
- if eight_idx == 0 {
3024
- return 0;
3025
- }
3026
- let eight_glyph = glyph_table.add(eight_idx as usize - 1);
3027
- let eight_fg = *pixel_data.add((*eight_glyph).pixel_offset as usize + 5);
3028
-
3029
- // Step 1c: ':' (0x3A) β€” required for the template.
3030
- let colon_idx = *char_to_glyph.add(0x3A);
3031
- if colon_idx == 0 {
3032
- return 0;
3033
- }
3034
-
3035
- let old_height = (*font_obj).height as i32; // signed via MOVSX in the original
3036
- let new_height = old_height + 2;
3037
- let font_width = (*font_obj).width as i16 as i32;
3038
-
3039
- // Step 3: compute total size and allocate.
3040
- // total_size = (colon.height * font_width + new_height * 6) * 2
3041
- // = colon_height * font_width * 2 + new_height * 12
3042
- // alloc = round_up_to_dword(total_size) + 0x20
3043
- let colon_glyph = glyph_table.add(colon_idx as usize - 1);
3044
- let colon_height = (*colon_glyph).height as i32;
3045
- let colon_width = (*colon_glyph).width as i32;
3046
- let colon_pixel_offset = (*colon_glyph).pixel_offset as usize;
3047
-
3048
- let total_size = (colon_height * font_width + new_height * 6) * 2;
3049
- let alloc_size = ((total_size as u32 + 3) & !3u32) + 0x20;
3050
- let new_buffer = wa_malloc(alloc_size);
3051
- if new_buffer.is_null() {
3052
- return 0;
3053
- }
3054
- // Zero-init the body (not the trailing 0x20 padding).
3055
- core::ptr::write_bytes(new_buffer, 0, total_size as usize);
3056
-
3057
- // _Dst = new_buffer + new_height*12 = where pixel data starts in the new buffer.
3058
- let new_glyph_table = new_buffer as *mut GlyphEntry;
3059
- let new_pixel_area = new_buffer.add((new_height as usize) * 12);
3060
-
3061
- // Save the allocation in aux_alloc so cleanup paths can free it
3062
- // (the original writes this *before* the copy loop).
3063
- (*font_obj).aux_alloc = new_buffer as u32;
3064
-
3065
- // Step 4: copy existing glyph entries.
3066
- if old_height > 0 {
3067
- core::ptr::copy_nonoverlapping(
3068
- glyph_table as *const u8,
3069
- new_buffer,
3070
- (old_height as usize) * 12,
3071
- );
3072
- }
3073
-
3074
- // Step 5: write new char_to_glyph_idx entries for '.' and ';'.
3075
- *char_to_glyph.add(0x2E) = (old_height as u8).wrapping_add(1);
3076
- *char_to_glyph.add(0x3B) = (old_height as u8).wrapping_add(2);
3077
-
3078
- // Step 5b: first new glyph (= colon copy with adjusted pixel_offset).
3079
- // pixel_offset = (_Dst - pixel_data)
3080
- let new_first = new_glyph_table.add(old_height as usize);
3081
- (*new_first).start_x = (*colon_glyph).start_x;
3082
- (*new_first).start_y = (*colon_glyph).start_y;
3083
- (*new_first).width = (*colon_glyph).width;
3084
- (*new_first).height = (*colon_glyph).height;
3085
- (*new_first).pixel_offset = (new_pixel_area as u32).wrapping_sub(pixel_data as u32);
3086
-
3087
- // Step 5c: second new glyph (= duplicate of first new glyph, but its
3088
- // pixel_offset is bumped by colon_height * font_width so it points to
3089
- // the *next* chunk of new pixel data).
3090
- let new_second = new_first.add(1);
3091
- (*new_second).start_x = (*new_first).start_x;
3092
- (*new_second).start_y = (*new_first).start_y;
3093
- (*new_second).width = (*new_first).width;
3094
- (*new_second).height = (*new_first).height;
3095
- (*new_second).pixel_offset = (*new_first)
3096
- .pixel_offset
3097
- .wrapping_add((colon_height * font_width) as u32);
3098
-
3099
- // Step 6a: memset top portion of FIRST new glyph with palette_value's
3100
- // low byte. Length = (colon_height * font_width) with sign-correct /2.
3101
- let memset_len = {
3102
- let v = colon_height.wrapping_mul(font_width);
3103
- // CDQ + SUB EAX,EDX + SAR EAX,1 = signed div by 2 with rounding toward zero
3104
- let v_signed = v;
3105
- let cdq = v_signed >> 31;
3106
- ((v_signed - cdq) >> 1) as usize
3107
- };
3108
- core::ptr::write_bytes(new_pixel_area, palette_value as u8, memset_len);
3109
-
3110
- // Step 6b: copy bottom half rows of ':' into FIRST new glyph at
3111
- // font-width row stride.
3112
- let half = (colon_height as u32 >> 1) as i32;
3113
- if half < colon_height {
3114
- for row in half..colon_height {
3115
- let src = pixel_data.add(colon_pixel_offset + (row * colon_width) as usize);
3116
- let dst = new_pixel_area.add((row * font_width) as usize);
3117
- core::ptr::copy_nonoverlapping(src, dst, colon_width as usize);
3118
- }
3119
- }
3120
-
3121
- // Step 7: pixel-by-pixel copy for SECOND new glyph with eight→minus
3122
- // foreground substitution. The destination is offset by
3123
- // colon_height * font_width within new_pixel_area.
3124
- if colon_height > 0 {
3125
- for row in 0..colon_height {
3126
- for col in 0..colon_width {
3127
- let src_addr =
3128
- pixel_data.add(colon_pixel_offset + (row * colon_width + col) as usize);
3129
- let dst_addr =
3130
- new_pixel_area.add(((colon_height + row) * font_width + col) as usize);
3131
- let src_byte = *src_addr;
3132
- if src_byte == eight_fg {
3133
- *dst_addr = minus_fg;
3134
- } else {
3135
- *dst_addr = src_byte;
3136
- }
3137
- }
3138
- }
3139
- }
3140
-
3141
- // Step 8: update font_obj fields.
3142
- (*font_obj).glyph_table = new_glyph_table;
3143
- (*font_obj).height = new_height as u16;
3144
- 1
3145
- }
3146
-
3147
  /// Port of `DisplayGfx::SetFontPalette` (vtable slot 36, 0x523690).
3148
  ///
3149
  /// Despite the name, this is the entry point for `font_set_palette_impl` β€”
@@ -3160,7 +2328,7 @@ pub unsafe extern "thiscall" fn set_font_palette(
3160
  font_index: u32,
3161
  palette_value: u32,
3162
  ) {
3163
- let font_obj = (*this).base.font_table[font_index as usize] as *mut FontObject;
3164
  let _ = font_set_palette_impl(font_obj, palette_value);
3165
  }
3166
 
@@ -3233,7 +2401,7 @@ pub unsafe fn font_blit_glyph(
3233
  /// `msg` must be a valid null-terminated byte string. `out_pen_x` and
3234
  /// `out_width` must be writable.
3235
  pub unsafe fn font_draw_text_impl(
3236
- font_obj: *const FontObject,
3237
  bitmap: *const BitGrid,
3238
  pen_x: i32,
3239
  pen_y: i32,
@@ -3389,7 +2557,7 @@ pub unsafe extern "thiscall" fn draw_text_on_bitmap(
3389
  bitmap: *mut BitGrid,
3390
  pen_x: i32,
3391
  pen_y: i32,
3392
- msg: *const core::ffi::c_char,
3393
  out_pen_x: *mut i32,
3394
  out_width: *mut i32,
3395
  ) -> i32 {
@@ -3397,7 +2565,7 @@ pub unsafe extern "thiscall" fn draw_text_on_bitmap(
3397
  if !(1..=31).contains(&font_id_low) {
3398
  return 0;
3399
  }
3400
- let font_obj = (*this).base.font_table[font_id_low as usize] as *const FontObject;
3401
  if font_obj.is_null() {
3402
  return 0;
3403
  }
 
1
+ use core::ffi::c_char;
2
+
3
+ use openwa_core::vtable;
4
+
5
  use crate::fixed::Fixed;
6
+ use crate::render::display::font::{
7
+ font_extend, font_get_info_impl, font_get_metric_impl, font_load_from_gfx,
8
+ font_set_palette_impl, font_set_param_impl, Font,
9
+ };
10
  use crate::render::display::line_draw::Vertex;
11
+ use crate::render::sprite::gfx_dir::GfxDir;
12
  use crate::render::sprite::sprite::{LayerSprite, LayerSpriteFrame};
13
  use crate::render::SpriteCache;
14
  use crate::wa_alloc::wa_malloc_struct_zeroed;
 
44
  /// Coordinate conventions:
45
  /// - Methods that add `camera * 0x10000` take Fixed (16.16) coordinates.
46
  /// - Methods that add `camera` directly take pixel-integer coordinates.
47
+ #[vtable(size = 38, va = 0x0066_A218, class = "DisplayGfx")]
48
+ pub struct DisplayGfxVtable {
49
  /// destructor (0x569CE0, RET 0x4)
50
  #[slot(0)]
51
  pub destructor: fn(this: *mut DisplayGfx, flags: u8) -> *mut DisplayGfx,
 
95
  pub draw_text_on_bitmap: fn(
96
  this: *mut DisplayGfx,
97
  font_id: i32,
98
+ bitmap: *mut BitGrid,
99
  pen_x: i32,
100
  pen_y: i32,
101
+ msg: *const c_char,
102
  out_pen_x: *mut i32,
103
  out_width: *mut i32,
104
  ) -> i32,
 
254
  pub set_layer_visibility: fn(this: *mut DisplayGfx, layer: i32, visible: i32),
255
  /// update palette from PaletteContext (0x56A610, RET 0x8)
256
  #[slot(24)]
257
+ pub update_palette: fn(this: *mut DisplayGfx, palette_ctx: *mut PaletteContext, commit: i32),
 
 
 
 
258
  // Slot 25: stub (CGameTask__vt19)
259
  /// flush pending render state (0x56A580, plain RET)
260
  ///
 
286
  layer: u32,
287
  id: u32,
288
  flag: u32,
289
+ gfx_dir: *mut GfxDir,
290
+ name: *const c_char,
291
  ) -> i32,
292
  /// check if a sprite ID is loaded (0x56A480, RET 0x4)
293
  ///
 
343
  this: *mut DisplayGfx,
344
  mode: i32,
345
  font_id: i32,
346
+ gfx_dir: *mut GfxDir,
347
+ filename: *const c_char,
348
  ) -> u32,
349
  /// load .fex font extension for a font slot (0x523620, RET 0x14)
350
  #[slot(35)]
351
  pub load_font_extension: fn(
352
  this: *mut DisplayGfx,
353
  font_id: i32,
354
+ path: *const c_char,
355
  char_map: *const u8,
356
  palette_value: u32,
357
  flag: i32,
 
365
  this: *mut DisplayGfx,
366
  layer: u32,
367
  id: u32,
368
+ gfx: *mut GfxDir,
369
+ name: *const c_char,
370
  ) -> i32,
371
  }
372
 
373
  // Generate calling wrappers: DisplayGfx::set_layer_color(), etc.
374
+ bind_DisplayGfxVtable!(DisplayGfx, base.vtable);
375
 
376
  // =========================================================================
377
  // Ported DisplayGfx vtable methods
 
1425
  /// The scan area is: `slot_table_guard` (guard=0) + `slot_table[0..255]` + `slot_table_sentinel` (sentinel=-1).
1426
  /// The guard at index 0 is always 0, so allocations start from index 1.
1427
  /// The sentinel (-1) at index 256 terminates the scan with failure.
1428
+ unsafe fn palette_slot_alloc(base: &mut DisplayBase<*const DisplayGfxVtable>, count: i32) -> i32 {
1429
  let count = count as usize;
1430
  let table = &base.slot_table_guard as *const u32;
1431
  // Total entries: slot_table_guard(1) + slot_table(255) + slot_table_sentinel(1) = 257
 
1666
  layer: u32,
1667
  id: u32,
1668
  flag: u32,
1669
+ gfx_dir: *mut GfxDir,
1670
+ _name: *const c_char,
1671
  load_sprite_from_vfs: unsafe extern "cdecl" fn(
1672
  sprite: *mut Sprite,
1673
+ gfx_dir: *mut GfxDir,
1674
+ name: *const c_char,
1675
  layer_ctx: u32,
1676
  ) -> i32,
1677
  ) -> i32 {
 
1704
 
1705
  // Load from VFS
1706
  let layer_ctx = base.layer_contexts[layer as usize];
1707
+ let result = load_sprite_from_vfs(sprite, gfx_dir, _name, layer_ctx);
1708
  if result == 0 {
1709
  // Load failed β€” destroy sprite via vtable[0]
1710
  if !sprite.is_null() {
 
1745
  /// All pointers must be valid. `sprite` must be a zeroed 0x70-byte allocation.
1746
  pub unsafe fn load_sprite_by_name(
1747
  sprite: *mut LayerSprite,
1748
+ gfx_dir: *mut GfxDir,
1749
  palette_ctx: *mut PaletteContext,
1750
+ name: *const c_char,
1751
  ) -> i32 {
1752
  use crate::address::va;
1753
  use crate::rebase::rb;
 
2047
  this: *mut DisplayGfx,
2048
  layer: u32,
2049
  id: u32,
2050
+ gfx_dir: *mut GfxDir,
2051
+ name: *const c_char,
2052
  ) -> i32 {
2053
  use crate::wa_alloc::wa_malloc_zeroed;
2054
 
 
2082
  (*sprite).gfx_dir = core::ptr::null_mut();
2083
 
2084
  // Load sprite data β€” pure Rust port of FUN_005733b0
2085
+ let result = load_sprite_by_name(sprite, gfx_dir, palette_ctx, name);
2086
  if result == 0 {
2087
  free_layer_sprite(sprite);
2088
  return 0;
 
2102
  // that's impractical to bridge directly. Porting requires first porting
2103
  // the FrameCache + decompression chain (FUN_004FA950 β†’ FUN_005B29E0).
2104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2105
  /// Port of `DisplayGfx::LoadFont` (vtable slot 34, 0x523560).
2106
  ///
2107
+ /// Validates `layer` (1..=3) and `font_id` (1..=31), then allocates a
2108
  /// zero-initialized 0x1C-byte `FontObject`, loads the named resource via
2109
  /// `font_load_from_gfx`, and stores the object in `DisplayBase::font_table`.
2110
  ///
2111
+ /// **`layer` is the WA "mode" parameter, but it's the same value space as
2112
+ /// the layer index everywhere else** β€” it indexes `layer_contexts[1..=3]`
2113
+ /// and `layer_visibility[1..=3]` exactly the way `set_layer_color` and
2114
+ /// `set_active_layer` do. The shipping game only ever passes `1`.
2115
+ ///
2116
  /// On load failure, the partially-initialized font object is leaked to match
2117
  /// the original's behavior (it calls a sprite-bank-style cleanup helper
2118
  /// `FUN_005230c0` which is not exercised here).
 
2123
  /// null-terminated C string.
2124
  pub unsafe fn load_font(
2125
  this: *mut DisplayGfx,
2126
+ layer: u32,
2127
  font_id: i32,
2128
+ gfx_dir: *mut GfxDir,
2129
+ filename: *const c_char,
2130
  ) -> u32 {
2131
  use crate::wa_alloc::wa_malloc_struct_zeroed;
2132
 
2133
+ if !(1..=3).contains(&layer) {
 
2134
  return 0;
2135
  }
2136
+
2137
  let base = &mut (*this).base;
2138
+ let layer_ctx = base.layer_contexts[layer as usize];
2139
  if layer_ctx == 0 {
2140
  return 0;
2141
  }
 
2144
  if !(1..=31).contains(&font_id) {
2145
  return 0;
2146
  }
2147
+ if !base.font_table[font_id as usize].is_null() {
2148
  return 0;
2149
  }
2150
 
2151
  // Allocate zeroed FontObject. The original uses WA_MallocMemset(0x1C)
2152
  // which only memsets the requested size; wa_malloc_struct_zeroed matches.
2153
+ let font_obj = wa_malloc_struct_zeroed::<Font>();
2154
  if font_obj.is_null() {
2155
  return 0;
2156
  }
2157
 
2158
  // Load and parse the font data.
2159
+ let result = font_load_from_gfx(
2160
+ font_obj,
2161
+ gfx_dir,
2162
+ layer_ctx as *mut PaletteContext,
2163
+ filename,
2164
+ );
2165
  if result == 0 {
2166
  // Original leaks here on failure (via an unported cleanup helper).
2167
  // We match that behavior rather than introducing a free path that
 
2169
  return 0;
2170
  }
2171
 
2172
+ // Install into font_table. The original also records which layer owns
2173
+ // this font slot at DisplayBase + 0x301C + font_id*4 (the gap region
2174
+ // next to font_table), then bumps the layer visibility counter.
2175
+ base.font_table[font_id as usize] = font_obj;
2176
+ base.font_layers[font_id as usize] = layer;
2177
+ base.layer_visibility[layer as usize] += 1;
2178
 
2179
  1
2180
  }
2181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2182
  /// Port of `DisplayGfx::LoadFontExtension` (vtable slot 35, 0x523620).
2183
  ///
2184
  /// Validates `font_id`, looks up the existing font object, resolves the
 
2192
  pub unsafe fn load_font_extension(
2193
  this: *mut DisplayGfx,
2194
  font_id: i32,
2195
+ path: *const c_char,
2196
  char_map: *const u8,
2197
  palette_value: u32,
2198
  _flag: i32,
 
2204
  }
2205
  let base = &mut (*this).base;
2206
  let font_obj_addr = base.font_table[font_id as usize];
2207
+ if font_obj_addr.is_null() {
2208
  return 0;
2209
  }
2210
+ let font_obj = font_obj_addr as *mut Font;
2211
 
2212
  // Resolve the RGB color via the layer-1 palette context (DisplayBase+0x3120).
2213
  // The original always reads layer_contexts[1] here, regardless of which
 
2217
  let mut resolved_rgb: u32 = 0;
2218
  let _ = palette_context_lookup_entry(layer1_ctx, palette_value as i32, &mut resolved_rgb);
2219
 
2220
+ // The actual font extension call uses the font's owning layer's palette ctx.
2221
+ let layer = base.font_layers[font_id as usize] as usize;
2222
+ let layer_ctx = base.layer_contexts[layer] as *mut PaletteContext;
2223
 
2224
  font_extend(font_obj, layer_ctx, path, char_map, resolved_rgb);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2226
  1
2227
  }
2228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2229
  /// Port of `DisplayGfx::GetFontInfo` (vtable slot 8, 0x523790).
2230
  ///
2231
  /// Validates `font_id` (must be in `1..=31` with a non-null entry in
 
2245
  if !(1..=31).contains(&font_id) {
2246
  return 0;
2247
  }
2248
+ let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
2249
  if font_obj.is_null() {
2250
  return 0;
2251
  }
 
2273
  if !(1..=31).contains(&font_id) {
2274
  return 0;
2275
  }
2276
+ let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
2277
  if font_obj.is_null() {
2278
  return 0;
2279
  }
 
2304
  if !(1..=31).contains(&font_id) {
2305
  return 0;
2306
  }
2307
+ let font_obj = (*this).base.font_table[font_id as usize] as *const Font;
2308
  if font_obj.is_null() {
2309
  return 0;
2310
  }
 
2312
  1
2313
  }
2314
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2315
  /// Port of `DisplayGfx::SetFontPalette` (vtable slot 36, 0x523690).
2316
  ///
2317
  /// Despite the name, this is the entry point for `font_set_palette_impl` β€”
 
2328
  font_index: u32,
2329
  palette_value: u32,
2330
  ) {
2331
+ let font_obj = (*this).base.font_table[font_index as usize] as *mut Font;
2332
  let _ = font_set_palette_impl(font_obj, palette_value);
2333
  }
2334
 
 
2401
  /// `msg` must be a valid null-terminated byte string. `out_pen_x` and
2402
  /// `out_width` must be writable.
2403
  pub unsafe fn font_draw_text_impl(
2404
+ font_obj: *const Font,
2405
  bitmap: *const BitGrid,
2406
  pen_x: i32,
2407
  pen_y: i32,
 
2557
  bitmap: *mut BitGrid,
2558
  pen_x: i32,
2559
  pen_y: i32,
2560
+ msg: *const c_char,
2561
  out_pen_x: *mut i32,
2562
  out_width: *mut i32,
2563
  ) -> i32 {
 
2565
  if !(1..=31).contains(&font_id_low) {
2566
  return 0;
2567
  }
2568
+ let font_obj = (*this).base.font_table[font_id_low as usize] as *const Font;
2569
  if font_obj.is_null() {
2570
  return 0;
2571
  }
crates/openwa-core/src/render/landscape.rs CHANGED
@@ -1,4 +1,4 @@
1
- use crate::engine::ddgame::DDGame;
2
 
3
  /// PCLandscape β€” terrain/landscape subsystem (0xB40 bytes).
4
  ///
@@ -86,9 +86,9 @@ pub struct PCLandscape {
86
  pub visible_right: u32,
87
  pub visible_bottom: u32,
88
  /// 0xB34: GfxHandler for Level.dir (param_1[0x2CD])
89
- pub level_gfx_dir: *mut u8,
90
  /// 0xB38: GfxHandler for Water.dir (param_1[0x2CE])
91
- pub water_gfx_dir: *mut u8,
92
  /// 0xB3C: Remaining control flag
93
  pub _unknown_b3c: u32,
94
  }
 
1
+ use crate::{engine::ddgame::DDGame, render::sprite::gfx_dir::GfxDir};
2
 
3
  /// PCLandscape β€” terrain/landscape subsystem (0xB40 bytes).
4
  ///
 
86
  pub visible_right: u32,
87
  pub visible_bottom: u32,
88
  /// 0xB34: GfxHandler for Level.dir (param_1[0x2CD])
89
+ pub level_gfx_dir: *mut GfxDir,
90
  /// 0xB38: GfxHandler for Water.dir (param_1[0x2CE])
91
+ pub water_gfx_dir: *mut GfxDir,
92
  /// 0xB3C: Remaining control flag
93
  pub _unknown_b3c: u32,
94
  }
crates/openwa-core/src/render/mod.rs CHANGED
@@ -9,7 +9,7 @@ pub mod turn_order;
9
 
10
  pub use ddraw::CompatRenderer;
11
  pub use display::{
12
- DisplayBase, DisplayBaseVtable, DisplayGfx, DisplayVtable, DrawScaledSpriteResult,
13
  FastcallResult, Palette, PaletteVtable, RenderContext, SpriteBufferCtrl, SpriteCache,
14
  };
15
  pub use landscape::{DirtyRect, PCLandscape};
 
9
 
10
  pub use ddraw::CompatRenderer;
11
  pub use display::{
12
+ DisplayBase, DisplayBaseVtable, DisplayGfx, DisplayGfxVtable, DrawScaledSpriteResult,
13
  FastcallResult, Palette, PaletteVtable, RenderContext, SpriteBufferCtrl, SpriteCache,
14
  };
15
  pub use landscape::{DirtyRect, PCLandscape};
crates/openwa-core/src/render/sprite/gfx_dir.rs CHANGED
@@ -104,7 +104,10 @@ unsafe fn gfx_dir_hash(name: *const u8) -> u32 {
104
  /// `gfx_dir` must be a valid GfxHandler with initialized hash table at +0x04.
105
  /// `name` must be a valid null-terminated C string.
106
  #[cfg(target_arch = "x86")]
107
- pub unsafe fn gfx_dir_find_entry(name: *const core::ffi::c_char, gfx_dir: *mut u8) -> *mut u8 {
 
 
 
108
  let mut current_name = name as *const u8;
109
 
110
  loop {
@@ -152,8 +155,7 @@ pub unsafe fn gfx_dir_find_entry(name: *const core::ffi::c_char, gfx_dir: *mut u
152
  let bucket = gfx_dir_hash(buf.as_ptr());
153
 
154
  // Walk linked list in hash bucket
155
- let gfx = &*(gfx_dir as *const GfxDir);
156
- let bucket_array = gfx.bucket_array as *const u32;
157
  let mut entry = *bucket_array.add(bucket as usize) as *mut GfxDirEntry;
158
 
159
  while !entry.is_null() {
@@ -336,7 +338,7 @@ pub unsafe fn gfx_dir_load_dir(handler: *mut u8) -> i32 {
336
  /// All pointers must be valid WA objects.
337
  #[cfg(target_arch = "x86")]
338
  pub unsafe fn gfx_resource_create(
339
- gfx_dir: *mut u8,
340
  name: *const core::ffi::c_char,
341
  output: *mut u8,
342
  ) -> *mut u8 {
@@ -345,7 +347,7 @@ pub unsafe fn gfx_resource_create(
345
  if !entry.is_null() {
346
  // gfx_dir->vtable[2](entry->field_4) β€” cached load
347
  let vt = *(gfx_dir as *const *const u32);
348
- let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
349
  core::mem::transmute(*vt.add(2));
350
  let entry_val = (*(entry as *const GfxDirEntry)).value;
351
  let cached = load_cached(gfx_dir, entry_val);
@@ -381,7 +383,7 @@ pub unsafe fn gfx_resource_create(
381
  /// Returns a DisplayGfx/sprite pointer or null.
382
  #[cfg(target_arch = "x86")]
383
  pub(crate) unsafe fn call_gfx_find_and_load(
384
- gfx_dir: *mut u8,
385
  name: &core::ffi::CStr,
386
  display_ctx: *mut u8,
387
  ) -> *mut u8 {
@@ -391,7 +393,7 @@ pub(crate) unsafe fn call_gfx_find_and_load(
391
  if !entry.is_null() {
392
  // Try cached load: gfx_dir->vtable[2](entry->field_4)
393
  let dir_vt = *(gfx_dir as *const *const u32);
394
- let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
395
  core::mem::transmute(*dir_vt.add(2));
396
  let cached = load_cached(gfx_dir, (*(entry as *const GfxDirEntry)).value);
397
  if !cached.is_null() {
@@ -411,7 +413,7 @@ pub(crate) unsafe fn call_gfx_find_and_load(
411
  /// Used by arrow sprite loop when GfxDir__FindEntry returns null.
412
  #[cfg(target_arch = "x86")]
413
  pub(crate) unsafe fn call_gfx_load_and_wrap(
414
- gfx_dir: *mut u8,
415
  name: *const core::ffi::c_char,
416
  display_ctx: *mut u8,
417
  ) -> *mut u8 {
@@ -511,7 +513,7 @@ pub(crate) unsafe fn call_gfx_load_dir(handler: *mut u8, addr: u32) -> i32 {
511
  #[cfg(target_arch = "x86")]
512
  #[unsafe(naked)]
513
  pub unsafe extern "C" fn call_gfx_load_image(
514
- _gfx_dir: *mut u8,
515
  _name: *const core::ffi::c_char,
516
  ) -> *mut u8 {
517
  core::arch::naked_asm!(
 
104
  /// `gfx_dir` must be a valid GfxHandler with initialized hash table at +0x04.
105
  /// `name` must be a valid null-terminated C string.
106
  #[cfg(target_arch = "x86")]
107
+ pub unsafe fn gfx_dir_find_entry(
108
+ name: *const core::ffi::c_char,
109
+ gfx_dir: *const GfxDir,
110
+ ) -> *mut u8 {
111
  let mut current_name = name as *const u8;
112
 
113
  loop {
 
155
  let bucket = gfx_dir_hash(buf.as_ptr());
156
 
157
  // Walk linked list in hash bucket
158
+ let bucket_array = (*gfx_dir).bucket_array as *const u32;
 
159
  let mut entry = *bucket_array.add(bucket as usize) as *mut GfxDirEntry;
160
 
161
  while !entry.is_null() {
 
338
  /// All pointers must be valid WA objects.
339
  #[cfg(target_arch = "x86")]
340
  pub unsafe fn gfx_resource_create(
341
+ gfx_dir: *mut GfxDir,
342
  name: *const core::ffi::c_char,
343
  output: *mut u8,
344
  ) -> *mut u8 {
 
347
  if !entry.is_null() {
348
  // gfx_dir->vtable[2](entry->field_4) β€” cached load
349
  let vt = *(gfx_dir as *const *const u32);
350
+ let load_cached: unsafe extern "thiscall" fn(*mut GfxDir, u32) -> *mut u8 =
351
  core::mem::transmute(*vt.add(2));
352
  let entry_val = (*(entry as *const GfxDirEntry)).value;
353
  let cached = load_cached(gfx_dir, entry_val);
 
383
  /// Returns a DisplayGfx/sprite pointer or null.
384
  #[cfg(target_arch = "x86")]
385
  pub(crate) unsafe fn call_gfx_find_and_load(
386
+ gfx_dir: *mut GfxDir,
387
  name: &core::ffi::CStr,
388
  display_ctx: *mut u8,
389
  ) -> *mut u8 {
 
393
  if !entry.is_null() {
394
  // Try cached load: gfx_dir->vtable[2](entry->field_4)
395
  let dir_vt = *(gfx_dir as *const *const u32);
396
+ let load_cached: unsafe extern "thiscall" fn(*mut GfxDir, u32) -> *mut u8 =
397
  core::mem::transmute(*dir_vt.add(2));
398
  let cached = load_cached(gfx_dir, (*(entry as *const GfxDirEntry)).value);
399
  if !cached.is_null() {
 
413
  /// Used by arrow sprite loop when GfxDir__FindEntry returns null.
414
  #[cfg(target_arch = "x86")]
415
  pub(crate) unsafe fn call_gfx_load_and_wrap(
416
+ gfx_dir: *mut GfxDir,
417
  name: *const core::ffi::c_char,
418
  display_ctx: *mut u8,
419
  ) -> *mut u8 {
 
513
  #[cfg(target_arch = "x86")]
514
  #[unsafe(naked)]
515
  pub unsafe extern "C" fn call_gfx_load_image(
516
+ _gfx_dir: *mut GfxDir,
517
  _name: *const core::ffi::c_char,
518
  ) -> *mut u8 {
519
  core::arch::naked_asm!(
crates/openwa-core/src/render/sprite/sprite.rs CHANGED
@@ -1,4 +1,4 @@
1
- use crate::render::SpriteCache;
2
 
3
  // Re-export SpriteId from its own module to keep this file focused on struct layouts.
4
  pub use super::sprite_id::SpriteId;
@@ -191,9 +191,9 @@ pub struct LayerSprite {
191
  /// 0x04: Sprite name (null-terminated, max 0x50 bytes including null).
192
  pub name: [u8; 0x50],
193
  /// 0x54: GfxDir pointer (set by load_sprite_by_name).
194
- pub gfx_dir: *mut u8,
195
  /// 0x58: DisplayGfx pointer (set by caller before load).
196
- pub display_gfx: *mut crate::render::display::gfx::DisplayGfx,
197
  /// 0x5C: PaletteContext pointer (as u32, set by load_sprite_by_name).
198
  pub palette_ctx: u32,
199
  /// 0x60: Header field from stream (4 bytes).
 
1
+ use crate::render::{display::gfx::DisplayGfx, sprite::gfx_dir::GfxDir, SpriteCache};
2
 
3
  // Re-export SpriteId from its own module to keep this file focused on struct layouts.
4
  pub use super::sprite_id::SpriteId;
 
191
  /// 0x04: Sprite name (null-terminated, max 0x50 bytes including null).
192
  pub name: [u8; 0x50],
193
  /// 0x54: GfxDir pointer (set by load_sprite_by_name).
194
+ pub gfx_dir: *mut GfxDir,
195
  /// 0x58: DisplayGfx pointer (set by caller before load).
196
+ pub display_gfx: *mut DisplayGfx,
197
  /// 0x5C: PaletteContext pointer (as u32, set by load_sprite_by_name).
198
  pub palette_ctx: u32,
199
  /// 0x60: Header field from stream (4 bytes).
crates/openwa-dll/src/replacements/ddgame_init.rs CHANGED
@@ -23,7 +23,7 @@ use openwa_core::engine::{
23
  ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init, DDGame, DDGameWrapper,
24
  };
25
  use openwa_core::render::sprite::gfx_dir::{
26
- gfx_dir_find_entry, gfx_dir_load_dir, gfx_resource_create,
27
  };
28
 
29
  // ─── DDGame__InitFields (0x526120) ──────────────────────────────────────────
@@ -97,10 +97,12 @@ extern "cdecl" fn impl_display_layer_init(wrapper: u32) -> u32 {
97
 
98
  // ─── GfxResource__Create_Maybe (0x4F6300) ───────────────────────────────────
99
 
100
- extern "cdecl" fn impl_gfx_resource_create(gfx_dir: u32, name: u32, output: u32) -> u32 {
101
- let result = unsafe {
102
- gfx_resource_create(gfx_dir as *mut u8, name as *const c_char, output as *mut u8)
103
- };
 
 
104
  result as u32
105
  }
106
 
@@ -132,8 +134,8 @@ hook::usercall_trampoline!(
132
 
133
  static FIND_ENTRY_LOG_COUNT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
134
 
135
- extern "cdecl" fn impl_find_entry(name: u32, gfx_dir: u32) -> u32 {
136
- let result = unsafe { gfx_dir_find_entry(name as *const c_char, gfx_dir as *mut u8) };
137
 
138
  // Log first 20 lookups for debugging
139
  let count = FIND_ENTRY_LOG_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
 
23
  ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init, DDGame, DDGameWrapper,
24
  };
25
  use openwa_core::render::sprite::gfx_dir::{
26
+ gfx_dir_find_entry, gfx_dir_load_dir, gfx_resource_create, GfxDir,
27
  };
28
 
29
  // ─── DDGame__InitFields (0x526120) ──────────────────────────────────────────
 
97
 
98
  // ─── GfxResource__Create_Maybe (0x4F6300) ───────────────────────────────────
99
 
100
+ extern "cdecl" fn impl_gfx_resource_create(
101
+ gfx_dir: *mut GfxDir,
102
+ name: *const c_char,
103
+ output: *mut u8,
104
+ ) -> u32 {
105
+ let result = unsafe { gfx_resource_create(gfx_dir, name, output) };
106
  result as u32
107
  }
108
 
 
134
 
135
  static FIND_ENTRY_LOG_COUNT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
136
 
137
+ extern "cdecl" fn impl_find_entry(name: *const c_char, gfx_dir: *mut GfxDir) -> u32 {
138
+ let result = unsafe { gfx_dir_find_entry(name, gfx_dir) };
139
 
140
  // Log first 20 lookups for debugging
141
  let count = FIND_ENTRY_LOG_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
crates/openwa-dll/src/replacements/render.rs CHANGED
@@ -5,9 +5,12 @@
5
  //! - DisplayGfx vtable patches (DisplayBase pure-call stubs, headless destructor,
6
  //! and ported DisplayGfx methods)
7
 
 
 
8
  use openwa_core::address::va;
9
  use openwa_core::rebase::rb;
10
  use openwa_core::render::queue::*;
 
11
  use openwa_core::task::{BungeeTrailTask, WeaponAimTask};
12
 
13
  use crate::hook::{self, usercall_trampoline};
@@ -688,7 +691,7 @@ fn install_render_queue() -> Result<(), String> {
688
 
689
  use openwa_core::bitgrid::DisplayBitGrid;
690
  use openwa_core::fixed::Fixed;
691
- use openwa_core::render::display::vtable::{self as display_vtable, DisplayVtable};
692
  use openwa_core::render::display::DisplayBase;
693
  use openwa_core::render::display::DisplayGfx;
694
  use openwa_core::vtable::patch_vtable;
@@ -1312,10 +1315,18 @@ unsafe extern "thiscall" fn load_sprite(
1312
  layer: u32,
1313
  id: u32,
1314
  flag: u32,
1315
- gfx: *mut u8,
1316
- name: *const core::ffi::c_char,
1317
  ) -> i32 {
1318
- display_vtable::load_sprite(this, layer, id, flag, gfx, name, wa_load_sprite_from_vfs)
 
 
 
 
 
 
 
 
1319
  }
1320
 
1321
  /// Bridge to LoadSpriteFromVfs (0x4FAAF0).
@@ -1328,8 +1339,8 @@ unsafe extern "thiscall" fn load_sprite(
1328
  #[unsafe(naked)]
1329
  unsafe extern "cdecl" fn wa_load_sprite_from_vfs(
1330
  _sprite: *mut openwa_core::render::sprite::Sprite,
1331
- _gfx: *mut u8,
1332
- _name: *const core::ffi::c_char,
1333
  _layer_ctx: u32,
1334
  ) -> i32 {
1335
  core::arch::naked_asm!(
@@ -1355,26 +1366,26 @@ unsafe extern "thiscall" fn load_sprite_by_layer(
1355
  this: *mut DisplayGfx,
1356
  layer: u32,
1357
  id: u32,
1358
- gfx: *mut u8,
1359
- name: *const core::ffi::c_char,
1360
  ) -> i32 {
1361
- display_vtable::load_sprite_by_layer(this, layer, id, gfx, name)
1362
  }
1363
 
1364
  unsafe extern "thiscall" fn load_font(
1365
  this: *mut DisplayGfx,
1366
- mode: i32,
1367
  font_id: i32,
1368
- gfx: *mut u8,
1369
- filename: *const core::ffi::c_char,
1370
  ) -> u32 {
1371
- display_vtable::load_font(this, mode, font_id, gfx, filename)
1372
  }
1373
 
1374
  unsafe extern "thiscall" fn load_font_extension(
1375
  this: *mut DisplayGfx,
1376
  font_id: i32,
1377
- path: *const core::ffi::c_char,
1378
  char_map: *const u8,
1379
  palette_value: u32,
1380
  flag: i32,
@@ -1421,7 +1432,7 @@ fn install_display() -> Result<(), String> {
1421
  LOAD_SPRITE_FROM_VFS_ADDR = rb(va::LOAD_SPRITE_FROM_VFS);
1422
 
1423
  // Patch DisplayGfx vtable (0x66A218): replace ported methods with Rust.
1424
- vtable_replace!(DisplayVtable, va::DISPLAY_GFX_VTABLE, {
1425
  get_dimensions => display_vtable::get_dimensions,
1426
  set_layer_color => display_vtable::set_layer_color,
1427
  set_active_layer => display_vtable::set_active_layer,
 
5
  //! - DisplayGfx vtable patches (DisplayBase pure-call stubs, headless destructor,
6
  //! and ported DisplayGfx methods)
7
 
8
+ use core::ffi::c_char;
9
+
10
  use openwa_core::address::va;
11
  use openwa_core::rebase::rb;
12
  use openwa_core::render::queue::*;
13
+ use openwa_core::render::sprite::gfx_dir::GfxDir;
14
  use openwa_core::task::{BungeeTrailTask, WeaponAimTask};
15
 
16
  use crate::hook::{self, usercall_trampoline};
 
691
 
692
  use openwa_core::bitgrid::DisplayBitGrid;
693
  use openwa_core::fixed::Fixed;
694
+ use openwa_core::render::display::vtable::{self as display_vtable, DisplayGfxVtable};
695
  use openwa_core::render::display::DisplayBase;
696
  use openwa_core::render::display::DisplayGfx;
697
  use openwa_core::vtable::patch_vtable;
 
1315
  layer: u32,
1316
  id: u32,
1317
  flag: u32,
1318
+ gfx_dir: *mut GfxDir,
1319
+ name: *const c_char,
1320
  ) -> i32 {
1321
+ display_vtable::load_sprite(
1322
+ this,
1323
+ layer,
1324
+ id,
1325
+ flag,
1326
+ gfx_dir,
1327
+ name,
1328
+ wa_load_sprite_from_vfs,
1329
+ )
1330
  }
1331
 
1332
  /// Bridge to LoadSpriteFromVfs (0x4FAAF0).
 
1339
  #[unsafe(naked)]
1340
  unsafe extern "cdecl" fn wa_load_sprite_from_vfs(
1341
  _sprite: *mut openwa_core::render::sprite::Sprite,
1342
+ _gfx_dir: *mut GfxDir,
1343
+ _name: *const c_char,
1344
  _layer_ctx: u32,
1345
  ) -> i32 {
1346
  core::arch::naked_asm!(
 
1366
  this: *mut DisplayGfx,
1367
  layer: u32,
1368
  id: u32,
1369
+ gfx_dir: *mut GfxDir,
1370
+ name: *const c_char,
1371
  ) -> i32 {
1372
+ display_vtable::load_sprite_by_layer(this, layer, id, gfx_dir, name)
1373
  }
1374
 
1375
  unsafe extern "thiscall" fn load_font(
1376
  this: *mut DisplayGfx,
1377
+ mode: u32,
1378
  font_id: i32,
1379
+ gfx_dir: *mut GfxDir,
1380
+ filename: *const c_char,
1381
  ) -> u32 {
1382
+ display_vtable::load_font(this, mode, font_id, gfx_dir, filename)
1383
  }
1384
 
1385
  unsafe extern "thiscall" fn load_font_extension(
1386
  this: *mut DisplayGfx,
1387
  font_id: i32,
1388
+ path: *const c_char,
1389
  char_map: *const u8,
1390
  palette_value: u32,
1391
  flag: i32,
 
1432
  LOAD_SPRITE_FROM_VFS_ADDR = rb(va::LOAD_SPRITE_FROM_VFS);
1433
 
1434
  // Patch DisplayGfx vtable (0x66A218): replace ported methods with Rust.
1435
+ vtable_replace!(DisplayGfxVtable, va::DISPLAY_GFX_VTABLE, {
1436
  get_dimensions => display_vtable::get_dimensions,
1437
  set_layer_color => display_vtable::set_layer_color,
1438
  set_active_layer => display_vtable::set_active_layer,