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;