engine/resources/
file_reader.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::{FileHandle, FileReadTask, Platform};
6
7use crate::{
8    allocators::LinearAllocator,
9    collections::{Queue, RingAllocationMetadata, RingBuffer, RingSlice},
10};
11
12/// The possible errors from [`FileReader::pop_read`].
13#[derive(Debug)]
14pub enum FileReadError {
15    /// There was no file reading operation queued.
16    NoReadsQueued,
17    /// The read was requested to not block, and had not finished.
18    WouldBlock,
19    /// The underlying file reading operation failed.
20    Platform,
21}
22
23struct LoadRequest {
24    first_byte: u64,
25    size: usize,
26}
27
28struct LoadTask {
29    file_read_task: FileReadTask,
30    read_buffer_metadata: RingAllocationMetadata,
31}
32
33/// File reading utility.
34///
35/// Can be used for both asynchronous and synchronous reads, as long as they're
36/// read in the same order they were queued up.
37pub struct FileReader {
38    staging_buffer: RingBuffer<'static, u8>,
39    to_load_queue: Queue<'static, LoadRequest>,
40    in_flight_queue: Queue<'static, LoadTask>,
41    file: FileHandle,
42}
43
44impl FileReader {
45    /// Creates a new [`FileReader`] for reading `file`, with a maximum of
46    /// `queue_capacity` concurrent read operations.
47    pub fn new(
48        arena: &'static LinearAllocator,
49        file: FileHandle,
50        staging_buffer_size: usize,
51        queue_capacity: usize,
52    ) -> Option<FileReader> {
53        Some(FileReader {
54            staging_buffer: RingBuffer::new(arena, staging_buffer_size)?,
55            to_load_queue: Queue::new(arena, queue_capacity)?,
56            in_flight_queue: Queue::new(arena, queue_capacity)?,
57            file,
58        })
59    }
60
61    /// Returns the size of the staging buffer where file read operations write
62    /// to, i.e. the size of the largest read supported by this file reader.
63    pub fn staging_buffer_size(&self) -> usize {
64        self.staging_buffer.capacity()
65    }
66
67    /// Queues up a read operation starting at `first_byte`, reading `size`
68    /// bytes. Returns `true` if the request fit in the queue.
69    ///
70    /// If `size` is larger than [`FileReader::staging_buffer_size`], this will
71    /// always return `false`.
72    #[must_use]
73    pub fn push_read(&mut self, first_byte: u64, size: usize) -> bool {
74        if size > self.staging_buffer_size() {
75            return false;
76        }
77
78        self.to_load_queue
79            .push_back(LoadRequest { first_byte, size })
80            .is_ok()
81    }
82
83    /// Starts file read operations for the queued up loading requests.
84    pub fn dispatch_reads(&mut self, platform: &dyn Platform) {
85        profiling::function_scope!();
86        while let Some(LoadRequest { size, .. }) = self.to_load_queue.peek_front() {
87            profiling::scope!("dispatch");
88            let Some(staging_slice) = self.staging_buffer.allocate(*size) else {
89                break;
90            };
91            let (buffer, read_buffer_metadata) = staging_slice.into_parts();
92
93            let LoadRequest {
94                first_byte,
95                size: _,
96            } = self.to_load_queue.pop_front().unwrap();
97
98            let file_read_task = platform.begin_file_read(self.file, first_byte, buffer);
99
100            self.in_flight_queue
101                .push_back(LoadTask {
102                    file_read_task,
103                    read_buffer_metadata,
104                })
105                .ok()
106                .unwrap();
107        }
108    }
109
110    /// Finishes the read operation at the front of the queue, and passes the
111    /// results to the closure if the read was successful.
112    ///
113    /// If `blocking` is `true` and [`FileReader::dispatch_reads`] has not been
114    /// called, this will call it for convenience.
115    pub fn pop_read<T, F>(
116        &mut self,
117        platform: &dyn Platform,
118        blocking: bool,
119        use_result: F,
120    ) -> Result<T, FileReadError>
121    where
122        F: FnOnce(&mut [u8]) -> T,
123    {
124        profiling::function_scope!();
125        if blocking && self.in_flight_queue.is_empty() {
126            self.dispatch_reads(platform);
127        }
128
129        let Some(LoadTask { file_read_task, .. }) = self.in_flight_queue.peek_front() else {
130            return Err(FileReadError::NoReadsQueued);
131        };
132
133        if !blocking && !platform.is_file_read_finished(file_read_task) {
134            return Err(FileReadError::WouldBlock);
135        }
136
137        let LoadTask {
138            file_read_task,
139            read_buffer_metadata,
140        } = self.in_flight_queue.pop_front().unwrap();
141
142        let (mut buffer, read_success) = match platform.finish_file_read(file_read_task) {
143            Ok(buffer) => (buffer, true),
144            Err(buffer) => (buffer, false),
145        };
146
147        let result = if read_success {
148            Ok(use_result(&mut buffer))
149        } else {
150            Err(FileReadError::Platform)
151        };
152
153        // Safety: each LoadTask gets its parts from one RingSlice, and
154        // these are from this specific LoadTask, so these are a pair.
155        let slice = unsafe { RingSlice::from_parts(buffer, read_buffer_metadata) };
156        self.staging_buffer.free(slice).unwrap();
157
158        result
159    }
160}