AssetServer: multithreaded sync/async asset loading

This commit is contained in:
Carter Anderson 2020-05-15 16:55:44 -07:00
parent 8a61ef48d3
commit 4e1abea161
18 changed files with 903 additions and 234 deletions

94
.vscode/launch.json vendored
View file

@ -857,6 +857,25 @@
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'bevy_text'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=bevy_text"
],
"filter": {
"name": "bevy_text",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
@ -1377,6 +1396,44 @@
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug example 'load_asset_folder'",
"env": { "CARGO_MANIFEST_DIR": "${workspaceFolder}" },
"cargo": {
"args": [
"build",
"--example=load_asset_folder",
"--package=bevy"
],
"filter": {
"name": "load_asset_folder",
"kind": "example"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in example 'load_asset_folder'",
"cargo": {
"args": [
"test",
"--no-run",
"--example=load_asset_folder",
"--package=bevy"
],
"filter": {
"name": "load_asset_folder",
"kind": "example"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
@ -1747,6 +1804,43 @@
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug example 'text'",
"cargo": {
"args": [
"build",
"--example=text",
"--package=bevy"
],
"filter": {
"name": "text",
"kind": "example"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in example 'text'",
"cargo": {
"args": [
"test",
"--no-run",
"--example=text",
"--package=bevy"
],
"filter": {
"name": "text",
"kind": "example"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",

View file

@ -9,4 +9,7 @@ bevy_app = { path = "../bevy_app" }
bevy_core = { path = "../bevy_core" }
legion = { path = "../bevy_legion" }
uuid = { version = "0.8", features = ["v4", "serde"] }
uuid = { version = "0.8", features = ["v4", "serde"] }
crossbeam-channel = "0.4.2"
anyhow = "1.0"
thiserror = "1.0"

View file

@ -0,0 +1,55 @@
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
#[derive(Clone, Debug)]
pub struct AssetPath {
pub path: Cow<'static, str>,
pub extension: Option<Cow<'static, str>>,
}
impl From<&Path> for AssetPath {
fn from(path: &Path) -> Self {
AssetPath {
path: Cow::Owned(
path.to_str()
.expect("Path should be a valid string.")
.to_string(),
),
extension: path.extension().map(|e| {
Cow::Owned(
e.to_str()
.expect("Extension should be a valid string.")
.to_string(),
)
}),
}
}
}
impl From<&PathBuf> for AssetPath {
fn from(path: &PathBuf) -> Self {
AssetPath {
path: Cow::Owned(
path.to_str()
.expect("Path should be a valid string.")
.to_string(),
),
extension: path.extension().map(|e| {
Cow::Owned(
e.to_str()
.expect("Extension should be a valid string.")
.to_string(),
)
}),
}
}
}
impl From<&str> for AssetPath {
fn from(path: &str) -> Self {
let path = Path::new(path);
AssetPath::from(path)
}
}

View file

@ -0,0 +1,245 @@
use crate::{Assets, Handle, HandleId, LoadRequest, AssetLoadError, AssetLoadRequestHandler, AssetLoader, AssetPath};
use anyhow::Result;
use legion::prelude::Resources;
use std::{
collections::HashMap,
env, fs, io,
path::Path,
sync::{Arc, RwLock},
thread,
};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AssetServerError {
#[error("Asset folder path is not a directory.")]
AssetFolderNotADirectory(String),
#[error("Invalid root path")]
InvalidRootPath,
#[error("No AssetHandler found for the given extension.")]
MissingAssetHandler,
#[error("No AssetLoader found for the given extension.")]
MissingAssetLoader,
#[error("Encountered an error while loading an asset.")]
AssetLoadError(#[from] AssetLoadError),
#[error("Encountered an io error.")]
Io(#[from] io::Error),
}
struct LoaderThread {
// NOTE: these must remain private. the LoaderThread Arc counters are used to determine thread liveness
// if there is one reference, the loader thread is dead. if there are two references, the loader thread is active
requests: Arc<RwLock<Vec<LoadRequest>>>,
}
pub struct AssetServer {
asset_folders: Vec<String>,
loader_threads: RwLock<Vec<LoaderThread>>,
max_loader_threads: usize,
asset_handlers: Arc<RwLock<Vec<Box<dyn AssetLoadRequestHandler>>>>,
// TODO: this is a hack to enable retrieving generic AssetLoader<T>s. there must be a better way!
loaders: Vec<Resources>,
extension_to_handler_index: HashMap<String, usize>,
extension_to_loader_index: HashMap<String, usize>,
}
impl Default for AssetServer {
fn default() -> Self {
AssetServer {
max_loader_threads: 4,
asset_folders: Vec::new(),
loader_threads: RwLock::new(Vec::new()),
asset_handlers: Arc::new(RwLock::new(Vec::new())),
loaders: Vec::new(),
extension_to_handler_index: HashMap::new(),
extension_to_loader_index: HashMap::new(),
}
}
}
impl AssetServer {
pub fn add_handler<T>(&mut self, asset_handler: T)
where
T: AssetLoadRequestHandler,
{
let mut asset_handlers = self.asset_handlers.write().expect("RwLock poisoned");
let handler_index = asset_handlers.len();
for extension in asset_handler.extensions().iter() {
self.extension_to_handler_index
.insert(extension.to_string(), handler_index);
}
asset_handlers.push(Box::new(asset_handler));
}
pub fn add_loader<TLoader, TAsset>(&mut self, loader: TLoader)
where
TLoader: AssetLoader<TAsset>,
TAsset: 'static,
{
let loader_index = self.loaders.len();
for extension in loader.extensions().iter() {
self.extension_to_loader_index
.insert(extension.to_string(), loader_index);
}
let mut resources = Resources::default();
resources.insert::<Box<dyn AssetLoader<TAsset>>>(Box::new(loader));
self.loaders.push(resources);
}
pub fn add_asset_folder(&mut self, path: &str) {
self.asset_folders.push(path.to_string());
}
pub fn get_root_path(&self) -> Result<String, AssetServerError> {
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
Ok(manifest_dir)
} else {
match env::current_exe() {
Ok(exe_path) => exe_path
.parent()
.ok_or(AssetServerError::InvalidRootPath)
.and_then(|exe_parent_path| {
exe_parent_path
.to_str()
.map(|path| path.to_string())
.ok_or(AssetServerError::InvalidRootPath)
}),
Err(err) => Err(AssetServerError::Io(err)),
}
}
}
pub fn load_assets(&self) -> Result<(), AssetServerError> {
let root_path_str = self.get_root_path()?;
let root_path = Path::new(&root_path_str);
for folder in self.asset_folders.iter() {
let asset_folder_path = root_path.join(folder);
self.load_assets_in_folder_recursive(&asset_folder_path)?;
}
Ok(())
}
pub fn load<T>(&self, path: &str) -> Result<Handle<T>, AssetServerError> {
self.load_untyped(path)
.map(|handle_id| Handle::from(handle_id))
}
pub fn load_sync<T>(
&self,
assets: &mut Assets<T>,
path: &str,
) -> Result<Handle<T>, AssetServerError>
where
T: 'static,
{
let asset_path = AssetPath::from(path);
if let Some(ref extension) = asset_path.extension {
if let Some(index) = self.extension_to_loader_index.get(extension.as_ref()) {
let handle_id = HandleId::new();
let resources = &self.loaders[*index];
let loader = resources.get::<Box<dyn AssetLoader<T>>>().unwrap();
let asset = loader.load_from_file(&asset_path)?;
let handle = Handle::from(handle_id);
assets.add_with_handle(handle, asset);
assets.set_path(handle, &asset_path.path);
Ok(handle)
} else {
Err(AssetServerError::MissingAssetHandler)
}
} else {
Err(AssetServerError::MissingAssetHandler)
}
}
pub fn load_untyped(&self, path: &str) -> Result<HandleId, AssetServerError> {
let asset_path = AssetPath::from(path);
if let Some(ref extension) = asset_path.extension {
if let Some(index) = self.extension_to_handler_index.get(extension.as_ref()) {
let handle_id = HandleId::new();
self.send_request_to_loader_thread(LoadRequest {
handle_id,
path: asset_path,
handler_index: *index,
});
Ok(handle_id)
} else {
Err(AssetServerError::MissingAssetHandler)
}
} else {
Err(AssetServerError::MissingAssetHandler)
}
}
fn send_request_to_loader_thread(&self, load_request: LoadRequest) {
let mut loader_threads = self.loader_threads.write().expect("RwLock poisoned");
if loader_threads.len() < self.max_loader_threads {
let loader_thread = LoaderThread {
requests: Arc::new(RwLock::new(vec![load_request])),
};
let requests = loader_thread.requests.clone();
loader_threads.push(loader_thread);
Self::start_thread(self.asset_handlers.clone(), requests);
} else {
let most_free_thread = loader_threads
.iter()
.min_by_key(|l| l.requests.read().expect("RwLock poisoned").len())
.unwrap();
let mut requests = most_free_thread.requests.write().expect("RwLock poisoned");
requests.push(load_request);
// if most free thread only has one reference, the thread as spun down. if so, we need to spin it back up!
if Arc::strong_count(&most_free_thread.requests) == 1 {
Self::start_thread(
self.asset_handlers.clone(),
most_free_thread.requests.clone(),
);
}
}
}
fn start_thread(
request_handlers: Arc<RwLock<Vec<Box<dyn AssetLoadRequestHandler>>>>,
requests: Arc<RwLock<Vec<LoadRequest>>>,
) {
thread::spawn(move || {
loop {
let request = {
let mut current_requests = requests.write().expect("RwLock poisoned");
if current_requests.len() == 0 {
// if there are no requests, spin down the thread
break;
}
current_requests.pop().unwrap()
};
let handlers = request_handlers.read().expect("RwLock poisoned");
let request_handler = &handlers[request.handler_index];
request_handler.handle_request(&request);
}
});
}
fn load_assets_in_folder_recursive(&self, path: &Path) -> Result<(), AssetServerError> {
if !path.is_dir() {
return Err(AssetServerError::AssetFolderNotADirectory(
path.to_str().unwrap().to_string(),
));
}
for entry in fs::read_dir(&path)? {
let entry = entry?;
let child_path = entry.path();
if !child_path.is_dir() {
let _ =
self.load_untyped(child_path.to_str().expect("Path should be a valid string"));
} else {
self.load_assets_in_folder_recursive(&child_path)?;
}
}
Ok(())
}
}

View file

@ -0,0 +1,147 @@
use crate::{
update_asset_storage_system, AssetChannel, AssetLoader, AssetServer, ChannelAssetHandler,
Handle, HandleId, DEFAULT_HANDLE_ID,
};
use bevy_app::{stage, AppBuilder, Events};
use bevy_core::bytes::GetBytes;
use legion::prelude::*;
use std::collections::HashMap;
pub enum AssetEvent<T> {
Created { handle: Handle<T> },
}
pub struct Assets<T> {
assets: HashMap<HandleId, T>,
paths: HashMap<String, Handle<T>>,
events: Events<AssetEvent<T>>,
}
impl<T> Default for Assets<T> {
fn default() -> Self {
Assets {
assets: HashMap::default(),
paths: HashMap::default(),
events: Events::default(),
}
}
}
impl<T> Assets<T> {
pub fn get_with_path(&mut self, path: &str) -> Option<Handle<T>> {
self.paths.get(path).map(|handle| *handle)
}
pub fn add(&mut self, asset: T) -> Handle<T> {
let id = HandleId::new();
self.assets.insert(id, asset);
let handle = Handle::from_id(id);
self.events.send(AssetEvent::Created { handle });
handle
}
pub fn add_with_handle(&mut self, handle: Handle<T>, asset: T) {
self.assets.insert(handle.id, asset);
self.events.send(AssetEvent::Created { handle });
}
pub fn add_default(&mut self, asset: T) -> Handle<T> {
self.assets.insert(DEFAULT_HANDLE_ID, asset);
let handle = Handle::default();
self.events.send(AssetEvent::Created { handle });
handle
}
pub fn set_path(&mut self, handle: Handle<T>, path: &str) {
self.paths.insert(path.to_string(), handle);
}
pub fn get_id(&self, id: HandleId) -> Option<&T> {
self.assets.get(&id)
}
pub fn get_id_mut(&mut self, id: HandleId) -> Option<&mut T> {
self.assets.get_mut(&id)
}
pub fn get(&self, handle: &Handle<T>) -> Option<&T> {
self.assets.get(&handle.id)
}
pub fn get_mut(&mut self, handle: &Handle<T>) -> Option<&mut T> {
self.assets.get_mut(&handle.id)
}
pub fn iter(&self) -> impl Iterator<Item = (Handle<T>, &T)> {
self.assets.iter().map(|(k, v)| (Handle::from_id(*k), v))
}
pub fn asset_event_system(
mut events: ResMut<Events<AssetEvent<T>>>,
mut assets: ResMut<Assets<T>>,
) {
events.extend(assets.events.drain())
}
}
impl<T> GetBytes for Handle<T> {
fn get_bytes(&self) -> Vec<u8> {
Vec::new()
}
fn get_bytes_ref(&self) -> Option<&[u8]> {
None
}
}
pub trait AddAsset {
fn add_asset<T>(&mut self) -> &mut Self
where
T: Send + Sync + 'static;
fn add_asset_loader<TLoader, TAsset>(&mut self, loader: TLoader) -> &mut Self
where
TLoader: AssetLoader<TAsset> + Clone,
TAsset: Send + Sync + 'static;
}
impl AddAsset for AppBuilder {
fn add_asset<T>(&mut self) -> &mut Self
where
T: Send + Sync + 'static,
{
self.init_resource::<Assets<T>>()
.add_system_to_stage(
stage::EVENT_UPDATE,
Assets::<T>::asset_event_system.system(),
)
.add_event::<AssetEvent<T>>()
}
fn add_asset_loader<TLoader, TAsset>(&mut self, loader: TLoader) -> &mut Self
where
TLoader: AssetLoader<TAsset> + Clone,
TAsset: Send + Sync + 'static,
{
{
if !self.resources().contains::<AssetChannel<TAsset>>() {
self.resources_mut().insert(AssetChannel::<TAsset>::new());
self.add_system_to_stage(
crate::stage::LOAD_ASSETS,
update_asset_storage_system::<TAsset>.system(),
);
}
let asset_channel = self
.resources()
.get::<AssetChannel<TAsset>>()
.expect("AssetChannel should always exist at this point.");
let mut asset_server = self
.resources()
.get_mut::<AssetServer>()
.expect("AssetServer does not exist. Consider adding it as a resource.");
asset_server.add_loader(loader.clone());
let handler = ChannelAssetHandler::new(loader, asset_channel.sender.clone());
asset_server.add_handler(handler);
}
self
}
}

View file

@ -63,6 +63,41 @@ impl<T> Handle<T> {
}
}
impl<T> From<HandleId> for Handle<T> {
fn from(value: HandleId) -> Self {
Handle::from_id(value)
}
}
impl<T> From<u128> for Handle<T> {
fn from(value: u128) -> Self {
Handle::from_u128(value)
}
}
impl<T> From<[u8; 16]> for Handle<T> {
fn from(value: [u8; 16]) -> Self {
Handle::from_bytes(value)
}
}
impl<T> From<HandleUntyped> for Handle<T>
where
T: 'static,
{
fn from(handle: HandleUntyped) -> Self {
if TypeId::of::<T>() == handle.type_id {
Handle {
id: handle.id,
marker: PhantomData::default(),
}
} else {
panic!("attempted to convert untyped handle to incorrect typed handle")
}
}
}
impl<T> Hash for Handle<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
@ -125,14 +160,4 @@ where
type_id: TypeId::of::<T>(),
}
}
}
impl<T> From<HandleUntyped> for Handle<T>
where
T: 'static,
{
fn from(handle: HandleUntyped) -> Self {
Handle::from_untyped(handle)
.expect("attempted to convert untyped handle to incorrect typed handle")
}
}
}

View file

@ -1,114 +1,28 @@
mod assets;
mod handle;
mod loader;
mod asset_server;
mod load_request;
mod asset_path;
pub use assets::*;
pub use handle::*;
pub use loader::*;
pub use asset_server::*;
pub use load_request::*;
pub use asset_path::*;
use bevy_app::{stage, AppBuilder, Events};
use bevy_core::bytes::GetBytes;
use legion::prelude::*;
use std::collections::HashMap;
use bevy_app::{AppBuilder, AppPlugin};
pub enum AssetEvent<T> {
Created { handle: Handle<T> },
pub mod stage {
pub const LOAD_ASSETS: &str = "load_assets";
}
pub struct Assets<T> {
assets: HashMap<HandleId, T>,
names: HashMap<String, Handle<T>>,
events: Events<AssetEvent<T>>,
}
#[derive(Default)]
pub struct AssetPlugin;
impl<T> Default for Assets<T> {
fn default() -> Self {
Assets {
assets: HashMap::default(),
names: HashMap::default(),
events: Events::default(),
}
}
}
impl<T> Assets<T> {
pub fn get_named(&mut self, name: &str) -> Option<Handle<T>> {
self.names.get(name).map(|handle| *handle)
}
pub fn add(&mut self, asset: T) -> Handle<T> {
let id = HandleId::new();
self.assets.insert(id, asset);
let handle = Handle::from_id(id);
self.events.send(AssetEvent::Created { handle });
handle
}
pub fn add_with_handle(&mut self, handle: Handle<T>, asset: T) {
self.assets.insert(handle.id, asset);
self.events.send(AssetEvent::Created { handle });
}
pub fn add_default(&mut self, asset: T) -> Handle<T> {
self.assets.insert(DEFAULT_HANDLE_ID, asset);
let handle = Handle::default();
self.events.send(AssetEvent::Created { handle });
handle
}
pub fn set_name(&mut self, name: &str, handle: Handle<T>) {
self.names.insert(name.to_string(), handle);
}
pub fn get_id(&self, id: HandleId) -> Option<&T> {
self.assets.get(&id)
}
pub fn get_id_mut(&mut self, id: HandleId) -> Option<&mut T> {
self.assets.get_mut(&id)
}
pub fn get(&self, handle: &Handle<T>) -> Option<&T> {
self.assets.get(&handle.id)
}
pub fn get_mut(&mut self, handle: &Handle<T>) -> Option<&mut T> {
self.assets.get_mut(&handle.id)
}
pub fn iter(&self) -> impl Iterator<Item = (Handle<T>, &T)> {
self.assets.iter().map(|(k, v)| (Handle::from_id(*k), v))
}
pub fn asset_event_system(
mut events: ResMut<Events<AssetEvent<T>>>,
mut assets: ResMut<Assets<T>>,
) {
events.extend(assets.events.drain())
}
}
impl<T> GetBytes for Handle<T> {
fn get_bytes(&self) -> Vec<u8> {
Vec::new()
}
fn get_bytes_ref(&self) -> Option<&[u8]> {
None
}
}
pub trait AddAsset {
fn add_asset<T>(&mut self) -> &mut Self
where
T: Send + Sync + 'static;
}
impl AddAsset for AppBuilder {
fn add_asset<T>(&mut self) -> &mut Self
where
T: Send + Sync + 'static,
{
self.init_resource::<Assets<T>>()
.add_system_to_stage(
stage::EVENT_UPDATE,
Assets::<T>::asset_event_system.system(),
)
.add_event::<AssetEvent<T>>()
impl AppPlugin for AssetPlugin {
fn build(&self, app: &mut AppBuilder) {
app.add_stage(stage::LOAD_ASSETS).init_resource::<AssetServer>();
}
}

View file

@ -0,0 +1,64 @@
use crate::{AssetLoadError, AssetLoader, AssetPath, AssetResult, Handle, HandleId};
use anyhow::Result;
use crossbeam_channel::Sender;
use fs::File;
use io::Read;
use std::{fs, io, path::Path};
#[derive(Debug)]
pub struct LoadRequest {
pub path: AssetPath,
pub handle_id: HandleId,
pub handler_index: usize,
}
pub trait AssetLoadRequestHandler: Send + Sync + 'static {
fn handle_request(&self, load_request: &LoadRequest);
fn extensions(&self) -> &[&str];
}
pub struct ChannelAssetHandler<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset>,
{
sender: Sender<AssetResult<TAsset>>,
loader: TLoader,
}
impl<TLoader, TAsset> ChannelAssetHandler<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset>,
{
pub fn new(loader: TLoader, sender: Sender<AssetResult<TAsset>>) -> Self {
ChannelAssetHandler { sender, loader }
}
fn load_asset(&self, load_request: &LoadRequest) -> Result<TAsset, AssetLoadError> {
let mut file = File::open(Path::new(load_request.path.path.as_ref()))?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
let asset = self.loader.from_bytes(&load_request.path, bytes)?;
Ok(asset)
}
}
impl<TLoader, TAsset> AssetLoadRequestHandler for ChannelAssetHandler<TLoader, TAsset>
where
TLoader: AssetLoader<TAsset> + 'static,
TAsset: Send + 'static,
{
fn handle_request(&self, load_request: &LoadRequest) {
let result = self.load_asset(load_request);
let asset_result = AssetResult {
handle: Handle::from(load_request.handle_id),
result,
path: load_request.path.clone(),
};
self.sender
.send(asset_result)
.expect("loaded asset should have been sent");
}
fn extensions(&self) -> &[&str] {
self.loader.extensions()
}
}

View file

@ -0,0 +1,65 @@
use crate::{AssetPath, Assets, Handle};
use anyhow::Result;
use crossbeam_channel::{Receiver, Sender, TryRecvError};
use fs::File;
use io::Read;
use legion::prelude::{Res, ResMut};
use std::{fs, io, path::Path};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AssetLoadError {
#[error("Encountered an io error while loading asset.")]
Io(#[from] io::Error),
#[error("This asset's loader encountered an error while loading.")]
LoaderError(#[from] anyhow::Error),
}
pub trait AssetLoader<T>: Send + Sync + 'static {
fn from_bytes(&self, asset_path: &AssetPath, bytes: Vec<u8>) -> Result<T, anyhow::Error>;
fn extensions(&self) -> &[&str];
fn load_from_file(&self, asset_path: &AssetPath) -> Result<T, AssetLoadError> {
let mut file = File::open(Path::new(asset_path.path.as_ref()))?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
let asset = self.from_bytes(asset_path, bytes)?;
Ok(asset)
}
}
pub struct AssetResult<T> {
pub result: Result<T, AssetLoadError>,
pub handle: Handle<T>,
pub path: AssetPath,
}
pub struct AssetChannel<T> {
pub sender: Sender<AssetResult<T>>,
pub receiver: Receiver<AssetResult<T>>,
}
impl<T> AssetChannel<T> {
pub fn new() -> Self {
let (sender, receiver) = crossbeam_channel::unbounded();
AssetChannel { sender, receiver }
}
}
pub fn update_asset_storage_system<T>(
asset_channel: Res<AssetChannel<T>>,
mut assets: ResMut<Assets<T>>,
) {
loop {
match asset_channel.receiver.try_recv() {
Ok(result) => {
let asset = result.result.unwrap();
assets.add_with_handle(result.handle, asset);
assets.set_path(result.handle, &result.path.path);
}
Err(TryRecvError::Empty) => {
break;
}
Err(TryRecvError::Disconnected) => panic!("AssetChannel disconnected"),
}
}
}

View file

@ -5,8 +5,10 @@ authors = ["Carter Anderson <mcanders1@gmail.com>"]
edition = "2018"
[dependencies]
bevy_app = { path = "../bevy_app" }
bevy_asset = { path = "../bevy_asset" }
bevy_render = { path = "../bevy_render" }
gltf = "0.15.2"
thiserror = "1.0"
thiserror = "1.0"
anyhow = "1.0"

View file

@ -1,109 +1,14 @@
use bevy_render::{
mesh::{Mesh, VertexAttribute, VertexAttributeValues},
pipeline::state_descriptors::PrimitiveTopology,
};
use gltf::{buffer::Source, iter, mesh::Mode};
use std::{fs, io, path::Path};
use thiserror::Error;
mod loader;
pub use loader::*;
#[derive(Error, Debug)]
pub enum GltfError {
#[error("Unsupported primitive mode.")]
UnsupportedPrimitive { mode: Mode },
#[error("Invalid GLTF file.")]
Gltf(#[from] gltf::Error),
#[error("Failed to load file.")]
Io(#[from] io::Error),
#[error("Binary buffers not supported yet.")]
BinaryBuffersUnsupported,
}
use bevy_app::{AppPlugin, AppBuilder};
use bevy_asset::{AddAsset};
fn get_primitive_topology(mode: Mode) -> Result<PrimitiveTopology, GltfError> {
match mode {
Mode::Points => Ok(PrimitiveTopology::PointList),
Mode::Lines => Ok(PrimitiveTopology::LineList),
Mode::LineStrip => Ok(PrimitiveTopology::LineStrip),
Mode::Triangles => Ok(PrimitiveTopology::TriangleList),
Mode::TriangleStrip => Ok(PrimitiveTopology::TriangleStrip),
mode @ _ => Err(GltfError::UnsupportedPrimitive { mode }),
#[derive(Default)]
pub struct GltfPlugin;
impl AppPlugin for GltfPlugin {
fn build(&self, app: &mut AppBuilder) {
app.add_asset_loader(GltfLoader);
}
}
pub fn load_gltf(path: &str) -> Result<Option<Mesh>, GltfError> {
let path: &Path = path.as_ref();
let file = fs::File::open(&path)?;
let reader = io::BufReader::new(file);
let gltf = gltf::Gltf::from_reader(reader)?;
let buffer_data = load_buffers(gltf.buffers(), path)?;
for scene in gltf.scenes() {
for node in scene.nodes() {
return Ok(Some(load_node(&buffer_data, &node, 1)?));
}
}
Ok(None)
}
fn load_node(buffer_data: &[Vec<u8>], node: &gltf::Node, depth: i32) -> Result<Mesh, GltfError> {
if let Some(mesh) = node.mesh() {
for primitive in mesh.primitives() {
let reader = primitive.reader(|buffer| Some(&buffer_data[buffer.index()]));
let primitive_topology = get_primitive_topology(primitive.mode())?;
let mut mesh = Mesh::new(primitive_topology);
reader
.read_positions()
.map(|v| VertexAttribute {
name: "Vertex_Position".into(),
values: VertexAttributeValues::Float3(v.collect()),
})
.map(|vertex_attribute| mesh.attributes.push(vertex_attribute));
reader
.read_normals()
.map(|v| VertexAttribute {
name: "Vertex_Normal".into(),
values: VertexAttributeValues::Float3(v.collect()),
})
.map(|vertex_attribute| mesh.attributes.push(vertex_attribute));
reader
.read_tex_coords(0)
.map(|v| VertexAttribute {
name: "Vertex_Uv".into(),
values: VertexAttributeValues::Float2(v.into_f32().collect()),
})
.map(|vertex_attribute| mesh.attributes.push(vertex_attribute));
reader.read_indices().map(|indices| {
mesh.indices = Some(indices.into_u32().collect::<Vec<u32>>());
});
return Ok(mesh);
}
}
for child in node.children() {
return Ok(load_node(buffer_data, &child, depth + 1)?);
}
panic!("failed to find mesh")
}
fn load_buffers(buffers: iter::Buffers, path: &Path) -> Result<Vec<Vec<u8>>, GltfError> {
let mut buffer_data = Vec::new();
for buffer in buffers {
match buffer.source() {
Source::Uri(uri) => {
if uri.starts_with("data:") {
} else {
let buffer_path = path.parent().unwrap().join(uri);
let buffer_bytes = fs::read(buffer_path)?;
buffer_data.push(buffer_bytes);
}
}
Source::Bin => return Err(GltfError::BinaryBuffersUnsupported),
}
}
Ok(buffer_data)
}
}

View file

@ -0,0 +1,127 @@
use bevy_render::{
mesh::{Mesh, VertexAttribute, VertexAttributeValues},
pipeline::state_descriptors::PrimitiveTopology,
};
use bevy_asset::{AssetLoader, AssetPath};
use gltf::{buffer::Source, iter, mesh::Mode};
use std::{fs, io, path::Path};
use thiserror::Error;
use anyhow::Result;
#[derive(Clone)]
pub struct GltfLoader;
impl AssetLoader<Mesh> for GltfLoader {
fn from_bytes(&self, asset_path: &AssetPath, bytes: Vec<u8>) -> Result<Mesh> {
let mesh = load_gltf(asset_path, bytes)?;
Ok(mesh)
}
fn extensions(&self) -> &[&str] {
static EXTENSIONS: &[&str] = &[
"gltf"
];
EXTENSIONS
}
}
#[derive(Error, Debug)]
pub enum GltfError {
#[error("Unsupported primitive mode.")]
UnsupportedPrimitive { mode: Mode },
#[error("Invalid GLTF file.")]
Gltf(#[from] gltf::Error),
#[error("Failed to load file.")]
Io(#[from] io::Error),
#[error("Binary buffers not supported yet.")]
BinaryBuffersUnsupported,
}
fn get_primitive_topology(mode: Mode) -> Result<PrimitiveTopology, GltfError> {
match mode {
Mode::Points => Ok(PrimitiveTopology::PointList),
Mode::Lines => Ok(PrimitiveTopology::LineList),
Mode::LineStrip => Ok(PrimitiveTopology::LineStrip),
Mode::Triangles => Ok(PrimitiveTopology::TriangleList),
Mode::TriangleStrip => Ok(PrimitiveTopology::TriangleStrip),
mode @ _ => Err(GltfError::UnsupportedPrimitive { mode }),
}
}
pub fn load_gltf(asset_path: &AssetPath, bytes: Vec<u8>) -> Result<Mesh, GltfError> {
let gltf = gltf::Gltf::from_slice(&bytes)?;
let buffer_data = load_buffers(gltf.buffers(), asset_path)?;
for scene in gltf.scenes() {
for node in scene.nodes() {
return Ok(load_node(&buffer_data, &node, 1)?);
}
}
// TODO: remove this when full gltf support is added
panic!("no mesh found!")
}
fn load_node(buffer_data: &[Vec<u8>], node: &gltf::Node, depth: i32) -> Result<Mesh, GltfError> {
if let Some(mesh) = node.mesh() {
for primitive in mesh.primitives() {
let reader = primitive.reader(|buffer| Some(&buffer_data[buffer.index()]));
let primitive_topology = get_primitive_topology(primitive.mode())?;
let mut mesh = Mesh::new(primitive_topology);
reader
.read_positions()
.map(|v| VertexAttribute {
name: "Vertex_Position".into(),
values: VertexAttributeValues::Float3(v.collect()),
})
.map(|vertex_attribute| mesh.attributes.push(vertex_attribute));
reader
.read_normals()
.map(|v| VertexAttribute {
name: "Vertex_Normal".into(),
values: VertexAttributeValues::Float3(v.collect()),
})
.map(|vertex_attribute| mesh.attributes.push(vertex_attribute));
reader
.read_tex_coords(0)
.map(|v| VertexAttribute {
name: "Vertex_Uv".into(),
values: VertexAttributeValues::Float2(v.into_f32().collect()),
})
.map(|vertex_attribute| mesh.attributes.push(vertex_attribute));
reader.read_indices().map(|indices| {
mesh.indices = Some(indices.into_u32().collect::<Vec<u32>>());
});
return Ok(mesh);
}
}
for child in node.children() {
return Ok(load_node(buffer_data, &child, depth + 1)?);
}
panic!("failed to find mesh")
}
fn load_buffers(buffers: iter::Buffers, asset_path: &AssetPath) -> Result<Vec<Vec<u8>>, GltfError> {
let mut buffer_data = Vec::new();
for buffer in buffers {
match buffer.source() {
Source::Uri(uri) => {
if uri.starts_with("data:") {
} else {
let path = Path::new(asset_path.path.as_ref());
let buffer_path = path.parent().unwrap().join(uri);
let buffer_bytes = fs::read(buffer_path)?;
buffer_data.push(buffer_bytes);
}
}
Source::Bin => return Err(GltfError::BinaryBuffersUnsupported),
}
}
Ok(buffer_data)
}

View file

@ -43,7 +43,7 @@ uuid = { version = "0.8", features = ["v4", "serde"] }
tracing = "0.1"
itertools = "0.8"
rayon = "1.2"
crossbeam-channel = "0.4.0"
crossbeam-channel = "0.4.2"
[[bench]]
name = "benchmarks"

View file

@ -24,7 +24,7 @@ downcast-rs = "1.0"
itertools = "0.8"
rayon = { version = "1.2", optional = true }
crossbeam-queue = { version = "0.2.0", optional = true }
crossbeam-channel = "0.4.0"
crossbeam-channel = "0.4.2"
derivative = "1"
smallvec = "1.2"
tracing = "0.1"

View file

@ -319,11 +319,12 @@ pub fn mesh_specializer_system() -> Box<dyn Schedulable> {
)
.build(|_, world, meshes, query| {
for (mesh_handle, mut renderable) in query.iter_mut(world) {
let mesh = meshes.get(&mesh_handle).unwrap();
renderable
.render_resource_assignments
.pipeline_specialization
.primitive_topology = mesh.primitive_topology;
if let Some(mesh) = meshes.get(&mesh_handle) {
renderable
.render_resource_assignments
.pipeline_specialization
.primitive_topology = mesh.primitive_topology;
}
}
})
}

View file

@ -1,4 +1,5 @@
use bevy::{gltf, prelude::*};
use bevy::prelude::*;
use bevy_asset::AssetServer;
fn main() {
App::build()
@ -9,12 +10,14 @@ fn main() {
fn setup(
command_buffer: &mut CommandBuffer,
asset_server: ResMut<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// load the mesh
let mesh = gltf::load_gltf("assets/models/monkey/Monkey.gltf").unwrap().unwrap();
let mesh_handle = meshes.add(mesh);
let mesh_handle = asset_server
.load_sync(&mut meshes, "assets/models/monkey/Monkey.gltf")
.unwrap();
// create a material for the mesh
let material_handle = materials.add(StandardMaterial {

View file

@ -0,0 +1,13 @@
use bevy::{asset::AssetServer, prelude::*};
fn main() {
App::build()
.add_default_plugins()
.add_startup_system(setup.system())
.run();
}
fn setup(mut asset_server: ResMut<AssetServer>) {
asset_server.add_asset_folder("assets");
asset_server.load_assets().expect("Assets should exist");
}

View file

@ -18,6 +18,9 @@ impl AddDefaultPlugins for AppBuilder {
#[cfg(feature = "window")]
self.add_plugin(bevy_window::WindowPlugin::default());
#[cfg(feature = "asset")]
self.add_plugin(bevy_asset::AssetPlugin::default());
#[cfg(feature = "render")]
self.add_plugin(bevy_render::RenderPlugin::default());
@ -27,6 +30,9 @@ impl AddDefaultPlugins for AppBuilder {
#[cfg(feature = "ui")]
self.add_plugin(bevy_ui::UiPlugin::default());
#[cfg(feature = "gltf")]
self.add_plugin(bevy_gltf::GltfPlugin::default());
#[cfg(feature = "winit")]
self.add_plugin(bevy_winit::WinitPlugin::default());
#[cfg(not(feature = "winit"))]