paavohtl Claude Opus 4.6 (1M context) commited on
Commit
189f2a5
·
1 Parent(s): 1bf3802

feat: port non-8bpp blit and introduce PixelGridMut for sound FFI

Browse files

Port all remaining paths in BitGrid__BlitSpriteRect (0x4F6910) to Rust,
eliminating the fallthrough to the original WA function:

- 1-bit byte-aligned fast path: memcpy/OR/nop for modes 0-3 (collision masks)
- Generic per-pixel fallback (port of FUN_004f80c0): modes 0-5 for any
cells_per_unit, handling unaligned 1-bit, erase, and collision test
- Orientation adjustment logic ported from the original function

Also introduce PixelGridMut<'a> (data: &mut [u8]) to replace the unsound
Vec::from_raw_parts hack used to wrap WA-allocated memory in PixelGrid.
All blit functions now accept PixelGridMut, and the three DLL bridge sites
use slice::from_raw_parts_mut instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/display/line_draw.rs CHANGED
@@ -46,6 +46,41 @@ pub struct PixelGrid {
46
  pub clip_bottom: u32,
47
  }
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  impl PixelGrid {
50
  /// Create a new zeroed pixel grid with default clip rect covering the full surface.
51
  pub fn new(width: u32, height: u32) -> Self {
@@ -63,6 +98,20 @@ impl PixelGrid {
63
  }
64
  }
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  /// Clear all pixels to zero.
67
  pub fn clear(&mut self) {
68
  self.data.fill(0);
 
46
  pub clip_bottom: u32,
47
  }
48
 
49
+ /// Borrowed mutable view into an 8bpp pixel buffer.
50
+ ///
51
+ /// Same layout as `PixelGrid`, but `data` is a `&mut [u8]` instead of `Vec<u8>`.
52
+ /// Used when the buffer is owned externally (e.g., WA's BitGrid data) and wrapping
53
+ /// it in a `Vec` would be unsound. All blit functions accept this type.
54
+ pub struct PixelGridMut<'a> {
55
+ pub data: &'a mut [u8],
56
+ pub width: u32,
57
+ pub height: u32,
58
+ pub row_stride: u32,
59
+ pub clip_left: u32,
60
+ pub clip_top: u32,
61
+ pub clip_right: u32,
62
+ pub clip_bottom: u32,
63
+ }
64
+
65
+ impl<'a> PixelGridMut<'a> {
66
+ /// Create a new `PixelGridMut` with a shorter lifetime, allowing the
67
+ /// original to be reused afterward. Equivalent to `&mut *self` for
68
+ /// a by-value type — needed when passing to functions that consume
69
+ /// `PixelGridMut` inside a loop.
70
+ pub fn reborrow(&mut self) -> PixelGridMut<'_> {
71
+ PixelGridMut {
72
+ data: self.data,
73
+ width: self.width,
74
+ height: self.height,
75
+ row_stride: self.row_stride,
76
+ clip_left: self.clip_left,
77
+ clip_top: self.clip_top,
78
+ clip_right: self.clip_right,
79
+ clip_bottom: self.clip_bottom,
80
+ }
81
+ }
82
+ }
83
+
84
  impl PixelGrid {
85
  /// Create a new zeroed pixel grid with default clip rect covering the full surface.
86
  pub fn new(width: u32, height: u32) -> Self {
 
98
  }
99
  }
100
 
101
+ /// Borrow this grid as a `PixelGridMut` — the type accepted by blit functions.
102
+ pub fn as_grid_mut(&mut self) -> PixelGridMut<'_> {
103
+ PixelGridMut {
104
+ data: &mut self.data,
105
+ width: self.width,
106
+ height: self.height,
107
+ row_stride: self.row_stride,
108
+ clip_left: self.clip_left,
109
+ clip_top: self.clip_top,
110
+ clip_right: self.clip_right,
111
+ clip_bottom: self.clip_bottom,
112
+ }
113
+ }
114
+
115
  /// Clear all pixels to zero.
