fmod/core/filesystem.rs
1// Copyright (c) 2024 Melody Madeline Lyons
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
7use fmod_sys::*;
8
9use crate::{FmodResultExt, Result};
10use lanyard::Utf8CStr;
11use std::ffi::{c_char, c_int, c_uint, c_void};
12
13#[cfg(doc)]
14use crate::{Error, System};
15
16// I was lost on how to do this for a while, so I took some pointers from https://github.com/CAD97/fmod-rs/blob/main/crates/fmod-rs/src/core/common/file.rs#L181
17// It's not copied verbatim, I made some different design choices (like opting to make handle be a *mut c_void instead)
18// for similar reasons to this crate not handling userdata.
19// This is such a power user feature that I'm not sure it's worth hiding away most of the implementation details
20
21// TODO test and validate my assumptions are correct
22
23/// The base trait for all filesystems.
24pub trait FileSystem {
25 /// Callback for opening a file.
26 ///
27 /// Return the appropriate error code such as [`Error::FileNotFound`] if the file fails to open.
28 /// If the callback is from [`System::attach_filesystem`], then the return value is ignored.
29 fn open(name: &Utf8CStr, userdata: *mut c_void) -> Result<(*mut c_void, c_uint)>;
30
31 /// Callback for closing a file.
32 ///
33 /// Close any user created file handle and perform any cleanup necessary for the file here.
34 /// If the callback is from [`System::attach_filesystem`], then the return value is ignored.
35 fn close(handle: *mut c_void, userdata: *mut c_void) -> Result<()>;
36}
37
38/// A mutable file buffer.
39///
40/// It's a lot like [`std::io::Cursor`].
41#[derive(Debug)]
42pub struct FileBuffer<'a> {
43 buffer: &'a mut [u8],
44 written: &'a mut u32,
45}
46
47impl FileBuffer<'_> {
48 /// The capacity of this file buffer.
49 pub fn capacity(&self) -> usize {
50 self.buffer.len()
51 }
52
53 /// The total amount of bytes written.
54 pub fn written(&self) -> u32 {
55 *self.written
56 }
57
58 /// Returns true if [`Self::written`] == [`Self::capacity`].
59 pub fn is_full(&self) -> bool {
60 self.written() == self.capacity() as u32
61 }
62}
63
64impl std::io::Write for FileBuffer<'_> {
65 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
66 let unwritten_region = &mut self.buffer[*self.written as usize..];
67 let len = buf.len().min(unwritten_region.len());
68 self.buffer[..len].copy_from_slice(&buf[..len]);
69 *self.written += len as u32;
70 Ok(len)
71 }
72
73 // no-op
74 fn flush(&mut self) -> std::io::Result<()> {
75 Ok(())
76 }
77}
78
79/// A synchronous filesystem.
80///
81/// You should prefer implementing this trait over [`FileSystemAsync`]- it's a lot easier to do.
82pub trait FileSystemSync: FileSystem {
83 /// Callback for reading from a file.
84 ///
85 /// If the callback is from [`System::attach_filesystem`], then the return value is ignored.
86 ///
87 /// If there is not enough data to read the requested number of bytes,
88 /// return fewer bytes in the bytesread parameter and and return [`Error::FileEof`].
89 fn read(handle: *mut c_void, userdata: *mut c_void, buffer: FileBuffer<'_>) -> Result<()>;
90
91 /// Callback for seeking within a file.
92 ///
93 /// If the callback is from [`System::attach_filesystem`], then the return value is ignored.
94 fn seek(handle: *mut c_void, userdata: *mut c_void, position: c_uint) -> Result<()>;
95}
96
97/// Information about a single asynchronous file operation.
98#[derive(Debug)]
99pub struct AsyncReadInfo {
100 raw: *mut FMOD_ASYNCREADINFO,
101}
102
103impl AsyncReadInfo {
104 /// File handle that was returned in [`FileSystem::open`].
105 pub fn handle(&self) -> *mut c_void {
106 unsafe { *self.raw }.handle
107 }
108
109 /// Offset within the file where the read operation should occur.
110 pub fn offset(&self) -> c_uint {
111 unsafe { *self.raw }.offset
112 }
113
114 /// Number of bytes to read.
115 pub fn size(&self) -> c_uint {
116 unsafe { *self.raw }.sizebytes
117 }
118
119 /// Priority hint for how quickly this operation should be serviced where 0 represents low importance and 100 represents extreme importance.
120 /// This could be used to prioritize the read order of a file job queue for example.
121 /// FMOD decides the importance of the read based on if it could degrade audio or not.
122 pub fn priority(&self) -> c_int {
123 unsafe { *self.raw }.priority
124 }
125
126 /// User value associated with this async operation, passed to [`FileSystemAsync::cancel`].
127 pub fn userdata(&self) -> *mut c_void {
128 unsafe { *self.raw }.userdata
129 }
130
131 /// Set the user value associated with this async operation.
132 ///
133 /// # Safety
134 ///
135 /// You cannot call this while a [`AsyncCancelInfo`] with the same raw pointer is live.
136 // FIXME: make this safe somehow?
137 pub unsafe fn set_userdata(&mut self, userdata: *mut c_void) {
138 unsafe { *self.raw }.userdata = userdata;
139 }
140
141 /// Get the raw pointer associated with this [`AsyncReadInfo`].
142 pub fn raw(&self) -> *mut FMOD_ASYNCREADINFO {
143 self.raw
144 }
145
146 /// Number of bytes currently read.
147 pub fn written(&self) -> c_uint {
148 unsafe { *self.raw }.bytesread
149 }
150
151 /// Get the [`FileBuffer`] associated with this [`AsyncReadInfo`].
152 // Normally this would be really unsafe because FMOD hands out the same *mut FMOD_ASYNCREADINFO to `read()` and `cancel()`.
153 // AsyncCancelInfo doesn't support accessing the buffer, so this should be safe.
154 pub fn buffer(&mut self) -> FileBuffer<'_> {
155 let ptr = unsafe { *self.raw }.buffer;
156 let len = self.size();
157
158 let buffer = unsafe { std::slice::from_raw_parts_mut(ptr.cast(), len as usize) };
159 let written = &mut unsafe { &mut *self.raw }.bytesread;
160 FileBuffer { buffer, written }
161 }
162
163 /// Signal the async read is done.
164 ///
165 /// If [`AsyncReadInfo::written`] != [`AsyncReadInfo::size`] this function will send an [`Error::FileEof`] for you.
166 ///
167 /// # Safety
168 ///
169 /// If you have a [`AsyncCancelInfo`] with the same raw pointer, it is immediately invalid after calling this function.
170 // I *really* don't like taking a result like this, but I can't think of another way...
171 pub unsafe fn finish(self, result: Result<()>) {
172 let mut fmod_result = FMOD_RESULT::from_result(result);
173 if fmod_result == FMOD_RESULT::FMOD_OK && self.written() < self.size() {
174 fmod_result = FMOD_RESULT::FMOD_ERR_FILE_EOF;
175 }
176 // Should never be null
177 unsafe { (*self.raw).done.unwrap_unchecked()(self.raw, fmod_result) }
178 }
179}
180
181/// Information about cancelling a asynchronous file operation.
182#[derive(Debug)]
183pub struct AsyncCancelInfo {
184 raw: *mut FMOD_ASYNCREADINFO,
185}
186
187impl AsyncCancelInfo {
188 /// File handle that was returned in [`FileSystem::open`].
189 pub fn handle(&self) -> *mut c_void {
190 unsafe { *self.raw }.handle
191 }
192
193 /// Offset within the file where the read operation should occur.
194 pub fn offset(&self) -> c_uint {
195 unsafe { *self.raw }.offset
196 }
197
198 /// Number of bytes to read.
199 pub fn size(&self) -> c_uint {
200 unsafe { *self.raw }.sizebytes
201 }
202
203 /// Priority hint for how quickly this operation should be serviced where 0 represents low importance and 100 represents extreme importance.
204 /// This could be used to prioritize the read order of a file job queue for example.
205 /// FMOD decides the importance of the read based on if it could degrade audio or not.
206 pub fn priority(&self) -> c_int {
207 unsafe { *self.raw }.priority
208 }
209
210 /// User value associated with this async operation, passed to [`FileSystemAsync::cancel`].
211 pub fn userdata(&self) -> *mut c_void {
212 unsafe { *self.raw }.userdata
213 }
214
215 /// Get the raw pointer associated with this [`AsyncCancelInfo`].
216 pub fn raw(&self) -> *mut FMOD_ASYNCREADINFO {
217 self.raw
218 }
219}
220
221/// An async filesystem.
222///
223/// # Safety
224///
225/// This trait is marked as unsafe because a correct implementation of [`FileSystemAsync`] is hard to get right.
226/// I'd suggest reading up on the FMOD documentation to get a better idea of how to write this.
227pub unsafe trait FileSystemAsync: FileSystem {
228 /// Callback for reading from a file asynchronously.
229 ///
230 /// This callback allows you to accept a file I/O request without servicing it immediately.
231 ///
232 /// The callback can queue or store the [`AsyncReadInfo`],
233 /// so that a 'servicing routine' can read the data and mark the job as done.
234 ///
235 /// Marking an asynchronous job as 'done' outside of this callback can be done by calling [`AsyncReadInfo::finish`] with the file read result as a parameter.
236 ///
237 /// If the servicing routine is processed in the same thread as the thread that invokes this callback
238 /// (for example the thread that calls [`System::create_sound`] or [`System::create_stream`]),
239 /// a deadlock will occur because while [`System::create_sound`] or [`System::create_stream`] waits for the file data,
240 /// the servicing routine in the main thread won't be able to execute.
241 ///
242 /// This typically means an outside servicing routine should typically be run in a separate thread.
243 ///
244 /// The read request can be queued or stored and this callback can return immediately with [`Ok`].
245 /// Returning an error at this point will cause FMOD to stop what it was doing and return back to the caller.
246 /// If it is from FMOD's stream thread, the stream will typically stop.
247 fn read(info: AsyncReadInfo, userdata: *mut c_void) -> Result<()>;
248
249 /// Callback for cancelling a pending asynchronous read.
250 ///
251 /// This callback is called to stop/release or shut down the resource that is holding the file,
252 /// for example: releasing a Sound stream.
253 ///
254 /// Before returning from this callback the implementation must ensure that all references to info are relinquished.
255 fn cancel(info: AsyncCancelInfo, userdata: *mut c_void) -> Result<()>;
256}
257
258pub(crate) unsafe extern "C" fn filesystem_open<F: FileSystem>(
259 name: *const c_char,
260 raw_filesize: *mut c_uint,
261 raw_handle: *mut *mut c_void,
262 userdata: *mut c_void,
263) -> FMOD_RESULT {
264 let name = unsafe { Utf8CStr::from_ptr_unchecked(name) };
265 let (handle, file_size) = match F::open(name, userdata) {
266 Ok(h) => h,
267 Err(e) => return e.into(),
268 };
269 unsafe {
270 *raw_filesize = file_size;
271 *raw_handle = handle;
272 }
273 FMOD_RESULT::FMOD_OK
274}
275
276pub(crate) unsafe extern "C" fn filesystem_close<F: FileSystem>(
277 handle: *mut c_void,
278 userdata: *mut c_void,
279) -> FMOD_RESULT {
280 let result = F::close(handle, userdata);
281 FMOD_RESULT::from_result(result)
282}
283
284pub(crate) unsafe extern "C" fn filesystem_read<F: FileSystemSync>(
285 handle: *mut c_void,
286 buffer: *mut c_void,
287 size_bytes: c_uint,
288 bytes_read: *mut c_uint,
289 userdata: *mut c_void,
290) -> FMOD_RESULT {
291 let buffer = unsafe {
292 FileBuffer {
293 buffer: std::slice::from_raw_parts_mut(buffer.cast(), size_bytes as usize),
294 written: &mut *bytes_read,
295 }
296 };
297 if let Err(e) = F::read(handle, userdata, buffer) {
298 return e.into();
299 }
300
301 if unsafe { *bytes_read } < size_bytes {
302 FMOD_RESULT::FMOD_ERR_FILE_EOF
303 } else {
304 FMOD_RESULT::FMOD_OK
305 }
306}
307
308pub(crate) unsafe extern "C" fn filesystem_seek<F: FileSystemSync>(
309 handle: *mut c_void,
310 pos: c_uint,
311 userdata: *mut c_void,
312) -> FMOD_RESULT {
313 let result = F::seek(handle, userdata, pos);
314 FMOD_RESULT::from_result(result)
315}
316
317pub(crate) unsafe extern "C" fn async_filesystem_read<F: FileSystemAsync>(
318 raw: *mut FMOD_ASYNCREADINFO,
319 userdata: *mut c_void,
320) -> FMOD_RESULT {
321 let result = F::read(AsyncReadInfo { raw }, userdata);
322 FMOD_RESULT::from_result(result)
323}
324
325pub(crate) unsafe extern "C" fn async_filesystem_cancel<F: FileSystemAsync>(
326 raw: *mut FMOD_ASYNCREADINFO,
327 userdata: *mut c_void,
328) -> FMOD_RESULT {
329 let result = F::cancel(AsyncCancelInfo { raw }, userdata);
330 FMOD_RESULT::from_result(result)
331}