diff --git a/Cargo.lock b/Cargo.lock
index 48a1c021b..65d906345 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2329,6 +2329,7 @@ version = "0.2.2"
 dependencies = [
  "glam",
  "strafesnet_common",
+ "strafesnet_deferred_loader",
  "vbsp",
  "vmdl",
 ]
diff --git a/lib/bsp_loader/Cargo.toml b/lib/bsp_loader/Cargo.toml
index a3ef41f9d..1c01e4169 100644
--- a/lib/bsp_loader/Cargo.toml
+++ b/lib/bsp_loader/Cargo.toml
@@ -12,5 +12,6 @@ authors = ["Rhys Lloyd <krakow20@gmail.com>"]
 [dependencies]
 glam = "0.29.0"
 strafesnet_common = { path = "../common", registry = "strafesnet" }
+strafesnet_deferred_loader = { version = "0.5.0", path = "../deferred_loader" }
 vbsp = "0.6.0"
 vmdl = "0.2.0"
diff --git a/lib/bsp_loader/src/bsp.rs b/lib/bsp_loader/src/bsp.rs
index 903c611ac..12bd173dd 100644
--- a/lib/bsp_loader/src/bsp.rs
+++ b/lib/bsp_loader/src/bsp.rs
@@ -1,18 +1,18 @@
-use strafesnet_common::{map,model,integer,gameplay_attributes};
+use std::borrow::Cow;
 