116
  pub fn clear(&mut self) {
117
  self.data.fill(0);
crates/openwa-core/src/display/sprite_blit.rs CHANGED
@@ -22,7 +22,7 @@
22
  //! └── Mode 3: subtractive color mix
23
  //! ```
24
 
25
- use super::line_draw::PixelGrid;
26
 
27
  /// Source surface for blit operations.
28
  ///
@@ -135,7 +135,7 @@ impl BlitBlend {
135
  ///
136
  /// Returns `true` if any pixels were drawn.
137
  pub fn blit_sprite_rect(
138
- dst: &mut PixelGrid,
139
  src: &BlitSource,
140
  dst_x: i32,
141
  dst_y: i32,
@@ -205,7 +205,7 @@ pub fn blit_sprite_rect(
205
  vis_bottom,
206
  );
207
  blit_copy(
208
- dst, src, vis_left, vis_top, vis_w, vis_h, sx_start, sy_start, 1, 1,
209
  );
210
  }
211
  BlitBlend::ColorTable => {
@@ -228,12 +228,12 @@ pub fn blit_sprite_rect(
228
  let table = color_table.unwrap_or(&IDENTITY_TABLE);
229
  if swap_axes {
230
  blit_color_table_swapped(
231
- dst, src, vis_left, vis_top, vis_w, vis_h, sx_start, sy_start, sx_step,
232
  sy_step, table,
233
  );
234
  } else {
235
  blit_color_table(
236
- dst, src, vis_left, vis_top, vis_w, vis_h, sx_start, sy_start, sx_step,
237
  sy_step, table,
238
  );
239
  }
@@ -254,7 +254,7 @@ pub fn blit_sprite_rect(
254
  vis_bottom,
255
  );
256
  blit_masked(
257
- dst,
258
  src,
259
  vis_left,
260
  vis_top,
@@ -284,7 +284,7 @@ pub fn blit_sprite_rect(
284
  vis_bottom,
285
  );
286
  blit_masked(
287
- dst,
288
  src,
289
  vis_left,
290
  vis_top,
@@ -381,7 +381,7 @@ fn orientation_steps(orientation: BlitOrientation) -> (i32, i32, bool) {
381
  ///
382
  /// Source is scanned with sx advancing per dst X, sy advancing per dst Y.
383
  fn blit_copy(
384
- dst: &mut PixelGrid,
385
  src: &BlitSource,
386
  vis_left: i32,
387
  vis_top: i32,
@@ -423,7 +423,7 @@ fn blit_copy(
423
  /// Each non-zero source pixel is looked up in the color table before writing.
424
  /// Zero (transparent) pixels are skipped.
425
  fn blit_color_table(
426
- dst: &mut PixelGrid,
427
  src: &BlitSource,
428
  vis_left: i32,
429
  vis_top: i32,
@@ -458,7 +458,7 @@ fn blit_color_table(
458
 
459
  /// Color-table blend blit — 90° rotated orientations (axes swapped).
460
  fn blit_color_table_swapped(
461
- dst: &mut PixelGrid,
462
  src: &BlitSource,
463
  vis_left: i32,
464
  vis_top: i32,
@@ -495,7 +495,7 @@ fn blit_color_table_swapped(
495
  /// Copies source pixels to destination where the predicate `should_write(src, dst)` is true.
496
  /// Used by modes 2 (additive) and 3 (subtractive).
497
  fn blit_masked(
498
- dst: &mut PixelGrid,
499
  src: &BlitSource,
500
  vis_left: i32,
501
  vis_top: i32,
@@ -551,6 +551,262 @@ pub fn pixel_grid_from_indexed(width: u32, height: u32, pixels: &[u8]) -> PixelG
551
  grid
552
  }
553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  // ---------------------------------------------------------------------------
555
  // Stippled blit (checkerboard pattern)
556
  // ---------------------------------------------------------------------------
@@ -567,7 +823,7 @@ pub fn pixel_grid_from_indexed(width: u32, height: u32, pixels: &[u8]) -> PixelG
567
  /// Only non-zero source pixels are drawn (transparent = 0).
568
  /// Pixels outside the destination clip rect are skipped.
569
  pub fn blit_stippled(
570
- dst: &mut PixelGrid,
571
  src: &BlitSource<'_>,
572
  dst_x: i32,
573
  dst_y: i32,
@@ -636,7 +892,7 @@ pub fn blit_stippled(
636
  /// `flags` is the blit flags word (orientation + blend mode) passed through
637
  /// to each individual `blit_sprite_rect` call.
638
  pub fn blit_tiled(
639
- dst: &mut PixelGrid,
640
  src: &BlitSource<'_>,
641
  initial_x: i32,
642
  dst_y: i32,
@@ -666,7 +922,7 @@ pub fn blit_tiled(
666
  // Tile rightward until past clip_right
667
  while x < clip_right {
668
  blit_sprite_rect(
669
- dst,
670
  src,
671
  x,
672
  dst_y,
@@ -711,7 +967,7 @@ mod tests {
711
  let mut dst = PixelGrid::new(16, 16);
712
 
713
  blit_sprite_rect(
714
- &mut dst,
715
  &src,
716
  4,
717
  4, // dst position
@@ -744,7 +1000,7 @@ mod tests {
744
  let mut dst_mirror = PixelGrid::new(16, 16);
745
 
746
  blit_sprite_rect(
747
- &mut dst_normal,
748
  &src,
749
  4,
750
  4,
@@ -757,7 +1013,7 @@ mod tests {
757
  BlitBlend::Copy,
758
  );
759
  blit_sprite_rect(
760
- &mut dst_mirror,
761
  &src,
762
  4,
763
  4,
@@ -784,7 +1040,7 @@ mod tests {
784
  let mut dst = PixelGrid::new(16, 16);
785
 
786
  blit_sprite_rect(
787
- &mut dst,
788
  &src,
789
  4,
790
  4,
@@ -811,7 +1067,7 @@ mod tests {
811
  let mut dst = PixelGrid::new(16, 16);
812
 
813
  blit_sprite_rect(
814
- &mut dst,
815
  &src,
816
  4,
817
  4,
@@ -841,7 +1097,7 @@ mod tests {
841
  dst.clip_top = 8;
842
 
843
  blit_sprite_rect(
844
- &mut dst,
845
  &src,
846
  4,
847
  4,
@@ -877,7 +1133,7 @@ mod tests {
877
  table[0] = 0; // transparent stays 0
878
 
879
  blit_sprite_rect(
880
- &mut dst,
881
  &src,
882
  4,
883
  4,
@@ -912,7 +1168,7 @@ mod tests {
912
  dst.data.fill(99); // background
913
 
914
  blit_sprite_rect(
915
- &mut dst,
916
  &src,
917
  0,
918
  0,
@@ -948,7 +1204,7 @@ mod tests {
948
  lut[4] = 40;
949
 
950
  blit_sprite_rect(
951
- &mut dst,
952
  &src,
953
  4,
954
  4,
@@ -983,7 +1239,7 @@ mod tests {
983
  lut[4] = 40;
984
 
985
  blit_sprite_rect(
986
- &mut dst,
987
  &src,
988
  4,
989
  4,
@@ -1017,7 +1273,7 @@ mod tests {
1017
  }
1018
 
1019
  blit_sprite_rect(
1020
- &mut dst,
1021
  &src,
1022
  4,
1023
  4,
@@ -1055,7 +1311,7 @@ mod tests {
1055
  }
1056
 
1057
  blit_sprite_rect(
1058
- &mut dst,
1059
  &src,
1060
  4,
1061
  4,
@@ -1102,7 +1358,7 @@ mod tests {
1102
  dst.data.fill(99); // all non-zero
1103
 
1104
  blit_sprite_rect(
1105
- &mut dst,
1106
  &src,
1107
  0,
1108
  0,
@@ -1241,7 +1497,7 @@ mod tests {
1241
  let mut dst = PixelGrid::new(canvas_w, canvas_h);
1242
 
1243
  blit_sprite_rect(
1244
- &mut dst,
1245
  &src,
1246
  16,
1247
  16,
@@ -1279,7 +1535,7 @@ mod tests {
1279
 
1280
  let mut dst = PixelGrid::new(sprite.width, sprite.height);
1281
  blit_sprite_rect(
1282
- &mut dst,
1283
  &src,
1284
  0,
1285
  0,
@@ -1311,7 +1567,7 @@ mod tests {
1311
  // Blit at (-16, -16) so only bottom-right portion is visible
1312
  let mut dst = PixelGrid::new(sprite.width, sprite.height);
1313
  blit_sprite_rect(
1314
- &mut dst,
1315
  &src,
1316
  -16,
1317
  -16,
@@ -1344,7 +1600,7 @@ mod tests {
1344
  dst.data.fill(42); // background color
1345
 
1346
  blit_sprite_rect(
1347
- &mut dst,
1348
  &src,
1349
  0,
1350
  0,
@@ -1397,7 +1653,7 @@ mod tests {
1397
  dst.data.fill(42);
1398
 
1399
  blit_sprite_rect(
1400
- &mut dst,
1401
  &src,
1402
  0,
1403
  0,
@@ -1504,7 +1760,7 @@ mod tests {
1504
  dst.data.fill(bg_fill);
1505
 
1506
  blit_sprite_rect(
1507
- &mut dst,
1508
  &src,
1509
  dst_x,
1510
  dst_y,
@@ -1759,7 +2015,18 @@ mod tests {
1759
  let mut dst = PixelGrid::new(canvas_w, canvas_h);
1760
  dst.data.fill(77); // background matching DLL capture
1761
 
1762
- blit_stippled(&mut dst, &src, 16, 16, sw, sh, 0, 0, stipple_mode, parity);
 
 
 
 
 
 
 
 
 
 
 
1763
 
1764
  assert_matches_snapshot(&dst, snap_name);
1765
  }
@@ -1803,8 +2070,16 @@ mod tests {
1803
  let clip_right = canvas_w as i32;
1804
 
1805
  blit_tiled(
1806
- &mut dst, &src, 8, // initial_x matching DLL capture
1807
- 16, sw, sh, clip_left, clip_right, None, flags,
 
 
 
 
 
 
 
 
1808
  );
1809
 
1810
  assert_matches_snapshot(&dst, snap_name);
 
22
  //! └── Mode 3: subtractive color mix
23
  //! ```
24
 
25
+ use super::line_draw::{PixelGrid, PixelGridMut};
26
 
27
  /// Source surface for blit operations.
28
  ///
 
135
  ///
136
  /// Returns `true` if any pixels were drawn.
137
  pub fn blit_sprite_rect(
138
+ mut dst: PixelGridMut<'_>,
139
  src: &BlitSource,
140
  dst_x: i32,
141
  dst_y: i32,
 
205
  vis_bottom,
206
  );
207
  blit_copy(
208
+ &mut dst, src, vis_left, vis_top, vis_w, vis_h, sx_start, sy_start, 1, 1,
209
  );
210
  }
211
  BlitBlend::ColorTable => {
 
228
  let table = color_table.unwrap_or(&IDENTITY_TABLE);
229
  if swap_axes {
230
  blit_color_table_swapped(
231
+ &mut dst, src, vis_left, vis_top, vis_w, vis_h, sx_start, sy_start, sx_step,
232
  sy_step, table,
233
  );
234
  } else {
235
  blit_color_table(
236
+ &mut dst, src, vis_left, vis_top, vis_w, vis_h, sx_start, sy_start, sx_step,
237
  sy_step, table,
238
  );
239
  }
 
254
  vis_bottom,
255
  );
256
  blit_masked(
257
+ &mut dst,
258
  src,
259
  vis_left,
260
  vis_top,
 
284
  vis_bottom,
285
  );
286
  blit_masked(
287
+ &mut dst,
288
  src,
289
  vis_left,
290
  vis_top,
 
381
  ///
382
  /// Source is scanned with sx advancing per dst X, sy advancing per dst Y.
383
  fn blit_copy(
384
+ dst: &mut PixelGridMut<'_>,
385
  src: &BlitSource,
386
  vis_left: i32,
387
  vis_top: i32,
 
423
  /// Each non-zero source pixel is looked up in the color table before writing.
424
  /// Zero (transparent) pixels are skipped.
425
  fn blit_color_table(
426
+ dst: &mut PixelGridMut<'_>,
427
  src: &BlitSource,
428
  vis_left: i32,
429
  vis_top: i32,
 
458
 
459
  /// Color-table blend blit — 90° rotated orientations (axes swapped).
460
  fn blit_color_table_swapped(
461
+ dst: &mut PixelGridMut<'_>,
462
  src: &BlitSource,
463
  vis_left: i32,
464
  vis_top: i32,
 
495
  /// Copies source pixels to destination where the predicate `should_write(src, dst)` is true.
496
  /// Used by modes 2 (additive) and 3 (subtractive).
497
  fn blit_masked(
498
+ dst: &mut PixelGridMut<'_>,
499
  src: &BlitSource,
500
  vis_left: i32,
501
  vis_top: i32,
 
551
  grid
552
  }
553
 
554
+ // ---------------------------------------------------------------------------
555
+ // 1-bit blit (collision masks)
556
+ // ---------------------------------------------------------------------------
557
+
558
+ /// Get a single bit from a 1-bit BitGrid data buffer.
559
+ ///
560
+ /// Bit layout matches WA's CollisionBitGrid (0x4F5D70):
561
+ /// byte = data[y * stride + (x >> 3)], bit = (byte >> (x & 7)) & 1
562
+ #[inline]
563
+ fn bit_get(data: &[u8], stride: u32, x: i32, y: i32) -> u8 {
564
+ let byte = data[(y as u32 * stride + (x as u32 >> 3)) as usize];
565
+ (byte >> (x & 7)) & 1
566
+ }
567
+
568
+ /// Set a single bit in a 1-bit BitGrid data buffer.
569
+ ///
570
+ /// Matches WA's CollisionBitGrid__Put (0x4F5DA0):
571
+ /// clear the bit, then OR in the new value.
572
+ #[inline]
573
+ fn bit_put(data: &mut [u8], stride: u32, x: i32, y: i32, value: u8) {
574
+ let idx = (y as u32 * stride + (x as u32 >> 3)) as usize;
575
+ let bit = (x & 7) as u8;
576
+ data[idx] = (data[idx] & !(1 << bit)) | (((value != 0) as u8) << bit);
577
+ }
578
+
579
+ /// Byte-aligned 1-bit blit fast path.
580
+ ///
581
+ /// Port of the optimized 1-bit path in BitGrid__BlitSpriteRect (0x4F6910).
582
+ /// Requires: both surfaces cpp==1, clip_left/clip_right/src_x all byte-aligned
583
+ /// (divisible by 8), no color table, Normal orientation.
584
+ ///
585
+ /// Blend modes:
586
+ /// - 0 (Copy): memcpy bytes
587
+ /// - 1 (ColorTable) / 3 (Subtractive): bitwise OR (both simplify to OR for 1-bit)
588
+ /// - 2 (Additive): no-op (AND of single bits is identity when both are 1)
589
+ pub fn blit_1bit_aligned(
590
+ dst: &mut [u8],
591
+ dst_stride: u32,
592
+ src: &[u8],
593
+ src_stride: u32,
594
+ clip_left: i32,
595
+ clip_top: i32,
596
+ clip_right: i32,
597
+ clip_bottom: i32,
598
+ src_x: i32,
599
+ src_y: i32,
600
+ blend_mode: u32,
601
+ ) {
602
+ let byte_width = ((clip_right - clip_left) >> 3) as usize;
603
+ let dst_byte_x = (clip_left >> 3) as usize;
604
+ let src_byte_x = (src_x >> 3) as usize;
605
+
606
+ match blend_mode {
607
+ 0 => {
608
+ // Mode 0 (Copy): memcpy per row
609
+ for row in clip_top..clip_bottom {
610
+ let dst_off = row as usize * dst_stride as usize + dst_byte_x;
611
+ let src_row = src_y + (row - clip_top);
612
+ let src_off = src_row as usize * src_stride as usize + src_byte_x;
613
+ dst[dst_off..dst_off + byte_width]
614
+ .copy_from_slice(&src[src_off..src_off + byte_width]);
615
+ }
616
+ }
617
+ 2 => {
618
+ // Mode 2 (Additive): no-op for 1-bit
619
+ }
620
+ _ => {
621
+ // Mode 1 (ColorTable) and 3 (Subtractive): bitwise OR
622
+ // Both simplify to OR for single-bit values.
623
+ for row in clip_top..clip_bottom {
624
+ let dst_off = row as usize * dst_stride as usize + dst_byte_x;
625
+ let src_row = src_y + (row - clip_top);
626
+ let src_off = src_row as usize * src_stride as usize + src_byte_x;
627
+ for i in 0..byte_width {
628
+ dst[dst_off + i] |= src[src_off + i];
629
+ }
630
+ }
631
+ }
632
+ }
633
+ }
634
+
635
+ /// Generic per-pixel blit fallback.
636
+ ///
637
+ /// Port of FUN_004f80c0 — handles all blend modes (0-5) for any cells_per_unit
638
+ /// by operating one pixel at a time. Slower than the specialized fast paths but
639
+ /// handles unaligned 1-bit blits, oriented blits that fell through, and blend
640
+ /// modes 4 (erase) and 5 (collision test).
641
+ ///
642
+ /// Parameters match the original's calling convention:
643
+ /// - clip_left..clip_right, clip_top..clip_bottom: destination region (already clipped)
644
+ /// - src_x: source X corresponding to clip_left
645
+ /// - src_y: source Y corresponding to clip_top
646
+ /// - blend_mode: 0=copy, 1=color_table, 2=additive, 3=subtractive, 4=erase, 5=collision_test
647
+ ///
648
+ /// Returns 1 normally, or for mode 5: 1 if collision detected, 0 if not.
649
+ pub fn blit_generic_perpixel(
650
+ dst: &mut [u8],
651
+ dst_stride: u32,
652
+ dst_cpp: u32,
653
+ src: &[u8],
654
+ src_stride: u32,
655
+ src_cpp: u32,
656
+ clip_left: i32,
657
+ clip_top: i32,
658
+ clip_right: i32,
659
+ clip_bottom: i32,
660
+ src_x: i32,
661
+ src_y: i32,
662
+ color_table: Option<&[u8; 256]>,
663
+ blend_mode: u32,
664
+ ) -> u32 {
665
+ // Pixel accessors based on cells_per_unit
666
+ #[inline]
667
+ fn get_pixel(data: &[u8], stride: u32, cpp: u32, x: i32, y: i32) -> u8 {
668
+ if cpp == 1 {
669
+ bit_get(data, stride, x, y)
670
+ } else {
671
+ data[y as usize * stride as usize + x as usize]
672
+ }
673
+ }
674
+
675
+ #[inline]
676
+ fn put_pixel(data: &mut [u8], stride: u32, cpp: u32, x: i32, y: i32, value: u8) {
677
+ if cpp == 1 {
678
+ bit_put(data, stride, x, y, value);
679
+ } else {
680
+ data[y as usize * stride as usize + x as usize] = value;
681
+ }
682
+ }
683
+
684
+ let src_x_offset = src_x - clip_left;
685
+
686
+ match blend_mode {
687
+ 0 => {
688
+ // Copy: get from src, put to dst
689
+ for y in clip_top..clip_bottom {
690
+ for x in clip_left..clip_right {
691
+ let pixel = get_pixel(
692
+ src,
693
+ src_stride,
694
+ src_cpp,
695
+ src_x_offset + x,
696
+ src_y + (y - clip_top),
697
+ );
698
+ put_pixel(dst, dst_stride, dst_cpp, x, y, pixel);
699
+ }
700
+ }
701
+ 1
702
+ }
703
+ 1 => {
704
+ // Color table blend — per-pixel with optional LUT
705
+ for y in clip_top..clip_bottom {
706
+ for x in clip_left..clip_right {
707
+ let pixel = get_pixel(
708
+ src,
709
+ src_stride,
710
+ src_cpp,
711
+ src_x_offset + x,
712
+ src_y + (y - clip_top),
713
+ );
714
+ if pixel != 0 {
715
+ let value = match color_table {
716
+ Some(table) => table[pixel as usize],
717
+ None => pixel,
718
+ };
719
+ put_pixel(dst, dst_stride, dst_cpp, x, y, value);
720
+ }
721
+ }
722
+ }
723
+ 1
724
+ }
725
+ 2 => {
726
+ // Additive: copy src where both src and dst non-zero
727
+ for y in clip_top..clip_bottom {
728
+ for x in clip_left..clip_right {
729
+ let src_pixel = get_pixel(
730
+ src,
731
+ src_stride,
732
+ src_cpp,
733
+ src_x_offset + x,
734
+ src_y + (y - clip_top),
735
+ );
736
+ if src_pixel != 0 {
737
+ let dst_pixel = get_pixel(dst, dst_stride, dst_cpp, x, y);
738
+ if dst_pixel != 0 {
739
+ put_pixel(dst, dst_stride, dst_cpp, x, y, src_pixel);
740
+ }
741
+ }
742
+ }
743
+ }
744
+ 1
745
+ }
746
+ 3 => {
747
+ // Subtractive: copy src where src non-zero and dst zero
748
+ for y in clip_top..clip_bottom {
749
+ for x in clip_left..clip_right {
750
+ let src_pixel = get_pixel(
751
+ src,
752
+ src_stride,
753
+ src_cpp,
754
+ src_x_offset + x,
755
+ src_y + (y - clip_top),
756
+ );
757
+ if src_pixel != 0 {
758
+ let dst_pixel = get_pixel(dst, dst_stride, dst_cpp, x, y);
759
+ if dst_pixel == 0 {
760
+ put_pixel(dst, dst_stride, dst_cpp, x, y, src_pixel);
761
+ }
762
+ }
763
+ }
764
+ }
765
+ 1
766
+ }
767
+ 4 => {
768
+ // Erase: clear dst where src non-zero
769
+ for y in clip_top..clip_bottom {
770
+ for x in clip_left..clip_right {
771
+ let src_pixel = get_pixel(
772
+ src,
773
+ src_stride,
774
+ src_cpp,
775
+ src_x_offset + x,
776
+ src_y + (y - clip_top),
777
+ );
778
+ if src_pixel != 0 {
779
+ put_pixel(dst, dst_stride, dst_cpp, x, y, 0);
780
+ }
781
+ }
782
+ }
783
+ 1
784
+ }
785
+ 5 => {
786
+ // Collision test: return 1 if any pixel overlaps (both non-zero)
787
+ for y in clip_top..clip_bottom {
788
+ for x in clip_left..clip_right {
789
+ let dst_pixel = get_pixel(dst, dst_stride, dst_cpp, x, y);
790
+ if dst_pixel != 0 {
791
+ let src_pixel = get_pixel(
792
+ src,
793
+ src_stride,
794
+ src_cpp,
795
+ src_x_offset + x,
796
+ src_y + (y - clip_top),
797
+ );
798
+ if src_pixel != 0 {
799
+ return 1;
800
+ }
801
+ }
802
+ }
803
+ }
804
+ 0
805
+ }
806
+ _ => 1,
807
+ }
808
+ }
809
+
810
  // ---------------------------------------------------------------------------
811
  // Stippled blit (checkerboard pattern)
812
  // ---------------------------------------------------------------------------
 
823
  /// Only non-zero source pixels are drawn (transparent = 0).
824
  /// Pixels outside the destination clip rect are skipped.
825
  pub fn blit_stippled(
826
+ dst: &mut PixelGridMut<'_>,
827
  src: &BlitSource<'_>,
828
  dst_x: i32,
829
  dst_y: i32,
 
892
  /// `flags` is the blit flags word (orientation + blend mode) passed through
893
  /// to each individual `blit_sprite_rect` call.
894
  pub fn blit_tiled(
895
+ dst: &mut PixelGridMut<'_>,
896
  src: &BlitSource<'_>,
897
  initial_x: i32,
898
  dst_y: i32,
 
922
  // Tile rightward until past clip_right
923
  while x < clip_right {
924
  blit_sprite_rect(
925
+ dst.reborrow(),
926
  src,
927
  x,
928
  dst_y,
 
967
  let mut dst = PixelGrid::new(16, 16);
968
 
969
  blit_sprite_rect(
970
+ dst.as_grid_mut(),
971
  &src,
972
  4,
973
  4, // dst position
 
1000
  let mut dst_mirror = PixelGrid::new(16, 16);
1001
 
1002
  blit_sprite_rect(
1003
+ dst_normal.as_grid_mut(),
1004
  &src,
1005
  4,
1006
  4,
 
1013
  BlitBlend::Copy,
1014
  );
1015
  blit_sprite_rect(
1016
+ dst_mirror.as_grid_mut(),
1017
  &src,
1018
  4,
1019
  4,
 
1040
  let mut dst = PixelGrid::new(16, 16);
1041
 
1042
  blit_sprite_rect(
1043
+ dst.as_grid_mut(),
1044
  &src,
1045
  4,
1046
  4,
 
1067
  let mut dst = PixelGrid::new(16, 16);
1068
 
1069
  blit_sprite_rect(
1070
+ dst.as_grid_mut(),
1071
  &src,
1072
  4,
1073
  4,
 
1097
  dst.clip_top = 8;
1098
 
1099
  blit_sprite_rect(
1100
+ dst.as_grid_mut(),
1101
  &src,
1102
  4,
1103
  4,
 
1133
  table[0] = 0; // transparent stays 0
1134
 
1135
  blit_sprite_rect(
1136
+ dst.as_grid_mut(),
1137
  &src,
1138
  4,
1139
  4,
 
1168
  dst.data.fill(99); // background
1169
 
1170
  blit_sprite_rect(
1171
+ dst.as_grid_mut(),
1172
  &src,
1173
  0,
1174
  0,
 
1204
  lut[4] = 40;
1205
 
1206
  blit_sprite_rect(
1207
+ dst.as_grid_mut(),
1208
  &src,
1209
  4,
1210
  4,
 
1239
  lut[4] = 40;
1240
 
1241
  blit_sprite_rect(
1242
+ dst.as_grid_mut(),
1243
  &src,
1244
  4,
1245
  4,
 
1273
  }
1274
 
1275
  blit_sprite_rect(
1276
+ dst.as_grid_mut(),
1277
  &src,
1278
  4,
1279
  4,
 
1311
  }
1312
 
1313
  blit_sprite_rect(
1314
+ dst.as_grid_mut(),
1315
  &src,
1316
  4,
1317
  4,
 
1358
  dst.data.fill(99); // all non-zero
1359
 
1360
  blit_sprite_rect(
1361
+ dst.as_grid_mut(),
1362
  &src,
1363
  0,
1364
  0,
 
1497
  let mut dst = PixelGrid::new(canvas_w, canvas_h);
1498
 
1499
  blit_sprite_rect(
1500
+ dst.as_grid_mut(),
1501
  &src,
1502
  16,
1503
  16,
 
1535
 
1536
  let mut dst = PixelGrid::new(sprite.width, sprite.height);
1537
  blit_sprite_rect(
1538
+ dst.as_grid_mut(),
1539
  &src,
1540
  0,
1541
  0,
 
1567
  // Blit at (-16, -16) so only bottom-right portion is visible
1568
  let mut dst = PixelGrid::new(sprite.width, sprite.height);
1569
  blit_sprite_rect(
1570
+ dst.as_grid_mut(),
1571
  &src,
1572
  -16,
1573
  -16,
 
1600
  dst.data.fill(42); // background color
1601
 
1602
  blit_sprite_rect(
1603
+ dst.as_grid_mut(),
1604
  &src,
1605
  0,
1606
  0,
 
1653
  dst.data.fill(42);
1654
 
1655
  blit_sprite_rect(
1656
+ dst.as_grid_mut(),
1657
  &src,
1658
  0,
1659
  0,
 
1760
  dst.data.fill(bg_fill);
1761
 
1762
  blit_sprite_rect(
1763
+ dst.as_grid_mut(),
1764
  &src,
1765
  dst_x,
1766
  dst_y,
 
2015
  let mut dst = PixelGrid::new(canvas_w, canvas_h);
2016
  dst.data.fill(77); // background matching DLL capture
2017
 
2018
+ blit_stippled(
2019
+ &mut dst.as_grid_mut(),
2020
+ &src,
2021
+ 16,
2022
+ 16,
2023
+ sw,
2024
+ sh,
2025
+ 0,
2026
+ 0,
2027
+ stipple_mode,
2028
+ parity,
2029
+ );
2030
 
2031
  assert_matches_snapshot(&dst, snap_name);
2032
  }
 
2070
  let clip_right = canvas_w as i32;
2071
 
2072
  blit_tiled(
2073
+ &mut dst.as_grid_mut(),
2074
+ &src,
2075
+ 8, // initial_x matching DLL capture
2076
+ 16,
2077
+ sw,
2078
+ sh,
2079
+ clip_left,
2080
+ clip_right,
2081
+ None,
2082
+ flags,
2083
  );
2084
 
2085
  assert_matches_snapshot(&dst, snap_name);
crates/openwa-dll/src/replacements/bitgrid.rs CHANGED
@@ -854,10 +854,13 @@ unsafe extern "C" fn blit_hook_trampoline() {
854
  );
855
  }
856
 
857
- /// Rust implementation of the core sprite blit.
858
  ///
859
- /// Handles all 8bpp blend modes: 0 (copy), 1 (color table), 2 (additive mask), 3 (subtractive mask).
860
- /// Falls through to the original WA function for non-8bpp surfaces.
 
 
 
861
  pub(crate) unsafe extern "cdecl" fn blit_impl(
862
  dst: *mut openwa_core::bitgrid::DisplayBitGrid,
863
  dst_x: i32,
@@ -870,10 +873,6 @@ pub(crate) unsafe extern "cdecl" fn blit_impl(
870
  color_table: *const u8,
871
  flags: u32,
872
  ) -> u32 {
873
- use openwa_core::display::sprite_blit::{
874
- blit_sprite_rect, BlitBlend, BlitOrientation, BlitSource,
875
- };
876
-
877
  if width == 0 || height == 0 {
878
  return 0;
879
  }
@@ -882,9 +881,9 @@ pub(crate) unsafe extern "cdecl" fn blit_impl(
882
  let src_cpp = (*src).cells_per_unit;
883
  let dst_cpp = (*dst).cells_per_unit;
884
 
885
- // Only handle 8bpp surfaces with blend modes 0-3
886
- if dst_cpp != 8 || src_cpp != 8 || blend_mode > 3 {
887
- return call_original_blit(
888
  dst,
889
  dst_x,
890
  dst_y,
@@ -898,7 +897,196 @@ pub(crate) unsafe extern "cdecl" fn blit_impl(
898
  );
899
  }
900
 
901
- // Convert color_table from raw pointer to typed reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
902
  let color_table_ref: Option<&[u8; 256]> = if !color_table.is_null() {
903
  Some(&*(color_table as *const [u8; 256]))
904
  } else {
@@ -908,7 +1096,6 @@ pub(crate) unsafe extern "cdecl" fn blit_impl(
908
  let orientation = BlitOrientation::from_flags(flags);
909
  let blend = BlitBlend::from_flags(flags);
910
 
911
- // Build source view
912
  let src_data =
913
  core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
914
  let blit_src = BlitSource {
@@ -918,29 +1105,20 @@ pub(crate) unsafe extern "cdecl" fn blit_impl(
918
  row_stride: (*src).row_stride,
919
  };
920
 
921
- // Build mutable destination view
922
- let dst_stride = (*dst).row_stride;
923
- let dst_h = (*dst).height;
924
- let dst_w = (*dst).width;
925
- let dst_data_len = (dst_stride * dst_h) as usize;
926
-
927
- // Create a temporary PixelGrid wrapping the destination BitGrid's memory
928
- let mut dst_grid = openwa_core::display::line_draw::PixelGrid {
929
- data: Vec::new(), // placeholder — we'll swap in the real data
930
- width: dst_w,
931
- height: dst_h,
932
- row_stride: dst_stride,
933
  clip_left: (*dst).clip_left,
934
  clip_top: (*dst).clip_top,
935
  clip_right: (*dst).clip_right,
936
  clip_bottom: (*dst).clip_bottom,
937
  };
938
- // Swap the real data in (avoids allocation)
939
- let mut real_data = Vec::from_raw_parts((*dst).data, dst_data_len, dst_data_len);
940
- core::mem::swap(&mut dst_grid.data, &mut real_data);
941
 
942
- let result = blit_sprite_rect(
943
- &mut dst_grid,
944
  &blit_src,
945
  dst_x,
946
  dst_y,
@@ -951,13 +1129,7 @@ pub(crate) unsafe extern "cdecl" fn blit_impl(
951
  color_table_ref,
952
  orientation,
953
  blend,
954
- );
955
-
956
- // Swap data back — we must NOT let Vec drop the BitGrid's data
957
- core::mem::swap(&mut dst_grid.data, &mut real_data);
958
- core::mem::forget(real_data); // BitGrid owns this memory
959
-
960
- result as u32
961
  }
962
 
963
  /// Stippled blit on raw BitGrid pointers.
@@ -987,23 +1159,17 @@ pub(crate) unsafe fn blit_stippled_raw(
987
  row_stride: (*src).row_stride,
988
  };
989
 
990
- let dst_stride = (*dst).row_stride;
991
- let dst_h = (*dst).height;
992
- let dst_w = (*dst).width;
993
- let dst_data_len = (dst_stride * dst_h) as usize;
994
-
995
- let mut dst_grid = openwa_core::display::line_draw::PixelGrid {
996
- data: Vec::new(),
997
- width: dst_w,
998
- height: dst_h,
999
- row_stride: dst_stride,
1000
  clip_left: (*dst).clip_left,
1001
  clip_top: (*dst).clip_top,
1002
  clip_right: (*dst).clip_right,
1003
  clip_bottom: (*dst).clip_bottom,
1004
  };
1005
- let mut real_data = Vec::from_raw_parts((*dst).data, dst_data_len, dst_data_len);
1006
- core::mem::swap(&mut dst_grid.data, &mut real_data);
1007
 
1008
  blit_stippled(
1009
  &mut dst_grid,
@@ -1017,9 +1183,6 @@ pub(crate) unsafe fn blit_stippled_raw(
1017
  stipple_mode,
1018
  parity,
1019
  );
1020
-
1021
- core::mem::swap(&mut dst_grid.data, &mut real_data);
1022
- core::mem::forget(real_data);
1023
  }
1024
 
1025
  /// Tiled blit on raw BitGrid pointers.
@@ -1048,23 +1211,17 @@ pub(crate) unsafe fn blit_tiled_raw(
1048
  row_stride: (*src).row_stride,
1049
  };
1050
 
1051
- let dst_stride = (*dst).row_stride;
1052
- let dst_h = (*dst).height;
1053
- let dst_w = (*dst).width;
1054
- let dst_data_len = (dst_stride * dst_h) as usize;
1055
-
1056
- let mut dst_grid = openwa_core::display::line_draw::PixelGrid {
1057
- data: Vec::new(),
1058
- width: dst_w,
1059
- height: dst_h,
1060
- row_stride: dst_stride,
1061
  clip_left: (*dst).clip_left,
1062
  clip_top: (*dst).clip_top,
1063
  clip_right: (*dst).clip_right,
1064
  clip_bottom: (*dst).clip_bottom,
1065
  };
1066
- let mut real_data = Vec::from_raw_parts((*dst).data, dst_data_len, dst_data_len);
1067
- core::mem::swap(&mut dst_grid.data, &mut real_data);
1068
 
1069
  blit_tiled(
1070
  &mut dst_grid,
@@ -1078,12 +1235,11 @@ pub(crate) unsafe fn blit_tiled_raw(
1078
  None, // color_table — tiled mode doesn't use one
1079
  flags,
1080
  );
1081
-
1082
- core::mem::swap(&mut dst_grid.data, &mut real_data);
1083
- core::mem::forget(real_data);
1084
  }
1085
 
1086
  /// Call the original WA blit function via trampoline.
 
 
1087
  unsafe fn call_original_blit(
1088
  dst: *mut openwa_core::bitgrid::DisplayBitGrid,
1089
  dst_x: i32,
@@ -1113,6 +1269,7 @@ unsafe fn call_original_blit(
1113
  }
1114
 
1115
  /// Naked asm to call the original blit with ESI set correctly.
 
1116
  #[unsafe(naked)]
1117
  unsafe extern "cdecl" fn call_original_blit_asm(
1118
  _dst: *mut openwa_core::bitgrid::DisplayBitGrid,
 
854
  );
855
  }
856
 
857
+ /// Rust implementation of the core sprite blit (BitGrid__BlitSpriteRect, 0x4F6910).
858
  ///
859
+ /// Handles all blend modes for both 8bpp and 1-bit surfaces:
860
+ /// - 8bpp modes 0-3: via `blit_sprite_rect` (orientation-aware, PixelGrid wrapper)
861
+ /// - 1-bit byte-aligned fast path: memcpy/OR/nop for modes 0-3 with Normal orientation
862
+ /// - Generic per-pixel fallback: handles all remaining cases (unaligned 1-bit, modes 4-5,
863
+ /// mixed cpp, etc.) via `blit_generic_perpixel`
864
  pub(crate) unsafe extern "cdecl" fn blit_impl(
865
  dst: *mut openwa_core::bitgrid::DisplayBitGrid,
866
  dst_x: i32,
 
873
  color_table: *const u8,
874
  flags: u32,
875
  ) -> u32 {
 
 
 
 
876
  if width == 0 || height == 0 {
877
  return 0;
878
  }
 
881
  let src_cpp = (*src).cells_per_unit;
882
  let dst_cpp = (*dst).cells_per_unit;
883
 
884
+ // Fast path: 8bpp surfaces with blend modes 0-3
885
+ if dst_cpp == 8 && src_cpp == 8 && blend_mode <= 3 {
886
+ return blit_8bpp(
887
  dst,
888
  dst_x,
889
  dst_y,
 
897
  );
898
  }
899
 
900
+ // --- Clipping (shared by 1-bit fast path and generic fallback) ---
901
+ // This mirrors the clipping logic at the top of BitGrid__BlitSpriteRect.
902
+ let dst_right = dst_x + width;
903
+ let dst_bottom = dst_y + height;
904
+
905
+ let clip_left = (*dst).clip_left as i32;
906
+ let clip_top = (*dst).clip_top as i32;
907
+ let clip_right = (*dst).clip_right as i32;
908
+ let clip_bottom = (*dst).clip_bottom as i32;
909
+
910
+ // Early-out: completely outside clip rect
911
+ if dst_x >= clip_right
912
+ || dst_right <= clip_left
913
+ || dst_y >= clip_bottom
914
+ || dst_bottom <= clip_top
915
+ {
916
+ return 0;
917
+ }
918
+
919
+ // Clamp visible region to clip rect
920
+ let vis_left = dst_x.max(clip_left);
921
+ let vis_right = dst_right.min(clip_right);
922
+ let vis_top = dst_y.max(clip_top);
923
+ let vis_bottom = dst_bottom.min(clip_bottom);
924
+
925
+ if vis_left >= vis_right || vis_top >= vis_bottom {
926
+ return 0;
927
+ }
928
+
929
+ // Adjust source coordinates for clipping and orientation.
930
+ // The orientation switch in the original adjusts src_x/src_y before the cpp check.
931
+ let orientation_code = (flags >> 16) & 0xFFFF;
932
+ let (adj_src_x, adj_src_y) = adjust_src_for_orientation(
933
+ src_x,
934
+ src_y,
935
+ dst_x,
936
+ dst_y,
937
+ width,
938
+ height,
939
+ vis_left,
940
+ vis_top,
941
+ vis_right,
942
+ vis_bottom,
943
+ orientation_code,
944
+ );
945
+
946
+ // Build slices from raw BitGrid data pointers
947
+ let dst_data =
948
+ core::slice::from_raw_parts_mut((*dst).data, ((*dst).row_stride * (*dst).height) as usize);
949
+ let src_data =
950
+ core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
951
+
952
+ // 1-bit byte-aligned fast path
953
+ if dst_cpp == 1 && src_cpp == 1 {
954
+ let left_aligned = (vis_left & 7) == 0;
955
+ let right_aligned = (vis_right & 7) == 0;
956
+ let src_aligned = (adj_src_x & 7) == 0;
957
+
958
+ if left_aligned && right_aligned && src_aligned && color_table.is_null() && flags < 4 {
959
+ openwa_core::display::sprite_blit::blit_1bit_aligned(
960
+ dst_data,
961
+ (*dst).row_stride,
962
+ src_data,
963
+ (*src).row_stride,
964
+ vis_left,
965
+ vis_top,
966
+ vis_right,
967
+ vis_bottom,
968
+ adj_src_x,
969
+ adj_src_y,
970
+ blend_mode,
971
+ );
972
+ return 1;
973
+ }
974
+ }
975
+
976
+ // Convert color_table pointer to typed reference for the generic path
977
+ let color_table_ref: Option<&[u8; 256]> = if !color_table.is_null() {
978
+ Some(&*(color_table as *const [u8; 256]))
979
+ } else {
980
+ None
981
+ };
982
+
983
+ // Generic per-pixel fallback — handles all remaining cases
984
+ openwa_core::display::sprite_blit::blit_generic_perpixel(
985
+ dst_data,
986
+ (*dst).row_stride,
987
+ dst_cpp,
988
+ src_data,
989
+ (*src).row_stride,
990
+ src_cpp,
991
+ vis_left,
992
+ vis_top,
993
+ vis_right,
994
+ vis_bottom,
995
+ adj_src_x,
996
+ adj_src_y,
997
+ color_table_ref,
998
+ blend_mode,
999
+ )
1000
+ }
1001
+
1002
+ /// Adjust source coordinates for clipping and orientation.
1003
+ ///
1004
+ /// Port of the orientation switch in BitGrid__BlitSpriteRect (0x4F69D3..0x4F6A9D).
1005
+ /// Maps the 16 orientation codes (0-15) to 8 unique source adjustments.
1006
+ fn adjust_src_for_orientation(
1007
+ src_x: i32,
1008
+ src_y: i32,
1009
+ dst_x: i32,
1010
+ dst_y: i32,
1011
+ width: i32,
1012
+ height: i32,
1013
+ vis_left: i32,
1014
+ vis_top: i32,
1015
+ vis_right: i32,
1016
+ vis_bottom: i32,
1017
+ orientation: u32,
1018
+ ) -> (i32, i32) {
1019
+ match orientation {
1020
+ 1 | 10 => {
1021
+ // MirrorX: src_x from right side, src_y from top
1022
+ let sx = src_x + (dst_x - vis_right) + width;
1023
+ let sy = src_y + (vis_top - dst_y);
1024
+ (sx, sy)
1025
+ }
1026
+ 2 | 9 => {
1027
+ // MirrorY: src_x from left, src_y from bottom
1028
+ let sx = src_x + (vis_left - dst_x);
1029
+ let sy = src_y + (dst_y - vis_bottom) + height;
1030
+ (sx, sy)
1031
+ }
1032
+ 3 | 8 => {
1033
+ // MirrorXY: src_x from right, src_y from bottom
1034
+ let sx = src_x + (dst_x - vis_right) + width;
1035
+ let sy = src_y + (dst_y - vis_bottom) + height;
1036
+ (sx, sy)
1037
+ }
1038
+ 4 | 15 => {
1039
+ // Rotate90: src_x tracks dst_y from bottom, src_y tracks dst_x from left
1040
+ let sx = src_x + (dst_y - vis_bottom) + height;
1041
+ let sy = src_y + (vis_left - dst_x);
1042
+ (sx, sy)
1043
+ }
1044
+ 5 | 14 => {
1045
+ // Rotate90+MirrorX
1046
+ let sx = src_x + (dst_y - vis_bottom) + height;
1047
+ let sy = src_y + (dst_x - vis_right) + width;
1048
+ (sx, sy)
1049
+ }
1050
+ 6 | 13 => {
1051
+ // Rotate90+MirrorY
1052
+ let sx = src_x + (vis_top - dst_y);
1053
+ let sy = src_y + (vis_left - dst_x);
1054
+ (sx, sy)
1055
+ }
1056
+ 7 | 12 => {
1057
+ // Rotate90+MirrorXY
1058
+ let sx = src_x + (vis_top - dst_y);
1059
+ let sy = src_y + (dst_x - vis_right) + width;
1060
+ (sx, sy)
1061
+ }
1062
+ _ => {
1063
+ // Normal (0, 11, or any default)
1064
+ let sx = src_x + (vis_left - dst_x);
1065
+ let sy = src_y + (vis_top - dst_y);
1066
+ (sx, sy)
1067
+ }
1068
+ }
1069
+ }
1070
+
1071
+ /// 8bpp blit path — extracted from the original blit_impl for clarity.
1072
+ ///
1073
+ /// Handles blend modes 0-3 on 8bpp surfaces via PixelGrid wrapper.
1074
+ unsafe fn blit_8bpp(
1075
+ dst: *mut openwa_core::bitgrid::DisplayBitGrid,
1076
+ dst_x: i32,
1077
+ dst_y: i32,
1078
+ width: i32,
1079
+ height: i32,
1080
+ src: *mut openwa_core::bitgrid::DisplayBitGrid,
1081
+ src_x: i32,
1082
+ src_y: i32,
1083
+ color_table: *const u8,
1084
+ flags: u32,
1085
+ ) -> u32 {
1086
+ use openwa_core::display::sprite_blit::{
1087
+ blit_sprite_rect, BlitBlend, BlitOrientation, BlitSource,
1088
+ };
1089
+
1090
  let color_table_ref: Option<&[u8; 256]> = if !color_table.is_null() {
1091
  Some(&*(color_table as *const [u8; 256]))
1092
  } else {
 
1096
  let orientation = BlitOrientation::from_flags(flags);
1097
  let blend = BlitBlend::from_flags(flags);
1098
 
 
1099
  let src_data =
1100
  core::slice::from_raw_parts((*src).data, ((*src).row_stride * (*src).height) as usize);
1101
  let blit_src = BlitSource {
 
1105
  row_stride: (*src).row_stride,
1106
  };
1107
 
1108
+ let dst_data_len = ((*dst).row_stride * (*dst).height) as usize;
1109
+ let dst_grid = openwa_core::display::line_draw::PixelGridMut {
1110
+ data: core::slice::from_raw_parts_mut((*dst).data, dst_data_len),
1111
+ width: (*dst).width,
1112
+ height: (*dst).height,
1113
+ row_stride: (*dst).row_stride,
 
 
 
 
 
 
1114
  clip_left: (*dst).clip_left,
1115
  clip_top: (*dst).clip_top,
1116
  clip_right: (*dst).clip_right,
1117
  clip_bottom: (*dst).clip_bottom,
1118
  };
 
 
 
1119
 
1120
+ blit_sprite_rect(
1121
+ dst_grid,
1122
  &blit_src,
1123
  dst_x,
1124
  dst_y,
 
1129
  color_table_ref,
1130
  orientation,
1131
  blend,
1132
+ ) as u32
 
 
 
 
 
 
1133
  }
1134
 
1135
  /// Stippled blit on raw BitGrid pointers.
 
1159
  row_stride: (*src).row_stride,
1160
  };
1161
 
1162
+ let dst_data_len = ((*dst).row_stride * (*dst).height) as usize;
1163
+ let mut dst_grid = openwa_core::display::line_draw::PixelGridMut {
1164
+ data: core::slice::from_raw_parts_mut((*dst).data, dst_data_len),
1165
+ width: (*dst).width,
1166
+ height: (*dst).height,
1167
+ row_stride: (*dst).row_stride,
 
 
 
 
1168
  clip_left: (*dst).clip_left,
1169
  clip_top: (*dst).clip_top,
1170
  clip_right: (*dst).clip_right,
1171
  clip_bottom: (*dst).clip_bottom,
1172
  };
 
 
1173
 
1174
  blit_stippled(
1175
  &mut dst_grid,
 
1183
  stipple_mode,
1184
  parity,
1185
  );
 
 
 
1186
  }
1187
 
1188
  /// Tiled blit on raw BitGrid pointers.
 
1211
  row_stride: (*src).row_stride,
1212
  };
1213
 
1214
+ let dst_data_len = ((*dst).row_stride * (*dst).height) as usize;
1215
+ let mut dst_grid = openwa_core::display::line_draw::PixelGridMut {
1216
+ data: core::slice::from_raw_parts_mut((*dst).data, dst_data_len),
1217
+ width: (*dst).width,
1218
+ height: (*dst).height,
1219
+ row_stride: (*dst).row_stride,
 
 
 
 
1220
  clip_left: (*dst).clip_left,
1221
  clip_top: (*dst).clip_top,
1222
  clip_right: (*dst).clip_right,
1223
  clip_bottom: (*dst).clip_bottom,
1224
  };
 
 
1225
 
1226
  blit_tiled(
1227
  &mut dst_grid,
 
1235
  None, // color_table — tiled mode doesn't use one
1236
  flags,
1237
  );
 
 
 
1238
  }
1239
 
1240
  /// Call the original WA blit function via trampoline.
1241
+ /// No longer used — kept for debugging if fallthrough is temporarily needed.
1242
+ #[allow(dead_code)]
1243
  unsafe fn call_original_blit(
1244
  dst: *mut openwa_core::bitgrid::DisplayBitGrid,
1245
  dst_x: i32,
 
1269
  }
1270
 
1271
  /// Naked asm to call the original blit with ESI set correctly.
1272
+ #[allow(dead_code)]
1273
  #[unsafe(naked)]
1274
  unsafe extern "cdecl" fn call_original_blit_asm(
1275
  _dst: *mut openwa_core::bitgrid::DisplayBitGrid,