engine/resources/
loader.rs

1// SPDX-FileCopyrightText: 2025 Jens Pitkänen <jens.pitkanen@helsinki.fi>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5use platform::Platform;
6
7use crate::{allocators::LinearAllocator, collections::Queue};
8
9use super::{
10    file_reader::{FileReadError, FileReader},
11    ChunkData, ResourceDatabase, SpriteChunkData,
12};
13
14#[derive(Debug, PartialEq, Eq)]
15enum LoadCategory {
16    Chunk,
17    SpriteChunk,
18}
19
20#[derive(Debug)]
21struct ChunkReadInfo {
22    chunk_index: u32,
23    category: LoadCategory,
24}
25
26/// Asynchronous loader for resource chunks.
27///
28/// Holds some staging memory where the chunk data is written by
29/// platform-defined asynchronous file reading utilities after
30/// [`ResourceLoader::dispatch_reads`]. The chunk data is read later to
31/// initialize chunks in [`ResourceLoader::finish_reads`]. Chunks are loaded in
32/// the order [`ResourceLoader::queue_chunk`] and
33/// [`ResourceLoader::queue_sprite_chunk`] are called.
34///
35/// Many asset usage related functions take this struct as a parameter for
36/// queueing up relevant chunks to be loaded.
37pub struct ResourceLoader {
38    file_reader: FileReader,
39    queued_reads: Queue<'static, ChunkReadInfo>,
40}
41
42impl ResourceLoader {
43    /// Creates a resource loader around the file reader.
44    ///
45    /// The file reader's `staging_buffer_size` should be at least
46    /// [`ResourceDatabase::largest_chunk_source`].
47    #[track_caller]
48    pub fn new(
49        arena: &'static LinearAllocator,
50        file_reader: FileReader,
51        resource_db: &ResourceDatabase,
52    ) -> Option<ResourceLoader> {
53        assert!(
54            file_reader.staging_buffer_size() as u64 >= resource_db.largest_chunk_source(),
55            "resource loader file reader's staging buffer size is smaller than the resource database's largest chunk source",
56        );
57
58        let total_chunks = resource_db.chunks.array_len() + resource_db.sprite_chunks.array_len();
59        Some(ResourceLoader {
60            file_reader,
61            queued_reads: Queue::new(arena, total_chunks)?,
62        })
63    }
64
65    /// Queues the regular chunk at `chunk_index` to be loaded.
66    ///
67    /// Note that this doesn't necessarily actually queue up a read operation,
68    /// the chunk might not be queued for read if e.g. it's already been loaded,
69    /// it's already been queued, or if the queue can't fit the request.
70    pub fn queue_chunk(&mut self, chunk_index: u32, resources: &ResourceDatabase) {
71        self.queue_load(chunk_index, LoadCategory::Chunk, resources);
72    }
73
74    /// Queues the sprite chunk at `chunk_index` to be loaded.
75    ///
76    /// Note that this doesn't necessarily actually queue up a read operation,
77    /// the chunk might not be queued for read if e.g. it's already been loaded,
78    /// it's already been queued, or if the queue can't fit the request.
79    pub fn queue_sprite_chunk(&mut self, chunk_index: u32, resources: &ResourceDatabase) {
80        self.queue_load(chunk_index, LoadCategory::SpriteChunk, resources);
81    }
82
83    fn queue_load(
84        &mut self,
85        chunk_index: u32,
86        category: LoadCategory,
87        resources: &ResourceDatabase,
88    ) {
89        profiling::function_scope!();
90
91        // Don't queue if the chunk has already been loaded.
92        if (category == LoadCategory::Chunk && resources.chunks.get(chunk_index).is_some())
93            || (category == LoadCategory::SpriteChunk
94                && resources.sprite_chunks.get(chunk_index).is_some())
95        {
96            return;
97        }
98
99        // Don't queue if the chunk has already been queued.
100        let already_queued =
101            |read: &ChunkReadInfo| read.chunk_index == chunk_index && read.category == category;
102        if self.queued_reads.iter().any(already_queued) {
103            return;
104        }
105
106        let chunk_source = match category {
107            LoadCategory::Chunk => &resources.chunk_descriptors[chunk_index as usize].source_bytes,
108            LoadCategory::SpriteChunk => {
109                &resources.sprite_chunk_descriptors[chunk_index as usize].source_bytes
110            }
111        };
112        let first_byte = resources.chunk_data_offset + chunk_source.start;
113        let size = (chunk_source.end - chunk_source.start) as usize;
114        // Attempt to queue:
115        if !self.queued_reads.is_full() && self.file_reader.push_read(first_byte, size) {
116            self.queued_reads
117                .push_back(ChunkReadInfo {
118                    chunk_index,
119                    category,
120                })
121                .unwrap();
122        }
123    }
124
125    /// Starts file read operations for the queued up chunk loading requests.
126    pub fn dispatch_reads(&mut self, platform: &dyn Platform) {
127        self.file_reader.dispatch_reads(platform);
128    }
129
130    /// Checks for finished file read requests and writes their results into the
131    /// resource database.
132    ///
133    /// The `max_readers` parameter can be used to limit the time it takes to
134    /// run this function when the queue has a lot of reads to process.
135    pub fn finish_reads(
136        &mut self,
137        resources: &mut ResourceDatabase,
138        platform: &dyn Platform,
139        max_reads: usize,
140    ) {
141        profiling::function_scope!();
142        for _ in 0..max_reads {
143            let read_result = self.file_reader.pop_read(platform, false, |source_bytes| {
144                profiling::scope!("process file read");
145                let ChunkReadInfo {
146                    chunk_index,
147                    category,
148                    ..
149                } = self.queued_reads.pop_front().unwrap();
150
151                match category {
152                    LoadCategory::Chunk => {
153                        let desc = &resources.chunk_descriptors[chunk_index as usize];
154                        let init_fn = || Some(ChunkData::empty());
155                        if let Some(dst) = resources.chunks.insert(chunk_index, init_fn) {
156                            dst.update(desc, source_bytes);
157                        }
158                    }
159
160                    LoadCategory::SpriteChunk => {
161                        let desc = &resources.sprite_chunk_descriptors[chunk_index as usize];
162                        let init_fn = || SpriteChunkData::empty(platform);
163                        if let Some(dst) = resources.sprite_chunks.insert(chunk_index, init_fn) {
164                            dst.update(desc, source_bytes, platform);
165                        }
166                    }
167                }
168            });
169
170            match read_result {
171                Ok(_) => {}
172                Err(FileReadError::NoReadsQueued | FileReadError::WouldBlock) => break,
173                Err(err) => {
174                    let info = self.queued_reads.pop_front().unwrap();
175                    platform.println(format_args!(
176                        "resource loader read ({info:?}) failed: {err:?}"
177                    ));
178                }
179            }
180        }
181    }
182}