Skip to content

Commit

Permalink
Merge pull request #56 from waych/for_upstream
Browse files Browse the repository at this point in the history
russimp: Introduce FileSystem loading
  • Loading branch information
Jhonny Knaak de Vargas committed Jan 16, 2024
2 parents 606292b + 35f9ff5 commit 9eb5883
Show file tree
Hide file tree
Showing 3 changed files with 359 additions and 2 deletions.
336 changes: 336 additions & 0 deletions src/fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
//! The `fs` module contains functionality for interfacing custom resource loading.
//!
//! Implement the FileSystem trait for your custom resource loading, with its open() method returning
//! objects satisfying the FileOperations trait.
use russimp_sys::{aiFile, aiFileIO, aiOrigin, aiReturn};
use std::convert::TryInto;
use std::ffi::CStr;
use std::io::SeekFrom;

/// Implement FileSystem to use custom resource loading using `Scene::from_filesystem()`.
///
/// Rusty version of the underlying aiFileIO type.
pub trait FileSystem {
fn open(&self, file_path: &str, mode: &str) -> Option<Box<dyn FileOperations>>;
}

/// Implement this for a given resource to support custom resource loading.
///
/// This trait class is the rusty version of the underlying aiFile type.
pub trait FileOperations {
/// Should return the number of bytes read, or Err if read unsuccessful.
fn read(&mut self, buf: &mut [u8]) -> Result<usize, ()>;
/// Should return the number of bytes written, or Err if write unsuccessful.
fn write(&mut self, buf: &[u8]) -> Result<usize, ()>;
fn tell(&mut self) -> usize;
fn size(&mut self) -> usize;
fn seek(&mut self, seek_from: SeekFrom) -> Result<(), ()>;
fn flush(&mut self);
fn close(&mut self);
}

/// This type allows us to generate C stubs for whatever trait object the user supplies.
pub(crate) struct FileOperationsWrapper<T: FileSystem> {
ai_file: aiFileIO,
_phantom: std::marker::PhantomData<T>,
}

