fix: replace all inline asm push+call with naked bridges
Browse filesRoot cause of GfxDir crashes: inline asm `push {param}; call {fn}`
with `clobber_abi("C")` breaks the compiler's stack tracking when
the callee does `RET N` (cleans the pushed param). The compiler
doesn't know the stack was adjusted, corrupting subsequent accesses.
Fixed by creating naked bridge functions:
- call_gfx_find_entry: usercall(EAX=name) + 1 stack, RET 0x4
- call_gfx_load_image: usercall(ESI=handler) + 1 stack, RET 0x4
- Rewrote call_gfx_load_and_wrap and call_gfx_find_and_load to use
these bridges instead of inline asm
Also fixed GfxDir__LoadImage convention: ESI=gfx_handler (not EAX).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/engine/ddgame.rs
CHANGED
|
@@ -374,6 +374,8 @@ pub fn init_constructor_addrs() {
|
|
| 374 |
FUN_570E20_ADDR = rb(va::FUN_570E20);
|
| 375 |
FUN_570A90_ADDR = rb(va::FUN_570A90);
|
| 376 |
GFX_RESOURCE_CREATE_ADDR = rb(va::GFX_RESOURCE_CREATE);
|
|
|
|
|
|
|
| 377 |
FUN_570F30_ADDR = rb(va::DDGAME_INIT_SOUND_PATHS);
|
| 378 |
LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS);
|
| 379 |
LOAD_WEAPON_SPRITES_ADDR = rb(va::DDGAME_LOAD_WEAPON_SPRITES);
|
|
@@ -861,10 +863,8 @@ unsafe fn init_graphics_and_resources(
|
|
| 861 |
release(gfx_resource);
|
| 862 |
}
|
| 863 |
|
| 864 |
-
//
|
| 865 |
-
|
| 866 |
-
let gfx_load_image = rb(va::GFX_DIR_LOAD_IMAGE) as u32;
|
| 867 |
-
let fun_4f5f80 = rb(0x4F5F80) as u32;
|
| 868 |
|
| 869 |
// ── Arrow sprites + collision regions (32 iterations) ──
|
| 870 |
{
|
|
@@ -884,18 +884,8 @@ unsafe fn init_graphics_and_resources(
|
|
| 884 |
set_layer((*ddgame).display, 1);
|
| 885 |
}
|
| 886 |
|
| 887 |
-
// GfxDir__FindEntry: usercall(EAX=name) +
|
| 888 |
-
let entry
|
| 889 |
-
core::arch::asm!(
|
| 890 |
-
"push {handler}",
|
| 891 |
-
"call {find}",
|
| 892 |
-
handler = in(reg) gfx_handler,
|
| 893 |
-
find = in(reg) gfx_find_entry,
|
| 894 |
-
inlateout("eax") name_buf.as_ptr() => entry,
|
| 895 |
-
out("ecx") _,
|
| 896 |
-
out("edx") _,
|
| 897 |
-
clobber_abi("C"),
|
| 898 |
-
);
|
| 899 |
|
| 900 |
let sprite: *mut u8;
|
| 901 |
if !entry.is_null() {
|
|
@@ -907,14 +897,13 @@ unsafe fn init_graphics_and_resources(
|
|
| 907 |
if !cached.is_null() {
|
| 908 |
sprite = cached;
|
| 909 |
} else {
|
| 910 |
-
// Fallback to GfxDir__LoadImage
|
| 911 |
sprite = call_gfx_load_and_wrap(
|
| 912 |
-
name_buf.as_ptr(),
|
| 913 |
);
|
| 914 |
}
|
| 915 |
} else {
|
| 916 |
sprite = call_gfx_load_and_wrap(
|
| 917 |
-
name_buf.as_ptr(),
|
| 918 |
);
|
| 919 |
}
|
| 920 |
|
|
@@ -1062,9 +1051,7 @@ unsafe fn init_graphics_and_resources(
|
|
| 1062 |
if s_var1 < 0x61 && level_height == 0x2B8 {
|
| 1063 |
// Simple gradient: load gradient.img directly
|
| 1064 |
let gradient = call_gfx_find_and_load(
|
| 1065 |
-
land_layer, b"gradient.img\0".as_ptr(),
|
| 1066 |
-
gfx_find_entry, gfx_load_image, fun_4f5f80,
|
| 1067 |
-
layer3_ctx,
|
| 1068 |
);
|
| 1069 |
(*ddgame).gradient_image = gradient;
|
| 1070 |
} else {
|
|
@@ -1078,9 +1065,7 @@ unsafe fn init_graphics_and_resources(
|
|
| 1078 |
{
|
| 1079 |
let layer2_ctx = set_layer((*ddgame).display, 2);
|
| 1080 |
let fill_sprite = call_gfx_find_and_load(
|
| 1081 |
-
land_layer, b"fill.img\0".as_ptr(),
|
| 1082 |
-
gfx_find_entry, gfx_load_image, fun_4f5f80,
|
| 1083 |
-
layer2_ctx,
|
| 1084 |
);
|
| 1085 |
if !fill_sprite.is_null() {
|
| 1086 |
// Get pixel value: fill_sprite->vtable[4](0, 0)
|
|
@@ -1144,23 +1129,9 @@ unsafe fn init_graphics_and_resources(
|
|
| 1144 |
unsafe fn call_gfx_find_and_load(
|
| 1145 |
gfx_dir: *mut u8,
|
| 1146 |
name: *const u8,
|
| 1147 |
-
find_entry_addr: u32,
|
| 1148 |
-
load_image_addr: u32,
|
| 1149 |
-
wrap_addr: u32,
|
| 1150 |
display_ctx: *mut u8,
|
| 1151 |
) -> *mut u8 {
|
| 1152 |
-
|
| 1153 |
-
let entry: *mut u8;
|
| 1154 |
-
core::arch::asm!(
|
| 1155 |
-
"push {dir}",
|
| 1156 |
-
"call {find}",
|
| 1157 |
-
dir = in(reg) gfx_dir,
|
| 1158 |
-
find = in(reg) find_entry_addr,
|
| 1159 |
-
inlateout("eax") name => entry,
|
| 1160 |
-
out("ecx") _,
|
| 1161 |
-
out("edx") _,
|
| 1162 |
-
clobber_abi("C"),
|
| 1163 |
-
);
|
| 1164 |
|
| 1165 |
if !entry.is_null() {
|
| 1166 |
// Try cached load: gfx_dir->vtable[2](entry->field_4)
|
|
@@ -1169,24 +1140,16 @@ unsafe fn call_gfx_find_and_load(
|
|
| 1169 |
core::mem::transmute(*dir_vt.add(2));
|
| 1170 |
let cached = load_cached(gfx_dir, *(entry.add(4) as *const u32));
|
| 1171 |
if !cached.is_null() {
|
| 1172 |
-
// Wrap with
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
cached = in(reg) cached,
|
| 1178 |
-
ctor = in(reg) rb(0x4F5E80) as u32, // DisplayGfx__Constructor_Maybe
|
| 1179 |
-
out("eax") result,
|
| 1180 |
-
out("ecx") _,
|
| 1181 |
-
out("edx") _,
|
| 1182 |
-
clobber_abi("C"),
|
| 1183 |
-
);
|
| 1184 |
-
return result;
|
| 1185 |
}
|
| 1186 |
}
|
| 1187 |
|
| 1188 |
// Fallback: load image directly
|
| 1189 |
-
call_gfx_load_and_wrap(
|
| 1190 |
}
|
| 1191 |
|
| 1192 |
// Statics for usercall bridge addresses
|
|
@@ -1253,6 +1216,25 @@ unsafe extern "C" fn call_gfx_resource_create(
|
|
| 1253 |
);
|
| 1254 |
}
|
| 1255 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1256 |
/// Bridge to GfxHandler__LoadDir (0x5663E0).
|
| 1257 |
/// Convention: usercall(EAX=handler), plain RET. Returns nonzero on success.
|
| 1258 |
#[cfg(target_arch = "x86")]
|
|
@@ -1269,45 +1251,41 @@ unsafe fn call_gfx_load_dir(handler: *mut u8, addr: u32) -> i32 {
|
|
| 1269 |
result
|
| 1270 |
}
|
| 1271 |
|
| 1272 |
-
//
|
| 1273 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1274 |
#[cfg(target_arch = "x86")]
|
| 1275 |
unsafe fn call_gfx_load_and_wrap(
|
|
|
|
| 1276 |
name: *const u8,
|
| 1277 |
-
|
| 1278 |
-
fun_4f5f80: u32,
|
| 1279 |
) -> *mut u8 {
|
| 1280 |
-
|
| 1281 |
-
let image: *mut u8;
|
| 1282 |
-
core::arch::asm!(
|
| 1283 |
-
"push {name}",
|
| 1284 |
-
"call {load}",
|
| 1285 |
-
name = in(reg) name,
|
| 1286 |
-
load = in(reg) gfx_load_image_addr,
|
| 1287 |
-
out("eax") image,
|
| 1288 |
-
out("ecx") _,
|
| 1289 |
-
out("edx") _,
|
| 1290 |
-
clobber_abi("C"),
|
| 1291 |
-
);
|
| 1292 |
if image.is_null() {
|
| 1293 |
return core::ptr::null_mut();
|
| 1294 |
}
|
| 1295 |
-
// FUN_004F5F80(
|
| 1296 |
-
let
|
| 1297 |
-
|
| 1298 |
-
|
| 1299 |
-
"push {img}",
|
| 1300 |
-
"push {ctx}",
|
| 1301 |
-
"call {wrap}",
|
| 1302 |
-
"add esp, 12",
|
| 1303 |
-
img = in(reg) image,
|
| 1304 |
-
ctx = in(reg) 0u32, // display_gfx context, often 0
|
| 1305 |
-
wrap = in(reg) fun_4f5f80,
|
| 1306 |
-
out("eax") result,
|
| 1307 |
-
out("ecx") _,
|
| 1308 |
-
out("edx") _,
|
| 1309 |
-
clobber_abi("C"),
|
| 1310 |
-
);
|
| 1311 |
// Release the raw image
|
| 1312 |
let image_vt = *(image as *const *const u32);
|
| 1313 |
let destroy: unsafe extern "thiscall" fn(*mut u8, u32) =
|
|
|
|
| 374 |
FUN_570E20_ADDR = rb(va::FUN_570E20);
|
| 375 |
FUN_570A90_ADDR = rb(va::FUN_570A90);
|
| 376 |
GFX_RESOURCE_CREATE_ADDR = rb(va::GFX_RESOURCE_CREATE);
|
| 377 |
+
GFX_FIND_ENTRY_ADDR = rb(va::GFX_DIR_FIND_ENTRY);
|
| 378 |
+
GFX_LOAD_IMAGE_ADDR = rb(va::GFX_DIR_LOAD_IMAGE);
|
| 379 |
FUN_570F30_ADDR = rb(va::DDGAME_INIT_SOUND_PATHS);
|
| 380 |
LOAD_SPEECH_BANKS_ADDR = rb(va::DSSOUND_LOAD_ALL_SPEECH_BANKS);
|
| 381 |
LOAD_WEAPON_SPRITES_ADDR = rb(va::DDGAME_LOAD_WEAPON_SPRITES);
|
|
|
|
| 863 |
release(gfx_resource);
|
| 864 |
}
|
| 865 |
|
| 866 |
+
// GfxDir addresses are resolved via statics (GFX_FIND_ENTRY_ADDR, GFX_LOAD_IMAGE_ADDR)
|
| 867 |
+
// and called through naked bridges (call_gfx_find_entry, call_gfx_load_image).
|
|
|
|
|
|
|
| 868 |
|
| 869 |
// ── Arrow sprites + collision regions (32 iterations) ──
|
| 870 |
{
|
|
|
|
| 884 |
set_layer((*ddgame).display, 1);
|
| 885 |
}
|
| 886 |
|
| 887 |
+
// GfxDir__FindEntry: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4
|
| 888 |
+
let entry = call_gfx_find_entry(name_buf.as_ptr(), gfx_handler);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 889 |
|
| 890 |
let sprite: *mut u8;
|
| 891 |
if !entry.is_null() {
|
|
|
|
| 897 |
if !cached.is_null() {
|
| 898 |
sprite = cached;
|
| 899 |
} else {
|
|
|
|
| 900 |
sprite = call_gfx_load_and_wrap(
|
| 901 |
+
gfx_handler, name_buf.as_ptr(), core::ptr::null_mut(),
|
| 902 |
);
|
| 903 |
}
|
| 904 |
} else {
|
| 905 |
sprite = call_gfx_load_and_wrap(
|
| 906 |
+
gfx_handler, name_buf.as_ptr(), core::ptr::null_mut(),
|
| 907 |
);
|
| 908 |
}
|
| 909 |
|
|
|
|
| 1051 |
if s_var1 < 0x61 && level_height == 0x2B8 {
|
| 1052 |
// Simple gradient: load gradient.img directly
|
| 1053 |
let gradient = call_gfx_find_and_load(
|
| 1054 |
+
land_layer, b"gradient.img\0".as_ptr(), layer3_ctx,
|
|
|
|
|
|
|
| 1055 |
);
|
| 1056 |
(*ddgame).gradient_image = gradient;
|
| 1057 |
} else {
|
|
|
|
| 1065 |
{
|
| 1066 |
let layer2_ctx = set_layer((*ddgame).display, 2);
|
| 1067 |
let fill_sprite = call_gfx_find_and_load(
|
| 1068 |
+
land_layer, b"fill.img\0".as_ptr(), layer2_ctx,
|
|
|
|
|
|
|
| 1069 |
);
|
| 1070 |
if !fill_sprite.is_null() {
|
| 1071 |
// Get pixel value: fill_sprite->vtable[4](0, 0)
|
|
|
|
| 1129 |
unsafe fn call_gfx_find_and_load(
|
| 1130 |
gfx_dir: *mut u8,
|
| 1131 |
name: *const u8,
|
|
|
|
|
|
|
|
|
|
| 1132 |
display_ctx: *mut u8,
|
| 1133 |
) -> *mut u8 {
|
| 1134 |
+
let entry = call_gfx_find_entry(name, gfx_dir);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
|
| 1136 |
if !entry.is_null() {
|
| 1137 |
// Try cached load: gfx_dir->vtable[2](entry->field_4)
|
|
|
|
| 1140 |
core::mem::transmute(*dir_vt.add(2));
|
| 1141 |
let cached = load_cached(gfx_dir, *(entry.add(4) as *const u32));
|
| 1142 |
if !cached.is_null() {
|
| 1143 |
+
// Wrap with DisplayGfx__Constructor_Maybe (0x4F5E80)
|
| 1144 |
+
// This is stdcall(1 param), RET 0x4
|
| 1145 |
+
let ctor: unsafe extern "stdcall" fn(*mut u8) -> *mut u8 =
|
| 1146 |
+
core::mem::transmute(rb(0x4F5E80) as usize);
|
| 1147 |
+
return ctor(cached);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1148 |
}
|
| 1149 |
}
|
| 1150 |
|
| 1151 |
// Fallback: load image directly
|
| 1152 |
+
call_gfx_load_and_wrap(gfx_dir, name, display_ctx)
|
| 1153 |
}
|
| 1154 |
|
| 1155 |
// Statics for usercall bridge addresses
|
|
|
|
| 1216 |
);
|
| 1217 |
}
|
| 1218 |
|
| 1219 |
+
/// Runtime address of GfxDir__FindEntry (set by init_constructor_addrs()).
|
| 1220 |
+
static mut GFX_FIND_ENTRY_ADDR: u32 = 0;
|
| 1221 |
+
|
| 1222 |
+
/// Bridge to GfxDir__FindEntry (0x566520).
|
| 1223 |
+
/// Convention: usercall(EAX=name) + 1 stack(gfx_handler), RET 0x4.
|
| 1224 |
+
/// Returns entry pointer or null.
|
| 1225 |
+
#[cfg(target_arch = "x86")]
|
| 1226 |
+
#[unsafe(naked)]
|
| 1227 |
+
unsafe extern "C" fn call_gfx_find_entry(_name: *const u8, _gfx_handler: *mut u8) -> *mut u8 {
|
| 1228 |
+
core::arch::naked_asm!(
|
| 1229 |
+
"movl 4(%esp), %eax", // EAX = name
|
| 1230 |
+
"pushl 8(%esp)", // push gfx_handler (callee cleans via RET 0x4)
|
| 1231 |
+
"calll *({addr})",
|
| 1232 |
+
"retl", // caller cleans our 2 cdecl params
|
| 1233 |
+
addr = sym GFX_FIND_ENTRY_ADDR,
|
| 1234 |
+
options(att_syntax),
|
| 1235 |
+
);
|
| 1236 |
+
}
|
| 1237 |
+
|
| 1238 |
/// Bridge to GfxHandler__LoadDir (0x5663E0).
|
| 1239 |
/// Convention: usercall(EAX=handler), plain RET. Returns nonzero on success.
|
| 1240 |
#[cfg(target_arch = "x86")]
|
|
|
|
| 1251 |
result
|
| 1252 |
}
|
| 1253 |
|
| 1254 |
+
// GfxDir__LoadImage is usercall(ESI=gfx_handler) + 1 stack(name), RET 0x4.
|
| 1255 |
+
// Returns raw image pointer or null.
|
| 1256 |
+
static mut GFX_LOAD_IMAGE_ADDR: u32 = 0;
|
| 1257 |
+
|
| 1258 |
+
#[cfg(target_arch = "x86")]
|
| 1259 |
+
#[unsafe(naked)]
|
| 1260 |
+
unsafe extern "C" fn call_gfx_load_image(_gfx_handler: *mut u8, _name: *const u8) -> *mut u8 {
|
| 1261 |
+
core::arch::naked_asm!(
|
| 1262 |
+
"pushl %esi",
|
| 1263 |
+
"movl 8(%esp), %esi", // ESI = gfx_handler
|
| 1264 |
+
"pushl 12(%esp)", // push name (callee cleans via RET 0x4)
|
| 1265 |
+
"calll *({addr})",
|
| 1266 |
+
"popl %esi",
|
| 1267 |
+
"retl",
|
| 1268 |
+
addr = sym GFX_LOAD_IMAGE_ADDR,
|
| 1269 |
+
options(att_syntax),
|
| 1270 |
+
);
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
/// Helper: load image via GfxDir__LoadImage + wrap as DisplayGfx.
|
| 1274 |
+
/// Used by arrow sprite loop when GfxDir__FindEntry returns null.
|
| 1275 |
#[cfg(target_arch = "x86")]
|
| 1276 |
unsafe fn call_gfx_load_and_wrap(
|
| 1277 |
+
gfx_handler: *mut u8,
|
| 1278 |
name: *const u8,
|
| 1279 |
+
display_ctx: *mut u8,
|
|
|
|
| 1280 |
) -> *mut u8 {
|
| 1281 |
+
let image = call_gfx_load_image(gfx_handler, name);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1282 |
if image.is_null() {
|
| 1283 |
return core::ptr::null_mut();
|
| 1284 |
}
|
| 1285 |
+
// FUN_004F5F80(display_ctx, image, 1) — cdecl, 3 params
|
| 1286 |
+
let f: unsafe extern "cdecl" fn(*mut u8, *mut u8, u32) -> *mut u8 =
|
| 1287 |
+
core::mem::transmute(rb(0x4F5F80) as usize);
|
| 1288 |
+
let result = f(display_ctx, image, 1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1289 |
// Release the raw image
|
| 1290 |
let image_vt = *(image as *const *const u32);
|
| 1291 |
let destroy: unsafe extern "thiscall" fn(*mut u8, u32) =
|