fix: correct display vtable call param counts — root cause of crash
Browse filesDisplay 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(
|
| 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 |
-
|
|
|
|
| 596 |
core::mem::transmute(*vt.add(4));
|
| 597 |
-
vt_10(disp);
|
| 598 |
-
|
|
|
|
| 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,
|
| 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.
|