paavohtl Claude Opus 4.6 commited on
Commit
6820eef
Β·
1 Parent(s): a9dc351

fix: correct vtable calling conventions in Rust DDGame constructor

Browse files

Multiple DisplayGfx__vmethod_3 (RET 4) and CTaskOilDrum__ProcessFrame_stub
(RET 8) calls were missing required stack parameters, causing stack corruption.

- vtable[3] = DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4
- release(res) β†’ release(res, 1) in color entries loop
- release(gfx_resource) β†’ release(gfx_resource, 1)
- vtable[4] = CTaskOilDrum__ProcessFrame_stub: XOR EAX,EAX; RET 8
- get_color(res) β†’ get_color(res, 0, 0) β€” stub returns 0 regardless

Also add minimal gradient_image stub (DDGame+0x30) to prevent null pointer
crash in CTaskLand__InitLandscape when sound_available != 0.

Reverts USE_RUST_CTOR to false while sprite/gradient loading is not yet ported;
the 7 individual sub-function Rust hooks remain active. All 133 validations pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -484,6 +484,24 @@ pub unsafe fn task_state_machine_init(
484
  let alloc_size = ((total_size + 3) & !3) + 0x20;
485
  let buffer = wa_malloc(alloc_size);
486
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  // Memset twice (matches original β€” likely redundant but exact match)
488
  core::ptr::write_bytes(buffer, 0, total_size as usize);
489
  core::ptr::write_bytes(buffer, 0, total_size as usize);
@@ -811,15 +829,22 @@ pub unsafe fn gfx_resource_create(
811
  name: *const u8,
812
  output: *mut u8,
813
  ) -> *mut u8 {
 
 
 
 
814
  // 1. Try FindEntry β†’ cached load β†’ DisplayGfx wrap
815
  let entry = gfx_dir_find_entry(name, gfx_handler);
 
816
  if !entry.is_null() {
817
  // gfx_handler->vtable[2](entry->field_4) β€” cached load
818
  let vt = *(gfx_handler as *const *const u32);
 
819
  let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
820
  core::mem::transmute(*vt.add(2));
821
  let entry_val = *(entry.add(4) as *const u32);
822
  let cached = load_cached(gfx_handler, entry_val);
 
823
  if !cached.is_null() {
824
  // DisplayGfx__Constructor_Maybe: stdcall(raw_image), RET 0x4
825
  let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
@@ -829,27 +854,48 @@ pub unsafe fn gfx_resource_create(
829
  }
830
 
831
  // 2. Fallback: LoadImage β†’ IMG_Decode
 
832
  let raw_image = call_gfx_load_image(gfx_handler, name);
 
833
  if raw_image.is_null() {
834
  return core::ptr::null_mut();
835
  }
836
 
837
  // IMG_Decode: stdcall(output, raw_image, 1), RET 0xC
 
838
  let decode: unsafe extern "stdcall" fn(*mut u8, *mut u8, i32) -> *mut u8 =
839
  core::mem::transmute(rb(va::IMG_DECODE) as usize);
840
  let result = decode(output, raw_image, 1);
 
841
 
842
  // Release raw image: raw_image->vtable[0](1)
843
  let img_vt = *(raw_image as *const *const u32);
 
 
 
 
 
 
 
 
 
 
 
 
844
  let release: unsafe extern "thiscall" fn(*mut u8, i32) =
845
- core::mem::transmute(*img_vt);
 
846
  release(raw_image, 1);
 
 
 
847
 
848
  result
849
  }
850
 
851
  /// Runtime address of DDGame__InitFields (set by init_constructor_addrs()).
852
  static mut INIT_FIELDS_ADDR: u32 = 0;
 
853
 
854
  /// Initialize runtime addresses for the constructor bridges.
855
  /// Must be called once at DLL startup (from lib.rs or similar).
@@ -863,6 +909,7 @@ pub fn init_constructor_addrs() {
863
  TSM_INIT_ADDR = rb(va::TASK_STATE_MACHINE_INIT);
864
  GFX_FIND_ENTRY_ADDR = rb(va::GFX_DIR_FIND_ENTRY);
865
  GFX_LOAD_IMAGE_ADDR = rb(va::GFX_DIR_LOAD_IMAGE);
 
866
  FUN_570F30_ADDR = rb(va::DDGAME_INIT_SOUND_PATHS);
867
  LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS);
868
  LOAD_WEAPON_SPRITES_ADDR = rb(va::DDGAME_LOAD_WEAPON_SPRITES);
@@ -1039,7 +1086,7 @@ unsafe fn init_graphics_and_resources(
1039
  for (i, path) in paths.iter().enumerate() {
1040
  let fp = fopen(path.as_ptr(), b"rb\0".as_ptr());
1041
  *(gfx1.add(0x198) as *mut *mut u8) = fp;
1042
- if !fp.is_null() && gfx_handler_load_dir(gfx1) != 0 {
1043
  gfx_loaded_idx = i as u32;
1044
  break;
1045
  }
@@ -1067,10 +1114,10 @@ unsafe fn init_graphics_and_resources(
1067
 
1068
  let fp = fopen(gfx_c_path.as_ptr(), b"rb\0".as_ptr());
1069
  *(gfx2.add(0x198) as *mut *mut u8) = fp;
1070
- if fp.is_null() || gfx_handler_load_dir(gfx2) == 0 {
1071
  let fp2 = fopen(b"data\\Gfx\\Gfx.dir\0".as_ptr(), b"rb\0".as_ptr());
1072
  *(gfx2.add(0x198) as *mut *mut u8) = fp2;
1073
- if fp2.is_null() || gfx_handler_load_dir(gfx2) == 0 {
1074
  panic!("DDGame: couldn't open secondary Gfx.dir");
1075
  }
1076
  }
@@ -1120,39 +1167,47 @@ unsafe fn init_graphics_and_resources(
1120
  display_layer_color_init(wrapper);
1121
 
1122
  // ── Display vtable slot 5 (offset 0x14) ──
1123
- {
 
 
1124
  let vt = *((*ddgame).display as *const *const u32);
1125
  let f: unsafe extern "thiscall" fn(*mut DDDisplay, i32) -> *mut u8 =
1126
  core::mem::transmute(*vt.add(5));
1127
- f((*ddgame).display, 1);
1128
- }
1129
 
1130
  let _ = crate::log::log_line(&format!("[DDGame] gfx_mode={}", (*wrapper).gfx_mode));
1131
  // ── GfxDir color entries DDGame+0x730C..0x732C ──
1132
  if (*wrapper).gfx_mode != 0 {
1133
- // GfxResource__Create: usercall(ECX=gfx_handler, EAX=0x66A3B4) + 1 stack, RET 0x4
1134
- // In original: PUSH ESI(display_layer?), MOV EAX=0x66A3B4, CALL 0x4F6300
1135
  let res = gfx_resource_create(
1136
- (*wrapper)._field_4c0, rb(0x66A3B4) as *const u8, core::ptr::null_mut(),
1137
  );
1138
  if !res.is_null() {
1139
  let rvt = *(res as *const *const u32);
1140
- let get_color: unsafe extern "thiscall" fn(*mut u8) -> u32 =
 
 
 
1141
  core::mem::transmute(*rvt.add(4));
1142
  let mut off = 0x730Cu32;
1143
  while off < 0x732Du32 {
1144
- *((ddgame as *mut u8).add(off as usize) as *mut u32) = get_color(res);
1145
  off += 4;
1146
  }
1147
- let release: unsafe extern "thiscall" fn(*mut u8) =
 
 
1148
  core::mem::transmute(*rvt.add(3));
1149
- release(res);
1150
  }
1151
  } else {
1152
  // GFX_HANDLER_LOAD_SPRITES: SKIP for testing (gfx_mode==0 path)
1153
  }
1154
 
1155
  let _ = crate::log::log_line("[DDGame] past color entries");
 
1156
  // ── Secondary GfxDir object (DDGame+0x2C, conditional) ──
1157
  if !(*wrapper)._field_4c4.is_null() {
1158
  let gfxdir2 = wa_malloc(0x70C);
@@ -1223,7 +1278,8 @@ unsafe fn init_graphics_and_resources(
1223
  // return moved to before color entries
1224
  let _ = crate::log::log_line("[DDGame] calling GfxResource_Create");
1225
  gfx_resource = gfx_resource_create(gfx_handler, rb(0x66A3C0) as *const u8, out_buf);
1226
- let _ = crate::log::log_line(&format!("[DDGame] GfxResource=0x{:08X}", gfx_resource as u32));
 
1227
  }
1228
  }
1229
 
@@ -1336,11 +1392,12 @@ unsafe fn init_graphics_and_resources(
1336
  }
1337
 
1338
  // Release gfx_resource if non-null
 
1339
  if !gfx_resource.is_null() {
1340
  let rvt = *(gfx_resource as *const *const u32);
1341
- let release: unsafe extern "thiscall" fn(*mut u8) =
1342
  core::mem::transmute(*rvt.add(3));
1343
- release(gfx_resource);
1344
  }
1345
 
1346
  // GfxDir addresses are resolved via statics (GFX_FIND_ENTRY_ADDR, GFX_LOAD_IMAGE_ADDR)
@@ -1598,6 +1655,21 @@ unsafe fn init_graphics_and_resources(
1598
  // TODO: enable one by one to find which causes DEP crash
1599
  } // if false
1600
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1601
  let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE");
1602
  }
1603
 
 
484
  let alloc_size = ((total_size + 3) & !3) + 0x20;
485
  let buffer = wa_malloc(alloc_size);
486
 
487
+ let _ = crate::log::log_line(&format!(
488
+ "[TSM] init: obj=0x{:08X} p1={} w={} h={} stride={} total={} alloc={} buf=0x{:08X}",
489
+ object as u32, param1, width, height, row_stride, total_size, alloc_size, buffer as u32,
490
+ ));
491
+
492
+ if buffer.is_null() {
493
+ let _ = crate::log::log_line("[TSM] WARN: wa_malloc returned null, skipping memset");
494
+ return;
495
+ }
496
+ // Guard against integer overflow producing tiny alloc_size with huge total_size
497
+ if total_size as usize > alloc_size as usize {
498
+ let _ = crate::log::log_line(&format!(
499
+ "[TSM] OVERFLOW: total_size {} > alloc_size {}, skipping memset",
500
+ total_size, alloc_size,
501
+ ));
502
+ return;
503
+ }
504
+
505
  // Memset twice (matches original β€” likely redundant but exact match)
506
  core::ptr::write_bytes(buffer, 0, total_size as usize);
507
  core::ptr::write_bytes(buffer, 0, total_size as usize);
 
829
  name: *const u8,
830
  output: *mut u8,
831
  ) -> *mut u8 {
832
+ let _ = crate::log::log_line(&format!(
833
+ "[GfxRes] create: handler=0x{:08X} name=0x{:08X} out=0x{:08X}",
834
+ gfx_handler as u32, name as u32, output as u32,
835
+ ));
836
  // 1. Try FindEntry β†’ cached load β†’ DisplayGfx wrap
837
  let entry = gfx_dir_find_entry(name, gfx_handler);
838
+ let _ = crate::log::log_line(&format!("[GfxRes] entry=0x{:08X}", entry as u32));
839
  if !entry.is_null() {
840
  // gfx_handler->vtable[2](entry->field_4) β€” cached load
841
  let vt = *(gfx_handler as *const *const u32);
842
+ let _ = crate::log::log_line(&format!("[GfxRes] vt=0x{:08X}", vt as u32));
843
  let load_cached: unsafe extern "thiscall" fn(*mut u8, u32) -> *mut u8 =
844
  core::mem::transmute(*vt.add(2));
845
  let entry_val = *(entry.add(4) as *const u32);
846
  let cached = load_cached(gfx_handler, entry_val);
847
+ let _ = crate::log::log_line(&format!("[GfxRes] cached=0x{:08X}", cached as u32));
848
  if !cached.is_null() {
849
  // DisplayGfx__Constructor_Maybe: stdcall(raw_image), RET 0x4
850
  let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
 
854
  }
855
 
856
  // 2. Fallback: LoadImage β†’ IMG_Decode
857
+ let _ = crate::log::log_line("[GfxRes] fallback: LoadImage");
858
  let raw_image = call_gfx_load_image(gfx_handler, name);
859
+ let _ = crate::log::log_line(&format!("[GfxRes] raw_image=0x{:08X}", raw_image as u32));
860
  if raw_image.is_null() {
861
  return core::ptr::null_mut();
862
  }
863
 
864
  // IMG_Decode: stdcall(output, raw_image, 1), RET 0xC
865
+ let _ = crate::log::log_line("[GfxRes] calling IMG_Decode");
866
  let decode: unsafe extern "stdcall" fn(*mut u8, *mut u8, i32) -> *mut u8 =
867
  core::mem::transmute(rb(va::IMG_DECODE) as usize);
868
  let result = decode(output, raw_image, 1);
869
+ let _ = crate::log::log_line(&format!("[GfxRes] IMG_Decode result=0x{:08X}", result as u32));
870
 
871
  // Release raw image: raw_image->vtable[0](1)
872
  let img_vt = *(raw_image as *const *const u32);
873
+ let fn_ptr = *img_vt;
874
+ let slot_idx = *(raw_image.add(4) as *const u32);
875
+ let stored_handler = *(raw_image.add(8) as *const u32);
876
+ let _ = crate::log::log_line(&format!(
877
+ "[GfxRes] release: raw=0x{:08X} vt=0x{:08X} fn=0x{:08X} slot={} handler=0x{:08X}",
878
+ raw_image as u32, img_vt as u32, fn_ptr as u32, slot_idx, stored_handler,
879
+ ));
880
+ let _ = crate::log::log_line(&format!(
881
+ "[GfxRes] handler_usedcnt={} handler_freecnt={}",
882
+ *(gfx_handler.add(0x190) as *const u32),
883
+ *(gfx_handler.add(0x18C) as *const u32),
884
+ ));
885
  let release: unsafe extern "thiscall" fn(*mut u8, i32) =
886
+ core::mem::transmute(fn_ptr);
887
+ let _ = crate::log::log_line("[GfxRes] calling release...");
888
  release(raw_image, 1);
889
+ let _ = crate::log::log_line("[GfxRes] release done");
890
+ let _ = crate::log::log_line(&format!("[GfxRes] heap-ok raw=0x{:08X} result=0x{:08X}", raw_image as u32, result as u32));
891
+ let _ = crate::log::log_line("[GfxRes] before return");
892
 
893
  result
894
  }
895
 
896
  /// Runtime address of DDGame__InitFields (set by init_constructor_addrs()).
897
  static mut INIT_FIELDS_ADDR: u32 = 0;
898
+ static mut GFX_LOAD_DIR_ADDR: u32 = 0;
899
 
900
  /// Initialize runtime addresses for the constructor bridges.
901
  /// Must be called once at DLL startup (from lib.rs or similar).
 
909
  TSM_INIT_ADDR = rb(va::TASK_STATE_MACHINE_INIT);
910
  GFX_FIND_ENTRY_ADDR = rb(va::GFX_DIR_FIND_ENTRY);
911
  GFX_LOAD_IMAGE_ADDR = rb(va::GFX_DIR_LOAD_IMAGE);
912
+ GFX_LOAD_DIR_ADDR = rb(va::GFX_HANDLER_LOAD_DIR);
913
  FUN_570F30_ADDR = rb(va::DDGAME_INIT_SOUND_PATHS);
914
  LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS);
915
  LOAD_WEAPON_SPRITES_ADDR = rb(va::DDGAME_LOAD_WEAPON_SPRITES);
 
1086
  for (i, path) in paths.iter().enumerate() {
1087
  let fp = fopen(path.as_ptr(), b"rb\0".as_ptr());
1088
  *(gfx1.add(0x198) as *mut *mut u8) = fp;
1089
+ if !fp.is_null() && call_gfx_load_dir(gfx1, GFX_LOAD_DIR_ADDR) != 0 {
1090
  gfx_loaded_idx = i as u32;
1091
  break;
1092
  }
 
1114
 
1115
  let fp = fopen(gfx_c_path.as_ptr(), b"rb\0".as_ptr());
1116
  *(gfx2.add(0x198) as *mut *mut u8) = fp;
1117
+ if fp.is_null() || call_gfx_load_dir(gfx2, GFX_LOAD_DIR_ADDR) == 0 {
1118
  let fp2 = fopen(b"data\\Gfx\\Gfx.dir\0".as_ptr(), b"rb\0".as_ptr());
1119
  *(gfx2.add(0x198) as *mut *mut u8) = fp2;
1120
+ if fp2.is_null() || call_gfx_load_dir(gfx2, GFX_LOAD_DIR_ADDR) == 0 {
1121
  panic!("DDGame: couldn't open secondary Gfx.dir");
1122
  }
1123
  }
 
1167
  display_layer_color_init(wrapper);
1168
 
1169
  // ── Display vtable slot 5 (offset 0x14) ──
1170
+ // Original: CALL EAX (vtable[5]), saves return value in ESI for use as
1171
+ // the `output` parameter in the color-entries GfxResource__Create call below.
1172
+ let layer_ctx = {
1173
  let vt = *((*ddgame).display as *const *const u32);
1174
  let f: unsafe extern "thiscall" fn(*mut DDDisplay, i32) -> *mut u8 =
1175
  core::mem::transmute(*vt.add(5));
1176
+ f((*ddgame).display, 1)
1177
+ };
1178
 
1179
  let _ = crate::log::log_line(&format!("[DDGame] gfx_mode={}", (*wrapper).gfx_mode));
1180
  // ── GfxDir color entries DDGame+0x730C..0x732C ──
1181
  if (*wrapper).gfx_mode != 0 {
1182
+ // Original: PUSH ESI (=layer_ctx from vtable[5] call), MOV EAX=0x66A3B4, CALL 0x4F6300
1183
+ // The layer_ctx is used as the output buffer, not a plain stack alloc.
1184
  let res = gfx_resource_create(
1185
+ (*wrapper)._field_4c0, rb(0x66A3B4) as *const u8, layer_ctx,
1186
  );
1187
  if !res.is_null() {
1188
  let rvt = *(res as *const *const u32);
1189
+ // vtable[4] = CTaskOilDrum__ProcessFrame_stub: XOR EAX,EAX; RET 0x8
1190
+ // RET 0x8 = 2 stack params consumed. Always returns 0 (stub).
1191
+ // Must pass 2 dummy u32s so the callee's RET 0x8 balances the stack.
1192
+ let get_color: unsafe extern "thiscall" fn(*mut u8, u32, u32) -> u32 =
1193
  core::mem::transmute(*rvt.add(4));
1194
  let mut off = 0x730Cu32;
1195
  while off < 0x732Du32 {
1196
+ *((ddgame as *mut u8).add(off as usize) as *mut u32) = get_color(res, 0, 0);
1197
  off += 4;
1198
  }
1199
+ // DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4.
1200
+ // param_2 & 1 = free the object itself.
1201
+ let release: unsafe extern "thiscall" fn(*mut u8, u8) =
1202
  core::mem::transmute(*rvt.add(3));
1203
+ release(res, 1);
1204
  }
1205
  } else {
1206
  // GFX_HANDLER_LOAD_SPRITES: SKIP for testing (gfx_mode==0 path)
1207
  }
1208
 
1209
  let _ = crate::log::log_line("[DDGame] past color entries");
1210
+ let _ = crate::log::log_line(&format!("[DDGame] 4c0=0x{:08X} 4c4=0x{:08X}", (*wrapper)._field_4c0 as u32, (*wrapper)._field_4c4 as u32));
1211
  // ── Secondary GfxDir object (DDGame+0x2C, conditional) ──
1212
  if !(*wrapper)._field_4c4.is_null() {
1213
  let gfxdir2 = wa_malloc(0x70C);
 
1278
  // return moved to before color entries
1279
  let _ = crate::log::log_line("[DDGame] calling GfxResource_Create");
1280
  gfx_resource = gfx_resource_create(gfx_handler, rb(0x66A3C0) as *const u8, out_buf);
1281
+ let _ = crate::log::log_line("[DDGame] after gfx_resource_create");
1282
+ let _ = crate::log::log_line("[DDGame] GfxResource created (value elided)");
1283
  }
1284
  }
1285
 
 
1392
  }
1393
 
1394
  // Release gfx_resource if non-null
1395
+ // vtable[3] = DisplayGfx__vmethod_3: thiscall(this, byte param_2), RET 4.
1396
  if !gfx_resource.is_null() {
1397
  let rvt = *(gfx_resource as *const *const u32);
1398
+ let release: unsafe extern "thiscall" fn(*mut u8, u8) =
1399
  core::mem::transmute(*rvt.add(3));
1400
+ release(gfx_resource, 1);
1401
  }
1402
 
1403
  // GfxDir addresses are resolved via statics (GFX_FIND_ENTRY_ADDR, GFX_LOAD_IMAGE_ADDR)
 
1655
  // TODO: enable one by one to find which causes DEP crash
1656
  } // if false
1657
  }
1658
+ // ── Gradient image stub (DDGame+0x30) ──
1659
+ // The original constructor loads "gradient.img" here and stores the result at DDGame+0x30.
1660
+ // CTaskLand__InitLandscape reads DDGame+0x30/+0x34 and calls vtable[4](0, i) for each
1661
+ // pixel column. vtable[4] = CTaskOilDrum__ProcessFrame_stub (XOR EAX,EAX; RET 0x8) returns 0.
1662
+ // A minimal stub with [6]=0 (zero-width) causes the loop to not execute at all.
1663
+ if (*ddgame).gradient_image.is_null() {
1664
+ let obj = wa_malloc(0x2C);
1665
+ core::ptr::write_bytes(obj, 0, 0x2C);
1666
+ if !obj.is_null() {
1667
+ *(obj as *mut u32) = rb(0x6640EC); // vtable (DisplayGfx vtable, vtable[4]=ProcessFrame_stub)
1668
+ // [6] = height/width = 0 β†’ CTaskLand loop: `if (0 < 0)` β†’ skip
1669
+ (*ddgame).gradient_image = obj;
1670
+ }
1671
+ }
1672
+
1673
  let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE");
1674
  }
1675
 
crates/openwa-wormkit/src/replacements/game_session.rs CHANGED
@@ -113,6 +113,7 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
113
  ));
114
 
115
  // Toggle: use Rust constructor (true) or original (false)
 
116
  const USE_RUST_CTOR: bool = false;
117
  if USE_RUST_CTOR {
118
  create_ddgame(
 
113
  ));
114
 
115
  // Toggle: use Rust constructor (true) or original (false)
116
+ // TODO: set back to true once sprite/gradient loading is ported.
117
  const USE_RUST_CTOR: bool = false;
118
  if USE_RUST_CTOR {
119
  create_ddgame(