Skip to main content

shuyi_ui/frame/
onebit.rs

1//! The one-bit renderer (game ADR-0074): draws a [`GlyphBuffer`] the size of
2//! the Playdate's screen (50 × 15 cells of 8 × 16 pixels,
3//! [`ConsoleSpec::text`](crate::frame::ConsoleSpec::text)) into a 400 × 240
4//! one-bit [`Frame`] laid out as the Playdate's own frame buffer, so its
5//! front end copies it to the screen as it is.
6//!
7//! Colours become [`Shade`]s, 8 × 8 patterns fixed to the screen's pixels
8//! (so neighbouring tiles line up and patterns don't swim when things
9//! move), as a [`Shades`] says. Two say it:
10//!
11//! - **The game's inks** ([`InkShades`], game ADR-0074): a colour that is
12//!   an ink's reserved [code](Ink::code) (the Playdate's palette is made
13//!   of them) is drawn in that ink, any other one white or black by its
14//!   brightness. [`render`] draws with them.
15//! - **Roles' patterns** ([`PatternMap`], ADR-0008 §4): the skin's palette
16//!   names its colours by role, and its pattern map gives each role a
17//!   foreground and a background [`Pattern`]. A colour of no role is
18//!   dithered by its [`luminance`]; a glyph's is solid, white or black, as
19//!   text is never dithered. [`to_one_bit`] draws with them.
20//!
21//! The order: the white ground; a backdrop's scene in its window
22//! (game ADR-0048), drawn the same way; then the cell backgrounds, `Under`
23//! items, the white edges of [`Ink::BlackEdged`] glyphs, the glyphs (each
24//! half of a wide one in its own cell: the font's one-bit atlas keeps the
25//! halves as two cells),
26//! `Over` items, and last the [`Ink::Invert`] rectangles, which flip what
27//! is under them. Pictures are one-bit [`Image`]s (`docs/design/playdate.md`,
28//! *One-bit pictures*). A painted picture (ADR-0078) is looked up in the
29//! frame's table: a one-bit one is drawn as a one-bit sprite, an RGBA or
30//! indexed one dithered as it is drawn ([`dither_row`]). Nothing here
31//! allocates.
32//!
33//! Made for the device's speed (ticket 1055, game ADR-0077): 32-bit
34//! coordinates, a pattern's byte looked up, whole bytes written whole
35//! (a sprite's too, ticket 3056), only the cells near the part being
36//! drawn visited, and
37//! [`render_changed`] redraws only where a frame differs from the last.
38//! [`reference`] is the plain renderer this one must equal.
39
40use core::ops::Range;
41
42use shuyi_assets::bitmap::Bitmap;
43use shuyi_assets::bitmap::dither::brightness;
44use shuyi_assets::font::FALLBACK_GLYPH;
45use shuyi_assets::font::FontAtlasDef;
46use shuyi_assets::image::ImageId;
47use shuyi_assets::ink::Ink;
48pub use shuyi_assets::onebit::{Base, Pattern, PatternMapDef, RolePatterns, Rows};
49
50use crate::frame::color::Rgb;
51use crate::frame::glyph_buffer::{Cell, GlyphBuffer, Item, Layer, Paint, PxRect, Rect, Sprite};
52use crate::frame::painted::{Painted, PaintedKind, PaintedSprite, PaintedTable};
53
54mod roles;
55pub use roles::{OneBitFrame, PatternMap, RowMask, changed_rows, luminance, to_one_bit};
56
57#[cfg(any(test, feature = "onebit-reference"))]
58pub mod reference;
59
60/// The screen's width in pixels.
61pub const WIDTH: i32 = 400;
62/// The screen's height in pixels.
63pub const HEIGHT: i32 = 240;
64/// Bytes per row of a [`Frame`]: the Playdate's stride (50 bytes of
65/// pixels, 2 unused).
66pub const ROW_BYTES: usize = 52;
67/// Bytes in a [`Frame`].
68pub const FRAME_BYTES: usize = ROW_BYTES * HEIGHT as usize;
69
70/// A one-bit picture of the screen in the Playdate's layout: row after
71/// row of [`ROW_BYTES`] bytes, a byte's highest bit its left pixel, a set
72/// bit white.
73pub type Frame = [u8; FRAME_BYTES];
74
75/// Whether pixel `(x, y)` of `frame` is white; off the screen, `false`.
76pub fn is_white(frame: &Frame, x: i32, y: i32) -> bool {
77    if !(0..WIDTH).contains(&x) || !(0..HEIGHT).contains(&y) {
78        return false;
79    }
80    let byte = frame[byte_index(x, y)];
81    byte >> (7 - (x & 7)) & 1 == 1
82}
83
84/// The ink a colour is drawn in: the ink it is the [code](Ink::code) of,
85/// or else white if it is at least half bright (in the eye's weighting of
86/// red, green and blue), black if not.
87pub fn ink_of(rgb: Rgb) -> Ink {
88    Ink::from_code([rgb.r, rgb.g, rgb.b]).unwrap_or_else(|| {
89        let luma = 299 * u32::from(rgb.r) + 587 * u32::from(rgb.g) + 114 * u32::from(rgb.b);
90        if luma >= 128 * 1000 {
91            Ink::White
92        } else {
93            Ink::Black
94        }
95    })
96}
97
98/// Whether `ink` makes screen pixel `(x, y)` white. [`Ink::BlackEdged`]
99/// and [`Ink::Invert`] are black here: the edge is drawn around glyphs,
100/// and only rectangles flip.
101pub const fn ink_is_white(ink: Ink, x: i32, y: i32) -> bool {
102    match ink {
103        Ink::White => true,
104        Ink::Black | Ink::BlackEdged | Ink::Invert => false,
105        // Black where x + y is even.
106        Ink::Grey => x.rem_euclid(2) != y.rem_euclid(2),
107        // Black every fourth pixel of every other row, the rows staggered.
108        Ink::Dots => !(y.rem_euclid(2) == 0 && x.rem_euclid(4) == y.rem_euclid(4)),
109        // Black where x − y is a multiple of 4: lines down to the right.
110        Ink::Diagonals => (x - y).rem_euclid(4) != 0,
111        // A checker of 2 × 2 squares: a 1-pixel line of it is dashed.
112        Ink::Dashes => x.div_euclid(2).rem_euclid(2) != y.div_euclid(2).rem_euclid(2),
113    }
114}
115
116/// How a colour is drawn: an 8 × 8 pattern fixed to the screen, and
117/// what two of the game's inks do besides.
118#[derive(Copy, Clone, Debug, PartialEq, Eq)]
119pub struct Shade {
120    /// The pattern: screen pixel `(x, y)` takes bit `x % 8` (from the
121    /// highest) of row `y % 8`, a set bit white. Every pattern repeats
122    /// within 8 pixels across, so a row's byte is the same all along it.
123    pub rows: Rows,
124    /// A glyph in it gets a white edge one pixel wide around it
125    /// ([`Ink::BlackEdged`]).
126    pub edged: bool,
127    /// A rectangle in it flips the pixels under it, after everything else
128    /// ([`Ink::Invert`]); anything else in it is drawn in the pattern.
129    pub flips: bool,
130}
131
132impl Shade {
133    /// Pattern `rows`, and nothing more.
134    pub const fn rows(rows: Rows) -> Self {
135        Self {
136            rows,
137            edged: false,
138            flips: false,
139        }
140    }
141
142    /// How `ink` draws ([`ink_is_white`]).
143    #[allow(clippy::cast_sign_loss)] // 0..8
144    pub const fn of_ink(ink: Ink) -> Self {
145        let mut rows = [0; 8];
146        let mut y = 0;
147        while y < 8 {
148            let mut x = 0;
149            while x < 8 {
150                if ink_is_white(ink, x, y) {
151                    rows[y as usize] |= 0x80 >> x;
152                }
153                x += 1;
154            }
155            y += 1;
156        }
157        Self {
158            rows,
159            edged: matches!(ink, Ink::BlackEdged),
160            flips: matches!(ink, Ink::Invert),
161        }
162    }
163
164    /// The eight pixels of screen row `y` it paints in a byte of a
165    /// [`Frame`].
166    #[allow(clippy::cast_sign_loss)] // 0..8
167    pub const fn row(self, y: i32) -> u8 {
168        self.rows[(y & 7) as usize]
169    }
170
171    /// Whether it makes screen pixel `(x, y)` white.
172    pub const fn is_white(self, x: i32, y: i32) -> bool {
173        self.row(y) >> (7 - (x & 7)) & 1 == 1
174    }
175}
176
177impl From<Pattern> for Shade {
178    fn from(pattern: Pattern) -> Self {
179        Self::rows(pattern.rows())
180    }
181}
182
183/// What a frame's colours are drawn in.
184pub trait Shades {
185    /// A cell's background in `rgb`.
186    fn ground(&self, rgb: Rgb) -> Shade;
187    /// A glyph in `rgb`.
188    fn glyph(&self, rgb: Rgb) -> Shade;
189    /// A rectangle, or a sprite painted solid, in `rgb`.
190    fn shape(&self, rgb: Rgb) -> Shade;
191}
192
193/// The game's inks (game ADR-0074): everything in `rgb` is drawn in
194/// [`ink_of`]`(rgb)`.
195#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
196pub struct InkShades;
197
198/// Each ink's [`Shade`], by `ink as usize`.
199const INK_SHADES: [Shade; Ink::ALL.len()] = {
200    let mut table = [Shade::rows([0; 8]); Ink::ALL.len()];
201    let mut i = 0;
202    while i < Ink::ALL.len() {
203        table[Ink::ALL[i] as usize] = Shade::of_ink(Ink::ALL[i]);
204        i += 1;
205    }
206    table
207};
208
209impl InkShades {
210    /// How `ink` draws, looked up.
211    const fn of(ink: Ink) -> Shade {
212        INK_SHADES[ink as usize]
213    }
214}
215
216impl Shades for InkShades {
217    fn ground(&self, rgb: Rgb) -> Shade {
218        Self::of(ink_of(rgb))
219    }
220
221    fn glyph(&self, rgb: Rgb) -> Shade {
222        Self::of(ink_of(rgb))
223    }
224
225    fn shape(&self, rgb: Rgb) -> Shade {
226        Self::of(ink_of(rgb))
227    }
228}
229
230/// The index in a [`Frame`] of the byte holding screen pixel `(x, y)`,
231/// which must be on the screen.
232#[allow(clippy::cast_sign_loss)] // on the screen
233const fn byte_index(x: i32, y: i32) -> usize {
234    y as usize * ROW_BYTES + (x >> 3) as usize
235}
236
237/// Where [`Font`] keeps that a glyph has no rows.
238const NO_ROWS: u32 = u32::MAX;
239
240/// The font as the one-bit renderer reads it: the glyph map and the
241/// one-bit atlas the game gives.
242#[derive(Copy, Clone, Debug)]
243pub struct Font<'a> {
244    atlas: &'a FontAtlasDef,
245    bits: &'a [u8],
246    /// Where in `bits` each ASCII character's rows start (the fallback
247    /// glyph's if the font lacks it), or [`NO_ROWS`]: the glyph map is a
248    /// tree, slow to search on the device, and nearly all text is ASCII.
249    ascii: [u32; 128],
250}
251
252impl<'a> Font<'a> {
253    /// The font of glyph map `atlas` and one-bit atlas `bits`. Fails if a
254    /// cell is wider than 8 pixels or `bits` lacks a glyph of the map.
255    pub fn new(atlas: &'a FontAtlasDef, bits: &'a [u8]) -> Result<Self, &'static str> {
256        if atlas.cell_w > 8 {
257            // check-text: not player text (a build problem for the log)
258            return Err("the one-bit font has cells at most 8 pixels wide");
259        }
260        let rows = atlas.cell_h as usize;
261        if atlas.cell_end() as usize * rows > bits.len() {
262            // check-text: not player text (a build problem for the log)
263            return Err("the one-bit atlas is shorter than its glyph map");
264        }
265        let mut ascii = [NO_ROWS; 128];
266        for (code, start) in (0u8..).zip(&mut ascii) {
267            let found = rows_start(atlas, char::from(code));
268            *start = found
269                .and_then(|at| u32::try_from(at).ok())
270                .unwrap_or(NO_ROWS);
271        }
272        Ok(Self { atlas, bits, ascii })
273    }
274
275    /// The pixel rows of `glyph` (a set bit is a pixel of the glyph), or
276    /// of the fallback glyph if the font lacks it.
277    pub fn rows(&self, glyph: char) -> Option<&'a [u8]> {
278        let start = match self.ascii.get(glyph as usize) {
279            Some(&NO_ROWS) => return None,
280            Some(&start) => start as usize,
281            None => rows_start(self.atlas, glyph)?,
282        };
283        self.bits.get(start..start + self.atlas.cell_h as usize)
284    }
285
286    /// The pixel rows of half `half` (0 the left, 1 the right) of wide
287    /// glyph `glyph`, from the wide set `set` first
288    /// ([`FontAtlasDef::wide_cell`]), or `None` if no set has `glyph`.
289    fn wide_rows(&self, glyph: char, half: usize, set: Option<&str>) -> Option<&'a [u8]> {
290        let rows = self.atlas.cell_h as usize;
291        let start = (self.atlas.wide_cell(set, glyph)? as usize + half) * rows;
292        self.bits.get(start..start + rows)
293    }
294
295    /// The pixel rows drawn in cell `(x, y)` of `buf`: its glyph's
296    /// ([`rows`](Self::rows)), or a half of a wide glyph's, the left in
297    /// its first cell and the right in its continuation. `None` for a
298    /// space, and for a continuation whose glyph the font has no wide
299    /// glyph for.
300    pub fn cell_rows(&self, buf: &GlyphBuffer, x: i32, y: i32) -> Option<&'a [u8]> {
301        let cell = buf.get(x, y)?;
302        if cell.is_continuation() {
303            let first = buf.get(x.checked_sub(1)?, y)?;
304            return self.wide_rows(first.glyph, 1, buf.widths().wide_font());
305        }
306        if cell.glyph == ' ' {
307            return None;
308        }
309        let wide = buf.get(x + 1, y).is_some_and(Cell::is_continuation);
310        match self
311            .wide_rows(cell.glyph, 0, buf.widths().wide_font())
312            .filter(|_| wide)
313        {
314            Some(rows) => Some(rows),
315            None => self.rows(cell.glyph),
316        }
317    }
318
319    /// How many screen rows a glyph drawn `scale` times as big covers.
320    fn height(&self, scale: i32) -> i32 {
321        i32::try_from(self.atlas.cell_h).map_or(i32::MAX, |h| h.saturating_mul(scale))
322    }
323}
324
325/// Where in a one-bit atlas the rows of `glyph` (or of the fallback
326/// glyph, if `atlas` lacks it) start.
327fn rows_start(atlas: &FontAtlasDef, glyph: char) -> Option<usize> {
328    let index = atlas
329        .glyphs
330        .get(&glyph)
331        .or_else(|| atlas.glyphs.get(&FALLBACK_GLYPH))?;
332    Some(*index as usize * atlas.cell_h as usize)
333}
334
335/// A one-bit picture: `height` rows of [`stride`](Image::stride) bytes, a
336/// byte's highest bit its left pixel, a set bit white; and optionally a
337/// mask of the same shape, a set bit where the picture is drawn (the
338/// Playdate's own bitmap layout). Without a mask every pixel is drawn.
339///
340/// Its **source size** is the size of the image a sprite names
341/// ([`ImageTable`](shuyi_assets::image::ImageTable)), whose pixels the
342/// sprite's `src` counts in. It is the picture's own size unless the
343/// picture was enlarged before it was dithered ([`shuyi_assets::bitmap`]):
344/// then each source pixel covers several of the picture's.
345#[derive(Copy, Clone, Debug, PartialEq, Eq)]
346pub struct Image<'a> {
347    width: u16,
348    height: u16,
349    source_width: u16,
350    source_height: u16,
351    data: &'a [u8],
352    mask: Option<&'a [u8]>,
353}
354
355impl<'a> Image<'a> {
356    /// The picture, its source size its own, or `None` if `data` (or
357    /// `mask`) is too short for it.
358    pub fn new(width: u16, height: u16, data: &'a [u8], mask: Option<&'a [u8]>) -> Option<Self> {
359        let size = Self::stride(width) * usize::from(height);
360        let fits = |bytes: &[u8]| bytes.len() >= size;
361        (fits(data) && mask.is_none_or(fits)).then_some(Self {
362            width,
363            height,
364            source_width: width,
365            source_height: height,
366            data,
367            mask,
368        })
369    }
370
371    /// Style `style` of one-bit picture file `bitmap`, with its mask and
372    /// source size, or `None` if it has no such style.
373    pub fn from_bitmap(bitmap: &Bitmap<'a>, style: usize) -> Option<Self> {
374        let data = bitmap.style(style)?;
375        let image = Self::new(bitmap.width(), bitmap.height(), data, bitmap.mask())?;
376        let (width, height) = bitmap.source_size();
377        Some(image.with_source(width, height))
378    }
379
380    /// The same picture standing for an image `width` × `height` pixels
381    /// (its source size); a side of 0 is left as it was.
382    #[must_use]
383    pub fn with_source(self, width: u16, height: u16) -> Self {
384        Self {
385            source_width: if width == 0 { self.source_width } else { width },
386            source_height: if height == 0 {
387                self.source_height
388            } else {
389                height
390            },
391            ..self
392        }
393    }
394
395    /// Bytes per row of a picture `width` pixels wide.
396    pub const fn stride(width: u16) -> usize {
397        (width as usize).div_ceil(8)
398    }
399
400    /// Pixel `(x, y)`: `Some(true)` white, `Some(false)` black, `None`
401    /// see-through or off the picture.
402    #[cfg(any(test, feature = "onebit-reference"))]
403    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] // checked first
404    fn pixel(&self, x: i64, y: i64) -> Option<bool> {
405        if !(0..i64::from(self.width)).contains(&x) || !(0..i64::from(self.height)).contains(&y) {
406            return None;
407        }
408        let at = y as usize * Self::stride(self.width) + (x >> 3) as usize;
409        let bit = |bytes: &[u8]| bytes[at] >> (7 - (x & 7)) & 1 == 1;
410        match self.mask {
411            Some(mask) if !bit(mask) => None,
412            _ => Some(bit(self.data)),
413        }
414    }
415
416    /// The picture's pixel along one side (`size` long, its source side
417    /// `source`) under the centre of pixel `d` of a `dest`-long stretch of
418    /// the source's `start..start + len`: nearest sampling, as `app` does.
419    /// With the source size the picture's own, it is `start + (2d + 1) ×
420    /// len / 2dest`.
421    fn sample(d: i64, start: i64, len: i64, dest: i64, size: u16, source: u16) -> i64 {
422        let (size, source) = (i64::from(size), i64::from(source.max(1)));
423        (start * 2 * dest + (2 * d + 1) * len) * size / (2 * dest * source)
424    }
425}
426
427/// Where the renderer finds a sprite's one-bit picture.
428pub trait Images {
429    /// The picture of `id`, or `None` if there is none (the sprite is
430    /// drawn as a grey rectangle).
431    fn image(&self, id: ImageId) -> Option<Image<'_>>;
432}
433
434/// The pictures behind a borrow are the same pictures: a `&dyn Images`
435/// draws as any other.
436impl<T: Images + ?Sized> Images for &T {
437    fn image(&self, id: ImageId) -> Option<Image<'_>> {
438        (**self).image(id)
439    }
440}
441
442/// No pictures at all.
443impl Images for () {
444    fn image(&self, _: ImageId) -> Option<Image<'_>> {
445        None
446    }
447}
448
449/// The pictures of a one-bit target's pack (`shuyi bake --target
450/// playdate`): the one-bit picture file at each image's path, its first
451/// style, read where it lies. A PNG, or no file, is no picture.
452impl Images for shuyi_assets::Bundle {
453    fn image(&self, id: ImageId) -> Option<Image<'_>> {
454        let bitmap = Bitmap::parse(self.bytes(id.path())?).ok()?;
455        Image::from_bitmap(&bitmap, 0)
456    }
457}
458
459/// The steps of a render, in order, for timing them on the device
460/// ([`render_marked`]).
461#[derive(Copy, Clone, Debug, PartialEq, Eq)]
462pub enum Phase {
463    /// The white ground.
464    Ground,
465    /// A backdrop's scene, all its steps.
466    Backdrop,
467    /// The cell backgrounds.
468    Backgrounds,
469    /// The `Under` items.
470    Under,
471    /// The white edges of edged glyphs.
472    Edges,
473    /// The glyphs.
474    Glyphs,
475    /// The `Over` items.
476    Over,
477    /// The invert rectangles.
478    Invert,
479}
480
481impl Phase {
482    /// Every phase, in drawing order.
483    pub const ALL: [Self; 8] = [
484        Self::Ground,
485        Self::Backdrop,
486        Self::Backgrounds,
487        Self::Under,
488        Self::Edges,
489        Self::Glyphs,
490        Self::Over,
491        Self::Invert,
492    ];
493
494    /// Its name in a log.
495    // check-text: not player text (the bench build's log)
496    pub const fn name(self) -> &'static str {
497        match self {
498            Self::Ground => "ground",
499            Self::Backdrop => "backdrop",
500            Self::Backgrounds => "backgrounds",
501            Self::Under => "under items",
502            Self::Edges => "edges",
503            Self::Glyphs => "glyphs",
504            Self::Over => "over items",
505            Self::Invert => "invert",
506        }
507    }
508}
509
510/// Draws `buf` into `out` in the game's inks ([`InkShades`]), from its
511/// top-left corner; whatever lies past 400 × 240 pixels is cut off. See
512/// the [module](self) for the order.
513pub fn render(buf: &GlyphBuffer, font: &Font<'_>, images: &impl Images, out: &mut Frame) {
514    render_with(buf, font, images, &InkShades, out);
515}
516
517/// [`render`], calling `mark` as each [`Phase`] ends (the Playdate's
518/// bench build times them).
519pub fn render_marked(
520    buf: &GlyphBuffer,
521    font: &Font<'_>,
522    images: &impl Images,
523    out: &mut Frame,
524    mark: &mut impl FnMut(Phase),
525) {
526    render_marked_with(buf, font, images, &InkShades, out, mark);
527}
528
529/// [`render`], its colours drawn as `shades` says.
530pub fn render_with(
531    buf: &GlyphBuffer,
532    font: &Font<'_>,
533    images: &impl Images,
534    shades: &impl Shades,
535    out: &mut Frame,
536) {
537    render_marked_with(buf, font, images, shades, out, &mut |_| {});
538}
539
540/// [`render_with`], calling `mark` as each [`Phase`] ends.
541pub fn render_marked_with(
542    buf: &GlyphBuffer,
543    font: &Font<'_>,
544    images: &impl Images,
545    shades: &impl Shades,
546    out: &mut Frame,
547    mark: &mut impl FnMut(Phase),
548) {
549    // The two unused bytes of each row too.
550    out.fill(u8::MAX);
551    mark(Phase::Ground);
552    let look = Look {
553        font,
554        images,
555        shades,
556        pictures: buf.pictures(),
557    };
558    draw(buf, &look, out, Clip::SCREEN, mark);
559}
560
561/// Makes `out`, which holds `old` as [`render`] drew it, hold `new` as
562/// `render` would draw it, redrawing only the 8 × 16 pixel tiles of the
563/// screen where they may differ: around a changed cell (one pixel more
564/// for a glyph's edge), under a changed item (the old and the new one),
565/// the window of a changed backdrop, and a painted item whose picture
566/// changed.
567pub fn render_changed(
568    old: &GlyphBuffer,
569    new: &GlyphBuffer,
570    font: &Font<'_>,
571    images: &impl Images,
572    out: &mut Frame,
573) {
574    render_changed_with(old, new, font, images, &InkShades, out);
575}
576
577/// [`render_changed`], the colours drawn as `shades` says (as they were
578/// for `old`).
579pub fn render_changed_with(
580    old: &GlyphBuffer,
581    new: &GlyphBuffer,
582    font: &Font<'_>,
583    images: &impl Images,
584    shades: &impl Shades,
585    out: &mut Frame,
586) {
587    if old.spec() != new.spec() {
588        render_with(new, font, images, shades, out);
589        return;
590    }
591    let mut dirty = Tiles::default();
592    let place = Place::ORIGIN;
593    let reach = Reach::of(new, font, 1);
594    let changed = |x: i32, y: i32| old.get(x, y) != new.get(x, y);
595    // A continuation draws its first cell's glyph's right half: it is
596    // drawn again when that cell changes. (Its first cell never needs the
597    // same for a change of the continuation: replacing a continuation
598    // makes its first cell a space.)
599    let continues = |x: i32, y: i32| new.get(x, y).is_some_and(Cell::is_continuation);
600    for y in 0..i32::from(new.height()) {
601        for x in 0..i32::from(new.width()) {
602            if changed(x, y) || (continues(x, y) && changed(x - 1, y)) {
603                dirty.add(reach.around(place, x, y));
604            }
605        }
606    }
607    let (before, after) = (old.items(), new.items());
608    // A painted item shows its picture as the frame's table has it.
609    let repainted = |items: &[Item]| {
610        items.iter().any(|item| match item {
611            Item::Painted(p) => old.pictures().get(p.picture) != new.pictures().get(p.picture),
612            _ => false,
613        })
614    };
615    for i in 0..before.len().max(after.len()) {
616        let (was, is) = (before.get(i), after.get(i));
617        if was != is || is.is_some_and(|item| repainted(core::slice::from_ref(item))) {
618            for item in was.into_iter().chain(is) {
619                dirty.add(place.screen(item.visible()));
620            }
621        }
622    }
623    let scene_repainted = new
624        .backdrop()
625        .is_some_and(|backdrop| repainted(backdrop.scene().items()));
626    if old.backdrop() != new.backdrop() || scene_repainted {
627        for backdrop in old.backdrop().into_iter().chain(new.backdrop()) {
628            dirty.add(place.screen(backdrop.clip_px()));
629        }
630    }
631    let look = Look {
632        font,
633        images,
634        shades,
635        pictures: new.pictures(),
636    };
637    for region in dirty.regions() {
638        Target {
639            frame: &mut *out,
640            clip: region,
641        }
642        .fill(region, Pen::WHITE);
643        draw(new, &look, out, region, &mut |_| {});
644    }
645}
646
647/// What the renderer draws with: the font, the pictures, what each
648/// colour is drawn in, and the frame's painted pictures (a backdrop's
649/// scene's painted items show them too).
650struct Look<'a, I, S> {
651    font: &'a Font<'a>,
652    images: &'a I,
653    shades: &'a S,
654    pictures: &'a PaintedTable,
655}
656
657/// The 8 × 16 pixel tiles of the screen (50 × 15) that must be redrawn,
658/// a bit each.
659#[derive(Default)]
660struct Tiles([u64; Tiles::ROWS]);
661
662impl Tiles {
663    const W: i32 = 8;
664    const H: i32 = 16;
665    const ROWS: usize = (HEIGHT / Self::H) as usize;
666
667    /// Marks every tile `rect` touches on the screen.
668    #[allow(clippy::cast_sign_loss)] // on the screen
669    fn add(&mut self, rect: Clip) {
670        let r = rect.within(Clip::SCREEN);
671        if r.is_empty() {
672            return;
673        }
674        let (first, last) = (r.x0 / Self::W, (r.x1 - 1) / Self::W);
675        let columns = (u64::MAX >> (63 - last)) & (u64::MAX << first);
676        for row in &mut self.0[(r.y0 / Self::H) as usize..=((r.y1 - 1) / Self::H) as usize] {
677            *row |= columns;
678        }
679    }
680
681    /// Each run of marked tiles along a row of them, as screen pixels.
682    /// Walks a fixed 64 columns a row, so it always ends.
683    #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)] // tiles
684    fn regions(&self) -> impl Iterator<Item = Clip> + '_ {
685        (0i32..).zip(self.0).flat_map(|(row, bits)| {
686            // A run starts at a marked tile whose left neighbour isn't.
687            let starts = bits & !(bits << 1);
688            (0..u64::BITS)
689                .filter(move |&column| starts >> column & 1 == 1)
690                .map(move |first| {
691                    let len = (bits >> first).trailing_ones();
692                    Clip {
693                        x0: first as i32 * Self::W,
694                        y0: row * Self::H,
695                        x1: (first + len) as i32 * Self::W,
696                        y1: (row + 1) * Self::H,
697                    }
698                })
699        })
700    }
701}
702
703/// Draws `buf` over the white ground of `region` of `out`: its backdrop's
704/// scene, then its own layers.
705fn draw<I: Images, S: Shades>(
706    buf: &GlyphBuffer,
707    look: &Look<'_, I, S>,
708    out: &mut Frame,
709    region: Clip,
710    mark: &mut impl FnMut(Phase),
711) {
712    let place = Place::ORIGIN;
713    if let Some(backdrop) = buf.backdrop() {
714        let window = place.screen(backdrop.clip_px());
715        let clip = region.within(window);
716        if !clip.is_empty() {
717            let (dx, dy) = backdrop.scene_offset(1);
718            let mut scene = Target {
719                frame: &mut *out,
720                clip,
721            };
722            let at = Place {
723                x: window.x0 + offset(dx),
724                y: window.y0 + offset(dy),
725                scale: i32::from(backdrop.zoom()),
726            };
727            draw_layers(&mut scene, backdrop.scene(), at, look, &mut |_| {});
728        }
729    }
730    mark(Phase::Backdrop);
731    let mut target = Target {
732        frame: out,
733        clip: region,
734    };
735    draw_layers(&mut target, buf, place, look, mark);
736}
737
738/// A scene's pixel offset, held within ±2³⁰: a scene that far out is off
739/// the screen anyway, however big (a buffer's pixels number less).
740#[allow(clippy::cast_possible_truncation)] // held within i32 first
741fn offset(d: i64) -> i32 {
742    d.clamp(-(1 << 30), 1 << 30) as i32
743}
744
745/// A rectangle of screen pixels, `x0..x1` × `y0..y1`.
746#[derive(Copy, Clone, Debug, PartialEq, Eq)]
747struct Clip {
748    x0: i32,
749    y0: i32,
750    x1: i32,
751    y1: i32,
752}
753
754impl Clip {
755    /// The whole screen.
756    const SCREEN: Self = Self {
757        x0: 0,
758        y0: 0,
759        x1: WIDTH,
760        y1: HEIGHT,
761    };
762
763    /// The part of this inside `other` (empty if none is).
764    fn within(self, other: Self) -> Self {
765        Self {
766            x0: self.x0.max(other.x0),
767            y0: self.y0.max(other.y0),
768            x1: self.x1.min(other.x1),
769            y1: self.y1.min(other.y1),
770        }
771    }
772
773    /// Whether it has no pixels.
774    const fn is_empty(self) -> bool {
775        self.x0 >= self.x1 || self.y0 >= self.y1
776    }
777}
778
779/// Where a buffer is drawn: its top-left pixel at screen `(x, y)`, each of
780/// its pixels `scale × scale` screen pixels.
781#[derive(Copy, Clone, Debug)]
782struct Place {
783    x: i32,
784    y: i32,
785    scale: i32,
786}
787
788impl Place {
789    /// The screen's own buffer: from the top-left, unscaled.
790    const ORIGIN: Self = Self {
791        x: 0,
792        y: 0,
793        scale: 1,
794    };
795
796    /// The screen pixels of buffer pixels `r`.
797    fn screen(self, r: PxRect) -> Clip {
798        let x0 = self.x + r.x * self.scale;
799        let y0 = self.y + r.y * self.scale;
800        Clip {
801            x0,
802            y0,
803            x1: x0 + r.w * self.scale,
804            y1: y0 + r.h * self.scale,
805        }
806    }
807}
808
809/// How far a buffer's cell, drawn at some scale, may reach on the screen:
810/// its size, or its glyph's if that is bigger, and a pixel all round for
811/// the glyph's white edge.
812#[derive(Copy, Clone)]
813struct Reach {
814    /// A cell's size on the screen.
815    cell_w: i32,
816    cell_h: i32,
817    /// How far right of and below its top-left a cell draws, edge and all.
818    right: i32,
819    below: i32,
820}
821
822impl Reach {
823    fn of(buf: &GlyphBuffer, font: &Font<'_>, scale: i32) -> Self {
824        let spec = buf.spec();
825        let (cell_w, cell_h) = (
826            i32::from(spec.cell_w) * scale,
827            i32::from(spec.cell_h) * scale,
828        );
829        Self {
830            cell_w,
831            cell_h,
832            right: cell_w.max(8 * scale) + 1,
833            below: cell_h.max(font.height(scale)).saturating_add(1),
834        }
835    }
836
837    /// The screen pixels cell `(x, y)` may draw on, at `at`.
838    fn around(self, at: Place, x: i32, y: i32) -> Clip {
839        let (x0, y0) = (at.x + x * self.cell_w, at.y + y * self.cell_h);
840        Clip {
841            x0: x0 - 1,
842            y0: y0 - 1,
843            x1: x0.saturating_add(self.right),
844            y1: y0.saturating_add(self.below),
845        }
846    }
847
848    /// The cells, of `count` along a line from `origin` in steps of
849    /// `size`, that may draw on pixels `lo..hi` of that line, reaching
850    /// `reach` past their start.
851    fn span(lo: i32, hi: i32, origin: i32, size: i32, reach: i32, count: u16) -> Range<i32> {
852        if size <= 0 {
853            return 0..0;
854        }
855        let first = (lo - origin).saturating_sub(reach).div_euclid(size);
856        let end = (hi - origin).saturating_add(size).div_euclid(size);
857        first.max(0)..end.min(i32::from(count))
858    }
859}
860
861/// A frame and the part of it that may be drawn on.
862struct Target<'f> {
863    frame: &'f mut Frame,
864    clip: Clip,
865}
866
867/// What a run of pixels does to the frame.
868#[derive(Copy, Clone)]
869enum Pen {
870    /// Paints them in the shade's pattern.
871    Paint(Shade),
872    /// Flips them.
873    Flip,
874}
875
876impl Pen {
877    /// Paints white.
878    const WHITE: Self = Self::ink(Ink::White);
879
880    /// Paints in `ink`.
881    const fn ink(ink: Ink) -> Self {
882        Self::Paint(InkShades::of(ink))
883    }
884
885    /// Byte `byte` with this pen applied to the pixels `mask` selects, on
886    /// screen row `y`.
887    const fn apply(self, byte: u8, mask: u8, y: i32) -> u8 {
888        match self {
889            // The pattern's bits where `mask` is set, the frame's elsewhere.
890            Pen::Paint(shade) => byte ^ ((byte ^ shade.row(y)) & mask),
891            Pen::Flip => byte ^ mask,
892        }
893    }
894}
895
896impl Target<'_> {
897    /// The pixels of a run from `x` (as [`run`](Self::run) takes it) if they
898    /// are one whole byte of the frame inside the clip: a glyph's row at a
899    /// whole cell, written straight.
900    #[allow(clippy::cast_possible_truncation)] // a byte's bits
901    fn whole_byte(&self, x: i32, bits: u64) -> Option<u8> {
902        let one_byte = x.trailing_zeros() >= 3 && bits << 8 == 0;
903        let inside = x >= self.clip.x0 && x + 8 <= self.clip.x1;
904        (one_byte && inside).then_some((bits >> 56) as u8)
905    }
906
907    /// Applies `pen` to the pixels of screen row `y` from `x` on that
908    /// `bits` selects (its highest bit is pixel `x`), inside the clip.
909    #[allow(
910        clippy::cast_possible_truncation,
911        clippy::cast_sign_loss,
912        clippy::cast_possible_wrap
913    )] // a byte's bits; offsets 0..=64; screen bytes
914    fn run(&mut self, y: i32, x: i32, bits: u64, pen: Pen) {
915        let clip = self.clip;
916        if bits == 0 || y < clip.y0 || y >= clip.y1 {
917            return;
918        }
919        if let Some(mask) = self.whole_byte(x, bits) {
920            let byte = &mut self.frame[byte_index(x, y)];
921            *byte = pen.apply(*byte, mask, y);
922            return;
923        }
924        let (lo, hi) = (x.max(clip.x0), x.saturating_add(64).min(clip.x1));
925        if lo >= hi {
926            return;
927        }
928        // Only the bits of pixels `lo..hi`.
929        let left = u64::MAX >> (lo - x);
930        let right = u64::MAX.checked_shr((hi - x) as u32).unwrap_or(0);
931        let bits = bits & left & !right;
932        let row = y as usize * ROW_BYTES;
933        for column in (lo >> 3)..=((hi - 1) >> 3) {
934            // The bits of the byte's 8 pixels, whose first lies `shift`
935            // pixels right of `x` (7 left of it at most).
936            let shift = column * 8 - x;
937            let mask = if shift >= 0 {
938                (bits << shift >> 56) as u8
939            } else {
940                (bits >> 56 >> -shift) as u8
941            };
942            let byte = &mut self.frame[row + column as usize];
943            *byte = pen.apply(*byte, mask, y);
944        }
945    }
946
947    /// Applies `pen` to every pixel of `rect` inside the clip: the bytes
948    /// at its ends in part, those between whole.
949    #[allow(clippy::cast_sign_loss)] // screen bytes
950    fn fill(&mut self, rect: Clip, pen: Pen) {
951        let r = rect.within(self.clip);
952        if r.is_empty() {
953            return;
954        }
955        let (first, last) = ((r.x0 >> 3) as usize, ((r.x1 - 1) >> 3) as usize);
956        let left = u8::MAX >> (r.x0 & 7);
957        let right = u8::MAX << (7 - ((r.x1 - 1) & 7));
958        let rows = &mut self.frame[r.y0 as usize * ROW_BYTES..r.y1 as usize * ROW_BYTES];
959        for (y, row) in (r.y0..).zip(rows.as_chunks_mut::<ROW_BYTES>().0) {
960            if first == last {
961                row[first] = pen.apply(row[first], left & right, y);
962                continue;
963            }
964            row[first] = pen.apply(row[first], left, y);
965            let whole = &mut row[first + 1..last];
966            match pen {
967                Pen::Paint(shade) => whole.fill(shade.row(y)),
968                Pen::Flip => whole.iter_mut().for_each(|byte| *byte = !*byte),
969            }
970            row[last] = pen.apply(row[last], right, y);
971        }
972    }
973}
974
975/// Draws `buf`'s cells and items at `at` into `target`: cell backgrounds
976/// (never a see-through cell's), `Under` items, the white edges of
977/// edged glyphs, the glyphs, `Over` items, the flipping rectangles. Only
978/// the cells that may reach into the clip are visited.
979fn draw_layers<I: Images, S: Shades>(
980    target: &mut Target<'_>,
981    buf: &GlyphBuffer,
982    at: Place,
983    look: &Look<'_, I, S>,
984    mark: &mut impl FnMut(Phase),
985) {
986    let (font, shades) = (look.font, look.shades);
987    let reach = Reach::of(buf, font, at.scale);
988    let clip = target.clip;
989    let columns = Reach::span(
990        clip.x0,
991        clip.x1,
992        at.x,
993        reach.cell_w,
994        reach.right,
995        buf.width(),
996    );
997    let rows = Reach::span(
998        clip.y0,
999        clip.y1,
1000        at.y,
1001        reach.cell_h,
1002        reach.below,
1003        buf.height(),
1004    );
1005    // Cells `x0..x1` of row `y`, on the screen.
1006    let span = |x0: i32, x1: i32, y: i32| Clip {
1007        x0: at.x + x0 * reach.cell_w,
1008        y0: at.y + y * reach.cell_h,
1009        x1: at.x + x1 * reach.cell_w,
1010        y1: at.y + (y + 1) * reach.cell_h,
1011    };
1012    let (columns, rows) = (&columns, &rows);
1013    // Each cell but a space with its top-left on the screen and where it
1014    // is.
1015    let cells = || {
1016        rows.clone().flat_map(move |y| {
1017            columns.clone().filter_map(move |x| {
1018                let top_left = (at.x + x * reach.cell_w, at.y + y * reach.cell_h);
1019                let cell = buf.get(x, y).filter(|c| c.glyph != ' ')?;
1020                Some((top_left, (x, y), cell))
1021            })
1022        })
1023    };
1024    let mut grounds = Remembered::new(|rgb| shades.ground(rgb));
1025    // Neighbouring backgrounds of one shade are filled as one rectangle.
1026    for y in rows.clone() {
1027        let mut from = (columns.start, None);
1028        for x in columns.start..=columns.end {
1029            let cell = (x < columns.end).then(|| buf.get(x, y)).flatten();
1030            let shade = cell.filter(|c| !c.see_through).map(|c| grounds.of(c.bg));
1031            if shade != from.1 {
1032                if let Some(shade) = from.1 {
1033                    target.fill(span(from.0, x, y), Pen::Paint(shade));
1034                }
1035                from = (x, shade);
1036            }
1037        }
1038    }
1039    mark(Phase::Backgrounds);
1040    draw_items(target, buf, Layer::Under, at, look);
1041    mark(Phase::Under);
1042    let mut glyphs = Remembered::new(|rgb| shades.glyph(rgb));
1043    for (top_left, (x, y), cell) in cells() {
1044        if glyphs.of(cell.fg).edged
1045            && let Some(rows) = font.cell_rows(buf, x, y)
1046        {
1047            draw_glyph(target, rows, top_left, at.scale, Glyph::Edge);
1048        }
1049    }
1050    mark(Phase::Edges);
1051    for (top_left, (x, y), cell) in cells() {
1052        if let Some(rows) = font.cell_rows(buf, x, y) {
1053            let shade = Glyph::Paint(glyphs.of(cell.fg));
1054            draw_glyph(target, rows, top_left, at.scale, shade);
1055        }
1056    }
1057    mark(Phase::Glyphs);
1058    draw_items(target, buf, Layer::Over, at, look);
1059    mark(Phase::Over);
1060    for item in buf.items() {
1061        if let Item::Rect(o) = item
1062            && shades.shape(o.color).flips
1063        {
1064            target.fill(at.screen(o.rect), Pen::Flip);
1065        }
1066    }
1067    mark(Phase::Invert);
1068}
1069
1070/// A colour's shade, remembering the last colour asked: neighbouring
1071/// cells mostly share their colours.
1072struct Remembered<F> {
1073    last: Option<(Rgb, Shade)>,
1074    shade: F,
1075}
1076
1077impl<F: Fn(Rgb) -> Shade> Remembered<F> {
1078    const fn new(shade: F) -> Self {
1079        Self { last: None, shade }
1080    }
1081
1082    /// The shade `rgb` is drawn in.
1083    fn of(&mut self, rgb: Rgb) -> Shade {
1084        match self.last {
1085            Some((last, shade)) if last == rgb => shade,
1086            _ => {
1087                let shade = (self.shade)(rgb);
1088                self.last = Some((rgb, shade));
1089                shade
1090            }
1091        }
1092    }
1093}
1094
1095/// Draws `buf`'s items of `layer` in order, but for the flipping
1096/// rectangles, which are drawn last.
1097fn draw_items<I: Images, S: Shades>(
1098    target: &mut Target<'_>,
1099    buf: &GlyphBuffer,
1100    layer: Layer,
1101    at: Place,
1102    look: &Look<'_, I, S>,
1103) {
1104    for item in buf.items().iter().filter(|item| item.layer() == layer) {
1105        match item {
1106            Item::Rect(o) => {
1107                let shade = look.shades.shape(o.color);
1108                if !shade.flips {
1109                    target.fill(at.screen(o.rect), Pen::Paint(shade));
1110                }
1111            }
1112            Item::Sprite(s) => draw_sprite(target, s, at, look),
1113            Item::Painted(p) => draw_painted(target, p, at, look),
1114            Item::Gizmo(g) => {
1115                let pen = Pen::Paint(look.shades.shape(g.color));
1116                g.for_each_run(|y, x0, x1| {
1117                    target.fill(at.screen(Rect::new(x0, y, x1 - x0, 1)), pen);
1118                });
1119            }
1120        }
1121    }
1122}
1123
1124/// What part of a glyph is drawn.
1125#[derive(Copy, Clone)]
1126enum Glyph {
1127    /// The glyph's pixels in this shade.
1128    Paint(Shade),
1129    /// White around it, one screen pixel wide all round (corners too).
1130    Edge,
1131}
1132
1133/// Draws a glyph's pixel `rows` in `part` with its cell's top-left at
1134/// screen `(left, top)`, each of its pixels `scale` screen pixels big.
1135#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] // a glyph's rows
1136#[allow(clippy::cast_sign_loss)] // checked to be a row first
1137fn draw_glyph(
1138    target: &mut Target<'_>,
1139    rows: &[u8],
1140    (left, top): (i32, i32),
1141    scale: i32,
1142    part: Glyph,
1143) {
1144    match part {
1145        Glyph::Paint(shade) => {
1146            let mut y = top;
1147            for &byte in rows {
1148                let bits = widen(byte, scale);
1149                for _ in 0..scale {
1150                    target.run(y, left, bits, Pen::Paint(shade));
1151                    y += 1;
1152                }
1153            }
1154        }
1155        Glyph::Edge => {
1156            let height = rows.len() as i32 * scale;
1157            // The glyph's pixels on screen row `top + dy`, from `left`.
1158            let row = |dy: i32| {
1159                if (0..height).contains(&dy) {
1160                    widen(rows[(dy / scale) as usize], scale)
1161                } else {
1162                    0
1163                }
1164            };
1165            for dy in -1..=height {
1166                // From one pixel left of the cell.
1167                let near = (row(dy - 1) | row(dy) | row(dy + 1)) >> 1;
1168                let edge = near | near << 1 | near >> 1;
1169                target.run(top + dy, left - 1, edge, Pen::WHITE);
1170            }
1171        }
1172    }
1173}
1174
1175/// `byte`'s eight pixels each `scale` (1 to 8) pixels wide, from the
1176/// highest bit down.
1177#[allow(clippy::cast_sign_loss)] // 1 to 8
1178fn widen(byte: u8, scale: i32) -> u64 {
1179    if scale == 1 {
1180        return u64::from(byte) << 56;
1181    }
1182    let scale = scale as u32;
1183    let block = u64::MAX >> (64 - scale);
1184    let mut out = 0u64;
1185    for i in 0..8 {
1186        if byte >> (7 - i) & 1 == 1 {
1187            out |= block << (64 - scale * (i + 1));
1188        }
1189    }
1190    out
1191}
1192
1193/// The Bayer 4 × 4 order a sprite's pixels fade out in, as its opacity
1194/// falls (`docs/design/playdate.md` dithers in the same pattern).
1195const BAYER: [[u32; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
1196
1197/// Whether a pixel of a sprite of `opacity` is drawn at screen `(x, y)`:
1198/// all of them at 255, none at 0, an even dither between.
1199#[allow(clippy::cast_sign_loss)] // 0..4
1200fn shows(opacity: u8, x: i32, y: i32) -> bool {
1201    let rank = BAYER[(y & 3) as usize][(x & 3) as usize];
1202    u32::from(opacity) * 16 > rank * 255 + 127
1203}
1204
1205/// The pixels of screen row `y` a sprite of `opacity` shows ([`shows`]),
1206/// in a byte of a [`Frame`]: like a pattern, the same all along the row.
1207fn shows_byte(opacity: u8, y: i32) -> u8 {
1208    (0..8)
1209        .filter(|&x| shows(opacity, x, y))
1210        .fold(0, |byte, x| byte + (0x80 >> x))
1211}
1212
1213/// The highest `n` bits of a byte (none for `n` ≤ 0, all for `n` ≥ 8).
1214#[allow(clippy::cast_possible_truncation)] // the low byte
1215fn ones(n: i32) -> u8 {
1216    (0xff00u16 >> n.clamp(0, 8)) as u8
1217}
1218
1219/// Of the eight columns from `u` on of a picture `width` wide, those left
1220/// of its right edge, as a byte. (A sprite shown pixel for pixel shows
1221/// none left of its left edge, [`Columns::of`].)
1222fn on_picture(u: i32, width: u16) -> u8 {
1223    ones(i32::from(width) - u)
1224}
1225
1226/// The eight pixels from column `u` on of a picture's row `row`, its
1227/// highest bit column `u`; a column past either end is 0.
1228#[allow(clippy::cast_possible_truncation)] // the high byte
1229fn byte_at(row: &[u8], u: i32) -> u8 {
1230    let get = |i: i32| {
1231        usize::try_from(i)
1232            .ok()
1233            .and_then(|i| row.get(i))
1234            .map_or(0, |&byte| byte)
1235    };
1236    let pair = u16::from_be_bytes([get(u >> 3), get((u >> 3) + 1)]);
1237    (pair << (u & 7) >> 8) as u8
1238}
1239
1240/// Whether a painted pixel of alpha `alpha` is drawn: from half opaque up,
1241/// as a one-bit picture's bake cuts by default.
1242pub(crate) const fn alpha_drawn(alpha: u8) -> bool {
1243    alpha >= 128
1244}
1245
1246/// `light` made darker by `brightness` (255 = as it is, 0 = black), in
1247/// integers rounded to the nearest.
1248pub(crate) fn darken(light: u8, brightness: u8) -> u8 {
1249    let product = (u32::from(light) * u32::from(brightness) + 127) / 255;
1250    u8::try_from(product).unwrap_or(u8::MAX)
1251}
1252
1253/// The eight pixels of a frame byte on screen row `y` (a byte's first
1254/// pixel is always at a multiple of 8 across) whose brightnesses are
1255/// `light` (0 black to 255 white, the first the leftmost), dithered as
1256/// ADR-0078 says: pixel `i` is white where `4 × BAYER[y % 8][i] + 2 ≥ 255 −
1257/// light[i]` ([`shuyi_assets::onebit::BAYER`]), the Bayer dither of
1258/// `shuyi bake` at the screen's pixels. Its highest bit is the leftmost
1259/// pixel, a set bit white. Integers only: the same on every platform.
1260#[allow(clippy::cast_sign_loss)] // 0..8
1261pub fn dither_row(light: [u8; 8], y: i32) -> u8 {
1262    let order = &shuyi_assets::onebit::BAYER[(y & 7) as usize];
1263    let mut byte = 0;
1264    for (i, (&level, &rank)) in light.iter().zip(order).enumerate() {
1265        if u16::from(rank) * 4 + 2 >= u16::from(u8::MAX - level) {
1266            byte |= 0x80 >> i;
1267        }
1268    }
1269    byte
1270}
1271
1272/// Where and how a picture is drawn: a sprite's placing, or a painted
1273/// picture's (unmirrored, solid, in its own colours).
1274#[derive(Copy, Clone)]
1275struct Placed {
1276    src: PxRect,
1277    dest: PxRect,
1278    flip_x: bool,
1279    opacity: u8,
1280    brightness: u8,
1281    paint: Paint,
1282}
1283
1284impl Placed {
1285    const fn of_sprite(s: &Sprite) -> Self {
1286        Self {
1287            src: s.src,
1288            dest: s.dest,
1289            flip_x: s.flip_x,
1290            opacity: s.opacity,
1291            brightness: s.brightness,
1292            paint: s.paint,
1293        }
1294    }
1295
1296    const fn of_painted(p: &PaintedSprite) -> Self {
1297        Self {
1298            src: p.src,
1299            dest: p.dest,
1300            flip_x: false,
1301            opacity: u8::MAX,
1302            brightness: p.brightness,
1303            paint: Paint::Image,
1304        }
1305    }
1306}
1307
1308/// One row of a sprite's picture: its pixels and its mask (`None`: all
1309/// drawn).
1310#[derive(Copy, Clone)]
1311struct PictureRow<'a> {
1312    data: &'a [u8],
1313    mask: Option<&'a [u8]>,
1314    width: u16,
1315}
1316
1317/// Which picture column each shown screen column of a sprite takes.
1318// The table is on the stack, built once a sprite: nothing here allocates.
1319#[allow(clippy::large_enum_variant)]
1320enum Columns {
1321    /// Pixel for pixel: screen column `x` takes picture column `k + x`,
1322    /// or `k − x` if `flipped`.
1323    Run { k: i32, flipped: bool },
1324    /// Screen column `x0 + i` takes picture column `table[i]`; one of the
1325    /// picture's width or more is off it.
1326    Table {
1327        x0: i32,
1328        table: [u16; WIDTH as usize],
1329    },
1330}
1331
1332impl Columns {
1333    /// The columns of `sprite`'s screen columns `shown`, drawn at `at`
1334    /// from a picture `width` wide standing for one `source_width` wide
1335    /// ([`Image`]'s source size): each takes the picture column under its
1336    /// centre ([`Image::sample`]).
1337    fn of_sprite(
1338        sprite: &Placed,
1339        (width, source_width): (u16, u16),
1340        at: Place,
1341        shown: Clip,
1342    ) -> Option<Self> {
1343        let (src, dest) = (sprite.src, sprite.dest);
1344        let (src_w, dest_w) = (i64::from(src.w), i64::from(dest.w));
1345        let column = |x: i32| {
1346            let mut dx = i64::from((x - at.x).div_euclid(at.scale)) - i64::from(dest.x);
1347            if sprite.flip_x {
1348                dx = dest_w - 1 - dx;
1349            }
1350            Image::sample(dx, src.x.into(), src_w, dest_w, width, source_width)
1351        };
1352        // Shown columns lie in `dest`, so from a `src` column of 0 or
1353        // more every sample divides a sum of 0 or more: a floor.
1354        let steady = at.scale == 1 && src.x >= 0;
1355        Self::of(shown.x0..shown.x1, steady, sprite.flip_x, width, column)
1356    }
1357
1358    /// The picture column screen column `x` (a shown one) takes, if it is
1359    /// on a picture `width` wide.
1360    #[allow(clippy::cast_sign_loss)] // `x` from `x0` on
1361    fn column(&self, x: i32, width: u16) -> Option<u16> {
1362        let u = match *self {
1363            Self::Run { k, flipped } => {
1364                let u = if flipped { k - x } else { k + x };
1365                u16::try_from(u).ok()?
1366            }
1367            Self::Table { x0, ref table } => table[(x - x0) as usize],
1368        };
1369        (u < width).then_some(u)
1370    }
1371
1372    /// The columns of screen columns `shown` (on the screen, at least
1373    /// one), screen column `x` taking picture column `column(x)` of a
1374    /// picture `width` wide; `None` if none is on the picture.
1375    ///
1376    /// If `steady`, each screen column is a point of the sampling of its
1377    /// own (scale 1) and each column the floor of a straight line's value
1378    /// (no rounding towards 0 of one below 0). Then, if the first and the
1379    /// last shown column are as far apart in the picture as on the
1380    /// screen, the sprite is pixel for pixel all along: the floor of a
1381    /// line steps by the whole numbers on either side of its slope, so
1382    /// either by 0 or 1 each, or by 1 or more, and steps that add up to as
1383    /// many as there are must each be 1. Otherwise a step of 0 and one of
1384    /// 2 can add up as two of 1: at a bigger scale, or where rounding
1385    /// towards 0 makes columns −0.5 and 0.5 both 0.
1386    #[allow(clippy::cast_sign_loss)] // `x` from `x0` on
1387    fn of(
1388        shown: Range<i32>,
1389        steady: bool,
1390        flipped: bool,
1391        width: u16,
1392        column: impl Fn(i32) -> i64,
1393    ) -> Option<Self> {
1394        let (first, last) = (column(shown.start), column(shown.end - 1));
1395        let span = if flipped { first - last } else { last - first };
1396        if steady && span == i64::from(shown.end - shown.start - 1) {
1397            if first.max(last) < 0 || first.min(last) >= i64::from(width) {
1398                return None;
1399            }
1400            // On the picture, so less than its width plus the screen's.
1401            let first = i32::try_from(first).ok()?;
1402            let k = if flipped {
1403                first + shown.start
1404            } else {
1405                first - shown.start
1406            };
1407            return Some(Self::Run { k, flipped });
1408        }
1409        let mut table = [u16::MAX; WIDTH as usize];
1410        for (x, u) in shown.clone().zip(&mut table) {
1411            *u = u16::try_from(column(x)).unwrap_or(u16::MAX);
1412        }
1413        Some(Self::Table {
1414            x0: shown.start,
1415            table,
1416        })
1417    }
1418
1419    /// The pixels of `row` that screen columns `x..x + 8` take, `lanes`
1420    /// of them (a byte, its highest bit `x`) shown: whether each is white,
1421    /// and whether it is drawn.
1422    #[allow(clippy::cast_sign_loss)] // `lanes` are shown columns
1423    fn bits(&self, row: PictureRow<'_>, x: i32, lanes: u8) -> (u8, u8) {
1424        let PictureRow { data, mask, width } = row;
1425        match *self {
1426            Self::Run { k, flipped } => {
1427                // The lowest of the eight picture columns.
1428                let u = if flipped { k - x - 7 } else { k + x };
1429                let white = byte_at(data, u);
1430                let drawn = on_picture(u, width) & mask.map_or(u8::MAX, |mask| byte_at(mask, u));
1431                if flipped {
1432                    (white.reverse_bits(), drawn.reverse_bits())
1433                } else {
1434                    (white, drawn)
1435                }
1436            }
1437            Self::Table { x0, ref table } => {
1438                let (mut white, mut drawn) = (0, 0);
1439                for i in 0..8 {
1440                    let bit = 0x80 >> i;
1441                    if lanes & bit == 0 {
1442                        continue;
1443                    }
1444                    let u = table[(x + i - x0) as usize];
1445                    if u >= width {
1446                        continue;
1447                    }
1448                    let (at, shift) = (usize::from(u >> 3), 7 - (u & 7));
1449                    if mask.is_none_or(|mask| mask[at] >> shift & 1 == 1) {
1450                        drawn |= bit;
1451                    }
1452                    if data[at] >> shift & 1 == 1 {
1453                        white |= bit;
1454                    }
1455                }
1456                (white, drawn)
1457            }
1458        }
1459    }
1460}
1461
1462/// Draws the visible part of `sprite` at `at`: each screen pixel takes
1463/// the picture's pixel under its centre, as nearest sampling does
1464/// ([`Image::sample`]), coloured as its paint says. Without an image it is
1465/// a grey rectangle.
1466///
1467/// A byte of the frame at a time (ticket 3056): which picture column each
1468/// screen column takes is worked out once for the sprite ([`Columns`]),
1469/// and the opacity's, brightness's and paint's pixels are patterns, a
1470/// byte a row. A sprite shown pixel for pixel takes a row's eight pixels
1471/// from the picture's in one shift.
1472fn draw_sprite<I: Images, S: Shades>(
1473    target: &mut Target<'_>,
1474    sprite: &Sprite,
1475    at: Place,
1476    look: &Look<'_, I, S>,
1477) {
1478    let shown = at.screen(sprite.clip).within(target.clip);
1479    if shown.is_empty() {
1480        return;
1481    }
1482    let Some(image) = look.images.image(sprite.image) else {
1483        target.fill(shown, Pen::ink(Ink::Grey));
1484        return;
1485    };
1486    let placed = Placed::of_sprite(sprite);
1487    draw_one_bit(target, &placed, &image, at, look.shades, shown);
1488}
1489
1490/// Draws a painted picture's item at `at` from the frame's table
1491/// (ADR-0078): a one-bit picture as a one-bit sprite of the same pixels,
1492/// an RGBA or indexed one dithered ([`draw_dithered`]); a grey rectangle
1493/// if the table lacks it.
1494fn draw_painted<I: Images, S: Shades>(
1495    target: &mut Target<'_>,
1496    painted: &PaintedSprite,
1497    at: Place,
1498    look: &Look<'_, I, S>,
1499) {
1500    let shown = at.screen(painted.clip).within(target.clip);
1501    if shown.is_empty() {
1502        return;
1503    }
1504    let Some(picture) = look.pictures.get(painted.picture) else {
1505        target.fill(shown, Pen::ink(Ink::Grey));
1506        return;
1507    };
1508    let placed = Placed::of_painted(painted);
1509    let (width, height) = picture.size();
1510    match picture.kind() {
1511        PaintedKind::OneBit => {
1512            if let Some(image) = Image::new(width, height, picture.pixels(), picture.mask()) {
1513                draw_one_bit(target, &placed, &image, at, look.shades, shown);
1514            }
1515        }
1516        PaintedKind::Rgba | PaintedKind::Indexed => {
1517            draw_dithered(target, &placed, picture, at, shown);
1518        }
1519    }
1520}
1521
1522/// Draws the screen pixels `shown` of an RGBA or indexed `picture` placed
1523/// as `placed` at `at`, dithered (ADR-0078): each takes the picture's
1524/// pixel under its centre, as a sprite's do; one less than half opaque
1525/// isn't drawn; the others' brightnesses, darkened by the item's, become
1526/// black and white a frame byte at a time ([`dither_row`]).
1527#[allow(clippy::cast_sign_loss)] // on the screen
1528fn draw_dithered(
1529    target: &mut Target<'_>,
1530    placed: &Placed,
1531    picture: &Painted,
1532    at: Place,
1533    shown: Clip,
1534) {
1535    let (width, height) = picture.size();
1536    let Some(columns) = Columns::of_sprite(placed, (width, width), at, shown) else {
1537        return;
1538    };
1539    // An indexed picture's colours' brightnesses, worked out once.
1540    let mut palette = [0u8; 256];
1541    for (light, c) in palette.iter_mut().zip(picture.palette()) {
1542        *light = brightness([c.r, c.g, c.b, u8::MAX]);
1543    }
1544    let (stride, pixels) = (picture.stride(), picture.pixels());
1545    let indexed = picture.kind() == PaintedKind::Indexed;
1546    let light = |u: u16, v: u16| {
1547        if indexed {
1548            let index = pixels[usize::from(v) * stride + usize::from(u)];
1549            return Some(palette[usize::from(index)]);
1550        }
1551        let rgba = picture.rgba_at(u, v);
1552        alpha_drawn(rgba[3]).then(|| brightness(rgba))
1553    };
1554    let (src, dest) = (placed.src, placed.dest);
1555    for y in shown.y0..shown.y1 {
1556        let dy = i64::from((y - at.y).div_euclid(at.scale)) - i64::from(dest.y);
1557        let v = Image::sample(
1558            dy,
1559            src.y.into(),
1560            src.h.into(),
1561            dest.h.into(),
1562            height,
1563            height,
1564        );
1565        let Some(v) = u16::try_from(v).ok().filter(|&v| v < height) else {
1566            continue;
1567        };
1568        let line = &mut target.frame[y as usize * ROW_BYTES..][..ROW_BYTES];
1569        for x in ((shown.x0 & !7)..shown.x1).step_by(8) {
1570            let lanes = ones(shown.x1 - x) & !ones(shown.x0 - x);
1571            let (mut levels, mut drawn) = ([0u8; 8], 0u8);
1572            for (i, level) in (0..8).zip(&mut levels) {
1573                let bit = 0x80 >> i;
1574                let pixel = (lanes & bit != 0)
1575                    .then(|| columns.column(x + i, width))
1576                    .flatten()
1577                    .and_then(|u| light(u, v));
1578                if let Some(pixel) = pixel {
1579                    *level = darken(pixel, placed.brightness);
1580                    drawn |= bit;
1581                }
1582            }
1583            let byte = &mut line[(x >> 3) as usize];
1584            *byte ^= (*byte ^ dither_row(levels, y)) & drawn;
1585        }
1586    }
1587}
1588
1589/// Draws the screen pixels `shown` of one-bit `image` placed as `sprite`
1590/// at `at` (the body of [`draw_sprite`]), a solid paint in its shade from
1591/// `shades`.
1592#[allow(clippy::cast_sign_loss)] // on the screen
1593fn draw_one_bit(
1594    target: &mut Target<'_>,
1595    sprite: &Placed,
1596    image: &Image<'_>,
1597    at: Place,
1598    shades: &impl Shades,
1599    shown: Clip,
1600) {
1601    let size = (image.width, image.source_width);
1602    let Some(columns) = Columns::of_sprite(sprite, size, at, shown) else {
1603        return;
1604    };
1605    let (src, dest) = (sprite.src, sprite.dest);
1606    let (src_h, dest_h) = (i64::from(src.h), i64::from(dest.h));
1607    let solid = match sprite.paint {
1608        Paint::Solid(color) => Some(shades.shape(color)),
1609        Paint::Image | Paint::Dimmed | Paint::Shaded(_) => None,
1610    };
1611    let opacity = [0, 1, 2, 3].map(|y| shows_byte(sprite.opacity, y));
1612    let brightness = [0, 1, 2, 3].map(|y| shows_byte(sprite.brightness, y));
1613    let stride = Image::stride(image.width);
1614    for y in shown.y0..shown.y1 {
1615        let dy = i64::from((y - at.y).div_euclid(at.scale)) - i64::from(dest.y);
1616        let v = Image::sample(
1617            dy,
1618            src.y.into(),
1619            src_h,
1620            dest_h,
1621            image.height,
1622            image.source_height,
1623        );
1624        let Some(v) = usize::try_from(v)
1625            .ok()
1626            .filter(|&v| v < usize::from(image.height))
1627        else {
1628            continue;
1629        };
1630        let start = v * stride;
1631        let row = PictureRow {
1632            data: &image.data[start..start + stride],
1633            mask: image.mask.map(|mask| &mask[start..start + stride]),
1634            width: image.width,
1635        };
1636        let (showing, light) = (opacity[(y & 3) as usize], brightness[(y & 3) as usize]);
1637        let line = &mut target.frame[y as usize * ROW_BYTES..][..ROW_BYTES];
1638        for x in ((shown.x0 & !7)..shown.x1).step_by(8) {
1639            let lanes = ones(shown.x1 - x) & !ones(shown.x0 - x);
1640            let (white, drawn) = columns.bits(row, x, lanes);
1641            let drawn = drawn & lanes & showing;
1642            // A darkened picture (game ADR-0075): fewer of its white pixels.
1643            let white = white & light;
1644            let bits = match (solid, sprite.paint) {
1645                (Some(shade), _) => shade.row(y),
1646                // One-bit has no dimming: every other pixel goes white
1647                // (`docs/design/playdate.md`, the faded listener).
1648                (None, Paint::Dimmed) => white | InkShades::of(Ink::Grey).row(y),
1649                (None, _) => white,
1650            };
1651            // The paint's bits where it is drawn, the frame's elsewhere.
1652            let byte = &mut line[(x >> 3) as usize];
1653            *byte ^= (*byte ^ bits) & drawn;
1654        }
1655    }
1656}
1657
1658#[cfg(test)]
1659mod painted_tests;
1660#[cfg(test)]
1661mod speed_tests;
1662#[cfg(test)]
1663mod tests;