diff --git a/Cargo.lock b/Cargo.lock
index 44c9085..3f8d249 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1838,7 +1838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
 dependencies = [
  "cfg-if",
- "windows-targets 0.52.6",
+ "windows-targets 0.48.5",
 ]
 
 [[package]]
@@ -3732,6 +3732,7 @@ dependencies = [
  "id",
  "linear_ops",
  "ratio_ops",
+ "vbsp",
 ]
 
 [[package]]
diff --git a/engine/graphics/src/graphics.rs b/engine/graphics/src/graphics.rs
index a7340df..cb7f3a7 100644
--- a/engine/graphics/src/graphics.rs
+++ b/engine/graphics/src/graphics.rs
@@ -5,7 +5,7 @@ use strafesnet_settings::settings;
 use strafesnet_session::session;
 use strafesnet_common::model::{self, ColorId, NormalId, PolygonIter, PositionId, RenderConfigId, TextureCoordinateId, VertexId};
 use wgpu::{util::DeviceExt,AstcBlock,AstcChannel};
-use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex};
+use crate::model::{self as model_graphics,IndexedGraphicsMeshOwnedRenderConfig,IndexedGraphicsMeshOwnedRenderConfigId,GraphicsMeshOwnedRenderConfig,GraphicsModelColor4,GraphicsModelOwned,GraphicsVertex,DebugGraphicsVertex};
 
 pub fn required_limits()->wgpu::Limits{
 	wgpu::Limits::default()
@@ -36,12 +36,22 @@ struct GraphicsModel{
 	instance_count:u32,
 }
 
+struct DebugGraphicsMesh{
+	indices:Indices,
+	vertex_buf:wgpu::Buffer,
+}
+struct DebugGraphicsModel{
+	debug_mesh_id:u32,
+	bind_group:wgpu::BindGroup,
+}
+
 struct GraphicsSamplers{
 	repeat:wgpu::Sampler,
 }
 
 struct GraphicsBindGroupLayouts{
 	model:wgpu::BindGroupLayout,
+	debug_model:wgpu::BindGroupLayout,
 }
 
 struct GraphicsBindGroups{
@@ -52,6 +62,7 @@ struct GraphicsBindGroups{
 struct GraphicsPipelines{
 	skybox:wgpu::RenderPipeline,
 	model:wgpu::RenderPipeline,
+	debug:wgpu::RenderPipeline,
 }
 
 struct GraphicsCamera{
@@ -112,6 +123,8 @@ pub struct GraphicsState{
 	camera_buf:wgpu::Buffer,
 	temp_squid_texture_view:wgpu::TextureView,
 	models:Vec<GraphicsModel>,
+	debug_meshes:Vec<DebugGraphicsMesh>,
+	debug_models:Vec<DebugGraphicsModel>,
 	depth_view:wgpu::TextureView,
 	staging_belt:wgpu::util::StagingBelt,
 }
@@ -146,6 +159,76 @@ impl GraphicsState{
 		self.camera.fov=user_settings.calculate_fov(1.0,&self.camera.screen_size).as_vec2();
 	}
 	pub fn generate_models(&mut self,device:&wgpu::Device,queue:&wgpu::Queue,map:&map::CompleteMap){
+		//generate debug meshes, each debug model refers to one
+		self.debug_meshes=map.meshes.iter().map(|mesh|{
+			let vertices:Vec<DebugGraphicsVertex>=mesh.unique_vertices.iter().map(|vertex|{
+				DebugGraphicsVertex{
+					pos:mesh.unique_pos[vertex.pos.get() as usize].to_array().map(Into::into),
+					normal:mesh.unique_normal[vertex.normal.get() as usize].to_array().map(Into::into),
+				}
+			}).collect();
+			let vertex_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
+				label:Some("Vertex"),
+				contents:bytemuck::cast_slice(&vertices),
+				usage:wgpu::BufferUsages::VERTEX,
+			});
+
+			let mut indices=Vec::new();
+			for physics_group in &mesh.physics_groups{
+				for polygon_group_id in &physics_group.groups{
+					for poly in mesh.polygon_groups[polygon_group_id.get() as usize].polys(){
+						// triangulate
+						let mut poly_vertices=poly.into_iter().copied();
+						if let (Some(a),Some(mut b))=(poly_vertices.next(),poly_vertices.next()){
+							for c in poly_vertices{
+								indices.extend([a,b,c]);
+								b=c;
+							}
+						}
+					}
+				}
+			}
+
+			DebugGraphicsMesh{
+				indices:if (u32::MAX as usize)<vertices.len(){
+					panic!("Model has too many vertices!")
+				}else if (u16::MAX as usize)<vertices.len(){
+					Indices::new(device,&indices.into_iter().map(|vertex_idx|vertex_idx.get() as u32).collect(),wgpu::IndexFormat::Uint32)
+				}else{
+					Indices::new(device,&indices.into_iter().map(|vertex_idx|vertex_idx.get() as u16).collect(),wgpu::IndexFormat::Uint16)
+				},
+				vertex_buf,
+			}
+		}).collect();
+
+		//generate debug models, only one will be rendered at a time
+		self.debug_models=map.models.iter().enumerate().map(|(model_id,model)|{
+			let model_uniforms=get_instance_buffer_data(&GraphicsModelOwned{
+				transform:model.transform.into(),
+				normal_transform:glam::Mat3::from_cols_array_2d(&model.transform.matrix3.to_array().map(|row|row.map(Into::into))).inverse().transpose(),
+				color:GraphicsModelColor4::new(glam::vec4(1.0,0.0,0.0,0.2)),
+			});
+			let model_buf=device.create_buffer_init(&wgpu::util::BufferInitDescriptor{
+				label:Some(format!("Debug Model{} Buf",model_id).as_str()),
+				contents:bytemuck::cast_slice(&model_uniforms),
+				usage:wgpu::BufferUsages::UNIFORM|wgpu::BufferUsages::COPY_DST,
+			});
+			let bind_group=device.create_bind_group(&wgpu::BindGroupDescriptor{
+				layout:&self.bind_group_layouts.debug_model,
+				entries:&[
+					wgpu::BindGroupEntry{
+						binding:0,
+						resource:model_buf.as_entire_binding(),
+					},
+				],
+				label:Some(format!("Debug Model{} Bind Group",model_id).as_str()),
+			});
+			DebugGraphicsModel{
+				debug_mesh_id:model.mesh.get(),
+				bind_group,
+			}
+		}).collect();
+
 		//generate texture view per texture
 		let texture_views:HashMap<strafesnet_common::model::TextureId,wgpu::TextureView>=map.textures.iter().enumerate().filter_map(|(texture_id,texture_data)|{
 			let texture_id=model::TextureId::new(texture_id as u32);
@@ -588,6 +671,21 @@ impl GraphicsState{
 				},
 			],
 		});
+		let debug_model_bind_group_layout=device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor{
+			label:Some("Debug Model Bind Group Layout"),
+			entries:&[
+				wgpu::BindGroupLayoutEntry{
+					binding:0,
+					visibility:wgpu::ShaderStages::VERTEX_FRAGMENT,
+					ty:wgpu::BindingType::Buffer{
+						ty:wgpu::BufferBindingType::Uniform,
+						has_dynamic_offset:false,
+						min_binding_size:None,
+					},
+					count:None,
+				},
+			],
+		});
 
 		let clamp_sampler=device.create_sampler(&wgpu::SamplerDescriptor{
 			label:Some("Clamp Sampler"),
@@ -736,6 +834,14 @@ impl GraphicsState{
 			],
 			push_constant_ranges:&[],
 		});
+		let debug_model_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
+				label:None,
+				bind_group_layouts:&[
+					&camera_bind_group_layout,
+					&debug_model_bind_group_layout,
+				],
+				push_constant_ranges:&[],
+			});
 		let sky_pipeline_layout=device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor{
 			label:None,
 			bind_group_layouts:&[
@@ -811,6 +917,45 @@ impl GraphicsState{
 			multiview:None,
 			cache:None,
 		});
+		let debug_model_pipeline=device.create_render_pipeline(&wgpu::RenderPipelineDescriptor{
+			label:Some("Debug Model Pipeline"),
+			layout:Some(&debug_model_pipeline_layout),
+			vertex:wgpu::VertexState{
+				module:&shader,
+				entry_point:Some("vs_debug"),
+				buffers:&[wgpu::VertexBufferLayout{
+					array_stride:std::mem::size_of::<DebugGraphicsVertex>() as wgpu::BufferAddress,
+					step_mode:wgpu::VertexStepMode::Vertex,
+					attributes:&wgpu::vertex_attr_array![0=>Float32x3,1=>Float32x3],
+				}],
+				compilation_options:wgpu::PipelineCompilationOptions::default(),
+			},
+			fragment:Some(wgpu::FragmentState{
+				module:&shader,
+				entry_point:Some("fs_debug"),
+				targets:&[Some(wgpu::ColorTargetState{
+					format:config.view_formats[0],
+					blend:Some(wgpu::BlendState::ALPHA_BLENDING),
+					write_mask:wgpu::ColorWrites::default(),
+				})],
+				compilation_options:wgpu::PipelineCompilationOptions::default(),
+			}),
+			primitive:wgpu::PrimitiveState{
+				front_face:wgpu::FrontFace::Cw,
+				cull_mode:Some(wgpu::Face::Front),
+				..Default::default()
+			},
+			depth_stencil:Some(wgpu::DepthStencilState{
+				format:Self::DEPTH_FORMAT,
+				depth_write_enabled:true,
+				depth_compare:wgpu::CompareFunction::Always,
+				stencil:wgpu::StencilState::default(),
+				bias:wgpu::DepthBiasState::default(),
+			}),
+			multisample:wgpu::MultisampleState::default(),
+			multiview:None,
+			cache:None,
+		});
 
 		let camera=GraphicsCamera::default();
 		let camera_uniforms=camera.to_uniform_data(glam::Vec3::ZERO,glam::Vec2::ZERO);
@@ -850,7 +995,8 @@ impl GraphicsState{
 		Self{
 			pipelines:GraphicsPipelines{
 				skybox:sky_pipeline,
-				model:model_pipeline
+				model:model_pipeline,
+				debug:debug_model_pipeline,
 			},
 			bind_groups:GraphicsBindGroups{
 				camera:camera_bind_group,
@@ -859,9 +1005,14 @@ impl GraphicsState{
 			camera,
 			camera_buf,
 			models:Vec::new(),
+			debug_meshes:Vec::new(),
+			debug_models:Vec::new(),
 			depth_view,
 			staging_belt:wgpu::util::StagingBelt::new(0x100),
-			bind_group_layouts:GraphicsBindGroupLayouts{model:model_bind_group_layout},
+			bind_group_layouts:GraphicsBindGroupLayouts{
+				model:model_bind_group_layout,
+				debug_model:debug_model_bind_group_layout,
+			},
 			samplers:GraphicsSamplers{repeat:repeat_sampler},
 			temp_squid_texture_view:squid_texture_view,
 		}
@@ -949,6 +1100,7 @@ impl GraphicsState{
 			rpass.set_bind_group(0,&self.bind_groups.camera,&[]);
 			rpass.set_bind_group(1,&self.bind_groups.skybox_texture,&[]);
 
+			// Draw all models.
 			rpass.set_pipeline(&self.pipelines.model);
 			for model in &self.models{
 				rpass.set_bind_group(2,&model.bind_group,&[]);
@@ -960,6 +1112,19 @@ impl GraphicsState{
 
 			rpass.set_pipeline(&self.pipelines.skybox);
 			rpass.draw(0..3,0..1);
+
+			// render a single debug_model in red
+			if let Some(model_id)=frame_state.debug_model{
+				if let Some(model)=self.debug_models.get(model_id.get() as usize){
+					let mesh=&self.debug_meshes[model.debug_mesh_id as usize];
+					rpass.set_pipeline(&self.pipelines.debug);
+					rpass.set_bind_group(1,&model.bind_group,&[]);
+					rpass.set_vertex_buffer(0,mesh.vertex_buf.slice(..));
+					rpass.set_index_buffer(mesh.indices.buf.slice(..),mesh.indices.format);
+					//TODO: loop over triangle strips
+					rpass.draw_indexed(0..mesh.indices.count,0,0..1);
+				}
+			}
 		}
 
 		queue.submit(std::iter::once(encoder.finish()));
@@ -968,21 +1133,23 @@ impl GraphicsState{
 	}
 }
 const MODEL_BUFFER_SIZE:usize=4*4 + 12 + 4;//let size=std::mem::size_of::<ModelInstance>();
-const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*4;
+const MODEL_BUFFER_SIZE_BYTES:usize=MODEL_BUFFER_SIZE*core::mem::size_of::<f32>();
+fn get_instance_buffer_data(instance:&GraphicsModelOwned)->[f32;MODEL_BUFFER_SIZE]{
+	let mut out=[0.0;MODEL_BUFFER_SIZE];
+	out[0..16].copy_from_slice(instance.transform.as_ref());
+	out[16..19].copy_from_slice(instance.normal_transform.x_axis.as_ref());
+	// out[20]=0.0;
+	out[20..23].copy_from_slice(instance.normal_transform.y_axis.as_ref());
+	// out[24]=0.0;
+	out[24..27].copy_from_slice(instance.normal_transform.z_axis.as_ref());
+	// out[28]=0.0;
+	out[28..32].copy_from_slice(instance.color.get().as_ref());
+	out
+}
 fn get_instances_buffer_data(instances:&[GraphicsModelOwned])->Vec<f32>{
 	let mut raw=Vec::with_capacity(MODEL_BUFFER_SIZE*instances.len());
 	for mi in instances{
-		//model transform
-		raw.extend_from_slice(&AsRef::<[f32; 4*4]>::as_ref(&mi.transform)[..]);
-		//normal transform
-		raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.x_axis));
-		raw.extend_from_slice(&[0.0]);
-		raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.y_axis));
-		raw.extend_from_slice(&[0.0]);
-		raw.extend_from_slice(AsRef::<[f32; 3]>::as_ref(&mi.normal_transform.z_axis));
-		raw.extend_from_slice(&[0.0]);
-		//color
-		raw.extend_from_slice(AsRef::<[f32; 4]>::as_ref(&mi.color.get()));
+		raw.extend_from_slice(&get_instance_buffer_data(mi));
 	}
 	raw
 }
diff --git a/engine/graphics/src/model.rs b/engine/graphics/src/model.rs
index 2468cda..775b7c6 100644
--- a/engine/graphics/src/model.rs
+++ b/engine/graphics/src/model.rs
@@ -8,6 +8,12 @@ pub struct GraphicsVertex{
 	pub normal:[f32;3],
 	pub color:[f32;4],
 }
+#[derive(Clone,Copy,Pod,Zeroable)]
+#[repr(C)]
+pub struct DebugGraphicsVertex{
+	pub pos:[f32;3],
+	pub normal:[f32;3],
+}
 #[derive(Clone,Copy,id::Id)]
 pub struct IndexedGraphicsMeshOwnedRenderConfigId(u32);
 pub struct IndexedGraphicsMeshOwnedRenderConfig{
diff --git a/engine/physics/src/physics.rs b/engine/physics/src/physics.rs
index 8644cb3..e71a79b 100644
--- a/engine/physics/src/physics.rs
+++ b/engine/physics/src/physics.rs
@@ -1,4 +1,5 @@
 use std::collections::{HashMap,HashSet};
+use crate::model::DirectedEdge;
 use crate::model::{self as model_physics,PhysicsMesh,PhysicsMeshTransform,TransformedMesh,MeshQuery,PhysicsMeshId,PhysicsSubmeshId};
 use strafesnet_common::bvh;
 use strafesnet_common::map;
@@ -280,7 +281,8 @@ impl PhysicsCamera{
 		.clamp(Self::ANGLE_PITCH_LOWER_LIMIT,Self::ANGLE_PITCH_UPPER_LIMIT);
 		mat3::from_rotation_yx(ax,ay)
 	}
-	fn rotation(&self)->Planar64Mat3{
+	#[inline]
+	pub fn rotation(&self)->Planar64Mat3{
 		self.get_rotation(self.clamped_mouse_pos)
 	}
 	fn simulate_move_rotation(&self,mouse_delta:glam::IVec2)->Planar64Mat3{
@@ -931,6 +933,7 @@ pub struct PhysicsData{
 	modes:gameplay_modes::Modes,
 	//cached calculations
 	hitbox_mesh:HitboxMesh,
+	pub le_models:Vec<strafesnet_common::model::Model>,
 }
 impl Default for PhysicsData{
 	fn default()->Self{
@@ -939,6 +942,7 @@ impl Default for PhysicsData{
 			models:Default::default(),
 			modes:Default::default(),
 			hitbox_mesh:StyleModifiers::default().calculate_mesh(),
+			le_models:Default::default(),
 		}
 	}
 }
@@ -980,6 +984,34 @@ impl PhysicsContext<'_>{
 	}
 }
 impl PhysicsData{
+	pub fn trace_ray(&self,ray:strafesnet_common::ray::Ray)->Option<ModelId>{
+		let (_time,convex_mesh_id)=self.bvh.sample_ray(&ray,Time::ZERO,Time::MAX/4,|&model,ray|{
+			let mesh=self.models.mesh(model);
+			// brute force trace every face
+			mesh.faces().filter_map(|face_id|{
+				let (n,d)=mesh.face_nd(face_id);
+				// trace ray onto face
+				// n.(o+d*t)==n.p
+				// n.o + n.d * t == n.p
+				// t == (n.p - n.o)/n.d
+				let nd=n.dot(ray.direction);
+				if nd.is_zero(){
+					return None;
+				}
+				let t=(d-n.dot(ray.origin))/nd;
+				// check if point of intersection is behind face edges
+				// *2 because average of 2 vertices
+				let p=ray.extrapolate(t)*2;
+				mesh.face_edges(face_id).iter().all(|&directed_edge_id|{
+					let edge_n=mesh.directed_edge_n(directed_edge_id);
+					let cross_n=edge_n.cross(n);
+					let &[vert0,vert1]=mesh.edge_verts(directed_edge_id.as_undirected()).as_ref();
+					cross_n.dot(p)<cross_n.dot(mesh.vert(vert0)+mesh.vert(vert1))
+				}).then(||t)
+			}).min().map(Into::into)
+		})?;
+		Some(convex_mesh_id.model_id.into())
+	}
 	/// use with caution, this is the only non-instruction way to mess with physics
 	pub fn generate_models(&mut self,map:&map::CompleteMap){
 		let mut modes=map.modes.clone().denormalize();
@@ -1112,6 +1144,7 @@ impl PhysicsData{
 		self.bvh=bvh;
 		self.models=models;
 		self.modes=modes;
+		self.le_models=map.models.clone();
 		//hitbox_mesh is unchanged
 		println!("Physics Objects: {}",model_count);
 	}
diff --git a/engine/session/src/session.rs b/engine/session/src/session.rs
index e7959b3..b782078 100644
--- a/engine/session/src/session.rs
+++ b/engine/session/src/session.rs
@@ -2,6 +2,7 @@ use std::collections::HashMap;
 
 use strafesnet_common::gameplay_modes::{ModeId,StageId};
 use strafesnet_common::instruction::{InstructionConsumer,InstructionEmitter,InstructionFeedback,TimedInstruction};
+use strafesnet_common::model::ModelId;
 // session represents the non-hardware state of the client.
 // Ideally it is a deterministic state which is atomically updated by instructions, same as the simulation state.
 use strafesnet_common::physics::{
@@ -61,6 +62,7 @@ pub struct FrameState{
 	pub body:physics::Body,
 	pub camera:physics::PhysicsCamera,
 	pub time:PhysicsTime,
+	pub debug_model:Option<strafesnet_common::model::ModelId>,
 }
 
 pub struct Simulation{
@@ -77,11 +79,12 @@ impl Simulation{
 			physics,
 		}
 	}
-	pub fn get_frame_state(&self,time:SessionTime)->FrameState{
+	pub fn get_frame_state(&self,time:SessionTime,debug_model:Option<ModelId>)->FrameState{
 		FrameState{
 			body:self.physics.camera_body(),
 			camera:self.physics.camera(),
 			time:self.timer.time(time),
+			debug_model,
 		}
 	}
 }
@@ -161,6 +164,7 @@ pub struct Session{
 	recording:Recording,
 	//players:HashMap<PlayerId,Simulation>,
 	replays:HashMap<BotId,Replay>,
+	last_ray_hit:Option<strafesnet_common::model::ModelId>,
 }
 impl Session{
 	pub fn new(
@@ -177,6 +181,7 @@ impl Session{
 			view_state:ViewState::Play,
 			recording:Default::default(),
 			replays:HashMap::new(),
+			last_ray_hit:None,
 		}
 	}
 	fn clear_recording(&mut self){
@@ -188,12 +193,30 @@ impl Session{
 	}
 	pub fn get_frame_state(&self,time:SessionTime)->Option<FrameState>{
 		match &self.view_state{
-			ViewState::Play=>Some(self.simulation.get_frame_state(time)),
+			ViewState::Play=>Some(self.simulation.get_frame_state(time,self.last_ray_hit)),
 			ViewState::Replay(bot_id)=>self.replays.get(bot_id).map(|replay|
-				replay.simulation.get_frame_state(time)
+				replay.simulation.get_frame_state(time,None)
 			),
 		}
 	}
+	pub fn debug_raycast_print_model_id_if_changed(&mut self,time:SessionTime){
+		if let Some(frame_state)=self.get_frame_state(time){
+			let ray=strafesnet_common::ray::Ray{
+				origin:frame_state.body.extrapolated_position(self.simulation.timer.time(time)),
+				direction:-frame_state.camera.rotation().z_axis,
+			};
+			let model_id=self.geometry_shared.trace_ray(ray);
+			if model_id!=self.last_ray_hit{
+				println!("hit={model_id:?}");
+				self.last_ray_hit=model_id;
+				if let Some(model_id)=model_id{
+					if let Some(model)=self.geometry_shared.le_models.get(model_id.get() as usize){
+						println!("{}",model.debug_info);
+					}
+				}
+			}
+		}
+	}
 	pub fn user_settings(&self)->&UserSettings{
 		&self.user_settings
 	}
diff --git a/lib/bsp_loader/src/bsp.rs b/lib/bsp_loader/src/bsp.rs
index e27c2c0..2669550 100644
--- a/lib/bsp_loader/src/bsp.rs
+++ b/lib/bsp_loader/src/bsp.rs
@@ -50,6 +50,7 @@ fn add_brush<'a>(
 	origin:vbsp::Vector,
 	rendercolor:vbsp::Color,
 	attributes:attr::CollisionAttributesId,
+	debug_info:model::DebugInfo,
 )->Option<AddBrush>{
 	let transform=integer::Planar64Affine3::from_translation(
 		valve_transform(origin.into())
@@ -67,7 +68,7 @@ fn add_brush<'a>(
 				let mesh=model::MeshId::new(mesh_id);
 				let model_id=model::ModelId::new(world_models.len() as u32);
 				world_models.push(
-					model::Model{mesh,attributes,transform,color}
+					model::Model{mesh,attributes,transform,color,debug_info}
 				);
 				Some(AddBrush::Available(model_id))
 			},
@@ -80,7 +81,7 @@ fn add_brush<'a>(
 			let mesh=mesh_deferred_loader.acquire_mesh_id(model);
 			let model_id=model::ModelId::new(prop_models.len() as u32);
 			prop_models.push(
-				model::Model{mesh,attributes,transform,color}
+				model::Model{mesh,attributes,transform,color,debug_info}
 			);
 			Some(AddBrush::Deferred(model_id))
 		}
@@ -150,6 +151,7 @@ pub fn convert<'a>(
 				valve_transform(prop.origin.into()),
 			),
 			color:glam::Vec4::ONE,
+			debug_info:model::DebugInfo::Prop,
 		}
 	}).collect();
 
@@ -222,6 +224,7 @@ pub fn convert<'a>(
 		attributes:ATTRIBUTE_DECORATION,
 		transform:integer::Planar64Affine3::IDENTITY,
 		color:glam::Vec4::W,
+		debug_info:model::DebugInfo::World,
 	});
 
 	// THE CUBE OF DESTINY
