Compare commits

..

10 Commits

Author SHA1 Message Date
3159366fe6 the bug 2026-01-21 09:00:58 -08:00
869a142875 debug 2026-01-21 09:00:55 -08:00
18ebb4d2ce debug 2026-01-21 09:00:37 -08:00
3bcbf6de88 debug 2026-01-21 08:22:00 -08:00
5f7594fe8b hack in md lua 2026-01-21 08:16:19 -08:00
cea4a82f6c bug repro 2026-01-21 08:11:46 -08:00
80bb346366 md inner infinite loop 2026-01-21 07:48:02 -08:00
f17648e7fa remove unhelpful debug prints 2026-01-21 07:44:24 -08:00
116155e39a move contains_point 2026-01-21 07:14:06 -08:00
6085ab4268 fix porting mistake 2026-01-19 14:20:06 -08:00
5 changed files with 127 additions and 92 deletions

View File

@@ -17,13 +17,6 @@ enum Simplex1_3<Vert>{
Simplex3(Simplex<3,Vert>),
}
impl<Vert> Simplex1_3<Vert>{
fn len(&self)->usize{
match self{
Simplex1_3::Simplex1(_)=>1,
Simplex1_3::Simplex2(_)=>2,
Simplex1_3::Simplex3(_)=>3,
}
}
fn push_front(self,v:Vert)->Simplex2_4<Vert>{
match self{
Simplex1_3::Simplex1([v0])=>Simplex2_4::Simplex2([v,v0]),
@@ -248,7 +241,6 @@ fn reduce3<M:MeshQuery>(
let p=-(p0+point);
let mut u=p1-p0;
let v=p2-p0;
println!("p={p} u={u} v={v}");
// local uv = u:Cross(v)
// local up = u:Cross(p)
@@ -272,7 +264,6 @@ fn reduce3<M:MeshQuery>(
}else{
uv
};
println!("and we got here?? direction={direction:?}");
// return direction, a0, a1, b0, b1, c0, c1
return Reduced{
@@ -364,6 +355,10 @@ fn reduce4<M:MeshQuery>(
let mut u=p1-p0;
let mut v=p2-p0;
let w=p3-p0;
println!("p={p}");
println!("u={u}");
println!("v={v}");
println!("w={w}");
// local uv = u:Cross(v)
// local vw = v:Cross(w)
@@ -381,11 +376,9 @@ fn reduce4<M:MeshQuery>(
let uv_p=uv.dot(p);
// if pvw/uvw >= 0 and upw/uvw >= 0 and uvp/uvw >= 0 then
let a=!pv_w.div_sign(uv_w).is_negative();
let b=!up_w.div_sign(uv_w).is_negative();
let c=!uv_p.div_sign(uv_w).is_negative();
println!("a={a} b={b} c={c}");
if a&&b&&c{
if !pv_w.div_sign(uv_w).is_negative()
&&!up_w.div_sign(uv_w).is_negative()
&&!uv_p.div_sign(uv_w).is_negative(){
// origin is contained, this is a positive detection
// local direction = Vector3.new(0, 0, 0)
// return direction, a0, a1, b0, b1, c0, c1, d0, d1
@@ -413,8 +406,6 @@ fn reduce4<M:MeshQuery>(
// b0, c0 = c0, d0
// b1, c1 = c1, d1
(v1,v2)=(v2,v3);
}else{
v2=v3;
}
}else{
// elseif wuDist == minDist3 then
@@ -427,8 +418,6 @@ fn reduce4<M:MeshQuery>(
// before [a,b,c,d]
(v1,v2)=(v3,v1);
// after [a,d,b]
}else{
v2=v3;
}
}
@@ -440,17 +429,23 @@ fn reduce4<M:MeshQuery>(
let pv=p.cross(v);
let uv_up=uv.dot(up);
let uv_pv=uv.dot(pv);
println!("up={up}");
println!("pv={pv}");
println!("uv_up={uv_up}");
println!("uv_pv={uv_pv}");
// if uv_up >= 0 and uv_pv >= 0 then
if !uv_up.is_negative()&&!uv_pv.is_negative(){
// local direction = uvw < 0 and uv or -uv
// return direction, a0, a1, b0, b1, c0, c1
if uv_w.is_negative(){
println!("a");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
});
}else{
println!("b");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(-uv),
simplex:Simplex1_3::Simplex3([v0,v1,v2]),
@@ -488,11 +483,13 @@ fn reduce4<M:MeshQuery>(
// direction = uvw < 0 and uv or -uv
// return direction, a0, a1, b0, b1
if uv_w.is_negative(){
println!("c");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex2([v0,v1]),
});
}else{
println!("d");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(-uv),
simplex:Simplex1_3::Simplex2([v0,v1]),
@@ -500,6 +497,7 @@ fn reduce4<M:MeshQuery>(
}
}
println!("e");
// return direction, a0, a1, b0, b1
return Reduce::Reduced(Reduced{
dir:narrow_dir3(direction),
@@ -513,11 +511,13 @@ fn reduce4<M:MeshQuery>(
if dir==vec3::zero(){
// direction = uvw < 0 and uv or -uv
if uv_w.is_negative(){
println!("f");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(uv),
simplex:Simplex1_3::Simplex1([v0]),
});
}else{
println!("g");
return Reduce::Reduced(Reduced{
dir:narrow_dir2(-uv),
simplex:Simplex1_3::Simplex1([v0]),
@@ -525,6 +525,7 @@ fn reduce4<M:MeshQuery>(
}
}
println!("h");
// return direction, a0, a1
Reduce::Reduced(Reduced{
dir,
@@ -552,24 +553,6 @@ impl<Vert> Simplex2_4<Vert>{
}
}
pub fn contains_point(mesh:&MinkowskiMesh<'_>,point:Planar64Vec3)->bool{
const ENABLE_FAST_FAIL:bool=true;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
// on_exact
|is_intersecting,_simplex|{
is_intersecting
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
true
},
// fast_fail value
||false
)
}
//infinity fev algorithm state transition
#[derive(Debug)]
enum Transition<Vert>{
@@ -768,16 +751,19 @@ fn crawl_to_closest_fev<'a>(mesh:&MinkowskiMesh<'a>,simplex:Simplex<3,MinkowskiV
},
}
}
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
#[derive(Debug)]
pub struct InfiniteLoop;
pub fn closest_fev_not_inside<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Result<Option<FEV<MinkowskiMesh<'a>>>,InfiniteLoop>{
println!("=== LUA ===");
let (hits,_details)=crate::minimum_difference_lua::minimum_difference_details(mesh,point).unwrap();
println!("=== RUST ===");
let closest_fev_not_inside=closest_fev_not_inside_inner(mesh,point);
let closest_fev_not_inside=closest_fev_not_inside_inner(mesh,point).unwrap();
assert_eq!(hits,closest_fev_not_inside.is_none(),"algorithms disagree");
closest_fev_not_inside
Ok(closest_fev_not_inside)
}
fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Option<FEV<MinkowskiMesh<'a>>>{
pub fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)->Result<Option<FEV<MinkowskiMesh<'a>>>,InfiniteLoop>{
const ENABLE_FAST_FAIL:bool=false;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
@@ -785,20 +771,16 @@ fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)-
|is_intersecting,simplex|{
println!("on_exact simplex={simplex:?}");
if is_intersecting{
return None;
return Ok(None);
}
// Convert simplex to FEV
// Vertices must be inverted since the mesh is inverted
Some(match simplex{
Ok(Some(match simplex{
Simplex1_3::Simplex1([v0])=>FEV::Vert(-v0),
Simplex1_3::Simplex2([v0,v1])=>{
// invert
let (v0,v1)=(-v0,-v1);
let ev=crawl_to_closest_ev(mesh,[v0,v1],point);
if !matches!(ev,EV::Edge(_)){
println!("I can't believe it's not an edge!");
}
ev.into()
crawl_to_closest_ev(mesh,[v0,v1],point).into()
},
Simplex1_3::Simplex3([v0,v1,v2])=>{
// invert
@@ -806,23 +788,40 @@ fn closest_fev_not_inside_inner<'a>(mesh:&MinkowskiMesh<'a>,point:Planar64Vec3)-
// Shimmy to the side until you find a face that contains the closest point
// it's ALWAYS representable as a face, but this algorithm may
// return E or V in edge cases but I don't think that will break the face crawler
let fev=crawl_to_closest_fev(mesh,[v0,v1,v2],point);
if !matches!(fev,FEV::Face(_)){
println!("I can't believe it's not a face!");
}
fev
crawl_to_closest_fev(mesh,[v0,v1,v2],point)
},
})
}))
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
// local norm, dist, u0, u1, v0, v1, w0, w1 = expand(queryP, queryQ, a0, a1, b0, b1, c0, c1, d0, d1, 1e-5)
// let simplex=refine_to_exact(mesh,simplex);
None
Ok(None)
},
// fast_fail value is irrelevant and will never be returned!
||unreachable!()
||unreachable!(),
||Err(InfiniteLoop),
)
}
pub fn contains_point(mesh:&MinkowskiMesh<'_>,point:Planar64Vec3)->bool{
const ENABLE_FAST_FAIL:bool=true;
// TODO: remove mesh negation
minimum_difference::<ENABLE_FAST_FAIL,_,_>(&-mesh,point,
// on_exact
|is_intersecting,_simplex|{
is_intersecting
},
// on_escape
|_simplex|{
// intersection is guaranteed at this point
true
},
// fast_fail value
||false,
// infinite loop
||false,
)
}
@@ -837,6 +836,7 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
on_exact:impl FnOnce(bool,Simplex1_3<M::Vert>)->T,
on_escape:impl FnOnce(Simplex<4,M::Vert>)->T,
on_fast_fail:impl FnOnce()->T,
on_infinite_loop:impl FnOnce()->T,
)->T{
// local initialAxis = queryQ() - queryP()
// local new_point_p = queryP(initialAxis)
@@ -855,8 +855,8 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
// exitRadius = testIntersection and 0 or exitRadius or 1/0
// for _ = 1, 100 do
loop{
println!("arity={} direction={direction}",simplex_small.len());
for _ in 0..100{
println!("direction={direction}");
// new_point_p = queryP(-direction)
// new_point_q = queryQ(direction)
@@ -909,6 +909,7 @@ fn minimum_difference<const ENABLE_FAST_FAIL:bool,T,M:MeshQuery>(
// next loop this will be a
last_pos=next_pos;
}
on_infinite_loop()
}
#[cfg(test)]
@@ -930,7 +931,8 @@ mod test{
true
},
// fast_fail value
||false
||false,
||false,
)
}

View File

@@ -670,12 +670,14 @@ impl MinkowskiMesh<'_>{
mesh1,
}
}
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
let fev=crate::minimum_difference::closest_fev_not_inside(self,relative_body.position)?;
pub fn predict_collision_in(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Result<Option<(MinkowskiFace,GigaTime)>,crate::minimum_difference::InfiniteLoop>{
let Some(fev)=crate::minimum_difference::closest_fev_not_inside(self,relative_body.position)?else{
return Ok(None);
};
//continue forwards along the body parabola
fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit()
Ok(fev.crawl(self,relative_body,range.start_bound(),range.end_bound()).hit())
}
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Option<(MinkowskiFace,GigaTime)>{
pub fn predict_collision_out(&self,relative_body:&Body,range:impl RangeBounds<Time>)->Result<Option<(MinkowskiFace,GigaTime)>,crate::minimum_difference::InfiniteLoop>{
let (lower_bound,upper_bound)=(range.start_bound(),range.end_bound());
// TODO: handle unbounded collision using infinity fev
let time=match upper_bound{
@@ -683,14 +685,16 @@ impl MinkowskiMesh<'_>{
Bound::Excluded(&time)=>time,
Bound::Unbounded=>unimplemented!("unbounded collision out"),
};
let fev=crate::minimum_difference::closest_fev_not_inside(self,relative_body.extrapolated_position(time))?;
let Some(fev)=crate::minimum_difference::closest_fev_not_inside(self,relative_body.extrapolated_position(time))?else{
return Ok(None);
};
// swap and negate bounds to do a time inversion
let (lower_bound,upper_bound)=(upper_bound.map(|&t|-t),lower_bound.map(|&t|-t));
let infinity_body=-relative_body;
//continue backwards along the body parabola
fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
Ok(fev.crawl(self,&infinity_body,lower_bound.as_ref(),upper_bound.as_ref()).hit()
//no need to test -time<time_limit because of the first step
.map(|(face,time)|(face,-time))
.map(|(face,time)|(face,-time)))
}
pub fn predict_collision_face_out(&self,relative_body:&Body,range:impl RangeBounds<Time>,contact_face_id:MinkowskiFace)->Option<(MinkowskiDirectedEdge,GigaTime)>{
// TODO: make better

View File

@@ -828,7 +828,7 @@ impl TouchingState{
}).collect();
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){
fn predict_collision_end(&self,collector:&mut instruction::InstructionCollector<InternalInstruction,Time>,models:&PhysicsModels,hitbox_mesh:&HitboxMesh,body:&Body,start_time:Time)->Result<(),crate::minimum_difference::InfiniteLoop>{
// let relative_body=body.relative_to(&Body::ZERO);
let relative_body=body;
for (convex_mesh_id,face_id) in &self.contacts{
@@ -849,7 +849,7 @@ impl TouchingState{
//detect model collision in reverse
let model_mesh=models.intersect_mesh(convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_out(&relative_body,start_time..collector.time()).map(|(_face,time)|{
collector.collect(minkowski.predict_collision_out(&relative_body,start_time..collector.time())?.map(|(_face,time)|{
TimedInstruction{
time:relative_body.time+time.into(),
instruction:InternalInstruction::CollisionEnd(
@@ -859,6 +859,7 @@ impl TouchingState{
}
}));
}
Ok(())
}
}
@@ -1199,7 +1200,7 @@ impl<'a> PhysicsContext<'a>{
collector.collect(state.next_move_instruction());
//check for collision ends
state.touching.predict_collision_end(&mut collector,&data.models,&data.hitbox_mesh,&state.body,state.time);
state.touching.predict_collision_end(&mut collector,&data.models,&data.hitbox_mesh,&state.body,state.time).unwrap();
//check for collision starts
let mut aabb=aabb::Aabb::default();
state.body.grow_aabb(&mut aabb,state.time,collector.time());
@@ -1214,17 +1215,17 @@ impl<'a> PhysicsContext<'a>{
//no checks are needed because of the time limits.
let model_mesh=data.models.mesh(*convex_mesh_id);
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(model_mesh,data.hitbox_mesh.transformed_mesh());
collector.collect(minkowski.predict_collision_in(relative_body,state.time..collector.time())
.map(|(face,dt)|
TimedInstruction{
time:relative_body.time+dt.into(),
instruction:InternalInstruction::CollisionStart(
Collision::new(*convex_mesh_id,face),
dt
)
}
let Ok(collision)=minkowski.predict_collision_in(relative_body,state.time..collector.time())else{
println!("Infinite loop! body={relative_body}");
return;
};
collector.collect(collision.map(|(face,dt)|TimedInstruction{
time:relative_body.time+dt.into(),
instruction:InternalInstruction::CollisionStart(
Collision::new(*convex_mesh_id,face),
dt
)
);
}));
});
collector.take()
}
@@ -1972,7 +1973,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10)).unwrap();
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
}
fn test_collision_rotated(relative_body:Body,expected_collision_time:Option<Time>){
@@ -1990,7 +1991,7 @@ mod test{
let hitbox_mesh=h1.transformed_mesh();
let platform_mesh=h0.transformed_mesh();
let minkowski=model_physics::MinkowskiMesh::minkowski_sum(platform_mesh,hitbox_mesh);
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10));
let collision=minkowski.predict_collision_in(&relative_body,Time::ZERO..Time::from_secs(10)).unwrap();
assert_eq!(collision.map(|tup|relative_body.time+tup.1.into()),expected_collision_time,"Incorrect time of collision");
}
fn test_collision(relative_body:Body,expected_collision_time:Option<Time>){

View File

@@ -121,13 +121,9 @@ fn test_scene_cylinder()->PhysicsData{
let cube_face_description=CubeFaceDescription::new(Default::default(),RenderConfigId::new(0));
let mesh=builder.push_mesh(strafesnet_rbx_loader::primitives::unit_cylinder(cube_face_description));
// place one 5x5x5 cylinder.
// log cylinder
// transform=PhysicsMeshTransform { vertex: Planar64Affine3 { matrix3: Matrix { array: [[-0.00000469, 20.41880000, -0.00001730], [-3.71969533, -0.00000d38, -11.c3c43095], [-11.c3c41e7d, 0.00000000, 3.719690ac]] }, translation: Vector { array: [-908.de600000, 28f.e5240000, 24c.c4d80000] } }, normal: Matrix { array: [[-0.00002d857e4861a0, 147.71af629e398c1d05, -0.0000ead3c8730458], [-6f.147da83887600000, -0.0001ab1b4486f8fc, -23d.04a89b8def680000], [-23d.04aae32fa55a8280, 0.0000018177694d73, 6f.147e3a4136524bf8]] }, det: 2942.07be40fdd83c96df87d34320 }
// wedge part
// transform=PhysicsMeshTransform { vertex: Planar64Affine3 { matrix3: Matrix { array: [[-4.9ba5b000, 0.00000000, 0.00000000], [0.00000000, 3.85509c00, 0.00000000], [0.00000000, 0.00000000, -4.9ba5b000]] }, translation: Vector { array: [-781.30380000, 35b.dab40000, 11c.a4160000] } }, normal: Matrix { array: [[-10.3941970ff7400000, 0.0000000000000000, 0.0000000000000000], [0.0000000000000000, 15.3bcf8e5c59000000, 0.0000000000000000], [0.0000000000000000, 0.0000000000000000, -10.3941970ff7400000]] }, det: 4a.c2312159fcd9163c00000000 }
builder.push_mesh_instance(mesh,Planar64Affine3::new(
mat3::Matrix3::from_cols([vec3::raw_array([-0x4_9ba5b000, 0x0_00000000, 0x0_00000000]), vec3::raw_array([0x0_00000000, 0x3_85509c00, 0x0_00000000]), vec3::raw_array([0x0_00000000, 0x0_00000000, -0x4_9ba5b000])]),
vec3::raw_array([-0x781_30380000, 0x35b_dab40000, 0x11c_a4160000])
mat3::from_diagonal(vec3::int(5,5,5)>>1),
vec3::int(0,-5,0)
));
builder.build()
}
@@ -135,13 +131,9 @@ fn test_scene_cylinder()->PhysicsData{
#[test]
fn test_minimum_difference(){
let physics_data=test_scene_cylinder();
// log cylinder
// H p(-2329.436, 694.175, 587.190) v(0.000, -5.714, 0.000) a(0.000, -100.000, 0.000) t(56s+880000000ns)
// wedge part
// H p(-1920.041, 867.905, 284.639) v(0.000, -4.695, 0.000) a(0.000, -100.000, 0.000) t(119s+260000000ns)
let body=strafesnet_physics::physics::Body::new(
vec3::try_from_f32_array([-1920.041, 867.905, 284.639]).unwrap(),
vec3::try_from_f32_array([0.000, -4.695, 0.000]).unwrap(),
vec3::int(4,1,4)>>1,
vec3::int(-1,-1,-2),
vec3::int(0,-100,0),
Time::ZERO,
);

View File

@@ -74,3 +74,39 @@ fn physics_bug_3()->Result<(),ReplayError>{
Ok(())
}
// Infinite loop! body=p(-1796.657, 677.618, 36.959) v(3.158, -53.650, -34.435) a(-0.000, -71.276, -45.248) t(288s+440000000ns)
// Infinite loop! body=p(-2382.440, 160.150, -379.151) v(53.632, 35.779, 44.904) a(0.000, -100.000, 0.000) t(306s+675758543ns)
// Infinite loop! body=p(-1798.724, 731.459, 68.784) v(-17.389, 0.000, -78.087) a(0.000, 0.000, 0.000) t(284s+006980061ns)
// Infinite loop! body=p(-1796.657, 677.618, 36.959) v(3.158, -53.650, -34.435) a(-0.000, -71.276, -45.248) t(288s+440000000ns)
// Infinite loop! body=p(-1797.504, 738.529, 74.864) v(-3.653, 0.000, -79.917) a(0.000, 0.000, 0.000) t(282s+709871336ns)
// Infinite loop! body=p(-1797.569, 735.449, 71.859) v(23.726, -76.309, -3.747) a(0.000, 0.000, 0.000) t(283s+325193187ns)
#[test]
fn physics_md_infinite_loop()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("../tools/bhop_maps/5692113331.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
println!("generating models..");
let physics_data=PhysicsData::new(&map);
println!("simulating...");
//teleport to bug
use strafesnet_common::integer::{vec3,Time};
let body=strafesnet_physics::physics::Body::new(
vec3::try_from_f32_array([-1796.657, 677.618, 36.959]).unwrap(),
vec3::try_from_f32_array([3.158, -53.650, -34.435]).unwrap(),
vec3::int(0,-100,0),
Time::ZERO,
);
let mut physics=PhysicsState::new_with_body(body);
// wait one second to activate the bug
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
Ok(())
}