paavohtl Claude Opus 4.6 (1M context) commited on
Commit
52cf816
·
1 Parent(s): 31262e9

refactor: move bitgrid blit hook and snapshots to own DLL module

Browse files

Move 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 12 methods → Rust");
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::display::capture_blit_snapshots();
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()?;