Skip to content

Commit

Permalink
Implement async loading
Browse files Browse the repository at this point in the history
  • Loading branch information
hasenbanck committed Dec 31, 2024
1 parent 52ddb26 commit 259a8ad
Show file tree
Hide file tree
Showing 8 changed files with 330 additions and 141 deletions.
5 changes: 1 addition & 4 deletions korangar/src/interface/windows/debug/profiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,7 @@ impl PrototypeWindow<InterfaceSettings> for ProfilerWindow {
PickList::default()
.with_options(vec![
("Main thread", crate::threads::Enum::Main),
("Picker thread", crate::threads::Enum::Picker),
("Shadow thread", crate::threads::Enum::Shadow),
("Point shadow thread", crate::threads::Enum::PointShadow),
("Deferred thread", crate::threads::Enum::Deferred),
("Loader thread", crate::threads::Enum::Loader),
])
.with_selected(self.visible_thread.clone())
.with_width(dimension_bound!(150))
Expand Down
211 changes: 211 additions & 0 deletions korangar/src/loaders/async/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
use std::cmp::PartialEq;
use std::sync::{Arc, Mutex};

use hashbrown::{HashMap, HashSet};
#[cfg(feature = "debug")]
use korangar_debug::logging::print_debug;
use korangar_networking::EntityData;
#[cfg(feature = "debug")]
use korangar_util::texture_atlas::AtlasAllocation;
use ragnarok_packets::{ClientTick, EntityId, TilePosition};
use rayon::{ThreadPool, ThreadPoolBuilder};

use crate::loaders::error::LoadError;
use crate::loaders::{ActionLoader, AnimationLoader, MapLoader, ModelLoader, ScriptLoader, SpriteLoader, TextureLoader};
#[cfg(feature = "debug")]
use crate::threads;
use crate::world::{Map, Npc};

#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum LoaderId {
Npc(EntityId),
Map(String),
}

pub enum LoadableResource {
Npc(Box<Npc>),
Map { map: Arc<Map>, player_position: TilePosition },
}

enum LoadStatus {
Loading,
Completed(LoadableResource),
Failed(LoadError),
}

impl PartialEq for LoadStatus {
fn eq(&self, other: &Self) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(other)
}
}

pub struct AsyncLoader {
action_loader: Arc<ActionLoader>,
animation_loader: Arc<AnimationLoader>,
map_loader: Arc<MapLoader>,
model_loader: Arc<ModelLoader>,
script_loader: Arc<ScriptLoader>,
sprite_loader: Arc<SpriteLoader>,
texture_loader: Arc<TextureLoader>,
resources: Arc<Mutex<AsyncLoaderResources>>,
thread_pool: ThreadPool,
}

pub struct AsyncLoaderResources {
pending_loads: HashMap<LoaderId, LoadStatus>,
cancelled_loads: HashSet<LoaderId>,
}