@@ -235,18 +238,25 @@ pub fn convert<'a>(
 	const ENTITY_ATTRIBUTE:gameplay_attributes::CollisionAttributesId=ATTRIBUTE_CONTACT_DEFAULT;
 	const ENTITY_TRIGGER_ATTRIBUTE:gameplay_attributes::CollisionAttributesId=ATTRIBUTE_INTERSECT_DEFAULT;
 	for raw_ent in &bsp.entities{
+		let debug_info=match model::EntityInfo::new(raw_ent.properties()){
+			Ok(entity_info)=>model::DebugInfo::Entity(entity_info),
+			Err(_)=>{
+				println!("EntityInfoError");
+				model::DebugInfo::World
+			},
+		};
 		macro_rules! ent_brush_default{
 			($entity:ident)=>{
-				{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,$entity.model,$entity.origin,$entity.rendercolor,ENTITY_ATTRIBUTE);}
+				{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,$entity.model,$entity.origin,$entity.rendercolor,ENTITY_ATTRIBUTE,debug_info);}
 			};
 		}	macro_rules! ent_brush_prop{
 			($entity:ident)=>{
-				{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,$entity.model,$entity.origin,WHITE,ENTITY_ATTRIBUTE);}
+				{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,$entity.model,$entity.origin,WHITE,ENTITY_ATTRIBUTE,debug_info);}
 			};
 		}
 		macro_rules! ent_brush_trigger{
 			($entity:ident)=>{
-				{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,$entity.model,$entity.origin,WHITE,ENTITY_TRIGGER_ATTRIBUTE);}
+				{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,$entity.model,$entity.origin,WHITE,ENTITY_TRIGGER_ATTRIBUTE,debug_info);}
 			};
 		}
 		match raw_ent.parse(){
@@ -305,7 +315,7 @@ pub fn convert<'a>(
 			Ok(Entity::FuncFishPool(_func_fish_pool))=>(),
 			Ok(Entity::FuncFootstepControl(_func_footstep_control))=>(),
 			Ok(Entity::FuncHostageRescue(_func_hostage_rescue))=>(),
-			Ok(Entity::FuncIllusionary(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION);},
+			Ok(Entity::FuncIllusionary(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor,ATTRIBUTE_DECORATION,debug_info);},
 			Ok(Entity::FuncLod(_func_lod))=>(),
 			Ok(Entity::FuncMonitor(brush))=>ent_brush_default!(brush),
 			Ok(Entity::FuncMovelinear(brush))=>ent_brush_default!(brush),
@@ -318,9 +328,9 @@ pub fn convert<'a>(
 			Ok(Entity::FuncSmokevolume(_func_smokevolume))=>(),
 			Ok(Entity::FuncTracktrain(brush))=>ent_brush_default!(brush),
 			Ok(Entity::FuncTrain(brush))=>ent_brush_default!(brush),
-			Ok(Entity::FuncWall(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ENTITY_ATTRIBUTE);},
-			Ok(Entity::FuncWallToggle(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ENTITY_ATTRIBUTE);},
-			Ok(Entity::FuncWaterAnalog(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor.unwrap_or(WHITE),ENTITY_ATTRIBUTE);},
+			Ok(Entity::FuncWall(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ENTITY_ATTRIBUTE,debug_info);},
+			Ok(Entity::FuncWallToggle(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin.unwrap_or_default(),brush.rendercolor,ENTITY_ATTRIBUTE,debug_info);},
+			Ok(Entity::FuncWaterAnalog(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model,brush.origin,brush.rendercolor.unwrap_or(WHITE),ENTITY_ATTRIBUTE,debug_info);},
 			Ok(Entity::GamePlayerEquip(_game_player_equip))=>(),
 			Ok(Entity::GameText(_game_text))=>(),
 			Ok(Entity::GameUi(_game_ui))=>(),
@@ -399,7 +409,7 @@ pub fn convert<'a>(
 			Ok(Entity::TriggerGravity(brush))=>ent_brush_trigger!(brush),
 			Ok(Entity::TriggerHurt(brush))=>ent_brush_trigger!(brush),
 			Ok(Entity::TriggerLook(brush))=>ent_brush_trigger!(brush),
-			Ok(Entity::TriggerMultiple(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ENTITY_TRIGGER_ATTRIBUTE);},
+			Ok(Entity::TriggerMultiple(brush))=>{add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ENTITY_TRIGGER_ATTRIBUTE,debug_info);},
 			Ok(Entity::TriggerOnce(brush))=>ent_brush_trigger!(brush),
 			Ok(Entity::TriggerProximity(brush))=>ent_brush_trigger!(brush),
 			// TriggerPush is booster
