engine/
resources.rs

1// SPDX-FileCopyrightText: 2024 Jens Pitkänen <jens.pitkanen@helsinki.fi>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5mod assets;
6mod chunks;
7mod deserialize;
8mod file_reader;
9mod loader;
10mod serialize;
11
12use assets::{audio_clip::AudioClipAsset, sprite::SpriteAsset};
13use platform::{PixelFormat, Platform, AUDIO_CHANNELS};
14
15pub use assets::*;
16pub use chunks::{ChunkData, ChunkDescriptor, SpriteChunkData, SpriteChunkDescriptor};
17pub use deserialize::{deserialize, Deserialize};
18pub use file_reader::FileReader;
19pub use loader::ResourceLoader;
20pub use serialize::{serialize, Serialize};
21
22use crate::{
23    allocators::LinearAllocator,
24    collections::{FixedVec, SparseArray},
25};
26
27/// Magic number used when de/serializing [`ResourceDatabaseHeader`].
28pub const RESOURCE_DB_MAGIC_NUMBER: u32 = 0xE97E6D00;
29/// Amount of bytes in the regular dynamically allocated chunks.
30pub const CHUNK_SIZE: u32 = 64 * 1024;
31/// Width and height of the dynamically allocated sprite chunks.
32pub const SPRITE_CHUNK_DIMENSIONS: (u16, u16) = (128, 128);
33/// Pixel format of the dynamically allocated sprite chunks.
34pub const SPRITE_CHUNK_FORMAT: PixelFormat = PixelFormat::Rgba;
35
36/// The amount of audio samples that fit in each chunk.
37pub const AUDIO_SAMPLES_PER_CHUNK: usize = CHUNK_SIZE as usize / size_of::<[i16; AUDIO_CHANNELS]>();
38
39/// Basic info about a [`ResourceDatabase`] used in its initialization and for
40/// de/serializing the db file.
41#[derive(Clone, Copy)]
42pub struct ResourceDatabaseHeader {
43    /// The amount of regular chunks in the database.
44    pub chunks: u32,
45    /// The amount of sprite chunks in the database.
46    pub sprite_chunks: u32,
47    /// The amount of [`SpriteAsset`]s in the database.
48    pub sprites: u32,
49    /// The amount of [`AudioClipAsset`]s in the database.
50    pub audio_clips: u32,
51}
52
53impl ResourceDatabaseHeader {
54    /// Returns the byte offset into the resource database file where the chunks
55    /// start.
56    ///
57    /// This is the size of the header, chunk descriptors, and asset metadata.
58    pub const fn chunk_data_offset(&self) -> u64 {
59        use serialize::Serialize as Ser;
60        <ResourceDatabaseHeader as Ser>::SERIALIZED_SIZE as u64
61            + self.chunks as u64 * <ChunkDescriptor as Ser>::SERIALIZED_SIZE as u64
62            + self.sprite_chunks as u64 * <SpriteChunkDescriptor as Ser>::SERIALIZED_SIZE as u64
63            + self.sprites as u64 * <NamedAsset<SpriteAsset> as Ser>::SERIALIZED_SIZE as u64
64            + self.audio_clips as u64 * <NamedAsset<AudioClipAsset> as Ser>::SERIALIZED_SIZE as u64
65    }
66}
67
68/// The resource database.
69///
70/// Game code should mostly use this for the `find_*` and `get_*` functions to
71/// query for assets, which implement the relevant logic for each asset type.
72pub struct ResourceDatabase {
73    // Asset metadata
74    sprites: FixedVec<'static, NamedAsset<SpriteAsset>>,
75    audio_clips: FixedVec<'static, NamedAsset<AudioClipAsset>>,
76    // Chunk loading metadata
77    chunk_data_offset: u64,
78    chunk_descriptors: FixedVec<'static, ChunkDescriptor>,
79    sprite_chunk_descriptors: FixedVec<'static, SpriteChunkDescriptor>,
80    // In-memory chunks
81    /// The regular chunks currently loaded in-memory. Loaded via
82    /// [`ResourceLoader`], usually by functions making use of an asset.
83    pub chunks: SparseArray<'static, ChunkData>,
84    /// The sprite chunks currently loaded in-memory. Loaded via
85    /// [`ResourceLoader`], usually by functions making use of an asset.
86    pub sprite_chunks: SparseArray<'static, SpriteChunkData>,
87}
88
89impl ResourceDatabase {
90    pub(crate) fn new(
91        platform: &dyn Platform,
92        arena: &'static LinearAllocator,
93        file_reader: &mut FileReader,
94        max_loaded_chunks: u32,
95        max_loaded_sprite_chunks: u32,
96    ) -> Option<ResourceDatabase> {
97        profiling::function_scope!();
98        use Deserialize as De;
99        let header_size = <ResourceDatabaseHeader as De>::SERIALIZED_SIZE;
100
101        assert!(file_reader.push_read(0, header_size));
102        let header = file_reader
103            .pop_read(platform, true, |header_bytes| {
104                deserialize::<ResourceDatabaseHeader>(header_bytes, &mut 0)
105            })
106            .expect("resource database file should be readable");
107
108        let chunk_data_offset = header.chunk_data_offset();
109        let ResourceDatabaseHeader {
110            chunks,
111            sprite_chunks,
112            sprites,
113            audio_clips,
114        } = header;
115
116        let mut cursor = header_size;
117        let mut queue_read = |size: usize| {
118            assert!(file_reader.push_read(cursor as u64, size));
119            cursor += size;
120        };
121
122        queue_read(chunks as usize * <ChunkDescriptor as De>::SERIALIZED_SIZE);
123        queue_read(sprite_chunks as usize * <SpriteChunkDescriptor as De>::SERIALIZED_SIZE);
124        queue_read(sprites as usize * <NamedAsset<SpriteAsset> as De>::SERIALIZED_SIZE);
125        queue_read(audio_clips as usize * <NamedAsset<AudioClipAsset> as De>::SERIALIZED_SIZE);
126
127        // NOTE: These deserialize_vec calls must be in the same order as the queue_reads above.
128        let chunk_descriptors = deserialize_vec(arena, file_reader, platform)?;
129        let sprite_chunk_descriptors = deserialize_vec(arena, file_reader, platform)?;
130        let sprites = sorted(deserialize_vec(arena, file_reader, platform)?);
131        let audio_clips = sorted(deserialize_vec(arena, file_reader, platform)?);
132
133        Some(ResourceDatabase {
134            sprites,
135            audio_clips,
136            chunk_data_offset,
137            chunk_descriptors,
138            sprite_chunk_descriptors,
139            chunks: SparseArray::new(arena, chunks, max_loaded_chunks)?,
140            sprite_chunks: SparseArray::new(arena, sprite_chunks, max_loaded_sprite_chunks)?,
141        })
142    }
143
144    /// Returns the longest source bytes length of all the chunks, i.e. the
145    /// minimum amount of staging memory required to be able to load any chunk
146    /// in this database.
147    pub fn largest_chunk_source(&self) -> u64 {
148        let largest_chunk_source = (self.chunk_descriptors.iter())
149            .map(|chunk| chunk.source_bytes.end - chunk.source_bytes.start)
150            .max()
151            .unwrap_or(0);
152        let largest_sprite_chunk_source = (self.sprite_chunk_descriptors.iter())
153            .map(|chunk| chunk.source_bytes.end - chunk.source_bytes.start)
154            .max()
155            .unwrap_or(0);
156        largest_chunk_source.max(largest_sprite_chunk_source)
157    }
158}
159
160fn sorted<T: Ord>(mut input: FixedVec<'_, T>) -> FixedVec<'_, T> {
161    input.sort_unstable();
162    input
163}
164
165fn deserialize_vec<'a, D: Deserialize>(
166    alloc: &'a LinearAllocator,
167    file_reader: &mut FileReader,
168    platform: &dyn Platform,
169) -> Option<FixedVec<'a, D>> {
170    file_reader
171        .pop_read(platform, true, |src| {
172            let count = src.len() / D::SERIALIZED_SIZE;
173            let mut vec = FixedVec::new(alloc, count)?;
174            assert_eq!(0, vec.len() % D::SERIALIZED_SIZE);
175            for element_bytes in src.chunks_exact(D::SERIALIZED_SIZE) {
176                let Ok(_) = vec.push(D::deserialize(element_bytes)) else {
177                    unreachable!()
178                };
179            }
180            Some(vec)
181        })
182        .expect("resource db file header should be readable")
183}
184
185pub use named_asset::{NamedAsset, ASSET_NAME_LENGTH};
186mod named_asset {
187    use core::cmp::Ordering;
188
189    use arrayvec::ArrayString;
190
191    /// Maximum length for the unique names of assets.
192    pub const ASSET_NAME_LENGTH: usize = 27;
193
194    /// A unique name and a `T`. Used in
195    /// [`ResourceDatabase`](super::ResourceDatabase) and when creating the db
196    /// file.
197    ///
198    /// Implements equality and comparison operators purely based on the name,
199    /// as assets with a specific name should be unique within a resource
200    /// database.
201    #[derive(Debug)]
202    pub struct NamedAsset<T> {
203        /// The unique name of the asset.
204        pub name: ArrayString<ASSET_NAME_LENGTH>,
205        /// The asset itself.
206        pub asset: T,
207    }
208
209    impl<T> PartialEq for NamedAsset<T> {
210        fn eq(&self, other: &Self) -> bool {
211            self.name == other.name
212        }
213    }
214
215    impl<T> Eq for NamedAsset<T> {} // The equality operator just checks the name, and ArrayString is Eq.
216
217    impl<T> PartialOrd for NamedAsset<T> {
218        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
219            Some(self.cmp(other))
220        }
221    }
222
223    impl<T> Ord for NamedAsset<T> {
224        fn cmp(&self, other: &Self) -> Ordering {
225            self.name.cmp(&other.name)
226        }
227    }
228}