Compare commits

..

12 Commits

Author SHA1 Message Date
af86be029b new bug who dis 2025-03-14 14:48:25 -07:00
7f20e73588 ma 2025-03-14 14:43:41 -07:00
9308cc8a5c debug 2025-03-14 14:43:41 -07:00
2f574b297f debug 2025-03-14 14:43:41 -07:00
7c306da7b0 physics debug view plan 2025-03-14 14:43:41 -07:00
6e9d38604e debug 2025-03-14 14:43:41 -07:00
a52d46b7cc debug 2025-03-14 14:43:41 -07:00
9ecb494748 isolate bug 2025-03-14 14:43:41 -07:00
1f73a8b5c6 it: physics bug 2 2025-03-14 14:43:41 -07:00
dbae80b1d2 fixme 2025-03-14 14:37:31 -07:00
8cc79304dc bsp_loader: max area triangulation 2025-03-14 14:34:16 -07:00
8e4792269d bsp_loader: truncate vertex precision to 16 bits
The physics algorithm expects vertices to align exactly with faces.  Since the face normal is calculated via the cross product of vertex positions, this allows the face normals to be exact with respect to the vertex positions.
2025-03-14 14:34:16 -07:00
9 changed files with 163 additions and 66 deletions

View File

@@ -24,7 +24,3 @@ resolver = "2"
#lto = true
strip = true
codegen-units = 1
[profile.dev]
strip = false
opt-level = 3

View File

@@ -11,6 +11,15 @@ pub fn required_limits()->wgpu::Limits{
wgpu::Limits::default()
}
// physics debug view:
// render two meshes adjacent according to Minkowski FEV
// render additional mesh at path point
// render path parabola
// highlight meshes according to Minkowski FEV
//
// debug process:
// press a button to step physics by one transition
struct Indices{
count:u32,
buf:wgpu::Buffer,
@@ -103,26 +112,6 @@ impl std::default::Default for GraphicsCamera{
}
}
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;
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
}
pub struct GraphicsState{
pipelines:GraphicsPipelines,
bind_groups:GraphicsBindGroups,
@@ -987,3 +976,22 @@ impl GraphicsState{
self.staging_belt.recall();
}
}
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;
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
}

View File