@@ -407,7 +417,7 @@ pub fn convert<'a>(
 			Ok(Entity::TriggerSoundscape(brush))=>ent_brush_trigger!(brush),
 			// TriggerTeleport is Trigger#
 			Ok(Entity::TriggerTeleport(brush))=>{
-				if let (Some(model_id),Some(target))=(add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ENTITY_TRIGGER_ATTRIBUTE),brush.target){
+				if let (Some(model_id),Some(target))=(add_brush(mesh_deferred_loader,&mut world_models,&mut prop_models,brush.model.unwrap_or_default(),brush.origin,WHITE,ENTITY_TRIGGER_ATTRIBUTE,debug_info),brush.target){
 					teleports.insert(model_id,target);
 				}
 			},
@@ -474,6 +484,13 @@ pub fn convert<'a>(
 			Ok(mesh)=>{
 				let mesh_id=model::MeshId::new(world_meshes.len() as u32);
 				world_meshes.push(mesh);
+				let sides={
+					let brush_start_idx=brush.brush_side as usize;
+					let sides_range=brush_start_idx..brush_start_idx+brush.num_brush_sides as usize;
+					bsp.brush_sides[sides_range].iter().filter_map(|side|bsp.texture_info(side.texture_info as usize)).map(|texture_info|{
+						texture_info.flags
+					}).collect()
+				};
 				world_models.push(model::Model{
 					mesh:mesh_id,
 					attributes,
@@ -482,6 +499,7 @@ pub fn convert<'a>(
 						integer::vec3::ZERO,
 					),
 					color:glam::Vec4::ONE,
+					debug_info:model::DebugInfo::Brush(model::BrushInfo{flags:brush.flags,sides}),
 				});
 			},
 			Err(e)=>println!("Brush mesh error: {e}"),
