paavohtl Claude Opus 4.6 (1M context) commited on
Commit
059b365
·
1 Parent(s): a1374b5

refactor: use Fixed type throughout line-drawing algorithms

Browse files

Replace 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
- (x1 + cam_x).to_raw(),
700
- (y1 + cam_y).to_raw(),
701
- (x2 + cam_x).to_raw(),
702
- (y2 + cam_y).to_raw(),
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
- (x1 + cam_x).to_raw(),
733
- (y1 + cam_y).to_raw(),
734
- (x2 + cam_x).to_raw(),
735
- (y2 + cam_y).to_raw(),
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 and return (header, pixel_data).
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 i32,
146
- y1: &mut i32,
147
- x2: &mut i32,
148
- y2: &mut i32,
149
  writer: &dyn PixelWriter,
150
  ) -> bool {
151
- let cl = writer.clip_left() << 16;
152
- let ct = writer.clip_top() << 16;
153
- let cr = writer.clip_right() << 16;
154
- let cb = writer.clip_bottom() << 16;
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 = fixed_div(cl - *x1, *x2 - *x1);
193
- *y1 += fixed_mul_shift(*y2 - *y1, ratio);
194
  *x1 = cl;
195
  } else if *x1 > cr {
196
- let ratio = fixed_div(cr - *x1, *x2 - *x1);
197
- *y1 += fixed_mul_shift(*y2 - *y1, ratio);
198
  *x1 = cr;
199
  }
200
 
201
  // Clip endpoint 2 against x bounds
202
  if *x2 < cl {
203
- let ratio = fixed_div(cl - *x2, *x1 - *x2);
204
- *y2 += fixed_mul_shift(*y1 - *y2, ratio);
205
  *x2 = cl;
206
  } else if *x2 > cr {
207
- let ratio = fixed_div(cr - *x2, *x1 - *x2);
208
- *y2 += fixed_mul_shift(*y1 - *y2, ratio);
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 = fixed_div(ct - *y1, *y2 - *y1);
226
- *x1 += fixed_mul_shift(*x2 - *x1, ratio);
227
  *y1 = ct;
228
  } else if *y1 > cb {
229
- let ratio = fixed_div(cb - *y1, *y2 - *y1);
230
- *x1 += fixed_mul_shift(*x2 - *x1, ratio);
231
  *y1 = cb;
232
  }
233
 
234
  // Clip endpoint 2 against y bounds