@@ -2,6 +2,7 @@ use crate::model::{GigaTime,FEV,MeshQuery,DirectedEdge};
use strafesnet_common::integer::{Fixed,Ratio,vec3::Vector3};
use crate::physics::{Time,Body};
#[derive(Debug)]
enum Transition<M:MeshQuery>{
Miss,
Next(FEV<M>,GigaTime),
@@ -27,17 +28,18 @@ impl<M:MeshQuery> CrawlResult<M>{
}
}
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>+std::fmt::Debug> FEV<M>
where
// This is hardcoded for MinkowskiMesh lol
M::Face:Copy,
M::Edge:Copy,
M::Vert:Copy,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>,
F:core::ops::Mul<Fixed<1,32>,Output=Fixed<4,128>>+std::fmt::Debug+std::fmt::Display,
<F as core::ops::Mul<Fixed<1,32>>>::Output:core::iter::Sum,
<M as MeshQuery>::Offset:core::ops::Sub<<F as std::ops::Mul<Fixed<1,32>>>::Output>,
{
fn next_transition(&self,body_time:GigaTime,mesh:&M,body:&Body,mut best_time:GigaTime)->Transition<M>{
println!("next_transition fev={self:?}");
//conflicting derivative means it crosses in the wrong direction.
//if the transition time is equal to an already tested transition, do not replace the current best.
let mut best_transition=Transition::Miss;
@@ -75,6 +77,7 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
//if none:
},
&FEV::Edge(edge_id)=>{
println!("test edge={edge_id:?}");
//test each face collision time, ignoring roots with zero or conflicting derivative
let edge_n=mesh.edge_n(edge_id);
let edge_verts=mesh.edge_verts(edge_id);
@@ -84,6 +87,7 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
let face_n=mesh.face_nd(edge_face_id).0;
//edge_n gets parity from the order of edge_faces
let n=face_n.cross(edge_n)*((i as i64)*2-1);
println!("edge_face={edge_face_id:?} face_n={face_n} n={n}");
//WARNING yada yada d *2
//wrap for speed
for dt in Fixed::<4,128>::zeroes2(n.dot(delta_pos).wrap_4(),n.dot(body.velocity).wrap_4()*2,n.dot(body.acceleration).wrap_4()){
@@ -138,7 +142,9 @@ impl<F:Copy,M:MeshQuery<Normal=Vector3<F>,Offset=Fixed<4,128>>> FEV<M>
Ratio::new(r.num.widen_4(),r.den.widen_4())
};
for _ in 0..20{
match self.next_transition(body_time,mesh,relative_body,time_limit){
let transition=self.next_transition(body_time,mesh,relative_body,time_limit);
println!("transition={transition:?}");
match transition{
Transition::Miss=>return CrawlResult::Miss(self),
Transition::Next(next_fev,next_time)=>(self,body_time)=(next_fev,next_time),
Transition::Hit(face,time)=>return CrawlResult::Hit(face,time),

File diff suppressed because one or more lines are too long

View File

@@ -857,6 +857,12 @@ impl Default for PhysicsState{
}
impl PhysicsState{
pub fn new_at_position(position:Planar64Vec3)->Self{
Self{
body:Body::new(position,vec3::int(0,0,0),vec3::int(0,-100,0),Time::ZERO),
..Self::default()
}
}
pub fn camera_body(&self)->Body{
Body{
position:self.body.position+self.style.camera_offset,
@@ -982,7 +988,7 @@ impl PhysicsContext<'_>{
impl PhysicsData{
/// use with caution, this is the only non-instruction way to mess with physics
pub fn generate_models(&mut self,map:&map::CompleteMap){
let modes=map.modes.clone().denormalize();
let mut modes=map.modes.clone().denormalize();
let mut used_contact_attributes=Vec::new();
let mut used_intersect_attributes=Vec::new();
@@ -1122,7 +1128,7 @@ impl PhysicsData{
//JUST POLLING!!! NO MUTATION
let mut collector=instruction::InstructionCollector::new(time_limit);
collector.collect(state.next_move_instruction());
// 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);
@@ -1134,6 +1140,7 @@ impl PhysicsData{
//let relative_body=state.body.relative_to(&Body::ZERO);
let relative_body=&state.body;
data.bvh.sample_aabb(&aabb,&mut |&convex_mesh_id|{
println!("sample model={:?}",convex_mesh_id.model_id);
//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());
@@ -1665,6 +1672,7 @@ fn atomic_internal_instruction(state:&mut PhysicsState,data:&PhysicsData,ins:Tim
}
match ins.instruction{
InternalInstruction::CollisionStart(collision,_)=>{
println!("yeaahhh!");
let mode=data.modes.get_mode(state.mode_state.get_mode_id());
match collision{
Collision::Contact(contact)=>collision_start_contact(

View File

@@ -9,10 +9,37 @@ fn main(){
match arg.as_deref(){
Some("determinism")|None=>test_determinism().unwrap(),
Some("replay")=>run_replay().unwrap(),
Some("bug2")=>physics_bug_2().unwrap(),
_=>println!("invalid argument"),
}
}
fn physics_bug_2()->Result<(),ReplayError>{
println!("loading map file..");
let data=read_entire_file("bhop_monster_jam.snfm")?;
let map=strafesnet_snf::read_map(data)?.into_complete_map()?;
// create recording
let mut physics_data=PhysicsData::default();
println!("generating models..");
physics_data.generate_models(&map);
println!("simulating...");
//teleport to bug
// body pos = Vector { array: [Fixed { bits: 554895163352 }, Fixed { bits: 1485633089990 }, Fixed { bits: 1279601007173 }] }
// after the fix it's still happening, possibly for a different reason, new position to evince:
// body pos = Vector { array: [Fixed { bits: 555690659654 }, Fixed { bits: 1490485868773 }, Fixed { bits: 1277783839382 }] }
let mut physics=PhysicsState::new_at_position(strafesnet_common::integer::vec3::raw_xyz(555690659654,1490485868773,1277783839382));
// wait one second to activate the bug
// hit=Some(ModelId(2262))
PhysicsContext::run_input_instruction(&mut physics,&physics_data,strafesnet_common::instruction::TimedInstruction{
time:strafesnet_common::integer::Time::from_millis(500),
instruction:strafesnet_common::physics::Instruction::Idle,
});
Ok(())
}
#[allow(unused)]
#[derive(Debug)]
enum ReplayError{

View File

@@ -1,5 +1,5 @@
use strafesnet_common::integer::Planar64;
use strafesnet_common::{model,integer};
use strafesnet_common::integer::{self,Planar64,Planar64Vec3};
use strafesnet_common::model::{self,VertexId};
use strafesnet_common::integer::{vec3::Vector3,Fixed,Ratio};
use crate::{valve_transform_normal,valve_transform_dist};
@@ -138,7 +138,15 @@ fn planes_to_faces(face_list:std::collections::HashSet<Face>)->Result<Faces,Plan
loop{
// push point onto vertices
// problem: this may push a vertex that does not fit in the fixed point range and is thus meaningless
face.push(intersection.divide().narrow_1().unwrap());
//
// physics bug 2 originates from vertices being imprecise?
//
// Mask off the most precise 16 bits so that
// when face normals are calculated from
// the remaining 16 fractional bits
// they never exceed 32 bits of precision.
const MASK:Planar64=Planar64::raw(!((1<<16)-1));
face.push(intersection.divide().narrow_1().unwrap().map(|c|c&MASK));
// we looped back around to face1, we're done!
if core::ptr::eq(face1,face2){
@@ -204,6 +212,33 @@ impl std::fmt::Display for BrushToMeshError{
}
impl core::error::Error for BrushToMeshError{}
fn subdivide_max_area(tris:&mut Vec<Vec<VertexId>>,cw_verts:&[(VertexId,Planar64Vec3)],i0:usize,i2:usize,id0:VertexId,id2:VertexId,v0:Planar64Vec3,v2:Planar64Vec3){
if i0+1==i2{
return;
}
let mut best_i1=i0+1;
if i0+2<i2{
let mut best_area={
let (_,v1)=cw_verts[best_i1.rem_euclid(cw_verts.len())];
(v2-v0).cross(v1-v0).length_squared()
};
for i1 in i0+2..=i2-1{
let (_,v1)=cw_verts[i1.rem_euclid(cw_verts.len())];
let area=(v2-v0).cross(v1-v0).length_squared();
if best_area<area{
best_i1=i1;
best_area=area;
}
}
}
let i1=best_i1;
let (id1,v1)=cw_verts[i1.rem_euclid(cw_verts.len())];
// draw max area first
tris.push(vec![id0,id1,id2]);
subdivide_max_area(tris,cw_verts,i0,i1,id0,id1,v0,v1);
subdivide_max_area(tris,cw_verts,i1,i2,id1,id2,v1,v2);
}
pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
// generate the mesh
let mut mb=model::MeshBuilder::new();
@@ -212,16 +247,34 @@ pub fn faces_to_mesh(faces:Vec<Vec<integer::Planar64Vec3>>)->model::Mesh{
// normals are ignored by physics
let normal=mb.acquire_normal_id(integer::vec3::ZERO);
let polygon_list=faces.into_iter().map(|face|{
face.into_iter().map(|pos|{
let pos=mb.acquire_pos_id(pos);
mb.acquire_vertex_id(model::IndexedVertex{
let polygon_list=faces.into_iter().flat_map(|face|{
let cw_verts=face.into_iter().map(|position|{
let pos=mb.acquire_pos_id(position);
(mb.acquire_vertex_id(model::IndexedVertex{
pos,
tex,
normal,
color,
})
}).collect()
}),position)
}).collect::<Vec<_>>();
// scan and select maximum area triangle O(n^3)
let len=cw_verts.len();
let cw_verts=cw_verts.as_slice();
let ((i0,i1,i2),(v0,v1,v2))=cw_verts[..len-2].iter().enumerate().flat_map(|(i0,&(_,v0))|
cw_verts[i0+1..len-1].iter().enumerate().flat_map(move|(i1,&(_,v1))|
cw_verts[i0+i1+2..].iter().enumerate().map(move|(i2,&(_,v2))|((i0,i0+i1+1,i0+i1+i2+2),(v0,v1,v2)))
)
).max_by_key(|&(_,(v0,v1,v2))|(v2-v0).cross(v1-v0).length_squared()).unwrap();
// scan and select more maximum area triangles n * O(n)
let mut tris=Vec::with_capacity(len-2);
// da big one
let (id0,id1,id2)=(cw_verts[i0].0,cw_verts[i1].0,cw_verts[i2].0);
tris.push(vec![id0,id1,id2]);
subdivide_max_area(&mut tris,cw_verts,i0,i1,id0,id1,v0,v1);
subdivide_max_area(&mut tris,cw_verts,i1,i2,id1,id2,v1,v2);
subdivide_max_area(&mut tris,cw_verts,i2,i0+len,id2,id0,v2,v0);
tris
}).collect();
let polygon_groups=vec![model::PolygonGroup::PolygonList(model::PolygonList::new(polygon_list))];

View File

@@ -401,10 +401,6 @@ impl Angle32{
pub const NEG_FRAC_PI_2:Self=Self(-1<<30);
pub const PI:Self=Self(-1<<31);
#[inline]
pub const fn raw(num:i32)->Self{
Self(num)
}
#[inline]
pub const fn wrap_from_i64(theta:i64)->Self{
//take lower bits
//note: this was checked on compiler explorer and compiles to 1 instruction!
@@ -562,10 +558,6 @@ pub mod vec3{
pub const MAX:Planar64Vec3=Planar64Vec3::new([Planar64::MAX;3]);
pub const ZERO:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO;3]);
pub const ZERO_2:linear_ops::types::Vector3<Fixed::<2,64>>=linear_ops::types::Vector3::new([Fixed::<2,64>::ZERO;3]);
pub const ZERO_3:linear_ops::types::Vector3<Fixed::<3,96>>=linear_ops::types::Vector3::new([Fixed::<3,96>::ZERO;3]);
pub const ZERO_4:linear_ops::types::Vector3<Fixed::<4,128>>=linear_ops::types::Vector3::new([Fixed::<4,128>::ZERO;3]);
pub const ZERO_5:linear_ops::types::Vector3<Fixed::<5,160>>=linear_ops::types::Vector3::new([Fixed::<5,160>::ZERO;3]);
pub const ZERO_6:linear_ops::types::Vector3<Fixed::<6,192>>=linear_ops::types::Vector3::new([Fixed::<6,192>::ZERO;3]);
pub const X:Planar64Vec3=Planar64Vec3::new([Planar64::ONE,Planar64::ZERO,Planar64::ZERO]);
pub const Y:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ONE,Planar64::ZERO]);
pub const Z:Planar64Vec3=Planar64Vec3::new([Planar64::ZERO,Planar64::ZERO,Planar64::ONE]);
@@ -656,7 +648,7 @@ pub mod mat3{
}
}
#[derive(Clone,Copy,Default,Hash,Eq,PartialEq)]
#[derive(Clone,Copy,Debug,Default,Hash,Eq,PartialEq)]
pub struct Planar64Affine3{
pub matrix3:Planar64Mat3,//includes scale above 1
pub translation:Planar64Vec3,

View File

@@ -1 +0,0 @@
RUST_BACKTRACE=1 mangohud ../target/debug/strafe-client "$@"