paavohtl Claude Opus 4.6 (1M context) commited on
Commit
7cb030a
·
1 Parent(s): 79f78c4

refactor: create GfxDir struct, rename GfxHandler → GfxDir throughout

Browse files

New GfxDir struct (0x19C bytes, #[repr(C)]):
- Typed fields: vtable, bucket_array, fallback_alloc, cache_slots,
index_table, slot_count, loaded, file_handle
- GfxDir::alloc(vtable) constructor replaces wa_malloc+memset+vtable pattern
- Size assertion: size_of::<GfxDir>() == 0x19C

Renamed across entire codebase:
- GfxHandler → GfxDir (struct, module, constants)
- gfx_handler.rs → gfx_dir.rs (module file)
- GFX_HANDLER_VTABLE → GFX_DIR_VTABLE
- GFX_HANDLER_LOAD_DIR → GFX_DIR_LOAD_DIR
- GFX_HANDLER_LOAD_SPRITES → GFX_DIR_LOAD_SPRITES
- DDGameWrapper._field_4c0 → primary_gfx_dir
- DDGameWrapper._field_4c4 → secondary_gfx_dir
- Ghidra: GfxHandler__LoadDir → GfxDir__LoadDir, vtable label updated

Eliminated pointer arithmetic:
- gfx_dir_reset: handler.add(0x04/0x08/0x194/etc.) → gfx.field
- gfx_dir_load_dir: handler.add(4/8/0x194) → gfx.field
- gfx_dir_find_entry: handler.add(4) → gfx.bucket_array
- ddgame.rs: gfx.add(0x198) → (*gfx).file_handle

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

crates/openwa-core/src/address.rs CHANGED
@@ -345,7 +345,7 @@ pub mod va {
345
  pub const DDGAME_WRAPPER_LOADING_PROGRESS_TICK: u32 = 0x0057_17A0;
346
  /// DDGame__LoadHudAndWeaponSprites (0x53D0E0): loads weapon icons, wind indicators,
347
  /// girder sprites, and creates DDGame+0x37C DisplayGfx object.
348
- /// thiscall(ECX=gfx_handler_4c0) + 2 stack(ddgame, wrapper_4c4), RET 0x8.
349
  pub const DDGAME_LOAD_HUD_AND_WEAPON_SPRITES: u32 = 0x0053_D0E0;
350
  /// PaletteContext__Init (0x5411A0): initialize palette context free-list.
351
  /// usercall(EAX=context), plain RET.
@@ -359,12 +359,12 @@ pub mod va {
359
  /// g_GameInfo global — set to current GameInfo* during DDGame construction.
360
  pub const G_GAME_INFO: u32 = 0x0077_49A0;
361
  /// GfxHandler__LoadSprites_Maybe (0x570B50): loads sprites from GfxHandler.
362
- pub const GFX_HANDLER_LOAD_SPRITES: u32 = 0x0057_0B50;
363
  /// FUN_00570A90: called before display palette setup (non-headless only).
364
  pub const FUN_570A90: u32 = 0x0057_0A90;
365
  /// FUN_00570E20: called after GfxHandler initialization.
366
  pub const FUN_570E20: u32 = 0x0057_0E20;
367
- pub const GFX_HANDLER_LOAD_DIR: u32 = 0x0056_63E0;
368
  pub const GFX_DIR_FIND_ENTRY: u32 = 0x0056_6520;
369
  pub const GFX_DIR_LOAD_IMAGE: u32 = 0x0056_66D0;
370
  /// DisplayGfx__Constructor_Maybe (0x4F5E80): stdcall(raw_image) -> DisplayGfx*, RET 0x4.
@@ -377,7 +377,7 @@ pub mod va {
377
  // === Vtables ===
378
 
379
  /// GfxHandler vtable (0x66B280).
380
- pub const GFX_HANDLER_VTABLE: u32 = 0x0066_B280;
381
  /// BitGrid base vtable (0x6640EC): shared by BitGrid, DisplayGfx base, gradient stubs.
382
  pub const BIT_GRID_VTABLE: u32 = 0x0066_40EC;
383
  /// BitGrid variant vtable (0x664118): used by TaskStateMachine-class objects.
 
345
  pub const DDGAME_WRAPPER_LOADING_PROGRESS_TICK: u32 = 0x0057_17A0;
346
  /// DDGame__LoadHudAndWeaponSprites (0x53D0E0): loads weapon icons, wind indicators,
347
  /// girder sprites, and creates DDGame+0x37C DisplayGfx object.
348
+ /// thiscall(ECX=gfx_dir_4c0) + 2 stack(ddgame, wrapper_4c4), RET 0x8.
349
  pub const DDGAME_LOAD_HUD_AND_WEAPON_SPRITES: u32 = 0x0053_D0E0;
350
  /// PaletteContext__Init (0x5411A0): initialize palette context free-list.
351
  /// usercall(EAX=context), plain RET.
 
359
  /// g_GameInfo global — set to current GameInfo* during DDGame construction.
360
  pub const G_GAME_INFO: u32 = 0x0077_49A0;
361
  /// GfxHandler__LoadSprites_Maybe (0x570B50): loads sprites from GfxHandler.
362
+ pub const GFX_DIR_LOAD_SPRITES: u32 = 0x0057_0B50;
363
  /// FUN_00570A90: called before display palette setup (non-headless only).
364
  pub const FUN_570A90: u32 = 0x0057_0A90;
365
  /// FUN_00570E20: called after GfxHandler initialization.
366
  pub const FUN_570E20: u32 = 0x0057_0E20;
367
+ pub const GFX_DIR_LOAD_DIR: u32 = 0x0056_63E0;
368
  pub const GFX_DIR_FIND_ENTRY: u32 = 0x0056_6520;
369
  pub const GFX_DIR_LOAD_IMAGE: u32 = 0x0056_66D0;
370
  /// DisplayGfx__Constructor_Maybe (0x4F5E80): stdcall(raw_image) -> DisplayGfx*, RET 0x4.
 
377
  // === Vtables ===
378
 
379
  /// GfxHandler vtable (0x66B280).
380
+ pub const GFX_DIR_VTABLE: u32 = 0x0066_B280;
381
  /// BitGrid base vtable (0x6640EC): shared by BitGrid, DisplayGfx base, gradient stubs.
382
  pub const BIT_GRID_VTABLE: u32 = 0x0066_40EC;
383
  /// BitGrid variant vtable (0x664118): used by TaskStateMachine-class objects.
crates/openwa-core/src/display/gradient.rs CHANGED
@@ -8,7 +8,7 @@
8
  use crate::address::va;
9
  use crate::engine::ddgame::DDGame;
10
  use crate::rebase::rb;
11
- use crate::render::gfx_handler::call_gfx_find_and_load;
12
  use crate::task::bit_grid::BitGrid;
13
  use crate::wa_alloc::{wa_free, wa_malloc};
14
 
 
8
  use crate::address::va;
9
  use crate::engine::ddgame::DDGame;
10
  use crate::rebase::rb;
11
+ use crate::render::gfx_dir::call_gfx_find_and_load;
12
  use crate::task::bit_grid::BitGrid;
13
  use crate::wa_alloc::{wa_free, wa_malloc};
14
 
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -11,12 +11,10 @@ use crate::engine::game_info::GameInfo;
11
  use crate::input::keyboard::DDKeyboard;
12
  use crate::rebase::rb;
13
  // Re-export public GfxHandler functions so existing `engine::ddgame::*` imports keep working.
14
- use crate::render::gfx_handler::{
15
- call_gfx_find_and_load, call_gfx_load_and_wrap, call_gfx_load_dir,
16
- };
17
- pub use crate::render::gfx_handler::{
18
- gfx_dir_find_entry, gfx_handler_load_dir, gfx_resource_create,
19
  };
 
20
  use crate::render::landscape::PCLandscape;
21
  use crate::render::queue::RenderQueue;
22
  use crate::render::turn_order::TurnOrderWidget;
@@ -200,7 +198,7 @@ pub struct DDGame {
200
  pub rng_state_2: u32,
201
  /// 0x72F4-0x7307: Unknown
202
  pub _unknown_72f4: [u8; 0x7308 - 0x72F4],
203
- /// 0x7308: Sprite/gfx dimension data (passed to GFX_HANDLER_LOAD_SPRITES).
204
  pub gfx_sprite_data: [u8; 0x730C - 0x7308],
205
  /// 0x730C-0x731C: 5 GfxDir color entries
206
  pub _gfx_color_entries: [u8; 0x7324 - 0x730C],
@@ -542,7 +540,7 @@ pub fn init_constructor_addrs() {
542
  LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS);
543
  LOADING_PROGRESS_TICK_ADDR = rb(va::DDGAME_WRAPPER_LOADING_PROGRESS_TICK);
544
  }
545
- crate::render::gfx_handler::init_addrs();
546
  }
547
 
548
  // ── Typed wrappers for WA stdcall functions ────────────────────────────────
@@ -566,7 +564,7 @@ unsafe fn wa_load_sprites(
566
  param4: u32,
567
  ) {
568
  let f: unsafe extern "stdcall" fn(*mut DDGameWrapper, *mut u8, u32, u32) =
569
- core::mem::transmute(rb(va::GFX_HANDLER_LOAD_SPRITES) as usize);
570
  f(wrapper, sprite_data, display_flags, param4);
571
  }
572
 
@@ -638,12 +636,12 @@ unsafe fn wa_init_display_final(display: *mut DDDisplay) {
638
  }
639
 
640
  /// DDGame__LoadHudAndWeaponSprites (0x53D0E0): load weapon icons and HUD sprites.
641
- /// thiscall(ECX=gfx_handler) + 2 stack(ddgame, secondary_gfx_handler), RET 0x8.
642
  #[cfg(target_arch = "x86")]
643
- unsafe fn wa_load_hud_sprites(gfx_handler: *mut u8, ddgame: *mut DDGame, secondary: *mut u8) {
644
  let f: unsafe extern "thiscall" fn(*mut u8, *mut DDGame, *mut u8) =
645
  core::mem::transmute(rb(va::DDGAME_LOAD_HUD_AND_WEAPON_SPRITES) as usize);
646
- f(gfx_handler, ddgame, secondary);
647
  }
648
 
649
  /// Create and initialize DDGame, matching DDGame__Constructor (0x56E220).
@@ -744,14 +742,12 @@ unsafe fn init_graphics_and_resources(
744
  let ddgame = (*wrapper).ddgame;
745
  let fopen: unsafe extern "cdecl" fn(*const u8, *const u8) -> *mut u8 =
746
  core::mem::transmute(rb(va::WA_FOPEN) as usize);
747
- let gfx_handler_vtable = rb(va::GFX_HANDLER_VTABLE) as u32;
748
 
749
- // ── GfxHandler #1 (primary) ──
750
- let gfx1 = wa_malloc(0x19C);
751
- core::ptr::write_bytes(gfx1, 0, 0x19C);
752
- *(gfx1 as *mut u32) = gfx_handler_vtable;
753
- (*wrapper)._field_4c0 = gfx1;
754
- (*wrapper)._field_4c4 = core::ptr::null_mut();
755
 
756
  // Build path list (order depends on headless + display_flags)
757
  let headless = (*game_info).headless_mode as i32;
@@ -787,9 +783,9 @@ unsafe fn init_graphics_and_resources(
787
  let mut gfx_loaded_idx = 0u32;
788
  for (i, path) in paths.iter().enumerate() {
789
  let fp = fopen(path.as_ptr(), b"rb\0".as_ptr());
790
- *(gfx1.add(0x198) as *mut *mut u8) = fp;
791
  if !fp.is_null()
792
- && call_gfx_load_dir(gfx1, crate::render::gfx_handler::gfx_load_dir_addr()) != 0
793
  {
794
  gfx_loaded_idx = i as u32;
795
  break;
@@ -806,7 +802,7 @@ unsafe fn init_graphics_and_resources(
806
  0
807
  };
808
 
809
- // ── GfxHandler #2 (conditional) ──
810
  let game_version = (*game_info).game_version;
811
  let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 };
812
  if game_version < threshold {
@@ -814,21 +810,16 @@ unsafe fn init_graphics_and_resources(
814
  let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
815
  gfx_c_path[14] = c_digit;
816
 
817
- let gfx2 = wa_malloc(0x19C);
818
- core::ptr::write_bytes(gfx2, 0, 0x19C);
819
- *(gfx2 as *mut u32) = gfx_handler_vtable;
820
- (*wrapper)._field_4c4 = gfx2;
821
 
822
  let fp = fopen(gfx_c_path.as_ptr(), b"rb\0".as_ptr());
823
- *(gfx2.add(0x198) as *mut *mut u8) = fp;
824
- if fp.is_null()
825
- || call_gfx_load_dir(gfx2, crate::render::gfx_handler::gfx_load_dir_addr()) == 0
826
- {
827
  let fp2 = fopen(b"data\\Gfx\\Gfx.dir\0".as_ptr(), b"rb\0".as_ptr());
828
- *(gfx2.add(0x198) as *mut *mut u8) = fp2;
829
- if fp2.is_null()
830
- || call_gfx_load_dir(gfx2, crate::render::gfx_handler::gfx_load_dir_addr()) == 0
831
- {
832
  panic!("DDGame: couldn't open secondary Gfx.dir");
833
  }
834
  }
@@ -840,16 +831,9 @@ unsafe fn init_graphics_and_resources(
840
  call_usercall_eax(wrapper, FUN_570A90_ADDR);
841
  }
842
  let disp = (*wrapper).display;
843
- let gfx_handler = (*wrapper)._field_4c0;
844
  DDDisplay::set_layer_color(disp, 1, 0xFE);
845
- DDDisplay::load_sprite(
846
- disp,
847
- 1,
848
- 1,
849
- 0,
850
- gfx_handler,
851
- rb(va::STR_CDROM_SPR) as *const u8,
852
- );
853
  DDDisplay::set_layer_visibility(disp, 1, -100);
854
 
855
  // Palette slot range init
@@ -880,7 +864,7 @@ unsafe fn init_graphics_and_resources(
880
  if (*wrapper).gfx_mode != 0 {
881
  // The layer_ctx is used as the output buffer, not a plain stack alloc.
882
  let res = gfx_resource_create(
883
- (*wrapper)._field_4c0,
884
  rb(va::STR_COLOURS_IMG) as *const u8,
885
  layer_ctx,
886
  );
@@ -906,7 +890,7 @@ unsafe fn init_graphics_and_resources(
906
  }
907
 
908
  // ── Secondary GfxDir object (DDGame+0x2C, conditional) ──
909
- if !(*wrapper)._field_4c4.is_null() {
910
  let gfxdir2 = wa_malloc(0x70C);
911
  core::ptr::write_bytes(gfxdir2, 0, 0x70C);
912
  *(gfxdir2 as *mut u16) = 1;
@@ -952,14 +936,13 @@ unsafe fn init_graphics_and_resources(
952
  }
953
  }
954
 
955
- // ── GfxResource: thiscall(ECX=gfx_handler) + EAX=name + 1 stack(output), RET 0x4 ──
956
  let gfx_resource: *mut u8;
957
  {
958
- let gfx_handler = (*wrapper)._field_4c0;
959
  let out_buf = wa_malloc(0x900);
960
  core::ptr::write_bytes(out_buf, 0, 0x900);
961
- gfx_resource =
962
- gfx_resource_create(gfx_handler, rb(va::STR_MASKS_IMG) as *const u8, out_buf);
963
  }
964
 
965
  // ── PCLandscape (alloc 0xB44, stdcall 11 params) ──
@@ -1054,7 +1037,7 @@ unsafe fn init_graphics_and_resources(
1054
 
1055
  // ── Arrow sprites + collision regions (32 iterations) ──
1056
  {
1057
- let gfx_handler = (*wrapper)._field_4c0;
1058
 
1059
  for i in 0u32..32 {
1060
  // Format "arrow%02u.img\0" into stack buffer
@@ -1064,25 +1047,25 @@ unsafe fn init_graphics_and_resources(
1064
 
1065
  let layer_ctx = DDDisplay::set_active_layer((*ddgame).display, 1);
1066
 
1067
- let entry = gfx_dir_find_entry(name_buf.as_ptr(), gfx_handler);
1068
 
1069
  let sprite: *mut u8;
1070
  if !entry.is_null() {
1071
- // Try gfx_handler->vtable[2](entry->field_4)
1072
- let gfx_vt = *(gfx_handler as *const *const u32);
1073
  let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
1074
  core::mem::transmute(*gfx_vt.add(2));
1075
  let entry_val = *(entry.add(4) as *const u32);
1076
- let cached = load_cached(gfx_handler, entry_val);
1077
  if !cached.is_null() {
1078
  sprite = wa_displaygfx_ctor(cached);
1079
  } else {
1080
  // Fallback: load from file via GfxDir__LoadImage + IMG_Decode
1081
- sprite = call_gfx_load_and_wrap(gfx_handler, name_buf.as_ptr(), layer_ctx);
1082
  }
1083
  } else {
1084
  // Entry not found — try direct file load
1085
- sprite = call_gfx_load_and_wrap(gfx_handler, name_buf.as_ptr(), layer_ctx);
1086
  }
1087
 
1088
  // Store arrow sprite at DDGame+0x38+i*4
@@ -1124,7 +1107,7 @@ unsafe fn init_graphics_and_resources(
1124
  let gfx_resource_create: unsafe extern "thiscall" fn(*mut u8, *mut u8) -> *mut u8 =
1125
  core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
1126
  (*ddgame).arrow_gfxdirs[i as usize] =
1127
- gfx_resource_create(gfx_handler, core::ptr::null_mut());
1128
  }
1129
  }
1130
  }
@@ -1200,12 +1183,12 @@ unsafe fn init_graphics_and_resources(
1200
 
1201
  // ── Sprite resource loading via DDGameWrapper vtable[0] ──
1202
  // DDNetGameWrapper__LoadResourceList: thiscall(ECX=wrapper) +
1203
- // 5 stack params (layer, gfx_handler, base_path, data_table, table_size)
1204
  {
1205
  let landscape_ptr = (*wrapper).landscape;
1206
- let water_layer = (*landscape_ptr).water_gfx_handler;
1207
- let land_layer = (*landscape_ptr).level_gfx_handler;
1208
- let gfx_handler = (*wrapper)._field_4c0;
1209
 
1210
  let wrapper_vt = *(wrapper as *const *const u32);
1211
  let load_resource_list: unsafe extern "thiscall" fn(
@@ -1220,7 +1203,7 @@ unsafe fn init_graphics_and_resources(
1220
  load_resource_list(
1221
  wrapper,
1222
  1,
1223
- gfx_handler,
1224
  rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8, // base path
1225
  rb(va::SPRITE_RESOURCE_TABLE_1) as *const u8,
1226
  0x1D88, // table size
@@ -1233,7 +1216,7 @@ unsafe fn init_graphics_and_resources(
1233
  load_resource_list(
1234
  wrapper,
1235
  1,
1236
- gfx_handler,
1237
  rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8,
1238
  rb(va::SPRITE_RESOURCE_TABLE_2) as *const u8,
1239
  0x18,
@@ -1302,7 +1285,7 @@ unsafe fn init_graphics_and_resources(
1302
  }
1303
 
1304
  // ── DDGame__LoadHudAndWeaponSprites (0x53D0E0) ──
1305
- wa_load_hud_sprites(gfx_handler, ddgame, (*wrapper)._field_4c4);
1306
 
1307
  // ── Loading progress ticks (2 of 4 — after LoadHudAndWeaponSprites) ──
1308
  call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
@@ -1321,12 +1304,12 @@ unsafe fn init_graphics_and_resources(
1321
  }
1322
 
1323
  // ── Release primary GfxHandler (vtable[3] = release, param 1 = free) ──
1324
- let gfx_handler_4c0 = (*wrapper)._field_4c0;
1325
- if !gfx_handler_4c0.is_null() {
1326
- let gfx_vt = *(gfx_handler_4c0 as *const *const u32);
1327
  let release: unsafe extern "thiscall" fn(*mut u8, u32) =
1328
  core::mem::transmute(*gfx_vt.add(3));
1329
- release(gfx_handler_4c0, 1);
1330
  }
1331
 
1332
  if !is_headless {
 
11
  use crate::input::keyboard::DDKeyboard;
12
  use crate::rebase::rb;
13
  // Re-export public GfxHandler functions so existing `engine::ddgame::*` imports keep working.
14
+ use crate::render::gfx_dir::{
15
+ call_gfx_find_and_load, call_gfx_load_and_wrap, call_gfx_load_dir, GfxDir,
 
 
 
16
  };
17
+ pub use crate::render::gfx_dir::{gfx_dir_find_entry, gfx_dir_load_dir, gfx_resource_create};
18
  use crate::render::landscape::PCLandscape;
19
  use crate::render::queue::RenderQueue;
20
  use crate::render::turn_order::TurnOrderWidget;
 
198
  pub rng_state_2: u32,
199
  /// 0x72F4-0x7307: Unknown
200
  pub _unknown_72f4: [u8; 0x7308 - 0x72F4],
201
+ /// 0x7308: Sprite/gfx dimension data (passed to GFX_DIR_LOAD_SPRITES).
202
  pub gfx_sprite_data: [u8; 0x730C - 0x7308],
203
  /// 0x730C-0x731C: 5 GfxDir color entries
204
  pub _gfx_color_entries: [u8; 0x7324 - 0x730C],
 
540
  LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS);
541
  LOADING_PROGRESS_TICK_ADDR = rb(va::DDGAME_WRAPPER_LOADING_PROGRESS_TICK);
542
  }
543
+ crate::render::gfx_dir::init_addrs();
544
  }
545
 
546
  // ── Typed wrappers for WA stdcall functions ────────────────────────────────
 
564
  param4: u32,
565
  ) {
566
  let f: unsafe extern "stdcall" fn(*mut DDGameWrapper, *mut u8, u32, u32) =
567
+ core::mem::transmute(rb(va::GFX_DIR_LOAD_SPRITES) as usize);
568
  f(wrapper, sprite_data, display_flags, param4);
569
  }
570
 
 
636
  }
637
 
638
  /// DDGame__LoadHudAndWeaponSprites (0x53D0E0): load weapon icons and HUD sprites.
639
+ /// thiscall(ECX=gfx_dir) + 2 stack(ddgame, secondary_gfx_dir), RET 0x8.
640
  #[cfg(target_arch = "x86")]
641
+ unsafe fn wa_load_hud_sprites(gfx_dir: *mut u8, ddgame: *mut DDGame, secondary: *mut u8) {
642
  let f: unsafe extern "thiscall" fn(*mut u8, *mut DDGame, *mut u8) =
643
  core::mem::transmute(rb(va::DDGAME_LOAD_HUD_AND_WEAPON_SPRITES) as usize);
644
+ f(gfx_dir, ddgame, secondary);
645
  }
646
 
647
  /// Create and initialize DDGame, matching DDGame__Constructor (0x56E220).
 
742
  let ddgame = (*wrapper).ddgame;
743
  let fopen: unsafe extern "cdecl" fn(*const u8, *const u8) -> *mut u8 =
744
  core::mem::transmute(rb(va::WA_FOPEN) as usize);
745
+ let gfx_dir_vtable = rb(va::GFX_DIR_VTABLE) as u32;
746
 
747
+ // ── GfxDir #1 (primary) ──
748
+ let gfx1 = GfxDir::alloc(gfx_dir_vtable);
749
+ (*wrapper).primary_gfx_dir = gfx1 as *mut u8;
750
+ (*wrapper).secondary_gfx_dir = core::ptr::null_mut();
 
 
751
 
752
  // Build path list (order depends on headless + display_flags)
753
  let headless = (*game_info).headless_mode as i32;
 
783
  let mut gfx_loaded_idx = 0u32;
784
  for (i, path) in paths.iter().enumerate() {
785
  let fp = fopen(path.as_ptr(), b"rb\0".as_ptr());
786
+ (*gfx1).file_handle = fp;
787
  if !fp.is_null()
788
+ && call_gfx_load_dir(gfx1 as *mut u8, crate::render::gfx_dir::gfx_load_dir_addr()) != 0
789
  {
790
  gfx_loaded_idx = i as u32;
791
  break;
 
802
  0
803
  };
804
 
805
+ // ── GfxDir #2 (conditional — supplemental sprites for certain game versions) ──
806
  let game_version = (*game_info).game_version;
807
  let threshold = if (*wrapper).gfx_mode != 0 { 33 } else { -2i32 };
808
  if game_version < threshold {
 
810
  let mut gfx_c_path = *b"data\\Gfx\\GfxC_3_0.dir\0";
811
  gfx_c_path[14] = c_digit;
812
 
813
+ let gfx2 = GfxDir::alloc(gfx_dir_vtable);
814
+ (*wrapper).secondary_gfx_dir = gfx2 as *mut u8;
 
 
815
 
816
  let fp = fopen(gfx_c_path.as_ptr(), b"rb\0".as_ptr());
817
+ (*gfx2).file_handle = fp;
818
+ let load_addr = crate::render::gfx_dir::gfx_load_dir_addr();
819
+ if fp.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
 
820
  let fp2 = fopen(b"data\\Gfx\\Gfx.dir\0".as_ptr(), b"rb\0".as_ptr());
821
+ (*gfx2).file_handle = fp2;
822
+ if fp2.is_null() || call_gfx_load_dir(gfx2 as *mut u8, load_addr) == 0 {
 
 
823
  panic!("DDGame: couldn't open secondary Gfx.dir");
824
  }
825
  }
 
831
  call_usercall_eax(wrapper, FUN_570A90_ADDR);
832
  }
833
  let disp = (*wrapper).display;
834
+ let gfx_dir = (*wrapper).primary_gfx_dir;
835
  DDDisplay::set_layer_color(disp, 1, 0xFE);
836
+ DDDisplay::load_sprite(disp, 1, 1, 0, gfx_dir, rb(va::STR_CDROM_SPR) as *const u8);
 
 
 
 
 
 
 
837
  DDDisplay::set_layer_visibility(disp, 1, -100);
838
 
839
  // Palette slot range init
 
864
  if (*wrapper).gfx_mode != 0 {
865
  // The layer_ctx is used as the output buffer, not a plain stack alloc.
866
  let res = gfx_resource_create(
867
+ (*wrapper).primary_gfx_dir,
868
  rb(va::STR_COLOURS_IMG) as *const u8,
869
  layer_ctx,
870
  );
 
890
  }
891
 
892
  // ── Secondary GfxDir object (DDGame+0x2C, conditional) ──
893
+ if !(*wrapper).secondary_gfx_dir.is_null() {
894
  let gfxdir2 = wa_malloc(0x70C);
895
  core::ptr::write_bytes(gfxdir2, 0, 0x70C);
896
  *(gfxdir2 as *mut u16) = 1;
 
936
  }
937
  }
938
 
939
+ // ── GfxResource: thiscall(ECX=gfx_dir) + EAX=name + 1 stack(output), RET 0x4 ──
940
  let gfx_resource: *mut u8;
941
  {
942
+ let gfx_dir = (*wrapper).primary_gfx_dir;
943
  let out_buf = wa_malloc(0x900);
944
  core::ptr::write_bytes(out_buf, 0, 0x900);
945
+ gfx_resource = gfx_resource_create(gfx_dir, rb(va::STR_MASKS_IMG) as *const u8, out_buf);
 
946
  }
947
 
948
  // ── PCLandscape (alloc 0xB44, stdcall 11 params) ──
 
1037
 
1038
  // ── Arrow sprites + collision regions (32 iterations) ──
1039
  {
1040
+ let gfx_dir = (*wrapper).primary_gfx_dir;
1041
 
1042
  for i in 0u32..32 {
1043
  // Format "arrow%02u.img\0" into stack buffer
 
1047
 
1048
  let layer_ctx = DDDisplay::set_active_layer((*ddgame).display, 1);
1049
 
1050
+ let entry = gfx_dir_find_entry(name_buf.as_ptr(), gfx_dir);
1051
 
1052
  let sprite: *mut u8;
1053
  if !entry.is_null() {
1054
+ // Try gfx_dir->vtable[2](entry->field_4)
1055
+ let gfx_vt = *(gfx_dir as *const *const u32);
1056
  let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
1057
  core::mem::transmute(*gfx_vt.add(2));
1058
  let entry_val = *(entry.add(4) as *const u32);
1059
+ let cached = load_cached(gfx_dir, entry_val);
1060
  if !cached.is_null() {
1061
  sprite = wa_displaygfx_ctor(cached);
1062
  } else {
1063
  // Fallback: load from file via GfxDir__LoadImage + IMG_Decode
1064
+ sprite = call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr(), layer_ctx);
1065
  }
1066
  } else {
1067
  // Entry not found — try direct file load
1068
+ sprite = call_gfx_load_and_wrap(gfx_dir, name_buf.as_ptr(), layer_ctx);
1069
  }
1070
 
1071
  // Store arrow sprite at DDGame+0x38+i*4
 
1107
  let gfx_resource_create: unsafe extern "thiscall" fn(*mut u8, *mut u8) -> *mut u8 =
1108
  core::mem::transmute(rb(va::GFX_RESOURCE_CREATE) as usize);
1109
  (*ddgame).arrow_gfxdirs[i as usize] =
1110
+ gfx_resource_create(gfx_dir, core::ptr::null_mut());
1111
  }
1112
  }
1113
  }
 
1183
 
1184
  // ── Sprite resource loading via DDGameWrapper vtable[0] ──
1185
  // DDNetGameWrapper__LoadResourceList: thiscall(ECX=wrapper) +
1186
+ // 5 stack params (layer, gfx_dir, base_path, data_table, table_size)
1187
  {
1188
  let landscape_ptr = (*wrapper).landscape;
1189
+ let water_layer = (*landscape_ptr).water_gfx_dir;
1190
+ let land_layer = (*landscape_ptr).level_gfx_dir;
1191
+ let gfx_dir = (*wrapper).primary_gfx_dir;
1192
 
1193
  let wrapper_vt = *(wrapper as *const *const u32);
1194
  let load_resource_list: unsafe extern "thiscall" fn(
 
1203
  load_resource_list(
1204
  wrapper,
1205
  1,
1206
+ gfx_dir,
1207
  rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8, // base path
1208
  rb(va::SPRITE_RESOURCE_TABLE_1) as *const u8,
1209
  0x1D88, // table size
 
1216
  load_resource_list(
1217
  wrapper,
1218
  1,
1219
+ gfx_dir,
1220
  rb(va::SPRITE_RESOURCE_BASE_PATH) as *const u8,
1221
  rb(va::SPRITE_RESOURCE_TABLE_2) as *const u8,
1222
  0x18,
 
1285
  }
1286
 
1287
  // ── DDGame__LoadHudAndWeaponSprites (0x53D0E0) ──
1288
+ wa_load_hud_sprites(gfx_dir, ddgame, (*wrapper).secondary_gfx_dir);
1289
 
1290
  // ── Loading progress ticks (2 of 4 — after LoadHudAndWeaponSprites) ──
1291
  call_usercall_ecx(wrapper, LOADING_PROGRESS_TICK_ADDR);
 
1304
  }
1305
 
1306
  // ── Release primary GfxHandler (vtable[3] = release, param 1 = free) ──
1307
+ let gfx_dir_4c0 = (*wrapper).primary_gfx_dir;
1308
+ if !gfx_dir_4c0.is_null() {
1309
+ let gfx_vt = *(gfx_dir_4c0 as *const *const u32);
1310
  let release: unsafe extern "thiscall" fn(*mut u8, u32) =
1311
  core::mem::transmute(*gfx_vt.add(3));
1312
+ release(gfx_dir_4c0, 1);
1313
  }
1314
 
1315
  if !is_headless {
crates/openwa-core/src/engine/ddgame_wrapper.rs CHANGED
@@ -31,10 +31,10 @@ pub struct DDGameWrapper {
31
  pub ddgame_secondary: *mut u8,
32
  /// 0x490-0x4BF: Unknown
33
  pub _unknown_490: [u8; 0x30],
34
- /// 0x4C0: Unknown object pointer (not GfxHandler — vtable reads as 0)
35
- pub _field_4c0: *mut u8,
36
- /// 0x4C4: Unknown pointer
37
- pub _field_4c4: *mut u8,
38
  /// 0x4C8: Graphics mode flag (DWORD index 0x132)
39
  pub gfx_mode: u32,
40
  /// 0x4CC: PCLandscape object pointer (DWORD index 0x133)
 
31
  pub ddgame_secondary: *mut u8,
32
  /// 0x490-0x4BF: Unknown
33
  pub _unknown_490: [u8; 0x30],
34
+ /// 0x4C0: Primary GfxDir — main sprite archive (Gfx.dir / Gfx0.dir / Gfx1.dir).
35
+ pub primary_gfx_dir: *mut u8,
36
+ /// 0x4C4: Secondary GfxDir — supplemental sprites (GfxC_3_0.dir), conditional on game version.
37
+ pub secondary_gfx_dir: *mut u8,
38
  /// 0x4C8: Graphics mode flag (DWORD index 0x132)
39
  pub gfx_mode: u32,
40
  /// 0x4CC: PCLandscape object pointer (DWORD index 0x133)
crates/openwa-core/src/render/{gfx_handler.rs → gfx_dir.rs} RENAMED
@@ -1,15 +1,61 @@
1
- //! GfxHandler/GfxDir resource loading subsystem.
 
 
 
 
2
 
3
  use crate::address::va;
4
  use crate::rebase::rb;
5
  use crate::wa_alloc::wa_malloc;
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  /// Entry in a GfxDir hash bucket linked list.
8
  /// Each entry maps a name string to a cached resource value.
9
  #[repr(C)]
10
  pub struct GfxDirEntry {
11
  pub next: *mut GfxDirEntry,
12
- /// Passed to GfxHandler vtable[2] for cached resource lookup.
13
  pub value: u32,
14
  pub _unknown_08: u32,
15
  // +0x0C: null-terminated name (variable-length, not in struct)
@@ -43,7 +89,7 @@ unsafe fn gfx_dir_hash(name: *const u8) -> u32 {
43
 
44
  /// Pure Rust implementation of GfxDir__FindEntry (0x566520).
45
  ///
46
- /// Convention: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4.
47
  ///
48
  /// Looks up a name in the GfxHandler's hash table. Names are case-insensitive.
49
  /// Supports `|`-separated fallback names (e.g. "path\\file.img|fallback.img").
@@ -55,10 +101,10 @@ unsafe fn gfx_dir_hash(name: *const u8) -> u32 {
55
  /// - entry+0x0C: name string (null-terminated, lowercase)
56
  ///
57
  /// # Safety
58
- /// `gfx_handler` must be a valid GfxHandler with initialized hash table at +0x04.
59
  /// `name` must be a valid null-terminated C string.
60
  #[cfg(target_arch = "x86")]
61
- pub unsafe fn gfx_dir_find_entry(name: *const u8, gfx_handler: *mut u8) -> *mut u8 {
62
  let mut current_name = name;
63
 
64
  loop {
@@ -106,8 +152,8 @@ pub unsafe fn gfx_dir_find_entry(name: *const u8, gfx_handler: *mut u8) -> *mut
106
  let bucket = gfx_dir_hash(buf.as_ptr());
107
 
108
  // Walk linked list in hash bucket
109
- // gfx_handler+4 = pointer to bucket array (1024 entries)
110
- let bucket_array = *(gfx_handler.add(4) as *const *const u32);
111
  let mut entry = *bucket_array.add(bucket as usize) as *mut GfxDirEntry;
112
 
113
  while !entry.is_null() {
@@ -145,20 +191,20 @@ pub unsafe fn gfx_dir_find_entry(name: *const u8, gfx_handler: *mut u8) -> *mut
145
  /// Pure Rust implementation of FUN_5665F0 (GfxHandler reset).
146
  ///
147
  /// Convention: usercall(ESI=handler), plain RET. Called at start of LoadDir.
148
- unsafe fn gfx_handler_reset(handler: *mut u8) {
149
- *(handler.add(0x04) as *mut u32) = 0;
150
- *(handler.add(0x08) as *mut u32) = 0;
151
- *(handler.add(0x194) as *mut u32) = 0;
 
152
 
153
  for i in 0..16u32 {
154
- // handler+0x0C + i*0x10 = 0 (16 entries at stride 0x10)
155
- *(handler.add(0x0C + i as usize * 0x10) as *mut u32) = 0;
156
- // handler+0x10C + i*4 = i (index array)
157
- *(handler.add(0x10C + i as usize * 4) as *mut u32) = i;
158
  }
159
 
160
- *(handler.add(400) as *mut u32) = 0; // 0x190
161
- *(handler.add(0x18C) as *mut u32) = 0x10;
162
  }
163
 
164
  /// Pure Rust implementation of GfxHandler__LoadDir (0x5663E0).
@@ -180,8 +226,9 @@ unsafe fn gfx_handler_reset(handler: *mut u8) {
180
  /// # Safety
181
  /// `handler` must be a valid GfxHandler with file handle at +0x198.
182
  #[cfg(target_arch = "x86")]
183
- pub unsafe fn gfx_handler_load_dir(handler: *mut u8) -> i32 {
184
- gfx_handler_reset(handler);
 
185
 
186
  let vt = *(handler as *const *const u32);
187
  // vtable[0] = read: thiscall(handler, buf, size) -> bytes_read
@@ -217,12 +264,10 @@ pub unsafe fn gfx_handler_load_dir(handler: *mut u8) -> i32 {
217
 
218
  // Try fast path: vtable[2] allocate (memory-maps the data)
219
  let data = vt_alloc(handler, alloc_size);
220
- *(handler.add(4) as *mut *mut u8) = data;
221
 
222
  if data.is_null() {
223
- // Fallback: seek past header, then malloc + read entire data block.
224
- // Original calls vt_seek(alloc_size) then WA_MallocMemset(EDI=read_size).
225
- // WA_MallocMemset allocates (size+3)&~3 + 0x20 and memsets size bytes.
226
  vt_seek(handler, alloc_size);
227
 
228
  let read_size = total_file_size - data_size - 4;
@@ -232,7 +277,7 @@ pub unsafe fn gfx_handler_load_dir(handler: *mut u8) -> i32 {
232
  return 0;
233
  }
234
  core::ptr::write_bytes(buf, 0, read_size as usize);
235
- *(handler.add(4) as *mut *mut u8) = buf;
236
 
237
  let bytes_read = vt_read(handler, buf, read_size);
238
  if bytes_read != read_size {
@@ -240,13 +285,12 @@ pub unsafe fn gfx_handler_load_dir(handler: *mut u8) -> i32 {
240
  return 0;
241
  }
242
 
243
- // Mark as "loaded from fallback" at handler+8
244
- *(handler.add(8) as *mut u32) = 1;
245
  }
246
 
247
  // Fix up relative pointers in the hash table
248
  // 1024 buckets at data[0..0x1000], each is a pointer to a linked list node
249
- let data = *(handler.add(4) as *const *mut u8);
250
  let base = data as u32;
251
 
252
  for bucket in 0..1024u32 {
@@ -276,14 +320,14 @@ pub unsafe fn gfx_handler_load_dir(handler: *mut u8) -> i32 {
276
  }
277
  }
278
 
279
- *(handler.add(0x194) as *mut u32) = 1;
280
 
281
  1 // success
282
  }
283
 
284
  /// Pure Rust implementation of GfxResource__Create_Maybe (0x4F6300).
285
  ///
286
- /// Convention: usercall(ECX=gfx_handler, EAX=name) + 1 stack(output), RET 0x4.
287
  ///
288
  /// Looks up `name` in the GfxHandler's directory, tries cached load, wraps
289
  /// as DisplayGfx. Falls back to loading the raw image and decoding it.
@@ -291,20 +335,16 @@ pub unsafe fn gfx_handler_load_dir(handler: *mut u8) -> i32 {
291
  /// # Safety
292
  /// All pointers must be valid WA objects.
293
  #[cfg(target_arch = "x86")]
294
- pub unsafe fn gfx_resource_create(
295
- gfx_handler: *mut u8,
296
- name: *const u8,
297
- output: *mut u8,
298
- ) -> *mut u8 {
299
  // 1. Try FindEntry → cached load → DisplayGfx wrap
300
- let entry = gfx_dir_find_entry(name, gfx_handler);
301
  if !entry.is_null() {
302
- // gfx_handler->vtable[2](entry->field_4) — cached load
303
- let vt = *(gfx_handler as *const *const u32);
304
  let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
305
  core::mem::transmute(*vt.add(2));
306
  let entry_val = (*(entry as *const GfxDirEntry)).value;
307
- let cached = load_cached(gfx_handler, entry_val);
308
  if !cached.is_null() {
309
  // DisplayGfx__Constructor_Maybe: stdcall(raw_image), RET 0x4
310
  let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
@@ -314,7 +354,7 @@ pub unsafe fn gfx_resource_create(
314
  }
315
 
316
  // 2. Fallback: LoadImage → IMG_Decode
317
- let raw_image = call_gfx_load_image(gfx_handler, name);
318
  if raw_image.is_null() {
319
  return core::ptr::null_mut();
320
  }
@@ -366,11 +406,11 @@ pub(crate) unsafe fn call_gfx_find_and_load(
366
  /// Used by arrow sprite loop when GfxDir__FindEntry returns null.
367
  #[cfg(target_arch = "x86")]
368
  pub(crate) unsafe fn call_gfx_load_and_wrap(
369
- gfx_handler: *mut u8,
370
  name: *const u8,
371
  display_ctx: *mut u8,
372
  ) -> *mut u8 {
373
- let image = call_gfx_load_image(gfx_handler, name);
374
  if image.is_null() {
375
  return core::ptr::null_mut();
376
  }
@@ -387,7 +427,7 @@ pub(crate) unsafe fn call_gfx_load_and_wrap(
387
 
388
  static mut GFX_LOAD_DIR_ADDR: u32 = 0;
389
 
390
- // GfxDir__LoadImage is usercall(ESI=gfx_handler) + 1 stack(name), RET 0x4.
391
  // Returns raw image pointer or null.
392
  static mut GFX_LOAD_IMAGE_ADDR: u32 = 0;
393
 
@@ -409,10 +449,10 @@ pub(crate) unsafe fn call_gfx_load_dir(handler: *mut u8, addr: u32) -> i32 {
409
 
410
  #[cfg(target_arch = "x86")]
411
  #[unsafe(naked)]
412
- unsafe extern "C" fn call_gfx_load_image(_gfx_handler: *mut u8, _name: *const u8) -> *mut u8 {
413
  core::arch::naked_asm!(
414
  "pushl %esi",
415
- "movl 8(%esp), %esi", // ESI = gfx_handler
416
  "pushl 12(%esp)", // push name (callee cleans via RET 0x4)
417
  "calll *({addr})",
418
  "popl %esi",
@@ -427,7 +467,7 @@ unsafe extern "C" fn call_gfx_load_image(_gfx_handler: *mut u8, _name: *const u8
427
  pub fn init_addrs() {
428
  unsafe {
429
  GFX_LOAD_IMAGE_ADDR = rb(va::GFX_DIR_LOAD_IMAGE);
430
- GFX_LOAD_DIR_ADDR = rb(va::GFX_HANDLER_LOAD_DIR);
431
  }
432
  }
433
 
 
1
+ //! GfxDir — .dir sprite archive reader.
2
+ //!
3
+ //! A GfxDir manages a hash table loaded from `.dir` files (e.g. `Gfx.dir`,
4
+ //! `Gfx0.dir`). It provides name→resource lookup via a 1024-bucket hash table
5
+ //! and delegates file I/O and caching to vtable methods.
6
 
7
  use crate::address::va;
8
  use crate::rebase::rb;
9
  use crate::wa_alloc::wa_malloc;
10
 
11
+ /// .dir archive reader (0x19C bytes, vtable 0x66B280).
12
+ ///
13
+ /// Loaded from `.dir` files via `gfx_dir_load`. Contains a 1024-bucket
14
+ /// hash table for name→resource lookups, plus a 16-slot cache.
15
+ #[repr(C)]
16
+ pub struct GfxDir {
17
+ /// 0x000: Vtable pointer (0x66B280).
18
+ pub vtable: u32,
19
+ /// 0x004: Bucket array — 1024 pointers to GfxDirEntry linked lists.
20
+ pub bucket_array: *mut u8,
21
+ /// 0x008: 1 if bucket_array was allocated via malloc fallback (needs free).
22
+ pub fallback_alloc: u32,
23
+ /// 0x00C-0x10B: 16 cache slots (0x10 bytes each, first u32 zeroed on reset).
24
+ pub cache_slots: [u8; 0x100],
25
+ /// 0x10C-0x14B: Index table (16 u32 entries, identity permutation on reset).
26
+ pub index_table: [u32; 16],
27
+ /// 0x14C-0x18B: Unknown padding.
28
+ pub _unknown_14c: [u8; 0x40],
29
+ /// 0x18C: Number of cache slots (always 0x10).
30
+ pub slot_count: u32,
31
+ /// 0x190: Unknown (zeroed on reset).
32
+ pub _unknown_190: u32,
33
+ /// 0x194: Loaded flag (1 after successful gfx_dir_load).
34
+ pub loaded: u32,
35
+ /// 0x198: FILE* handle to the open .dir file.
36
+ pub file_handle: *mut u8,
37
+ }
38
+
39
+ const _: () = assert!(core::mem::size_of::<GfxDir>() == 0x19C);
40
+
41
+ impl GfxDir {
42
+ /// Allocate and initialize a new GfxDir with the given vtable.
43
+ pub unsafe fn alloc(vtable: u32) -> *mut Self {
44
+ let ptr = wa_malloc(core::mem::size_of::<Self>() as u32) as *mut Self;
45
+ if !ptr.is_null() {
46
+ core::ptr::write_bytes(ptr as *mut u8, 0, core::mem::size_of::<Self>());
47
+ (*ptr).vtable = vtable;
48
+ }
49
+ ptr
50
+ }
51
+ }
52
+
53
  /// Entry in a GfxDir hash bucket linked list.
54
  /// Each entry maps a name string to a cached resource value.
55
  #[repr(C)]
56
  pub struct GfxDirEntry {
57
  pub next: *mut GfxDirEntry,
58
+ /// Passed to GfxDir vtable[2] for cached resource lookup.
59
  pub value: u32,
60
  pub _unknown_08: u32,
61
  // +0x0C: null-terminated name (variable-length, not in struct)
 
89
 
90
  /// Pure Rust implementation of GfxDir__FindEntry (0x566520).
91
  ///
92
+ /// Convention: usercall(EAX=name) + 1 stack(gfx_dir), RET 0x4.
93
  ///
94
  /// Looks up a name in the GfxHandler's hash table. Names are case-insensitive.
95
  /// Supports `|`-separated fallback names (e.g. "path\\file.img|fallback.img").
 
101
  /// - entry+0x0C: name string (null-terminated, lowercase)
102
  ///
103
  /// # Safety
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 u8, gfx_dir: *mut u8) -> *mut u8 {
108
  let mut current_name = name;
109
 
110
  loop {
 
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() {
 
191
  /// Pure Rust implementation of FUN_5665F0 (GfxHandler reset).
192
  ///
193
  /// Convention: usercall(ESI=handler), plain RET. Called at start of LoadDir.
194
+ unsafe fn gfx_dir_reset(handler: *mut u8) {
195
+ let gfx = &mut *(handler as *mut GfxDir);
196
+ gfx.bucket_array = core::ptr::null_mut();
197
+ gfx.fallback_alloc = 0;
198
+ gfx.loaded = 0;
199
 
200
  for i in 0..16u32 {
201
+ // Zero first u32 of each cache slot (stride 0x10)
202
+ *(gfx.cache_slots.as_mut_ptr().add(i as usize * 0x10) as *mut u32) = 0;
203
+ gfx.index_table[i as usize] = i;
 
204
  }
205
 
206
+ gfx._unknown_190 = 0;
207
+ gfx.slot_count = 0x10;
208
  }
209
 
210
  /// Pure Rust implementation of GfxHandler__LoadDir (0x5663E0).
 
226
  /// # Safety
227
  /// `handler` must be a valid GfxHandler with file handle at +0x198.
228
  #[cfg(target_arch = "x86")]
229
+ pub unsafe fn gfx_dir_load_dir(handler: *mut u8) -> i32 {
230
+ gfx_dir_reset(handler);
231
+ let gfx = &mut *(handler as *mut GfxDir);
232
 
233
  let vt = *(handler as *const *const u32);
234
  // vtable[0] = read: thiscall(handler, buf, size) -> bytes_read
 
264
 
265
  // Try fast path: vtable[2] allocate (memory-maps the data)
266
  let data = vt_alloc(handler, alloc_size);
267
+ gfx.bucket_array = data;
268
 
269
  if data.is_null() {
270
+ // Fallback: seek past header, then malloc + read entire data block
 
 
271
  vt_seek(handler, alloc_size);
272
 
273
  let read_size = total_file_size - data_size - 4;
 
277
  return 0;
278
  }
279
  core::ptr::write_bytes(buf, 0, read_size as usize);
280
+ gfx.bucket_array = buf;
281
 
282
  let bytes_read = vt_read(handler, buf, read_size);
283
  if bytes_read != read_size {
 
285
  return 0;
286
  }
287
 
288
+ gfx.fallback_alloc = 1;
 
289
  }
290
 
291
  // Fix up relative pointers in the hash table
292
  // 1024 buckets at data[0..0x1000], each is a pointer to a linked list node
293
+ let data = gfx.bucket_array;
294
  let base = data as u32;
295
 
296
  for bucket in 0..1024u32 {
 
320
  }
321
  }
322
 
323
+ gfx.loaded = 1;
324
 
325
  1 // success
326
  }
327
 
328
  /// Pure Rust implementation of GfxResource__Create_Maybe (0x4F6300).
329
  ///
330
+ /// Convention: usercall(ECX=gfx_dir, EAX=name) + 1 stack(output), RET 0x4.
331
  ///
332
  /// Looks up `name` in the GfxHandler's directory, tries cached load, wraps
333
  /// as DisplayGfx. Falls back to loading the raw image and decoding it.
 
335
  /// # Safety
336
  /// All pointers must be valid WA objects.
337
  #[cfg(target_arch = "x86")]
338
+ pub unsafe fn gfx_resource_create(gfx_dir: *mut u8, name: *const u8, output: *mut u8) -> *mut u8 {
 
 
 
 
339
  // 1. Try FindEntry → cached load → DisplayGfx wrap
340
+ let entry = gfx_dir_find_entry(name, gfx_dir);
341
  if !entry.is_null() {
342
+ // gfx_dir->vtable[2](entry->field_4) — cached load
343
+ let vt = *(gfx_dir as *const *const u32);
344
  let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
345
  core::mem::transmute(*vt.add(2));
346
  let entry_val = (*(entry as *const GfxDirEntry)).value;
347
+ let cached = load_cached(gfx_dir, entry_val);
348
  if !cached.is_null() {
349
  // DisplayGfx__Constructor_Maybe: stdcall(raw_image), RET 0x4
350
  let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
 
354
  }
355
 
356
  // 2. Fallback: LoadImage → IMG_Decode
357
+ let raw_image = call_gfx_load_image(gfx_dir, name);
358
  if raw_image.is_null() {
359
  return core::ptr::null_mut();
360
  }
 
406
  /// Used by arrow sprite loop when GfxDir__FindEntry returns null.
407
  #[cfg(target_arch = "x86")]
408
  pub(crate) unsafe fn call_gfx_load_and_wrap(
409
+ gfx_dir: *mut u8,
410
  name: *const u8,
411
  display_ctx: *mut u8,
412
  ) -> *mut u8 {
413
+ let image = call_gfx_load_image(gfx_dir, name);
414
  if image.is_null() {
415
  return core::ptr::null_mut();
416
  }
 
427
 
428
  static mut GFX_LOAD_DIR_ADDR: u32 = 0;
429
 
430
+ // GfxDir__LoadImage is usercall(ESI=gfx_dir) + 1 stack(name), RET 0x4.
431
  // Returns raw image pointer or null.
432
  static mut GFX_LOAD_IMAGE_ADDR: u32 = 0;
433
 
 
449
 
450
  #[cfg(target_arch = "x86")]
451
  #[unsafe(naked)]
452
+ unsafe extern "C" fn call_gfx_load_image(_gfx_dir: *mut u8, _name: *const u8) -> *mut u8 {
453
  core::arch::naked_asm!(
454
  "pushl %esi",
455
+ "movl 8(%esp), %esi", // ESI = gfx_dir
456
  "pushl 12(%esp)", // push name (callee cleans via RET 0x4)
457
  "calll *({addr})",
458
  "popl %esi",
 
467
  pub fn init_addrs() {
468
  unsafe {
469
  GFX_LOAD_IMAGE_ADDR = rb(va::GFX_DIR_LOAD_IMAGE);
470
+ GFX_LOAD_DIR_ADDR = rb(va::GFX_DIR_LOAD_DIR);
471
  }
472
  }
473
 
crates/openwa-core/src/render/landscape.rs CHANGED
@@ -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_handler: *mut u8,
90
  /// 0xB38: GfxHandler for Water.dir (param_1[0x2CE])
91
- pub water_gfx_handler: *mut u8,
92
  /// 0xB3C: Remaining control flag
93
  pub _unknown_b3c: u32,
94
  }
 
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
  }
crates/openwa-core/src/render/mod.rs CHANGED
@@ -1,4 +1,4 @@
1
- pub mod gfx_handler;
2
  pub mod landscape;
3
  pub mod queue;
4
  pub mod sprite;
 
1
+ pub mod gfx_dir;
2
  pub mod landscape;
3
  pub mod queue;
4
  pub mod sprite;
crates/openwa-harness/src/lib.rs CHANGED
@@ -301,7 +301,7 @@ mod tests {
301
  ("CGameTask", va::CGAMETASK_VTABLE),
302
  ("CGameTask2", va::CGAMETASK_VTABLE2),
303
  ("DDGameWrapper", va::DDGAME_WRAPPER_VTABLE),
304
- ("GfxHandler", va::GFX_HANDLER_VTABLE),
305
  ("DisplayGfx", va::DISPLAY_GFX_VTABLE),
306
  ("PCLandscape", va::PC_LANDSCAPE_VTABLE),
307
  ("LandscapeShader", va::LANDSCAPE_SHADER_VTABLE),
@@ -326,7 +326,7 @@ mod tests {
326
  ("CTask", va::CTASK_VTABLE),
327
  ("CGameTask", va::CGAMETASK_VTABLE),
328
  ("DDGameWrapper", va::DDGAME_WRAPPER_VTABLE),
329
- ("GfxHandler", va::GFX_HANDLER_VTABLE),
330
  ("DisplayGfx", va::DISPLAY_GFX_VTABLE),
331
  ("PCLandscape", va::PC_LANDSCAPE_VTABLE),
332
  ("LandscapeShader", va::LANDSCAPE_SHADER_VTABLE),
 
301
  ("CGameTask", va::CGAMETASK_VTABLE),
302
  ("CGameTask2", va::CGAMETASK_VTABLE2),
303
  ("DDGameWrapper", va::DDGAME_WRAPPER_VTABLE),
304
+ ("GfxHandler", va::GFX_DIR_VTABLE),
305
  ("DisplayGfx", va::DISPLAY_GFX_VTABLE),
306
  ("PCLandscape", va::PC_LANDSCAPE_VTABLE),
307
  ("LandscapeShader", va::LANDSCAPE_SHADER_VTABLE),
 
326
  ("CTask", va::CTASK_VTABLE),
327
  ("CGameTask", va::CGAMETASK_VTABLE),
328
  ("DDGameWrapper", va::DDGAME_WRAPPER_VTABLE),
329
+ ("GfxHandler", va::GFX_DIR_VTABLE),
330
  ("DisplayGfx", va::DISPLAY_GFX_VTABLE),
331
  ("PCLandscape", va::PC_LANDSCAPE_VTABLE),
332
  ("LandscapeShader", va::LANDSCAPE_SHADER_VTABLE),
crates/openwa-wormkit/src/replacements/ddgame_init.rs CHANGED
@@ -15,7 +15,7 @@ use crate::hook;
15
  use openwa_core::address::va;
16
  use openwa_core::engine::ddgame::{
17
  bit_grid_init, ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init,
18
- gfx_dir_find_entry, gfx_handler_load_dir, gfx_resource_create, DDGame,
19
  };
20
  use openwa_core::engine::DDGameWrapper;
21
 
@@ -83,10 +83,9 @@ extern "cdecl" fn impl_display_layer_init(wrapper: u32) -> u32 {
83
 
84
  // ─── GfxResource__Create_Maybe (0x4F6300) ───────────────────────────────────
85
 
86
- extern "cdecl" fn impl_gfx_resource_create(gfx_handler: u32, name: u32, output: u32) -> u32 {
87
- let result = unsafe {
88
- gfx_resource_create(gfx_handler as *mut u8, name as *const u8, output as *mut u8)
89
- };
90
  result as u32
91
  }
92
 
@@ -96,7 +95,7 @@ unsafe extern "C" fn gfx_resource_create_trampoline() {
96
  "push edx",
97
  "push [esp+8]", // output (stack param)
98
  "push eax", // name
99
- "push ecx", // gfx_handler
100
  "call {impl_fn}",
101
  "add esp, 12",
102
  "pop edx",
@@ -107,7 +106,7 @@ unsafe extern "C" fn gfx_resource_create_trampoline() {
107
 
108
  // ─── GfxDir__FindEntry (0x566520) ────────────────────────────────────────────
109
  //
110
- // Convention: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4.
111
 
112
  hook::usercall_trampoline!(
113
  fn find_entry_trampoline;
@@ -118,8 +117,8 @@ hook::usercall_trampoline!(
118
 
119
  static FIND_ENTRY_LOG_COUNT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
120
 
121
- extern "cdecl" fn impl_find_entry(name: u32, gfx_handler: u32) -> u32 {
122
- let result = unsafe { gfx_dir_find_entry(name as *const u8, gfx_handler as *mut u8) };
123
 
124
  // Log first 20 lookups for debugging
125
  let count = FIND_ENTRY_LOG_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
@@ -151,7 +150,7 @@ hook::usercall_trampoline!(
151
  );
152
 
153
  extern "cdecl" fn impl_load_dir(handler: u32) -> u32 {
154
- unsafe { gfx_handler_load_dir(handler as *mut u8) as u32 }
155
  }
156
 
157
  // ─── Hook installation ──────────────────────────────────────────────────────
@@ -196,7 +195,7 @@ pub fn install() -> Result<(), String> {
196
 
197
  hook::install(
198
  "GfxHandler__LoadDir",
199
- va::GFX_HANDLER_LOAD_DIR,
200
  load_dir_trampoline as *const (),
201
  )?;
202
  }
 
15
  use openwa_core::address::va;
16
  use openwa_core::engine::ddgame::{
17
  bit_grid_init, ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init,
18
+ gfx_dir_find_entry, gfx_dir_load_dir, gfx_resource_create, DDGame,
19
  };
20
  use openwa_core::engine::DDGameWrapper;
21
 
 
83
 
84
  // ─── GfxResource__Create_Maybe (0x4F6300) ───────────────────────────────────
85
 
86
+ extern "cdecl" fn impl_gfx_resource_create(gfx_dir: u32, name: u32, output: u32) -> u32 {
87
+ let result =
88
+ unsafe { gfx_resource_create(gfx_dir as *mut u8, name as *const u8, output as *mut u8) };
 
89
  result as u32
90
  }
91
 
 
95
  "push edx",
96
  "push [esp+8]", // output (stack param)
97
  "push eax", // name
98
+ "push ecx", // gfx_dir
99
  "call {impl_fn}",
100
  "add esp, 12",
101
  "pop edx",
 
106
 
107
  // ─── GfxDir__FindEntry (0x566520) ────────────────────────────────────────────
108
  //
109
+ // Convention: usercall(EAX=name) + 1 stack(gfx_dir), RET 0x4.
110
 
111
  hook::usercall_trampoline!(
112
  fn find_entry_trampoline;
 
117
 
118
  static FIND_ENTRY_LOG_COUNT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0);
119
 
120
+ extern "cdecl" fn impl_find_entry(name: u32, gfx_dir: u32) -> u32 {
121
+ let result = unsafe { gfx_dir_find_entry(name as *const u8, gfx_dir as *mut u8) };
122
 
123
  // Log first 20 lookups for debugging
124
  let count = FIND_ENTRY_LOG_COUNT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
 
150
  );
151
 
152
  extern "cdecl" fn impl_load_dir(handler: u32) -> u32 {
153
+ unsafe { gfx_dir_load_dir(handler as *mut u8) as u32 }
154
  }
155
 
156
  // ─── Hook installation ──────────────────────────────────────────────────────
 
195
 
196
  hook::install(
197
  "GfxHandler__LoadDir",
198
+ va::GFX_DIR_LOAD_DIR,
199
  load_dir_trampoline as *const (),
200
  )?;
201
  }
crates/openwa-wormkit/src/validation/mod.rs CHANGED
@@ -116,7 +116,7 @@ fn validate_addresses(result: &mut ValidationResult) {
116
  va::CGAMETASK_SOUND_EMITTER_VT,
117
  ),
118
  ("DDGameWrapper vtable", va::DDGAME_WRAPPER_VTABLE),
119
- ("GfxHandler vtable", va::GFX_HANDLER_VTABLE),
120
  ("DisplayGfx vtable", va::DISPLAY_GFX_VTABLE),
121
  ("PCLandscape vtable", va::PC_LANDSCAPE_VTABLE),
122
  ("LandscapeShader vtable", va::LANDSCAPE_SHADER_VTABLE),
@@ -316,7 +316,7 @@ fn validate_struct_offsets(result: &mut ValidationResult) {
316
  let _ = log_validation(" DDGameWrapper:");
317
  check_offset!(result, DDGameWrapper, vtable, 0x00);
318
  check_offset!(result, DDGameWrapper, ddgame, 0x488);
319
- check_offset!(result, DDGameWrapper, _field_4c0, 0x4C0);
320
  check_offset!(result, DDGameWrapper, landscape, 0x4CC);
321
  check_offset!(result, DDGameWrapper, display, 0x4D0);
322
 
@@ -825,12 +825,12 @@ fn dump_landscape() {
825
  land.resource_handle as u32
826
  ));
827
  let _ = log_validation(&format!(
828
- " level_gfx_handler: 0x{:08X}",
829
- land.level_gfx_handler as u32
830
  ));
831
  let _ = log_validation(&format!(
832
- " water_gfx_handler: 0x{:08X}",
833
- land.water_gfx_handler as u32
834
  ));
835
  let _ = log_validation(&format!(
836
  " visible_bounds: left={} top={} right={} bottom={}",
 
116
  va::CGAMETASK_SOUND_EMITTER_VT,
117
  ),
118
  ("DDGameWrapper vtable", va::DDGAME_WRAPPER_VTABLE),
119
+ ("GfxHandler vtable", va::GFX_DIR_VTABLE),
120
  ("DisplayGfx vtable", va::DISPLAY_GFX_VTABLE),
121
  ("PCLandscape vtable", va::PC_LANDSCAPE_VTABLE),
122
  ("LandscapeShader vtable", va::LANDSCAPE_SHADER_VTABLE),
 
316
  let _ = log_validation(" DDGameWrapper:");
317
  check_offset!(result, DDGameWrapper, vtable, 0x00);
318
  check_offset!(result, DDGameWrapper, ddgame, 0x488);
319
+ check_offset!(result, DDGameWrapper, primary_gfx_dir, 0x4C0);
320
  check_offset!(result, DDGameWrapper, landscape, 0x4CC);
321
  check_offset!(result, DDGameWrapper, display, 0x4D0);
322
 
 
825
  land.resource_handle as u32
826
  ));
827
  let _ = log_validation(&format!(
828
+ " level_gfx_dir: 0x{:08X}",
829
+ land.level_gfx_dir as u32
830
  ));
831
  let _ = log_validation(&format!(
832
+ " water_gfx_dir: 0x{:08X}",
833
+ land.water_gfx_dir as u32
834
  ));
835
  let _ = log_validation(&format!(
836
  " visible_bounds: left={} top={} right={} bottom={}",