impl<T: FileSystem> FileOperationsWrapper<T> {
/// Returns a wrapper that can create an aiFileIO to be used with the assimp C-API.
pub fn new(file_system: &T) -> FileOperationsWrapper<T> {
let trait_obj: &dyn FileSystem = file_system;
let managed_box = Box::new(trait_obj);
let user_data = Box::into_raw(managed_box);
let user_data = user_data as *mut i8;
FileOperationsWrapper {
ai_file: aiFileIO {
OpenProc: Some(FileOperationsWrapper::<T>::io_open),
CloseProc: Some(FileOperationsWrapper::<T>::io_close),
UserData: user_data,
},
_phantom: Default::default(),
}
}
/// Get the aiFileIO to pass to the C-interface.
pub fn ai_file(&mut self) -> &mut aiFileIO {
&mut self.ai_file
}
/// Implementation for aiFileIO::OpenProc.
unsafe extern "C" fn io_open(
ai_file_io: *mut aiFileIO,
file_path: *const ::std::os::raw::c_char,
mode: *const ::std::os::raw::c_char,
) -> *mut aiFile {
let file_system = Box::leak(Box::from_raw(
(*ai_file_io).UserData as *mut &dyn FileSystem,
));

let file_path = CStr::from_ptr(file_path)
.to_str()
.unwrap_or("Invalid UTF-8 Filename");
let mode = CStr::from_ptr(mode)
.to_str()
.unwrap_or("Invalid UTF-8 Mode");
let file = match file_system.open(file_path, mode) {
None => return std::ptr::null_mut(),
Some(file) => file,
};

// Take the returned file, and double box it here so that it can be converted to a single
// raw pointer that can be stuffed in the UserData.
let double_box = Box::new(file);
let managed_box = Box::into_raw(double_box); // Cleaned up in io_close.
let user_data = managed_box as *mut i8;
let ai_file = aiFile {
ReadProc: Some(Self::io_read),
WriteProc: Some(Self::io_write),
TellProc: Some(Self::io_tell),
FileSizeProc: Some(Self::io_size),
SeekProc: Some(Self::io_seek),
FlushProc: Some(Self::io_flush),
UserData: user_data,
};
// Lifetime of ai_file is managed by backend assimp library, cleaned up in io_close().
Box::into_raw(Box::new(ai_file))
}

/// Implementation for aiFileIO::CloseProc.
unsafe extern "C" fn io_close(_ai_file_io: *mut aiFileIO, ai_file: *mut aiFile) {
// Given that this is close, we are careful to not leak, but instead drop the file when we
// exit this scope.
let ai_file = Box::from_raw(ai_file);
let mut file: Box<Box<dyn FileOperations>> =
Box::from_raw((*ai_file).UserData as *mut Box<dyn FileOperations>);
file.close();
}
/// Turn an aiFile pointer into a the "self" object.
///
/// Safety: Only safe to call once from within each of the io_* callbacks. This assumes that
/// the loading library has ownership of the aiFile object that was returned by the FileSystem.
/// It is expected to only be called serially on a single thread for the lifetype 'a, which
/// *should* keep access scoped to within the io_* callback.
unsafe fn get_file<'a>(ai_file: *mut aiFile) -> &'a mut Box<dyn FileOperations> {
// We return a "leaked" pointer here, using the saved off double box pointer that we
// stuffed in the UserData. We "leak" as we don't acutally want to return ownership, only
// the mutable reference. The box is manually cleaned up as part of io_close.
Box::leak(Box::from_raw(
(*ai_file).UserData as *mut Box<dyn FileOperations>,
))
}
// Implementation for aiFile::ReadProc.
unsafe extern "C" fn io_read(
ai_file: *mut aiFile,
buffer: *mut std::os::raw::c_char,
size: usize,
count: usize,
) -> usize {
let file = Self::get_file(ai_file);
let mut buffer =
std::slice::from_raw_parts_mut(buffer as *mut u8, (size * count).try_into().unwrap());
if size == 0 {
panic!("Size 0 is invalid");
}
if count == 0 {
panic!("Count 0 is invalid");
}
if size > std::usize::MAX {
panic!("huge read size not supported");
}
let size = size as usize;
if size == 1 {
// This looks like a memcpy.
if count > std::usize::MAX {
panic!("huge read not supported");
}
let count = count as usize;

let (buffer, _) = buffer.split_at_mut(count);
match file.read(buffer) {
Ok(size) => size,
Err(_) => std::usize::MAX,
}
} else {
// We have to copy in strides. Implement this by looping for each object and tally the
// count of full objects read.
let mut total: usize = 0;
for _ in 0..count {
let split = buffer.split_at_mut(size as usize);
buffer = split.1;
let bytes_read = match file.read(split.0) {
Err(_) => break,
Ok(bytes_read) => bytes_read,
};
if bytes_read != size {
break;
}
total = total + 1;
}
total
}
}
// Implementation for aiFile::WriteProc.
unsafe extern "C" fn io_write(
ai_file: *mut aiFile,
buffer: *const std::os::raw::c_char,
size: usize,
count: usize,
) -> usize {
let file = Self::get_file(ai_file);
let mut buffer =
std::slice::from_raw_parts(buffer as *mut u8, (size * count).try_into().unwrap());
if size == 0 {
panic!("Write of size 0");
}
if count == 0 {
panic!("Write of count 0");
}
if size > std::usize::MAX {
panic!("huge write size not supported");
}
let size = size as usize;
if size == 1 {
if count > std::usize::MAX {
panic!("huge write not supported");
}
let count = count as usize;

let (buffer, _) = buffer.split_at(count);
match file.write(buffer) {
Ok(size) => size,
Err(_) => std::usize::MAX,
}
} else {
// Write in strides. Implement this by looping for each object and tally the
// count of full objects written.
let mut total: usize = 0;
for _ in 0..count {
let split = buffer.split_at(size as usize);
buffer = split.1;
let bytes_written = match file.write(split.0) {
Err(_) => break,
Ok(bytes_written) => bytes_written,
};
if bytes_written != size {
break;
}
total = total + 1;
}
total
}
}
// Implementation for aiFile::TellProc.
unsafe extern "C" fn io_tell(ai_file: *mut aiFile) -> usize {
let file = Self::get_file(ai_file);
file.tell()
}
// Implementation for aiFile::FileSizeProc.
unsafe extern "C" fn io_size(ai_file: *mut aiFile) -> usize {
let file = Self::get_file(ai_file);
file.size()
}
// Implementation for aiFile::SeekProc.
unsafe extern "C" fn io_seek(ai_file: *mut aiFile, pos: usize, origin: aiOrigin) -> aiReturn {
let file = Self::get_file(ai_file);
let seek_from = match origin {
russimp_sys::aiOrigin_aiOrigin_SET => SeekFrom::Start(pos as u64),
russimp_sys::aiOrigin_aiOrigin_CUR => SeekFrom::Current(pos as i64),
russimp_sys::aiOrigin_aiOrigin_END => SeekFrom::End(pos as i64),
_ => panic!("Assimp passed invalid origin"),
};
match file.seek(seek_from) {
Ok(()) => 0,
Err(()) => russimp_sys::aiReturn_aiReturn_FAILURE,
}
}
// Implementation for aiFile::FlushProc.
unsafe extern "C" fn io_flush(ai_file: *mut aiFile) {
let file = Self::get_file(ai_file);
file.flush();
}
}

