paavohtl Claude Opus 4.6 (1M context) commited on
Commit
93f1a62
·
1 Parent(s): 57a95a3

fix: correct display vtable call param counts — root cause of crash

Browse files

Display palette vtable calls had wrong parameter counts:
- vtable[4] (offset 0x10): was fn(self), should be fn(self, 1, 0xFE)
The callee does RET 0x8 (cleans 2 params) but we pushed 0 → stack
corruption of 8 bytes per call
- vtable[0x1F] (offset 0x7C): was fn(self, 3 params), should be
fn(self, 5 params) including gfx_handler and data table ptr
- vtable[0x17] (offset 0x5C): was correct (2 params)

The stack corruption from vtable[4] shifted the return address,
causing DEP violations (execute from ntdll address) on return.
This was the deterministic crash — not a race condition.

Re-enable all audio calls and loading tick.

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

crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -427,8 +427,6 @@ pub unsafe fn create_ddgame(
427
  game_info: *mut GameInfo,
428
  network_ecx: u32, // implicit ECX from caller
429
  ) -> *mut DDGame {
430
- let _ = log_line("[DDGame] create_ddgame: begin");
431
-
432
  // ── 1. Allocate and zero-fill (matches: memset(piVar3, 0, 0x98B8)) ──
433
  let ddgame = wa_malloc(0x98B8) as *mut DDGame;
434
  if ddgame.is_null() {
@@ -457,6 +455,14 @@ pub unsafe fn create_ddgame(
457
  // Now safe to expose — all fields that concurrent readers check are set.
458
  (*wrapper).ddgame = ddgame;
459
 
 
 
 
 
 
 
 
 
460
  // ── 5. Set g_GameInfo global ──
461
  *(rb(va::G_GAME_INFO) as *mut *mut GameInfo) = game_info;
462
 
@@ -489,11 +495,7 @@ pub unsafe fn create_ddgame(
489
  // ── 10. GfxHandler, landscape, sprites, audio, resources ──
490
  init_graphics_and_resources(wrapper, game_info, net_game, display, is_headless);
491
 
492
- let _ = log_line(&format!(
493
- "[DDGame] create_ddgame done: landscape=0x{:08X}, display_gfx=0x{:08X}, coord_list=0x{:08X}",
494
- (*ddgame).landscape as u32, (*ddgame).display_gfx as u32, (*ddgame).coord_list as u32,
495
- ));
496
-
497
  ddgame
498
  }
499
 
@@ -581,7 +583,6 @@ unsafe fn init_graphics_and_resources(
581
  }
582
  }
583
 
584
- let _ = log_line("[DDGame] past gfxhandlers");
585
  // ── Display palette setup (non-headless) ──
586
  if !is_headless {
587
  if *(rb(0x88E485) as *const u8) == 0 {
@@ -591,16 +592,20 @@ unsafe fn init_graphics_and_resources(
591
  // BUT: FUN_00570A90 or something else may write to wrapper in ways we
592
  // don't expect. Read via struct field instead of raw offset.
593
  let disp = (*wrapper).display as *mut u8;
 
594
  let vt = *(disp as *const *const u32);
595
- let vt_10: unsafe extern "thiscall" fn(*mut u8) =
 
596
  core::mem::transmute(*vt.add(4));
597
- vt_10(disp);
598
- let vt_7c: unsafe extern "thiscall" fn(*mut u8, i32, i32, i32) =
 
599
  core::mem::transmute(*vt.add(0x1F));
600
- vt_7c(disp, 1, 1, 0);
 
601
  let vt_5c: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
602
  core::mem::transmute(*vt.add(0x17));
603
- vt_5c(disp, -100, 1);
604
 
605
  // Palette slot range init
606
  let palette_range_ptr = *(disp.add(0x3120) as *const *mut i16);
@@ -617,7 +622,6 @@ unsafe fn init_graphics_and_resources(
617
  }
618
  }
619
 
620
- let _ = log_line("[DDGame] past palette");
621
  // ── FUN_00570E20: usercall(ESI=wrapper), plain RET ──
622
  call_usercall_esi(wrapper, FUN_570E20_ADDR);
623
 
@@ -681,11 +685,12 @@ unsafe fn init_graphics_and_resources(
681
  (*wrapper)._field_4e0 = 0xFFFFFF9C; // -100
682
  (*wrapper).speech_name_count = 0;
683
 
684
- let _ = log_line("[DDGame] past 570e20+display+colors+gfxdir+fields");
685
  // ── Audio init (non-headless + sound available) ──
 
686
  if !is_headless {
687
  // FUN_570F30: usercall(ESI=wrapper)
688
  call_usercall_esi(wrapper, FUN_570F30_ADDR);
 
689
 
690
  if !(*ddgame).sound.is_null() {
691
  // DSSound_LoadEffectWAVs: stdcall(wrapper)
@@ -694,6 +699,7 @@ unsafe fn init_graphics_and_resources(
694
  f(wrapper);
695
  // DSSound_LoadAllSpeechBanks: usercall(ESI=wrapper)
696
  call_usercall_esi(wrapper, LOAD_SPEECH_BANKS_ADDR);
 
697
  // Allocate ActiveSoundTable (0x608 bytes)
698
  let ast = wa_malloc(0x608) as *mut ActiveSoundTable;
699
  (*ast).ddgame = ddgame;
@@ -703,13 +709,23 @@ unsafe fn init_graphics_and_resources(
703
  }
704
  }
705
 
 
 
 
 
 
 
706
  // ── GfxResource: thiscall(ECX=gfx_handler) + EAX=name + 1 stack(output), RET 0x4 ──
707
  let gfx_resource: *mut u8;
708
  {
709
  let gfx_handler = (*wrapper)._field_4c0;
710
  let out_buf = wa_malloc(0x900);
711
  core::ptr::write_bytes(out_buf, 0, 0x900);
 
 
712
  gfx_resource = call_gfx_resource_create(gfx_handler, rb(0x66A3C0) as *const u8, out_buf);
 
 
713
  }
714
 
715
  // ── PCLandscape (alloc 0xB44, stdcall 11 params) ──
@@ -933,7 +949,6 @@ unsafe fn init_graphics_and_resources(
933
  // This pumps the message loop — commented out to test if it causes crashes.
934
  // call_usercall_ecx(wrapper, LOAD_WEAPON_SPRITES_ADDR);
935
  // call_usercall_ecx(wrapper, LOAD_WEAPON_SPRITES_ADDR);
936
- let _ = log_line("[DDGame] past audio init");
937
 
938
  // ── Sprite resource loading via DDGameWrapper vtable[0] ──
939
  // DDNetGameWrapper__LoadResourceList: thiscall(ECX=wrapper) +
@@ -950,14 +965,12 @@ unsafe fn init_graphics_and_resources(
950
  ) = core::mem::transmute(*wrapper_vt);
951
 
952
  // Load resources for layer 1 (main sprites)
953
- let _ = log_line("[DDGame] M2: load_resource_list #1");
954
  load_resource_list(
955
  wrapper, 1, gfx_handler,
956
  rb(0x643F2B) as *const u8, // base path
957
  rb(0x6AD2C0) as *const u8, // resource table
958
  0x1D88, // table size
959
  );
960
- let _ = log_line("[DDGame] M3: load_resource_list #1 done");
961
 
962
  // Set global flag based on game version
963
  let d778 = *((*ddgame).game_info as *const u8).add(0xD778).cast::<i32>();
@@ -1093,8 +1106,6 @@ unsafe fn init_graphics_and_resources(
1093
  set_vis(disp_obj, 2, 0);
1094
  set_vis(disp_obj, 3, 1);
1095
  }
1096
-
1097
- let _ = log_line("[DDGame] create_ddgame complete");
1098
  }
1099
 
1100
  /// Helper: find entry in GfxDir and load image, or load directly.
 
427
  game_info: *mut GameInfo,
428
  network_ecx: u32, // implicit ECX from caller
429
  ) -> *mut DDGame {
 
 
430
  // ── 1. Allocate and zero-fill (matches: memset(piVar3, 0, 0x98B8)) ──
431
  let ddgame = wa_malloc(0x98B8) as *mut DDGame;
432
  if ddgame.is_null() {
 
455
  // Now safe to expose — all fields that concurrent readers check are set.
456
  (*wrapper).ddgame = ddgame;
457
 
458
+ // Verify game_info is set
459
+ let _ = crate::log::log_line(&format!(
460
+ "[DDGame] exposed: ddgame=0x{:08X}, game_info=0x{:08X}, ddgame+0x24=0x{:08X}",
461
+ ddgame as u32,
462
+ game_info as u32,
463
+ (*ddgame).game_info as u32,
464
+ ));
465
+
466
  // ── 5. Set g_GameInfo global ──
467
  *(rb(va::G_GAME_INFO) as *mut *mut GameInfo) = game_info;
468
 
 
495
  // ── 10. GfxHandler, landscape, sprites, audio, resources ──
496
  init_graphics_and_resources(wrapper, game_info, net_game, display, is_headless);
497
 
498
+ let _ = crate::log::log_line("[DDGame] create_ddgame complete");
 
 
 
 
499
  ddgame
500
  }
501
 
 
583
  }
584
  }
585
 
 
586
  // ── Display palette setup (non-headless) ──
587
  if !is_headless {
588
  if *(rb(0x88E485) as *const u8) == 0 {
 
592
  // BUT: FUN_00570A90 or something else may write to wrapper in ways we
593
  // don't expect. Read via struct field instead of raw offset.
594
  let disp = (*wrapper).display as *mut u8;
595
+ let gfx_handler = (*wrapper)._field_4c0;
596
  let vt = *(disp as *const *const u32);
597
+ // vtable[4] (offset 0x10): thiscall + 2 stack params (1, 0xFE), RET 0x8
598
+ let vt_10: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
599
  core::mem::transmute(*vt.add(4));
600
+ vt_10(disp, 1, 0xFE);
601
+ // vtable[0x1F] (offset 0x7C): thiscall + 5 stack params, RET 0x14
602
+ let vt_7c: unsafe extern "thiscall" fn(*mut u8, i32, i32, i32, *mut u8, u32) =
603
  core::mem::transmute(*vt.add(0x1F));
604
+ vt_7c(disp, 1, 1, 0, gfx_handler, rb(0x66A3A8));
605
+ // vtable[0x17] (offset 0x5C): thiscall + 2 stack params, RET 0x8
606
  let vt_5c: unsafe extern "thiscall" fn(*mut u8, i32, i32) =
607
  core::mem::transmute(*vt.add(0x17));
608
+ vt_5c(disp, 1, -100);
609
 
610
  // Palette slot range init
611
  let palette_range_ptr = *(disp.add(0x3120) as *const *mut i16);
 
622
  }
623
  }
624
 
 
625
  // ── FUN_00570E20: usercall(ESI=wrapper), plain RET ──
626
  call_usercall_esi(wrapper, FUN_570E20_ADDR);
627
 
 
685
  (*wrapper)._field_4e0 = 0xFFFFFF9C; // -100
686
  (*wrapper).speech_name_count = 0;
687
 
 
688
  // ── Audio init (non-headless + sound available) ──
689
+ let _ = crate::log::log_line("[DDGame] audio: start");
690
  if !is_headless {
691
  // FUN_570F30: usercall(ESI=wrapper)
692
  call_usercall_esi(wrapper, FUN_570F30_ADDR);
693
+ let _ = crate::log::log_line("[DDGame] audio: past 570F30");
694
 
695
  if !(*ddgame).sound.is_null() {
696
  // DSSound_LoadEffectWAVs: stdcall(wrapper)
 
699
  f(wrapper);
700
  // DSSound_LoadAllSpeechBanks: usercall(ESI=wrapper)
701
  call_usercall_esi(wrapper, LOAD_SPEECH_BANKS_ADDR);
702
+ let _ = crate::log::log_line("[DDGame] audio: done");
703
  // Allocate ActiveSoundTable (0x608 bytes)
704
  let ast = wa_malloc(0x608) as *mut ActiveSoundTable;
705
  (*ast).ddgame = ddgame;
 
709
  }
710
  }
711
 
712
+ let _ = crate::log::log_line("[DDGame] past ActiveSoundTable");
713
+
714
+ // ── Loading tick: usercall(ECX=wrapper) ──
715
+ call_usercall_ecx(wrapper, LOAD_WEAPON_SPRITES_ADDR);
716
+ call_usercall_ecx(wrapper, LOAD_WEAPON_SPRITES_ADDR);
717
+
718
  // ── GfxResource: thiscall(ECX=gfx_handler) + EAX=name + 1 stack(output), RET 0x4 ──
719
  let gfx_resource: *mut u8;
720
  {
721
  let gfx_handler = (*wrapper)._field_4c0;
722
  let out_buf = wa_malloc(0x900);
723
  core::ptr::write_bytes(out_buf, 0, 0x900);
724
+ let _ = crate::log::log_line(&format!(
725
+ "[DDGame] calling GfxResource: handler=0x{:08X}", gfx_handler as u32));
726
  gfx_resource = call_gfx_resource_create(gfx_handler, rb(0x66A3C0) as *const u8, out_buf);
727
+ let _ = crate::log::log_line(&format!(
728
+ "[DDGame] GfxResource returned: 0x{:08X}", gfx_resource as u32));
729
  }
730
 
731
  // ── PCLandscape (alloc 0xB44, stdcall 11 params) ──
 
949
  // This pumps the message loop — commented out to test if it causes crashes.
950
  // call_usercall_ecx(wrapper, LOAD_WEAPON_SPRITES_ADDR);
951
  // call_usercall_ecx(wrapper, LOAD_WEAPON_SPRITES_ADDR);
 
952
 
953
  // ── Sprite resource loading via DDGameWrapper vtable[0] ──
954
  // DDNetGameWrapper__LoadResourceList: thiscall(ECX=wrapper) +
 
965
  ) = core::mem::transmute(*wrapper_vt);
966
 
967
  // Load resources for layer 1 (main sprites)
 
968
  load_resource_list(
969
  wrapper, 1, gfx_handler,
970
  rb(0x643F2B) as *const u8, // base path
971
  rb(0x6AD2C0) as *const u8, // resource table
972
  0x1D88, // table size
973
  );
 
974
 
975
  // Set global flag based on game version
976
  let d778 = *((*ddgame).game_info as *const u8).add(0xD778).cast::<i32>();
 
1106
  set_vis(disp_obj, 2, 0);
1107
  set_vis(disp_obj, 3, 1);
1108
  }
 
 
1109
  }
1110
 
1111
  /// Helper: find entry in GfxDir and load image, or load directly.