@@ -496,6 +514,7 @@ pub fn convert<'a>(
 			attributes:ATTRIBUTE_INTERSECT_DEFAULT,
 			transform:integer::Planar64Affine3::from_translation(valve_transform(spawn_point.into())),
 			color:glam::Vec4::W,
+			debug_info:model::DebugInfo::World,
 		});
 
 		Stage::empty(model_id)
diff --git a/lib/common/Cargo.toml b/lib/common/Cargo.toml
index c4ed365..0f16aab 100644
--- a/lib/common/Cargo.toml
+++ b/lib/common/Cargo.toml
@@ -17,3 +17,4 @@ linear_ops = { version = "0.1.0", path = "../linear_ops", registry = "strafesnet
 ratio_ops = { version = "0.1.0", path = "../ratio_ops", registry = "strafesnet" }
 glam = "0.30.0"
 id = { version = "0.1.0", registry = "strafesnet" }
+vbsp = "0.8.0"
diff --git a/lib/common/src/model.rs b/lib/common/src/model.rs
index 41d981e..3c6306f 100644
--- a/lib/common/src/model.rs
+++ b/lib/common/src/model.rs
@@ -208,9 +208,87 @@ impl MeshBuilder{
 
 #[derive(Debug,Clone,Copy,Hash,id::Id,Eq,PartialEq)]
 pub struct ModelId(u32);
+#[derive(Clone)]
 pub struct Model{
 	pub mesh:MeshId,
 	pub attributes:gameplay_attributes::CollisionAttributesId,
 	pub color:Color4,//transparency is in here
 	pub transform:Planar64Affine3,
+	pub debug_info:DebugInfo,
+}
+
+#[derive(Debug,Clone)]
+pub enum DebugInfo{
+	World,
+	Prop,
+	Brush(BrushInfo),
+	Entity(EntityInfo),
+}
+impl std::fmt::Display for DebugInfo{
+	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
+		match self{
+			DebugInfo::World=>write!(f,"World"),
+			DebugInfo::Prop=>write!(f,"Prop"),
+			DebugInfo::Brush(brush_info)=>brush_info.fmt(f),
+			DebugInfo::Entity(entity_info)=>entity_info.fmt(f),
+		}
+	}
+}
+
+#[derive(Debug,Clone)]
+pub struct BrushInfo{
+	pub flags:vbsp::BrushFlags,
+	pub sides:Vec<vbsp::TextureFlags>,
+}
+impl std::fmt::Display for BrushInfo{
+	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
+		let noice=self.flags.iter_names().filter_map(|(name,flags)|{
+			(flags.bits()!=0).then(||name)
+		}).collect::<Vec<&str>>().join("|");
+		writeln!(f,"brush_info.flags={noice}")?;
+		for (i,side) in self.sides.iter().enumerate(){
+			let noice_string=side.iter_names().filter_map(|(name,flags)|{
+				(flags.bits()!=0).then(||name)
+			}).collect::<Vec<&str>>().join("|");
+			writeln!(f,"brush_info.sides[{i}]={noice_string}")?;
+		}
+		Ok(())
+	}
+}
+
+#[derive(Debug,Clone)]
+pub struct EntityInfo{
+	pub classname:Box<str>,
+	pub properties:Vec<(Box<str>,Box<str>)>,
+}
+pub enum EntityInfoError{
+	MissingClassname,
+}
+impl EntityInfo{
+	pub fn new<'a>(iter:impl IntoIterator<Item=(&'a str,&'a str)>)->Result<Self,EntityInfoError>{
+		let mut classname:Option<Box<str>>=None;
+		let mut properties:Vec<(Box<str>,Box<str>)>=Vec::new();
+		for (name,value) in iter{
+			match name{
+				"classname"=>classname=Some(value.into()),
+				"hammerid"=>(),
+				_=>properties.push((name.into(),value.into())),
+			}
+		}
+		properties.sort_by(|(n0,_),(n1,_)|n0.cmp(n1));
+		let Some(classname)=classname else{
+			return Err(EntityInfoError::MissingClassname);
+		};
+		Ok(EntityInfo{classname,properties})
+	}
+}
+impl std::fmt::Display for EntityInfo{
+	fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
+		writeln!(f,"struct {}{{",self.classname)?;
+		for (name,value) in &self.properties{
+			writeln!(f,"\t{name}:{value},")?;
+		}
+		write!(f,"}}")?;
+		Ok(())
+	}
 }