235
  if *y2 < ct {
236
- let ratio = fixed_div(ct - *y2, *y1 - *y2);
237
- *x2 += fixed_mul_shift(*x1 - *x2, ratio);
238
  *y2 = ct;
239
  } else if *y2 > cb {
240
- let ratio = fixed_div(cb - *y2, *y1 - *y2);
241
- *x2 += fixed_mul_shift(*x1 - *x2, ratio);
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(writer: &mut dyn PixelWriter, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) {
261
- let start = (x1 & !0xFFFF) + 0x8000; // round to pixel center
262
- let end = (x2 & !0xFFFF) + 0x8000;
 
 
 
 
 
 
 
263
  if start == end {
264
  return;
265
  }
266
 
267
- // slope = ((y2 - y1) << 16) / (x2 - x1), in 16.16 fixed
268
- let slope = fixed_div(y2 - y1, x2 - x1);
269
- let mut px = start >> 16;
270
- let mut y = y1; // starts at raw y1, NOT adjusted for sub-pixel offset
271
- let count = (end - start) >> 16;
272
 
273
  for _ in 0..=count {
274
- writer.put_pixel_clipped(px, (y + 0x8000) >> 16, color);
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(writer: &mut dyn PixelWriter, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) {
284
- let start = (y1 & !0xFFFF) + 0x8000;
285
- let end = (y2 & !0xFFFF) + 0x8000;
 
 
 
 
 
 
 
286
  if start == end {
287
  return;
288
  }
289
 
290
- // slope = ((x2 - x1) << 16) / (y2 - y1)
291
- let slope = fixed_div(x2 - x1, y2 - y1);
292
- let mut py = start >> 16;
293
- let mut x = x1; // starts at raw x1
294
- let count = (end - start) >> 16;
295
 
296
  for _ in 0..=count {
297
- writer.put_pixel_clipped((x + 0x8000) >> 16, py, color);
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: i32,
309
- mut y1: i32,
310
- mut x2: i32,
311
- mut y2: i32,
312
  color: u8,
313
  ) {
314
- let abs_dx = (x2 - x1).unsigned_abs();
315
- let abs_dy = (y2 - y1).unsigned_abs();
316
 
317
- if abs_dx < abs_dy {
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 outline rasterizer (port of 0x4F77C0).
343
  ///
344
- /// Draws 8 outline pixels per step around a 2x2 body center.
345
- fn raster_thick_hmajor_outline(
346
  writer: &mut dyn PixelWriter,
347
- x1: i32,
348
- y1: i32,
349
- x2: i32,
350
- y2: i32,
351
  color: u8,
352
  ) {
353
- let start = (x1 & !0xFFFF_i32) + 0x8000;
354
- let end = (x2 & !0xFFFF_i32) + 0x8000;
355
  if start == end {
356
  return;
357
  }
358
 
359
- // slope = ((y2 - y1) << 16) / (x2 - x1)
360
- let slope = fixed_div(y2 - y1, x2 - x1);
361
- // Sub-pixel y adjustment for pixel-center start
362
- let mut y = y1 + fixed_mul_shift(slope, start - x1);
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 >> 16;
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 fill rasterizer (port of 0x4F75F0).
387
  ///
388
- /// Draws 4 body pixels per step (2x2 center).
389
- fn raster_thick_hmajor_fill(
390
  writer: &mut dyn PixelWriter,
391
- x1: i32,
392
- y1: i32,
393
- x2: i32,
394
- y2: i32,
395
  color: u8,
396
  ) {
397
- let start = (x1 & !0xFFFF_i32) + 0x8000;
398
- let end = (x2 & !0xFFFF_i32) + 0x8000;
399
  if start == end {
400
  return;
401
  }
402
 
403
- let slope = fixed_div(y2 - y1, x2 - x1);
404
- let mut y = y1 + fixed_mul_shift(slope, start - x1);
405
- let mut px = start >> 16;
406
- let count = (end - start) >> 16;
407
  if count == 0 {
408
  return;
409
  }
410
 
411
  for _ in 0..count {
412
- let py = y >> 16;
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 outline rasterizer (port of 0x4F7910).
425
  ///
426
- /// Transposed version of the horizontal-major outline — draws 8 pixels per step.
427
- fn raster_thick_vmajor_outline(
428
  writer: &mut dyn PixelWriter,
429
- x1: i32,
430
- y1: i32,
431
- x2: i32,
432
- y2: i32,
433
  color: u8,
434
  ) {
435
- let start = (y1 & !0xFFFF_i32) + 0x8000;
436
- let end = (y2 & !0xFFFF_i32) + 0x8000;
437
  if start == end {
438
  return;
439
  }
440
 
441
- let slope = fixed_div(x2 - x1, y2 - y1);
442
- let mut x = x1 + fixed_mul_shift(slope, start - y1);
443
- let mut py = start >> 16;
444
- let count = (end - start) >> 16;
445
 
446
  for _ in 0..count {
447
- let px = x >> 16;
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 fill rasterizer (port of 0x4F76D0).
464
  ///
465
  /// Transposed version — draws 4 body pixels per step.
466
- fn raster_thick_vmajor_fill(
467
  writer: &mut dyn PixelWriter,
468
- x1: i32,
469
- y1: i32,
470
- x2: i32,
471
- y2: i32,
472
  color: u8,
473
  ) {
474
- let start = (y1 & !0xFFFF_i32) + 0x8000;
475
- let end = (y2 & !0xFFFF_i32) + 0x8000;
476
  if start == end {
477
  return;
478
  }
479
 
480
- let slope = fixed_div(x2 - x1, y2 - y1);
481
- let mut x = x1 + fixed_mul_shift(slope, start - y1);
482
- let mut py = start >> 16;
483
- let count = (end - start) >> 16;
484
 
485
  for _ in 0..count {
486
- let px = x >> 16;
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
- /// All coordinates are Fixed-point (16.16). Draws a 2px-wide line with
501
- /// `color1` as outline and `color2` as body fill.
502
  pub fn draw_line_two_color(
503
  writer: &mut dyn PixelWriter,
504
- mut x1: i32,
505
- mut y1: i32,
506
- mut x2: i32,
507
- mut y2: i32,
508
  color1: u8,
509
  color2: u8,
510
  ) {
511
- let abs_dx = (x2 - x1).unsigned_abs();
512
- let abs_dy = (y2 - y1).unsigned_abs();
513
 
514
- if abs_dx < abs_dy {
515
  // Vertical-major
516
- // Special case: zero horizontal extent → nudge x coords back by 1 pixel
517
- if abs_dx == 0 {
518
- x1 -= 0x10000;
519
- x2 -= 0x10000;
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
- // First pass: color2 wide (8 pixels/step), then color1 narrow (4 pixels/step)
528
- raster_thick_vmajor_outline(writer, x1, y1, x2, y2, color2);
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 coords back by 1 pixel
534
- if abs_dy == 0 {
535
- y1 -= 0x10000;
536
- y2 -= 0x10000;
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
- // First pass: color2 wide (8 pixels/step), then color1 narrow (4 pixels/step)
545
- raster_thick_hmajor_outline(writer, x1, y1, x2, y2, color2);
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) -> i32 {
591
- x << 16
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 {