1use crate::{
9 assets::{MapData, SpriteSheet, SPRITES_PER_ROW, SPRITE_COUNT, SPRITE_SIZE},
10 font, palette,
11};
12
13pub const WIDTH: i32 = 128;
15pub const HEIGHT: i32 = 128;
17
18const IDENTITY_PALETTE: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
20const DEFAULT_TRANSPARENT: u16 = 0x0001;
22
23pub struct Framebuffer {
25 pixels: Vec<u8>,
26 camera_x: i32,
27 camera_y: i32,
28 clip: (i32, i32, i32, i32),
29 draw_pal: [u8; 16],
30 display_pal: [u8; 16],
31 transparent: u16,
32 fill_pattern: u16,
33 fill_secondary: u8,
34 fill_transparent: bool,
35 pen_color: u8,
36 cursor_x: i32,
37 cursor_y: i32,
38}
39
40impl Default for Framebuffer {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl Framebuffer {
47 pub fn new() -> Self {
48 Self {
49 pixels: vec![0; (WIDTH * HEIGHT) as usize],
50 camera_x: 0,
51 camera_y: 0,
52 clip: (0, 0, WIDTH, HEIGHT),
53 draw_pal: IDENTITY_PALETTE,
54 display_pal: IDENTITY_PALETTE,
55 transparent: DEFAULT_TRANSPARENT,
56 fill_pattern: 0,
57 fill_secondary: 0,
58 fill_transparent: false,
59 pen_color: 6,
60 cursor_x: 0,
61 cursor_y: 0,
62 }
63 }
64
65 pub fn pixels(&self) -> &[u8] {
67 &self.pixels
68 }
69
70 pub fn display_palette(&self) -> &[u8; 16] {
74 &self.display_pal
75 }
76
77 pub fn write_rgba(&self, out: &mut [u8]) {
79 let mut lut = [[0u8; 4]; 16];
84 for (i, entry) in lut.iter_mut().enumerate() {
85 *entry = palette::rgba(self.display_pal[i]);
86 }
87 let (chunks, _) = out.as_chunks_mut::<4>();
88 for (chunk, &c) in chunks.iter_mut().zip(self.pixels.iter()) {
89 *chunk = lut[(c & 0x0f) as usize];
90 }
91 }
92
93 pub fn camera(&mut self, x: i32, y: i32) {
95 self.camera_x = x;
96 self.camera_y = y;
97 }
98
99 pub fn clip(&mut self, x: i32, y: i32, w: i32, h: i32) {
101 let x0 = x.clamp(0, WIDTH);
102 let y0 = y.clamp(0, HEIGHT);
103 let x1 = (x + w).clamp(0, WIDTH);
104 let y1 = (y + h).clamp(0, HEIGHT);
105 self.clip = (x0, y0, x1, y1);
106 }
107
108 pub fn clip_reset(&mut self) {
110 self.clip = (0, 0, WIDTH, HEIGHT);
111 }
112
113 pub fn reset_state(&mut self) {
115 self.camera_x = 0;
116 self.camera_y = 0;
117 self.clip_reset();
118 self.draw_pal = IDENTITY_PALETTE;
119 self.display_pal = IDENTITY_PALETTE;
120 self.transparent = DEFAULT_TRANSPARENT;
121 self.fill_pattern = 0;
122 self.fill_secondary = 0;
123 self.fill_transparent = false;
124 self.pen_color = 6;
125 self.cursor_x = 0;
126 self.cursor_y = 0;
127 }
128
129 pub fn set_transparent_color(&mut self, color: u8, transparent: bool) {
131 let bit = 1u16 << (color & 0x0f);
132 if transparent {
133 self.transparent |= bit;
134 } else {
135 self.transparent &= !bit;
136 }
137 }
138
139 pub fn reset_transparency(&mut self) {
141 self.transparent = DEFAULT_TRANSPARENT;
142 }
143
144 pub fn remap_color(&mut self, from: u8, to: u8) {
146 self.draw_pal[(from & 0x0f) as usize] = to & 0x0f;
147 }
148
149 pub fn remap_display_color(&mut self, from: u8, to: u8) {
151 self.display_pal[(from & 0x0f) as usize] = to & 0x0f;
152 }
153
154 pub fn reset_palette(&mut self) {
156 self.draw_pal = IDENTITY_PALETTE;
157 self.display_pal = IDENTITY_PALETTE;
158 }
159
160 pub fn set_fill_pattern(&mut self, pattern: u16, secondary: u8, transparent: bool) {
165 self.fill_pattern = pattern;
166 self.fill_secondary = secondary & 0x0f;
167 self.fill_transparent = transparent;
168 }
169
170 fn fill_color_at(&self, x: i32, y: i32, primary: u8) -> Option<u8> {
173 if self.fill_pattern == 0 {
174 return Some(primary);
175 }
176 let idx = ((y & 3) * 4 + (x & 3)) as u16;
177 if (self.fill_pattern >> (15 - idx)) & 1 == 0 {
178 Some(primary)
179 } else if self.fill_transparent {
180 None
181 } else {
182 Some(self.fill_secondary)
183 }
184 }
185
186 fn raw_pset_fill(&mut self, x: i32, y: i32, primary: u8) {
188 if let Some(c) = self.fill_color_at(x, y, primary) {
189 self.raw_pset(x, y, c);
190 }
191 }
192
193 fn fill_span(&mut self, x0: i32, x1: i32, y: i32, color: u8) {
198 let (cx0, cy0, cx1, cy1) = self.clip;
199 if y < cy0 || y >= cy1 {
200 return;
201 }
202 let xa = x0.max(cx0);
203 let xb = x1.min(cx1 - 1);
204 if xa > xb {
205 return;
206 }
207 let c = self.draw_pal[(color & 0x0f) as usize] & 0x0f;
208 let start = (y * WIDTH + xa) as usize;
209 let end = (y * WIDTH + xb + 1) as usize;
210 self.pixels[start..end].fill(c);
211 }
212
213 fn plot_far(&mut self, x: i64, y: i64, color: u8) {
218 let (cx0, cy0, cx1, cy1) = self.clip;
219 if x >= i64::from(cx0) && x < i64::from(cx1) && y >= i64::from(cy0) && y < i64::from(cy1) {
220 self.raw_pset(x as i32, y as i32, color);
221 }
222 }
223
224 fn clipped_span_far(&self, x0: i64, x1: i64, y: i64) -> Option<(i32, i32, i32)> {
227 let (cx0, cy0, cx1, cy1) = self.clip;
228 if y < i64::from(cy0) || y >= i64::from(cy1) {
229 return None;
230 }
231 let xa = x0.max(i64::from(cx0));
232 let xb = x1.min(i64::from(cx1) - 1);
233 if xa > xb {
234 return None;
235 }
236 Some((xa as i32, xb as i32, y as i32))
237 }
238
239 fn clipped_rows(&self, ya: i32, yb: i32) -> (i32, i32) {
244 let (_, cy0, _, cy1) = self.clip;
245 (
246 ya.max(cy0.saturating_add(self.camera_y)),
247 yb.min((cy1 - 1).saturating_add(self.camera_y)),
248 )
249 }
250
251 fn clipped_cols(&self, xa: i32, xb: i32) -> (i32, i32) {
254 let (cx0, _, cx1, _) = self.clip;
255 (
256 xa.max(cx0.saturating_add(self.camera_x)),
257 xb.min((cx1 - 1).saturating_add(self.camera_x)),
258 )
259 }
260
261 pub fn cls(&mut self, color: u8) {
263 self.pixels.fill(color & 0x0f);
264 }
265
266 #[inline]
267 fn raw_pset(&mut self, x: i32, y: i32, color: u8) {
268 let (cx0, cy0, cx1, cy1) = self.clip;
269 if x >= cx0 && x < cx1 && y >= cy0 && y < cy1 {
270 let c = self.draw_pal[(color & 0x0f) as usize] & 0x0f;
271 self.pixels[(y * WIDTH + x) as usize] = c;
272 }
273 }
274
275 pub fn pset(&mut self, x: i32, y: i32, color: u8) {
277 self.raw_pset(x - self.camera_x, y - self.camera_y, color);
278 }
279
280 pub fn pget(&self, x: i32, y: i32) -> u8 {
282 if (0..WIDTH).contains(&x) && (0..HEIGHT).contains(&y) {
283 self.pixels[(y * WIDTH + x) as usize]
284 } else {
285 0
286 }
287 }
288
289 pub fn line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
291 let (ax, ay) = (
293 i64::from(x0) - i64::from(self.camera_x),
294 i64::from(y0) - i64::from(self.camera_y),
295 );
296 let (bx, by) = (
297 i64::from(x1) - i64::from(self.camera_x),
298 i64::from(y1) - i64::from(self.camera_y),
299 );
300 let (dx, dy) = ((bx - ax).abs(), (by - ay).abs());
301 let (sx, sy) = (if ax < bx { 1 } else { -1 }, if ay < by { 1 } else { -1 });
302 let steps = dx.max(dy);
310 let x_major = dx >= dy;
311 let (cx0, cy0, cx1, cy1) = self.clip;
312 let (start, step, lo, hi) = if x_major {
313 (ax, sx, i64::from(cx0), i64::from(cx1) - 1)
314 } else {
315 (ay, sy, i64::from(cy0), i64::from(cy1) - 1)
316 };
317 let (k_lo, k_hi) = if step > 0 {
318 (lo - start, hi - start)
319 } else {
320 (start - hi, start - lo)
321 };
322 let (major, minor) = if x_major { (dx, dy) } else { (dy, dx) };
323 for k in k_lo.max(0)..=k_hi.min(steps) {
324 let m = if major == 0 {
326 0
327 } else {
328 ((2 * i128::from(minor) * i128::from(k) + i128::from(major))
329 / (2 * i128::from(major))) as i64
330 };
331 let (px, py) = if x_major {
332 (ax + sx * k, ay + sy * m)
333 } else {
334 (ax + sx * m, ay + sy * k)
335 };
336 self.plot_far(px, py, color);
337 }
338 }
339
340 pub fn rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
342 let (xa, xb) = (x0.min(x1), x0.max(x1));
343 let (ya, yb) = (y0.min(y1), y0.max(y1));
344 self.line(xa, ya, xb, ya, color);
345 self.line(xa, yb, xb, yb, color);
346 self.line(xa, ya, xa, yb, color);
347 self.line(xb, ya, xb, yb, color);
348 }
349
350 pub fn rectfill(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
352 let (xa, xb) = (x0.min(x1), x0.max(x1));
353 let (ya, yb) = (y0.min(y1), y0.max(y1));
354 let (ra, rb) = self.clipped_rows(ya, yb);
359 if self.fill_pattern == 0 {
360 for y in ra..=rb {
362 self.fill_span(
363 xa - self.camera_x,
364 xb - self.camera_x,
365 y - self.camera_y,
366 color,
367 );
368 }
369 } else {
370 let (ca, cb) = self.clipped_cols(xa, xb);
371 for y in ra..=rb {
372 for x in ca..=cb {
373 self.raw_pset_fill(x - self.camera_x, y - self.camera_y, color);
374 }
375 }
376 }
377 }
378
379 pub fn circ(&mut self, cx: i32, cy: i32, r: i32, color: u8) {
381 self.circle_impl(cx, cy, r.max(0), color, false);
382 }
383
384 pub fn circfill(&mut self, cx: i32, cy: i32, r: i32, color: u8) {
386 self.circle_impl(cx, cy, r.max(0), color, true);
387 }
388
389 fn circle_impl(&mut self, cx: i32, cy: i32, r: i32, color: u8, fill: bool) {
390 let (cx, cy) = (
391 i64::from(cx) - i64::from(self.camera_x),
392 i64::from(cy) - i64::from(self.camera_y),
393 );
394 let r = i64::from(r);
395 let (cx0, cy0, cx1, cy1) = self.clip;
396 let (cx0, cy0, cx1, cy1) = (
397 i64::from(cx0),
398 i64::from(cy0),
399 i64::from(cx1) - 1,
400 i64::from(cy1) - 1,
401 );
402 if cx + r < cx0 || cx - r > cx1 || cy + r < cy0 || cy - r > cy1 {
405 return;
406 }
407
408 let shallow = [(cy0 - cy, cy1 - cy), (cy - cy1, cy - cy0)];
419 let steep = if fill {
420 [
424 arc_rows_to_y(r, cy0 - cy, cy1 - cy),
425 arc_rows_to_y(r, cy - cy1, cy - cy0),
426 ]
427 } else {
428 [(cx0 - cx, cx1 - cx), (cx - cx1, cx - cx0)]
431 };
432 for (run_lo, run_hi) in merge_runs(shallow, steep, r) {
433 let mut y = run_lo;
434 let mut x = arc_x(r, y);
435 let mut err = x * x - r * r + y * y + 2 * y - x + 1;
438 while x >= y && y <= run_hi {
439 if fill && self.fill_pattern == 0 {
440 self.circle_span(cx - x, cx + x, cy + y, color);
442 self.circle_span(cx - x, cx + x, cy - y, color);
443 self.circle_span(cx - y, cx + y, cy + x, color);
444 self.circle_span(cx - y, cx + y, cy - x, color);
445 } else if fill {
446 self.circle_run(cx - x, cx + x, cy + y, color);
447 self.circle_run(cx - x, cx + x, cy - y, color);
448 self.circle_run(cx - y, cx + y, cy + x, color);
449 self.circle_run(cx - y, cx + y, cy - x, color);
450 } else {
451 for (px, py) in [
452 (cx + x, cy + y),
453 (cx - x, cy + y),
454 (cx + x, cy - y),
455 (cx - x, cy - y),
456 (cx + y, cy + x),
457 (cx - y, cy + x),
458 (cx + y, cy - x),
459 (cx - y, cy - x),
460 ] {
461 self.plot_far(px, py, color);
462 }
463 }
464 y += 1;
465 if err < 0 {
466 err += 2 * y + 1;
467 } else {
468 x -= 1;
469 err += 2 * (y - x) + 1;
470 }
471 }
472 }
473 }
474
475 fn circle_span(&mut self, x0: i64, x1: i64, y: i64, color: u8) {
477 if let Some((xa, xb, y)) = self.clipped_span_far(x0, x1, y) {
478 self.fill_span(xa, xb, y, color);
479 }
480 }
481
482 fn circle_run(&mut self, x0: i64, x1: i64, y: i64, color: u8) {
485 let Some((xa, xb, y)) = self.clipped_span_far(x0, x1, y) else {
486 return;
487 };
488 for x in xa..=xb {
489 self.raw_pset_fill(x, y, color);
490 }
491 }
492
493 pub fn oval(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
495 self.oval_impl(x0, y0, x1, y1, color, false);
496 }
497
498 pub fn ovalfill(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
500 self.oval_impl(x0, y0, x1, y1, color, true);
501 }
502
503 fn oval_impl(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: u8, fill: bool) {
504 let (xa, xb) = (x0.min(x1), x0.max(x1));
505 let (ya, yb) = (y0.min(y1), y0.max(y1));
506 let cx = (xa + xb) as f32 / 2.0;
507 let cy = (ya + yb) as f32 / 2.0;
508 let a = (xb - xa) as f32 / 2.0;
509 let b = (yb - ya) as f32 / 2.0;
510 let (ra, rb) = self.clipped_rows(ya, yb);
514 if fill {
515 for y in ra..=rb {
516 let dy = if b > 0.0 { (y as f32 - cy) / b } else { 0.0 };
517 let s = 1.0 - dy * dy;
518 if s < 0.0 {
519 continue;
520 }
521 let dx = a * s.sqrt();
522 let left = (cx - dx).round() as i32;
523 let right = (cx + dx).round() as i32;
524 if self.fill_pattern == 0 {
525 self.fill_span(
527 left - self.camera_x,
528 right - self.camera_x,
529 y - self.camera_y,
530 color,
531 );
532 } else {
533 let (ca, cb) = self.clipped_cols(left, right);
534 for x in ca..=cb {
535 self.raw_pset_fill(x - self.camera_x, y - self.camera_y, color);
536 }
537 }
538 }
539 } else {
540 for y in ra..=rb {
542 let dy = if b > 0.0 { (y as f32 - cy) / b } else { 0.0 };
543 let s = 1.0 - dy * dy;
544 if s < 0.0 {
545 continue;
546 }
547 let dx = a * s.sqrt();
548 self.pset((cx - dx).round() as i32, y, color);
549 self.pset((cx + dx).round() as i32, y, color);
550 }
551 let (ca, cb) = self.clipped_cols(xa, xb);
552 for x in ca..=cb {
553 let dx = if a > 0.0 { (x as f32 - cx) / a } else { 0.0 };
554 let s = 1.0 - dx * dx;
555 if s < 0.0 {
556 continue;
557 }
558 let dy = b * s.sqrt();
559 self.pset(x, (cy - dy).round() as i32, color);
560 self.pset(x, (cy + dy).round() as i32, color);
561 }
562 }
563 }
564
565 pub fn print(&mut self, text: &str, x: i32, y: i32, color: u8) -> i32 {
568 let mut cx = x;
569 let mut cy = y;
570 for ch in text.chars() {
571 if ch == '\n' {
572 cx = x;
573 cy += font::GLYPH_H;
574 continue;
575 }
576 let rows = font::glyph(ch);
577 for (ry, row) in rows.iter().enumerate() {
578 for rx in 0..3 {
579 if row & (0b100 >> rx) != 0 {
580 self.pset(cx + rx, cy + ry as i32, color);
581 }
582 }
583 }
584 cx += font::GLYPH_W;
585 }
586 cx
587 }
588
589 pub fn set_pen_color(&mut self, color: u8) {
591 self.pen_color = color & 0x0f;
592 }
593
594 pub fn set_cursor(&mut self, x: i32, y: i32) {
596 self.cursor_x = x;
597 self.cursor_y = y;
598 }
599
600 pub fn print_pen(&mut self, text: &str) -> i32 {
603 let (x, y) = (self.cursor_x, self.cursor_y);
604 let end = self.print(text, x, y, self.pen_color);
605 self.cursor_y = y + font::GLYPH_H;
606 end
607 }
608
609 #[allow(clippy::too_many_arguments)]
613 pub fn spr(
614 &mut self,
615 sheet: &SpriteSheet,
616 n: u32,
617 x: i32,
618 y: i32,
619 w: i32,
620 h: i32,
621 flip_x: bool,
622 flip_y: bool,
623 ) {
624 let pw = w.max(0);
626 let ph = h.max(0);
627
628 let (dx0, dy0) = (x - self.camera_x, y - self.camera_y);
635 let (cx0, cy0, cx1, cy1) = self.clip;
636 let px_lo = (cx0 - dx0).max(0);
637 let px_hi = (cx1 - dx0).min(pw);
638 let py_lo = (cy0 - dy0).max(0);
639 let py_hi = (cy1 - dy0).min(ph);
640 if px_lo >= px_hi || py_lo >= py_hi {
641 return;
642 }
643
644 let n = (n as usize) % SPRITE_COUNT;
649 let base_sx = (n % SPRITES_PER_ROW * SPRITE_SIZE) as i32;
650 let base_sy = (n / SPRITES_PER_ROW * SPRITE_SIZE) as i32;
651 for py in py_lo..py_hi {
652 let sy = if flip_y { ph - 1 - py } else { py };
653 for px in px_lo..px_hi {
654 let sx = if flip_x { pw - 1 - px } else { px };
655 let c = sheet.get(base_sx + sx, base_sy + sy);
656 if ((self.transparent >> c) & 1) == 0 {
657 let px_dst = dx0 + px;
659 let py_dst = dy0 + py;
660 self.pixels[(py_dst * WIDTH + px_dst) as usize] =
661 self.draw_pal[(c & 0x0f) as usize] & 0x0f;
662 }
663 }
664 }
665 }
666
667 #[allow(clippy::too_many_arguments)]
671 pub fn sspr(
672 &mut self,
673 sheet: &SpriteSheet,
674 sx: i32,
675 sy: i32,
676 sw: i32,
677 sh: i32,
678 dx: i32,
679 dy: i32,
680 dw: i32,
681 dh: i32,
682 flip_x: bool,
683 flip_y: bool,
684 ) {
685 if sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 {
686 return;
687 }
688
689 let (dx0, dy0) = (
695 i64::from(dx) - i64::from(self.camera_x),
696 i64::from(dy) - i64::from(self.camera_y),
697 );
698 let (cx0, cy0, cx1, cy1) = self.clip;
699 let px_lo = (i64::from(cx0) - dx0).clamp(0, i64::from(dw)) as i32;
700 let px_hi = (i64::from(cx1) - dx0).clamp(0, i64::from(dw)) as i32;
701 let py_lo = (i64::from(cy0) - dy0).clamp(0, i64::from(dh)) as i32;
702 let py_hi = (i64::from(cy1) - dy0).clamp(0, i64::from(dh)) as i32;
703 if px_lo >= px_hi || py_lo >= py_hi {
704 return;
705 }
706
707 for py in py_lo..py_hi {
708 let fy = if flip_y { dh - 1 - py } else { py };
709 let src_y = sy.saturating_add((i64::from(fy) * i64::from(sh) / i64::from(dh)) as i32);
712 let row = (dy0 + i64::from(py)) as usize * WIDTH as usize;
713 for px in px_lo..px_hi {
714 let fx = if flip_x { dw - 1 - px } else { px };
715 let src_x =
716 sx.saturating_add((i64::from(fx) * i64::from(sw) / i64::from(dw)) as i32);
717 let c = sheet.get(src_x, src_y);
718 if ((self.transparent >> c) & 1) == 0 {
719 let col = (dx0 + i64::from(px)) as usize;
721 self.pixels[row + col] = self.draw_pal[(c & 0x0f) as usize] & 0x0f;
722 }
723 }
724 }
725 }
726
727 #[allow(clippy::too_many_arguments)]
730 pub fn map(
731 &mut self,
732 map: &MapData,
733 sheet: &SpriteSheet,
734 cel_x: i32,
735 cel_y: i32,
736 sx: i32,
737 sy: i32,
738 cel_w: i32,
739 cel_h: i32,
740 layers: u8,
741 ) {
742 let (cel_w, cel_h) = (i64::from(cel_w.max(0)), i64::from(cel_h.max(0)));
747 let cel_px = SPRITE_SIZE as i64;
749 let (cx0, cy0, cx1, cy1) = self.clip;
750 let base_x = i64::from(sx) - i64::from(self.camera_x);
751 let base_y = i64::from(sy) - i64::from(self.camera_y);
752 let tx_lo = (i64::from(cx0) - base_x).div_euclid(cel_px).clamp(0, cel_w) as i32;
756 let tx_hi = (i64::from(cx1) - base_x + cel_px - 1)
757 .div_euclid(cel_px)
758 .clamp(0, cel_w) as i32;
759 let ty_lo = (i64::from(cy0) - base_y).div_euclid(cel_px).clamp(0, cel_h) as i32;
760 let ty_hi = (i64::from(cy1) - base_y + cel_px - 1)
761 .div_euclid(cel_px)
762 .clamp(0, cel_h) as i32;
763
764 for ty in ty_lo..ty_hi {
765 for tx in tx_lo..tx_hi {
766 let tile = map.get(cel_x + tx, cel_y + ty);
767 if tile == 0 {
768 continue;
769 }
770 if layers != 0 && sheet.flags(tile as u32) & layers == 0 {
771 continue;
772 }
773 self.spr(
774 sheet,
775 tile as u32,
776 sx + tx * SPRITE_SIZE as i32,
777 sy + ty * SPRITE_SIZE as i32,
778 SPRITE_SIZE as i32,
779 SPRITE_SIZE as i32,
780 false,
781 false,
782 );
783 }
784 }
785 }
786}
787
788fn merge_runs(shallow: [(i64, i64); 2], steep: [(i64, i64); 2], cap: i64) -> [(i64, i64); 4] {
792 let mut runs = [(0i64, -1i64); 4];
793 let mut n = 0;
794 for (lo, hi) in shallow.into_iter().chain(steep) {
795 let (lo, hi) = (lo.max(0), hi.min(cap));
796 if lo <= hi {
797 runs[n] = (lo, hi);
798 n += 1;
799 }
800 }
801 runs[..n].sort_unstable();
802 let mut merged = [(0i64, -1i64); 4];
803 let mut m = 0;
804 for &(lo, hi) in &runs[..n] {
805 if m > 0 && lo <= merged[m - 1].1 + 1 {
806 merged[m - 1].1 = merged[m - 1].1.max(hi);
807 } else {
808 merged[m] = (lo, hi);
809 m += 1;
810 }
811 }
812 merged
813}
814
815fn arc_rows_to_y(r: i64, lo: i64, hi: i64) -> (i64, i64) {
818 let (lo, hi) = (lo.max(0), hi.min(r));
820 if lo > hi {
821 return (0, -1);
822 }
823 let y_lo = ceil_sqrt((r * r - hi * (hi + 1)).max(0));
826 let y_hi = if lo == 0 {
827 r
828 } else {
829 ceil_sqrt((r * r - (lo - 1) * lo).max(0)) - 1
830 };
831 (y_lo, y_hi)
832}
833
834fn arc_x(r: i64, y: i64) -> i64 {
840 let m = r * r - y * y;
841 if m <= 0 {
842 return 0;
843 }
844 let v = ceil_sqrt(m);
845 if v > 0 && (v - 1) * v >= m {
846 v - 1
847 } else {
848 v
849 }
850}
851
852fn ceil_sqrt(m: i64) -> i64 {
854 let s = m.isqrt();
855 if s * s < m {
856 s + 1
857 } else {
858 s
859 }
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865 use std::{sync::mpsc, thread, time::Duration};
866
867 #[test]
868 fn cls_fills_screen() {
869 let mut fb = Framebuffer::new();
870 fb.cls(7);
871 assert!(fb.pixels().iter().all(|&p| p == 7));
872 }
873
874 #[test]
875 fn pset_pget_roundtrip() {
876 let mut fb = Framebuffer::new();
877 fb.pset(10, 20, 8);
878 assert_eq!(fb.pget(10, 20), 8);
879 assert_eq!(fb.pget(11, 20), 0);
880 }
881
882 #[test]
883 fn out_of_bounds_is_safe() {
884 let mut fb = Framebuffer::new();
885 fb.pset(-1, 0, 5);
886 fb.pset(0, 99999, 5);
887 fb.line(-50, -50, 200, 200, 6);
888 fb.circfill(0, 0, 300, 3);
889 assert_eq!(fb.pget(-1, 0), 0);
890 }
891
892 #[test]
893 fn camera_offsets_draws() {
894 let mut fb = Framebuffer::new();
895 fb.camera(10, 0);
896 fb.pset(15, 5, 9);
897 assert_eq!(fb.pget(5, 5), 9);
898 fb.reset_state();
899 fb.pset(15, 5, 9);
900 assert_eq!(fb.pget(15, 5), 9);
901 }
902
903 #[test]
904 fn clip_constrains_drawing() {
905 let mut fb = Framebuffer::new();
906 fb.clip(0, 0, 4, 4);
907 fb.rectfill(0, 0, 127, 127, 7);
908 assert_eq!(fb.pget(3, 3), 7);
909 assert_eq!(fb.pget(4, 4), 0);
910 }
911
912 #[test]
913 fn rect_outline_is_hollow() {
914 let mut fb = Framebuffer::new();
915 fb.rect(0, 0, 4, 4, 7);
916 assert_eq!(fb.pget(0, 0), 7);
917 assert_eq!(fb.pget(4, 4), 7);
918 assert_eq!(fb.pget(2, 2), 0);
919 }
920
921 #[test]
922 fn print_advances_cursor() {
923 let mut fb = Framebuffer::new();
924 let end = fb.print("abc", 0, 0, 7);
925 assert_eq!(end, 3 * font::GLYPH_W);
926 }
927
928 #[test]
929 fn partial_pixel_sprite_draws_a_partial_slice() {
930 let mut fb = Framebuffer::new();
931 let mut sheet = SpriteSheet::default();
932 for y in 0..8 {
934 for x in 0..8 {
935 sheet.set(x, y, 7);
936 }
937 }
938 fb.spr(&sheet, 0, 0, 0, 4, 8, false, false);
940 assert_eq!(fb.pget(3, 4), 7, "left half is drawn");
941 assert_eq!(fb.pget(4, 4), 0, "right half is untouched");
942 assert_eq!(fb.pget(7, 7), 0, "bottom-right corner is untouched");
943 }
944
945 #[test]
946 fn spr_clipped_flip_mirrors_about_full_sprite() {
947 let mut fb = Framebuffer::new();
952 let mut sheet = SpriteSheet::default();
953 sheet.set(0, 0, 8); sheet.set(7, 0, 9); sheet.set(0, 7, 11); sheet.set(7, 7, 12); fb.clip(0, 0, 4, 4);
958 fb.spr(&sheet, 0, 0, 0, 8, 8, true, true);
959 assert_eq!(
960 fb.pget(0, 0),
961 12,
962 "source bottom-right mirrors to dest (0,0) and survives the clip"
963 );
964 assert_eq!(fb.pget(7, 7), 0, "dest (7,7) is outside the 4x4 clip");
965 assert_eq!(fb.pget(0, 7), 0, "dest (0,7) is outside the 4x4 clip");
966 assert_eq!(fb.pget(7, 0), 0, "dest (7,0) is outside the 4x4 clip");
967 }
968
969 #[test]
970 fn spr_partly_offscreen_under_camera_aligns_source() {
971 let mut fb = Framebuffer::new();
975 let mut sheet = SpriteSheet::default();
976 for y in 0..8 {
977 for x in 0..8 {
978 sheet.set(x, y, 7);
979 }
980 }
981 sheet.set(2, 3, 9); fb.camera(2, 3);
983 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
984 assert_eq!(
985 fb.pget(0, 0),
986 9,
987 "source (2,3) lands at the top-left corner"
988 );
989 assert_eq!(fb.pget(1, 0), 7, "source (3,3) is the next column");
990 assert_eq!(fb.pget(5, 4), 7, "the rest of the on-screen part is drawn");
991 assert_eq!(fb.pget(127, 127), 0, "no wrap to the opposite corner");
993 }
994
995 #[test]
996 fn spr_partial_pixel_width_meets_clip_edge() {
997 let mut fb = Framebuffer::new();
1000 let mut sheet = SpriteSheet::default();
1001 for y in 0..8 {
1002 for x in 0..8 {
1003 sheet.set(x, y, 7);
1004 }
1005 }
1006 fb.clip(0, 0, 2, 128);
1007 fb.spr(&sheet, 0, 0, 0, 4, 8, false, false);
1008 assert_eq!(fb.pget(1, 3), 7, "inside the clip and the partial width");
1009 assert_eq!(fb.pget(2, 3), 0, "clipped away at x=2");
1010 assert_eq!(fb.pget(4, 3), 0, "beyond the 4px width anyway");
1011 }
1012
1013 #[test]
1014 fn transparency_mask_controls_sprite_pixels() {
1015 let mut fb = Framebuffer::new();
1016 let mut sheet = SpriteSheet::default();
1017 for y in 0..8 {
1018 for x in 0..8 {
1019 sheet.set(x, y, 8); }
1021 }
1022 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
1024 assert_eq!(fb.pget(1, 1), 8);
1025 fb.cls(3);
1027 fb.set_transparent_color(8, true);
1028 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
1029 assert_eq!(fb.pget(1, 1), 3, "red made transparent");
1030 fb.reset_transparency();
1032 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
1033 assert_eq!(fb.pget(1, 1), 8);
1034 }
1035
1036 #[test]
1037 fn color_zero_can_be_made_opaque() {
1038 let mut fb = Framebuffer::new();
1039 let sheet = SpriteSheet::default(); fb.cls(7);
1041 fb.set_transparent_color(0, false);
1042 fb.spr(&sheet, 0, 0, 0, 8, 8, false, false);
1043 assert_eq!(fb.pget(3, 3), 0, "color 0 now drawn over white");
1044 }
1045
1046 #[test]
1047 fn draw_palette_remaps_writes() {
1048 let mut fb = Framebuffer::new();
1049 fb.remap_color(8, 12); fb.pset(5, 5, 8);
1051 assert_eq!(fb.pget(5, 5), 12);
1052 fb.reset_palette();
1053 fb.pset(6, 6, 8);
1054 assert_eq!(fb.pget(6, 6), 8);
1055 }
1056
1057 #[test]
1058 fn cls_ignores_draw_palette() {
1059 let mut fb = Framebuffer::new();
1060 fb.remap_color(0, 8);
1061 fb.cls(0);
1062 assert_eq!(fb.pget(10, 10), 0, "cls clears to the literal color");
1063 }
1064
1065 #[test]
1066 fn display_palette_remaps_at_upload() {
1067 let mut fb = Framebuffer::new();
1068 fb.pset(0, 0, 8); fb.remap_display_color(8, 12); let mut out = vec![0u8; (WIDTH * HEIGHT * 4) as usize];
1071 fb.write_rgba(&mut out);
1072 assert_eq!(&out[0..4], &palette::rgba(12), "pixel uploaded as blue");
1073 assert_eq!(fb.pget(0, 0), 8, "stored index is unchanged");
1074 }
1075
1076 #[test]
1077 fn ovalfill_fills_center_not_corner() {
1078 let mut fb = Framebuffer::new();
1079 fb.ovalfill(0, 0, 10, 6, 7);
1080 assert_eq!(fb.pget(5, 3), 7, "center filled");
1081 assert_eq!(fb.pget(0, 0), 0, "bounding-box corner stays empty");
1082 }
1083
1084 #[test]
1085 fn oval_outline_is_hollow() {
1086 let mut fb = Framebuffer::new();
1087 fb.oval(0, 0, 10, 10, 7);
1088 assert_eq!(fb.pget(5, 0), 7, "top of the outline is set");
1089 assert_eq!(fb.pget(5, 5), 0, "center is hollow");
1090 }
1091
1092 #[test]
1093 fn two_color_fill_pattern_alternates() {
1094 let mut fb = Framebuffer::new();
1095 fb.set_fill_pattern(0b1010_0101_1010_0101, 12, false);
1097 fb.rectfill(0, 0, 3, 3, 7);
1098 assert_eq!(
1099 fb.pget(0, 0),
1100 12,
1101 "pattern-1 pixel uses the secondary color"
1102 );
1103 assert_eq!(fb.pget(1, 0), 7, "pattern-0 pixel uses the primary color");
1104 }
1105
1106 #[test]
1107 fn transparent_fill_pattern_skips_pixels() {
1108 let mut fb = Framebuffer::new();
1109 fb.cls(3);
1110 fb.set_fill_pattern(0xffff, 0, true); fb.rectfill(0, 0, 3, 3, 7);
1112 assert_eq!(
1113 fb.pget(1, 1),
1114 3,
1115 "all pattern-1 pixels skipped; background shows"
1116 );
1117 }
1118
1119 #[test]
1120 fn zero_pattern_fills_solid() {
1121 let mut fb = Framebuffer::new();
1122 fb.set_fill_pattern(0, 0, false);
1123 fb.rectfill(0, 0, 3, 3, 7);
1124 assert_eq!(fb.pget(2, 2), 7);
1125 }
1126
1127 #[test]
1128 fn sspr_upscales_with_nearest_neighbor() {
1129 let mut fb = Framebuffer::new();
1130 let mut sheet = SpriteSheet::default();
1131 sheet.set(0, 0, 8); fb.sspr(&sheet, 0, 0, 1, 1, 10, 10, 4, 4, false, false);
1133 assert_eq!(fb.pget(10, 10), 8);
1134 assert_eq!(
1135 fb.pget(13, 13),
1136 8,
1137 "the whole 4x4 block is the source pixel"
1138 );
1139 }
1140
1141 #[test]
1142 fn sspr_respects_transparency() {
1143 let mut fb = Framebuffer::new();
1144 let sheet = SpriteSheet::default(); fb.cls(3);
1146 fb.sspr(&sheet, 0, 0, 2, 2, 0, 0, 4, 4, false, false);
1147 assert_eq!(fb.pget(1, 1), 3, "color 0 is transparent by default");
1148 }
1149
1150 #[test]
1151 fn sspr_flips_horizontally() {
1152 let mut fb = Framebuffer::new();
1153 let mut sheet = SpriteSheet::default();
1154 sheet.set(0, 0, 8);
1155 sheet.set(1, 0, 9);
1156 fb.sspr(&sheet, 0, 0, 2, 1, 0, 0, 2, 1, true, false);
1157 assert_eq!(
1158 fb.pget(1, 0),
1159 8,
1160 "flip puts the source-left pixel on the right"
1161 );
1162 assert_eq!(fb.pget(0, 0), 9);
1163 }
1164
1165 struct ClipCase {
1168 name: &'static str,
1169 src: (i32, i32, i32, i32),
1170 dst: (i32, i32, i32, i32),
1171 camera: (i32, i32),
1172 clip: (i32, i32, i32, i32),
1173 }
1174
1175 struct ClipState {
1177 name: &'static str,
1178 camera: (i32, i32),
1179 clip: (i32, i32, i32, i32),
1180 }
1181
1182 const CLIP_STATES: [ClipState; 4] = [
1184 ClipState {
1185 name: "plain",
1186 camera: (0, 0),
1187 clip: (0, 0, WIDTH, HEIGHT),
1188 },
1189 ClipState {
1190 name: "camera",
1191 camera: (23, -17),
1192 clip: (0, 0, WIDTH, HEIGHT),
1193 },
1194 ClipState {
1195 name: "clip rect",
1196 camera: (0, 0),
1197 clip: (12, 20, 33, 41),
1198 },
1199 ClipState {
1200 name: "camera and clip",
1201 camera: (-9, 6),
1202 clip: (12, 20, 33, 41),
1203 },
1204 ];
1205
1206 fn patterned_sheet() -> SpriteSheet {
1209 let mut sheet = SpriteSheet::default();
1210 for y in 0..24 {
1211 for x in 0..64 {
1212 sheet.set(x, y, ((x * 5 + y * 7) % 16) as u8);
1213 }
1214 }
1215 for n in 0..8 {
1216 sheet.set_flag(n, 0, n % 2 == 0);
1217 }
1218 sheet
1219 }
1220
1221 fn patterned_map() -> MapData {
1224 let mut map = MapData::default();
1225 for y in 0..20 {
1226 for x in 0..30 {
1227 map.set(x, y, ((x * 3 + y * 5) % 7) as u8);
1228 }
1229 }
1230 map
1231 }
1232
1233 struct PenState {
1238 name: &'static str,
1239 remap: &'static [(u8, u8)],
1240 transparent: &'static [u8],
1241 }
1242
1243 const PEN_STATES: [PenState; 2] = [
1244 PenState {
1245 name: "default pen",
1246 remap: &[],
1247 transparent: &[],
1248 },
1249 PenState {
1250 name: "remapped pen",
1251 remap: &[(9, 3), (5, 14), (0, 7)],
1252 transparent: &[4, 11],
1253 },
1254 ];
1255
1256 fn staged_fb(camera: (i32, i32), clip: (i32, i32, i32, i32), pen: &PenState) -> Framebuffer {
1259 let mut fb = Framebuffer::new();
1260 fb.cls(1);
1261 fb.camera(camera.0, camera.1);
1262 fb.clip(clip.0, clip.1, clip.2, clip.3);
1263 for &(from, to) in pen.remap {
1264 fb.remap_color(from, to);
1265 }
1266 for &color in pen.transparent {
1267 fb.set_transparent_color(color, true);
1268 }
1269 fb
1270 }
1271
1272 fn assert_same_pixels(got: &Framebuffer, want: &Framebuffer, case: &str) {
1275 let diff = got
1276 .pixels()
1277 .iter()
1278 .zip(want.pixels())
1279 .position(|(g, w)| g != w);
1280 if let Some(i) = diff {
1281 let (x, y) = (i as i32 % WIDTH, i as i32 / WIDTH);
1282 panic!(
1283 "{case}: pixel ({x}, {y}) is {}, want {}",
1284 got.pixels()[i],
1285 want.pixels()[i]
1286 );
1287 }
1288 }
1289
1290 const SWEEP_DEADLINE: Duration = Duration::from_secs(20);
1295
1296 fn drawn_within_deadline<F>(what: &str, draw: F) -> Framebuffer
1299 where
1300 F: FnOnce(&mut Framebuffer) + Send + 'static,
1301 {
1302 let (tx, rx) = mpsc::channel();
1303 thread::spawn(move || {
1304 let mut fb = Framebuffer::new();
1305 draw(&mut fb);
1306 let _ = tx.send(fb);
1307 });
1308 match rx.recv_timeout(SWEEP_DEADLINE) {
1309 Ok(fb) => fb,
1310 Err(_) => panic!("{what} did not finish within {SWEEP_DEADLINE:?}"),
1311 }
1312 }
1313
1314 fn sspr_unclipped(
1317 fb: &mut Framebuffer,
1318 sheet: &SpriteSheet,
1319 case: &ClipCase,
1320 flip_x: bool,
1321 flip_y: bool,
1322 ) {
1323 let (sx, sy, sw, sh) = case.src;
1324 let (dx, dy, dw, dh) = case.dst;
1325 if sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 {
1326 return;
1327 }
1328 for py in 0..dh {
1329 for px in 0..dw {
1330 let fx = if flip_x { dw - 1 - px } else { px };
1331 let fy = if flip_y { dh - 1 - py } else { py };
1332 let c = sheet.get(sx + fx * sw / dw, sy + fy * sh / dh);
1333 if ((fb.transparent >> c) & 1) == 0 {
1334 fb.pset(dx + px, dy + py, c);
1335 }
1336 }
1337 }
1338 }
1339
1340 #[test]
1341 fn sspr_clipping_matches_the_unclipped_walk() {
1342 let sheet = patterned_sheet();
1343 let full = (0, 0, WIDTH, HEIGHT);
1344 let cases = [
1345 ClipCase {
1346 name: "fully on screen",
1347 src: (0, 0, 8, 8),
1348 dst: (10, 10, 16, 16),
1349 camera: (0, 0),
1350 clip: full,
1351 },
1352 ClipCase {
1353 name: "over the left edge",
1354 src: (0, 0, 8, 8),
1355 dst: (-9, 10, 16, 16),
1356 camera: (0, 0),
1357 clip: full,
1358 },
1359 ClipCase {
1360 name: "over the right edge",
1361 src: (0, 0, 8, 8),
1362 dst: (121, 10, 16, 16),
1363 camera: (0, 0),
1364 clip: full,
1365 },
1366 ClipCase {
1367 name: "over the top edge",
1368 src: (0, 0, 8, 8),
1369 dst: (10, -11, 16, 16),
1370 camera: (0, 0),
1371 clip: full,
1372 },
1373 ClipCase {
1374 name: "over the bottom edge",
1375 src: (0, 0, 8, 8),
1376 dst: (10, 119, 16, 16),
1377 camera: (0, 0),
1378 clip: full,
1379 },
1380 ClipCase {
1381 name: "entirely off to the right",
1382 src: (0, 0, 8, 8),
1383 dst: (200, 60, 16, 16),
1384 camera: (0, 0),
1385 clip: full,
1386 },
1387 ClipCase {
1388 name: "entirely off past the origin",
1389 src: (0, 0, 8, 8),
1390 dst: (-40, -40, 16, 16),
1391 camera: (0, 0),
1392 clip: full,
1393 },
1394 ClipCase {
1395 name: "negative dx and dy straddling the origin",
1396 src: (0, 0, 8, 8),
1397 dst: (-5, -3, 24, 24),
1398 camera: (0, 0),
1399 clip: full,
1400 },
1401 ClipCase {
1402 name: "downscale",
1403 src: (0, 0, 16, 16),
1404 dst: (60, 60, 3, 3),
1405 camera: (0, 0),
1406 clip: full,
1407 },
1408 ClipCase {
1409 name: "downscale over an edge",
1410 src: (0, 0, 16, 16),
1411 dst: (-2, 120, 5, 11),
1412 camera: (0, 0),
1413 clip: full,
1414 },
1415 ClipCase {
1416 name: "upscale with an awkward ratio",
1417 src: (1, 2, 3, 5),
1418 dst: (-2, 100, 37, 41),
1419 camera: (0, 0),
1420 clip: full,
1421 },
1422 ClipCase {
1423 name: "upscale larger than the screen",
1426 src: (0, 0, 8, 8),
1427 dst: (-30, -20, 200, 220),
1428 camera: (0, 0),
1429 clip: full,
1430 },
1431 ClipCase {
1432 name: "upscale far larger than the screen, then clipped",
1433 src: (0, 0, 5, 7),
1434 dst: (10, 10, 500, 400),
1435 camera: (0, 0),
1436 clip: (20, 20, 60, 60),
1437 },
1438 ClipCase {
1439 name: "pushed off screen by the camera",
1440 src: (0, 0, 8, 8),
1441 dst: (10, 10, 16, 16),
1442 camera: (30, 20),
1443 clip: full,
1444 },
1445 ClipCase {
1446 name: "pulled on screen by a negative camera",
1447 src: (0, 0, 8, 8),
1448 dst: (10, 10, 20, 20),
1449 camera: (-100, -115),
1450 clip: full,
1451 },
1452 ClipCase {
1453 name: "trimmed by a clip rect",
1454 src: (0, 0, 8, 8),
1455 dst: (0, 0, 32, 32),
1456 camera: (0, 0),
1457 clip: (4, 4, 20, 20),
1458 },
1459 ClipCase {
1460 name: "camera and clip together",
1461 src: (2, 3, 12, 9),
1462 dst: (0, 0, 40, 40),
1463 camera: (-5, 7),
1464 clip: (10, 10, 40, 40),
1465 },
1466 ClipCase {
1467 name: "one-pixel destination",
1468 src: (0, 0, 8, 8),
1469 dst: (64, 64, 1, 1),
1470 camera: (0, 0),
1471 clip: full,
1472 },
1473 ClipCase {
1474 name: "source origin outside the sheet",
1475 src: (-4, -4, 8, 8),
1476 dst: (20, 20, 16, 16),
1477 camera: (0, 0),
1478 clip: full,
1479 },
1480 ClipCase {
1481 name: "clip rect that rejects everything",
1482 src: (0, 0, 8, 8),
1483 dst: (0, 0, 16, 16),
1484 camera: (0, 0),
1485 clip: (100, 100, 4, 4),
1486 },
1487 ];
1488 for case in &cases {
1489 for pen in &PEN_STATES {
1490 for (flip_x, flip_y) in [(false, false), (true, false), (false, true), (true, true)]
1491 {
1492 let mut got = staged_fb(case.camera, case.clip, pen);
1493 let mut want = staged_fb(case.camera, case.clip, pen);
1494 let (sx, sy, sw, sh) = case.src;
1495 let (dx, dy, dw, dh) = case.dst;
1496 got.sspr(&sheet, sx, sy, sw, sh, dx, dy, dw, dh, flip_x, flip_y);
1497 sspr_unclipped(&mut want, &sheet, case, flip_x, flip_y);
1498 assert_same_pixels(
1499 &got,
1500 &want,
1501 &format!(
1502 "{} / {} (flip_x {flip_x}, flip_y {flip_y})",
1503 case.name, pen.name
1504 ),
1505 );
1506 }
1507 }
1508 }
1509 }
1510
1511 #[test]
1512 fn sspr_bounded_by_the_screen_not_the_request() {
1513 for side in [4096, 1_000_000] {
1520 let fb = drawn_within_deadline(&format!("an 8x8 -> {side}x{side} sspr"), move |fb| {
1521 let mut sheet = SpriteSheet::default();
1522 for y in 0..8 {
1523 for x in 0..8 {
1524 sheet.set(x, y, 9);
1525 }
1526 }
1527 fb.sspr(&sheet, 0, 0, 8, 8, 0, 0, side, side, false, false);
1528 });
1529 assert!(
1530 fb.pixels().iter().all(|&p| p == 9),
1531 "the visible part of a {side}-square stretch is drawn"
1532 );
1533 }
1534 }
1535
1536 #[test]
1537 fn sspr_survives_a_destination_at_the_i32_ceiling() {
1538 let mut sheet = SpriteSheet::default();
1543 for y in 0..8 {
1544 for x in 0..8 {
1545 sheet.set(x, y, 9);
1546 }
1547 }
1548 for (flip_x, flip_y) in [(false, false), (true, false), (false, true), (true, true)] {
1549 let sheet = sheet.clone();
1550 let fb = drawn_within_deadline("an sspr at the i32 ceiling", move |fb| {
1551 fb.sspr(
1552 &sheet,
1553 0,
1554 0,
1555 8,
1556 8,
1557 -1_000_000,
1558 -1_000_000,
1559 i32::MAX,
1560 i32::MAX,
1561 flip_x,
1562 flip_y,
1563 );
1564 });
1565 assert!(
1566 fb.pixels().iter().all(|&p| p == 9),
1567 "flip_x {flip_x}, flip_y {flip_y}"
1568 );
1569 }
1570 }
1571
1572 #[allow(clippy::too_many_arguments)]
1575 fn spr_via_pset(
1576 fb: &mut Framebuffer,
1577 sheet: &SpriteSheet,
1578 n: u32,
1579 x: i32,
1580 y: i32,
1581 w: i32,
1582 h: i32,
1583 flip_x: bool,
1584 flip_y: bool,
1585 ) {
1586 let (pw, ph) = (w.max(0), h.max(0));
1587 let n = (n as usize) % SPRITE_COUNT;
1588 let base_sx = (n % SPRITES_PER_ROW * SPRITE_SIZE) as i32;
1589 let base_sy = (n / SPRITES_PER_ROW * SPRITE_SIZE) as i32;
1590 for py in 0..ph {
1591 let sy = if flip_y { ph - 1 - py } else { py };
1592 for px in 0..pw {
1593 let sx = if flip_x { pw - 1 - px } else { px };
1594 let c = sheet.get(base_sx + sx, base_sy + sy);
1595 if ((fb.transparent >> c) & 1) == 0 {
1596 fb.pset(x + px, y + py, c);
1597 }
1598 }
1599 }
1600 }
1601
1602 #[test]
1603 fn spr_writes_match_the_pset_path() {
1604 let sheet = patterned_sheet();
1609 for pen in &PEN_STATES {
1610 for state in &CLIP_STATES {
1611 for (n, at, size) in [
1612 (0u32, (10, 10), (8, 8)),
1613 (3, (-4, 60), (8, 8)),
1614 (9, (124, -2), (16, 16)),
1615 (1, (30, 30), (5, 3)),
1616 ] {
1617 for (flip_x, flip_y) in
1618 [(false, false), (true, false), (false, true), (true, true)]
1619 {
1620 let mut got = staged_fb(state.camera, state.clip, pen);
1621 let mut want = staged_fb(state.camera, state.clip, pen);
1622 got.spr(&sheet, n, at.0, at.1, size.0, size.1, flip_x, flip_y);
1623 spr_via_pset(
1624 &mut want, &sheet, n, at.0, at.1, size.0, size.1, flip_x, flip_y,
1625 );
1626 assert_same_pixels(
1627 &got,
1628 &want,
1629 &format!(
1630 "sprite {n} at {at:?} / {} / {} (flip {flip_x} {flip_y})",
1631 state.name, pen.name
1632 ),
1633 );
1634 }
1635 }
1636 }
1637 }
1638 }
1639
1640 struct MapCase {
1643 name: &'static str,
1644 cel: (i32, i32, i32, i32),
1645 at: (i32, i32),
1646 camera: (i32, i32),
1647 clip: (i32, i32, i32, i32),
1648 }
1649
1650 fn map_unclipped(
1652 fb: &mut Framebuffer,
1653 map: &MapData,
1654 sheet: &SpriteSheet,
1655 case: &MapCase,
1656 layers: u8,
1657 ) {
1658 let (cel_x, cel_y, cel_w, cel_h) = case.cel;
1659 let (sx, sy) = case.at;
1660 for ty in 0..cel_h {
1661 for tx in 0..cel_w {
1662 let tile = map.get(cel_x + tx, cel_y + ty);
1663 if tile == 0 {
1664 continue;
1665 }
1666 if layers != 0 && sheet.flags(tile as u32) & layers == 0 {
1667 continue;
1668 }
1669 fb.spr(
1670 sheet,
1671 tile as u32,
1672 sx + tx * SPRITE_SIZE as i32,
1673 sy + ty * SPRITE_SIZE as i32,
1674 SPRITE_SIZE as i32,
1675 SPRITE_SIZE as i32,
1676 false,
1677 false,
1678 );
1679 }
1680 }
1681 }
1682
1683 #[test]
1684 fn map_clipping_matches_the_unclipped_walk() {
1685 let map = patterned_map();
1686 let sheet = patterned_sheet();
1687 let full = (0, 0, WIDTH, HEIGHT);
1688 let cases = [
1689 MapCase {
1690 name: "fully on screen",
1691 cel: (0, 0, 8, 8),
1692 at: (0, 0),
1693 camera: (0, 0),
1694 clip: full,
1695 },
1696 MapCase {
1697 name: "wider than the screen",
1698 cel: (0, 0, 30, 20),
1699 at: (0, 0),
1700 camera: (0, 0),
1701 clip: full,
1702 },
1703 MapCase {
1704 name: "over the left edge",
1705 cel: (0, 0, 20, 16),
1706 at: (-20, 0),
1707 camera: (0, 0),
1708 clip: full,
1709 },
1710 MapCase {
1711 name: "over the top edge on a half-cel offset",
1712 cel: (0, 0, 20, 16),
1713 at: (-3, -13),
1714 camera: (0, 0),
1715 clip: full,
1716 },
1717 MapCase {
1718 name: "over the bottom-right corner",
1719 cel: (0, 0, 20, 16),
1720 at: (100, 100),
1721 camera: (0, 0),
1722 clip: full,
1723 },
1724 MapCase {
1725 name: "entirely off screen",
1726 cel: (0, 0, 20, 16),
1727 at: (-500, -500),
1728 camera: (0, 0),
1729 clip: full,
1730 },
1731 MapCase {
1732 name: "shifted by the camera",
1733 cel: (0, 0, 20, 16),
1734 at: (0, 0),
1735 camera: (37, 21),
1736 clip: full,
1737 },
1738 MapCase {
1739 name: "pulled back by a negative camera",
1740 cel: (0, 0, 20, 16),
1741 at: (0, 0),
1742 camera: (-19, -5),
1743 clip: full,
1744 },
1745 MapCase {
1746 name: "trimmed by a clip rect",
1747 cel: (0, 0, 20, 16),
1748 at: (0, 0),
1749 camera: (0, 0),
1750 clip: (10, 10, 30, 30),
1751 },
1752 MapCase {
1753 name: "cel origin offset into the map",
1754 cel: (5, 3, 10, 10),
1755 at: (2, 2),
1756 camera: (-6, 11),
1757 clip: (3, 7, 90, 60),
1758 },
1759 MapCase {
1760 name: "empty cel rectangle",
1761 cel: (0, 0, 0, 0),
1762 at: (0, 0),
1763 camera: (0, 0),
1764 clip: full,
1765 },
1766 ];
1767 for case in &cases {
1768 for pen in &PEN_STATES {
1769 for layers in [0u8, 1] {
1770 let mut got = staged_fb(case.camera, case.clip, pen);
1771 let mut want = staged_fb(case.camera, case.clip, pen);
1772 let (cel_x, cel_y, cel_w, cel_h) = case.cel;
1773 let (sx, sy) = case.at;
1774 got.map(&map, &sheet, cel_x, cel_y, sx, sy, cel_w, cel_h, layers);
1775 map_unclipped(&mut want, &map, &sheet, case, layers);
1776 assert_same_pixels(
1777 &got,
1778 &want,
1779 &format!("{} / {} (layers {layers})", case.name, pen.name),
1780 );
1781 }
1782 }
1783 }
1784 }
1785
1786 #[test]
1787 fn map_bounded_by_the_screen_not_the_request() {
1788 let got = drawn_within_deadline("a 100_000-cel map", |fb| {
1792 fb.map(
1793 &patterned_map(),
1794 &patterned_sheet(),
1795 0,
1796 0,
1797 0,
1798 0,
1799 100_000,
1800 100_000,
1801 0,
1802 );
1803 });
1804 let mut want = Framebuffer::new();
1805 want.map(&patterned_map(), &patterned_sheet(), 0, 0, 0, 0, 16, 16, 0);
1806 assert_same_pixels(&got, &want, "a 100_000-cel request");
1807 }
1808
1809 fn line_unclipped(fb: &mut Framebuffer, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
1812 let (mut x0, mut y0) = (x0 - fb.camera_x, y0 - fb.camera_y);
1813 let (x1, y1) = (x1 - fb.camera_x, y1 - fb.camera_y);
1814 let dx = (x1 - x0).abs();
1815 let dy = -(y1 - y0).abs();
1816 let sx = if x0 < x1 { 1 } else { -1 };
1817 let sy = if y0 < y1 { 1 } else { -1 };
1818 let mut err = dx + dy;
1819 loop {
1820 fb.raw_pset(x0, y0, color);
1821 if x0 == x1 && y0 == y1 {
1822 break;
1823 }
1824 let e2 = 2 * err;
1825 if e2 >= dy {
1826 err += dy;
1827 x0 += sx;
1828 }
1829 if e2 <= dx {
1830 err += dx;
1831 y0 += sy;
1832 }
1833 }
1834 }
1835
1836 #[test]
1837 fn line_clipping_matches_the_unclipped_walk() {
1838 const ENDS: [i32; 7] = [-201, -1, 0, 37, 89, 127, 260];
1843 for state in CLIP_STATES {
1844 for x0 in ENDS {
1845 for y0 in ENDS {
1846 for x1 in ENDS {
1847 for y1 in ENDS {
1848 let mut got = staged_fb(state.camera, state.clip, &PEN_STATES[0]);
1849 let mut want = staged_fb(state.camera, state.clip, &PEN_STATES[0]);
1850 got.line(x0, y0, x1, y1, 7);
1851 line_unclipped(&mut want, x0, y0, x1, y1, 7);
1852 assert_same_pixels(
1853 &got,
1854 &want,
1855 &format!("({x0}, {y0}) - ({x1}, {y1}) / {}", state.name),
1856 );
1857 }
1858 }
1859 }
1860 }
1861 }
1862 }
1863
1864 #[test]
1865 fn line_bounded_by_the_screen_not_the_endpoints() {
1866 type LineCase = (&'static str, (i32, i32, i32, i32), fn(&mut Framebuffer));
1873
1874 let far = 2_000_000_000;
1875 let cases: [LineCase; 5] = [
1876 ("rightwards along row 0", (0, 0, far, 0), |fb| {
1877 for x in 0..WIDTH {
1878 fb.pset(x, 0, 7);
1879 }
1880 }),
1881 ("downwards along column 0", (0, 0, 0, far), |fb| {
1882 for y in 0..HEIGHT {
1883 fb.pset(0, y, 7);
1884 }
1885 }),
1886 ("the main diagonal", (0, 0, far, far), |fb| {
1887 for i in 0..WIDTH {
1888 fb.pset(i, i, 7);
1889 }
1890 }),
1891 (
1892 "the main diagonal, walked backwards",
1893 (far, far, 0, 0),
1894 |fb| {
1895 for i in 0..WIDTH {
1896 fb.pset(i, i, 7);
1897 }
1898 },
1899 ),
1900 ("half slope", (0, 0, far, far / 2), |fb| {
1902 for x in 0..WIDTH {
1903 fb.pset(x, (x + 1) / 2, 7);
1904 }
1905 }),
1906 ];
1907 for (name, ends, expected) in cases {
1908 let got = drawn_within_deadline(name, move |fb| {
1909 fb.line(ends.0, ends.1, ends.2, ends.3, 7);
1910 });
1911 let mut want = Framebuffer::new();
1912 expected(&mut want);
1913 assert_same_pixels(&got, &want, name);
1914 }
1915 }
1916
1917 fn rectfill_unclipped(fb: &mut Framebuffer, x0: i32, y0: i32, x1: i32, y1: i32, color: u8) {
1919 let (xa, xb) = (x0.min(x1), x0.max(x1));
1920 let (ya, yb) = (y0.min(y1), y0.max(y1));
1921 if fb.fill_pattern == 0 {
1922 for y in ya..=yb {
1923 fb.fill_span(xa - fb.camera_x, xb - fb.camera_x, y - fb.camera_y, color);
1924 }
1925 } else {
1926 for y in ya..=yb {
1927 for x in xa..=xb {
1928 fb.raw_pset_fill(x - fb.camera_x, y - fb.camera_y, color);
1929 }
1930 }
1931 }
1932 }
1933
1934 #[test]
1935 fn rectfill_clipping_matches_the_unclipped_walk() {
1936 let rects = [
1937 ("fully on screen", (10, 10, 40, 30)),
1938 ("over the top-left", (-20, -30, 40, 30)),
1939 ("over the bottom-right", (100, 90, 200, 300)),
1940 ("entirely off screen", (300, 300, 400, 400)),
1941 ("inverted corners", (60, 70, 20, 10)),
1942 ("wider than the screen", (-50, 40, 400, 44)),
1943 ("a single pixel", (64, 64, 64, 64)),
1944 ];
1945 for pattern in [0u16, 0b1010_0101_1010_0101] {
1946 for (rect_name, (x0, y0, x1, y1)) in rects {
1947 for state in CLIP_STATES {
1948 let (state_name, mut got, mut want) = (
1949 state.name,
1950 staged_fb(state.camera, state.clip, &PEN_STATES[0]),
1951 staged_fb(state.camera, state.clip, &PEN_STATES[0]),
1952 );
1953 got.set_fill_pattern(pattern, 12, false);
1954 want.set_fill_pattern(pattern, 12, false);
1955 got.rectfill(x0, y0, x1, y1, 7);
1956 rectfill_unclipped(&mut want, x0, y0, x1, y1, 7);
1957 assert_same_pixels(
1958 &got,
1959 &want,
1960 &format!("{rect_name} / {state_name} / pattern {pattern:#06x}"),
1961 );
1962 }
1963 }
1964 }
1965 }
1966
1967 #[test]
1968 fn rectfill_bounded_by_the_screen_not_the_request() {
1969 for pattern in [0u16, 0b1010_0101_1010_0101] {
1973 let got = drawn_within_deadline("a rectfill spanning the i32 range", move |fb| {
1974 fb.set_fill_pattern(pattern, 12, false);
1975 fb.rectfill(
1976 i32::MIN / 2,
1977 i32::MIN / 2,
1978 i32::MAX / 2 + 1,
1979 i32::MAX / 2 + 1,
1980 7,
1981 );
1982 });
1983 let mut want = Framebuffer::new();
1984 want.set_fill_pattern(pattern, 12, false);
1985 want.rectfill(0, 0, WIDTH - 1, HEIGHT - 1, 7);
1986 assert_same_pixels(&got, &want, &format!("pattern {pattern:#06x}"));
1987 }
1988 }
1989
1990 fn oval_unclipped(
1992 fb: &mut Framebuffer,
1993 x0: i32,
1994 y0: i32,
1995 x1: i32,
1996 y1: i32,
1997 color: u8,
1998 fill: bool,
1999 ) {
2000 let (xa, xb) = (x0.min(x1), x0.max(x1));
2001 let (ya, yb) = (y0.min(y1), y0.max(y1));
2002 let cx = (xa + xb) as f32 / 2.0;
2003 let cy = (ya + yb) as f32 / 2.0;
2004 let a = (xb - xa) as f32 / 2.0;
2005 let b = (yb - ya) as f32 / 2.0;
2006 let half_extent = |v: f32, half: f32| -> Option<f32> {
2007 let d = if half > 0.0 { v / half } else { 0.0 };
2008 let s = 1.0 - d * d;
2009 if s < 0.0 {
2010 None
2011 } else {
2012 Some(s.sqrt())
2013 }
2014 };
2015 if fill {
2016 for y in ya..=yb {
2017 let Some(s) = half_extent(y as f32 - cy, b) else {
2018 continue;
2019 };
2020 let left = (cx - a * s).round() as i32;
2021 let right = (cx + a * s).round() as i32;
2022 if fb.fill_pattern == 0 {
2023 fb.fill_span(
2024 left - fb.camera_x,
2025 right - fb.camera_x,
2026 y - fb.camera_y,
2027 color,
2028 );
2029 } else {
2030 for x in left..=right {
2031 fb.raw_pset_fill(x - fb.camera_x, y - fb.camera_y, color);
2032 }
2033 }
2034 }
2035 } else {
2036 for y in ya..=yb {
2037 let Some(s) = half_extent(y as f32 - cy, b) else {
2038 continue;
2039 };
2040 fb.pset((cx - a * s).round() as i32, y, color);
2041 fb.pset((cx + a * s).round() as i32, y, color);
2042 }
2043 for x in xa..=xb {
2044 let Some(s) = half_extent(x as f32 - cx, a) else {
2045 continue;
2046 };
2047 fb.pset(x, (cy - b * s).round() as i32, color);
2048 fb.pset(x, (cy + b * s).round() as i32, color);
2049 }
2050 }
2051 }
2052
2053 #[test]
2054 fn oval_clipping_matches_the_unclipped_walk() {
2055 let boxes = [
2056 ("fully on screen", (10, 10, 60, 40)),
2057 ("over the top-left", (-30, -25, 30, 20)),
2058 ("over the bottom-right", (90, 80, 190, 200)),
2059 ("entirely off screen", (300, 300, 380, 360)),
2060 ("bigger than the screen", (-200, -200, 320, 320)),
2061 ("a degenerate line", (20, 50, 90, 50)),
2062 ];
2063 for pattern in [0u16, 0b1010_0101_1010_0101] {
2064 for fill in [false, true] {
2065 for (shape, (x0, y0, x1, y1)) in boxes {
2066 for state in CLIP_STATES {
2067 let (state_name, mut got, mut want) = (
2068 state.name,
2069 staged_fb(state.camera, state.clip, &PEN_STATES[0]),
2070 staged_fb(state.camera, state.clip, &PEN_STATES[0]),
2071 );
2072 got.set_fill_pattern(pattern, 12, false);
2073 want.set_fill_pattern(pattern, 12, false);
2074 if fill {
2075 got.ovalfill(x0, y0, x1, y1, 7);
2076 } else {
2077 got.oval(x0, y0, x1, y1, 7);
2078 }
2079 oval_unclipped(&mut want, x0, y0, x1, y1, 7, fill);
2080 assert_same_pixels(
2081 &got,
2082 &want,
2083 &format!(
2084 "{shape} / {state_name} / fill {fill} / pattern {pattern:#06x}"
2085 ),
2086 );
2087 }
2088 }
2089 }
2090 }
2091 }
2092
2093 #[test]
2094 fn oval_bounded_by_the_screen_not_the_request() {
2095 let (lo, hi) = (i32::MIN / 2 + 1, i32::MAX / 2);
2099 let filled = drawn_within_deadline("an ovalfill spanning the i32 range", move |fb| {
2100 fb.ovalfill(lo, lo, hi, hi, 7);
2101 });
2102 assert!(
2103 filled.pixels().iter().all(|&p| p == 7),
2104 "the screen is well inside the ellipse"
2105 );
2106 let outline = drawn_within_deadline("an oval spanning the i32 range", move |fb| {
2107 fb.oval(lo, lo, hi, hi, 7);
2108 });
2109 assert!(
2110 outline.pixels().iter().all(|&p| p == 0),
2111 "the outline never comes near the screen"
2112 );
2113 }
2114
2115 fn circle_unclipped(fb: &mut Framebuffer, cx: i32, cy: i32, r: i32, color: u8, fill: bool) {
2118 let (cx, cy) = (cx - fb.camera_x, cy - fb.camera_y);
2119 let (mut x, mut y, mut err) = (r.max(0), 0, 1 - r.max(0));
2120 while x >= y {
2121 if fill && fb.fill_pattern == 0 {
2122 fb.fill_span(cx - x, cx + x, cy + y, color);
2123 fb.fill_span(cx - x, cx + x, cy - y, color);
2124 fb.fill_span(cx - y, cx + y, cy + x, color);
2125 fb.fill_span(cx - y, cx + y, cy - x, color);
2126 } else if fill {
2127 for px in (cx - x)..=(cx + x) {
2128 fb.raw_pset_fill(px, cy + y, color);
2129 fb.raw_pset_fill(px, cy - y, color);
2130 }
2131 for px in (cx - y)..=(cx + y) {
2132 fb.raw_pset_fill(px, cy + x, color);
2133 fb.raw_pset_fill(px, cy - x, color);
2134 }
2135 } else {
2136 for (px, py) in [
2137 (cx + x, cy + y),
2138 (cx - x, cy + y),
2139 (cx + x, cy - y),
2140 (cx - x, cy - y),
2141 (cx + y, cy + x),
2142 (cx - y, cy + x),
2143 (cx + y, cy - x),
2144 (cx - y, cy - x),
2145 ] {
2146 fb.raw_pset(px, py, color);
2147 }
2148 }
2149 y += 1;
2150 if err < 0 {
2151 err += 2 * y + 1;
2152 } else {
2153 x -= 1;
2154 err += 2 * (y - x) + 1;
2155 }
2156 }
2157 }
2158
2159 #[test]
2160 fn circle_clipping_matches_the_unclipped_walk() {
2161 let circles = [
2162 ("centered", (64, 64, 30)),
2163 ("over the top-left", (-5, -8, 24)),
2164 ("over the bottom-right", (130, 120, 40)),
2165 ("entirely off screen", (400, 400, 50)),
2166 ("bigger than the screen", (64, 64, 300)),
2167 ("zero radius", (64, 64, 0)),
2168 ("negative radius", (64, 64, -9)),
2169 ];
2170 for pattern in [0u16, 0b1010_0101_1010_0101] {
2171 for fill in [false, true] {
2172 for (shape, (cx, cy, r)) in circles {
2173 for state in CLIP_STATES {
2174 let (state_name, mut got, mut want) = (
2175 state.name,
2176 staged_fb(state.camera, state.clip, &PEN_STATES[0]),
2177 staged_fb(state.camera, state.clip, &PEN_STATES[0]),
2178 );
2179 got.set_fill_pattern(pattern, 12, false);
2180 want.set_fill_pattern(pattern, 12, false);
2181 if fill {
2182 got.circfill(cx, cy, r, 7);
2183 } else {
2184 got.circ(cx, cy, r, 7);
2185 }
2186 circle_unclipped(&mut want, cx, cy, r, 7, fill);
2187 assert_same_pixels(
2188 &got,
2189 &want,
2190 &format!(
2191 "{shape} / {state_name} / fill {fill} / pattern {pattern:#06x}"
2192 ),
2193 );
2194 }
2195 }
2196 }
2197 }
2198 }
2199
2200 fn assert_circle_matches(
2203 cx: i32,
2204 cy: i32,
2205 r: i32,
2206 fill: bool,
2207 pattern: u16,
2208 state: &ClipState,
2209 ) {
2210 let (mut got, mut want) = (
2211 staged_fb(state.camera, state.clip, &PEN_STATES[0]),
2212 staged_fb(state.camera, state.clip, &PEN_STATES[0]),
2213 );
2214 got.set_fill_pattern(pattern, 12, false);
2215 want.set_fill_pattern(pattern, 12, false);
2216 if fill {
2217 got.circfill(cx, cy, r, 7);
2218 } else {
2219 got.circ(cx, cy, r, 7);
2220 }
2221 circle_unclipped(&mut want, cx, cy, r, 7, fill);
2222 assert_same_pixels(
2223 &got,
2224 &want,
2225 &format!(
2226 "({cx}, {cy}) r {r} / {} / fill {fill} / pattern {pattern:#06x}",
2227 state.name
2228 ),
2229 );
2230 }
2231
2232 #[test]
2233 fn circle_seeded_walk_matches_the_full_walk_at_every_small_radius() {
2234 for r in 0..=130 {
2238 for (cx, cy) in [(64, 64), (0, 0), (-30, 70), (140, -12), (127, 127)] {
2239 for fill in [false, true] {
2240 assert_circle_matches(cx, cy, r, fill, 0, &CLIP_STATES[0]);
2241 }
2242 }
2243 }
2244 }
2245
2246 #[test]
2247 fn circle_seeded_walk_matches_the_full_walk_at_a_large_radius() {
2248 for r in [9_001, 120_000] {
2254 for (cx, cy) in [
2255 (64, 64),
2256 (64, 64 + r),
2257 (64, 64 - r),
2258 (64 + r, 64),
2259 (64 - r, 64),
2260 (40 - r, 90 + r),
2261 ] {
2262 for fill in [false, true] {
2263 for state in &CLIP_STATES {
2264 assert_circle_matches(cx, cy, r, fill, 0, state);
2267 }
2268 }
2269 }
2270 }
2271 }
2272
2273 #[test]
2274 fn circle_bounded_by_the_screen_not_the_radius() {
2275 let r = 2_000_000_000;
2280 let filled = drawn_within_deadline("a circfill swallowing the screen", move |fb| {
2281 fb.circfill(64, 64, r, 7);
2282 });
2283 assert!(
2284 filled.pixels().iter().all(|&p| p == 7),
2285 "the screen is well inside the disc"
2286 );
2287 let outline = drawn_within_deadline("a circ swallowing the screen", move |fb| {
2288 fb.circ(64, 64, r, 7);
2289 });
2290 assert!(
2291 outline.pixels().iter().all(|&p| p == 0),
2292 "the ring runs a billion pixels off every edge"
2293 );
2294
2295 let capped = drawn_within_deadline("a circfill touching the screen at a pole", move |fb| {
2298 fb.circfill(64, 64 + r, r, 7);
2299 });
2300 assert_eq!(capped.pget(64, 64), 7, "the top of the disc is on screen");
2301 assert!(
2302 capped.pixels()[..(64 * WIDTH) as usize]
2303 .iter()
2304 .all(|&p| p == 0),
2305 "nothing above the disc is touched"
2306 );
2307 assert!(
2308 capped.pixels()[(127 * WIDTH) as usize..]
2309 .iter()
2310 .all(|&p| p == 7),
2311 "the bottom row is deep inside the disc"
2312 );
2313 let arc = drawn_within_deadline("a circ touching the screen at a pole", move |fb| {
2314 fb.circ(64, 64 + r, r, 7);
2315 });
2316 assert_eq!(arc.pget(64, 64), 7, "the top of the ring is on screen");
2317 assert!(
2318 arc.pixels()[..(64 * WIDTH) as usize]
2319 .iter()
2320 .all(|&p| p == 0),
2321 "nothing above the ring is touched"
2322 );
2323 }
2324
2325 #[test]
2326 fn circle_that_cannot_reach_the_clip_rect_is_dropped() {
2327 let mut fb = Framebuffer::new();
2331 fb.circ(2_000_000_000, 0, 1_000_000_000, 7);
2332 fb.circfill(0, -2_000_000_000, 1_000_000_000, 7);
2333 assert!(
2334 fb.pixels().iter().all(|&p| p == 0),
2335 "nothing on screen was touched"
2336 );
2337 }
2338
2339 #[test]
2340 fn print_pen_matches_print_at_cursor() {
2341 let mut a = Framebuffer::new();
2342 let mut b = Framebuffer::new();
2343 a.set_pen_color(9);
2344 a.set_cursor(10, 20);
2345 let end_a = a.print_pen("hi");
2346 let end_b = b.print("hi", 10, 20, 9);
2347 assert_eq!(end_a, end_b);
2348 assert_eq!(
2349 a.pixels(),
2350 b.pixels(),
2351 "print_pen draws identically to print"
2352 );
2353 }
2354
2355 #[test]
2356 fn print_pen_advances_cursor_one_line() {
2357 let mut fb = Framebuffer::new();
2358 fb.set_cursor(5, 5);
2359 fb.print_pen("x");
2360 fb.print_pen("y");
2361 let mut expect = Framebuffer::new();
2362 expect.print("x", 5, 5, 6); expect.print("y", 5, 5 + font::GLYPH_H, 6);
2364 assert_eq!(fb.pixels(), expect.pixels());
2365 }
2366}