diff --git a/lib/snf/src/newtypes/model.rs b/lib/snf/src/newtypes/model.rs
index 6d3ce51..c290d47 100644
--- a/lib/snf/src/newtypes/model.rs
+++ b/lib/snf/src/newtypes/model.rs
@@ -250,6 +250,7 @@ impl Into<strafesnet_common::model::Model> for Model{
 				]),
 				strafesnet_common::integer::vec3::raw_xyz(_9,_a,_b)
 			),
+			debug_info:strafesnet_common::model::DebugInfo::World,
 		}
 	}
 }
diff --git a/strafe-client/src/physics_worker.rs b/strafe-client/src/physics_worker.rs
index 63ce68f..e6c1c92 100644
--- a/strafe-client/src/physics_worker.rs
+++ b/strafe-client/src/physics_worker.rs
@@ -77,5 +77,8 @@ pub fn new<'a>(
 				run_session_instruction!(ins.time,SessionInstruction::LoadReplay(bot));
 			}
 		}
+
+		//whatever just do it
+		session.debug_raycast_print_model_id_if_changed(ins.time);
 	})
 }
diff --git a/strafe-client/src/shader.wgsl b/strafe-client/src/shader.wgsl
index 4298a03..a9bb9b6 100644
--- a/strafe-client/src/shader.wgsl
+++ b/strafe-client/src/shader.wgsl
@@ -86,6 +86,29 @@ fn vs_entity_texture(
 	return result;
 }
 