impl AsyncLoader {
pub fn new(
action_loader: Arc<ActionLoader>,
animation_loader: Arc<AnimationLoader>,
map_loader: Arc<MapLoader>,
model_loader: Arc<ModelLoader>,
script_loader: Arc<ScriptLoader>,
sprite_loader: Arc<SpriteLoader>,
texture_loader: Arc<TextureLoader>,
) -> Self {
let thread_pool = ThreadPoolBuilder::new()
.num_threads(1)
.thread_name(|_| "async loader".to_string())
.build()
.unwrap();

Self {
action_loader,
animation_loader,
map_loader,
model_loader,
script_loader,
sprite_loader,
texture_loader,
resources: Arc::new(Mutex::new(AsyncLoaderResources {
pending_loads: HashMap::new(),
cancelled_loads: HashSet::new(),
})),
thread_pool,
}
}

pub fn request_npc_load(&self, map: Arc<Map>, entity_data: EntityData, client_tick: ClientTick) {
let sprite_loader = self.sprite_loader.clone();
let action_loader = self.action_loader.clone();
let animation_loader = self.animation_loader.clone();
let script_loader = self.script_loader.clone();

self.request_load(LoaderId::Npc(entity_data.entity_id), move || {
let npc = Npc::new(
&sprite_loader,
&action_loader,
&animation_loader,
&script_loader,
&map,
entity_data,
client_tick,
);
Ok(LoadableResource::Npc(Box::new(npc)))
});
}

pub fn request_map_load(
&self,
map_name: String,
player_position: TilePosition,
#[cfg(feature = "debug")] tile_texture_mapping: Arc<Vec<AtlasAllocation>>,
) {
let map_loader = self.map_loader.clone();
let model_loader = self.model_loader.clone();
let texture_loader = self.texture_loader.clone();

self.request_load(LoaderId::Map(map_name.clone()), move || {
let map = map_loader.load(
map_name,
&model_loader,
texture_loader,
#[cfg(feature = "debug")]
&tile_texture_mapping,
)?;
Ok(LoadableResource::Map { map, player_position })
});
}

fn request_load<F>(&self, id: LoaderId, load_function: F)
where
F: FnOnce() -> Result<LoadableResource, LoadError> + Send + 'static,
{
let resources = Arc::clone(&self.resources);

resources.lock().unwrap().pending_loads.insert(id.clone(), LoadStatus::Loading);

self.thread_pool.spawn(move || {
#[cfg(feature = "debug")]
let _measurement = threads::Loader::start_frame();

let result = load_function();

let mut resources = resources.lock().unwrap();

if !resources.pending_loads.contains_key(&id) {
return;
}

if resources.cancelled_loads.contains(&id) {
resources.pending_loads.remove(&id);
resources.cancelled_loads.remove(&id);
return;
}

let status = match result {
Ok(resource) => LoadStatus::Completed(resource),
Err(err) => LoadStatus::Failed(err),
};

resources.pending_loads.insert(id, status);
});
}

pub fn remove_loading_and_cancelled_npc_load(&self, entity_id: EntityId) {
let mut resources = self.resources.lock().unwrap();
let id = LoaderId::Npc(entity_id);
resources.pending_loads.remove(&id);
resources.cancelled_loads.remove(&id);
}

pub fn cancel_npc_load(&self, entity_id: EntityId) {
let mut resources = self.resources.lock().unwrap();

if resources.pending_loads.contains_key(&LoaderId::Npc(entity_id)) {
resources.cancelled_loads.insert(LoaderId::Npc(entity_id));
}
}

pub fn take_completed(&self) -> impl Iterator<Item = (LoaderId, LoadableResource)> + '_ {
std::iter::from_fn({
let resources = Arc::clone(&self.resources);
move || {
let mut guard = resources.lock().unwrap();

let completed_id = guard
.pending_loads
.iter()
.find(|(_, status)| matches!(status, LoadStatus::Completed(_) | LoadStatus::Failed(_)))
.map(|(id, _)| id.clone());

if let Some(id) = completed_id {
match guard.pending_loads.remove(&id).unwrap() {
LoadStatus::Failed(_error) => {
#[cfg(feature = "debug")]
print_debug!("Async load error: {:?}", _error);
None
}
LoadStatus::Completed(resource) => Some((id, resource)),
_ => unreachable!(),
}
} else {
None
}
}
})
}
}
4 changes: 2 additions & 2 deletions korangar/src/loaders/map/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl MapLoader {
model_loader: &ModelLoader,
texture_loader: Arc<TextureLoader>,
#[cfg(feature = "debug")] tile_texture_mapping: &[AtlasAllocation],
) -> Result<Map, LoadError> {
) -> Result<Arc<Map>, LoadError> {
#[cfg(feature = "debug")]
let timer = Timer::new_dynamic(format!("load map from {}", &resource_file));

Expand Down Expand Up @@ -232,7 +232,7 @@ impl MapLoader {
#[cfg(feature = "debug")]
timer.stop();

Ok(map)
Ok(Arc::new(map))
}

fn generate_vertex_buffer_and_atlas_texture(
Expand Down
2 changes: 2 additions & 0 deletions korangar/src/loaders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod action;
mod animation;
mod archive;

mod r#async;
mod effect;
pub mod error;
mod font;
Expand All @@ -20,6 +21,7 @@ pub use self::font::{FontLoader, FontSize, GlyphInstruction, Scaling, TextLayout
pub use self::gamefile::*;
pub use self::map::{MapLoader, MAP_TILE_SIZE};
pub use self::model::*;
pub use self::r#async::*;
pub use self::script::{ResourceMetadata, ScriptLoader};
pub use self::server::{load_client_info, ClientInfo, ServiceId};
pub use self::sprite::*;
Expand Down
Loading

0 comments on commit 259a8ad

Please sign in to comment.