celesterender/rendering/
mod.rs

1pub mod entity;
2pub mod tileset;
3
4use std::{
5    collections::{BTreeMap, HashMap},
6    fs::File,
7    marker::PhantomData,
8    ops::{BitOr, Sub},
9    path::Path,
10};
11
12use anyhow::{anyhow, ensure, Context, Result};
13use celesteloader::{
14    archive::ModArchive,
15    atlas::Sprite,
16    map::{utils::parse_map_name, Bounds, Decal, Map, Pos, Room},
17    CelesteInstallation,
18};
19use tiny_skia::{
20    BlendMode, Color, IntSize, Paint, PathBuilder, Pattern, Pixmap, PixmapRef,
21    PremultipliedColorU8, Rect, Shader, Stroke, Transform,
22};
23use tracing::instrument;
24
25use crate::asset::{AssetDb, LookupAsset, SpriteLocation};
26
27use self::tileset::{tiles_to_matrix, tiles_to_matrix_scenery, Matrix, ParsedTileset};
28
29#[derive(Clone, Copy)]
30pub struct Layer(u8);
31impl Layer {
32    pub const NONE: Layer = Layer(0b00000000);
33    pub const ALL: Layer = Layer(0b00111111);
34    pub const TILES_BG: Layer = Layer(1 << 0);
35    pub const DECALS_BG: Layer = Layer(1 << 1);
36    pub const ENTITIES: Layer = Layer(1 << 2);
37    pub const TILES_FG: Layer = Layer(1 << 3);
38    pub const DECALS_FG: Layer = Layer(1 << 4);
39    pub const TRIGGERS: Layer = Layer(1 << 5);
40
41    pub fn has(self, other: Layer) -> bool {
42        self.0 & other.0 == other.0
43    }
44}
45impl BitOr for Layer {
46    type Output = Layer;
47
48    fn bitor(self, rhs: Self) -> Self::Output {
49        Layer(self.0.bitor(rhs.0))
50    }
51}
52impl Sub for Layer {
53    type Output = Layer;
54
55    fn sub(self, rhs: Self) -> Self::Output {
56        Layer(self.0 & !rhs.0)
57    }
58}
59
60pub struct MapTileset {
61    pub tileset_fg: HashMap<char, ParsedTileset>,
62    pub tileset_bg: HashMap<char, ParsedTileset>,
63}
64
65impl MapTileset {
66    pub fn vanilla(celeste: &CelesteInstallation) -> Result<Self> {
67        let fgtiles_xml = celeste.read_to_string("Content/Graphics/ForegroundTiles.xml")?;
68        let bgtiles_xml = celeste.read_to_string("Content/Graphics/BackgroundTiles.xml")?;
69        Self::parse(&fgtiles_xml, &bgtiles_xml)
70    }
71
72    pub fn parse(fgtiles_xml: &str, bgtiles_xml: &str) -> Result<Self> {
73        let tileset_fg =
74            celesteloader::tileset::parse_tilesets(fgtiles_xml).context("error parsing fgtiles")?;
75        let tileset_bg =
76            celesteloader::tileset::parse_tilesets(bgtiles_xml).context("error parsing bgtiles")?;
77
78        Ok(MapTileset {
79            tileset_fg: ParsedTileset::parse(&tileset_fg)?,
80            tileset_bg: ParsedTileset::parse(&tileset_bg)?,
81        })
82    }
83}
84
85pub struct CelesteRenderData {
86    pub gameplay_sprites: HashMap<String, Sprite>,
87    pub map_tileset: MapTileset,
88    pub gameplay_atlas: Pixmap,
89    pub scenery: Sprite,
90}
91
92impl CelesteRenderData {
93    pub fn for_map(
94        celeste: &CelesteInstallation,
95        archive: &mut ModArchive,
96        map: &Map,
97    ) -> Result<Self> {
98        let mut base = CelesteRenderData::base(celeste)?;
99        base.load_map_tileset(celeste, archive, map)?;
100        Ok(base)
101    }
102
103    pub fn vanilla(celeste: &CelesteInstallation) -> Result<Self> {
104        let mut base = CelesteRenderData::base(celeste)?;
105        base.map_tileset = MapTileset::vanilla(celeste)?;
106        Ok(base)
107    }
108
109    pub fn base(celeste: &CelesteInstallation) -> Result<Self> {
110        let gameplay_atlas_meta = celeste.gameplay_atlas()?;
111        let gameplay_atlas_image = celeste.decode_atlas_image(&gameplay_atlas_meta)?;
112        let gameplay_atlas = Pixmap::from_vec(
113            gameplay_atlas_image.2,
114            IntSize::from_wh(gameplay_atlas_image.0, gameplay_atlas_image.1).expect("atlas size"),
115        )
116        .expect("atlas size");
117
118        let scenery = gameplay_atlas_meta
119            .sprites
120            .iter()
121            .find(|i| i.path == "tilesets/scenery")
122            .expect("no scenery sprite")
123            .clone();
124
125        let gameplay_sprites = gameplay_atlas_meta
126            .sprites
127            .into_iter()
128            .map(|sprite| (sprite.path.clone(), sprite))
129            .collect::<HashMap<_, _>>();
130
131        Ok(CelesteRenderData {
132            map_tileset: MapTileset {
133                tileset_fg: HashMap::new(),
134                tileset_bg: HashMap::new(),
135            },
136            gameplay_atlas,
137            gameplay_sprites,
138            scenery,
139        })
140    }
141
142    pub fn load_map_tileset(
143        &mut self,
144        celeste: &CelesteInstallation,
145        archive: &mut ModArchive,
146        map: &Map,
147    ) -> Result<()> {
148        let (fgtiles, bgtiles) = archive.map_fgtiles_bgtiles(map)?;
149
150        let fgtiles = match fgtiles {
151            Some(fgtiles) => fgtiles,
152            None => celeste.read_to_string("Content/Graphics/ForegroundTiles.xml")?,
153        };
154        let bgtiles = match bgtiles {
155            Some(bgtiles) => bgtiles,
156            None => celeste.read_to_string("Content/Graphics/BackgroundTiles.xml")?,
157        };
158
159        self.map_tileset = MapTileset::parse(&fgtiles, &bgtiles)?;
160
161        Ok(())
162    }
163}
164
165pub struct RenderResult {
166    pub image: Pixmap,
167    pub bounds: Bounds,
168    pub unknown_entities: BTreeMap<String, u32>,
169}
170impl RenderResult {
171    /// Takes the image
172    #[tracing::instrument(skip_all, fields(path = path.as_ref().to_str().unwrap_or("")))]
173    pub fn save_png(
174        &mut self,
175        path: impl AsRef<Path>,
176        compression: png::Compression,
177    ) -> Result<(), png::EncodingError> {
178        let file = File::create(path)?;
179        self.encode_png(file, compression)
180    }
181
182    pub fn encode_png(
183        &mut self,
184        w: impl std::io::Write,
185        compression: png::Compression,
186    ) -> Result<(), png::EncodingError> {
187        let mut image = std::mem::replace(&mut self.image, Pixmap::new(1, 1).unwrap());
188
189        for pixel in image.pixels_mut() {
190            let c = pixel.demultiply();
191            // SAFETY: we just demultiplied
192            *pixel = unsafe {
193                PremultipliedColorU8::from_rgba(c.red(), c.green(), c.blue(), c.alpha())
194                    .unwrap_unchecked()
195            };
196        }
197
198        let mut encoder = png::Encoder::new(w, image.width(), image.height());
199        encoder.set_color(png::ColorType::Rgba);
200        encoder.set_depth(png::BitDepth::Eight);
201        encoder.set_compression(compression);
202        encoder.set_filter(png::Filter::Adaptive);
203        let mut writer = encoder.write_header()?;
204        writer.write_image_data(image.data())?;
205
206        Ok(())
207    }
208}
209
210pub struct RenderMapSettings<'a> {
211    pub layer: Layer,
212    pub include_room: &'a dyn Fn(&Room) -> bool,
213    pub status_update: &'a dyn Fn(usize, usize),
214}
215impl<'a> Default for RenderMapSettings<'a> {
216    fn default() -> Self {
217        Self {
218            layer: Layer::ALL,
219            include_room: &|_| true,
220            status_update: &|_, _| {},
221        }
222    }
223}
224impl<'a> RenderMapSettings<'a> {
225    pub fn include_room(self, f: &'a dyn Fn(&Room) -> bool) -> Self {
226        RenderMapSettings {
227            layer: self.layer,
228            include_room: f,
229            status_update: self.status_update,
230        }
231    }
232
233    pub fn status_update(self, f: &'a dyn Fn(usize, usize)) -> Self {
234        RenderMapSettings {
235            layer: self.layer,
236            include_room: self.include_room,
237            status_update: f,
238        }
239    }
240}
241
242#[instrument(skip_all, fields(name = map.package))]
243pub fn render<L: LookupAsset>(
244    render_data: &CelesteRenderData,
245    asset_db: &mut AssetDb<L>,
246    map: &Map,
247    settings: RenderMapSettings,
248) -> Result<RenderResult> {
249    fastrand::seed(2);
250
251    let parsed_map_name = parse_map_name(&map.package);
252
253    let mut map_bounds = Bounds::empty();
254    let mut rooms = Vec::new();
255    for room in &map.rooms {
256        if (settings.include_room)(room) {
257            map_bounds = map_bounds.join(room.bounds);
258            rooms.push(room);
259        }
260    }
261
262    ensure!(!rooms.is_empty(), "No rooms to render");
263
264    let pixmap = {
265        let size_pixels = map_bounds.size.0 as usize * map_bounds.size.1 as usize;
266
267        let data = {
268            let _span = tracing::info_span!("allocate_pixmap").entered();
269            allocate_data(size_pixels, [50, 50, 50, 255]).map_err(|_| {
270                anyhow!(
271                    "could not allocate {:.02}GiB",
272                    size_pixels as f32 * 4.0 / (1024.0 * 1024.0 * 1024.0)
273                )
274            })?
275        };
276
277        Pixmap::from_vec(
278            data,
279            IntSize::from_wh(map_bounds.size.0, map_bounds.size.1).unwrap(),
280        )
281        .context("failed to create pixmap")?
282    };
283
284    let mut cx = RenderContext {
285        map_bounds,
286        pixmap,
287        unknown_entities: Default::default(),
288        area_id: parsed_map_name.order,
289        _marker: PhantomData::<L>,
290    };
291    if parsed_map_name.name == "LostLevels" {
292        cx.area_id = Some(10);
293    }
294
295    for (i, room) in rooms.iter().enumerate() {
296        (settings.status_update)(i, rooms.len());
297        cx.render_room(room, render_data, asset_db, settings.layer)?;
298    }
299
300    Ok(RenderResult {
301        image: cx.pixmap,
302        bounds: map_bounds,
303        unknown_entities: cx.unknown_entities,
304    })
305}
306
307struct RenderContext<L> {
308    map_bounds: Bounds,
309    pixmap: Pixmap,
310    unknown_entities: BTreeMap<String, u32>,
311    area_id: Option<u32>,
312    _marker: PhantomData<L>,
313}
314
315impl<L: LookupAsset> RenderContext<L> {
316    /// World space to image space
317    fn transform_pos(&self, pos: Pos) -> (i32, i32) {
318        let top_left = self.map_bounds.position;
319        (pos.x - top_left.x, pos.y - top_left.y)
320    }
321    fn transform_pos_f32(&self, (x, y): (f32, f32)) -> (f32, f32) {
322        let top_left = self.map_bounds.position;
323        (x - top_left.x as f32, y - top_left.y as f32)
324    }
325
326    /// World space to image space
327    fn transform_bounds(&self, bounds: Bounds) -> Rect {
328        let pos = self.transform_pos(bounds.position);
329        Rect::from_xywh(
330            pos.0 as f32,
331            pos.1 as f32,
332            bounds.size.0 as f32,
333            bounds.size.1 as f32,
334        )
335        .unwrap()
336    }
337}
338
339struct SpriteDesc {
340    scale: (f32, f32),
341    justify: (f32, f32),
342    quad: Option<(i16, i16, i16, i16)>,
343    tint: Option<Color>,
344    rotation: f32,
345}
346
347impl Default for SpriteDesc {
348    fn default() -> Self {
349        Self {
350            justify: (0.5, 0.5),
351            scale: (1.0, 1.0),
352            quad: None,
353            tint: None,
354            rotation: 0.0,
355        }
356    }
357}
358
359impl<L: LookupAsset> RenderContext<L> {
360    pub fn circle(&mut self, pos: (f32, f32), radius: f32, color: Color) {
361        let (x, y) = self.transform_pos_f32(pos);
362
363        let mut pb = PathBuilder::new();
364        pb.push_circle(x, y, radius);
365
366        self.pixmap.stroke_path(
367            &pb.finish().unwrap(),
368            &Paint {
369                shader: tiny_skia::Shader::SolidColor(color),
370                anti_alias: false,
371                blend_mode: tiny_skia::BlendMode::Plus,
372
373                ..Default::default()
374            },
375            &Stroke::default(),
376            Transform::identity(),
377            None,
378        );
379    }
380
381    fn rect_inset(&mut self, inset: f32, map_pos: (f32, f32), size: (f32, f32), color: Color) {
382        let (rect_x, rect_y) = self.transform_pos_f32((map_pos.0 + inset, map_pos.1 + inset));
383        self.rect(
384            Rect::from_xywh(rect_x, rect_y, size.0 - (2. * inset), size.1 - (2. * inset)).unwrap(),
385            color,
386            BlendMode::SourceOver,
387        );
388    }
389
390    fn rect(&mut self, rect: Rect, color: Color, blend_mode: BlendMode) {
391        self.pixmap.fill_rect(
392            rect,
393            &Paint {
394                shader: tiny_skia::Shader::SolidColor(color),
395                anti_alias: false,
396                blend_mode,
397
398                ..Default::default()
399            },
400            Transform::identity(),
401            None,
402        );
403    }
404    fn stroke_rect(&mut self, rect: Rect, color: Color) {
405        let rect = Rect::from_ltrb(
406            rect.left(),
407            rect.top(),
408            rect.right() - 1.0,
409            rect.bottom() - 1.0,
410        )
411        .unwrap_or(rect);
412
413        let mut pb = PathBuilder::new();
414        pb.push_rect(rect);
415
416        self.pixmap.stroke_path(
417            &pb.finish().unwrap(),
418            &Paint {
419                shader: tiny_skia::Shader::SolidColor(color),
420                anti_alias: false,
421                ..Default::default()
422            },
423            &Stroke::default(),
424            Transform::identity(),
425            None,
426        );
427    }
428
429    fn sprite(
430        &mut self,
431        cx: &CelesteRenderData,
432        map_pos: (f32, f32),
433        sprite: SpriteLocation,
434        desc: SpriteDesc,
435    ) -> Result<()> {
436        let SpriteDesc {
437            scale,
438            justify,
439            quad,
440            tint,
441            rotation,
442        } = desc;
443
444        let (x, y) = self.transform_pos_f32(map_pos);
445
446        let (
447            mut real_w,
448            mut real_h,
449            mut sprite_w,
450            mut sprite_h,
451            sprite_offset_x,
452            sprite_offset_y,
453            atlas,
454        ) = match &sprite {
455            SpriteLocation::Atlas(sprite) => (
456                sprite.real_w,
457                sprite.real_h,
458                sprite.w,
459                sprite.h,
460                sprite.offset_x,
461                sprite.offset_y,
462                cx.gameplay_atlas.as_ref(),
463            ),
464            SpriteLocation::Raw(pixmap) => (
465                pixmap.width() as i16,
466                pixmap.height() as i16,
467                pixmap.width() as i16,
468                pixmap.height() as i16,
469                0,
470                0,
471                pixmap.as_ref(),
472            ),
473        };
474
475        let (quad_x, quad_y) = if let Some((quad_x, quad_y, quad_w, quad_h)) = quad {
476            real_w = quad_w;
477            sprite_w = quad_w;
478            real_h = quad_h;
479            sprite_h = quad_h;
480
481            (quad_x, quad_y)
482        } else {
483            (0, 0)
484        };
485
486        let justify_offset_x = (real_w as f32 * justify.0 + sprite_offset_x as f32) * scale.0;
487        let justify_offset_y = (real_h as f32 * justify.1 + sprite_offset_y as f32) * scale.1;
488        let draw_x = (x - justify_offset_x).floor();
489        let draw_y = (y - justify_offset_y).floor();
490
491        let pattern_transform = match sprite {
492            SpriteLocation::Atlas(sprite) => Transform::from_translate(
493                draw_x - sprite.x as f32 - quad_x as f32,
494                draw_y - sprite.y as f32 - quad_y as f32,
495            ),
496            SpriteLocation::Raw(_) => Transform::from_translate(draw_x, draw_y),
497        };
498
499        let scale_transform = Transform::from_translate(-draw_x, -draw_y)
500            .post_translate(-justify_offset_x, -justify_offset_y)
501            .post_rotate(rotation.to_degrees())
502            .post_translate(justify_offset_x, justify_offset_y)
503            .post_scale(scale.0, scale.1)
504            .post_translate(draw_x, draw_y);
505
506        let rect = Rect::from_xywh(draw_x, draw_y, sprite_w as f32, sprite_h as f32).unwrap();
507
508        self.pixmap.fill_rect(
509            rect,
510            &Paint {
511                shader: Pattern::new(
512                    atlas,
513                    tiny_skia::SpreadMode::Pad,
514                    tiny_skia::FilterQuality::Nearest,
515                    1.0,
516                    pattern_transform,
517                ),
518                anti_alias: false,
519                ..Default::default()
520            },
521            scale_transform,
522            None,
523        );
524
525        // TODO: tint should only tint the sprite itself
526        if let Some(tint) = tint {
527            self.pixmap.fill_rect(
528                rect,
529                &Paint {
530                    shader: Shader::SolidColor(tint),
531                    blend_mode: tiny_skia::BlendMode::Multiply,
532                    anti_alias: false,
533                    ..Default::default()
534                },
535                scale_transform,
536                None,
537            );
538        }
539
540        Ok(())
541    }
542
543    fn tile_sprite(
544        &mut self,
545        atlas: PixmapRef,
546        pos: Pos,
547        atlas_position: (i16, i16),
548        tint: Option<Color>,
549    ) {
550        let (x, y) = self.transform_pos(pos);
551
552        let rect = Rect::from_xywh(x as f32, y as f32, 8.0, 8.0).unwrap();
553
554        let pattern_transform = Transform::from_translate(
555            (x - atlas_position.0 as i32) as f32,
556            (y - atlas_position.1 as i32) as f32,
557        );
558        self.pixmap.fill_rect(
559            rect,
560            &Paint {
561                shader: Pattern::new(
562                    atlas,
563                    tiny_skia::SpreadMode::Pad,
564                    tiny_skia::FilterQuality::Nearest,
565                    1.0,
566                    pattern_transform,
567                ),
568                anti_alias: false,
569                ..Default::default()
570            },
571            Transform::identity(),
572            None,
573        );
574
575        if let Some(tint) = tint {
576            self.pixmap.fill_rect(
577                rect,
578                &Paint {
579                    shader: Shader::SolidColor(tint),
580                    blend_mode: tiny_skia::BlendMode::Multiply,
581                    anti_alias: false,
582                    ..Default::default()
583                },
584                Transform::identity(),
585                None,
586            );
587        }
588    }
589
590    #[instrument(skip_all, fields(name = room.name))]
591    fn render_room(
592        &mut self,
593        room: &Room,
594        cx: &CelesteRenderData,
595        asset_db: &mut AssetDb<L>,
596        layer: Layer,
597    ) -> Result<()> {
598        if false {
599            let mut pb = tiny_skia::PathBuilder::new();
600            pb.push_rect(self.transform_bounds(room.bounds));
601            let path = pb.finish().unwrap();
602            self.pixmap.stroke_path(
603                &path,
604                &Paint::default(),
605                &tiny_skia::Stroke::default(),
606                Transform::identity(),
607                None,
608            );
609        }
610
611        let bgtiles = tiles_to_matrix(room.bounds.size_tiles(), &room.bg_tiles_raw)?;
612        let fgtiles = tiles_to_matrix(room.bounds.size_tiles(), &room.fg_tiles_raw)?;
613
614        if layer.has(Layer::TILES_BG) {
615            self.render_tileset(room, &bgtiles, &cx.map_tileset.tileset_bg, cx, asset_db)?;
616            self.render_tileset_scenery(room, &room.scenery_bg_raw, cx)?;
617        }
618        if layer.has(Layer::DECALS_BG) {
619            self.render_decals(room, &room.decals_bg, cx, asset_db)?;
620        }
621        if layer.has(Layer::ENTITIES) {
622            // TODO: sort by depth
623            self.render_entities(room, &fgtiles, cx, asset_db)?;
624        }
625        if layer.has(Layer::TILES_FG) {
626            self.render_tileset(room, &fgtiles, &cx.map_tileset.tileset_fg, cx, asset_db)?;
627            self.render_tileset_scenery(room, &room.scenery_fg_raw, cx)?;
628        }
629        if layer.has(Layer::DECALS_FG) {
630            self.render_decals(room, &room.decals_fg, cx, asset_db)?;
631        }
632        if layer.has(Layer::TRIGGERS) {
633            // trigger
634        }
635
636        Ok(())
637    }
638
639    fn render_tileset(
640        &mut self,
641        room: &Room,
642        tiles: &Matrix<char>,
643        tilesets: &HashMap<char, ParsedTileset>,
644        cx: &CelesteRenderData,
645        asset_db: &mut AssetDb<L>,
646    ) -> Result<()> {
647        let tile_pos = room.bounds.position;
648        self.render_tileset_inner(
649            room.bounds.size_tiles(),
650            tile_pos,
651            tiles,
652            tilesets,
653            cx,
654            asset_db,
655        )
656    }
657
658    #[instrument(skip_all)]
659    fn render_tileset_inner(
660        &mut self,
661        size: (u32, u32),
662        tile_pos: Pos,
663        tiles: &Matrix<char>,
664        tilesets: &HashMap<char, ParsedTileset>,
665        cx: &CelesteRenderData,
666        asset_db: &mut AssetDb<L>,
667    ) -> Result<()> {
668        let (w, h) = size;
669
670        for x in 0..w {
671            for y in 0..h {
672                let c = tiles.get(x, y);
673
674                if c == '0' {
675                    continue;
676                }
677
678                let tileset = tilesets
679                    .get(&c)
680                    .ok_or_else(|| anyhow!("tileset for '{}' not found", c))?;
681
682                let random_tiles = tileset::choose_tile(tileset, x, y, tiles)?.unwrap();
683                let sprite_tile_offset = fastrand::choice(random_tiles).unwrap();
684
685                let sprite = asset_db.lookup_gameplay(cx, &format!("tilesets/{}", tileset.path))?;
686
687                let (sprite_x, sprite_y, sprite_offset_x, sprite_offset_y, atlas) = match &sprite {
688                    SpriteLocation::Atlas(sprite) => (
689                        sprite.x,
690                        sprite.y,
691                        sprite.offset_x,
692                        sprite.offset_y,
693                        cx.gameplay_atlas.as_ref(),
694                    ),
695                    SpriteLocation::Raw(pixmap) => {
696                        // dbg!(sprite_tile_offset);
697                        (0, 0, 0, 0, pixmap.as_ref())
698                    }
699                };
700
701                let sprite_pos = (
702                    sprite_x + sprite_tile_offset.0 as i16 * 8,
703                    sprite_y + sprite_tile_offset.1 as i16 * 8,
704                );
705
706                if sprite_offset_x != 0 {
707                    panic!();
708                }
709                if sprite_offset_y != 0 {
710                    panic!();
711                }
712
713                self.tile_sprite(
714                    atlas,
715                    tile_pos.offset_tile(x as i32, y as i32),
716                    sprite_pos,
717                    None,
718                );
719            }
720        }
721
722        Ok(())
723    }
724
725    #[instrument(skip_all)]
726    fn render_tileset_scenery(
727        &mut self,
728        room: &Room,
729        tiles: &str,
730        cx: &CelesteRenderData,
731    ) -> Result<()> {
732        let (w, h) = room.bounds.size_tiles();
733
734        let matrix = tiles_to_matrix_scenery(room.bounds.size_tiles(), tiles);
735
736        for x in 0..w {
737            for y in 0..h {
738                let index = matrix.get(x, y);
739
740                if index == -1 {
741                    continue;
742                }
743
744                let scenery_width = cx.scenery.real_w / 8;
745                let _scenery_height = cx.scenery.real_h / 8;
746                let quad_x = index % scenery_width;
747                let quad_y = index / scenery_width;
748
749                let sprite_x = cx.scenery.x - cx.scenery.offset_x + quad_x * 8;
750                let sprite_y = cx.scenery.y - cx.scenery.offset_y + quad_y * 8;
751                let _w = 8;
752                let _h = 8;
753
754                let tile_pos = room.bounds.position.offset_tile(x as i32, y as i32);
755                self.tile_sprite(
756                    cx.gameplay_atlas.as_ref(),
757                    tile_pos,
758                    (sprite_x, sprite_y),
759                    None,
760                );
761            }
762        }
763
764        Ok(())
765    }
766
767    #[instrument(skip_all)]
768    fn render_decals(
769        &mut self,
770        room: &Room,
771        decals: &[Decal],
772        cx: &CelesteRenderData,
773        asset_db: &mut AssetDb<L>,
774    ) -> Result<()> {
775        for decal in decals {
776            let map_pos = (
777                room.bounds.position.x as f32 + decal.x,
778                room.bounds.position.y as f32 + decal.y,
779            );
780
781            let sprite = asset_db.lookup_gameplay(cx, &format!("decals/{}", decal.texture))?;
782            self.sprite(
783                cx,
784                map_pos,
785                sprite,
786                SpriteDesc {
787                    scale: (decal.scale_x, decal.scale_y),
788                    ..Default::default()
789                },
790            )?;
791        }
792
793        Ok(())
794    }
795
796    fn render_entities(
797        &mut self,
798        room: &Room,
799        fgtiles: &Matrix<char>,
800        cx: &CelesteRenderData,
801        asset_db: &mut AssetDb<L>,
802    ) -> Result<()> {
803        {
804            let _span = tracing::info_span!("render_entities_pre").entered();
805            for e in &room.entities {
806                entity::pre_render_entity(self, cx, asset_db, room, e)?;
807            }
808        }
809        {
810            let _span = tracing::info_span!("render_entities").entered();
811            for e in &room.entities {
812                if !entity::render_entity(self, fgtiles, cx, asset_db, room, e)
813                    .with_context(|| format!("couldn't render entity {}", e.name))?
814                {
815                    *self.unknown_entities.entry(e.name.clone()).or_default() += 1;
816                }
817            }
818        }
819
820        Ok(())
821    }
822}
823
824pub fn allocate_data(
825    size_pixels: usize,
826    default_color_premultiplied: [u8; 4],
827) -> Result<Vec<u8>, ()> {
828    use std::alloc::{alloc, Layout};
829
830    let size_bytes = size_pixels * 4;
831
832    assert!(size_bytes > 0);
833    unsafe {
834        // SAFETY: layout has non-zero size
835        let allocation = alloc(Layout::from_size_align(size_bytes, 4).unwrap()) as *mut [u8; 4];
836        if allocation.is_null() {
837            return Err(());
838        }
839
840        for i in 0..size_pixels {
841            // SAFETY: inbounds, aligned, valid for write
842            allocation.add(i).write(default_color_premultiplied);
843        }
844
845        // SAFETY: global allocator, u8 has align 1, size matches, length=capacity
846        Ok(Vec::from_raw_parts(
847            allocation.cast::<u8>(),
848            size_bytes,
849            size_bytes,
850        ))
851    }
852}