refactor: use Fixed type throughout line-drawing algorithms
Browse filesReplace raw i32 with Fixed in all line_draw.rs function signatures and
internals. Add Fixed helper methods: floor(), pixel_center(),
round_to_int(), div_raw(), mul_raw(), HALF. Eliminates all magic
0x10000/0x8000/0xFFFF literals in favor of Fixed::ONE/HALF/floor().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
crates/openwa-core/src/display/dd_display.rs
CHANGED
|
@@ -696,10 +696,10 @@ pub unsafe extern "thiscall" fn draw_line(
|
|
| 696 |
let mut writer = BitGridWriter((*gfx).layer_0);
|
| 697 |
line_draw::draw_line_two_color(
|
| 698 |
&mut writer,
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
color1 as u8,
|
| 704 |
color2 as u8,
|
| 705 |
);
|
|
@@ -729,10 +729,10 @@ pub unsafe extern "thiscall" fn draw_line_clipped(
|
|
| 729 |
let mut writer = BitGridWriter((*gfx).layer_0);
|
| 730 |
line_draw::draw_line_clipped(
|
| 731 |
&mut writer,
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
color as u8,
|
| 737 |
);
|
| 738 |
}
|
|
|
|
| 696 |
let mut writer = BitGridWriter((*gfx).layer_0);
|
| 697 |
line_draw::draw_line_two_color(
|
| 698 |
&mut writer,
|
| 699 |
+
x1 + cam_x,
|
| 700 |
+
y1 + cam_y,
|
| 701 |
+
x2 + cam_x,
|
| 702 |
+
y2 + cam_y,
|
| 703 |
color1 as u8,
|
| 704 |
color2 as u8,
|
| 705 |
);
|
|
|
|
| 729 |
let mut writer = BitGridWriter((*gfx).layer_0);
|
| 730 |
line_draw::draw_line_clipped(
|
| 731 |
&mut writer,
|
| 732 |
+
x1 + cam_x,
|
| 733 |
+
y1 + cam_y,
|
| 734 |
+
x2 + cam_x,
|
| 735 |
+
y2 + cam_y,
|
| 736 |
color as u8,
|
| 737 |
);
|
| 738 |
}
|
crates/openwa-core/src/display/line_draw.rs
CHANGED
|
@@ -15,6 +15,8 @@
|
|
| 15 |
//! └── DDA rasterizer — 1 pixel per step (horizontal or vertical major)
|
| 16 |
//! ```
|
| 17 |
|
|
|
|
|
|
|
| 18 |
/// Trait for pixel-addressable surfaces used by line-drawing algorithms.
|
| 19 |
///
|
| 20 |
/// Implemented for `PixelGrid` (test-only, pure Rust) and can be implemented
|
|
@@ -64,7 +66,7 @@ impl PixelGrid {
|
|
| 64 |
self.data.fill(0);
|
| 65 |
}
|
| 66 |
|
| 67 |
-
/// Load a snapshot file
|
| 68 |
#[cfg(test)]
|
| 69 |
pub fn from_snapshot(bytes: &[u8]) -> Self {
|
| 70 |
let width = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
|
|
@@ -118,23 +120,6 @@ impl PixelWriter for PixelGrid {
|
|
| 118 |
// Fixed-point line clipping (port of 0x4F7150)
|
| 119 |
// =========================================================================
|
| 120 |
|
| 121 |
-
/// Fixed-point divide: `(numerator << 16) / denominator`.
|
| 122 |
-
///
|
| 123 |
-
/// Port of FUN_005b3501. Used by the line clipper for endpoint interpolation.
|
| 124 |
-
#[inline]
|
| 125 |
-
fn fixed_div(numerator: i32, denominator: i32) -> i32 {
|
| 126 |
-
if denominator == 0 {
|
| 127 |
-
return 0;
|
| 128 |
-
}
|
| 129 |
-
(((numerator as i64) << 16) / denominator as i64) as i32
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
/// Fixed-point multiply-shift: `(a * b) >> 16`.
|
| 133 |
-
#[inline]
|
| 134 |
-
fn fixed_mul_shift(a: i32, b: i32) -> i32 {
|
| 135 |
-
((a as i64 * b as i64) >> 16) as i32
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
/// Clip a line segment to the writer's clip rectangle.
|
| 139 |
///
|
| 140 |
/// Port of 0x4F7150. All coordinates are Fixed-point (16.16).
|
|
@@ -142,16 +127,16 @@ fn fixed_mul_shift(a: i32, b: i32) -> i32 {
|
|
| 142 |
///
|
| 143 |
/// The clip rectangle is scaled from the writer's pixel bounds to Fixed.
|
| 144 |
fn clip_line(
|
| 145 |
-
x1: &mut
|
| 146 |
-
y1: &mut
|
| 147 |
-
x2: &mut
|
| 148 |
-
y2: &mut
|
| 149 |
writer: &dyn PixelWriter,
|
| 150 |
) -> bool {
|
| 151 |
-
let cl = writer.clip_left()
|
| 152 |
-
let ct = writer.clip_top()
|
| 153 |
-
let cr = writer.clip_right()
|
| 154 |
-
let cb = writer.clip_bottom()
|
| 155 |
|
| 156 |
// Cohen-Sutherland outcodes
|
| 157 |
let mut code1 = 0u8;
|
|
@@ -189,30 +174,27 @@ fn clip_line(
|
|
| 189 |
|
| 190 |
// Clip endpoint 1 against x bounds
|
| 191 |
if *x1 < cl {
|
| 192 |
-
let ratio =
|
| 193 |
-
*y1 +=
|
| 194 |
*x1 = cl;
|
| 195 |
} else if *x1 > cr {
|
| 196 |
-
let ratio =
|
| 197 |
-
*y1 +=
|
| 198 |
*x1 = cr;
|
| 199 |
}
|
| 200 |
|
| 201 |
// Clip endpoint 2 against x bounds
|
| 202 |
if *x2 < cl {
|
| 203 |
-
let ratio =
|
| 204 |
-
*y2 +=
|
| 205 |
*x2 = cl;
|
| 206 |
} else if *x2 > cr {
|
| 207 |
-
let ratio =
|
| 208 |
-
*y2 +=
|
| 209 |
*x2 = cr;
|
| 210 |
}
|
| 211 |
|
| 212 |
// Check y visibility after x clipping
|
| 213 |
-
if (*y1 > cb || *y2 > cb) && (*y1 < ct || *y2 < ct) {
|
| 214 |
-
// Spans both sides — skip (original logic)
|
| 215 |
-
}
|
| 216 |
if !(*y1 <= cb || *y2 <= cb) {
|
| 217 |
return false;
|
| 218 |
}
|
|
@@ -222,23 +204,23 @@ fn clip_line(
|
|
| 222 |
|
| 223 |
// Clip endpoint 1 against y bounds
|
| 224 |
if *y1 < ct {
|
| 225 |
-
let ratio =
|
| 226 |
-
*x1 +=
|
| 227 |
*y1 = ct;
|
| 228 |
} else if *y1 > cb {
|
| 229 |
-
let ratio =
|
| 230 |
-
*x1 +=
|
| 231 |
*y1 = cb;
|
| 232 |
}
|
| 233 |
|
| 234 |
// Clip endpoint 2 against y bounds
|
| 235 |
if *y2 < ct {
|
| 236 |
-
let ratio =
|
| 237 |
-
*x2 +=
|
| 238 |
*y2 = ct;
|
| 239 |
} else if *y2 > cb {
|
| 240 |
-
let ratio =
|
| 241 |
-
*x2 +=
|
| 242 |
*y2 = cb;
|
| 243 |
}
|
| 244 |
|
|
@@ -257,21 +239,27 @@ fn clip_line(
|
|
| 257 |
/// Horizontal-major DDA rasterizer (port of 0x4F7400).
|
| 258 |
///
|
| 259 |
/// Iterates from x1 to x2 (pixel-rounded), computing y via Fixed-point slope.
|
| 260 |
-
fn raster_hmajor(
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
if start == end {
|
| 264 |
return;
|
| 265 |
}
|
| 266 |
|
| 267 |
-
|
| 268 |
-
let
|
| 269 |
-
let mut
|
| 270 |
-
let
|
| 271 |
-
let count = (end - start) >> 16;
|
| 272 |
|
| 273 |
for _ in 0..=count {
|
| 274 |
-
writer.put_pixel_clipped(px,
|
| 275 |
y += slope;
|
| 276 |
px += 1;
|
| 277 |
}
|
|
@@ -280,41 +268,45 @@ fn raster_hmajor(writer: &mut dyn PixelWriter, x1: i32, y1: i32, x2: i32, y2: i3
|
|
| 280 |
/// Vertical-major DDA rasterizer (port of 0x4F7480).
|
| 281 |
///
|
| 282 |
/// Iterates from y1 to y2 (pixel-rounded), computing x via Fixed-point slope.
|
| 283 |
-
fn raster_vmajor(
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
if start == end {
|
| 287 |
return;
|
| 288 |
}
|
| 289 |
|
| 290 |
-
|
| 291 |
-
let
|
| 292 |
-
let mut
|
| 293 |
-
let
|
| 294 |
-
let count = (end - start) >> 16;
|
| 295 |
|
| 296 |
for _ in 0..=count {
|
| 297 |
-
writer.put_pixel_clipped(
|
| 298 |
x += slope;
|
| 299 |
py += 1;
|
| 300 |
}
|
| 301 |
}
|
| 302 |
|
| 303 |
/// Draw a single-color clipped line. Port of 0x4F7500.
|
| 304 |
-
///
|
| 305 |
-
/// All coordinates are Fixed-point (16.16).
|
| 306 |
pub fn draw_line_clipped(
|
| 307 |
writer: &mut dyn PixelWriter,
|
| 308 |
-
mut x1:
|
| 309 |
-
mut y1:
|
| 310 |
-
mut x2:
|
| 311 |
-
mut y2:
|
| 312 |
color: u8,
|
| 313 |
) {
|
| 314 |
-
let
|
| 315 |
-
let
|
| 316 |
|
| 317 |
-
if
|
| 318 |
// Vertical-major: sort so y1 <= y2
|
| 319 |
if y1 > y2 {
|
| 320 |
core::mem::swap(&mut x1, &mut x2);
|
|
@@ -339,36 +331,33 @@ pub fn draw_line_clipped(
|
|
| 339 |
// Two-color thick line rasterizer (port of 0x4F7A60)
|
| 340 |
// =========================================================================
|
| 341 |
|
| 342 |
-
/// Horizontal-major thick
|
| 343 |
///
|
| 344 |
-
/// Draws 8
|
| 345 |
-
fn
|
| 346 |
writer: &mut dyn PixelWriter,
|
| 347 |
-
x1:
|
| 348 |
-
y1:
|
| 349 |
-
x2:
|
| 350 |
-
y2:
|
| 351 |
color: u8,
|
| 352 |
) {
|
| 353 |
-
let start =
|
| 354 |
-
let end =
|
| 355 |
if start == end {
|
| 356 |
return;
|
| 357 |
}
|
| 358 |
|
| 359 |
-
|
| 360 |
-
let
|
| 361 |
-
|
| 362 |
-
let
|
| 363 |
-
let mut px = start >> 16;
|
| 364 |
-
let count = (end - start) >> 16;
|
| 365 |
if count == 0 {
|
| 366 |
return;
|
| 367 |
}
|
| 368 |
|
| 369 |
for _ in 0..count {
|
| 370 |
-
let py = y
|
| 371 |
-
// 8 outline pixels around the 2x2 body
|
| 372 |
writer.put_pixel_clipped(px, py - 1, color);
|
| 373 |
writer.put_pixel_clipped(px + 1, py - 1, color);
|
| 374 |
writer.put_pixel_clipped(px - 1, py, color);
|
|
@@ -383,34 +372,33 @@ fn raster_thick_hmajor_outline(
|
|
| 383 |
}
|
| 384 |
}
|
| 385 |
|
| 386 |
-
/// Horizontal-major thick
|
| 387 |
///
|
| 388 |
-
/// Draws 4 body pixels per step (2x2 center).
|
| 389 |
-
fn
|
| 390 |
writer: &mut dyn PixelWriter,
|
| 391 |
-
x1:
|
| 392 |
-
y1:
|
| 393 |
-
x2:
|
| 394 |
-
y2:
|
| 395 |
color: u8,
|
| 396 |
) {
|
| 397 |
-
let start =
|
| 398 |
-
let end =
|
| 399 |
if start == end {
|
| 400 |
return;
|
| 401 |
}
|
| 402 |
|
| 403 |
-
let slope =
|
| 404 |
-
let mut y = y1 +
|
| 405 |
-
let mut px = start
|
| 406 |
-
let count = (end - start)
|
| 407 |
if count == 0 {
|
| 408 |
return;
|
| 409 |
}
|
| 410 |
|
| 411 |
for _ in 0..count {
|
| 412 |
-
let py = y
|
| 413 |
-
// 2x2 body
|
| 414 |
writer.put_pixel_clipped(px, py, color);
|
| 415 |
writer.put_pixel_clipped(px + 1, py, color);
|
| 416 |
writer.put_pixel_clipped(px, py + 1, color);
|
|
@@ -421,31 +409,30 @@ fn raster_thick_hmajor_fill(
|
|
| 421 |
}
|
| 422 |
}
|
| 423 |
|
| 424 |
-
/// Vertical-major thick
|
| 425 |
///
|
| 426 |
-
/// Transposed version
|
| 427 |
-
fn
|
| 428 |
writer: &mut dyn PixelWriter,
|
| 429 |
-
x1:
|
| 430 |
-
y1:
|
| 431 |
-
x2:
|
| 432 |
-
y2:
|
| 433 |
color: u8,
|
| 434 |
) {
|
| 435 |
-
let start =
|
| 436 |
-
let end =
|
| 437 |
if start == end {
|
| 438 |
return;
|
| 439 |
}
|
| 440 |
|
| 441 |
-
let slope =
|
| 442 |
-
let mut x = x1 +
|
| 443 |
-
let mut py = start
|
| 444 |
-
let count = (end - start)
|
| 445 |
|
| 446 |
for _ in 0..count {
|
| 447 |
-
let px = x
|
| 448 |
-
// 8 outline pixels (transposed pattern)
|
| 449 |
writer.put_pixel_clipped(px, py - 1, color);
|
| 450 |
writer.put_pixel_clipped(px + 1, py - 1, color);
|
| 451 |
writer.put_pixel_clipped(px - 1, py, color);
|
|
@@ -460,31 +447,30 @@ fn raster_thick_vmajor_outline(
|
|
| 460 |
}
|
| 461 |
}
|
| 462 |
|
| 463 |
-
/// Vertical-major thick
|
| 464 |
///
|
| 465 |
/// Transposed version — draws 4 body pixels per step.
|
| 466 |
-
fn
|
| 467 |
writer: &mut dyn PixelWriter,
|
| 468 |
-
x1:
|
| 469 |
-
y1:
|
| 470 |
-
x2:
|
| 471 |
-
y2:
|
| 472 |
color: u8,
|
| 473 |
) {
|
| 474 |
-
let start =
|
| 475 |
-
let end =
|
| 476 |
if start == end {
|
| 477 |
return;
|
| 478 |
}
|
| 479 |
|
| 480 |
-
let slope =
|
| 481 |
-
let mut x = x1 +
|
| 482 |
-
let mut py = start
|
| 483 |
-
let count = (end - start)
|
| 484 |
|
| 485 |
for _ in 0..count {
|
| 486 |
-
let px = x
|
| 487 |
-
// 2x2 body
|
| 488 |
writer.put_pixel_clipped(px, py, color);
|
| 489 |
writer.put_pixel_clipped(px + 1, py, color);
|
| 490 |
writer.put_pixel_clipped(px, py + 1, color);
|
|
@@ -497,53 +483,49 @@ fn raster_thick_vmajor_fill(
|
|
| 497 |
|
| 498 |
/// Draw a two-color thick clipped line. Port of 0x4F7A60.
|
| 499 |
///
|
| 500 |
-
///
|
| 501 |
-
///
|
| 502 |
pub fn draw_line_two_color(
|
| 503 |
writer: &mut dyn PixelWriter,
|
| 504 |
-
mut x1:
|
| 505 |
-
mut y1:
|
| 506 |
-
mut x2:
|
| 507 |
-
mut y2:
|
| 508 |
color1: u8,
|
| 509 |
color2: u8,
|
| 510 |
) {
|
| 511 |
-
let
|
| 512 |
-
let
|
| 513 |
|
| 514 |
-
if
|
| 515 |
// Vertical-major
|
| 516 |
-
// Special case: zero horizontal extent → nudge x
|
| 517 |
-
if
|
| 518 |
-
x1 -=
|
| 519 |
-
x2 -=
|
| 520 |
}
|
| 521 |
-
// Sort so y1 <= y2
|
| 522 |
if y1 > y2 {
|
| 523 |
core::mem::swap(&mut x1, &mut x2);
|
| 524 |
core::mem::swap(&mut y1, &mut y2);
|
| 525 |
}
|
| 526 |
if clip_line(&mut x1, &mut y1, &mut x2, &mut y2, writer) {
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
raster_thick_vmajor_fill(writer, x1, y1, x2, y2, color1);
|
| 530 |
}
|
| 531 |
} else {
|
| 532 |
// Horizontal-major
|
| 533 |
-
// Special case: zero vertical extent → nudge y
|
| 534 |
-
if
|
| 535 |
-
y1 -=
|
| 536 |
-
y2 -=
|
| 537 |
}
|
| 538 |
-
// Sort so x1 <= x2
|
| 539 |
if x1 > x2 {
|
| 540 |
core::mem::swap(&mut x1, &mut x2);
|
| 541 |
core::mem::swap(&mut y1, &mut y2);
|
| 542 |
}
|
| 543 |
if clip_line(&mut x1, &mut y1, &mut x2, &mut y2, writer) {
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
raster_thick_hmajor_fill(writer, x1, y1, x2, y2, color1);
|
| 547 |
}
|
| 548 |
}
|
| 549 |
}
|
|
@@ -587,8 +569,8 @@ mod tests {
|
|
| 587 |
assert_eq!(grid.data[4 * grid.row_stride as usize + 3], 0);
|
| 588 |
}
|
| 589 |
|
| 590 |
-
fn f(x: i32) ->
|
| 591 |
-
x
|
| 592 |
}
|
| 593 |
|
| 594 |
fn load_snapshot(name: &str) -> PixelGrid {
|
|
@@ -611,7 +593,6 @@ mod tests {
|
|
| 611 |
"{name}: stride mismatch"
|
| 612 |
);
|
| 613 |
if actual.data != expected.data {
|
| 614 |
-
// Find first differing pixel for a helpful error message
|
| 615 |
let mut diff_count = 0;
|
| 616 |
let mut first_diff = None;
|
| 617 |
for y in 0..actual.height {
|
|
@@ -700,10 +681,10 @@ mod tests {
|
|
| 700 |
snapshot_test_clipped!(
|
| 701 |
snap_clipped_subpixel,
|
| 702 |
"clipped_subpixel",
|
| 703 |
-
f(10) + 0x8000,
|
| 704 |
-
f(20) + 0x4000,
|
| 705 |
-
f(100) + 0xC000,
|
| 706 |
-
f(80) + 0x2000,
|
| 707 |
7
|
| 708 |
);
|
| 709 |
snapshot_test_clipped!(
|
|
@@ -820,10 +801,10 @@ mod tests {
|
|
| 820 |
snapshot_test_twocol!(
|
| 821 |
snap_twocol_subpixel,
|
| 822 |
"twocol_subpixel",
|
| 823 |
-
f(10) + 0x8000,
|
| 824 |
-
f(20) + 0x4000,
|
| 825 |
-
f(100) + 0xC000,
|
| 826 |
-
f(80) + 0x2000,
|
| 827 |
7,
|
| 828 |
8
|
| 829 |
);
|
|
|
|
| 15 |
//! └── DDA rasterizer — 1 pixel per step (horizontal or vertical major)
|
| 16 |
//! ```
|
| 17 |
|
| 18 |
+
use crate::fixed::Fixed;
|
| 19 |
+
|
| 20 |
/// Trait for pixel-addressable surfaces used by line-drawing algorithms.
|
| 21 |
///
|
| 22 |
/// Implemented for `PixelGrid` (test-only, pure Rust) and can be implemented
|
|
|
|
| 66 |
self.data.fill(0);
|
| 67 |
}
|
| 68 |
|
| 69 |
+
/// Load a snapshot file.
|
| 70 |
#[cfg(test)]
|
| 71 |
pub fn from_snapshot(bytes: &[u8]) -> Self {
|
| 72 |
let width = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
|
|
|
|
| 120 |
// Fixed-point line clipping (port of 0x4F7150)
|
| 121 |
// =========================================================================
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
/// Clip a line segment to the writer's clip rectangle.
|
| 124 |
///
|
| 125 |
/// Port of 0x4F7150. All coordinates are Fixed-point (16.16).
|
|
|
|
| 127 |
///
|
| 128 |
/// The clip rectangle is scaled from the writer's pixel bounds to Fixed.
|
| 129 |
fn clip_line(
|
| 130 |
+
x1: &mut Fixed,
|
| 131 |
+
y1: &mut Fixed,
|
| 132 |
+
x2: &mut Fixed,
|
| 133 |
+
y2: &mut Fixed,
|
| 134 |
writer: &dyn PixelWriter,
|
| 135 |
) -> bool {
|
| 136 |
+
let cl = Fixed::from_int(writer.clip_left());
|
| 137 |
+
let ct = Fixed::from_int(writer.clip_top());
|
| 138 |
+
let cr = Fixed::from_int(writer.clip_right());
|
| 139 |
+
let cb = Fixed::from_int(writer.clip_bottom());
|
| 140 |
|
| 141 |
// Cohen-Sutherland outcodes
|
| 142 |
let mut code1 = 0u8;
|
|
|
|
| 174 |
|
| 175 |
// Clip endpoint 1 against x bounds
|
| 176 |
if *x1 < cl {
|
| 177 |
+
let ratio = (cl - *x1).div_raw(*x2 - *x1);
|
| 178 |
+
*y1 += (*y2 - *y1).mul_raw(ratio);
|
| 179 |
*x1 = cl;
|
| 180 |
} else if *x1 > cr {
|
| 181 |
+
let ratio = (cr - *x1).div_raw(*x2 - *x1);
|
| 182 |
+
*y1 += (*y2 - *y1).mul_raw(ratio);
|
| 183 |
*x1 = cr;
|
| 184 |
}
|
| 185 |
|
| 186 |
// Clip endpoint 2 against x bounds
|
| 187 |
if *x2 < cl {
|
| 188 |
+
let ratio = (cl - *x2).div_raw(*x1 - *x2);
|
| 189 |
+
*y2 += (*y1 - *y2).mul_raw(ratio);
|
| 190 |
*x2 = cl;
|
| 191 |
} else if *x2 > cr {
|
| 192 |
+
let ratio = (cr - *x2).div_raw(*x1 - *x2);
|
| 193 |
+
*y2 += (*y1 - *y2).mul_raw(ratio);
|
| 194 |
*x2 = cr;
|
| 195 |
}
|
| 196 |
|
| 197 |
// Check y visibility after x clipping
|
|
|
|
|
|
|
|
|
|
| 198 |
if !(*y1 <= cb || *y2 <= cb) {
|
| 199 |
return false;
|
| 200 |
}
|
|
|
|
| 204 |
|
| 205 |
// Clip endpoint 1 against y bounds
|
| 206 |
if *y1 < ct {
|
| 207 |
+
let ratio = (ct - *y1).div_raw(*y2 - *y1);
|
| 208 |
+
*x1 += (*x2 - *x1).mul_raw(ratio);
|
| 209 |
*y1 = ct;
|
| 210 |
} else if *y1 > cb {
|
| 211 |
+
let ratio = (cb - *y1).div_raw(*y2 - *y1);
|
| 212 |
+
*x1 += (*x2 - *x1).mul_raw(ratio);
|
| 213 |
*y1 = cb;
|
| 214 |
}
|
| 215 |
|
| 216 |
// Clip endpoint 2 against y bounds
|
| 217 |
if *y2 < ct {
|
| 218 |
+
let ratio = (ct - *y2).div_raw(*y1 - *y2);
|
| 219 |
+
*x2 += (*x1 - *x2).mul_raw(ratio);
|
| 220 |
*y2 = ct;
|
| 221 |
} else if *y2 > cb {
|
| 222 |
+
let ratio = (cb - *y2).div_raw(*y1 - *y2);
|
| 223 |
+
*x2 += (*x1 - *x2).mul_raw(ratio);
|
| 224 |
*y2 = cb;
|
| 225 |
}
|
| 226 |
|
|
|
|
| 239 |
/// Horizontal-major DDA rasterizer (port of 0x4F7400).
|
| 240 |
///
|
| 241 |
/// Iterates from x1 to x2 (pixel-rounded), computing y via Fixed-point slope.
|
| 242 |
+
fn raster_hmajor(
|
| 243 |
+
writer: &mut dyn PixelWriter,
|
| 244 |
+
x1: Fixed,
|
| 245 |
+
y1: Fixed,
|
| 246 |
+
x2: Fixed,
|
| 247 |
+
_y2: Fixed,
|
| 248 |
+
color: u8,
|
| 249 |
+
) {
|
| 250 |
+
let start = x1.pixel_center();
|
| 251 |
+
let end = x2.pixel_center();
|
| 252 |
if start == end {
|
| 253 |
return;
|
| 254 |
}
|
| 255 |
|
| 256 |
+
let slope = (_y2 - y1).div_raw(x2 - x1);
|
| 257 |
+
let mut px = start.to_int();
|
| 258 |
+
let mut y = y1; // raw Fixed y, NOT adjusted for sub-pixel offset
|
| 259 |
+
let count = (end - start).to_int();
|
|
|
|
| 260 |
|
| 261 |
for _ in 0..=count {
|
| 262 |
+
writer.put_pixel_clipped(px, y.round_to_int(), color);
|
| 263 |
y += slope;
|
| 264 |
px += 1;
|
| 265 |
}
|
|
|
|
| 268 |
/// Vertical-major DDA rasterizer (port of 0x4F7480).
|
| 269 |
///
|
| 270 |
/// Iterates from y1 to y2 (pixel-rounded), computing x via Fixed-point slope.
|
| 271 |
+
fn raster_vmajor(
|
| 272 |
+
writer: &mut dyn PixelWriter,
|
| 273 |
+
x1: Fixed,
|
| 274 |
+
y1: Fixed,
|
| 275 |
+
_x2: Fixed,
|
| 276 |
+
y2: Fixed,
|
| 277 |
+
color: u8,
|
| 278 |
+
) {
|
| 279 |
+
let start = y1.pixel_center();
|
| 280 |
+
let end = y2.pixel_center();
|
| 281 |
if start == end {
|
| 282 |
return;
|
| 283 |
}
|
| 284 |
|
| 285 |
+
let slope = (_x2 - x1).div_raw(y2 - y1);
|
| 286 |
+
let mut py = start.to_int();
|
| 287 |
+
let mut x = x1; // raw Fixed x
|
| 288 |
+
let count = (end - start).to_int();
|
|
|
|
| 289 |
|
| 290 |
for _ in 0..=count {
|
| 291 |
+
writer.put_pixel_clipped(x.round_to_int(), py, color);
|
| 292 |
x += slope;
|
| 293 |
py += 1;
|
| 294 |
}
|
| 295 |
}
|
| 296 |
|
| 297 |
/// Draw a single-color clipped line. Port of 0x4F7500.
|
|
|
|
|
|
|
| 298 |
pub fn draw_line_clipped(
|
| 299 |
writer: &mut dyn PixelWriter,
|
| 300 |
+
mut x1: Fixed,
|
| 301 |
+
mut y1: Fixed,
|
| 302 |
+
mut x2: Fixed,
|
| 303 |
+
mut y2: Fixed,
|
| 304 |
color: u8,
|
| 305 |
) {
|
| 306 |
+
let dx = (x2 - x1).abs();
|
| 307 |
+
let dy = (y2 - y1).abs();
|
| 308 |
|
| 309 |
+
if dx < dy {
|
| 310 |
// Vertical-major: sort so y1 <= y2
|
| 311 |
if y1 > y2 {
|
| 312 |
core::mem::swap(&mut x1, &mut x2);
|
|
|
|
| 331 |
// Two-color thick line rasterizer (port of 0x4F7A60)
|
| 332 |
// =========================================================================
|
| 333 |
|
| 334 |
+
/// Horizontal-major thick wide rasterizer (port of 0x4F77C0).
|
| 335 |
///
|
| 336 |
+
/// Draws 8 surrounding pixels per step around a 2x2 body center.
|
| 337 |
+
fn raster_thick_hmajor_wide(
|
| 338 |
writer: &mut dyn PixelWriter,
|
| 339 |
+
x1: Fixed,
|
| 340 |
+
y1: Fixed,
|
| 341 |
+
x2: Fixed,
|
| 342 |
+
y2: Fixed,
|
| 343 |
color: u8,
|
| 344 |
) {
|
| 345 |
+
let start = x1.pixel_center();
|
| 346 |
+
let end = x2.pixel_center();
|
| 347 |
if start == end {
|
| 348 |
return;
|
| 349 |
}
|
| 350 |
|
| 351 |
+
let slope = (y2 - y1).div_raw(x2 - x1);
|
| 352 |
+
let mut y = y1 + slope.mul_raw(start - x1);
|
| 353 |
+
let mut px = start.to_int();
|
| 354 |
+
let count = (end - start).to_int();
|
|
|
|
|
|
|
| 355 |
if count == 0 {
|
| 356 |
return;
|
| 357 |
}
|
| 358 |
|
| 359 |
for _ in 0..count {
|
| 360 |
+
let py = y.to_int();
|
|
|
|
| 361 |
writer.put_pixel_clipped(px, py - 1, color);
|
| 362 |
writer.put_pixel_clipped(px + 1, py - 1, color);
|
| 363 |
writer.put_pixel_clipped(px - 1, py, color);
|
|
|
|
| 372 |
}
|
| 373 |
}
|
| 374 |
|
| 375 |
+
/// Horizontal-major thick narrow rasterizer (port of 0x4F75F0).
|
| 376 |
///
|
| 377 |
+
/// Draws 4 body pixels per step (2x2 center), overwriting the wide pass.
|
| 378 |
+
fn raster_thick_hmajor_narrow(
|
| 379 |
writer: &mut dyn PixelWriter,
|
| 380 |
+
x1: Fixed,
|
| 381 |
+
y1: Fixed,
|
| 382 |
+
x2: Fixed,
|
| 383 |
+
y2: Fixed,
|
| 384 |
color: u8,
|
| 385 |
) {
|
| 386 |
+
let start = x1.pixel_center();
|
| 387 |
+
let end = x2.pixel_center();
|
| 388 |
if start == end {
|
| 389 |
return;
|
| 390 |
}
|
| 391 |
|
| 392 |
+
let slope = (y2 - y1).div_raw(x2 - x1);
|
| 393 |
+
let mut y = y1 + slope.mul_raw(start - x1);
|
| 394 |
+
let mut px = start.to_int();
|
| 395 |
+
let count = (end - start).to_int();
|
| 396 |
if count == 0 {
|
| 397 |
return;
|
| 398 |
}
|
| 399 |
|
| 400 |
for _ in 0..count {
|
| 401 |
+
let py = y.to_int();
|
|
|
|
| 402 |
writer.put_pixel_clipped(px, py, color);
|
| 403 |
writer.put_pixel_clipped(px + 1, py, color);
|
| 404 |
writer.put_pixel_clipped(px, py + 1, color);
|
|
|
|
| 409 |
}
|
| 410 |
}
|
| 411 |
|
| 412 |
+
/// Vertical-major thick wide rasterizer (port of 0x4F7910).
|
| 413 |
///
|
| 414 |
+
/// Transposed version — draws 8 surrounding pixels per step.
|
| 415 |
+
fn raster_thick_vmajor_wide(
|
| 416 |
writer: &mut dyn PixelWriter,
|
| 417 |
+
x1: Fixed,
|
| 418 |
+
y1: Fixed,
|
| 419 |
+
x2: Fixed,
|
| 420 |
+
y2: Fixed,
|
| 421 |
color: u8,
|
| 422 |
) {
|
| 423 |
+
let start = y1.pixel_center();
|
| 424 |
+
let end = y2.pixel_center();
|
| 425 |
if start == end {
|
| 426 |
return;
|
| 427 |
}
|
| 428 |
|
| 429 |
+
let slope = (x2 - x1).div_raw(y2 - y1);
|
| 430 |
+
let mut x = x1 + slope.mul_raw(start - y1);
|
| 431 |
+
let mut py = start.to_int();
|
| 432 |
+
let count = (end - start).to_int();
|
| 433 |
|
| 434 |
for _ in 0..count {
|
| 435 |
+
let px = x.to_int();
|
|
|
|
| 436 |
writer.put_pixel_clipped(px, py - 1, color);
|
| 437 |
writer.put_pixel_clipped(px + 1, py - 1, color);
|
| 438 |
writer.put_pixel_clipped(px - 1, py, color);
|
|
|
|
| 447 |
}
|
| 448 |
}
|
| 449 |
|
| 450 |
+
/// Vertical-major thick narrow rasterizer (port of 0x4F76D0).
|
| 451 |
///
|
| 452 |
/// Transposed version — draws 4 body pixels per step.
|
| 453 |
+
fn raster_thick_vmajor_narrow(
|
| 454 |
writer: &mut dyn PixelWriter,
|
| 455 |
+
x1: Fixed,
|
| 456 |
+
y1: Fixed,
|
| 457 |
+
x2: Fixed,
|
| 458 |
+
y2: Fixed,
|
| 459 |
color: u8,
|
| 460 |
) {
|
| 461 |
+
let start = y1.pixel_center();
|
| 462 |
+
let end = y2.pixel_center();
|
| 463 |
if start == end {
|
| 464 |
return;
|
| 465 |
}
|
| 466 |
|
| 467 |
+
let slope = (x2 - x1).div_raw(y2 - y1);
|
| 468 |
+
let mut x = x1 + slope.mul_raw(start - y1);
|
| 469 |
+
let mut py = start.to_int();
|
| 470 |
+
let count = (end - start).to_int();
|
| 471 |
|
| 472 |
for _ in 0..count {
|
| 473 |
+
let px = x.to_int();
|
|
|
|
| 474 |
writer.put_pixel_clipped(px, py, color);
|
| 475 |
writer.put_pixel_clipped(px + 1, py, color);
|
| 476 |
writer.put_pixel_clipped(px, py + 1, color);
|
|
|
|
| 483 |
|
| 484 |
/// Draw a two-color thick clipped line. Port of 0x4F7A60.
|
| 485 |
///
|
| 486 |
+
/// Draws a 2px-wide line. First pass draws 8 surrounding pixels in `color2`,
|
| 487 |
+
/// second pass overwrites 4 center pixels in `color1`.
|
| 488 |
pub fn draw_line_two_color(
|
| 489 |
writer: &mut dyn PixelWriter,
|
| 490 |
+
mut x1: Fixed,
|
| 491 |
+
mut y1: Fixed,
|
| 492 |
+
mut x2: Fixed,
|
| 493 |
+
mut y2: Fixed,
|
| 494 |
color1: u8,
|
| 495 |
color2: u8,
|
| 496 |
) {
|
| 497 |
+
let dx = (x2 - x1).abs();
|
| 498 |
+
let dy = (y2 - y1).abs();
|
| 499 |
|
| 500 |
+
if dx < dy {
|
| 501 |
// Vertical-major
|
| 502 |
+
// Special case: zero horizontal extent → nudge x back by 1 pixel
|
| 503 |
+
if dx == Fixed::ZERO {
|
| 504 |
+
x1 -= Fixed::ONE;
|
| 505 |
+
x2 -= Fixed::ONE;
|
| 506 |
}
|
|
|
|
| 507 |
if y1 > y2 {
|
| 508 |
core::mem::swap(&mut x1, &mut x2);
|
| 509 |
core::mem::swap(&mut y1, &mut y2);
|
| 510 |
}
|
| 511 |
if clip_line(&mut x1, &mut y1, &mut x2, &mut y2, writer) {
|
| 512 |
+
raster_thick_vmajor_wide(writer, x1, y1, x2, y2, color2);
|
| 513 |
+
raster_thick_vmajor_narrow(writer, x1, y1, x2, y2, color1);
|
|
|
|
| 514 |
}
|
| 515 |
} else {
|
| 516 |
// Horizontal-major
|
| 517 |
+
// Special case: zero vertical extent → nudge y back by 1 pixel
|
| 518 |
+
if dy == Fixed::ZERO {
|
| 519 |
+
y1 -= Fixed::ONE;
|
| 520 |
+
y2 -= Fixed::ONE;
|
| 521 |
}
|
|
|
|
| 522 |
if x1 > x2 {
|
| 523 |
core::mem::swap(&mut x1, &mut x2);
|
| 524 |
core::mem::swap(&mut y1, &mut y2);
|
| 525 |
}
|
| 526 |
if clip_line(&mut x1, &mut y1, &mut x2, &mut y2, writer) {
|
| 527 |
+
raster_thick_hmajor_wide(writer, x1, y1, x2, y2, color2);
|
| 528 |
+
raster_thick_hmajor_narrow(writer, x1, y1, x2, y2, color1);
|
|
|
|
| 529 |
}
|
| 530 |
}
|
| 531 |
}
|
|
|
|
| 569 |
assert_eq!(grid.data[4 * grid.row_stride as usize + 3], 0);
|
| 570 |
}
|
| 571 |
|
| 572 |
+
fn f(x: i32) -> Fixed {
|
| 573 |
+
Fixed::from_int(x)
|
| 574 |
}
|
| 575 |
|
| 576 |
fn load_snapshot(name: &str) -> PixelGrid {
|
|
|
|
| 593 |
"{name}: stride mismatch"
|
| 594 |
);
|
| 595 |
if actual.data != expected.data {
|
|
|
|
| 596 |
let mut diff_count = 0;
|
| 597 |
let mut first_diff = None;
|
| 598 |
for y in 0..actual.height {
|
|
|
|
| 681 |
snapshot_test_clipped!(
|
| 682 |
snap_clipped_subpixel,
|
| 683 |
"clipped_subpixel",
|
| 684 |
+
Fixed::from_raw(f(10).to_raw() + 0x8000),
|
| 685 |
+
Fixed::from_raw(f(20).to_raw() + 0x4000),
|
| 686 |
+
Fixed::from_raw(f(100).to_raw() + 0xC000),
|
| 687 |
+
Fixed::from_raw(f(80).to_raw() + 0x2000),
|
| 688 |
7
|
| 689 |
);
|
| 690 |
snapshot_test_clipped!(
|
|
|
|
| 801 |
snapshot_test_twocol!(
|
| 802 |
snap_twocol_subpixel,
|
| 803 |
"twocol_subpixel",
|
| 804 |
+
Fixed::from_raw(f(10).to_raw() + 0x8000),
|
| 805 |
+
Fixed::from_raw(f(20).to_raw() + 0x4000),
|
| 806 |
+
Fixed::from_raw(f(100).to_raw() + 0xC000),
|
| 807 |
+
Fixed::from_raw(f(80).to_raw() + 0x2000),
|
| 808 |
7,
|
| 809 |
8
|
| 810 |
);
|
crates/openwa-core/src/fixed.rs
CHANGED
|
@@ -68,6 +68,47 @@ impl Fixed {
|
|
| 68 |
pub fn abs(self) -> Self {
|
| 69 |
Self(self.0.abs())
|
| 70 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
impl Add for Fixed {
|
|
|
|
| 68 |
pub fn abs(self) -> Self {
|
| 69 |
Self(self.0.abs())
|
| 70 |
}
|
| 71 |
+
|
| 72 |
+
/// Floor to integer boundary: clears the fractional bits.
|
| 73 |
+
/// `Fixed(0x38000).floor()` = `Fixed(0x30000)`.
|
| 74 |
+
#[inline]
|
| 75 |
+
pub const fn floor(self) -> Self {
|
| 76 |
+
Self(self.0 & !0xFFFF)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/// Pixel center: floor + 0.5. Used by line rasterizers to snap to pixel centers.
|
| 80 |
+
#[inline]
|
| 81 |
+
pub const fn pixel_center(self) -> Self {
|
| 82 |
+
Self((self.0 & !0xFFFF) + 0x8000)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/// Round to nearest integer (half-up): `(self + 0.5).to_int()`.
|
| 86 |
+
#[inline]
|
| 87 |
+
pub const fn round_to_int(self) -> i32 {
|
| 88 |
+
(self.0 + 0x8000) >> Self::FRACTIONAL_BITS
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/// Fixed-point division for line clipping: `(self << 16) / rhs`.
|
| 92 |
+
///
|
| 93 |
+
/// This is NOT the same as `Fixed / Fixed` (which is `(self << 16) / rhs`
|
| 94 |
+
/// treating both as Fixed). This divides two Fixed values and returns a
|
| 95 |
+
/// Fixed-point ratio suitable for interpolation.
|
| 96 |
+
#[inline]
|
| 97 |
+
pub fn div_raw(self, rhs: Self) -> Self {
|
| 98 |
+
if rhs.0 == 0 {
|
| 99 |
+
return Self::ZERO;
|
| 100 |
+
}
|
| 101 |
+
Self((((self.0 as i64) << 16) / rhs.0 as i64) as i32)
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/// Fixed-point multiply returning Fixed: `(self * rhs) >> 16`.
|
| 105 |
+
#[inline]
|
| 106 |
+
pub fn mul_raw(self, rhs: Self) -> Self {
|
| 107 |
+
Self(((self.0 as i64 * rhs.0 as i64) >> 16) as i32)
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
/// Half a pixel (0.5 in Fixed).
|
| 111 |
+
pub const HALF: Self = Self(0x8000);
|
| 112 |
}
|
| 113 |
|
| 114 |
impl Add for Fixed {
|