diff --git a/engine/physics/src/physics.rs b/engine/physics/src/physics.rs
index b226795..6750e23 100644
--- a/engine/physics/src/physics.rs
+++ b/engine/physics/src/physics.rs
@@ -26,6 +26,8 @@ use strafesnet_common::physics::{Instruction,MouseInstruction,ModeInstruction,Mi
 #[derive(Debug)]
 pub enum InternalInstruction{
 	CollisionStart(Collision,model_physics::GigaTime),
+	// transfer to a flush minkowski face
+	CollisionTransfer(ContactCollision,model_physics::MinkowskiFace),
 	CollisionEnd(Collision,model_physics::GigaTime),
 	StrafeTick,
 	ReachWalkTargetVelocity,
@@ -785,19 +787,40 @@ impl TouchingState{
 		crate::push_solve::push_solve(&contacts,acceleration)
 	}
 	fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,Time>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time){
+		use model_physics::DirectedEdge;
 		// let relative_body=body.relative_to(&Body::ZERO);
 		let relative_body=body;
 		for contact in &self.contacts{
 			//detect face slide off
 			let model_mesh=models.contact_mesh(contact);
 			let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
-			collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time..collector.time(),contact.face_id).map(|(_face,time)|{
-				TimedInstruction{
-					time:relative_body.time+time.into(),
-					instruction:InternalInstruction::CollisionEnd(
-						Collision::Contact(*contact),
-						time
-					),
+			collector.collect(minkowski.predict_collision_face_out(&relative_body,start_time..collector.time(),contact.face_id).map(|(out_edge,time)|{
+				// TODO: determine if this code should go inside predict_collision_face_out
+				// check if the face across the out_edge is flush to contact.face_id
+				let (src_face_n,_src_face_d)=minkowski.face_nd(contact.face_id);
+				// the face across may be left or right depending on the directed edge parity
+				let dst_face=minkowski.edge_faces(out_edge.as_undirected()).as_ref()[!out_edge.parity() as usize];
+				let (dst_face_n,_dst_face_d)=minkowski.face_nd(dst_face);
+				// are they exactly flush
+				if src_face_n.cross(dst_face_n)==vec3::ZERO_6
+				// implicitly don't need to check d values because faces share two vertices
+				// && n0*d1==n1*d0
+				{
+					TimedInstruction{
+						time:relative_body.time+time.into(),
+						instruction:InternalInstruction::CollisionTransfer(
+							*contact,
+							dst_face,
+						),
+					}
+				}else{
+					TimedInstruction{
+						time:relative_body.time+time.into(),
+						instruction:InternalInstruction::CollisionEnd(
+							Collision::Contact(*contact),
+							time
+						),
+					}
 				}
 			}));
 		}
@@ -1656,6 +1679,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
 		|InternalInstruction::CollisionEnd(_,dt)=>(true,Some(dt)),
 		InternalInstruction::StrafeTick
 		|InternalInstruction::ReachWalkTargetVelocity=>(true,None),
+		InternalInstruction::CollisionTransfer(..)=>(false,None),
 	};
 	if should_advance_body{
 		match goober_time{
@@ -1700,6 +1724,15 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
 					state.time
 				),
 			},
+			InternalInstruction::CollisionTransfer(collision,dst_face)=>{
+				// transfer to a flush surface of the same object
+				state.touching.contacts.remove(&collision);
+				state.touching.contacts.insert(ContactCollision{
+					face_id:dst_face,
+					model_id:collision.model_id,
+					submesh_id:collision.submesh_id,
+				});
+			},
 			InternalInstruction::StrafeTick=>{
 				//TODO make this less huge
 				if let Some(strafe_settings)=&state.style.strafe{