diff --git a/src/roblox.rs b/src/roblox.rs
index e43a11e..da2c0d1 100644
--- a/src/roblox.rs
+++ b/src/roblox.rs
@@ -241,33 +241,16 @@ async fn download_retry(stats:&mut Stats,context:&rbx_asset::cookie::CookieConte
 		}
 	}
 }
-#[allow(unused)]
-#[derive(Debug)]
+#[derive(Debug,thiserror::Error)]
 enum ConvertTextureError{
-	Io(std::io::Error),
-	Image(image::ImageError),
-	DDS(image_dds::CreateDdsError),
-	DDSWrite(image_dds::ddsfile::Error),
-}
-impl From<std::io::Error> for ConvertTextureError{
-	fn from(value:std::io::Error)->Self{
-		Self::Io(value)
-	}
-}
-impl From<image::ImageError> for ConvertTextureError{
-	fn from(value:image::ImageError)->Self{
-		Self::Image(value)
-	}
-}
-impl From<image_dds::CreateDdsError> for ConvertTextureError{
-	fn from(value:image_dds::CreateDdsError)->Self{
-		Self::DDS(value)
-	}
-}
-impl From<image_dds::ddsfile::Error> for ConvertTextureError{
-	fn from(value:image_dds::ddsfile::Error)->Self{
-		Self::DDSWrite(value)
-	}
+	#[error("Io error {0:?}")]
+	Io(#[from]std::io::Error),
+	#[error("Image error {0:?}")]
+	Image(#[from]image::ImageError),
+	#[error("DDS create error {0:?}")]
+	DDS(#[from]image_dds::CreateDdsError),
+	#[error("DDS write error {0:?}")]
+	DDSWrite(#[from]image_dds::ddsfile::Error),
 }
 async fn convert_texture(asset_id:RobloxAssetId,download_result:DownloadResult)->Result<(),ConvertTextureError>{
 	let data=match download_result{
diff --git a/src/source.rs b/src/source.rs
index 95cad8f..cebda5f 100644
--- a/src/source.rs
+++ b/src/source.rs
@@ -1,7 +1,10 @@
 use std::path::{Path,PathBuf};
+use std::borrow::Cow;
 use clap::{Args,Subcommand};
 use anyhow::Result as AResult;
-use strafesnet_deferred_loader::deferred_loader::LoadFailureMode;
+use futures::StreamExt;
+use strafesnet_deferred_loader::loader::Loader;
+use strafesnet_deferred_loader::deferred_loader::{LoadFailureMode,MeshDeferredLoader,RenderConfigDeferredLoader};
 
 #[derive(Subcommand)]
 pub enum Commands{
@@ -19,11 +22,11 @@ pub struct SourceToSNFSubcommand {
 	input_files:Vec<PathBuf>,
 }
 #[derive(Args)]
-pub struct ExtractTexturesSubcommand {
+pub struct ExtractTexturesSubcommand{
+	#[arg(required=true)]
+	bsp_files:Vec<PathBuf>,
 	#[arg(long)]
-	bsp_file:PathBuf,
-	#[arg(long)]
-	vpk_dir_files:Vec<PathBuf>
+	vpk_dir_file:Vec<PathBuf>,
 }
 #[derive(Args)]
 pub struct VPKContentsSubcommand {
@@ -39,8 +42,8 @@ pub struct BSPContentsSubcommand {
 impl Commands{
 	pub async fn run(self)->AResult<()>{
 		match self{
-			Commands::ExtractTextures(subcommand)=>extract_textures(vec![subcommand.bsp_file],subcommand.vpk_dir_files),
 			Commands::SourceToSNF(subcommand)=>source_to_snf(subcommand.input_files,subcommand.output_folder).await,
+			Commands::ExtractTextures(subcommand)=>extract_textures(subcommand.bsp_files,subcommand.vpk_dir_file).await,
 			Commands::VPKContents(subcommand)=>vpk_contents(subcommand.input_file),
 			Commands::BSPContents(subcommand)=>bsp_contents(subcommand.input_file),
 		}
@@ -121,139 +124,220 @@ fn recursive_vmt_loader<F:Fn(String)->AResult<Option<Vec<u8>>>>(find_stuff:&F,ma
 		VMTContent::Unresolved=>{println!("Unresolved vmt");Ok(None)},
 	}
 }
-
-fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
-	std::fs::create_dir_all("textures")?;
-	let vpk_list:Vec<vpk::VPK>=vpk_paths.into_iter().map(|vpk_path|vpk::VPK::read(&vpk_path).expect("vpk file does not exist")).collect();
-	for path in paths{
-		let mut deduplicate=std::collections::HashSet::new();
-		let bsp=vbsp::Bsp::read(std::fs::read(path)?.as_ref())?;
-		for texture in bsp.textures(){
-			deduplicate.insert(PathBuf::from(texture.name()));
-		}
-		//dedupe prop models
-		let mut model_dedupe=std::collections::HashSet::new();
-		for prop in bsp.static_props(){
-			model_dedupe.insert(prop.model());
-		}
-
-		//grab texture names from props
-		for model_name in model_dedupe{
-			//.mdl, .vvd, .dx90.vtx
-			let mut path=PathBuf::from(model_name);
-			let file_name=PathBuf::from(path.file_stem().unwrap());
-			path.pop();
-			path.push(file_name);
-			let mut vvd_path=path.clone();
-			let mut vtx_path=path.clone();
-			vvd_path.set_extension("vvd");
-			vtx_path.set_extension("dx90.vtx");
-			match (bsp.pack.get(model_name),bsp.pack.get(vvd_path.as_os_str().to_str().unwrap()),bsp.pack.get(vtx_path.as_os_str().to_str().unwrap())){
-				(Ok(Some(mdl_file)),Ok(Some(vvd_file)),Ok(Some(vtx_file)))=>{
-					match (vmdl::mdl::Mdl::read(mdl_file.as_ref()),vmdl::vvd::Vvd::read(vvd_file.as_ref()),vmdl::vtx::Vtx::read(vtx_file.as_ref())){
-						(Ok(mdl),Ok(vvd),Ok(vtx))=>{
-							let model=vmdl::Model::from_parts(mdl,vtx,vvd);
-							for texture in model.textures(){
-								for search_path in &texture.search_paths{
-									let mut path=PathBuf::from(search_path.as_str());
-									path.push(texture.name.as_str());
-									deduplicate.insert(path);
-								}
-							}
-						},
-						_=>println!("model_name={} error",model_name),
-					}
-				},
-				_=>println!("no model name={}",model_name),
-			}
-		}
-
-		let pack=&bsp.pack;
-		let vpk_list=&vpk_list;
-		std::thread::scope(move|s|{
-			let mut thread_handles=Vec::new();
-			for texture_name in deduplicate{
-				let mut found_texture=false;
-				//LMAO imagine having to write type names
-				let write_image=|mut stuff,write_file_name|{
-					let image=vtf::from_bytes(&mut stuff)?.highres_image.decode(0)?.to_rgba8();
-
-					let format=if image.width()%4!=0||image.height()%4!=0{
-						image_dds::ImageFormat::Rgba8UnormSrgb
-					}else{
-						image_dds::ImageFormat::BC7RgbaUnormSrgb
-					};
-					//this fails if the image dimensions are not a multiple of 4
-					let dds = image_dds::dds_from_image(
-						&image,
-						format,
-						image_dds::Quality::Slow,
-						image_dds::Mipmaps::GeneratedAutomatic,
-					)?;
-
-					//write dds
-					let mut dest=PathBuf::from("textures");
-					dest.push(write_file_name);
-					dest.set_extension("dds");
-					std::fs::create_dir_all(dest.parent().unwrap())?;
-					let mut writer = std::io::BufWriter::new(std::fs::File::create(dest)?);
-					dds.write(&mut writer)?;
-					Ok::<(),anyhow::Error>(())
-				};
-				let find_stuff=|search_file_name:String|{
-					println!("search_file_name={}",search_file_name);
-					match pack.get(search_file_name.as_str())?{
-						Some(file)=>return Ok(Some(file)),
-						_=>(),
-					}
-					//search pak list
-					for vpk_index in vpk_list{
-						if let Some(vpk_entry)=vpk_index.tree.get(search_file_name.as_str()){
-							return Ok(Some(match vpk_entry.get()?{
-								std::borrow::Cow::Borrowed(bytes)=>bytes.to_vec(),
-								std::borrow::Cow::Owned(bytes)=>bytes,
-							}));
-						}
-					}
-					Ok::<Option<Vec<u8>>,anyhow::Error>(None)
-				};
-				let loader=|texture_name:String|{
-					let mut texture_file_name=PathBuf::from("materials");
-					//lower case
-					let texture_file_name_lowercase=texture_name.to_lowercase();
-					texture_file_name.push(texture_file_name_lowercase.clone());
-					//remove stem and search for both vtf and vmt files
-					let stem=PathBuf::from(texture_file_name.file_stem().unwrap());
-					texture_file_name.pop();
-					texture_file_name.push(stem);
-					//somehow search for both files
-					let mut texture_file_name_vmt=texture_file_name.clone();
-					texture_file_name.set_extension("vtf");
-					texture_file_name_vmt.set_extension("vmt");
-					if let Some(stuff)=find_stuff(texture_file_name.to_string_lossy().to_string())?{
-						return Ok(Some(stuff))
-					}
-					recursive_vmt_loader(&find_stuff,get_vmt(&find_stuff,texture_file_name_vmt.to_string_lossy().to_string())?)
-				};
-				if let Some(stuff)=loader(texture_name.to_string_lossy().to_string())?{
-					found_texture=true;
-					let texture_name=texture_name.clone();
-					thread_handles.push(s.spawn(move||write_image(stuff,texture_name)));
-				}
-				if !found_texture{
-					println!("no data");
-				}
-			}
-			for thread in thread_handles{
-				match thread.join(){
-					Ok(Err(e))=>println!("write error: {:?}",e),
-					Err(e)=>println!("thread error: {:?}",e),
-					Ok(_)=>(),
-				}
-			}
-			Ok::<(),anyhow::Error>(())
-		})?
+#[derive(Debug,thiserror::Error)]
+enum ExtractTextureError{
+	#[error("Io error {0:?}")]
+	Io(#[from]std::io::Error),
+	#[error("Bsp error {0:?}")]
+	Bsp(#[from]vbsp::BspError),
+	#[error("MeshLoad error {0:?}")]
+	MeshLoad(#[from]strafesnet_bsp_loader::loader::MeshError),
+}
+fn find_stuff(){
+	println!("search_file_name={}",search_file_name);
+	match pack.get(search_file_name.as_str())?{
+		Some(file)=>return Ok(Some(file)),
+		_=>(),
 	}
+	//search pak list
+	for vpk_index in vpk_list{
+		if let Some(vpk_entry)=vpk_index.tree.get(search_file_name.as_str()){
+			return Ok(Some(match vpk_entry.get()?{
+				std::borrow::Cow::Borrowed(bytes)=>bytes.to_vec(),
+				std::borrow::Cow::Owned(bytes)=>bytes,
+			}));
+		}
+	}
+	Ok::<Option<Vec<u8>>,anyhow::Error>(None)
+}
+fn load_texture(){
+	let mut texture_file_name=PathBuf::from("materials");
+	//lower case
+	let texture_file_name_lowercase=texture_name.to_lowercase();
+	texture_file_name.push(texture_file_name_lowercase.clone());
+	//remove stem and search for both vtf and vmt files
+	let stem=PathBuf::from(texture_file_name.file_stem().unwrap());
+	texture_file_name.pop();
+	texture_file_name.push(stem);
+	//somehow search for both files
+	let mut texture_file_name_vmt=texture_file_name.clone();
+	texture_file_name.set_extension("vtf");
+	texture_file_name_vmt.set_extension("vmt");
+	if let Some(stuff)=find_stuff(texture_file_name.to_string_lossy().to_string())?{
+		return Ok(Some(stuff))
+	}
+	recursive_vmt_loader(&find_stuff,get_vmt(&find_stuff,texture_file_name_vmt.to_string_lossy().to_string())?)
+}
+async fn gimme_them_textures(path:PathBuf)->Result<(),ExtractTextureError>{
+	let mut deduplicate=std::collections::HashSet::new();
+	let bsp=vbsp::Bsp::read(tokio::fs::read(path).await?.as_ref())?;
+
+	let mut texture_deferred_loader=RenderConfigDeferredLoader::new();
+	for texture in bsp.textures(){
+		texture_deferred_loader.acquire_render_config_id(Some(Cow::Borrowed(texture.name())));
+	}
+
+	let mut mesh_deferred_loader=MeshDeferredLoader::new();
+	for prop in bsp.static_props(){
+		mesh_deferred_loader.acquire_mesh_id(prop.model());
+	}
+
+	// TODO: include more packs in mesh loader
+	let mut mesh_loader=strafesnet_bsp_loader::loader::MeshLoader::new(&strafesnet_bsp_loader::Bsp::new(bsp),&mut texture_deferred_loader);
+
+	// load models and collect requested textures
+	for &model_path in mesh_deferred_loader.indices(){
+		// TODO: fix loader to return pre-converted mesh
+		let model:vmdl::Model=mesh_loader.load(model_path)?;
+		for texture in model.textures(){
+			for search_path in &texture.search_paths{
+				let mut path=PathBuf::from(search_path.as_str());
+				path.push(texture.name.as_str());
+				let path=path.to_str().unwrap().to_owned();
+				texture_deferred_loader.acquire_render_config_id(Some(Cow::Owned(path)));
+			}
+		}
+	}
+
+	for &texture_path in texture_deferred_loader.indices(){
+		if let Some(texture)=load_texture(texture_path)?{
+			send_texture.send(texture).unwrap();
+		}
+	}
+
+	let pack=&bsp.pack;
+	let vpk_list=&vpk_list;
+	std::thread::scope(move|s|{
+		let mut thread_handles=Vec::new();
+		for texture_name in deduplicate{
+			let mut found_texture=false;
+			//LMAO imagine having to write type names
+			let write_image=|mut stuff,write_file_name|{
+				let image=vtf::from_bytes(&mut stuff)?.highres_image.decode(0)?.to_rgba8();
+
+				let format=if image.width()%4!=0||image.height()%4!=0{
+					image_dds::ImageFormat::Rgba8UnormSrgb
+				}else{
+					image_dds::ImageFormat::BC7RgbaUnormSrgb
+				};
+				//this fails if the image dimensions are not a multiple of 4
+				let dds = image_dds::dds_from_image(
+					&image,
+					format,
+					image_dds::Quality::Slow,
+					image_dds::Mipmaps::GeneratedAutomatic,
+				)?;
+
+				//write dds
+				let mut dest=PathBuf::from("textures");
+				dest.push(write_file_name);
+				dest.set_extension("dds");
+				std::fs::create_dir_all(dest.parent().unwrap())?;
+				let mut writer = std::io::BufWriter::new(std::fs::File::create(dest)?);
+				dds.write(&mut writer)?;
+				Ok::<(),anyhow::Error>(())
+			};
+			let find_stuff=|search_file_name:String|{
+				println!("search_file_name={}",search_file_name);
+				match pack.get(search_file_name.as_str())?{
+					Some(file)=>return Ok(Some(file)),
+					_=>(),
+				}
+				//search pak list
+				for vpk_index in vpk_list{
+					if let Some(vpk_entry)=vpk_index.tree.get(search_file_name.as_str()){
+						return Ok(Some(match vpk_entry.get()?{
+							std::borrow::Cow::Borrowed(bytes)=>bytes.to_vec(),
+							std::borrow::Cow::Owned(bytes)=>bytes,
+						}));
+					}
+				}
+				Ok::<Option<Vec<u8>>,anyhow::Error>(None)
+			};
+			let loader=|texture_name:String|{
+				let mut texture_file_name=PathBuf::from("materials");
+				//lower case
+				let texture_file_name_lowercase=texture_name.to_lowercase();
+				texture_file_name.push(texture_file_name_lowercase.clone());
+				//remove stem and search for both vtf and vmt files
+				let stem=PathBuf::from(texture_file_name.file_stem().unwrap());
+				texture_file_name.pop();
+				texture_file_name.push(stem);
+				//somehow search for both files
+				let mut texture_file_name_vmt=texture_file_name.clone();
+				texture_file_name.set_extension("vtf");
+				texture_file_name_vmt.set_extension("vmt");
+				if let Some(stuff)=find_stuff(texture_file_name.to_string_lossy().to_string())?{
+					return Ok(Some(stuff))
+				}
+				recursive_vmt_loader(&find_stuff,get_vmt(&find_stuff,texture_file_name_vmt.to_string_lossy().to_string())?)
+			};
+			if let Some(stuff)=loader(texture_name.to_string_lossy().to_string())?{
+				found_texture=true;
+				let texture_name=texture_name.clone();
+				thread_handles.push(s.spawn(move||write_image(stuff,texture_name)));
+			}
+			if !found_texture{
+				println!("no data");
+			}
+		}
+		for thread in thread_handles{
+			match thread.join(){
+				Ok(Err(e))=>println!("write error: {:?}",e),
+				Err(e)=>println!("thread error: {:?}",e),
+				Ok(_)=>(),
+			}
+		}
+		Ok::<(),anyhow::Error>(())
+	})?
+	Ok(())
+}
+
+async fn extract_textures(paths:Vec<PathBuf>,vpk_paths:Vec<PathBuf>)->AResult<()>{
+	tokio::try_join!(
+		tokio::fs::create_dir_all("extracted_textures"),
+		tokio::fs::create_dir_all("textures"),
+		tokio::fs::create_dir_all("meshes"),
+	)?;
+	let thread_limit=std::thread::available_parallelism()?.get();
+
+	// load vpk list
+	let vpk_list=futures::stream::iter(vpk_paths).map(|vpk_path|async{
+		// idk why it doesn't want to pass out the errors but this is fatal anyways
+		tokio::task::spawn_blocking(move||vpk::VPK::read(&vpk_path)).await.unwrap().unwrap()
+	})
+	.buffer_unordered(thread_limit)
+	.collect::<Vec<vpk::VPK>>().await;
+
+	// leak vpk_list for static lifetime?
+
+	let (send_texture,mut recv_texture)=tokio::sync::mpsc::channel(thread_limit);
+	let mut it=paths.into_iter();
+	let extract_thread=tokio::spawn(async move{
+		static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
+		SEM.add_permits(thread_limit);
+		while let (Ok(permit),Some(path))=(SEM.acquire().await,it.next()){
+			let send=send_texture.clone();
+			tokio::spawn(async move{
+				let result=gimme_them_textures(path,send).await;
+				drop(permit);
+				result.unwrap();
+			});
+		}
+	});
+
+	// convert images
+	static SEM:tokio::sync::Semaphore=tokio::sync::Semaphore::const_new(0);
+	SEM.add_permits(thread_limit);
+	while let (Ok(permit),Some(data))=(SEM.acquire().await,recv_texture.recv().await){
+		tokio::spawn(async move{
+			let result=convert_texture(data).await;
+			drop(permit);
+			result.unwrap();
+		});
+	}
+	extract_thread.await??;
+	SEM.acquire_many(thread_limit as u32).await?;
 	Ok(())
 }