impl<T: FileSystem> Drop for FileOperationsWrapper<T> {
fn drop(&mut self) {
// Re-construct and drop the box that was used for the C-API.
let _managed_box: Box<&dyn FileSystem> =
unsafe { Box::from_raw(self.ai_file.UserData as *mut &dyn FileSystem) };
}
}

#[cfg(test)]
mod test {
use crate::scene::PostProcess;
use crate::scene::Scene;
use crate::utils;
use std::fs::File;
use std::io::{SeekFrom, prelude::*};

struct MyFileOperations {
file: File,
}

impl super::FileOperations for MyFileOperations {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, ()> {
self.file.read(buf).or_else(|_| Err(()))
}

fn write(&mut self, _buf: &[u8]) -> Result<usize, ()> {
unimplemented!("write support");
}

fn tell(&mut self) -> usize {
self.file.seek(SeekFrom::Current(0)).unwrap_or(0)
}

fn size(&mut self) -> usize {
self.file.metadata().expect("Missing metadata").len()
}

fn seek(&mut self, seek_from: SeekFrom) -> Result<(), ()> {
match self.file.seek(seek_from) {
Ok(_) => Ok(()),
Err(_) => Err(()),
}
}

fn flush(&mut self) {
// write suppot not implemented.
}

fn close(&mut self) {
// Nothing to do.
}
}

struct MyFS {}

impl super::FileSystem for MyFS {
fn open(&self, file_path: &str, mode: &str) -> Option<Box<dyn super::FileOperations>> {
// We only support reading for this test.
assert_eq!(mode, "rb");
let file = File::open(file_path).expect("Couldn't open {file_path}");
Some(Box::new(MyFileOperations{file}))
}
}

#[test]
fn test_file_operations() {
// Load the cube.obj as it also has to load the cube.mtl through the filesystem to get the
// materials right.
let model_path = utils::get_model("models/OBJ/cube.obj");
let mut myfs = MyFS {};
let scene = Scene::from_file_system(
model_path.as_str(),
vec![
PostProcess::CalculateTangentSpace,
PostProcess::Triangulate,
PostProcess::JoinIdenticalVertices,
PostProcess::SortByPrimitiveType,
],
&mut myfs,
).unwrap();

assert_eq!(scene.meshes[0].texture_coords.len(), 8);
assert_eq!(scene.materials.len(), 2);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub mod animation;
pub mod bone;
pub mod camera;
pub mod face;
pub mod fs;
pub mod light;
pub mod material;
pub mod mesh;
Expand Down
24 changes: 22 additions & 2 deletions src/scene.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::material::generate_materials;
use crate::{
animation::Animation, camera::Camera, light::Light, material::Material, mesh::Mesh,
metadata::MetaData, node::Node, sys::*, *,
animation::Animation, camera::Camera, fs::{FileSystem, FileOperationsWrapper}, light::Light, material::Material,
mesh::Mesh, metadata::MetaData, node::Node, sys::*, *,
};
use std::{
ffi::{CStr, CString},
Expand Down Expand Up @@ -430,6 +430,20 @@ impl Scene {
result
}

pub fn from_file_system<T: FileSystem>(file_path: &str, flags: PostProcessSteps, file_io: &mut T) -> Russult<Scene> {
let bitwise_flag = flags.into_iter().fold(0, |acc, x| acc | (x as u32));
let file_path = CString::new(file_path).unwrap();

let raw_scene = Scene::get_scene_from_filesystem(file_path, bitwise_flag, file_io);
if raw_scene.is_none() {
return Err(Scene::get_error());
}

let result = Scene::new(raw_scene.unwrap());
Scene::drop_scene(raw_scene);

result
}
pub fn from_buffer(buffer: &[u8], flags: PostProcessSteps, hint: &str) -> Russult<Scene> {
let bitwise_flag = flags.into_iter().fold(0, |acc, x| acc | (x as u32));
let hint = CString::new(hint).unwrap();
Expand Down Expand Up @@ -459,6 +473,12 @@ impl Scene {
unsafe { aiImportFile(string.as_ptr(), flags).as_ref() }
}

#[inline]
fn get_scene_from_filesystem<'a, T: FileSystem>(string: CString, flags: u32, fs: &mut T) -> Option<&'a aiScene> {
let mut file_io = FileOperationsWrapper::new(fs);
unsafe { aiImportFileEx(string.as_ptr(), flags, file_io.ai_file()).as_ref() }
}

#[inline]
fn get_scene_from_file_from_memory<'a>(
buffer: &[u8],
Expand Down

0 comments on commit 9eb5883

Please sign in to comment.