-const VALVE_SCALE:f32=1.0/16.0;
-fn valve_transform([x,y,z]:[f32;3])->integer::Planar64Vec3{
-	integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap()
-}
-pub fn convert_bsp<AcquireRenderConfigId,AcquireMeshId>(
-	bsp:&vbsp::Bsp,
-	mut acquire_render_config_id:AcquireRenderConfigId,
-	mut acquire_mesh_id:AcquireMeshId
-)->PartialMap1
-where
-	AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
-	AcquireMeshId:FnMut(&str)->model::MeshId,
-{
+use strafesnet_common::{map,model,integer,gameplay_attributes};
+use strafesnet_deferred_loader::deferred_loader::{MeshDeferredLoader,RenderConfigDeferredLoader};
+use strafesnet_deferred_loader::mesh::Meshes;
+use strafesnet_deferred_loader::texture::{RenderConfigs,Texture};
+
+use crate::valve_transform;
+
+pub fn convert<'a>(
+	bsp:&'a crate::Bsp,
+	render_config_deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>,
+	mesh_deferred_loader:&mut MeshDeferredLoader<&'a str>,
+)->PartialMap1{
+	let bsp=bsp.as_ref();
 	//figure out real attributes later
 	let mut unique_attributes=Vec::new();
 	unique_attributes.push(gameplay_attributes::CollisionAttributes::Decoration);
@@ -22,7 +22,7 @@ where
 	//declare all prop models to Loader
 	let prop_models=bsp.static_props().map(|prop|{
 		//get or create mesh_id
-		let mesh_id=acquire_mesh_id(prop.model());
+		let mesh_id=mesh_deferred_loader.acquire_mesh_id(prop.model());
 		//not the most failsafe code but this is just for the map tool lmao
 		if prop_mesh_count==mesh_id.get(){
 			prop_mesh_count+=1;
@@ -65,7 +65,7 @@ where
 
 			//this automatically figures out what the texture is trying to do and creates
 			//a render config for it, and then returns the id to that render config
-			let render_id=acquire_render_config_id(Some(face_texture_data.name()));
+			let render_id=render_config_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(face_texture_data.name())));
 
 			//normal
 			let normal=face.normal();
@@ -176,26 +176,13 @@ pub struct PartialMap1{
 	modes:strafesnet_common::gameplay_modes::Modes,
 }
 impl PartialMap1{
-	pub fn add_prop_meshes<AcquireRenderConfigId>(
+	pub fn add_prop_meshes<'a>(
 		self,
-		prop_meshes:impl IntoIterator<Item=(model::MeshId,crate::data::ModelData)>,
-		mut acquire_render_config_id:AcquireRenderConfigId,
-	)->PartialMap2
-	where
-		AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
-	{
+		prop_meshes:Meshes,
+	)->PartialMap2{
 		PartialMap2{
 			attributes:self.attributes,
-			prop_meshes:prop_meshes.into_iter().filter_map(|(mesh_id,model_data)|
-				//this will generate new render ids and texture ids
-				match convert_mesh(model_data,&mut acquire_render_config_id){
-					Ok(mesh)=>Some((mesh_id,mesh)),
-					Err(e)=>{
-						println!("error converting mesh: {e}");
-						None
-					}
-				}
-			).collect(),
+			prop_meshes:prop_meshes.consume().collect(),
 			prop_models:self.prop_models,
 			world_meshes:self.world_meshes,
 			world_models:self.world_models,
@@ -214,8 +201,7 @@ pub struct PartialMap2{
 impl PartialMap2{
 	pub fn add_render_configs_and_textures(
 		mut self,
-		render_configs:impl IntoIterator<Item=(model::RenderConfigId,model::RenderConfig)>,
-		textures:impl IntoIterator<Item=(model::TextureId,Vec<u8>)>,
+		render_configs:RenderConfigs,
 	)->map::CompleteMap{
 		//merge mesh and model lists, flatten and remap all ids
 		let mesh_id_offset=self.world_meshes.len();
@@ -234,10 +220,11 @@ impl PartialMap2{
 			})
 		));
 		//let mut models=Vec::new();
+		let (textures,render_configs)=render_configs.consume();
 		let (textures,texture_id_map):(Vec<Vec<u8>>,std::collections::HashMap<model::TextureId,model::TextureId>)
 		=textures.into_iter()
 		//.filter_map(f) cull unused textures
-		.enumerate().map(|(new_texture_id,(old_texture_id,texture))|{
+		.enumerate().map(|(new_texture_id,(old_texture_id,Texture::ImageDDS(texture)))|{
 			(texture,(old_texture_id,model::TextureId::new(new_texture_id as u32)))
 		}).unzip();
 		let render_configs=render_configs.into_iter().map(|(_render_config_id,mut render_config)|{
@@ -257,77 +244,3 @@ impl PartialMap2{
 		}
 	}
 }
-
-fn convert_mesh<AcquireRenderConfigId>(
-	model_data:crate::data::ModelData,
-	acquire_render_config_id:&mut AcquireRenderConfigId,
-)->Result<model::Mesh,vmdl::ModelError>
-where
-	AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId,
-{
-	let model=model_data.read_model()?;
-	let texture_paths=model.texture_directories();
-	if texture_paths.len()!=1{
-		println!("WARNING: multiple texture paths");
-	}
-	let skin=model.skin_tables().nth(0).unwrap();
-
-	let mut spam_pos=Vec::with_capacity(model.vertices().len());
-	let mut spam_normal=Vec::with_capacity(model.vertices().len());
-	let mut spam_tex=Vec::with_capacity(model.vertices().len());
-	let mut spam_vertices=Vec::with_capacity(model.vertices().len());
-	for (i,vertex) in model.vertices().iter().enumerate(){
-		spam_pos.push(valve_transform(vertex.position.into()));
-		spam_normal.push(valve_transform(vertex.normal.into()));
-		spam_tex.push(glam::Vec2::from_array(vertex.texture_coordinates));
-		spam_vertices.push(model::IndexedVertex{
-			pos:model::PositionId::new(i as u32),
-			tex:model::TextureCoordinateId::new(i as u32),
-			normal:model::NormalId::new(i as u32),
-			color:model::ColorId::new(0),
-		});
-	}
-	let mut graphics_groups=Vec::new();
-	let mut physics_groups=Vec::new();
-	let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{
-		let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
-
-		let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){
-			let mut path=std::path::PathBuf::from(texture_path.as_str());
-			path.push(texture_name);
-			acquire_render_config_id(path.as_os_str().to_str())
-		}else{
-			acquire_render_config_id(None)
-		};
-
-		graphics_groups.push(model::IndexedGraphicsGroup{
-			render:render_id,
-			groups:vec![polygon_group_id],
-		});
-		physics_groups.push(model::IndexedPhysicsGroup{
-			groups:vec![polygon_group_id],
-		});
-		model::PolygonGroup::PolygonList(model::PolygonList::new(
-			//looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function
-			mesh.vertex_strip_indices().flat_map(|mut strip|
-				std::iter::from_fn(move||{
-					match (strip.next(),strip.next(),strip.next()){
-						(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3].map(|vertex_id|model::VertexId::new(vertex_id as u32)).to_vec()),
-						//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
-						_=>None,
-					}
-				})
-			).collect()
-		))
-	}).collect();
-	Ok(model::Mesh{
-		unique_pos:spam_pos,
-		unique_normal:spam_normal,
-		unique_tex:spam_tex,
-		unique_color:vec![glam::Vec4::ONE],
-		unique_vertices:spam_vertices,
-		polygon_groups,
-		graphics_groups,
-		physics_groups,
-	})
-}
diff --git a/lib/bsp_loader/src/data.rs b/lib/bsp_loader/src/data.rs
deleted file mode 100644
index 83ca6782e..000000000
--- a/lib/bsp_loader/src/data.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-pub struct Bsp(vbsp::Bsp);
-impl Bsp{
-	pub const fn new(value:vbsp::Bsp)->Self{
-		Self(value)
-	}
-}
-impl AsRef<vbsp::Bsp> for Bsp{
-	fn as_ref(&self)->&vbsp::Bsp{
-		&self.0
-	}
-}
-
-pub struct MdlData(Vec<u8>);
-impl MdlData{
-	pub const fn new(value:Vec<u8>)->Self{
-		Self(value)
-	}
-}
-impl AsRef<[u8]> for MdlData{
-	fn as_ref(&self)->&[u8]{
-		self.0.as_ref()
-	}
-}
-pub struct VtxData(Vec<u8>);
-impl VtxData{
-	pub const fn new(value:Vec<u8>)->Self{
-		Self(value)
-	}
-}
-impl AsRef<[u8]> for VtxData{
-	fn as_ref(&self)->&[u8]{
-		self.0.as_ref()
-	}
-}
-pub struct VvdData(Vec<u8>);
-impl VvdData{
-	pub const fn new(value:Vec<u8>)->Self{
-		Self(value)
-	}
-}
-impl AsRef<[u8]> for VvdData{
-	fn as_ref(&self)->&[u8]{
-		self.0.as_ref()
-	}
-}
-
-pub struct ModelData{
-	pub mdl:MdlData,
-	pub vtx:VtxData,
-	pub vvd:VvdData,
-}
-impl ModelData{
-	pub fn read_model(&self)->Result<vmdl::Model,vmdl::ModelError>{
-		Ok(vmdl::Model::from_parts(
-			vmdl::mdl::Mdl::read(self.mdl.as_ref())?,
-			vmdl::vtx::Vtx::read(self.vtx.as_ref())?,
-			vmdl::vvd::Vvd::read(self.vvd.as_ref())?,
-		))
-	}
-}
\ No newline at end of file
diff --git a/lib/bsp_loader/src/lib.rs b/lib/bsp_loader/src/lib.rs
index e8d4d91d1..21d16f3d8 100644
--- a/lib/bsp_loader/src/lib.rs
+++ b/lib/bsp_loader/src/lib.rs
@@ -1,7 +1,23 @@
 mod bsp;
-pub mod data;
+mod mesh;
+pub mod loader;
 
-pub use data::Bsp;
+pub struct Bsp(vbsp::Bsp);
+impl Bsp{
+	pub const fn new(value:vbsp::Bsp)->Self{
+		Self(value)
+	}
+}
+impl AsRef<vbsp::Bsp> for Bsp{
+	fn as_ref(&self)->&vbsp::Bsp{
+		&self.0
+	}
+}
+
+const VALVE_SCALE:f32=1.0/16.0;
+pub(crate) fn valve_transform([x,y,z]:[f32;3])->strafesnet_common::integer::Planar64Vec3{
+	strafesnet_common::integer::vec3::try_from_f32_array([x*VALVE_SCALE,z*VALVE_SCALE,-y*VALVE_SCALE]).unwrap()
+}
 
 #[derive(Debug)]
 pub enum ReadError{
@@ -24,14 +40,4 @@ pub fn read<R:std::io::Read>(mut input:R)->Result<Bsp,ReadError>{
 	vbsp::Bsp::read(s.as_slice()).map(Bsp::new).map_err(ReadError::Bsp)
 }
 
-pub fn convert<AcquireRenderConfigId,AcquireMeshId>(
-	bsp:&Bsp,
-	acquire_render_config_id:AcquireRenderConfigId,
-	acquire_mesh_id:AcquireMeshId
-)->bsp::PartialMap1
-where
-	AcquireRenderConfigId:FnMut(Option<&str>)->strafesnet_common::model::RenderConfigId,
-	AcquireMeshId:FnMut(&str)->strafesnet_common::model::MeshId,
-{
-	bsp::convert_bsp(bsp.as_ref(),acquire_render_config_id,acquire_mesh_id)
-}
\ No newline at end of file
+pub use bsp::convert;
diff --git a/lib/bsp_loader/src/loader.rs b/lib/bsp_loader/src/loader.rs
new file mode 100644
index 000000000..6f69fe46f
--- /dev/null
+++ b/lib/bsp_loader/src/loader.rs
@@ -0,0 +1,112 @@
+use std::{borrow::Cow, io::Read};
+
+use strafesnet_common::model::Mesh;
+use strafesnet_deferred_loader::{loader::Loader,texture::Texture};
+
+use crate::{mesh::ModelData, Bsp};
+
+#[allow(dead_code)]
+#[derive(Debug)]
+pub enum TextureError{
+	Io(std::io::Error),
+}
+impl std::fmt::Display for TextureError{
+	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
+		write!(f,"{self:?}")
+	}
+}
+impl std::error::Error for TextureError{}
+impl From<std::io::Error> for TextureError{
+	fn from(value:std::io::Error)->Self{
+		Self::Io(value)
+	}
+}
+
+pub struct TextureLoader<'a>(std::marker::PhantomData<&'a ()>);
+impl TextureLoader<'_>{
+	pub fn new()->Self{
+		Self(std::marker::PhantomData)
+	}
+}
+impl<'a> Loader for TextureLoader<'a>{
+	type Error=TextureError;
+	type Index=Cow<'a,str>;
+	type Resource=Texture;
+	fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
+		let file_name=format!("textures/{}.dds",index);
+		let mut file=std::fs::File::open(file_name)?;
+		let mut data=Vec::new();
+		file.read_to_end(&mut data)?;
+		Ok(Texture::ImageDDS(data))
+	}
+}
+
+#[allow(dead_code)]
+#[derive(Debug)]
+pub enum MeshError{
+	Io(std::io::Error),
+	VMDL(vmdl::ModelError),
+	VBSP(vbsp::BspError),
+	MissingMdl,
+	MissingVtx,
+	MissingVvd,
+}
+impl std::fmt::Display for MeshError{
+	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
+		write!(f,"{self:?}")
+	}
+}
+impl std::error::Error for MeshError{}
+impl From<std::io::Error> for MeshError{
+	fn from(value:std::io::Error)->Self{
+		Self::Io(value)
+	}
+}
+impl From<vmdl::ModelError> for MeshError{
+	fn from(value:vmdl::ModelError)->Self{
+		Self::VMDL(value)
+	}
+}
+impl From<vbsp::BspError> for MeshError{
+	fn from(value:vbsp::BspError)->Self{
+		Self::VBSP(value)
+	}
+}
+
+pub struct MeshLoader<'a,'b>{
+	bsp:&'a Bsp,
+	deferred_loader:&'b mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
+}
+impl MeshLoader<'_,'_>{
+	pub fn new<'a,'b>(
+		bsp:&'a Bsp,
+		deferred_loader:&'b mut strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader<Cow<'a,str>>,
+	)->MeshLoader<'a,'b>{
+		MeshLoader{
+			bsp,
+			deferred_loader,
+		}
+	}
+}
+impl<'a> Loader for MeshLoader<'a,'_>{
+	type Error=MeshError;
+	type Index=&'a str;
+	type Resource=Mesh;
+	fn load(&mut self,index:Self::Index)->Result<Self::Resource,Self::Error>{
+		let mdl_path_lower=index.to_lowercase();
+		//.mdl, .vvd, .dx90.vtx
+		let path=std::path::PathBuf::from(mdl_path_lower.as_str());
+		let mut vvd_path=path.clone();
+		let mut vtx_path=path;
+		vvd_path.set_extension("vvd");
+		vtx_path.set_extension("dx90.vtx");
+		// TODO: search more packs, possibly using an index of multiple packs
+		let bsp=self.bsp.as_ref();
+		let mdl=bsp.pack.get(mdl_path_lower.as_str())?.ok_or(MeshError::MissingMdl)?;
+		let vtx=bsp.pack.get(vtx_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVtx)?;
+		let vvd=bsp.pack.get(vvd_path.as_os_str().to_str().unwrap())?.ok_or(MeshError::MissingVvd)?;
+		let model=ModelData{mdl,vtx,vvd};
+		let mesh=model.convert_mesh(&mut self.deferred_loader)?;
+		Ok(mesh)
+	}
+}
diff --git a/lib/bsp_loader/src/mesh.rs b/lib/bsp_loader/src/mesh.rs
new file mode 100644
index 000000000..d6a3339b4
--- /dev/null
+++ b/lib/bsp_loader/src/mesh.rs
@@ -0,0 +1,89 @@
+use std::borrow::Cow;
+
+use strafesnet_common::model;
+use strafesnet_deferred_loader::deferred_loader::RenderConfigDeferredLoader;
+
+use crate::valve_transform;
+
+pub struct ModelData{
+	pub mdl:Vec<u8>,
+	pub vtx:Vec<u8>,
+	pub vvd:Vec<u8>,
+}
+impl ModelData{
+	fn read_model(&self)->Result<vmdl::Model,vmdl::ModelError>{
+		Ok(vmdl::Model::from_parts(
+			vmdl::mdl::Mdl::read(self.mdl.as_ref())?,
+			vmdl::vtx::Vtx::read(self.vtx.as_ref())?,
+			vmdl::vvd::Vvd::read(self.vvd.as_ref())?,
+		))
+	}
+	pub fn convert_mesh<'a>(self,deferred_loader:&mut RenderConfigDeferredLoader<Cow<'a,str>>)->Result<model::Mesh,vmdl::ModelError>{
+		let model=self.read_model()?;
+		let texture_paths=model.texture_directories();
+		if texture_paths.len()!=1{
+			println!("WARNING: multiple texture paths");
+		}
+		let skin=model.skin_tables().nth(0).unwrap();
+
+		let mut spam_pos=Vec::with_capacity(model.vertices().len());
+		let mut spam_normal=Vec::with_capacity(model.vertices().len());
+		let mut spam_tex=Vec::with_capacity(model.vertices().len());
+		let mut spam_vertices=Vec::with_capacity(model.vertices().len());
+		for (i,vertex) in model.vertices().iter().enumerate(){
+			spam_pos.push(valve_transform(vertex.position.into()));
+			spam_normal.push(valve_transform(vertex.normal.into()));
+			spam_tex.push(glam::Vec2::from_array(vertex.texture_coordinates));
+			spam_vertices.push(model::IndexedVertex{
+				pos:model::PositionId::new(i as u32),
+				tex:model::TextureCoordinateId::new(i as u32),
+				normal:model::NormalId::new(i as u32),
+				color:model::ColorId::new(0),
+			});
+		}
+		let mut graphics_groups=Vec::new();
+		let mut physics_groups=Vec::new();
+		let polygon_groups=model.meshes().enumerate().map(|(polygon_group_id,mesh)|{
+			let polygon_group_id=model::PolygonGroupId::new(polygon_group_id as u32);
+
+			let render_id=if let (Some(texture_path),Some(texture_name))=(texture_paths.get(0),skin.texture(mesh.material_index())){
+				let mut path=std::path::PathBuf::from(texture_path.as_str());
+				path.push(texture_name);
+				let index=path.as_os_str().to_str().map(|s|Cow::Owned(s.to_owned()));
+				deferred_loader.acquire_render_config_id(index)
+			}else{
+				deferred_loader.acquire_render_config_id(None)
+			};
+
+			graphics_groups.push(model::IndexedGraphicsGroup{
+				render:render_id,
+				groups:vec![polygon_group_id],
+			});
+			physics_groups.push(model::IndexedPhysicsGroup{
+				groups:vec![polygon_group_id],
+			});
+			model::PolygonGroup::PolygonList(model::PolygonList::new(
+				//looking at the code, it would seem that the strips are pre-deindexed into triangle lists when calling this function
+				mesh.vertex_strip_indices().flat_map(|mut strip|
+					std::iter::from_fn(move||{
+						match (strip.next(),strip.next(),strip.next()){
+							(Some(v1),Some(v2),Some(v3))=>Some([v1,v2,v3].map(|vertex_id|model::VertexId::new(vertex_id as u32)).to_vec()),
+							//ignore extra vertices, not sure what to do in this case, failing the whole conversion could be appropriate
+							_=>None,
+						}
+					})
+				).collect()
+			))
+		}).collect();
+		Ok(model::Mesh{
+			unique_pos:spam_pos,
+			unique_normal:spam_normal,
+			unique_tex:spam_tex,
+			unique_color:vec![glam::Vec4::ONE],
+			unique_vertices:spam_vertices,
+			polygon_groups,
+			graphics_groups,
+			physics_groups,
+		})
+	}
+}