feat: line-drawing snapshot test infrastructure
Browse filesAdd PixelGrid (pure-Rust test grid) and PixelWriter trait for unit
testing line algorithms without WA runtime. Add snapshot capture in
DLL (OPENWA_CAPTURE_LINE_SNAPSHOTS=1, triggered at frame 10) that
draws test lines on a BitGrid using WA's native functions and saves
pixel data for comparison.
Also fix headful test crash: remove --minimized flag which became
incompatible with WA's DirectDraw initialization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- crates/openwa-core/src/display/line_draw.rs +289 -0
- crates/openwa-core/src/display/mod.rs +1 -0
- crates/openwa-dll/src/replacements/display.rs +128 -0
- crates/openwa-dll/src/replacements/frame_hook.rs +7 -0
- crates/openwa-test-runner/src/main.rs +0 -1
- testdata/snapshots/clipped_diagonal_45.bin +3 -0
- testdata/snapshots/clipped_diagonal_shallow.bin +3 -0
- testdata/snapshots/clipped_diagonal_steep.bin +3 -0
- testdata/snapshots/clipped_fully_outside.bin +3 -0
- testdata/snapshots/clipped_horizontal.bin +3 -0
- testdata/snapshots/clipped_negative_slope.bin +3 -0
- testdata/snapshots/clipped_partially_outside.bin +3 -0
- testdata/snapshots/clipped_restricted_clip.bin +3 -0
- testdata/snapshots/clipped_subpixel.bin +3 -0
- testdata/snapshots/clipped_vertical.bin +3 -0
- testdata/snapshots/clipped_zero_length.bin +3 -0
- testdata/snapshots/twocol_diagonal_45.bin +3 -0
- testdata/snapshots/twocol_horizontal.bin +3 -0
- testdata/snapshots/twocol_negative.bin +3 -0
- testdata/snapshots/twocol_restricted_clip.bin +3 -0
- testdata/snapshots/twocol_shallow.bin +3 -0
- testdata/snapshots/twocol_steep.bin +3 -0
- testdata/snapshots/twocol_subpixel.bin +3 -0
- testdata/snapshots/twocol_vertical.bin +3 -0
crates/openwa-core/src/display/line_draw.rs
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Line-drawing algorithms for 8bpp BitGrid surfaces.
|
| 2 |
+
//!
|
| 3 |
+
//! Pure Rust ports of WA's thick-line drawing system (0x4F7500 / 0x4F7A60).
|
| 4 |
+
//! The original draws 2-pixel-wide Bresenham-style lines with Fixed-point
|
| 5 |
+
//! Cohen-Sutherland clipping.
|
| 6 |
+
//!
|
| 7 |
+
//! ## Architecture
|
| 8 |
+
//!
|
| 9 |
+
//! ```text
|
| 10 |
+
//! draw_line_clipped / draw_line_two_color (outer dispatch)
|
| 11 |
+
//! ├── Determine dominant axis (|dx| vs |dy|)
|
| 12 |
+
//! ├── Sort endpoints in positive direction
|
| 13 |
+
//! ├── clip_line() — Cohen-Sutherland, Fixed-point
|
| 14 |
+
//! ├── Rasterizer (horizontal-major or vertical-major)
|
| 15 |
+
//! │ └── put_pixel_clipped × 8 per step (outline)
|
| 16 |
+
//! └── Fill rasterizer — 4 pixels per step (body color)
|
| 17 |
+
//! ```
|
| 18 |
+
|
| 19 |
+
use crate::fixed::Fixed;
|
| 20 |
+
|
| 21 |
+
/// Trait for pixel-addressable surfaces used by line-drawing algorithms.
|
| 22 |
+
///
|
| 23 |
+
/// Implemented for `PixelGrid` (test-only, pure Rust) and can be implemented
|
| 24 |
+
/// for `DisplayBitGrid` (runtime, via vtable dispatch).
|
| 25 |
+
pub trait PixelWriter {
|
| 26 |
+
fn put_pixel_clipped(&mut self, x: i32, y: i32, color: u8);
|
| 27 |
+
fn clip_left(&self) -> i32;
|
| 28 |
+
fn clip_top(&self) -> i32;
|
| 29 |
+
fn clip_right(&self) -> i32;
|
| 30 |
+
fn clip_bottom(&self) -> i32;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/// Pure-Rust 8bpp pixel grid for unit testing. No vtable, no WA heap.
|
| 34 |
+
///
|
| 35 |
+
/// Row-major layout: `data[y * row_stride + x]`.
|
| 36 |
+
/// Row stride is 4-byte aligned (matching WA's BitGrid allocation).
|
| 37 |
+
pub struct PixelGrid {
|
| 38 |
+
pub data: Vec<u8>,
|
| 39 |
+
pub width: u32,
|
| 40 |
+
pub height: u32,
|
| 41 |
+
pub row_stride: u32,
|
| 42 |
+
pub clip_left: u32,
|
| 43 |
+
pub clip_top: u32,
|
| 44 |
+
pub clip_right: u32,
|
| 45 |
+
pub clip_bottom: u32,
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
impl PixelGrid {
|
| 49 |
+
/// Create a new zeroed pixel grid with default clip rect covering the full surface.
|
| 50 |
+
pub fn new(width: u32, height: u32) -> Self {
|
| 51 |
+
let row_stride = (width + 3) & !3; // 4-byte aligned
|
| 52 |
+
let size = (row_stride * height) as usize;
|
| 53 |
+
Self {
|
| 54 |
+
data: vec![0u8; size],
|
| 55 |
+
width,
|
| 56 |
+
height,
|
| 57 |
+
row_stride,
|
| 58 |
+
clip_left: 0,
|
| 59 |
+
clip_top: 0,
|
| 60 |
+
clip_right: width,
|
| 61 |
+
clip_bottom: height,
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/// Clear all pixels to zero.
|
| 66 |
+
pub fn clear(&mut self) {
|
| 67 |
+
self.data.fill(0);
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
impl PixelWriter for PixelGrid {
|
| 72 |
+
#[inline]
|
| 73 |
+
fn put_pixel_clipped(&mut self, x: i32, y: i32, color: u8) {
|
| 74 |
+
if x >= self.clip_left as i32
|
| 75 |
+
&& x < self.clip_right as i32
|
| 76 |
+
&& y >= self.clip_top as i32
|
| 77 |
+
&& y < self.clip_bottom as i32
|
| 78 |
+
{
|
| 79 |
+
self.data[(y as u32 * self.row_stride + x as u32) as usize] = color;
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
#[inline]
|
| 84 |
+
fn clip_left(&self) -> i32 {
|
| 85 |
+
self.clip_left as i32
|
| 86 |
+
}
|
| 87 |
+
#[inline]
|
| 88 |
+
fn clip_top(&self) -> i32 {
|
| 89 |
+
self.clip_top as i32
|
| 90 |
+
}
|
| 91 |
+
#[inline]
|
| 92 |
+
fn clip_right(&self) -> i32 {
|
| 93 |
+
self.clip_right as i32
|
| 94 |
+
}
|
| 95 |
+
#[inline]
|
| 96 |
+
fn clip_bottom(&self) -> i32 {
|
| 97 |
+
self.clip_bottom as i32
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// =========================================================================
|
| 102 |
+
// Fixed-point line clipping (port of 0x4F7150)
|
| 103 |
+
// =========================================================================
|
| 104 |
+
|
| 105 |
+
/// Cohen-Sutherland outcode bits for line clipping.
|
| 106 |
+
const LEFT: u8 = 1;
|
| 107 |
+
const RIGHT: u8 = 2;
|
| 108 |
+
const TOP: u8 = 4;
|
| 109 |
+
const BOTTOM: u8 = 8;
|
| 110 |
+
|
| 111 |
+
/// Compute Cohen-Sutherland outcode for a Fixed-point coordinate.
|
| 112 |
+
fn outcode(x: Fixed, y: Fixed, clip: &ClipRect) -> u8 {
|
| 113 |
+
let mut code = 0u8;
|
| 114 |
+
if x.to_raw() < clip.left.to_raw() {
|
| 115 |
+
code |= LEFT;
|
| 116 |
+
}
|
| 117 |
+
if x.to_raw() > clip.right.to_raw() {
|
| 118 |
+
code |= RIGHT;
|
| 119 |
+
}
|
| 120 |
+
if y.to_raw() < clip.top.to_raw() {
|
| 121 |
+
code |= TOP;
|
| 122 |
+
}
|
| 123 |
+
if y.to_raw() > clip.bottom.to_raw() {
|
| 124 |
+
code |= BOTTOM;
|
| 125 |
+
}
|
| 126 |
+
code
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
/// Clip rectangle in Fixed-point coordinates.
|
| 130 |
+
struct ClipRect {
|
| 131 |
+
left: Fixed,
|
| 132 |
+
top: Fixed,
|
| 133 |
+
right: Fixed,
|
| 134 |
+
bottom: Fixed,
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
/// Fixed-point multiply then shift: `(a * b) >> 16`.
|
| 138 |
+
///
|
| 139 |
+
/// Port of the pattern used throughout the WA line clipper.
|
| 140 |
+
#[inline]
|
| 141 |
+
fn fixed_mul_shift(a: i32, b: i32) -> i32 {
|
| 142 |
+
((a as i64 * b as i64) >> 16) as i32
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/// Clip a line segment to the clip rectangle.
|
| 146 |
+
///
|
| 147 |
+
/// Port of 0x4F7150. Returns true if the clipped line is visible.
|
| 148 |
+
/// Modifies coordinates in-place.
|
| 149 |
+
///
|
| 150 |
+
/// The clip rectangle is read from the PixelWriter's bounds, scaled to Fixed.
|
| 151 |
+
fn clip_line(
|
| 152 |
+
x1: &mut Fixed,
|
| 153 |
+
y1: &mut Fixed,
|
| 154 |
+
x2: &mut Fixed,
|
| 155 |
+
y2: &mut Fixed,
|
| 156 |
+
writer: &dyn PixelWriter,
|
| 157 |
+
) -> bool {
|
| 158 |
+
let clip = ClipRect {
|
| 159 |
+
left: Fixed::from_int(writer.clip_left()),
|
| 160 |
+
top: Fixed::from_int(writer.clip_top()),
|
| 161 |
+
right: Fixed::from_int(writer.clip_right()),
|
| 162 |
+
bottom: Fixed::from_int(writer.clip_bottom()),
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
let code1 = outcode(*x1, *y1, &clip);
|
| 166 |
+
let code2 = outcode(*x2, *y2, &clip);
|
| 167 |
+
|
| 168 |
+
// Trivial reject: both endpoints on same side
|
| 169 |
+
if code1 & code2 != 0 {
|
| 170 |
+
return false;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Clip x1 against left/right bounds
|
| 174 |
+
if x1.to_raw() < clip.left.to_raw() {
|
| 175 |
+
let ratio = fixed_div(clip.left.to_raw() - x1.to_raw(), x2.to_raw() - x1.to_raw());
|
| 176 |
+
y1.0 += fixed_mul_shift(y2.to_raw() - y1.to_raw(), ratio);
|
| 177 |
+
*x1 = clip.left;
|
| 178 |
+
} else if x1.to_raw() > clip.right.to_raw() {
|
| 179 |
+
let ratio = fixed_div(clip.right.to_raw() - x1.to_raw(), x2.to_raw() - x1.to_raw());
|
| 180 |
+
y1.0 += fixed_mul_shift(y2.to_raw() - y1.to_raw(), ratio);
|
| 181 |
+
*x1 = clip.right;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Clip x2 against left/right bounds
|
| 185 |
+
if x2.to_raw() < clip.left.to_raw() {
|
| 186 |
+
let ratio = fixed_div(clip.left.to_raw() - x2.to_raw(), x1.to_raw() - x2.to_raw());
|
| 187 |
+
y2.0 += fixed_mul_shift(y1.to_raw() - y2.to_raw(), ratio);
|
| 188 |
+
*x2 = clip.left;
|
| 189 |
+
} else if x2.to_raw() > clip.right.to_raw() {
|
| 190 |
+
let ratio = fixed_div(clip.right.to_raw() - x2.to_raw(), x1.to_raw() - x2.to_raw());
|
| 191 |
+
y2.0 += fixed_mul_shift(y1.to_raw() - y2.to_raw(), ratio);
|
| 192 |
+
*x2 = clip.right;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// Check y bounds after x clipping
|
| 196 |
+
if y1.to_raw() > clip.bottom.to_raw() || y2.to_raw() > clip.bottom.to_raw() {
|
| 197 |
+
return false;
|
| 198 |
+
}
|
| 199 |
+
if y1.to_raw() < clip.top.to_raw() && y2.to_raw() < clip.top.to_raw() {
|
| 200 |
+
return false;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// Clip y1 against top/bottom bounds
|
| 204 |
+
if y1.to_raw() < clip.top.to_raw() {
|
| 205 |
+
let ratio = fixed_div(clip.top.to_raw() - y1.to_raw(), y2.to_raw() - y1.to_raw());
|
| 206 |
+
x1.0 += fixed_mul_shift(x2.to_raw() - x1.to_raw(), ratio);
|
| 207 |
+
*y1 = clip.top;
|
| 208 |
+
} else if y1.to_raw() > clip.bottom.to_raw() {
|
| 209 |
+
let ratio = fixed_div(
|
| 210 |
+
clip.bottom.to_raw() - y1.to_raw(),
|
| 211 |
+
y2.to_raw() - y1.to_raw(),
|
| 212 |
+
);
|
| 213 |
+
x1.0 += fixed_mul_shift(x2.to_raw() - x1.to_raw(), ratio);
|
| 214 |
+
*y1 = clip.bottom;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// Clip y2 against top/bottom bounds
|
| 218 |
+
if y2.to_raw() < clip.top.to_raw() {
|
| 219 |
+
let ratio = fixed_div(clip.top.to_raw() - y2.to_raw(), y1.to_raw() - y2.to_raw());
|
| 220 |
+
x2.0 += fixed_mul_shift(x1.to_raw() - x2.to_raw(), ratio);
|
| 221 |
+
*y2 = clip.top;
|
| 222 |
+
} else if y2.to_raw() > clip.bottom.to_raw() {
|
| 223 |
+
let ratio = fixed_div(
|
| 224 |
+
clip.bottom.to_raw() - y2.to_raw(),
|
| 225 |
+
y1.to_raw() - y2.to_raw(),
|
| 226 |
+
);
|
| 227 |
+
x2.0 += fixed_mul_shift(x1.to_raw() - x2.to_raw(), ratio);
|
| 228 |
+
*y2 = clip.bottom;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
// Final check: both endpoints must be within bounds
|
| 232 |
+
if (x1.to_raw() < clip.left.to_raw() || x1.to_raw() > clip.right.to_raw())
|
| 233 |
+
&& (x2.to_raw() < clip.left.to_raw() || x2.to_raw() > clip.right.to_raw())
|
| 234 |
+
{
|
| 235 |
+
return false;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
true
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
/// Fixed-point divide used by the line clipper.
|
| 242 |
+
///
|
| 243 |
+
/// Port of FUN_005b3501: computes `(numerator << 16) / denominator`.
|
| 244 |
+
#[inline]
|
| 245 |
+
fn fixed_div(numerator: i32, denominator: i32) -> i32 {
|
| 246 |
+
if denominator == 0 {
|
| 247 |
+
return 0;
|
| 248 |
+
}
|
| 249 |
+
(((numerator as i64) << 16) / denominator as i64) as i32
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
#[cfg(test)]
|
| 253 |
+
mod tests {
|
| 254 |
+
use super::*;
|
| 255 |
+
|
| 256 |
+
#[test]
|
| 257 |
+
fn pixel_grid_basic() {
|
| 258 |
+
let mut grid = PixelGrid::new(16, 16);
|
| 259 |
+
grid.put_pixel_clipped(5, 5, 0xFF);
|
| 260 |
+
assert_eq!(grid.data[5 * grid.row_stride as usize + 5], 0xFF);
|
| 261 |
+
|
| 262 |
+
// Out of bounds — should be no-op
|
| 263 |
+
grid.put_pixel_clipped(-1, 5, 0xAA);
|
| 264 |
+
grid.put_pixel_clipped(16, 5, 0xAA);
|
| 265 |
+
grid.put_pixel_clipped(5, -1, 0xAA);
|
| 266 |
+
grid.put_pixel_clipped(5, 16, 0xAA);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
#[test]
|
| 270 |
+
fn pixel_grid_clipping() {
|
| 271 |
+
let mut grid = PixelGrid::new(16, 16);
|
| 272 |
+
grid.clip_left = 4;
|
| 273 |
+
grid.clip_top = 4;
|
| 274 |
+
grid.clip_right = 12;
|
| 275 |
+
grid.clip_bottom = 12;
|
| 276 |
+
|
| 277 |
+
// Inside clip rect — should write
|
| 278 |
+
grid.put_pixel_clipped(4, 4, 1);
|
| 279 |
+
assert_eq!(grid.data[4 * grid.row_stride as usize + 4], 1);
|
| 280 |
+
|
| 281 |
+
// At clip boundary (exclusive) — should NOT write
|
| 282 |
+
grid.put_pixel_clipped(12, 4, 2);
|
| 283 |
+
assert_eq!(grid.data[4 * grid.row_stride as usize + 12], 0);
|
| 284 |
+
|
| 285 |
+
// Outside clip rect — should NOT write
|
| 286 |
+
grid.put_pixel_clipped(3, 4, 3);
|
| 287 |
+
assert_eq!(grid.data[4 * grid.row_stride as usize + 3], 0);
|
| 288 |
+
}
|
| 289 |
+
}
|
crates/openwa-core/src/display/mod.rs
CHANGED
|
@@ -5,6 +5,7 @@ pub mod dd_display;
|
|
| 5 |
pub mod display_wrapper;
|
| 6 |
pub mod gfx;
|
| 7 |
pub mod gradient;
|
|
|
|
| 8 |
pub mod opengl;
|
| 9 |
pub mod palette;
|
| 10 |
|
|
|
|
| 5 |
pub mod display_wrapper;
|
| 6 |
pub mod gfx;
|
| 7 |
pub mod gradient;
|
| 8 |
+
pub mod line_draw;
|
| 9 |
pub mod opengl;
|
| 10 |
pub mod palette;
|
| 11 |
|
crates/openwa-dll/src/replacements/display.rs
CHANGED
|
@@ -48,6 +48,134 @@ unsafe extern "thiscall" fn headless_destructor(
|
|
| 48 |
this
|
| 49 |
}
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
pub fn install() -> Result<(), String> {
|
| 52 |
let _ = log_line("[Display] Patching DisplayBase vtables");
|
| 53 |
|
|
|
|
| 48 |
this
|
| 49 |
}
|
| 50 |
|
| 51 |
+
/// Capture line-drawing snapshots from WA's native BitGrid line functions.
|
| 52 |
+
///
|
| 53 |
+
/// Creates a test BitGrid, calls WA line functions with known inputs,
|
| 54 |
+
/// saves pixel data to `testdata/snapshots/`. Activated by
|
| 55 |
+
/// `OPENWA_CAPTURE_LINE_SNAPSHOTS=1`.
|
| 56 |
+
pub unsafe fn capture_line_snapshots() {
|
| 57 |
+
use openwa_core::display::bitgrid::DisplayBitGrid;
|
| 58 |
+
use std::fs;
|
| 59 |
+
use std::io::Write;
|
| 60 |
+
|
| 61 |
+
let grid_w: u32 = 128;
|
| 62 |
+
let grid_h: u32 = 128;
|
| 63 |
+
let grid = DisplayBitGrid::alloc(8, grid_w, grid_h);
|
| 64 |
+
if grid.is_null() {
|
| 65 |
+
let _ = log_line("[Display] SNAPSHOT: Failed to allocate test BitGrid");
|
| 66 |
+
return;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
let row_stride = (*grid).row_stride;
|
| 70 |
+
let data_ptr = (*grid).data;
|
| 71 |
+
let data_size = (row_stride * grid_h) as usize;
|
| 72 |
+
|
| 73 |
+
// WA line functions (stdcall)
|
| 74 |
+
let draw_clipped: unsafe extern "stdcall" fn(*mut DisplayBitGrid, i32, i32, i32, i32, u32) =
|
| 75 |
+
core::mem::transmute(rb(va::DRAW_LINE_CLIPPED) as usize);
|
| 76 |
+
|
| 77 |
+
let draw_two: unsafe extern "stdcall" fn(*mut DisplayBitGrid, i32, i32, i32, i32, u32, u32) =
|
| 78 |
+
core::mem::transmute(rb(va::DRAW_LINE_TWO_COLOR) as usize);
|
| 79 |
+
|
| 80 |
+
let dir = "testdata/snapshots";
|
| 81 |
+
let _ = fs::create_dir_all(dir);
|
| 82 |
+
|
| 83 |
+
// Macro: clear grid, reset clip, run drawing code, save to file.
|
| 84 |
+
macro_rules! snap {
|
| 85 |
+
($name:expr, $body:expr) => {{
|
| 86 |
+
core::ptr::write_bytes(data_ptr, 0, data_size);
|
| 87 |
+
(*grid).clip_left = 0;
|
| 88 |
+
(*grid).clip_top = 0;
|
| 89 |
+
(*grid).clip_right = grid_w;
|
| 90 |
+
(*grid).clip_bottom = grid_h;
|
| 91 |
+
$body;
|
| 92 |
+
let path = format!("{}/{}.bin", dir, $name);
|
| 93 |
+
if let Ok(mut file) = fs::File::create(&path) {
|
| 94 |
+
let _ = file.write_all(&grid_w.to_le_bytes());
|
| 95 |
+
let _ = file.write_all(&grid_h.to_le_bytes());
|
| 96 |
+
let _ = file.write_all(&row_stride.to_le_bytes());
|
| 97 |
+
let _ = file.write_all(core::slice::from_raw_parts(data_ptr, data_size));
|
| 98 |
+
}
|
| 99 |
+
}};
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
let f = |x: i32| x << 16; // int to Fixed raw
|
| 103 |
+
let mut count = 0u32;
|
| 104 |
+
|
| 105 |
+
// Single-color line tests
|
| 106 |
+
for &(name, x1, y1, x2, y2, color) in &[
|
| 107 |
+
("clipped_horizontal", f(10), f(64), f(118), f(64), 1u32),
|
| 108 |
+
("clipped_vertical", f(64), f(10), f(64), f(118), 2),
|
| 109 |
+
("clipped_diagonal_45", f(10), f(10), f(118), f(118), 3),
|
| 110 |
+
("clipped_diagonal_steep", f(60), f(10), f(68), f(118), 4),
|
| 111 |
+
("clipped_diagonal_shallow", f(10), f(60), f(118), f(68), 5),
|
| 112 |
+
("clipped_negative_slope", f(118), f(10), f(10), f(118), 6),
|
| 113 |
+
(
|
| 114 |
+
"clipped_subpixel",
|
| 115 |
+
f(10) + 0x8000,
|
| 116 |
+
f(20) + 0x4000,
|
| 117 |
+
f(100) + 0xC000,
|
| 118 |
+
f(80) + 0x2000,
|
| 119 |
+
7,
|
| 120 |
+
),
|
| 121 |
+
("clipped_zero_length", f(64), f(64), f(64), f(64), 8),
|
| 122 |
+
("clipped_partially_outside", f(-20), f(64), f(148), f(64), 9),
|
| 123 |
+
("clipped_fully_outside", f(-50), f(-50), f(-10), f(-10), 10),
|
| 124 |
+
] {
|
| 125 |
+
snap!(name, draw_clipped(grid, x1, y1, x2, y2, color));
|
| 126 |
+
count += 1;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Two-color line tests
|
| 130 |
+
for &(name, x1, y1, x2, y2, c1, c2) in &[
|
| 131 |
+
("twocol_horizontal", f(10), f(64), f(118), f(64), 1u32, 2u32),
|
| 132 |
+
("twocol_vertical", f(64), f(10), f(64), f(118), 1, 2),
|
| 133 |
+
("twocol_diagonal_45", f(10), f(10), f(118), f(118), 1, 2),
|
| 134 |
+
("twocol_steep", f(60), f(10), f(68), f(118), 3, 4),
|
| 135 |
+
("twocol_shallow", f(10), f(60), f(118), f(68), 3, 4),
|
| 136 |
+
("twocol_negative", f(118), f(10), f(10), f(118), 5, 6),
|
| 137 |
+
(
|
| 138 |
+
"twocol_subpixel",
|
| 139 |
+
f(10) + 0x8000,
|
| 140 |
+
f(20) + 0x4000,
|
| 141 |
+
f(100) + 0xC000,
|
| 142 |
+
f(80) + 0x2000,
|
| 143 |
+
7,
|
| 144 |
+
8,
|
| 145 |
+
),
|
| 146 |
+
] {
|
| 147 |
+
snap!(name, draw_two(grid, x1, y1, x2, y2, c1, c2));
|
| 148 |
+
count += 1;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Restricted clip rect tests
|
| 152 |
+
snap!("clipped_restricted_clip", {
|
| 153 |
+
(*grid).clip_left = 30;
|
| 154 |
+
(*grid).clip_top = 30;
|
| 155 |
+
(*grid).clip_right = 98;
|
| 156 |
+
(*grid).clip_bottom = 98;
|
| 157 |
+
draw_clipped(grid, f(10), f(10), f(118), f(118), 11)
|
| 158 |
+
});
|
| 159 |
+
count += 1;
|
| 160 |
+
|
| 161 |
+
snap!("twocol_restricted_clip", {
|
| 162 |
+
(*grid).clip_left = 30;
|
| 163 |
+
(*grid).clip_top = 30;
|
| 164 |
+
(*grid).clip_right = 98;
|
| 165 |
+
(*grid).clip_bottom = 98;
|
| 166 |
+
draw_two(grid, f(10), f(10), f(118), f(118), 9, 10)
|
| 167 |
+
});
|
| 168 |
+
count += 1;
|
| 169 |
+
|
| 170 |
+
let _ = log_line(&format!(
|
| 171 |
+
"[Display] SNAPSHOT: Saved {count} line snapshots to {dir}/"
|
| 172 |
+
));
|
| 173 |
+
|
| 174 |
+
// Free the grid
|
| 175 |
+
let destructor = (*(*grid).vtable).destructor;
|
| 176 |
+
destructor(grid, 1);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
pub fn install() -> Result<(), String> {
|
| 180 |
let _ = log_line("[Display] Patching DisplayBase vtables");
|
| 181 |
|
crates/openwa-dll/src/replacements/frame_hook.rs
CHANGED
|
@@ -63,6 +63,13 @@ unsafe extern "stdcall" fn hook_turn_manager(turngame: u32) {
|
|
| 63 |
|
| 64 |
let frame = FRAMES_PROCESSED.fetch_add(1, Ordering::Relaxed) + 1;
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
// Replay test: fast-forward + milestone tracking (no-op if not in replay test mode)
|
| 67 |
super::replay_test::on_frame(frame);
|
| 68 |
}
|
|
|
|
| 63 |
|
| 64 |
let frame = FRAMES_PROCESSED.fetch_add(1, Ordering::Relaxed) + 1;
|
| 65 |
|
| 66 |
+
// Line snapshot capture: run once at frame 10 when env var is set
|
| 67 |
+
if frame == 10 {
|
| 68 |
+
if std::env::var("OPENWA_CAPTURE_LINE_SNAPSHOTS").is_ok() {
|
| 69 |
+
super::display::capture_line_snapshots();
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
// Replay test: fast-forward + milestone tracking (no-op if not in replay test mode)
|
| 74 |
super::replay_test::on_frame(frame);
|
| 75 |
}
|
crates/openwa-test-runner/src/main.rs
CHANGED
|
@@ -816,7 +816,6 @@ fn run_headful_test(
|
|
| 816 |
let errorlog_path = run_dir.join(format!("{}.errorlog.txt", test.name));
|
| 817 |
|
| 818 |
let result = Command::new(launcher)
|
| 819 |
-
.arg("--minimized")
|
| 820 |
.arg(wa_exe)
|
| 821 |
.arg(&test.replay_path)
|
| 822 |
.env("OPENWA_REPLAY_TEST", "1")
|
|
|
|
| 816 |
let errorlog_path = run_dir.join(format!("{}.errorlog.txt", test.name));
|
| 817 |
|
| 818 |
let result = Command::new(launcher)
|
|
|
|
| 819 |
.arg(wa_exe)
|
| 820 |
.arg(&test.replay_path)
|
| 821 |
.env("OPENWA_REPLAY_TEST", "1")
|
testdata/snapshots/clipped_diagonal_45.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a6d578b7421ee45ef7d10700614024892972c3ab255705fc7ec750dc4b70852b
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_diagonal_shallow.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d4154d14e49950ce44801998957d4d316287a574d2006db21e262c5b7459521e
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_diagonal_steep.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b4f9440349534e65a92869a779075877c14fd169bf58ff1074c438b80919e72c
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_fully_outside.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:60d2f81832d645fd3abf309ea419b7c75bfa3516cd69f8105fa35c89a0c58c1d
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_horizontal.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dd0f3f8abfa05ba3d0483c8c2b974d9a1efbbc7558d32d8d32666c723cba9801
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_negative_slope.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7ceedb4d2c03ad3b6ebf3f1be84a8b13580d7500b991b496d0077004cbeb8e79
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_partially_outside.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e6e4be906d80defb36b321eff3f64be21c13ac43f5f24749be5119dbf97b010e
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_restricted_clip.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cd3afc44b242bbc7c03109e564660e8ca8aa3af2d6558730025044601ae5bdb6
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_subpixel.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dc55020f352f34210d0e0ca685979f3a5a94552fbda4303b55d2b6206a20c665
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_vertical.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d858f5c4aafd68f2fa4ae6cbeea4ea8443889db40a7c70817734b1edd80c7bd9
|
| 3 |
+
size 16396
|
testdata/snapshots/clipped_zero_length.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:60d2f81832d645fd3abf309ea419b7c75bfa3516cd69f8105fa35c89a0c58c1d
|
| 3 |
+
size 16396
|
testdata/snapshots/twocol_diagonal_45.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2dd5a7b9994e85f003fe1c19ca8fdb34e94894ebe9e710124d1fc6b6b31aa1ea
|
| 3 |
+
size 16396
|
testdata/snapshots/twocol_horizontal.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:afb48702dc248c6fc3a0e6a6dcd603420997b2b894034114e641bfddec57170b
|
| 3 |
+
size 16396
|
testdata/snapshots/twocol_negative.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f2cb64e70b3dfc6ff185ff08494ffd0f799d872492470611fcea89c53b51518d
|
| 3 |
+
size 16396
|
testdata/snapshots/twocol_restricted_clip.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cef5d61f71bc09964371eed8157c000bd0f404678d9bd6b5e78cff68f8379845
|
| 3 |
+
size 16396
|
testdata/snapshots/twocol_shallow.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5b1ea09b4e78cd1f0fa1ad38665f3225cc016ff957c680c1a1fe42b656e3ffe6
|
| 3 |
+
size 16396
|
testdata/snapshots/twocol_steep.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d545b902251a860bd4f6753b0e8b6d8181cd21fc512dddf6fd4d93abf4b9ac94
|
| 3 |
+
size 16396
|
testdata/snapshots/twocol_subpixel.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1e47b85680ab580348b4abf968d1c4b9dd3b0491f84fd2f95bf919f237b98d1f
|
| 3 |
+
size 16396
|
testdata/snapshots/twocol_vertical.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f98dec117c9422729adf2ce21d479d8513e3b1a62ef6de8b17a50c27244f4a50
|
| 3 |
+
size 16396
|