feat: port PaletteContext__MapColor to Rust with global hook
Browse filesPure Rust reimplementation of the palette color mapping function
(0x5412B0). All 10 callers now use the Rust version via MinHook detour.
Uses raw pointer ops to match WA's cache overflow behavior (cache_count
can reach 256, writing past the cache array into the dirty field) and
to avoid noalias miscompilation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
crates/openwa-core/src/render/mod.rs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
pub mod gfx_dir;
|
| 2 |
pub mod landscape;
|
|
|
|
| 3 |
pub mod queue;
|
| 4 |
pub mod spr;
|
| 5 |
pub mod sprite;
|
|
|
|
| 1 |
pub mod gfx_dir;
|
| 2 |
pub mod landscape;
|
| 3 |
+
pub mod palette;
|
| 4 |
pub mod queue;
|
| 5 |
pub mod spr;
|
| 6 |
pub mod sprite;
|
crates/openwa-core/src/render/palette.rs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Palette context — maps RGB colors to display palette indices.
|
| 2 |
+
//!
|
| 3 |
+
//! WA's `PaletteContext` maintains a 256-entry palette with a free-slot
|
| 4 |
+
//! allocator and a recently-mapped cache for fast repeated lookups.
|
| 5 |
+
|
| 6 |
+
/// PaletteContext struct layout.
|
| 7 |
+
///
|
| 8 |
+
/// Used by `PaletteContext__MapColor` (0x5412B0) and related functions.
|
| 9 |
+
/// May be embedded in a larger composite object (e.g. GfxDir + palette data).
|
| 10 |
+
#[repr(C)]
|
| 11 |
+
pub struct PaletteContext {
|
| 12 |
+
/// +0x00: Unknown (possibly vtable or parent object field).
|
| 13 |
+
pub _unknown_00: u32,
|
| 14 |
+
/// +0x04: RGB values stored per palette index (24-bit RGB in low 3 bytes).
|
| 15 |
+
pub rgb_table: [u32; 256],
|
| 16 |
+
/// +0x404: In-use flag per palette index (1 = allocated).
|
| 17 |
+
pub in_use: [u8; 256],
|
| 18 |
+
/// +0x504: Number of free slots remaining in the free stack.
|
| 19 |
+
pub free_count: i16,
|
| 20 |
+
/// +0x506: Free slot index stack (LIFO). Entries are palette indices.
|
| 21 |
+
pub free_stack: [u8; 256],
|
| 22 |
+
/// +0x606: Number of entries in the recently-mapped cache.
|
| 23 |
+
pub cache_count: i16,
|
| 24 |
+
/// +0x608: Recently-mapped palette indices (cache for fast lookup).
|
| 25 |
+
pub cache: [u8; 256],
|
| 26 |
+
/// +0x708: Dirty flag (set to 1 when a new slot is allocated).
|
| 27 |
+
pub dirty: u16,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const _: () = assert!(core::mem::offset_of!(PaletteContext, rgb_table) == 0x04);
|
| 31 |
+
const _: () = assert!(core::mem::offset_of!(PaletteContext, in_use) == 0x404);
|
| 32 |
+
const _: () = assert!(core::mem::offset_of!(PaletteContext, free_count) == 0x504);
|
| 33 |
+
const _: () = assert!(core::mem::offset_of!(PaletteContext, free_stack) == 0x506);
|
| 34 |
+
const _: () = assert!(core::mem::offset_of!(PaletteContext, cache_count) == 0x606);
|
| 35 |
+
const _: () = assert!(core::mem::offset_of!(PaletteContext, cache) == 0x608);
|
| 36 |
+
const _: () = assert!(core::mem::offset_of!(PaletteContext, dirty) == 0x708);
|
| 37 |
+
|
| 38 |
+
/// Map an RGB color to the nearest display palette index.
|
| 39 |
+
///
|
| 40 |
+
/// Rust port of `PaletteContext__MapColor` (0x5412B0). Operates on a raw
|
| 41 |
+
/// pointer to WA's PaletteContext struct.
|
| 42 |
+
///
|
| 43 |
+
/// Algorithm:
|
| 44 |
+
/// 1. Search the recently-mapped cache for an exact 24-bit RGB match
|
| 45 |
+
/// 2. On miss: pop a free slot, store the RGB, add to cache, return new index
|
| 46 |
+
/// 3. If no free slots: return 0xFFFFFFFF
|
| 47 |
+
///
|
| 48 |
+
/// # Safety
|
| 49 |
+
///
|
| 50 |
+
/// `ctx` must point to a valid WA PaletteContext struct.
|
| 51 |
+
pub unsafe fn palette_map_color(ctx: *mut PaletteContext, rgb: u32) -> u32 {
|
| 52 |
+
// Raw pointer ops throughout — no &mut to avoid noalias miscompilation,
|
| 53 |
+
// and to match WA's overflow behavior when cache_count reaches 256
|
| 54 |
+
// (writes past cache[] into dirty field, which is then set to 1 anyway).
|
| 55 |
+
let p = ctx as *mut u8;
|
| 56 |
+
|
| 57 |
+
let cache_count = *(p.add(0x606) as *const i16);
|
| 58 |
+
|
| 59 |
+
// Search cache for matching RGB (24-bit compare)
|
| 60 |
+
for i in 0..cache_count as usize {
|
| 61 |
+
let idx = *p.add(0x608 + i) as usize;
|
| 62 |
+
if idx != 0 && (*(p.add(0x04 + idx * 4) as *const u32) ^ rgb) & 0xFF_FFFF == 0 {
|
| 63 |
+
return idx as u32;
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Cache miss — allocate a free slot
|
| 68 |
+
let free_count = *(p.add(0x504) as *const i16);
|
| 69 |
+
if free_count <= 0 {
|
| 70 |
+
return 0xFFFF_FFFF;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
let new_free_count = free_count - 1;
|
| 74 |
+
*(p.add(0x504) as *mut i16) = new_free_count;
|
| 75 |
+
let slot = *p.add(0x506 + new_free_count as usize);
|
| 76 |
+
let slot_idx = slot as usize;
|
| 77 |
+
|
| 78 |
+
// Add to cache (may overflow into dirty field at +0x708 when cache is full —
|
| 79 |
+
// matches WA behavior, and dirty is set to 1 immediately after anyway)
|
| 80 |
+
*p.add(0x608 + cache_count as usize) = slot;
|
| 81 |
+
*(p.add(0x606) as *mut i16) = cache_count + 1;
|
| 82 |
+
|
| 83 |
+
// Store RGB and mark in-use
|
| 84 |
+
*(p.add(0x04 + slot_idx * 4) as *mut u32) = rgb;
|
| 85 |
+
*p.add(0x404 + slot_idx) = 1;
|
| 86 |
+
*(p.add(0x708) as *mut u16) = 1;
|
| 87 |
+
|
| 88 |
+
slot_idx as u32
|
| 89 |
+
}
|
crates/openwa-wormkit/src/replacements/sprite.rs
CHANGED
|
@@ -42,14 +42,17 @@ unsafe extern "cdecl" fn construct_sprite_impl(sprite: u32, context: u32) {
|
|
| 42 |
usercall_trampoline!(fn trampoline_process_sprite; impl_fn = process_sprite_impl;
|
| 43 |
regs = [eax, ecx]; stack_params = 1; ret_bytes = "0x4");
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
//
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
unsafe extern "cdecl" fn process_sprite_impl(sprite: u32, palette_ctx: u32, raw_data: u32) -> u32 {
|
|
@@ -100,11 +103,12 @@ unsafe extern "cdecl" fn process_sprite_impl(sprite: u32, palette_ctx: u32, raw_
|
|
| 100 |
|
| 101 |
// Map each RGB entry to display palette index
|
| 102 |
let rgb_start = data_ptr.add(hdr.palette_offset);
|
|
|
|
| 103 |
for i in 0..hdr.palette_count as usize {
|
| 104 |
// Read 4 bytes (3 RGB + 1 from next entry, matching WA behavior)
|
| 105 |
let rgb_val = *(rgb_start.add(i * 3) as *const u32);
|
| 106 |
-
let mapped = palette_map_color(
|
| 107 |
-
*palette_base.add(1 + i) = mapped;
|
| 108 |
}
|
| 109 |
|
| 110 |
// --- Secondary frame table (if header_flags & 0x4000) ---
|
|
@@ -188,8 +192,6 @@ unsafe extern "cdecl" fn process_sprite_impl(sprite: u32, palette_ctx: u32, raw_
|
|
| 188 |
|
| 189 |
pub fn install() -> Result<(), String> {
|
| 190 |
unsafe {
|
| 191 |
-
PALETTE_MAP_COLOR_ADDR = rb(va::PALETTE_CONTEXT_MAP_COLOR);
|
| 192 |
-
|
| 193 |
let _ = hook::install(
|
| 194 |
"ConstructSprite",
|
| 195 |
va::CONSTRUCT_SPRITE,
|
|
@@ -201,6 +203,12 @@ pub fn install() -> Result<(), String> {
|
|
| 201 |
va::PROCESS_SPRITE,
|
| 202 |
trampoline_process_sprite as *const (),
|
| 203 |
)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
}
|
| 205 |
Ok(())
|
| 206 |
}
|
|
|
|
| 42 |
usercall_trampoline!(fn trampoline_process_sprite; impl_fn = process_sprite_impl;
|
| 43 |
regs = [eax, ecx]; stack_params = 1; ret_bytes = "0x4");
|
| 44 |
|
| 45 |
+
// ---------------------------------------------------------------------------
|
| 46 |
+
// PaletteContext__MapColor (0x5412B0)
|
| 47 |
+
// thiscall(ECX=palette_ctx, stack=rgb_u32), RET 0x4
|
| 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;
|
| 55 |
+
openwa_core::render::palette::palette_map_color(ctx, rgb)
|
| 56 |
}
|
| 57 |
|
| 58 |
unsafe extern "cdecl" fn process_sprite_impl(sprite: u32, palette_ctx: u32, raw_data: u32) -> u32 {
|
|
|
|
| 103 |
|
| 104 |
// Map each RGB entry to display palette index
|
| 105 |
let rgb_start = data_ptr.add(hdr.palette_offset);
|
| 106 |
+
let ctx = palette_ctx as *mut openwa_core::render::palette::PaletteContext;
|
| 107 |
for i in 0..hdr.palette_count as usize {
|
| 108 |
// Read 4 bytes (3 RGB + 1 from next entry, matching WA behavior)
|
| 109 |
let rgb_val = *(rgb_start.add(i * 3) as *const u32);
|
| 110 |
+
let mapped = openwa_core::render::palette::palette_map_color(ctx, rgb_val);
|
| 111 |
+
*palette_base.add(1 + i) = mapped as u8;
|
| 112 |
}
|
| 113 |
|
| 114 |
// --- Secondary frame table (if header_flags & 0x4000) ---
|
|
|
|
| 192 |
|
| 193 |
pub fn install() -> Result<(), String> {
|
| 194 |
unsafe {
|
|
|
|
|
|
|
| 195 |
let _ = hook::install(
|
| 196 |
"ConstructSprite",
|
| 197 |
va::CONSTRUCT_SPRITE,
|
|
|
|
| 203 |
va::PROCESS_SPRITE,
|
| 204 |
trampoline_process_sprite as *const (),
|
| 205 |
)?;
|
| 206 |
+
|
| 207 |
+
let _ = hook::install(
|
| 208 |
+
"PaletteContext__MapColor",
|
| 209 |
+
va::PALETTE_CONTEXT_MAP_COLOR,
|
| 210 |
+
trampoline_palette_map_color as *const (),
|
| 211 |
+
)?;
|
| 212 |
}
|
| 213 |
Ok(())
|
| 214 |
}
|