refactor: move bitgrid blit hook and snapshots to own DLL module
Browse filesMove BitGrid__BlitSpriteRect hook, snapshot capture, and GIF helpers
from replacements/display.rs to replacements/bitgrid.rs. The blit
function is a core engine primitive (used for collision masks, not
just rendering), so it belongs in its own module alongside the
bitgrid type that moved to crate root in the previous commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-dll/src/replacements/bitgrid.rs
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! BitGrid blit hook and snapshot capture.
|
| 2 |
+
//!
|
| 3 |
+
//! Hooks BitGrid__BlitSpriteRect (0x4F6910), the core sprite/bitmap blitting
|
| 4 |
+
//! function used for both rendering and collision mask construction.
|
| 5 |
+
//! Rust handles 8bpp blend modes 0 (copy) and 1 (color table / transparency),
|
| 6 |
+
//! falling through to the original for unsupported modes.
|
| 7 |
+
|
| 8 |
+
use crate::log_line;
|
| 9 |
+
use core::sync::atomic::{AtomicU32, Ordering};
|
| 10 |
+
use openwa_core::address::va;
|
| 11 |
+
use openwa_core::rebase::rb;
|
| 12 |
+
|
| 13 |
+
// =========================================================================
|
| 14 |
+
// Blit snapshot capture
|
| 15 |
+
// =========================================================================
|
| 16 |
+
|
| 17 |
+
/// Capture sprite blit snapshots from WA's native BitGrid__BlitSpriteRect.
|
| 18 |
+
///
|
| 19 |
+
/// Creates source and destination BitGrids, loads test GIF images,
|
| 20 |
+
/// calls WA's blit with various orientations and blend modes,
|
| 21 |
+
/// saves pixel output to `testdata/snapshots/`. Activated by
|
| 22 |
+
/// `OPENWA_CAPTURE_BLIT_SNAPSHOTS=1`.
|
| 23 |
+
pub unsafe fn capture_blit_snapshots() {
|
| 24 |
+
use openwa_core::bitgrid::DisplayBitGrid;
|
| 25 |
+
use std::fs;
|
| 26 |
+
use std::io::Write;
|
| 27 |
+
|
| 28 |
+
let _ = log_line("[BitGrid] BLIT SNAPSHOT: Starting capture");
|
| 29 |
+
|
| 30 |
+
// Project root at compile time — WA.exe runs from its install dir,
|
| 31 |
+
// so we need absolute paths to reach testdata/.
|
| 32 |
+
const PROJECT_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../..");
|
| 33 |
+
|
| 34 |
+
// Load test images from GIF files
|
| 35 |
+
let opaque_data = match fs::read(format!("{PROJECT_ROOT}/testdata/assets/sprite_test.gif")) {
|
| 36 |
+
Ok(d) => d,
|
| 37 |
+
Err(e) => {
|
| 38 |
+
let _ = log_line(&format!(
|
| 39 |
+
"[BitGrid] BLIT SNAPSHOT: Failed to read sprite_test.gif: {e}"
|
| 40 |
+
));
|
| 41 |
+
return;
|
| 42 |
+
}
|
| 43 |
+
};
|
| 44 |
+
let transparent_data = match fs::read(format!(
|
| 45 |
+
"{PROJECT_ROOT}/testdata/assets/sprite_transparent_test.gif"
|
| 46 |
+
)) {
|
| 47 |
+
Ok(d) => d,
|
| 48 |
+
Err(e) => {
|
| 49 |
+
let _ = log_line(&format!(
|
| 50 |
+
"[BitGrid] BLIT SNAPSHOT: Failed to read sprite_transparent_test.gif: {e}"
|
| 51 |
+
));
|
| 52 |
+
return;
|
| 53 |
+
}
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
let opaque_img = match decode_gif_indexed(&opaque_data) {
|
| 57 |
+
Some(img) => img,
|
| 58 |
+
None => {
|
| 59 |
+
let _ = log_line("[BitGrid] BLIT SNAPSHOT: Failed to decode sprite_test.gif");
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
};
|
| 63 |
+
let transparent_img = match decode_gif_indexed(&transparent_data) {
|
| 64 |
+
Some(img) => img,
|
| 65 |
+
None => {
|
| 66 |
+
let _ =
|
| 67 |
+
log_line("[BitGrid] BLIT SNAPSHOT: Failed to decode sprite_transparent_test.gif");
|
| 68 |
+
return;
|
| 69 |
+
}
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
let dir = format!("{PROJECT_ROOT}/testdata/snapshots");
|
| 73 |
+
let _ = fs::create_dir_all(&dir);
|
| 74 |
+
let mut count = 0u32;
|
| 75 |
+
|
| 76 |
+
// Helper: save a BitGrid to a snapshot file
|
| 77 |
+
let save_grid = |grid: *mut DisplayBitGrid, name: &str| {
|
| 78 |
+
let w = (*grid).width;
|
| 79 |
+
let h = (*grid).height;
|
| 80 |
+
let stride = (*grid).row_stride;
|
| 81 |
+
let data = (*grid).data;
|
| 82 |
+
let data_size = (stride * h) as usize;
|
| 83 |
+
let path = format!("{dir}/{name}.bin");
|
| 84 |
+
if let Ok(mut file) = fs::File::create(&path) {
|
| 85 |
+
let _ = file.write_all(&w.to_le_bytes());
|
| 86 |
+
let _ = file.write_all(&h.to_le_bytes());
|
| 87 |
+
let _ = file.write_all(&stride.to_le_bytes());
|
| 88 |
+
let _ = file.write_all(core::slice::from_raw_parts(data, data_size));
|
| 89 |
+
}
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
// For each test image, run blit with various parameters
|
| 93 |
+
for (img, prefix) in [
|
| 94 |
+
(&opaque_img, "blit_opaque"),
|
| 95 |
+
(&transparent_img, "blit_transparent"),
|
| 96 |
+
] {
|
| 97 |
+
// Create source BitGrid from image data
|
| 98 |
+
let src = DisplayBitGrid::alloc(8, img.width, img.height);
|
| 99 |
+
if src.is_null() {
|
| 100 |
+
let _ = log_line("[BitGrid] BLIT SNAPSHOT: Failed to allocate source BitGrid");
|
| 101 |
+
continue;
|
| 102 |
+
}
|
| 103 |
+
// Copy image pixels into source BitGrid
|
| 104 |
+
for y in 0..img.height {
|
| 105 |
+
let src_row = (y * img.width) as usize;
|
| 106 |
+
let dst_row = (*src).data.add((y * (*src).row_stride) as usize);
|
| 107 |
+
core::ptr::copy_nonoverlapping(
|
| 108 |
+
img.pixels.as_ptr().add(src_row),
|
| 109 |
+
dst_row,
|
| 110 |
+
img.width as usize,
|
| 111 |
+
);
|
| 112 |
+
}
|
| 113 |
+
(*src).clip_left = 0;
|
| 114 |
+
(*src).clip_top = 0;
|
| 115 |
+
(*src).clip_right = img.width;
|
| 116 |
+
(*src).clip_bottom = img.height;
|
| 117 |
+
|
| 118 |
+
let sw = img.width as i32;
|
| 119 |
+
let sh = img.height as i32;
|
| 120 |
+
|
| 121 |
+
// Destination is larger than source to test positioning
|
| 122 |
+
let dw = (sw + 32) as u32;
|
| 123 |
+
let dh = (sh + 32) as u32;
|
| 124 |
+
let dst = DisplayBitGrid::alloc(8, dw, dh);
|
| 125 |
+
if dst.is_null() {
|
| 126 |
+
let destructor = (*(*src).vtable).destructor;
|
| 127 |
+
destructor(src, 1);
|
| 128 |
+
let _ = log_line("[BitGrid] BLIT SNAPSHOT: Failed to allocate dest BitGrid");
|
| 129 |
+
continue;
|
| 130 |
+
}
|
| 131 |
+
let dst_data = (*dst).data;
|
| 132 |
+
let dst_size = ((*dst).row_stride * dh) as usize;
|
| 133 |
+
|
| 134 |
+
// Test cases: (name_suffix, dst_x, dst_y, width, height, src_x, src_y, flags, bg_fill)
|
| 135 |
+
let test_cases: &[(&str, i32, i32, i32, i32, i32, i32, u32, u8)] = &[
|
| 136 |
+
// Basic orientations (blend mode 0 = direct copy)
|
| 137 |
+
("identity", 16, 16, sw, sh, 0, 0, 0x0000_0000, 0),
|
| 138 |
+
("mirror_x", 16, 16, sw, sh, 0, 0, 0x0001_0000, 0),
|
| 139 |
+
("mirror_y", 16, 16, sw, sh, 0, 0, 0x0002_0000, 0),
|
| 140 |
+
("mirror_xy", 16, 16, sw, sh, 0, 0, 0x0003_0000, 0),
|
| 141 |
+
("rotate90", 16, 16, sh, sw, 0, 0, 0x0004_0000, 0),
|
| 142 |
+
// Clipped (negative offset, only bottom-right visible)
|
| 143 |
+
("clipped", -16, -16, sw, sh, 0, 0, 0x0000_0000, 0),
|
| 144 |
+
// Color-table blend (mode 1) with identity table — tests transparency
|
| 145 |
+
("colortable", 16, 16, sw, sh, 0, 0, 0x0000_0001, 77),
|
| 146 |
+
// Color-table + mirror_x
|
| 147 |
+
("colortable_mx", 16, 16, sw, sh, 0, 0, 0x0001_0001, 77),
|
| 148 |
+
// Source sub-rect
|
| 149 |
+
(
|
| 150 |
+
"subrect",
|
| 151 |
+
16,
|
| 152 |
+
16,
|
| 153 |
+
sw / 2,
|
| 154 |
+
sh / 2,
|
| 155 |
+
sw / 4,
|
| 156 |
+
sh / 4,
|
| 157 |
+
0x0000_0000,
|
| 158 |
+
0,
|
| 159 |
+
),
|
| 160 |
+
];
|
| 161 |
+
|
| 162 |
+
let blit_target = rb(va::BLIT_SPRITE_RECT);
|
| 163 |
+
|
| 164 |
+
for &(suffix, dx, dy, w, h, sx, sy, flags, bg) in test_cases {
|
| 165 |
+
// Clear destination
|
| 166 |
+
core::ptr::write_bytes(dst_data, bg, dst_size);
|
| 167 |
+
|
| 168 |
+
// Call WA's native blit: ESI=dst, 9 stack params
|
| 169 |
+
wa_blit_sprite_rect(
|
| 170 |
+
dst,
|
| 171 |
+
dx,
|
| 172 |
+
dy,
|
| 173 |
+
w,
|
| 174 |
+
h,
|
| 175 |
+
src,
|
| 176 |
+
sx,
|
| 177 |
+
sy,
|
| 178 |
+
0, // color_table = 0 (none) for mode 0; ignored for mode 1 without table
|
| 179 |
+
flags,
|
| 180 |
+
blit_target,
|
| 181 |
+
);
|
| 182 |
+
|
| 183 |
+
let name = format!("{prefix}_{suffix}");
|
| 184 |
+
save_grid(dst, &name);
|
| 185 |
+
count += 1;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// Free grids
|
| 189 |
+
let destructor = (*(*src).vtable).destructor;
|
| 190 |
+
destructor(src, 1);
|
| 191 |
+
let destructor = (*(*dst).vtable).destructor;
|
| 192 |
+
destructor(dst, 1);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
let _ = log_line(&format!(
|
| 196 |
+
"[BitGrid] BLIT SNAPSHOT: Saved {count} snapshots to {dir}/"
|
| 197 |
+
));
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/// Decoded indexed image (no WA dependencies).
|
| 201 |
+
struct IndexedImage {
|
| 202 |
+
width: u32,
|
| 203 |
+
height: u32,
|
| 204 |
+
pixels: Vec<u8>,
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/// Decode a GIF to raw indexed pixels, remapping transparent index to 0.
|
| 208 |
+
fn decode_gif_indexed(data: &[u8]) -> Option<IndexedImage> {
|
| 209 |
+
use gif::DecodeOptions;
|
| 210 |
+
use std::io::Cursor;
|
| 211 |
+
|
| 212 |
+
let mut opts = DecodeOptions::new();
|
| 213 |
+
opts.set_color_output(gif::ColorOutput::Indexed);
|
| 214 |
+
let mut decoder = opts.read_info(Cursor::new(data)).ok()?;
|
| 215 |
+
let _global_pal = decoder.global_palette().map(|p| p.to_vec());
|
| 216 |
+
let frame = decoder.read_next_frame().ok()?.cloned()?;
|
| 217 |
+
|
| 218 |
+
let w = frame.width as u32;
|
| 219 |
+
let h = frame.height as u32;
|
| 220 |
+
let mut pixels = frame.buffer.to_vec();
|
| 221 |
+
|
| 222 |
+
// Remap transparent index to 0 (WA convention)
|
| 223 |
+
if let Some(ti) = frame.transparent {
|
| 224 |
+
if ti != 0 {
|
| 225 |
+
for p in &mut pixels {
|
| 226 |
+
if *p == 0 {
|
| 227 |
+
*p = ti;
|
| 228 |
+
} else if *p == ti {
|
| 229 |
+
*p = 0;
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
Some(IndexedImage {
|
| 236 |
+
width: w,
|
| 237 |
+
height: h,
|
| 238 |
+
pixels,
|
| 239 |
+
})
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
/// Call WA's native blit function (BitGrid__BlitSpriteRect, 0x4F6910).
|
| 243 |
+
///
|
| 244 |
+
/// The function is a usercall: ESI = destination BitGrid, 9 stack params.
|
| 245 |
+
/// `target` is the rebased runtime address.
|
| 246 |
+
#[unsafe(naked)]
|
| 247 |
+
unsafe extern "cdecl" fn wa_blit_sprite_rect(
|
| 248 |
+
_dst: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 249 |
+
_dst_x: i32,
|
| 250 |
+
_dst_y: i32,
|
| 251 |
+
_width: i32,
|
| 252 |
+
_height: i32,
|
| 253 |
+
_src: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 254 |
+
_src_x: i32,
|
| 255 |
+
_src_y: i32,
|
| 256 |
+
_color_table: u32,
|
| 257 |
+
_flags: u32,
|
| 258 |
+
_target: u32,
|
| 259 |
+
) {
|
| 260 |
+
core::arch::naked_asm!(
|
| 261 |
+
"push esi",
|
| 262 |
+
"push edi",
|
| 263 |
+
"mov esi, [esp + 12]", // ESI = dst BitGrid
|
| 264 |
+
"mov edi, [esp + 52]", // EDI = target function address
|
| 265 |
+
"push dword ptr [esp + 48]", // flags
|
| 266 |
+
"push dword ptr [esp + 48]", // color_table
|
| 267 |
+
"push dword ptr [esp + 48]", // src_y
|
| 268 |
+
"push dword ptr [esp + 48]", // src_x
|
| 269 |
+
"push dword ptr [esp + 48]", // src
|
| 270 |
+
"push dword ptr [esp + 48]", // height
|
| 271 |
+
"push dword ptr [esp + 48]", // width
|
| 272 |
+
"push dword ptr [esp + 48]", // dst_y
|
| 273 |
+
"push dword ptr [esp + 48]", // dst_x
|
| 274 |
+
"call edi", // RET 0x24 cleans 9 params
|
| 275 |
+
"pop edi",
|
| 276 |
+
"pop esi",
|
| 277 |
+
"ret",
|
| 278 |
+
);
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
// =========================================================================
|
| 282 |
+
// Core sprite blit hook (BitGrid__BlitSpriteRect, 0x4F6910)
|
| 283 |
+
// =========================================================================
|
| 284 |
+
|
| 285 |
+
/// Trampoline to the original WA blit function (for unsupported modes).
|
| 286 |
+
static ORIG_BLIT: AtomicU32 = AtomicU32::new(0);
|
| 287 |
+
|
| 288 |
+
/// Naked trampoline: captures ESI (dst BitGrid) and forwards to cdecl impl.
|
| 289 |
+
///
|
| 290 |
+
/// Original calling convention: ESI=dst, 9 stdcall params, RET 0x24.
|
| 291 |
+
#[unsafe(naked)]
|
| 292 |
+
unsafe extern "C" fn blit_hook_trampoline() {
|
| 293 |
+
core::arch::naked_asm!(
|
| 294 |
+
"push ebp",
|
| 295 |
+
"mov ebp, esp",
|
| 296 |
+
"push dword ptr [ebp+0x28]", // flags
|
| 297 |
+
"push dword ptr [ebp+0x24]", // color_table
|
| 298 |
+
"push dword ptr [ebp+0x20]", // src_y
|
| 299 |
+
"push dword ptr [ebp+0x1C]", // src_x
|
| 300 |
+
"push dword ptr [ebp+0x18]", // src BitGrid
|
| 301 |
+
"push dword ptr [ebp+0x14]", // height
|
| 302 |
+
"push dword ptr [ebp+0x10]", // width
|
| 303 |
+
"push dword ptr [ebp+0x0C]", // dst_y
|
| 304 |
+
"push dword ptr [ebp+0x08]", // dst_x
|
| 305 |
+
"push esi", // dst BitGrid
|
| 306 |
+
"call {impl_fn}",
|
| 307 |
+
"add esp, 40",
|
| 308 |
+
"mov esp, ebp",
|
| 309 |
+
"pop ebp",
|
| 310 |
+
"ret 0x24",
|
| 311 |
+
impl_fn = sym blit_impl,
|
| 312 |
+
);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
/// Rust implementation of the core sprite blit.
|
| 316 |
+
///
|
| 317 |
+
/// Handles 8bpp blend modes 0 (copy) and 1 (color table / transparency).
|
| 318 |
+
/// Falls through to the original WA function for unsupported modes.
|
| 319 |
+
unsafe extern "cdecl" fn blit_impl(
|
| 320 |
+
dst: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 321 |
+
dst_x: i32,
|
| 322 |
+
dst_y: i32,
|
| 323 |
+
width: i32,
|
| 324 |
+
height: i32,
|
| 325 |
+
src: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 326 |
+
src_x: i32,
|
| 327 |
+
src_y: i32,
|
| 328 |
+
color_table: u32,
|
| 329 |
+
flags: u32,
|
| 330 |
+
) -> u32 {
|
| 331 |
+
use openwa_core::display::sprite_blit::{
|
| 332 |
+
blit_sprite_rect, BlitBlend, BlitOrientation, BlitSource,
|
| 333 |
+
};
|
| 334 |
+
|
| 335 |
+
if width == 0 || height == 0 {
|
| 336 |
+
return 0;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
let blend_mode = flags & 0xFFFF;
|
| 340 |
+
let src_cpp = (*src).cells_per_unit;
|
| 341 |
+
let dst_cpp = (*dst).cells_per_unit;
|
| 342 |
+
|
| 343 |
+
// Only handle 8bpp surfaces with blend modes 0 and 1
|
| 344 |
+
if dst_cpp != 8 || src_cpp != 8 || blend_mode > 1 {
|
| 345 |
+
return call_original_blit(
|
| 346 |
+
dst,
|
| 347 |
+
dst_x,
|
| 348 |
+
dst_y,
|
| 349 |
+
width,
|
| 350 |
+
height,
|
| 351 |
+
src,
|
| 352 |
+
src_x,
|
| 353 |
+
src_y,
|
| 354 |
+
color_table,
|
| 355 |
+
flags,
|
| 356 |
+
);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// For mode 1 with a color table pointer, fall through for now
|
| 360 |
+
// (we'd need to read 256 bytes from the pointer, which could be a mixing LUT)
|
| 361 |
+
if blend_mode == 1 && color_table != 0 {
|
| 362 |
+
return call_original_blit(
|
| 363 |
+
dst,
|
| 364 |
+
dst_x,
|
| 365 |
+
dst_y,
|
| 366 |
+
width,
|
| 367 |
+
height,
|
| 368 |
+
src,
|
| 369 |
+
src_x,
|
| 370 |
+
src_y,
|
| 371 |
+
color_table,
|
| 372 |
+
flags,
|
| 373 |
+
);
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
let orientation = BlitOrientation::from_flags(flags);
|
| 377 |
+
let blend = BlitBlend::from_flags(flags);
|
| 378 |
+
|
| 379 |
+
// Build source view
|
| 380 |
+
let src_data =
|
| 381 |
+
core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
|
| 382 |
+
let blit_src = BlitSource {
|
| 383 |
+
data: src_data,
|
| 384 |
+
width: (*src).width,
|
| 385 |
+
height: (*src).height,
|
| 386 |
+
row_stride: (*src).row_stride,
|
| 387 |
+
};
|
| 388 |
+
|
| 389 |
+
// Build mutable destination view
|
| 390 |
+
let dst_stride = (*dst).row_stride;
|
| 391 |
+
let dst_h = (*dst).height;
|
| 392 |
+
let dst_w = (*dst).width;
|
| 393 |
+
let dst_data_len = (dst_stride * dst_h) as usize;
|
| 394 |
+
|
| 395 |
+
// Create a temporary PixelGrid wrapping the destination BitGrid's memory
|
| 396 |
+
let mut dst_grid = openwa_core::display::line_draw::PixelGrid {
|
| 397 |
+
data: Vec::new(), // placeholder — we'll swap in the real data
|
| 398 |
+
width: dst_w,
|
| 399 |
+
height: dst_h,
|
| 400 |
+
row_stride: dst_stride,
|
| 401 |
+
clip_left: (*dst).clip_left,
|
| 402 |
+
clip_top: (*dst).clip_top,
|
| 403 |
+
clip_right: (*dst).clip_right,
|
| 404 |
+
clip_bottom: (*dst).clip_bottom,
|
| 405 |
+
};
|
| 406 |
+
// Swap the real data in (avoids allocation)
|
| 407 |
+
let mut real_data = Vec::from_raw_parts((*dst).data, dst_data_len, dst_data_len);
|
| 408 |
+
core::mem::swap(&mut dst_grid.data, &mut real_data);
|
| 409 |
+
|
| 410 |
+
let result = blit_sprite_rect(
|
| 411 |
+
&mut dst_grid,
|
| 412 |
+
&blit_src,
|
| 413 |
+
dst_x,
|
| 414 |
+
dst_y,
|
| 415 |
+
width,
|
| 416 |
+
height,
|
| 417 |
+
src_x,
|
| 418 |
+
src_y,
|
| 419 |
+
None, // no color table for modes we handle
|
| 420 |
+
orientation,
|
| 421 |
+
blend,
|
| 422 |
+
);
|
| 423 |
+
|
| 424 |
+
// Swap data back — we must NOT let Vec drop the BitGrid's data
|
| 425 |
+
core::mem::swap(&mut dst_grid.data, &mut real_data);
|
| 426 |
+
core::mem::forget(real_data); // BitGrid owns this memory
|
| 427 |
+
|
| 428 |
+
result as u32
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/// Call the original WA blit function via trampoline.
|
| 432 |
+
unsafe fn call_original_blit(
|
| 433 |
+
dst: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 434 |
+
dst_x: i32,
|
| 435 |
+
dst_y: i32,
|
| 436 |
+
width: i32,
|
| 437 |
+
height: i32,
|
| 438 |
+
src: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 439 |
+
src_x: i32,
|
| 440 |
+
src_y: i32,
|
| 441 |
+
color_table: u32,
|
| 442 |
+
flags: u32,
|
| 443 |
+
) -> u32 {
|
| 444 |
+
let orig = ORIG_BLIT.load(Ordering::Relaxed);
|
| 445 |
+
call_original_blit_asm(
|
| 446 |
+
dst,
|
| 447 |
+
dst_x,
|
| 448 |
+
dst_y,
|
| 449 |
+
width,
|
| 450 |
+
height,
|
| 451 |
+
src,
|
| 452 |
+
src_x,
|
| 453 |
+
src_y,
|
| 454 |
+
color_table,
|
| 455 |
+
flags,
|
| 456 |
+
orig,
|
| 457 |
+
)
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
/// Naked asm to call the original blit with ESI set correctly.
|
| 461 |
+
#[unsafe(naked)]
|
| 462 |
+
unsafe extern "cdecl" fn call_original_blit_asm(
|
| 463 |
+
_dst: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 464 |
+
_dst_x: i32,
|
| 465 |
+
_dst_y: i32,
|
| 466 |
+
_width: i32,
|
| 467 |
+
_height: i32,
|
| 468 |
+
_src: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 469 |
+
_src_x: i32,
|
| 470 |
+
_src_y: i32,
|
| 471 |
+
_color_table: u32,
|
| 472 |
+
_flags: u32,
|
| 473 |
+
_target: u32,
|
| 474 |
+
) -> u32 {
|
| 475 |
+
core::arch::naked_asm!(
|
| 476 |
+
"push esi",
|
| 477 |
+
"push edi",
|
| 478 |
+
"mov esi, [esp + 12]", // dst
|
| 479 |
+
"mov edi, [esp + 52]", // target
|
| 480 |
+
"push dword ptr [esp + 48]", // flags
|
| 481 |
+
"push dword ptr [esp + 48]", // color_table
|
| 482 |
+
"push dword ptr [esp + 48]", // src_y
|
| 483 |
+
"push dword ptr [esp + 48]", // src_x
|
| 484 |
+
"push dword ptr [esp + 48]", // src
|
| 485 |
+
"push dword ptr [esp + 48]", // height
|
| 486 |
+
"push dword ptr [esp + 48]", // width
|
| 487 |
+
"push dword ptr [esp + 48]", // dst_y
|
| 488 |
+
"push dword ptr [esp + 48]", // dst_x
|
| 489 |
+
"call edi",
|
| 490 |
+
"pop edi",
|
| 491 |
+
"pop esi",
|
| 492 |
+
"ret",
|
| 493 |
+
);
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
pub fn install() -> Result<(), String> {
|
| 497 |
+
let _ = log_line("[BitGrid] Hooking BitGrid__BlitSpriteRect");
|
| 498 |
+
|
| 499 |
+
unsafe {
|
| 500 |
+
let orig = crate::hook::install(
|
| 501 |
+
"BitGrid__BlitSpriteRect",
|
| 502 |
+
va::BLIT_SPRITE_RECT,
|
| 503 |
+
blit_hook_trampoline as *const (),
|
| 504 |
+
)?;
|
| 505 |
+
ORIG_BLIT.store(orig as u32, Ordering::Relaxed);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
Ok(())
|
| 509 |
+
}
|
crates/openwa-dll/src/replacements/display.rs
CHANGED
|
@@ -266,502 +266,6 @@ pub unsafe fn capture_line_snapshots() {
|
|
| 266 |
destructor(grid, 1);
|
| 267 |
}
|
| 268 |
|
| 269 |
-
/// Capture sprite blit snapshots from WA's native FUN_004f6910.
|
| 270 |
-
///
|
| 271 |
-
/// Creates source and destination BitGrids, loads test GIF images,
|
| 272 |
-
/// calls WA's blit with various orientations and blend modes,
|
| 273 |
-
/// saves pixel output to `testdata/snapshots/`. Activated by
|
| 274 |
-
/// `OPENWA_CAPTURE_BLIT_SNAPSHOTS=1`.
|
| 275 |
-
pub unsafe fn capture_blit_snapshots() {
|
| 276 |
-
use openwa_core::bitgrid::DisplayBitGrid;
|
| 277 |
-
use std::fs;
|
| 278 |
-
use std::io::Write;
|
| 279 |
-
|
| 280 |
-
let _ = log_line("[Display] BLIT SNAPSHOT: Starting capture");
|
| 281 |
-
|
| 282 |
-
// Project root at compile time — WA.exe runs from its install dir,
|
| 283 |
-
// so we need absolute paths to reach testdata/.
|
| 284 |
-
const PROJECT_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../..");
|
| 285 |
-
|
| 286 |
-
// Load test images from GIF files
|
| 287 |
-
let opaque_data = match fs::read(format!("{PROJECT_ROOT}/testdata/assets/sprite_test.gif")) {
|
| 288 |
-
Ok(d) => d,
|
| 289 |
-
Err(e) => {
|
| 290 |
-
let _ = log_line(&format!(
|
| 291 |
-
"[Display] BLIT SNAPSHOT: Failed to read sprite_test.gif: {e}"
|
| 292 |
-
));
|
| 293 |
-
return;
|
| 294 |
-
}
|
| 295 |
-
};
|
| 296 |
-
let transparent_data = match fs::read(format!(
|
| 297 |
-
"{PROJECT_ROOT}/testdata/assets/sprite_transparent_test.gif"
|
| 298 |
-
)) {
|
| 299 |
-
Ok(d) => d,
|
| 300 |
-
Err(e) => {
|
| 301 |
-
let _ = log_line(&format!(
|
| 302 |
-
"[Display] BLIT SNAPSHOT: Failed to read sprite_transparent_test.gif: {e}"
|
| 303 |
-
));
|
| 304 |
-
return;
|
| 305 |
-
}
|
| 306 |
-
};
|
| 307 |
-
|
| 308 |
-
let opaque_img = match decode_gif_indexed(&opaque_data) {
|
| 309 |
-
Some(img) => img,
|
| 310 |
-
None => {
|
| 311 |
-
let _ = log_line("[Display] BLIT SNAPSHOT: Failed to decode sprite_test.gif");
|
| 312 |
-
return;
|
| 313 |
-
}
|
| 314 |
-
};
|
| 315 |
-
let transparent_img = match decode_gif_indexed(&transparent_data) {
|
| 316 |
-
Some(img) => img,
|
| 317 |
-
None => {
|
| 318 |
-
let _ =
|
| 319 |
-
log_line("[Display] BLIT SNAPSHOT: Failed to decode sprite_transparent_test.gif");
|
| 320 |
-
return;
|
| 321 |
-
}
|
| 322 |
-
};
|
| 323 |
-
|
| 324 |
-
let dir = format!("{PROJECT_ROOT}/testdata/snapshots");
|
| 325 |
-
let _ = fs::create_dir_all(&dir);
|
| 326 |
-
let mut count = 0u32;
|
| 327 |
-
|
| 328 |
-
// Helper: save a BitGrid to a snapshot file
|
| 329 |
-
let save_grid = |grid: *mut DisplayBitGrid, name: &str| {
|
| 330 |
-
let w = (*grid).width;
|
| 331 |
-
let h = (*grid).height;
|
| 332 |
-
let stride = (*grid).row_stride;
|
| 333 |
-
let data = (*grid).data;
|
| 334 |
-
let data_size = (stride * h) as usize;
|
| 335 |
-
let path = format!("{dir}/{name}.bin");
|
| 336 |
-
if let Ok(mut file) = fs::File::create(&path) {
|
| 337 |
-
let _ = file.write_all(&w.to_le_bytes());
|
| 338 |
-
let _ = file.write_all(&h.to_le_bytes());
|
| 339 |
-
let _ = file.write_all(&stride.to_le_bytes());
|
| 340 |
-
let _ = file.write_all(core::slice::from_raw_parts(data, data_size));
|
| 341 |
-
}
|
| 342 |
-
};
|
| 343 |
-
|
| 344 |
-
// For each test image, run blit with various parameters
|
| 345 |
-
for (img, prefix) in [
|
| 346 |
-
(&opaque_img, "blit_opaque"),
|
| 347 |
-
(&transparent_img, "blit_transparent"),
|
| 348 |
-
] {
|
| 349 |
-
// Create source BitGrid from image data
|
| 350 |
-
let src = DisplayBitGrid::alloc(8, img.width, img.height);
|
| 351 |
-
if src.is_null() {
|
| 352 |
-
let _ = log_line("[Display] BLIT SNAPSHOT: Failed to allocate source BitGrid");
|
| 353 |
-
continue;
|
| 354 |
-
}
|
| 355 |
-
// Copy image pixels into source BitGrid
|
| 356 |
-
for y in 0..img.height {
|
| 357 |
-
let src_row = (y * img.width) as usize;
|
| 358 |
-
let dst_row = (*src).data.add((y * (*src).row_stride) as usize);
|
| 359 |
-
core::ptr::copy_nonoverlapping(
|
| 360 |
-
img.pixels.as_ptr().add(src_row),
|
| 361 |
-
dst_row,
|
| 362 |
-
img.width as usize,
|
| 363 |
-
);
|
| 364 |
-
}
|
| 365 |
-
(*src).clip_left = 0;
|
| 366 |
-
(*src).clip_top = 0;
|
| 367 |
-
(*src).clip_right = img.width;
|
| 368 |
-
(*src).clip_bottom = img.height;
|
| 369 |
-
|
| 370 |
-
let sw = img.width as i32;
|
| 371 |
-
let sh = img.height as i32;
|
| 372 |
-
|
| 373 |
-
// Destination is larger than source to test positioning
|
| 374 |
-
let dw = (sw + 32) as u32;
|
| 375 |
-
let dh = (sh + 32) as u32;
|
| 376 |
-
let dst = DisplayBitGrid::alloc(8, dw, dh);
|
| 377 |
-
if dst.is_null() {
|
| 378 |
-
let destructor = (*(*src).vtable).destructor;
|
| 379 |
-
destructor(src, 1);
|
| 380 |
-
let _ = log_line("[Display] BLIT SNAPSHOT: Failed to allocate dest BitGrid");
|
| 381 |
-
continue;
|
| 382 |
-
}
|
| 383 |
-
let dst_data = (*dst).data;
|
| 384 |
-
let dst_size = ((*dst).row_stride * dh) as usize;
|
| 385 |
-
|
| 386 |
-
// Test cases: (name_suffix, dst_x, dst_y, width, height, src_x, src_y, flags, bg_fill)
|
| 387 |
-
let test_cases: &[(&str, i32, i32, i32, i32, i32, i32, u32, u8)] = &[
|
| 388 |
-
// Basic orientations (blend mode 0 = direct copy)
|
| 389 |
-
("identity", 16, 16, sw, sh, 0, 0, 0x0000_0000, 0),
|
| 390 |
-
("mirror_x", 16, 16, sw, sh, 0, 0, 0x0001_0000, 0),
|
| 391 |
-
("mirror_y", 16, 16, sw, sh, 0, 0, 0x0002_0000, 0),
|
| 392 |
-
("mirror_xy", 16, 16, sw, sh, 0, 0, 0x0003_0000, 0),
|
| 393 |
-
("rotate90", 16, 16, sh, sw, 0, 0, 0x0004_0000, 0),
|
| 394 |
-
// Clipped (negative offset, only bottom-right visible)
|
| 395 |
-
("clipped", -16, -16, sw, sh, 0, 0, 0x0000_0000, 0),
|
| 396 |
-
// Color-table blend (mode 1) with identity table — tests transparency
|
| 397 |
-
("colortable", 16, 16, sw, sh, 0, 0, 0x0000_0001, 77),
|
| 398 |
-
// Color-table + mirror_x
|
| 399 |
-
("colortable_mx", 16, 16, sw, sh, 0, 0, 0x0001_0001, 77),
|
| 400 |
-
// Source sub-rect
|
| 401 |
-
(
|
| 402 |
-
"subrect",
|
| 403 |
-
16,
|
| 404 |
-
16,
|
| 405 |
-
sw / 2,
|
| 406 |
-
sh / 2,
|
| 407 |
-
sw / 4,
|
| 408 |
-
sh / 4,
|
| 409 |
-
0x0000_0000,
|
| 410 |
-
0,
|
| 411 |
-
),
|
| 412 |
-
];
|
| 413 |
-
|
| 414 |
-
let blit_target = rb(va::BLIT_SPRITE_RECT);
|
| 415 |
-
|
| 416 |
-
for &(suffix, dx, dy, w, h, sx, sy, flags, bg) in test_cases {
|
| 417 |
-
// Clear destination
|
| 418 |
-
core::ptr::write_bytes(dst_data, bg, dst_size);
|
| 419 |
-
|
| 420 |
-
// Call WA's native blit: ESI=dst, 9 stack params
|
| 421 |
-
wa_blit_sprite_rect(
|
| 422 |
-
dst,
|
| 423 |
-
dx,
|
| 424 |
-
dy,
|
| 425 |
-
w,
|
| 426 |
-
h,
|
| 427 |
-
src,
|
| 428 |
-
sx,
|
| 429 |
-
sy,
|
| 430 |
-
0, // color_table = 0 (none) for mode 0; ignored for mode 1 without table
|
| 431 |
-
flags,
|
| 432 |
-
blit_target,
|
| 433 |
-
);
|
| 434 |
-
|
| 435 |
-
let name = format!("{prefix}_{suffix}");
|
| 436 |
-
save_grid(dst, &name);
|
| 437 |
-
count += 1;
|
| 438 |
-
}
|
| 439 |
-
|
| 440 |
-
// Free grids
|
| 441 |
-
let destructor = (*(*src).vtable).destructor;
|
| 442 |
-
destructor(src, 1);
|
| 443 |
-
let destructor = (*(*dst).vtable).destructor;
|
| 444 |
-
destructor(dst, 1);
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
-
let _ = log_line(&format!(
|
| 448 |
-
"[Display] BLIT SNAPSHOT: Saved {count} snapshots to {dir}/"
|
| 449 |
-
));
|
| 450 |
-
}
|
| 451 |
-
|
| 452 |
-
/// Decoded indexed image (no WA dependencies).
|
| 453 |
-
struct IndexedImage {
|
| 454 |
-
width: u32,
|
| 455 |
-
height: u32,
|
| 456 |
-
pixels: Vec<u8>,
|
| 457 |
-
}
|
| 458 |
-
|
| 459 |
-
/// Decode a GIF to raw indexed pixels, remapping transparent index to 0.
|
| 460 |
-
fn decode_gif_indexed(data: &[u8]) -> Option<IndexedImage> {
|
| 461 |
-
use gif::DecodeOptions;
|
| 462 |
-
use std::io::Cursor;
|
| 463 |
-
|
| 464 |
-
let mut opts = DecodeOptions::new();
|
| 465 |
-
opts.set_color_output(gif::ColorOutput::Indexed);
|
| 466 |
-
let mut decoder = opts.read_info(Cursor::new(data)).ok()?;
|
| 467 |
-
let _global_pal = decoder.global_palette().map(|p| p.to_vec());
|
| 468 |
-
let frame = decoder.read_next_frame().ok()?.cloned()?;
|
| 469 |
-
|
| 470 |
-
let w = frame.width as u32;
|
| 471 |
-
let h = frame.height as u32;
|
| 472 |
-
let mut pixels = frame.buffer.to_vec();
|
| 473 |
-
|
| 474 |
-
// Remap transparent index to 0 (WA convention)
|
| 475 |
-
if let Some(ti) = frame.transparent {
|
| 476 |
-
if ti != 0 {
|
| 477 |
-
for p in &mut pixels {
|
| 478 |
-
if *p == 0 {
|
| 479 |
-
*p = ti;
|
| 480 |
-
} else if *p == ti {
|
| 481 |
-
*p = 0;
|
| 482 |
-
}
|
| 483 |
-
}
|
| 484 |
-
}
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
Some(IndexedImage {
|
| 488 |
-
width: w,
|
| 489 |
-
height: h,
|
| 490 |
-
pixels,
|
| 491 |
-
})
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
/// Call WA's native blit function (FUN_004f6910).
|
| 495 |
-
///
|
| 496 |
-
/// The function is a usercall: ESI = destination BitGrid, 9 stack params.
|
| 497 |
-
/// `target` is the rebased runtime address of FUN_004f6910.
|
| 498 |
-
#[unsafe(naked)]
|
| 499 |
-
unsafe extern "cdecl" fn wa_blit_sprite_rect(
|
| 500 |
-
dst: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 501 |
-
dst_x: i32,
|
| 502 |
-
dst_y: i32,
|
| 503 |
-
width: i32,
|
| 504 |
-
height: i32,
|
| 505 |
-
src: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 506 |
-
src_x: i32,
|
| 507 |
-
src_y: i32,
|
| 508 |
-
color_table: u32,
|
| 509 |
-
flags: u32,
|
| 510 |
-
target: u32,
|
| 511 |
-
) {
|
| 512 |
-
// cdecl params on stack: [ESP+4]=dst, [ESP+8]=dst_x, ...
|
| 513 |
-
// [ESP+40]=flags, [ESP+44]=target
|
| 514 |
-
// Need to: set ESI=dst, push 9 params, call target, restore ESI
|
| 515 |
-
//
|
| 516 |
-
// After push esi + push edi, the params start at ESP+12:
|
| 517 |
-
// ESP+12=dst, ESP+16=dst_x, ESP+20=dst_y, ESP+24=width,
|
| 518 |
-
// ESP+28=height, ESP+32=src, ESP+36=src_x, ESP+40=src_y,
|
| 519 |
-
// ESP+44=color_table, ESP+48=flags, ESP+52=target
|
| 520 |
-
//
|
| 521 |
-
// Each `push [esp+48]` pushes the topmost remaining param (flags first),
|
| 522 |
-
// then the sliding window moves: after each push, the NEXT param occupies
|
| 523 |
-
// the same [ESP+48] slot.
|
| 524 |
-
core::arch::naked_asm!(
|
| 525 |
-
"push esi",
|
| 526 |
-
"push edi",
|
| 527 |
-
"mov esi, [esp + 12]", // ESI = dst BitGrid
|
| 528 |
-
"mov edi, [esp + 52]", // EDI = target function address
|
| 529 |
-
"push dword ptr [esp + 48]", // flags
|
| 530 |
-
"push dword ptr [esp + 48]", // color_table
|
| 531 |
-
"push dword ptr [esp + 48]", // src_y
|
| 532 |
-
"push dword ptr [esp + 48]", // src_x
|
| 533 |
-
"push dword ptr [esp + 48]", // src
|
| 534 |
-
"push dword ptr [esp + 48]", // height
|
| 535 |
-
"push dword ptr [esp + 48]", // width
|
| 536 |
-
"push dword ptr [esp + 48]", // dst_y
|
| 537 |
-
"push dword ptr [esp + 48]", // dst_x
|
| 538 |
-
"call edi", // RET 0x24 cleans 9 params
|
| 539 |
-
"pop edi",
|
| 540 |
-
"pop esi",
|
| 541 |
-
"ret",
|
| 542 |
-
);
|
| 543 |
-
}
|
| 544 |
-
|
| 545 |
-
// =========================================================================
|
| 546 |
-
// Core sprite blit hook (BitGrid__BlitSpriteRect, 0x4F6910)
|
| 547 |
-
// =========================================================================
|
| 548 |
-
|
| 549 |
-
use core::sync::atomic::{AtomicU32, Ordering};
|
| 550 |
-
|
| 551 |
-
/// Trampoline to the original WA blit function (for unsupported modes).
|
| 552 |
-
static ORIG_BLIT: AtomicU32 = AtomicU32::new(0);
|
| 553 |
-
|
| 554 |
-
/// Naked trampoline: captures ESI (dst BitGrid) and forwards to cdecl impl.
|
| 555 |
-
///
|
| 556 |
-
/// Original calling convention: ESI=dst, 9 stdcall params, RET 0x24.
|
| 557 |
-
#[unsafe(naked)]
|
| 558 |
-
unsafe extern "C" fn blit_hook_trampoline() {
|
| 559 |
-
core::arch::naked_asm!(
|
| 560 |
-
"push ebp",
|
| 561 |
-
"mov ebp, esp",
|
| 562 |
-
// Push 9 stack params + ESI as 10 cdecl params
|
| 563 |
-
"push dword ptr [ebp+0x28]", // flags
|
| 564 |
-
"push dword ptr [ebp+0x24]", // color_table
|
| 565 |
-
"push dword ptr [ebp+0x20]", // src_y
|
| 566 |
-
"push dword ptr [ebp+0x1C]", // src_x
|
| 567 |
-
"push dword ptr [ebp+0x18]", // src BitGrid
|
| 568 |
-
"push dword ptr [ebp+0x14]", // height
|
| 569 |
-
"push dword ptr [ebp+0x10]", // width
|
| 570 |
-
"push dword ptr [ebp+0x0C]", // dst_y
|
| 571 |
-
"push dword ptr [ebp+0x08]", // dst_x
|
| 572 |
-
"push esi", // dst BitGrid
|
| 573 |
-
"call {impl_fn}",
|
| 574 |
-
"add esp, 40",
|
| 575 |
-
"mov esp, ebp",
|
| 576 |
-
"pop ebp",
|
| 577 |
-
"ret 0x24",
|
| 578 |
-
impl_fn = sym blit_impl,
|
| 579 |
-
);
|
| 580 |
-
}
|
| 581 |
-
|
| 582 |
-
/// Rust implementation of the core sprite blit.
|
| 583 |
-
///
|
| 584 |
-
/// Handles 8bpp blend modes 0 (copy) and 1 (color table / transparency).
|
| 585 |
-
/// Falls through to the original WA function for unsupported modes.
|
| 586 |
-
unsafe extern "cdecl" fn blit_impl(
|
| 587 |
-
dst: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 588 |
-
dst_x: i32,
|
| 589 |
-
dst_y: i32,
|
| 590 |
-
width: i32,
|
| 591 |
-
height: i32,
|
| 592 |
-
src: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 593 |
-
src_x: i32,
|
| 594 |
-
src_y: i32,
|
| 595 |
-
color_table: u32,
|
| 596 |
-
flags: u32,
|
| 597 |
-
) -> u32 {
|
| 598 |
-
use openwa_core::display::sprite_blit::{
|
| 599 |
-
blit_sprite_rect, BlitBlend, BlitOrientation, BlitSource,
|
| 600 |
-
};
|
| 601 |
-
|
| 602 |
-
if width == 0 || height == 0 {
|
| 603 |
-
return 0;
|
| 604 |
-
}
|
| 605 |
-
|
| 606 |
-
let blend_mode = flags & 0xFFFF;
|
| 607 |
-
let src_cpp = (*src).cells_per_unit;
|
| 608 |
-
let dst_cpp = (*dst).cells_per_unit;
|
| 609 |
-
|
| 610 |
-
// Only handle 8bpp surfaces with blend modes 0 and 1
|
| 611 |
-
if dst_cpp != 8 || src_cpp != 8 || blend_mode > 1 {
|
| 612 |
-
return call_original_blit(
|
| 613 |
-
dst,
|
| 614 |
-
dst_x,
|
| 615 |
-
dst_y,
|
| 616 |
-
width,
|
| 617 |
-
height,
|
| 618 |
-
src,
|
| 619 |
-
src_x,
|
| 620 |
-
src_y,
|
| 621 |
-
color_table,
|
| 622 |
-
flags,
|
| 623 |
-
);
|
| 624 |
-
}
|
| 625 |
-
|
| 626 |
-
// For mode 1 with a color table pointer, fall through for now
|
| 627 |
-
// (we'd need to read 256 bytes from the pointer, which could be a mixing LUT)
|
| 628 |
-
if blend_mode == 1 && color_table != 0 {
|
| 629 |
-
return call_original_blit(
|
| 630 |
-
dst,
|
| 631 |
-
dst_x,
|
| 632 |
-
dst_y,
|
| 633 |
-
width,
|
| 634 |
-
height,
|
| 635 |
-
src,
|
| 636 |
-
src_x,
|
| 637 |
-
src_y,
|
| 638 |
-
color_table,
|
| 639 |
-
flags,
|
| 640 |
-
);
|
| 641 |
-
}
|
| 642 |
-
|
| 643 |
-
let orientation = BlitOrientation::from_flags(flags);
|
| 644 |
-
let blend = BlitBlend::from_flags(flags);
|
| 645 |
-
|
| 646 |
-
// Build source view
|
| 647 |
-
let src_data =
|
| 648 |
-
core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
|
| 649 |
-
let blit_src = BlitSource {
|
| 650 |
-
data: src_data,
|
| 651 |
-
width: (*src).width,
|
| 652 |
-
height: (*src).height,
|
| 653 |
-
row_stride: (*src).row_stride,
|
| 654 |
-
};
|
| 655 |
-
|
| 656 |
-
// Build mutable destination view
|
| 657 |
-
let dst_stride = (*dst).row_stride;
|
| 658 |
-
let dst_h = (*dst).height;
|
| 659 |
-
let dst_w = (*dst).width;
|
| 660 |
-
let dst_data_len = (dst_stride * dst_h) as usize;
|
| 661 |
-
|
| 662 |
-
// Create a temporary PixelGrid wrapping the destination BitGrid's memory
|
| 663 |
-
let mut dst_grid = openwa_core::display::line_draw::PixelGrid {
|
| 664 |
-
data: Vec::new(), // placeholder — we'll swap in the real data
|
| 665 |
-
width: dst_w,
|
| 666 |
-
height: dst_h,
|
| 667 |
-
row_stride: dst_stride,
|
| 668 |
-
clip_left: (*dst).clip_left,
|
| 669 |
-
clip_top: (*dst).clip_top,
|
| 670 |
-
clip_right: (*dst).clip_right,
|
| 671 |
-
clip_bottom: (*dst).clip_bottom,
|
| 672 |
-
};
|
| 673 |
-
// Swap the real data in (avoids allocation)
|
| 674 |
-
let mut real_data = Vec::from_raw_parts((*dst).data, dst_data_len, dst_data_len);
|
| 675 |
-
core::mem::swap(&mut dst_grid.data, &mut real_data);
|
| 676 |
-
|
| 677 |
-
let result = blit_sprite_rect(
|
| 678 |
-
&mut dst_grid,
|
| 679 |
-
&blit_src,
|
| 680 |
-
dst_x,
|
| 681 |
-
dst_y,
|
| 682 |
-
width,
|
| 683 |
-
height,
|
| 684 |
-
src_x,
|
| 685 |
-
src_y,
|
| 686 |
-
None, // no color table for modes we handle
|
| 687 |
-
orientation,
|
| 688 |
-
blend,
|
| 689 |
-
);
|
| 690 |
-
|
| 691 |
-
// Swap data back — we must NOT let Vec drop the BitGrid's data
|
| 692 |
-
core::mem::swap(&mut dst_grid.data, &mut real_data);
|
| 693 |
-
core::mem::forget(real_data); // BitGrid owns this memory
|
| 694 |
-
|
| 695 |
-
result as u32
|
| 696 |
-
}
|
| 697 |
-
|
| 698 |
-
/// Call the original WA blit function via trampoline.
|
| 699 |
-
///
|
| 700 |
-
/// The trampoline expects: ESI=dst, 9 stdcall params.
|
| 701 |
-
unsafe fn call_original_blit(
|
| 702 |
-
dst: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 703 |
-
dst_x: i32,
|
| 704 |
-
dst_y: i32,
|
| 705 |
-
width: i32,
|
| 706 |
-
height: i32,
|
| 707 |
-
src: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 708 |
-
src_x: i32,
|
| 709 |
-
src_y: i32,
|
| 710 |
-
color_table: u32,
|
| 711 |
-
flags: u32,
|
| 712 |
-
) -> u32 {
|
| 713 |
-
let orig = ORIG_BLIT.load(Ordering::Relaxed);
|
| 714 |
-
call_original_blit_asm(
|
| 715 |
-
dst,
|
| 716 |
-
dst_x,
|
| 717 |
-
dst_y,
|
| 718 |
-
width,
|
| 719 |
-
height,
|
| 720 |
-
src,
|
| 721 |
-
src_x,
|
| 722 |
-
src_y,
|
| 723 |
-
color_table,
|
| 724 |
-
flags,
|
| 725 |
-
orig,
|
| 726 |
-
)
|
| 727 |
-
}
|
| 728 |
-
|
| 729 |
-
/// Naked asm to call the original blit with ESI set correctly.
|
| 730 |
-
#[unsafe(naked)]
|
| 731 |
-
unsafe extern "cdecl" fn call_original_blit_asm(
|
| 732 |
-
_dst: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 733 |
-
_dst_x: i32,
|
| 734 |
-
_dst_y: i32,
|
| 735 |
-
_width: i32,
|
| 736 |
-
_height: i32,
|
| 737 |
-
_src: *mut openwa_core::bitgrid::DisplayBitGrid,
|
| 738 |
-
_src_x: i32,
|
| 739 |
-
_src_y: i32,
|
| 740 |
-
_color_table: u32,
|
| 741 |
-
_flags: u32,
|
| 742 |
-
_target: u32,
|
| 743 |
-
) -> u32 {
|
| 744 |
-
core::arch::naked_asm!(
|
| 745 |
-
"push esi",
|
| 746 |
-
"push edi",
|
| 747 |
-
"mov esi, [esp + 12]", // dst
|
| 748 |
-
"mov edi, [esp + 52]", // target
|
| 749 |
-
"push dword ptr [esp + 48]", // flags
|
| 750 |
-
"push dword ptr [esp + 48]", // color_table
|
| 751 |
-
"push dword ptr [esp + 48]", // src_y
|
| 752 |
-
"push dword ptr [esp + 48]", // src_x
|
| 753 |
-
"push dword ptr [esp + 48]", // src
|
| 754 |
-
"push dword ptr [esp + 48]", // height
|
| 755 |
-
"push dword ptr [esp + 48]", // width
|
| 756 |
-
"push dword ptr [esp + 48]", // dst_y
|
| 757 |
-
"push dword ptr [esp + 48]", // dst_x
|
| 758 |
-
"call edi",
|
| 759 |
-
"pop edi",
|
| 760 |
-
"pop esi",
|
| 761 |
-
"ret",
|
| 762 |
-
);
|
| 763 |
-
}
|
| 764 |
-
|
| 765 |
pub fn install() -> Result<(), String> {
|
| 766 |
let _ = log_line("[Display] Patching DisplayBase vtables");
|
| 767 |
|
|
@@ -809,15 +313,7 @@ pub fn install() -> Result<(), String> {
|
|
| 809 |
set_clip_rect => dd_display::set_clip_rect,
|
| 810 |
is_sprite_loaded => dd_display::is_sprite_loaded,
|
| 811 |
})?;
|
| 812 |
-
let _ = log_line("[Display] DDDisplay: patched
|
| 813 |
-
|
| 814 |
-
// Hook core sprite blit (0x4F6910) via MinHook
|
| 815 |
-
let orig = crate::hook::install(
|
| 816 |
-
"BitGrid__BlitSpriteRect",
|
| 817 |
-
va::BLIT_SPRITE_RECT,
|
| 818 |
-
blit_hook_trampoline as *const (),
|
| 819 |
-
)?;
|
| 820 |
-
ORIG_BLIT.store(orig as u32, Ordering::Relaxed);
|
| 821 |
}
|
| 822 |
|
| 823 |
Ok(())
|
|
|
|
| 266 |
destructor(grid, 1);
|
| 267 |
}
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
pub fn install() -> Result<(), String> {
|
| 270 |
let _ = log_line("[Display] Patching DisplayBase vtables");
|
| 271 |
|
|
|
|
| 313 |
set_clip_rect => dd_display::set_clip_rect,
|
| 314 |
is_sprite_loaded => dd_display::is_sprite_loaded,
|
| 315 |
})?;
|
| 316 |
+
let _ = log_line("[Display] DDDisplay: patched 13 methods → Rust");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
}
|
| 318 |
|
| 319 |
Ok(())
|
crates/openwa-dll/src/replacements/frame_hook.rs
CHANGED
|
@@ -69,7 +69,7 @@ unsafe extern "stdcall" fn hook_turn_manager(turngame: u32) {
|
|
| 69 |
super::display::capture_line_snapshots();
|
| 70 |
}
|
| 71 |
if std::env::var("OPENWA_CAPTURE_BLIT_SNAPSHOTS").is_ok() {
|
| 72 |
-
super::
|
| 73 |
}
|
| 74 |
}
|
| 75 |
|
|
|
|
| 69 |
super::display::capture_line_snapshots();
|
| 70 |
}
|
| 71 |
if std::env::var("OPENWA_CAPTURE_BLIT_SNAPSHOTS").is_ok() {
|
| 72 |
+
super::bitgrid::capture_blit_snapshots();
|
| 73 |
}
|
| 74 |
}
|
| 75 |
|
crates/openwa-dll/src/replacements/mod.rs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
mod config;
|
| 2 |
mod ddgame_init;
|
| 3 |
pub(crate) mod debug_utils;
|
|
@@ -45,6 +46,7 @@ pub fn install_all() -> Result<(), String> {
|
|
| 45 |
|
| 46 |
// Normal mode: install all hooks
|
| 47 |
replay_test::install()?;
|
|
|
|
| 48 |
display::install()?;
|
| 49 |
game_session::install()?;
|
| 50 |
hardware_init::install()?;
|
|
|
|
| 1 |
+
mod bitgrid;
|
| 2 |
mod config;
|
| 3 |
mod ddgame_init;
|
| 4 |
pub(crate) mod debug_utils;
|
|
|
|
| 46 |
|
| 47 |
// Normal mode: install all hooks
|
| 48 |
replay_test::install()?;
|
| 49 |
+
bitgrid::install()?;
|
| 50 |
display::install()?;
|
| 51 |
game_session::install()?;
|
| 52 |
hardware_init::install()?;
|