Skip to main content

shuyi_ui/frame/
painted.rs

1//! Pictures a skin paints at run time (ADR-0078): a [`Painted`] picture
2//! whose pixels the game makes while it runs (a decoded video frame, a
3//! software renderer's screen, a plasma), kept between frames in the
4//! frame's [`PaintedTable`] and placed with a [`PaintedSprite`].
5//!
6//! The table lives in the [`GlyphBuffer`] a screen draws into: the
7//! game's buffer outlives every frame, so its pictures do too. A skin
8//! takes its picture from the table, writes its pixels when they change,
9//! and places it:
10//!
11//! ```
12//! use shuyi_ui::frame::painted::{Painted, PaintedId, PaintedSprite};
13//! use shuyi_ui::frame::{Cell, GlyphBuffer, Layer, PxRect, Rgb};
14//!
15//! const PLASMA: PaintedId = PaintedId::new("plasma");
16//! let mut buf = GlyphBuffer::new(20, 6, Cell::new(' ', Rgb::new(255, 255, 255), Rgb::new(0, 0, 0)));
17//! let picture = buf
18//!     .pictures_mut()
19//!     .get_or_insert_with(PLASMA, || Painted::rgba(64, 48));
20//! for pixel in picture.pixels_mut().chunks_exact_mut(4) {
21//!     pixel.copy_from_slice(&[255, 128, 0, 255]);
22//! }
23//! let whole = PxRect::new(0, 0, 64, 48);
24//! buf.add_painted(PaintedSprite::new(PLASMA, whole, PxRect::new(8, 8, 128, 96), Layer::Over));
25//! assert_eq!(buf.pictures().get(PLASMA).map(Painted::revision), Some(1));
26//! ```
27//!
28//! Every pixel is written with [`Painted::pixels_mut`] (or the palette
29//! with [`Painted::palette_mut`]), which moves the picture's
30//! [revision](Painted::revision): `shuyi-app` uploads a picture again only
31//! in a frame whose revision moved. Borrow them only in a frame that
32//! changes them.
33//!
34//! [`GlyphBuffer`]: crate::frame::glyph_buffer::GlyphBuffer
35
36use crate::prelude::*;
37use alloc::collections::BTreeMap;
38use alloc::collections::btree_map::Entry;
39use core::fmt;
40
41use shuyi_assets::fnv::Hasher;
42
43use crate::frame::color::Rgb;
44use crate::frame::glyph_buffer::{Layer, PxRect};
45
46/// How many colours an indexed picture's palette holds.
47pub const PALETTE_LEN: usize = 256;
48
49/// The game's name for a painted picture, as an image's path names an
50/// image: cheap to copy and compare.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
52pub struct PaintedId(&'static str);
53
54impl PaintedId {
55    /// The picture named `name`.
56    pub const fn new(name: &'static str) -> Self {
57        Self(name)
58    }
59
60    /// Its name.
61    pub const fn name(self) -> &'static str {
62        self.0
63    }
64}
65
66impl fmt::Display for PaintedId {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.write_str(self.0)
69    }
70}
71
72/// What a [`Painted`] picture's bytes are.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum PaintedKind {
76    /// Four bytes a pixel, red, green, blue and alpha, row after row.
77    Rgba,
78    /// A byte a pixel, an index into the picture's palette of
79    /// [`PALETTE_LEN`] colours, row after row. Every pixel is opaque.
80    Indexed,
81    /// A bit a pixel: rows of `ceil(width / 8)` bytes, a byte's highest
82    /// bit its leftmost pixel, a set bit white, as
83    /// [`onebit::Image`](crate::frame::onebit::Image) has them; with an
84    /// optional mask of the same shape, a set bit drawn.
85    OneBit,
86}
87
88impl PaintedKind {
89    /// Its name in a snapshot.
90    pub const fn name(self) -> &'static str {
91        match self {
92            Self::Rgba => "rgba",
93            Self::Indexed => "indexed",
94            Self::OneBit => "one-bit",
95        }
96    }
97
98    /// Bytes per row of a picture of this kind `width` pixels wide.
99    pub const fn stride(self, width: u16) -> usize {
100        match self {
101            Self::Rgba => width as usize * 4,
102            Self::Indexed => width as usize,
103            Self::OneBit => (width as usize).div_ceil(8),
104        }
105    }
106}
107
108/// A picture a skin paints at run time (ADR-0078). Its fields keep their
109/// sizes, so they are read and written through methods; the ones that
110/// write move its [revision](Self::revision).
111#[derive(Clone, PartialEq, Eq)]
112#[non_exhaustive]
113pub struct Painted {
114    /// First, so comparing two pictures stops at it when it differs.
115    revision: u32,
116    width: u16,
117    height: u16,
118    kind: PaintedKind,
119    pixels: Vec<u8>,
120    /// [`PALETTE_LEN`] colours for an indexed picture, else none.
121    palette: Vec<Rgb>,
122    /// A one-bit picture's mask, if it has one.
123    mask: Option<Vec<u8>>,
124}
125
126impl Painted {
127    /// A picture of `kind`, `width` × `height`, every byte 0.
128    fn blank(kind: PaintedKind, width: u16, height: u16) -> Self {
129        Self {
130            revision: 0,
131            width,
132            height,
133            kind,
134            pixels: vec![0; kind.stride(width) * usize::from(height)],
135            palette: Vec::new(),
136            mask: None,
137        }
138    }
139
140    /// An RGBA picture `width` × `height`, every pixel clear.
141    pub fn rgba(width: u16, height: u16) -> Self {
142        Self::blank(PaintedKind::Rgba, width, height)
143    }
144
145    /// An indexed picture `width` × `height`, every pixel index 0, its
146    /// palette the first [`PALETTE_LEN`] colours of `palette` and black
147    /// for the rest.
148    pub fn indexed(width: u16, height: u16, palette: &[Rgb]) -> Self {
149        let mut colours = palette
150            .iter()
151            .copied()
152            .take(PALETTE_LEN)
153            .collect::<Vec<_>>();
154        colours.resize(PALETTE_LEN, Rgb::new(0, 0, 0));
155        Self {
156            palette: colours,
157            ..Self::blank(PaintedKind::Indexed, width, height)
158        }
159    }
160
161    /// A one-bit picture `width` × `height`, every pixel black, without a
162    /// mask (every pixel drawn).
163    pub fn one_bit(width: u16, height: u16) -> Self {
164        Self::blank(PaintedKind::OneBit, width, height)
165    }
166
167    /// The same one-bit picture with a mask, every pixel drawn; any other
168    /// picture as it is.
169    #[must_use]
170    pub fn with_mask(mut self) -> Self {
171        if self.kind == PaintedKind::OneBit {
172            self.mask = Some(vec![u8::MAX; self.pixels.len()]);
173        }
174        self
175    }
176
177    /// Its width and height in pixels.
178    pub const fn size(&self) -> (u16, u16) {
179        (self.width, self.height)
180    }
181
182    /// What its bytes are.
183    pub const fn kind(&self) -> PaintedKind {
184        self.kind
185    }
186
187    /// Bytes per row of its [pixels](Self::pixels) (and mask).
188    pub const fn stride(&self) -> usize {
189        self.kind.stride(self.width)
190    }
191
192    /// How many times its pixels, palette or mask were borrowed for
193    /// writing, wrapping: a picture whose revision didn't move since a
194    /// frame is that frame's.
195    pub const fn revision(&self) -> u32 {
196        self.revision
197    }
198
199    /// Its bytes, as its [kind](Self::kind) says.
200    pub fn pixels(&self) -> &[u8] {
201        &self.pixels
202    }
203
204    /// Its bytes to write; moves the revision.
205    pub fn pixels_mut(&mut self) -> &mut [u8] {
206        self.touch();
207        &mut self.pixels
208    }
209
210    /// An indexed picture's [`PALETTE_LEN`] colours; none for another.
211    pub fn palette(&self) -> &[Rgb] {
212        &self.palette
213    }
214
215    /// An indexed picture's colours to write (none for another); moves the
216    /// revision.
217    pub fn palette_mut(&mut self) -> &mut [Rgb] {
218        self.touch();
219        &mut self.palette
220    }
221
222    /// A one-bit picture's mask, a set bit drawn, if it has one.
223    pub fn mask(&self) -> Option<&[u8]> {
224        self.mask.as_deref()
225    }
226
227    /// The mask to write, if it has one; then moves the revision.
228    pub fn mask_mut(&mut self) -> Option<&mut [u8]> {
229        if self.mask.is_some() {
230            self.touch();
231        }
232        self.mask.as_deref_mut()
233    }
234
235    /// Moves the revision.
236    fn touch(&mut self) {
237        self.revision = self.revision.wrapping_add(1);
238    }
239
240    /// Pixel `(x, y)`, which must lie in the picture, as RGBA8: an indexed
241    /// one's colour, opaque; a one-bit one white or black, clear where the
242    /// mask leaves it out.
243    pub fn rgba_at(&self, x: u16, y: u16) -> [u8; 4] {
244        let row = usize::from(y) * self.stride();
245        match self.kind {
246            PaintedKind::Rgba => {
247                let at = row + usize::from(x) * 4;
248                [
249                    self.pixels[at],
250                    self.pixels[at + 1],
251                    self.pixels[at + 2],
252                    self.pixels[at + 3],
253                ]
254            }
255            PaintedKind::Indexed => {
256                let c = self.palette[usize::from(self.pixels[row + usize::from(x)])];
257                [c.r, c.g, c.b, u8::MAX]
258            }
259            PaintedKind::OneBit => {
260                let (at, shift) = (row + usize::from(x >> 3), 7 - (x & 7));
261                let bit = |bytes: &[u8]| bytes[at] >> shift & 1 == 1;
262                let white = if bit(&self.pixels) { u8::MAX } else { 0 };
263                let drawn = self.mask.as_deref().is_none_or(bit);
264                [white, white, white, if drawn { u8::MAX } else { 0 }]
265            }
266        }
267    }
268
269    /// Writes the whole picture as RGBA8, row after row, into `out` (its
270    /// old bytes gone): each pixel as [`rgba_at`](Self::rgba_at) has it.
271    /// What a painter that draws RGBA uploads.
272    pub fn rgba_into(&self, out: &mut Vec<u8>) {
273        out.clear();
274        if self.kind == PaintedKind::Rgba {
275            out.extend_from_slice(&self.pixels);
276            return;
277        }
278        for y in 0..self.height {
279            for x in 0..self.width {
280                out.extend_from_slice(&self.rgba_at(x, y));
281            }
282        }
283    }
284
285    /// The FNV hash (`shuyi_assets::fnv`) of its size, kind, pixels,
286    /// palette and mask, not its revision: what a snapshot shows of it.
287    pub fn fingerprint(&self) -> u64 {
288        let mut hash = Hasher::new();
289        hash.word(u64::from(self.width));
290        hash.word(u64::from(self.height));
291        hash.sized(self.kind.name().as_bytes());
292        hash.sized(&self.pixels);
293        let palette: Vec<u8> = self.palette.iter().flat_map(|c| [c.r, c.g, c.b]).collect();
294        hash.sized(&palette);
295        match &self.mask {
296            Some(mask) => hash.sized(mask),
297            None => hash.word(u64::MAX),
298        }
299        hash.finish()
300    }
301
302    /// Whether it shows the same as `other`: everything but the revision.
303    fn same_look(&self, other: &Self) -> bool {
304        self.width == other.width
305            && self.height == other.height
306            && self.kind == other.kind
307            && self.pixels == other.pixels
308            && self.palette == other.palette
309            && self.mask == other.mask
310    }
311}
312
313/// Its size, kind, revision and fingerprint: its pixels would bury a
314/// failed test's message.
315impl fmt::Debug for Painted {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        write!(
318            f,
319            "Painted({}x{} {} r{} {:016x})",
320            self.width,
321            self.height,
322            self.kind.name(),
323            self.revision,
324            self.fingerprint()
325        )
326    }
327}
328
329/// Where a painted picture is drawn (ADR-0078): part of it, `src`,
330/// stretched to `dest` with nearest-pixel sampling, never smoothed.
331#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
332#[non_exhaustive]
333pub struct PaintedSprite {
334    /// Which picture of the frame's table.
335    pub picture: PaintedId,
336    /// The part of the picture shown, in its pixels. It must lie inside
337    /// the picture.
338    pub src: PxRect,
339    /// Where `src` is stretched to, in console pixels.
340    pub dest: PxRect,
341    /// The part of `dest` that is drawn; clipping only ever shrinks it,
342    /// as a sprite's.
343    pub clip: PxRect,
344    /// Under or over the glyphs.
345    pub layer: Layer,
346    /// How bright its pixels are drawn: 255 = as painted, lower = that
347    /// much darker ([`GlyphBuffer::fade`] lowers it).
348    ///
349    /// [`GlyphBuffer::fade`]: crate::frame::glyph_buffer::GlyphBuffer::fade
350    pub brightness: u8,
351}
352
353impl PaintedSprite {
354    /// `src` of `picture` shown in all of `dest`, as painted.
355    pub const fn new(picture: PaintedId, src: PxRect, dest: PxRect, layer: Layer) -> Self {
356        Self {
357            picture,
358            src,
359            dest,
360            clip: dest,
361            layer,
362            brightness: u8::MAX,
363        }
364    }
365
366    /// The part of `src` that maps to `clip`, as `[x, y, w, h]` in the
367    /// picture's pixels, as a sprite's
368    /// [`clipped_src`](crate::frame::glyph_buffer::Sprite::clipped_src).
369    pub fn clipped_src(&self) -> [f32; 4] {
370        crate::frame::glyph_buffer::clipped_src(self.src, self.dest, self.clip, false)
371    }
372}
373
374/// The painted pictures of a frame, by id (ADR-0078). A [`GlyphBuffer`]
375/// holds one ([`GlyphBuffer::pictures_mut`]).
376///
377/// [`GlyphBuffer`]: crate::frame::glyph_buffer::GlyphBuffer
378/// [`GlyphBuffer::pictures_mut`]: crate::frame::glyph_buffer::GlyphBuffer::pictures_mut
379#[derive(Clone, Default, PartialEq, Eq, Debug)]
380pub struct PaintedTable {
381    pictures: BTreeMap<PaintedId, Painted>,
382}
383
384impl PaintedTable {
385    /// The picture `id`, if the table has it.
386    pub fn get(&self, id: PaintedId) -> Option<&Painted> {
387        self.pictures.get(&id)
388    }
389
390    /// The picture `id` to change, if the table has it. Taking it moves
391    /// nothing; writing its pixels does.
392    pub fn get_mut(&mut self, id: PaintedId) -> Option<&mut Painted> {
393        self.pictures.get_mut(&id)
394    }
395
396    /// The picture `id`, made by `make` if the table hasn't one, whatever
397    /// its size: put another in with [`insert`](Self::insert).
398    pub fn get_or_insert_with(
399        &mut self,
400        id: PaintedId,
401        make: impl FnOnce() -> Painted,
402    ) -> &mut Painted {
403        self.pictures.entry(id).or_insert_with(make)
404    }
405
406    /// Puts `picture` in as `id`, in place of any it had: its revision
407    /// moves past the old one's, so it never names the old pixels.
408    pub fn insert(&mut self, id: PaintedId, mut picture: Painted) -> &mut Painted {
409        match self.pictures.entry(id) {
410            Entry::Occupied(mut slot) => {
411                picture.revision = slot.get().revision.wrapping_add(1);
412                slot.insert(picture);
413                slot.into_mut()
414            }
415            Entry::Vacant(slot) => slot.insert(picture),
416        }
417    }
418
419    /// Takes the picture `id` out.
420    pub fn remove(&mut self, id: PaintedId) -> Option<Painted> {
421        self.pictures.remove(&id)
422    }
423
424    /// Every picture, by id.
425    pub fn iter(&self) -> impl Iterator<Item = (PaintedId, &Painted)> + '_ {
426        self.pictures.iter().map(|(&id, picture)| (id, picture))
427    }
428
429    /// How many pictures it holds.
430    pub fn len(&self) -> usize {
431        self.pictures.len()
432    }
433
434    /// Whether it holds none.
435    pub fn is_empty(&self) -> bool {
436        self.pictures.is_empty()
437    }
438
439    /// Puts `picture` in as `id` unless the table's already looks the same
440    /// (its size, kind, pixels, palette and mask), which it then keeps: so
441    /// the revision moves only when the picture changed, and a skin may
442    /// make its picture afresh each frame and have it uploaded only when it
443    /// differs. One that differs replaces the table's, as
444    /// [`insert`](Self::insert) does.
445    pub fn update(&mut self, id: PaintedId, mut picture: Painted) -> &mut Painted {
446        match self.pictures.entry(id) {
447            Entry::Occupied(mut slot) => {
448                if !slot.get().same_look(&picture) {
449                    picture.revision = slot.get().revision.wrapping_add(1);
450                    slot.insert(picture);
451                }
452                slot.into_mut()
453            }
454            Entry::Vacant(slot) => slot.insert(picture),
455        }
456    }
457
458    /// Copies `picture` in as `id` as [`update`](Self::update) does: a
459    /// sub-buffer's or a scene's picture coming into the frame (ADR-0078
460    /// §3), copied only when it differs.
461    pub(crate) fn absorb(&mut self, id: PaintedId, picture: &Painted) {
462        if !self.get(id).is_some_and(|old| old.same_look(picture)) {
463            self.update(id, picture.clone());
464        }
465    }
466}
467
468#[cfg(test)]
469mod tests;