+@group(1)
+@binding(0)
+var<uniform> model_instance: ModelInstance;
+
+struct DebugEntityOutput {
+	@builtin(position) position: vec4<f32>,
+	@location(1) normal: vec3<f32>,
+	@location(2) view: vec3<f32>,
+};
+
+@vertex
+fn vs_debug(
+	@location(0) pos: vec3<f32>,
+	@location(1) normal: vec3<f32>,
+) -> DebugEntityOutput {
+	var position: vec4<f32> = model_instance.transform * vec4<f32>(pos, 1.0);
+	var result: DebugEntityOutput;
+	result.normal = model_instance.normal_transform * normal;
+	result.view = position.xyz - camera.view_inv[3].xyz;//col(3)
+	result.position = camera.proj * camera.view * position;
+	return result;
+}
+
 //group 2 is the skybox texture
 @group(1)
 @binding(0)
@@ -110,3 +133,8 @@ fn fs_entity_texture(vertex: EntityOutputTexture) -> @location(0) vec4<f32> {
 	let reflected_color = textureSample(cube_texture, cube_sampler, reflected).rgb;
 	return mix(vec4<f32>(vec3<f32>(0.05) + 0.2 * reflected_color,1.0),mix(vertex.model_color,vec4<f32>(fragment_color.rgb,1.0),fragment_color.a),0.5+0.5*abs(d));
 }
+
+@fragment
+fn fs_debug(vertex: DebugEntityOutput) -> @location(0) vec4<f32> {
+	return model_instance.color;
+}