fix: preserve ECX in usercall trampoline for thiscall MapColor hook
Browse filesWA's ProcessSprite loops calling PaletteContext__MapColor without
re-setting ECX between iterations, relying on the original function
preserving ECX (it only reads [ECX+offset]). Our trampoline's cdecl
call clobbered ECX, causing garbage reads on 2nd+ iteration and
rendering all sprites/text white.
Add a `preserve_ecx` variant to usercall_trampoline! that saves and
restores ECX across the cdecl impl call.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
crates/openwa-wormkit/src/hook.rs
CHANGED
|
@@ -57,6 +57,29 @@ macro_rules! usercall_trampoline {
|
|
| 57 |
}
|
| 58 |
};
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
// 1 register arg, 3 stack params, ret N
|
| 61 |
(fn $name:ident; impl_fn = $impl:path; reg = $reg:ident;
|
| 62 |
stack_params = 3; ret_bytes = $ret:literal) => {
|
|
|
|
| 57 |
}
|
| 58 |
};
|
| 59 |
|
| 60 |
+
// 1 register arg (ECX), 1 stack param, ret N — preserves ECX across call.
|
| 61 |
+
// Callers of thiscall functions may rely on ECX being preserved even though
|
| 62 |
+
// the ABI says it's caller-saved (MSVC often generates loops that don't
|
| 63 |
+
// re-set ECX between calls if the callee happens to preserve it).
|
| 64 |
+
(fn $name:ident; impl_fn = $impl:path; reg = ecx;
|
| 65 |
+
stack_params = 1; ret_bytes = $ret:literal; preserve_ecx) => {
|
| 66 |
+
#[unsafe(naked)]
|
| 67 |
+
unsafe extern "C" fn $name() {
|
| 68 |
+
core::arch::naked_asm!(
|
| 69 |
+
"push edx",
|
| 70 |
+
"push ecx", // save ECX for restoration
|
| 71 |
+
"push [esp+12]", // stack param (shifted +4 by extra push)
|
| 72 |
+
"push ecx", // cdecl arg (ECX still valid here)
|
| 73 |
+
"call {impl_fn}",
|
| 74 |
+
"add esp, 8",
|
| 75 |
+
"pop ecx", // restore ECX
|
| 76 |
+
"pop edx",
|
| 77 |
+
concat!("ret ", $ret),
|
| 78 |
+
impl_fn = sym $impl,
|
| 79 |
+
);
|
| 80 |
+
}
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
// 1 register arg, 3 stack params, ret N
|
| 84 |
(fn $name:ident; impl_fn = $impl:path; reg = $reg:ident;
|
| 85 |
stack_params = 3; ret_bytes = $ret:literal) => {
|
crates/openwa-wormkit/src/replacements/sprite.rs
CHANGED
|
@@ -48,7 +48,7 @@ usercall_trampoline!(fn trampoline_process_sprite; impl_fn = process_sprite_impl
|
|
| 48 |
// ---------------------------------------------------------------------------
|
| 49 |
|
| 50 |
usercall_trampoline!(fn trampoline_palette_map_color; impl_fn = palette_map_color_impl;
|
| 51 |
-
reg = ecx; stack_params = 1; ret_bytes = "0x4");
|
| 52 |
|
| 53 |
unsafe extern "cdecl" fn palette_map_color_impl(palette_ctx: u32, rgb: u32) -> u32 {
|
| 54 |
let ctx = palette_ctx as *mut openwa_core::render::palette::PaletteContext;
|
|
|
|
| 48 |
// ---------------------------------------------------------------------------
|
| 49 |
|
| 50 |
usercall_trampoline!(fn trampoline_palette_map_color; impl_fn = palette_map_color_impl;
|
| 51 |
+
reg = ecx; stack_params = 1; ret_bytes = "0x4"; preserve_ecx);
|
| 52 |
|
| 53 |
unsafe extern "cdecl" fn palette_map_color_impl(palette_ctx: u32, rgb: u32) -> u32 {
|
| 54 |
let ctx = palette_ctx as *mut openwa_core::render::palette::PaletteContext;
|