From fcb6ac7b716a96ba78bae34ce89453ce181d9014 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 14:05:56 -0500 Subject: [PATCH 01/14] Migrate from a standard BVH to BVH4 --- .../bullet/collision/shapes/optimized_bvh.rs | 7 +- rocketsim/src/shared/bvh.rs | 440 +++++++++++++----- rocketsim/src/sim/boost_pad/boost_pad_grid.rs | 5 +- 3 files changed, 325 insertions(+), 127 deletions(-) diff --git a/rocketsim/src/bullet/collision/shapes/optimized_bvh.rs b/rocketsim/src/bullet/collision/shapes/optimized_bvh.rs index cda29ade..e9bcd53d 100644 --- a/rocketsim/src/bullet/collision/shapes/optimized_bvh.rs +++ b/rocketsim/src/bullet/collision/shapes/optimized_bvh.rs @@ -48,10 +48,5 @@ pub fn create_bvh(triangles: &TriangleMesh, aabb: Aabb) -> Tree { }) .collect(); - let num_leaf_nodes = leaf_nodes.len(); - - let mut bvh = Tree::new(aabb, num_leaf_nodes); - bvh.build_tree(&mut leaf_nodes, 0, num_leaf_nodes); - - bvh + Tree::build(aabb, &mut leaf_nodes) } diff --git a/rocketsim/src/shared/bvh.rs b/rocketsim/src/shared/bvh.rs index 983f8e20..f72d13f5 100644 --- a/rocketsim/src/shared/bvh.rs +++ b/rocketsim/src/shared/bvh.rs @@ -15,18 +15,25 @@ pub trait ProcessQuadRayNode { #[derive(Debug, Default, Clone)] pub struct Tree { pub aabb: Aabb, - pub cur_node_idx: usize, - pub nodes: Box<[Node]>, + wide_nodes: Box<[WideNode]>, + leaves: Box<[WideLeaf]>, } impl Tree { const SAH_BINS: usize = 4; + const TRAVERSAL_STACK_SIZE: usize = 128; + + pub fn build(aabb: Aabb, leaf_nodes: &mut [Node]) -> Self { + let binary = BinaryTree::build(aabb, leaf_nodes); + let mut wide_nodes = Vec::new(); + let mut leaves = Vec::with_capacity(leaf_nodes.len()); + let max_wide_depth = binary.build_wide_node(0, &mut wide_nodes, &mut leaves); + assert!(max_wide_depth * 3 < Self::TRAVERSAL_STACK_SIZE); - pub fn new(aabb: Aabb, num_leaf_nodes: usize) -> Self { Self { aabb, - cur_node_idx: 0, - nodes: repeat_n(Node::DEFAULT, 2 * num_leaf_nodes).collect(), + wide_nodes: wide_nodes.into_boxed_slice(), + leaves: leaves.into_boxed_slice(), } } @@ -154,162 +161,280 @@ impl Tree { mem::swap(a, b); } - pub fn build_tree(&mut self, leaf_nodes: &mut [Node], start_idx: usize, end_idx: usize) { + pub fn check_overlap_with(&self, aabb: &Aabb) -> bool { + if !aabb.intersects(&self.aabb) { + return false; + } + + let mut stack = [0usize; Self::TRAVERSAL_STACK_SIZE]; + let mut stack_len = 1; + while stack_len != 0 { + stack_len -= 1; + let node = &self.wide_nodes[stack[stack_len]]; + let mask = node.intersection_mask(aabb); + for lane in 0..node.child_count as usize { + if mask & (1 << lane) == 0 { + continue; + } + if let Some(child_idx) = node.children[lane].leaf_idx() { + let _ = child_idx; + return true; + } + stack[stack_len] = node.children[lane].branch_idx(); + stack_len += 1; + } + } + false + } + + pub fn report_aabb_overlapping_node(&self, node_callback: &mut T, aabb: &Aabb) { + if !aabb.intersects(&self.aabb) { + return; + } + + let mut stack = [WideChild::default(); Self::TRAVERSAL_STACK_SIZE]; + stack[0] = WideChild::branch(0); + let mut stack_len = 1; + while stack_len != 0 { + stack_len -= 1; + let work = stack[stack_len]; + if let Some(storage_idx) = work.leaf_idx() { + node_callback.process_node(self.leaves[storage_idx].leaf_idx); + continue; + } + + let node = &self.wide_nodes[work.branch_idx()]; + let mask = node.intersection_mask(aabb); + for lane in (0..node.child_count as usize).rev() { + if mask & (1 << lane) != 0 { + stack[stack_len] = node.children[lane]; + stack_len += 1; + } + } + } + } + + pub fn report_quad_ray_overlapping_node( + &self, + node_callback: &mut T, + ray_info: &mut QuadRayInfo, + ) { + if !ray_info.aabb.intersects(&self.aabb) { + return; + } + + let (origins, inv_dirs) = ray_info.calc_pos_dir(); + let mut stack = [WideChild::default(); Self::TRAVERSAL_STACK_SIZE]; + stack[0] = WideChild::branch(0); + let mut stack_len = 1; + while stack_len != 0 { + stack_len -= 1; + let work = stack[stack_len]; + if let Some(storage_idx) = work.leaf_idx() { + let leaf = self.leaves[storage_idx]; + let mask = QuadRayInfo::intersect_quad_ray_aabb( + &origins, + &inv_dirs, + &leaf.bounds, + ray_info.lambda_max, + ); + if mask != 0 { + node_callback.process_node(leaf.leaf_idx, mask, &mut ray_info.lambda_max); + } + continue; + } + + let node = &self.wide_nodes[work.branch_idx()]; + let mask = node.intersection_mask(&ray_info.aabb); + for lane in (0..node.child_count as usize).rev() { + if mask & (1 << lane) != 0 { + stack[stack_len] = node.children[lane]; + stack_len += 1; + } + } + } + } +} + +struct BinaryTree { + aabb: Aabb, + cur_node_idx: usize, + nodes: Box<[Node]>, +} + +impl BinaryTree { + fn build(aabb: Aabb, leaf_nodes: &mut [Node]) -> Self { + assert!(!leaf_nodes.is_empty()); + let mut tree = Self { + aabb, + cur_node_idx: 0, + nodes: repeat_n(Node::DEFAULT, 2 * leaf_nodes.len()).collect(), + }; + tree.build_subtree(leaf_nodes, 0, leaf_nodes.len()); + tree + } + + fn build_subtree(&mut self, leaf_nodes: &mut [Node], start_idx: usize, end_idx: usize) { let num_indices = end_idx - start_idx; let cur_idx = self.cur_node_idx; - debug_assert!(num_indices > 0); - if num_indices == 1 { self.nodes[self.cur_node_idx] = leaf_nodes[start_idx]; self.cur_node_idx += 1; return; } - let split_idx = Self::calc_sah_split(leaf_nodes, start_idx, end_idx); - + let split_idx = Tree::calc_sah_split(leaf_nodes, start_idx, end_idx); let internal_node_idx = self.cur_node_idx; { let node = &mut self.nodes[internal_node_idx]; node.aabb.min = self.aabb.max; node.aabb.max = self.aabb.min; - for leaf in &leaf_nodes[start_idx..end_idx] { node.aabb += leaf.aabb; } } self.cur_node_idx += 1; + self.build_subtree(leaf_nodes, start_idx, split_idx); + self.build_subtree(leaf_nodes, split_idx, end_idx); - self.build_tree(leaf_nodes, start_idx, split_idx); - self.build_tree(leaf_nodes, split_idx, end_idx); + self.nodes[internal_node_idx].node_type = BvhNodeType::Branch { + escape_idx: self.cur_node_idx - cur_idx, + }; + } - let escape_idx = self.cur_node_idx - cur_idx; - self.nodes[internal_node_idx].node_type = BvhNodeType::Branch { escape_idx }; + fn children(&self, node_idx: usize) -> [usize; 2] { + let left_idx = node_idx + 1; + let right_idx = match self.nodes[left_idx].node_type { + BvhNodeType::Leaf { .. } => left_idx + 1, + BvhNodeType::Branch { escape_idx } => left_idx + escape_idx, + }; + [left_idx, right_idx] } - fn walk_stackless_tree_find_overlap( + fn build_wide_node( &self, - aabb: &Aabb, - start_node_idx: usize, - end_node_idx: usize, - ) -> bool { - let mut cur_idx = start_node_idx; - while cur_idx < end_node_idx { - let root_node = &self.nodes[cur_idx]; - let aabb_overlap = aabb.intersects(&root_node.aabb); - - match root_node.node_type { - BvhNodeType::Leaf { leaf_idx: _ } => { - if aabb_overlap { - return true; - } + binary_root: usize, + wide_nodes: &mut Vec, + leaves: &mut Vec, + ) -> usize { + let wide_idx = wide_nodes.len(); + wide_nodes.push(WideNode::default()); + + let mut frontier = vec![binary_root]; + while frontier.len() < 4 { + let Some((slot, _)) = frontier + .iter() + .enumerate() + .filter_map(|(slot, &idx)| match self.nodes[idx].node_type { + BvhNodeType::Leaf { .. } => None, + BvhNodeType::Branch { escape_idx } => Some((slot, escape_idx)), + }) + .max_by_key(|&(_, subtree_size)| subtree_size) + else { + break; + }; + + let [left, right] = self.children(frontier[slot]); + frontier.splice(slot..=slot, [left, right]); + } - cur_idx += 1; + let mut wide = WideNode { + child_count: frontier.len() as u8, + ..Default::default() + }; + let mut max_child_depth = 0; + for (lane, binary_idx) in frontier.into_iter().enumerate() { + let node = self.nodes[binary_idx]; + wide.set_bounds(lane, node.aabb); + wide.children[lane] = match node.node_type { + BvhNodeType::Leaf { leaf_idx } => { + let storage_idx = leaves.len(); + leaves.push(WideLeaf { + bounds: node.aabb, + leaf_idx, + }); + WideChild::leaf(storage_idx) } - BvhNodeType::Branch { escape_idx } => { - cur_idx += if aabb_overlap { 1 } else { escape_idx }; + BvhNodeType::Branch { .. } => { + let child_idx = wide_nodes.len(); + max_child_depth = + max_child_depth.max(self.build_wide_node(binary_idx, wide_nodes, leaves)); + WideChild::branch(child_idx) } - } + }; } + wide_nodes[wide_idx] = wide; + max_child_depth + 1 + } +} - false +#[derive(Debug, Clone, Copy)] +struct WideLeaf { + bounds: Aabb, + leaf_idx: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +struct WideNode { + min_x: Vec4, + min_y: Vec4, + min_z: Vec4, + max_x: Vec4, + max_y: Vec4, + max_z: Vec4, + children: [WideChild; 4], + child_count: u8, +} + +impl WideNode { + fn set_bounds(&mut self, lane: usize, aabb: Aabb) { + self.min_x[lane] = aabb.min.x; + self.min_y[lane] = aabb.min.y; + self.min_z[lane] = aabb.min.z; + self.max_x[lane] = aabb.max.x; + self.max_y[lane] = aabb.max.y; + self.max_z[lane] = aabb.max.z; } - pub fn check_overlap_with(&self, aabb: &Aabb) -> bool { - aabb.intersects(&self.aabb) - && self.walk_stackless_tree_find_overlap(aabb, 0, self.cur_node_idx) + fn intersection_mask(&self, aabb: &Aabb) -> u32 { + let overlap = self.min_x.cmple(Vec4::splat(aabb.max.x)) + & self.max_x.cmpge(Vec4::splat(aabb.min.x)) + & self.min_y.cmple(Vec4::splat(aabb.max.y)) + & self.max_y.cmpge(Vec4::splat(aabb.min.y)) + & self.min_z.cmple(Vec4::splat(aabb.max.z)) + & self.max_z.cmpge(Vec4::splat(aabb.min.z)); + overlap.bitmask() & ((1 << self.child_count) - 1) } +} - fn walk_stackless_tree( - &self, - node_callback: &mut T, - aabb: &Aabb, - start_node_idx: usize, - end_node_idx: usize, - ) { - let mut cur_idx = start_node_idx; - while cur_idx < end_node_idx { - let root_node = &self.nodes[cur_idx]; - let aabb_overlap = aabb.intersects(&root_node.aabb); +#[derive(Debug, Default, Clone, Copy)] +struct WideChild(usize); - match root_node.node_type { - BvhNodeType::Leaf { leaf_idx } => { - if aabb_overlap { - node_callback.process_node(leaf_idx); - } +impl WideChild { + const LEAF_BIT: usize = 1 << (usize::BITS - 1); - cur_idx += 1; - } - BvhNodeType::Branch { escape_idx } => { - cur_idx += if aabb_overlap { 1 } else { escape_idx }; - } - } - } + const fn leaf(leaf_idx: usize) -> Self { + assert!(leaf_idx < Self::LEAF_BIT); + Self(Self::LEAF_BIT | leaf_idx) } - pub fn report_aabb_overlapping_node(&self, node_callback: &mut T, aabb: &Aabb) { - if aabb.intersects(&self.aabb) { - self.walk_stackless_tree(node_callback, aabb, 0, self.cur_node_idx); - } + const fn branch(branch_idx: usize) -> Self { + Self(branch_idx) } - fn walk_stackless_tree_against_quad_ray( - &self, - node_callback: &mut T, - ray_info: &mut QuadRayInfo, - origins: &[Vec4; 3], - inv_dir: &[Vec4; 3], - start_node_idx: usize, - end_node_idx: usize, - ) { - let mut cur_idx = start_node_idx; - while cur_idx < end_node_idx { - let root_node = &self.nodes[cur_idx]; - let overlap = ray_info.aabb.intersects(&root_node.aabb); - - match root_node.node_type { - BvhNodeType::Leaf { leaf_idx } => { - if overlap { - let mask = QuadRayInfo::intersect_quad_ray_aabb( - origins, - inv_dir, - &root_node.aabb, - ray_info.lambda_max, - ); - - if mask != 0 { - node_callback.process_node(leaf_idx, mask, &mut ray_info.lambda_max); - } - } - cur_idx += 1; - } - BvhNodeType::Branch { escape_idx } => { - cur_idx += if overlap { 1 } else { escape_idx }; - } - } + const fn leaf_idx(self) -> Option { + if self.0 & Self::LEAF_BIT != 0 { + Some(self.0 & !Self::LEAF_BIT) + } else { + None } } - pub fn report_quad_ray_overlapping_node( - &self, - node_callback: &mut T, - ray_info: &mut QuadRayInfo, - ) { - if !ray_info.aabb.intersects(&self.aabb) { - return; - } - - let (origins, inv_dirs) = ray_info.calc_pos_dir(); - self.walk_stackless_tree_against_quad_ray( - node_callback, - ray_info, - &origins, - &inv_dirs, - 0, - self.cur_node_idx, - ); + const fn branch_idx(self) -> usize { + self.0 } } @@ -331,3 +456,84 @@ impl Node { node_type: BvhNodeType::Leaf { leaf_idx: 0 }, }; } + +#[cfg(test)] +mod tests { + use std::mem::size_of; + + use glam::Vec3A; + + use super::{Aabb, BvhNodeType, Node, ProcessNode, Tree}; + + #[derive(Default)] + struct Collector(Vec); + + impl ProcessNode for Collector { + fn process_node(&mut self, leaf_idx: usize) { + self.0.push(leaf_idx); + } + } + + #[test] + fn tree_stores_only_runtime_bvh_data() { + assert_eq!(size_of::(), 64); + } + + #[test] + fn wide_traversal_handles_single_leaf() { + let bounds = Aabb::new(Vec3A::ZERO, Vec3A::ONE); + let mut leaves = [Node { + aabb: bounds, + node_type: BvhNodeType::Leaf { leaf_idx: 7 }, + }]; + let tree = Tree::build(bounds, &mut leaves); + + let mut actual = Collector::default(); + tree.report_aabb_overlapping_node(&mut actual, &bounds); + assert_eq!(actual.0, [7]); + assert!(tree.check_overlap_with(&bounds)); + } + + #[test] + fn wide_traversal_matches_brute_force_in_binary_order() { + let mut leaves: Vec<_> = (0..257) + .map(|idx| { + let x = ((idx * 37) % 101) as f32 - 50.0; + let y = ((idx * 61) % 97) as f32 - 48.0; + let z = ((idx * 17) % 43) as f32 - 21.0; + let min = Vec3A::new(x, y, z); + Node { + aabb: Aabb::new(min, min + Vec3A::splat(1.5)), + node_type: BvhNodeType::Leaf { leaf_idx: idx }, + } + }) + .collect(); + let tree_aabb = leaves + .iter() + .skip(1) + .fold(leaves[0].aabb, |bounds, leaf| bounds.combine(&leaf.aabb)); + let tree = Tree::build(tree_aabb, &mut leaves); + + for query_idx in 0..100 { + let center = Vec3A::new( + ((query_idx * 29) % 113) as f32 - 56.0, + ((query_idx * 47) % 109) as f32 - 54.0, + ((query_idx * 13) % 53) as f32 - 26.0, + ); + let query = Aabb::new(center - 8.0, center + 8.0); + let expected: Vec<_> = leaves + .iter() + .filter_map(|node| match node.node_type { + BvhNodeType::Leaf { leaf_idx } if query.intersects(&node.aabb) => { + Some(leaf_idx) + } + _ => None, + }) + .collect(); + let mut actual = Collector::default(); + tree.report_aabb_overlapping_node(&mut actual, &query); + assert_eq!(actual.0, expected); + assert_eq!(tree.check_overlap_with(&query), !expected.is_empty()); + } + } +} diff --git a/rocketsim/src/sim/boost_pad/boost_pad_grid.rs b/rocketsim/src/sim/boost_pad/boost_pad_grid.rs index 712207a0..abca8836 100644 --- a/rocketsim/src/sim/boost_pad/boost_pad_grid.rs +++ b/rocketsim/src/sim/boost_pad/boost_pad_grid.rs @@ -84,8 +84,6 @@ impl BoostPadGrid { all_aabb_accum.unwrap() }; - let mut bvh_tree = bvh::Tree::new(all_aabb, pad_configs.len()); - let mut aabb_nodes = Vec::new(); for (i, pad) in all_pads.iter().enumerate() { let node = bvh::Node { @@ -95,8 +93,7 @@ impl BoostPadGrid { aabb_nodes.push(node); } - let num_nodes = aabb_nodes.len(); - bvh_tree.build_tree(&mut aabb_nodes, 0, num_nodes); + let bvh_tree = bvh::Tree::build(all_aabb, &mut aabb_nodes); Self { bvh_tree, From 0892c1dbd439220b643ff983914eb1fc6e38791e Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 14:29:24 -0500 Subject: [PATCH 02/14] Pack broadphase pair IDs into hash keys --- .../collision/broadphase/overlapping_pair_cache.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rocketsim/src/bullet/collision/broadphase/overlapping_pair_cache.rs b/rocketsim/src/bullet/collision/broadphase/overlapping_pair_cache.rs index ee2a35cb..17d3b726 100644 --- a/rocketsim/src/bullet/collision/broadphase/overlapping_pair_cache.rs +++ b/rocketsim/src/bullet/collision/broadphase/overlapping_pair_cache.rs @@ -13,7 +13,7 @@ use crate::bullet::{ pub struct HashedOverlappingPairCache { overlapping_pair_array: Vec, - hash_table: FxHashMap<(u32, u32), usize>, + hash_table: FxHashMap, } impl Default for HashedOverlappingPairCache { @@ -41,8 +41,9 @@ impl HashedOverlappingPairCache { mem::swap(&mut proxy0_idx, &mut proxy1_idx); } + let pair_key = (u64::from(proxy0_id) << 32) | u64::from(proxy1_id); self.hash_table - .insert((proxy0_id, proxy1_id), self.overlapping_pair_array.len()); + .insert(pair_key, self.overlapping_pair_array.len()); self.overlapping_pair_array.push(BroadphasePair { proxy0: proxy0_idx, @@ -78,8 +79,8 @@ impl HashedOverlappingPairCache { mem::swap(&mut proxy0, &mut proxy1); } - self.hash_table - .contains_key(&(proxy0.unique_id, proxy1.unique_id)) + let pair_key = (u64::from(proxy0.unique_id) << 32) | u64::from(proxy1.unique_id); + self.hash_table.contains_key(&pair_key) } #[inline] From 5f91267fae7c84ffc1652fd0456a58ecb8fcda6e Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 14:29:35 -0500 Subject: [PATCH 03/14] Use cached wheel contact velocity --- rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs b/rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs index 0d849ba1..e8806a24 100644 --- a/rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs +++ b/rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs @@ -202,9 +202,8 @@ impl WheelInfo { let car_rel_contact_point = contact_point - chassis.get_world_trans().translation; - let v1 = chassis.get_vel_in_local_point(car_rel_contact_point); - let v2 = ground_rb.get_vel_in_local_point(car_rel_contact_point); - let contact_vel = v1 - v2; + let contact_vel = self.vel_at_contact_point + - ground_rb.get_vel_in_local_point(car_rel_contact_point); let mut rel_vel = contact_vel.dot(forward_dir); if time_step > 1.0 / 80.0 { From 579fd12cd70a87ecefc605382f89929e99bf3453 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 14:29:43 -0500 Subject: [PATCH 04/14] Keep triangle mesh raycasts in world space --- .../collision/dispatch/collision_world.rs | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/rocketsim/src/bullet/collision/dispatch/collision_world.rs b/rocketsim/src/bullet/collision/dispatch/collision_world.rs index af9d630c..4a376ae2 100644 --- a/rocketsim/src/bullet/collision/dispatch/collision_world.rs +++ b/rocketsim/src/bullet/collision/dispatch/collision_world.rs @@ -9,6 +9,7 @@ use crate::{ collision::{ broadphase::GridBroadphase, narrowphase::persistent_manifold::{CONTACT_BREAKING_THRESHOLD, ContactAddedCallback}, + shapes::collision_shape::CollisionShapes, }, dynamics::rigid_body::RigidBody, linear_math::AffineExt, @@ -121,21 +122,26 @@ impl CollisionWorld { obj_idx: usize, result_callback: &mut T, ) { - let world_to_co = co.get_world_trans().transpose(); - - let ray_from_local = [ - world_to_co.transform_point3a(ray_from[0]), - world_to_co.transform_point3a(ray_from[1]), - world_to_co.transform_point3a(ray_from[2]), - world_to_co.transform_point3a(ray_from[3]), - ]; - - let ray_to_local = [ - world_to_co.transform_point3a(ray_to[0]), - world_to_co.transform_point3a(ray_to[1]), - world_to_co.transform_point3a(ray_to[2]), - world_to_co.transform_point3a(ray_to[3]), - ]; + let (ray_from_local, ray_to_local) = + if matches!(co.get_collision_shape(), CollisionShapes::TriangleMesh(_)) { + (*ray_from, *ray_to) + } else { + let world_to_co = co.get_world_trans().transpose(); + ( + [ + world_to_co.transform_point3a(ray_from[0]), + world_to_co.transform_point3a(ray_from[1]), + world_to_co.transform_point3a(ray_from[2]), + world_to_co.transform_point3a(ray_from[3]), + ], + [ + world_to_co.transform_point3a(ray_to[0]), + world_to_co.transform_point3a(ray_to[1]), + world_to_co.transform_point3a(ray_to[2]), + world_to_co.transform_point3a(ray_to[3]), + ], + ) + }; let mut rcb = BridgeTriQuadRayCallback { from: &ray_from_local, From ef70a11639e7969e12e20ba2227b5b371b5a9bcb Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 14:29:50 -0500 Subject: [PATCH 05/14] Filter grid ray candidates by combined AABB --- .../src/bullet/collision/broadphase/grid_broadphase.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs b/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs index fccea832..3a93d2c4 100644 --- a/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs +++ b/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs @@ -331,10 +331,16 @@ impl GridBroadphase { debug_assert!(ray_from[2].distance_squared(ray_to[2]) < self.cell_grid.cell_size_sq); debug_assert!(ray_from[3].distance_squared(ray_to[3]) < self.cell_grid.cell_size_sq); let cell = &self.cell_grid.cells[self.cell_grid.get_cell_idx(ray_from[0])]; + let ray_aabb = ray_from.iter().zip(ray_to).skip(1).fold( + Aabb::new(ray_from[0].min(ray_to[0]), ray_from[0].max(ray_to[0])), + |bounds, (from, to)| bounds.combine(&Aabb::new(from.min(*to), from.max(*to))), + ); for &other_proxy_idx in cell.static_handles.iter().chain(&cell.dyn_handles) { let other_proxy = &self.handles[other_proxy_idx]; - ray_callback.process(other_proxy); + if ray_aabb.intersects(&other_proxy.aabb) { + ray_callback.process(other_proxy); + } } } } From 2ff8989da7d7c27d957775dd656d4e6b212364de Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 15:05:51 -0500 Subject: [PATCH 06/14] Compact collision data layouts --- .../collision/broadphase/broadphase_proxy.rs | 28 ++++++++++--- .../collision/broadphase/grid_broadphase.rs | 22 ++++++----- .../dispatch/collision_dispatcher.rs | 4 +- .../dispatch/compound_collision_alg.rs | 2 +- .../dispatch/internal_edge_utility.rs | 14 +++---- .../collision/dispatch/quad_ray_callbacks.rs | 7 ++-- .../collision/narrowphase/manifold_point.rs | 5 +-- .../narrowphase/persistent_manifold.rs | 3 +- .../collision/shapes/collision_shape.rs | 9 ++++- .../bullet/collision/shapes/optimized_bvh.rs | 2 +- .../bullet/collision/shapes/triangle_shape.rs | 39 +++++++++++-------- 11 files changed, 82 insertions(+), 53 deletions(-) diff --git a/rocketsim/src/bullet/collision/broadphase/broadphase_proxy.rs b/rocketsim/src/bullet/collision/broadphase/broadphase_proxy.rs index 303ead22..ec28238f 100644 --- a/rocketsim/src/bullet/collision/broadphase/broadphase_proxy.rs +++ b/rocketsim/src/bullet/collision/broadphase/broadphase_proxy.rs @@ -1,7 +1,5 @@ use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor}; -use glam::USizeVec3; - use crate::shared::Aabb; pub enum CollisionFilterGroups { @@ -74,13 +72,13 @@ impl BitOr for CollisionFilterGroups { pub struct BroadphaseProxy { /// The index of the client `RigidBody` in `CollisionWorld` - pub client_obj_idx: usize, + pub client_obj_idx: u32, pub collision_filter_group: u8, pub collision_filter_mask: u8, pub unique_id: u32, pub aabb: Aabb, - pub cell_idx: usize, - pub indices: USizeVec3, + pub cell_idx: u32, + pub indices: [u32; 3], } pub struct BroadphasePair { @@ -91,3 +89,23 @@ pub struct BroadphasePair { pub trait BroadphaseAabbCallback { fn process(&mut self, proxy: &BroadphaseProxy) -> bool; } + +#[cfg(test)] +mod layout_tests { + use std::mem::size_of; + + use crate::bullet::collision::{ + narrowphase::{manifold_point::ManifoldPoint, persistent_manifold::PersistentManifold}, + shapes::triangle_shape::TriangleShape, + }; + + use super::BroadphaseProxy; + + #[test] + fn compact_layouts() { + assert_eq!(size_of::(), 64); + assert_eq!(size_of::(), 80); + assert_eq!(size_of::(), 128); + assert_eq!(size_of::(), 560); + } +} diff --git a/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs b/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs index 3a93d2c4..241a51c1 100644 --- a/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs +++ b/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs @@ -207,16 +207,18 @@ impl GridBroadphase { if (sbp.collision_filter_group & CollisionFilterGroups::Static) != 0 { self.cell_grid.update_cells_static(sbp, col_obj, proxy_idx); } else { - let old_idx = sbp.cell_idx; + let old_idx = sbp.cell_idx as usize; let new_indices = self.cell_grid.get_cell_indices(aabb.min); let new_idx = self.cell_grid.cell_indices_to_idx(new_indices); - self.handles[proxy_idx].cell_idx = new_idx; + self.handles[proxy_idx].cell_idx = u32::try_from(new_idx).unwrap(); if new_idx != old_idx { - let old_indices = self.handles[proxy_idx].indices; - self.cell_grid - .update_cells_dynamic::(proxy_idx, old_indices); - self.handles[proxy_idx].indices = new_indices; + let [x, y, z] = self.handles[proxy_idx].indices; + self.cell_grid.update_cells_dynamic::( + proxy_idx, + USizeVec3::new(x as usize, y as usize, z as usize), + ); + self.handles[proxy_idx].indices = new_indices.as_uvec3().to_array(); self.cell_grid .update_cells_dynamic::(proxy_idx, new_indices); } @@ -239,12 +241,12 @@ impl GridBroadphase { let new_handle = BroadphaseProxy { aabb, - client_obj_idx: co.world_array_idx, + client_obj_idx: u32::try_from(co.world_array_idx).unwrap(), collision_filter_group, collision_filter_mask, unique_id: u32::try_from(new_handle_idx).unwrap(), - cell_idx, - indices, + cell_idx: u32::try_from(cell_idx).unwrap(), + indices: indices.as_uvec3().to_array(), }; if is_static { @@ -278,7 +280,7 @@ impl GridBroadphase { (proxy.collision_filter_group & CollisionFilterGroups::Static) == 0 }) { - let cell = &self.cell_grid.cells[proxy.cell_idx]; + let cell = &self.cell_grid.cells[proxy.cell_idx as usize]; for &other_proxy_idx in &cell.static_handles { let other_proxy = &self.handles[other_proxy_idx]; diff --git a/rocketsim/src/bullet/collision/dispatch/collision_dispatcher.rs b/rocketsim/src/bullet/collision/dispatch/collision_dispatcher.rs index a7ea222a..68697ea7 100644 --- a/rocketsim/src/bullet/collision/dispatch/collision_dispatcher.rs +++ b/rocketsim/src/bullet/collision/dispatch/collision_dispatcher.rs @@ -195,8 +195,8 @@ impl CollisionDispatcher { proxy1: &BroadphaseProxy, contact_added_callback: &mut T, ) { - let rb0 = &collision_objs[proxy0.client_obj_idx]; - let rb1 = &collision_objs[proxy1.client_obj_idx]; + let rb0 = &collision_objs[proxy0.client_obj_idx as usize]; + let rb1 = &collision_objs[proxy1.client_obj_idx as usize]; if !rb0.is_active() && !rb1.is_active() || !rb0.has_contact_response() diff --git a/rocketsim/src/bullet/collision/dispatch/compound_collision_alg.rs b/rocketsim/src/bullet/collision/dispatch/compound_collision_alg.rs index 3d7b1337..d2d0b542 100644 --- a/rocketsim/src/bullet/collision/dispatch/compound_collision_alg.rs +++ b/rocketsim/src/bullet/collision/dispatch/compound_collision_alg.rs @@ -44,7 +44,7 @@ struct ConvexTriangleCallback<'a, T: ContactAddedCallback> { impl ProcessTriangle for ConvexTriangleCallback<'_, T> { fn process_triangle(&mut self, triangle: &TriangleShape, triangle_idx: usize) { - if !triangle.aabb.intersects(self.local_convex_aabb) { + if !triangle.aabb().intersects(self.local_convex_aabb) { return; } diff --git a/rocketsim/src/bullet/collision/dispatch/internal_edge_utility.rs b/rocketsim/src/bullet/collision/dispatch/internal_edge_utility.rs index 4fd1ec0c..957d431a 100644 --- a/rocketsim/src/bullet/collision/dispatch/internal_edge_utility.rs +++ b/rocketsim/src/bullet/collision/dispatch/internal_edge_utility.rs @@ -110,7 +110,7 @@ impl ProcessTriangle for ConnectivityProcessor<'_> { match sum_verts_a { 1 => { - let edge = (-self.shape.edges[0]).normalize(); + let edge = (-self.shape.edge(0)).normalize(); let orn = Quat::from_axis_angle_simd(edge, -corrected_angle); let mut computed_normal_b = orn * self.shape.normal; if computed_normal_b.dot(tri.normal) < 0.0 { @@ -124,7 +124,7 @@ impl ProcessTriangle for ConnectivityProcessor<'_> { } } 2 => { - let edge = (-self.shape.edges[2]).normalize(); + let edge = (-self.shape.edge(2)).normalize(); let orn = Quat::from_axis_angle_simd(edge, -corrected_angle); let mut computed_normal_b = orn * self.shape.normal; if computed_normal_b.dot(tri.normal) < 0.0 { @@ -138,7 +138,7 @@ impl ProcessTriangle for ConnectivityProcessor<'_> { } } 3 => { - let edge = (-self.shape.edges[1]).normalize(); + let edge = (-self.shape.edge(1)).normalize(); let orn = Quat::from_axis_angle_simd(edge, -corrected_angle); let mut computed_normal_b = orn * self.shape.normal; if computed_normal_b.dot(tri.normal) < 0.0 { @@ -169,7 +169,7 @@ pub fn generate_internal_edge_info(bvh: &Tree, mesh_interface: &TriangleMesh) -> let mut my_node_callback = NodeOverlapCallback::new(mesh_interface, &mut connectivity_processor); - bvh.report_aabb_overlapping_node(&mut my_node_callback, &triangle_a.aabb); + bvh.report_aabb_overlapping_node(&mut my_node_callback, &triangle_a.aabb()); } triangle_info_map @@ -276,7 +276,7 @@ pub fn adjust_internal_edge_contacts( let is_edge_convex = (info.flags & TriInfoFlag::V0V1Convex) != 0; let swap_factor = f32::from(is_edge_convex) * 2.0 - 1.0; - let edge = -tri.edges[0]; + let edge = -tri.edge(0); let n_a = swap_factor * tri.normal; let orn = Quat::from_axis_angle_simd(edge.normalize(), info.edge_v0_v1_angle); let mut computed_normal_b = orn * tri.normal; @@ -322,7 +322,7 @@ pub fn adjust_internal_edge_contacts( let is_edge_convex = (info.flags & TriInfoFlag::V1V2Convex) != 0; let swap_factor = f32::from(is_edge_convex) * 2.0 - 1.0; - let edge = -tri.edges[1]; + let edge = -tri.edge(1); let n_a = swap_factor * tri.normal; let orn = Quat::from_axis_angle_simd(edge.normalize(), info.edge_v1_v2_angle); let mut computed_normal_b = orn * tri.normal; @@ -368,7 +368,7 @@ pub fn adjust_internal_edge_contacts( let is_edge_convex = (info.flags & TriInfoFlag::V2V0Convex) != 0; let swap_factor = f32::from(is_edge_convex) * 2.0 - 1.0; - let edge = -tri.edges[2]; + let edge = -tri.edge(2); let n_a = swap_factor * tri.normal; let orn = Quat::from_axis_angle_simd(edge.normalize(), info.edge_v2_v0_angle); let mut computed_normal_b = orn * tri.normal; diff --git a/rocketsim/src/bullet/collision/dispatch/quad_ray_callbacks.rs b/rocketsim/src/bullet/collision/dispatch/quad_ray_callbacks.rs index e050d5e1..2567db9a 100644 --- a/rocketsim/src/bullet/collision/dispatch/quad_ray_callbacks.rs +++ b/rocketsim/src/bullet/collision/dispatch/quad_ray_callbacks.rs @@ -44,7 +44,7 @@ pub trait QuadRayResultCallback { } fn needs_collision(&self, proxy0: &BroadphaseProxy) -> bool { let base = self.get_base(); - if base.ignore_obj_world_idx == Some(proxy0.client_obj_idx) { + if base.ignore_obj_world_idx == Some(proxy0.client_obj_idx as usize) { return false; } @@ -129,7 +129,8 @@ impl<'a, T: QuadRayResultCallback> QuadRayCallback<'a, T> { impl BroadphaseAabbCallback for QuadRayCallback<'_, T> { fn process(&mut self, proxy: &BroadphaseProxy) -> bool { - let rb = &self.world.collision_objs[proxy.client_obj_idx]; + let obj_idx = proxy.client_obj_idx as usize; + let rb = &self.world.collision_objs[obj_idx]; let handle = &self.world.broadphase_pair_cache.handles[rb.get_broadphase_handle()]; if self.result_callback.needs_collision(handle) { @@ -137,7 +138,7 @@ impl BroadphaseAabbCallback for QuadRayCallback<'_, T> self.ray_from_world, self.ray_to_world, rb, - proxy.client_obj_idx, + obj_idx, self.result_callback, ); } diff --git a/rocketsim/src/bullet/collision/narrowphase/manifold_point.rs b/rocketsim/src/bullet/collision/narrowphase/manifold_point.rs index 68705180..0fb267a8 100644 --- a/rocketsim/src/bullet/collision/narrowphase/manifold_point.rs +++ b/rocketsim/src/bullet/collision/narrowphase/manifold_point.rs @@ -16,7 +16,6 @@ pub struct ManifoldPoint { pub combined_restitution: f32, pub applied_impulse: f32, pub lateral_friction_dir_1: Vec3A, - pub lateral_friction_dir_2: Vec3A, pub is_special: bool, } @@ -33,7 +32,6 @@ impl ManifoldPoint { combined_restitution: 0.0, applied_impulse: 0.0, lateral_friction_dir_1: Vec3A::ZERO, - lateral_friction_dir_2: Vec3A::ZERO, is_special: false, } } @@ -57,8 +55,7 @@ impl ManifoldPoint { if lat_rel_vel > f32::EPSILON { self.lateral_friction_dir_1 *= 1.0 / lat_rel_vel.sqrt(); } else { - (self.lateral_friction_dir_1, self.lateral_friction_dir_2) = - plane_space_2(self.normal_world_on_b); + self.lateral_friction_dir_1 = plane_space_2(self.normal_world_on_b).0; } } } diff --git a/rocketsim/src/bullet/collision/narrowphase/persistent_manifold.rs b/rocketsim/src/bullet/collision/narrowphase/persistent_manifold.rs index ef8864c0..6c8af4f4 100644 --- a/rocketsim/src/bullet/collision/narrowphase/persistent_manifold.rs +++ b/rocketsim/src/bullet/collision/narrowphase/persistent_manifold.rs @@ -201,8 +201,7 @@ impl PersistentManifold { new_pt.combined_friction = Self::calculate_combined_friction(body0, body1); new_pt.combined_restitution = Self::calculate_combined_restitution(body0, body1); - (new_pt.lateral_friction_dir_1, new_pt.lateral_friction_dir_2) = - plane_space_2(new_pt.normal_world_on_b); + new_pt.lateral_friction_dir_1 = plane_space_2(new_pt.normal_world_on_b).0; let insert_idx = self.add_manifold_point(new_pt); diff --git a/rocketsim/src/bullet/collision/shapes/collision_shape.rs b/rocketsim/src/bullet/collision/shapes/collision_shape.rs index 79b63dad..356dba9c 100644 --- a/rocketsim/src/bullet/collision/shapes/collision_shape.rs +++ b/rocketsim/src/bullet/collision/shapes/collision_shape.rs @@ -49,7 +49,7 @@ impl CollisionShapes { debug_assert!(fast_compare_trans(t, &Affine3A::IDENTITY)); shape.aabb_ident_cache } - Self::Triangle(shape) => shape.aabb, + Self::Triangle(shape) => shape.aabb(), } } @@ -62,7 +62,12 @@ impl CollisionShapes { Self::StaticPlane(shape) => &shape.aabb_ident_cache, Self::TriangleMesh(shape) => &shape.aabb_ident_cache, Self::ConvexHull(shape) => shape.get_ident_aabb(), - Self::Triangle(shape) => &shape.aabb, + Self::Triangle(shape) => { + let aabb = shape.aabb(); + let center = (aabb.min + aabb.max) * 0.5; + let radius = (aabb.max - aabb.min).length() * 0.5; + return (center, radius); + } }; let center = (aabb.min + aabb.max) * 0.5; diff --git a/rocketsim/src/bullet/collision/shapes/optimized_bvh.rs b/rocketsim/src/bullet/collision/shapes/optimized_bvh.rs index e9bcd53d..fbcd45b3 100644 --- a/rocketsim/src/bullet/collision/shapes/optimized_bvh.rs +++ b/rocketsim/src/bullet/collision/shapes/optimized_bvh.rs @@ -10,7 +10,7 @@ fn update_triangle_aabb(triangle: &TriangleShape) -> Aabb { const MIN_AABB_DIMENSION: f32 = 0.002; const MIN_AABB_HALF_DIMENSION: f32 = MIN_AABB_DIMENSION / 2.0; - let mut aabb = triangle.aabb; + let mut aabb = triangle.aabb(); let diff = (aabb.max - aabb.min).cmplt(Vec3A::splat(MIN_AABB_DIMENSION)); if diff.any() { let [x, y, z] = diff.into(); diff --git a/rocketsim/src/bullet/collision/shapes/triangle_shape.rs b/rocketsim/src/bullet/collision/shapes/triangle_shape.rs index e9e33167..28c735ea 100644 --- a/rocketsim/src/bullet/collision/shapes/triangle_shape.rs +++ b/rocketsim/src/bullet/collision/shapes/triangle_shape.rs @@ -12,14 +12,28 @@ pub struct ContactInfo { #[derive(Clone, Copy, Debug, Default)] pub struct TriangleShape { pub points: [Vec3A; 3], - /// `edges` = \[`p1 - p0`, `p2 - p1`, `p0 - p2`] - pub edges: [Vec3A; 3], + pub normal: Vec3A, pub normal_length: f32, - pub aabb: Aabb, } impl TriangleShape { + pub fn edge(&self, index: usize) -> Vec3A { + match index { + 0 => self.points[1] - self.points[0], + 1 => self.points[2] - self.points[1], + 2 => self.points[0] - self.points[2], + _ => unreachable!(), + } + } + + pub fn aabb(&self) -> Aabb { + Aabb { + min: self.points[0].min(self.points[1]).min(self.points[2]), + max: self.points[0].max(self.points[1]).max(self.points[2]), + } + } + /// Create a new triangle from 3 points. pub fn new(points: [Vec3A; 3]) -> Self { let edges = [ @@ -30,17 +44,10 @@ impl TriangleShape { let (normal, normal_length) = edges[0].cross(-edges[2]).normalize_and_length(); - let aabb = Aabb { - min: points[0].min(points[1]).min(points[2]), - max: points[0].max(points[1]).max(points[2]), - }; - Self { points, - edges, normal, normal_length, - aabb, } } @@ -57,17 +64,17 @@ impl TriangleShape { /// Check if a point projected onto the same place as the triangle /// is within the bounds of it. pub fn face_contains(&self, n: Vec3A, obj_to_points: &[Vec3A; 3]) -> bool { - let c0 = self.edges[0].cross(obj_to_points[0]); + let c0 = self.edge(0).cross(obj_to_points[0]); if c0.dot(n) < 0. { return false; } - let c1 = self.edges[1].cross(obj_to_points[1]); + let c1 = self.edge(1).cross(obj_to_points[1]); if c1.dot(n) < 0. { return false; } - let c2 = self.edges[2].cross(obj_to_points[2]); + let c2 = self.edge(2).cross(obj_to_points[2]); c2.dot(n) >= 0. } @@ -75,8 +82,8 @@ impl TriangleShape { /// we use the method described here which is much faster: /// pub fn closest_point(&self, obj_to_points: &[Vec3A; 3]) -> Vec3A { - let ab = self.edges[0]; - let ac = -self.edges[2]; + let ab = self.edge(0); + let ac = -self.edge(2); let d1 = ab.dot(obj_to_points[0]); let d2 = ac.dot(obj_to_points[0]); @@ -111,7 +118,7 @@ impl TriangleShape { let va = d3 * d6 - d5 * d4; if va <= 0. && (d4 - d3) >= 0. && (d5 - d6) >= 0. { let v = (d4 - d3) / ((d4 - d3) + (d5 - d6)); - return self.points[1] + v * self.edges[1]; + return self.points[1] + v * self.edge(1); } let denom = 1. / (va + vb + vc); From c0305eb72cbd60fec9dc7d0ad18b513591316bb0 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 15:11:03 -0500 Subject: [PATCH 07/14] Modernize Rust iteration and glam camera APIs --- .../src/bullet/collision/shapes/convex_hull_shape.rs | 4 +++- .../src/bullet/collision/shapes/triangle_mesh.rs | 4 +++- rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs | 12 ++++-------- rocketsim/tests/rl_comparison_test/compare.rs | 10 +++------- rocketsim/tests/rl_comparison_test/mod.rs | 4 ++-- rocketsim_vis/src/backend/vis_renderer.rs | 9 ++++++--- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/rocketsim/src/bullet/collision/shapes/convex_hull_shape.rs b/rocketsim/src/bullet/collision/shapes/convex_hull_shape.rs index 87973f2a..2137cd6f 100644 --- a/rocketsim/src/bullet/collision/shapes/convex_hull_shape.rs +++ b/rocketsim/src/bullet/collision/shapes/convex_hull_shape.rs @@ -21,7 +21,9 @@ pub struct ConvexHullShape { impl ConvexHullShape { pub fn new(unscaled_points: Box<[Vec3A]>) -> Self { let simd_unscaled_points: Box<_> = unscaled_points - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .map(|chunk| { [ Vec4::new(chunk[0].x, chunk[1].x, chunk[2].x, chunk[3].x), diff --git a/rocketsim/src/bullet/collision/shapes/triangle_mesh.rs b/rocketsim/src/bullet/collision/shapes/triangle_mesh.rs index ed5db1d5..dcc09319 100644 --- a/rocketsim/src/bullet/collision/shapes/triangle_mesh.rs +++ b/rocketsim/src/bullet/collision/shapes/triangle_mesh.rs @@ -13,7 +13,9 @@ impl TriangleMesh { Self { triangles: ids - .chunks_exact(3) + .as_chunks::<3>() + .0 + .iter() .map(|ids| TriangleShape::from_points_iter(ids.iter().map(|&j| verts[j]))) .collect(), } diff --git a/rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs b/rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs index e8806a24..7ca2a2b8 100644 --- a/rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs +++ b/rocketsim/src/bullet/dynamics/vehicle/wheel_info.rs @@ -126,16 +126,12 @@ impl WheelInfo { let proj_vel = contact_normal.dot(self.vel_at_contact_point); let denom = contact_normal.dot(up); - let suspension_relative_vel; - let clipped_inv_contact_dot_suspension; - if denom > 0.1 { + let (suspension_relative_vel, clipped_inv_contact_dot_suspension) = if denom > 0.1 { let inv = 1.0 / denom; - suspension_relative_vel = proj_vel * inv; - clipped_inv_contact_dot_suspension = inv; + (proj_vel * inv, inv) } else { - suspension_relative_vel = 0.0; - clipped_inv_contact_dot_suspension = 10.0; - } + (0.0, 10.0) + }; if is_in_contact_with_world { let ray_pushback_thresh = self.suspension_rest_length_1 + self.wheels_radius diff --git a/rocketsim/tests/rl_comparison_test/compare.rs b/rocketsim/tests/rl_comparison_test/compare.rs index f95185d6..91d6be4c 100644 --- a/rocketsim/tests/rl_comparison_test/compare.rs +++ b/rocketsim/tests/rl_comparison_test/compare.rs @@ -180,13 +180,9 @@ pub fn compare_states_to_tick( tick: &TickRecord, ) -> ComparisonSet { let mut car_comparisons_all: Vec = Vec::new(); - for i in 0..car_states.len() { + for (car_state, car_record) in car_states.iter().zip(&tick.car_records) { let mut car_comparison_set = ComparisonSet::new(); - compare_car( - &car_states[i], - &tick.car_records[i], - &mut car_comparison_set, - ); + compare_car(car_state, car_record, &mut car_comparison_set); car_comparisons_all.push(car_comparison_set); } @@ -195,7 +191,7 @@ pub fn compare_states_to_tick( let mut all_comparisons = ComparisonSet::new(); for (i, car_comparison_set) in car_comparisons_all.iter().enumerate() { - all_comparisons.append_with_prefix(&car_comparison_set, &format!("car_{i}_")); + all_comparisons.append_with_prefix(car_comparison_set, &format!("car_{i}_")); } all_comparisons.append_with_prefix(&ball_comparisons, "ball_"); all_comparisons diff --git a/rocketsim/tests/rl_comparison_test/mod.rs b/rocketsim/tests/rl_comparison_test/mod.rs index 491db190..180f70a4 100644 --- a/rocketsim/tests/rl_comparison_test/mod.rs +++ b/rocketsim/tests/rl_comparison_test/mod.rs @@ -91,8 +91,8 @@ fn test_recording(recording: &Recording) { } } lines.push("CAR CONTROLS DURING TICK:".to_string()); - for j in 0..num_cars { - lines.push(format!(" > Car [{j}]: {:?}", car_states[j].controls)); + for (j, car_state) in car_states.iter().enumerate().take(num_cars) { + lines.push(format!(" > Car [{j}]: {:?}", car_state.controls)); } lines.push("CAR IMPULSES DURING TICK:".to_string()); for j in 0..num_cars { diff --git a/rocketsim_vis/src/backend/vis_renderer.rs b/rocketsim_vis/src/backend/vis_renderer.rs index f19e1b53..fad671fb 100644 --- a/rocketsim_vis/src/backend/vis_renderer.rs +++ b/rocketsim_vis/src/backend/vis_renderer.rs @@ -1,6 +1,9 @@ use std::{sync::Mutex, thread::JoinHandle}; -use glam::{Mat4, Vec2, Vec3, Vec4}; +use glam::{ + Mat4, Vec2, Vec3, Vec4, + camera::lh::{proj::directx, view::look_at_mat4}, +}; use miniquad::{ Bindings, BlendFactor, BlendState, BlendValue, BufferId, BufferLayout, BufferSource, BufferType, BufferUsage, Equation, EventHandler, KeyCode, KeyMods, MouseButton, Pipeline, @@ -362,7 +365,7 @@ impl EventHandler for VisRenderer { fov_degrees: f32, cur_window_size: Vec2, ) -> (Mat4, Mat4) { - let view = Mat4::look_at_lh(pos, look_target, Vec3::Z); + let view = look_at_mat4(pos, look_target, Vec3::Z); let aspect = cur_window_size.x / cur_window_size.y; @@ -370,7 +373,7 @@ impl EventHandler for VisRenderer { const Z_RANGE_FAR: f32 = 50_000.0; let proj = - Mat4::perspective_lh(fov_degrees.to_radians(), aspect, Z_RANGE_NEAR, Z_RANGE_FAR); + directx::perspective(fov_degrees.to_radians(), aspect, Z_RANGE_NEAR, Z_RANGE_FAR); (view, proj) } From 76ab832ef2cb4a819052a38435ba1876fddf7d18 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 15:12:29 -0500 Subject: [PATCH 08/14] Simplify impulse and ball tick APIs --- rocketsim/src/bullet/dynamics/rigid_body.rs | 6 +----- rocketsim/src/sim/arena/base.rs | 1 - rocketsim/src/sim/ball/base.rs | 7 +------ 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/rocketsim/src/bullet/dynamics/rigid_body.rs b/rocketsim/src/bullet/dynamics/rigid_body.rs index 4eb12821..64405634 100644 --- a/rocketsim/src/bullet/dynamics/rigid_body.rs +++ b/rocketsim/src/bullet/dynamics/rigid_body.rs @@ -339,13 +339,9 @@ impl RigidBody { if accum { self.accum_lin_vel += lin_impulse; - } else { - self.lin_vel += lin_impulse; - } - - if accum { self.accum_ang_vel += ang_impulse; } else { + self.lin_vel += lin_impulse; self.ang_vel += ang_impulse; } diff --git a/rocketsim/src/sim/arena/base.rs b/rocketsim/src/sim/arena/base.rs index 7a0c37d2..cfb0a1e5 100644 --- a/rocketsim/src/sim/arena/base.rs +++ b/rocketsim/src/sim/arena/base.rs @@ -538,7 +538,6 @@ impl Arena { self.ball.pre_tick_update( &mut self.bullet_world.bodies_mut()[self.ball.rigid_body_idx], self.config.game_mode, - &self.config.mutators, ); self.bullet_world diff --git a/rocketsim/src/sim/ball/base.rs b/rocketsim/src/sim/ball/base.rs index a71e58e6..3de992f9 100644 --- a/rocketsim/src/sim/ball/base.rs +++ b/rocketsim/src/sim/ball/base.rs @@ -131,12 +131,7 @@ impl Ball { self.state = state; } - pub(crate) fn pre_tick_update( - &mut self, - rb: &mut RigidBody, - game_mode: GameMode, - _mutator_config: &MutatorConfig, // TODO: Remove - ) { + pub(crate) fn pre_tick_update(&mut self, rb: &mut RigidBody, game_mode: GameMode) { match game_mode { GameMode::Heatseeker => { if self.state.hs_info.y_target_dir == 0 { From cd75fb4b6410bf180cb2ef3d017d300e160a81bc Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 15:12:55 -0500 Subject: [PATCH 09/14] Extract rigid body quantization --- rocketsim/src/shared/mod.rs | 1 + rocketsim/src/shared/quantize.rs | 62 ++++++++++++++++++++++++++++++++ rocketsim/src/shared/rsmath.rs | 42 ---------------------- rocketsim/src/sim/arena/base.rs | 31 +++------------- 4 files changed, 67 insertions(+), 69 deletions(-) create mode 100644 rocketsim/src/shared/quantize.rs diff --git a/rocketsim/src/shared/mod.rs b/rocketsim/src/shared/mod.rs index 9586e7c1..bca083a5 100644 --- a/rocketsim/src/shared/mod.rs +++ b/rocketsim/src/shared/mod.rs @@ -1,6 +1,7 @@ mod aabb; pub(crate) mod bvh; mod quad_ray; +pub(crate) mod quantize; pub mod rand; pub mod rsmath; diff --git a/rocketsim/src/shared/quantize.rs b/rocketsim/src/shared/quantize.rs new file mode 100644 index 00000000..be555114 --- /dev/null +++ b/rocketsim/src/shared/quantize.rs @@ -0,0 +1,62 @@ +use glam::Vec3A; + +use crate::{ + bullet::dynamics::rigid_body::RigidBody, + sim::consts::{BT_TO_UU, UU_TO_BT, quantize}, +}; + +/// An extension of `Vec3A::signum` that keeps zero-components as zero (regardless of sign bit). +#[must_use] +fn vec3a_sign_3(v: Vec3A) -> Vec3A { + let signum = v.signum(); + Vec3A::select(v.cmpeq(Vec3A::ZERO), Vec3A::ZERO, signum) +} + +enum VecQuantizeMode { + Position, + Velocity, +} + +/// UE3-networking-style quantization of vectors. +#[must_use] +fn quantize_vec_ue3(vec: Vec3A, scale: f32, quantize_mode: VecQuantizeMode) -> Vec3A { + match quantize_mode { + VecQuantizeMode::Position => (vec * scale).round() / scale, + VecQuantizeMode::Velocity => { + let inv_scale = 1.0 / scale; + let mut rounded = Vec3A::ZERO; + for i in 0..3 { + let i_val = (vec[i] * scale) as i32; + rounded[i] = (i_val as f32) * inv_scale; + } + + const OFFSET_CORRECT_FRAC: f32 = 0.1; + let offset_mag = OFFSET_CORRECT_FRAC * inv_scale; + + rounded + (vec3a_sign_3(rounded) * offset_mag) + } + } +} + +/// Quantizes the position, linear velocity, and angular velocity of a rigid body. +pub fn quantize(body: &mut RigidBody) { + let new_pos = quantize_vec_ue3( + body.get_world_pos() * BT_TO_UU, + quantize::POS_SCALE, + VecQuantizeMode::Position, + ) * UU_TO_BT; + let new_vel = quantize_vec_ue3( + body.lin_vel * BT_TO_UU, + quantize::VEL_SCALE, + VecQuantizeMode::Velocity, + ) * UU_TO_BT; + let new_ang_vel = quantize_vec_ue3( + body.ang_vel * BT_TO_UU, + quantize::ANG_VEL_SCALE, + VecQuantizeMode::Velocity, + ) * UU_TO_BT; + + body.set_world_pos(new_pos); + body.set_lin_vel(new_vel); + body.set_ang_vel(new_ang_vel); +} diff --git a/rocketsim/src/shared/rsmath.rs b/rocketsim/src/shared/rsmath.rs index 15da6323..97d22346 100644 --- a/rocketsim/src/shared/rsmath.rs +++ b/rocketsim/src/shared/rsmath.rs @@ -1,14 +1,5 @@ use glam::Vec3A; -#[must_use] -/// An extension of `Vec3A::signum` that keeps zero-components as zero (regardless of sign bit) -/// -/// Example: `sign_3([-3.0, 0.0, 0.5])` -> `[-1.0, 0.0, 1.0]` -pub fn vec3a_sign_3(v: Vec3A) -> Vec3A { - let signum = v.signum(); - Vec3A::select(v.cmpeq(Vec3A::ZERO), Vec3A::ZERO, signum) -} - #[must_use] pub fn try_calc_tri_normal(points: &[Vec3A; 3]) -> Option { (points[1] - points[0]) @@ -78,36 +69,3 @@ pub fn closest_points_between_segments( pub fn to_2d(v: Vec3A) -> Vec3A { v.truncate().extend(0.0).to_vec3a() } - -pub enum VecQuantizeMode { - Position, - Velocity, -} -#[must_use] -/// UE3-networking-style quantization of vectors -pub fn quantize_vec_ue3(vec: Vec3A, scale: f32, quantize_mode: VecQuantizeMode) -> Vec3A { - match quantize_mode { - VecQuantizeMode::Position => { - // Simple rounding method - // - // NOTE: Rocket League does something that's slightly different *sometimes*, - // possibly due to fast math float optimizations or something of that sort, - // but since a simple round is already perfect in 95% of cases, and the - // occasional errors are so tiny (e.g. 0.0001uu), I can't be bothered lol - (vec * scale).round() / scale - } - VecQuantizeMode::Velocity => { - let inv_scale = 1.0 / scale; - let mut rounded = Vec3A::ZERO; - for i in 0..3 { - let i_val = (vec[i] * scale) as i32; - rounded[i] = (i_val as f32) * inv_scale; - } - - const OFFSET_CORRECT_FRAC: f32 = 0.1; - let offset_mag = OFFSET_CORRECT_FRAC * inv_scale; - - rounded + (vec3a_sign_3(rounded) * offset_mag) - } - } -} diff --git a/rocketsim/src/sim/arena/base.rs b/rocketsim/src/sim/arena/base.rs index cfb0a1e5..04de9601 100644 --- a/rocketsim/src/sim/arena/base.rs +++ b/rocketsim/src/sim/arena/base.rs @@ -1,6 +1,4 @@ use super::ArenaContactTracker; -use crate::shared::rsmath; -use crate::shared::rsmath::VecQuantizeMode; use crate::{ ARENA_COLLISION_SHAPES, ArenaConfig, ArenaEvent::{BallHitWorld, CarPickupBoost}, @@ -22,6 +20,7 @@ use crate::{ }, consts::{self, BT_TO_UU, TICK_RATE, TICK_TIME, UU_TO_BT}, make_tile_shapes, + shared::quantize, sim::{ ArenaEvent, Ball, BallState, BoostPad, CarHitBallEvent, CarHitCarEvent, CarHitWorldEvent, DemoMode, UserInfoTypes, arena::ArenaEventList, @@ -476,41 +475,19 @@ impl Arena { // Limit velocities, then quantize physics values { - use consts::{ball, car, quantize}; - - fn quantize_rb(rb: &mut RigidBody) { - let new_pos = rsmath::quantize_vec_ue3( - rb.get_world_pos() * BT_TO_UU, - quantize::POS_SCALE, - VecQuantizeMode::Position, - ) * UU_TO_BT; - let new_vel = rsmath::quantize_vec_ue3( - rb.lin_vel * BT_TO_UU, - quantize::VEL_SCALE, - VecQuantizeMode::Velocity, - ) * UU_TO_BT; - let new_ang_vel = rsmath::quantize_vec_ue3( - rb.ang_vel * BT_TO_UU, - quantize::ANG_VEL_SCALE, - VecQuantizeMode::Velocity, - ) * UU_TO_BT; - - rb.set_world_pos(new_pos); - rb.set_lin_vel(new_vel); - rb.set_ang_vel(new_ang_vel); - } + use consts::{ball, car}; for car_idx in 0..self.cars.len() { let car_rb = &mut self.bullet_world.bodies_mut()[self.cars[car_idx].rigid_body_idx]; car_rb.limit_vels(car::MAX_SPEED * UU_TO_BT, car::MAX_ANG_SPEED); - quantize_rb(car_rb); + quantize::quantize(car_rb); } let ball_rb = &mut self.bullet_world.bodies_mut()[self.ball.rigid_body_idx]; ball_rb.limit_vels( self.config.mutators.ball_max_speed * UU_TO_BT, ball::MAX_ANG_SPEED, ); - quantize_rb(ball_rb); + quantize::quantize(ball_rb); } // Update ball activation From 6a76a273d2d97901f85bdc8ece73eba781fddd3a Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 15:13:46 -0500 Subject: [PATCH 10/14] Reorganize shared and linear math utilities --- .../collision/dispatch/box_box_detector.rs | 2 +- .../seq_impulse_constraint_solver.rs | 5 +- rocketsim/src/bullet/dynamics/rigid_body.rs | 2 +- .../src/bullet/linear_math/affine_ext.rs | 21 ++++ rocketsim/src/bullet/linear_math/mod.rs | 117 ++---------------- rocketsim/src/bullet/linear_math/quat_ext.rs | 16 +++ rocketsim/src/bullet/linear_math/util.rs | 64 ++++++++++ .../{bullet/linear_math => shared}/angle.rs | 0 rocketsim/src/shared/mod.rs | 2 + rocketsim/src/sim/ball/base.rs | 2 +- 10 files changed, 118 insertions(+), 113 deletions(-) create mode 100644 rocketsim/src/bullet/linear_math/affine_ext.rs create mode 100644 rocketsim/src/bullet/linear_math/quat_ext.rs create mode 100644 rocketsim/src/bullet/linear_math/util.rs rename rocketsim/src/{bullet/linear_math => shared}/angle.rs (100%) diff --git a/rocketsim/src/bullet/collision/dispatch/box_box_detector.rs b/rocketsim/src/bullet/collision/dispatch/box_box_detector.rs index 4649ca94..a69ecb1e 100644 --- a/rocketsim/src/bullet/collision/dispatch/box_box_detector.rs +++ b/rocketsim/src/bullet/collision/dispatch/box_box_detector.rs @@ -14,7 +14,7 @@ use crate::bullet::{ shapes::box_shape::BoxShape, }, dynamics::rigid_body::RigidBody, - linear_math::obb::Obb, + linear_math::Obb, }; fn line_closest_approach(pa: Vec3A, ua: Vec3A, pb: Vec3A, ub: Vec3A) -> f32 { diff --git a/rocketsim/src/bullet/dynamics/constraint_solver/seq_impulse_constraint_solver.rs b/rocketsim/src/bullet/dynamics/constraint_solver/seq_impulse_constraint_solver.rs index dd6d5508..8f768dc1 100644 --- a/rocketsim/src/bullet/dynamics/constraint_solver/seq_impulse_constraint_solver.rs +++ b/rocketsim/src/bullet/dynamics/constraint_solver/seq_impulse_constraint_solver.rs @@ -6,10 +6,7 @@ use crate::bullet::{ manifold_point::ManifoldPoint, persistent_manifold::PersistentManifold, }, dynamics::rigid_body::{CollisionFlags, RigidBody}, - linear_math::{ - plane_space_1, - transform_util::{integrate_trans, integrate_trans_no_rot}, - }, + linear_math::{integrate_trans, integrate_trans_no_rot, plane_space_1}, }; struct SpecialResolveInfo { diff --git a/rocketsim/src/bullet/dynamics/rigid_body.rs b/rocketsim/src/bullet/dynamics/rigid_body.rs index 64405634..28bd9ff4 100644 --- a/rocketsim/src/bullet/dynamics/rigid_body.rs +++ b/rocketsim/src/bullet/dynamics/rigid_body.rs @@ -8,7 +8,7 @@ use indexmap::IndexMap; use crate::{ bullet::{ collision::shapes::collision_shape::CollisionShapes, - linear_math::transform_util::{integrate_trans, integrate_trans_no_rot}, + linear_math::{integrate_trans, integrate_trans_no_rot}, }, sim::UserInfoTypes, }; diff --git a/rocketsim/src/bullet/linear_math/affine_ext.rs b/rocketsim/src/bullet/linear_math/affine_ext.rs new file mode 100644 index 00000000..0ddc6984 --- /dev/null +++ b/rocketsim/src/bullet/linear_math/affine_ext.rs @@ -0,0 +1,21 @@ +use glam::{Affine3A, Vec3A}; + +pub trait AffineExt { + fn transpose(&self) -> Self; + fn inv_x_form(&self, in_vec: Vec3A) -> Vec3A; +} + +impl AffineExt for Affine3A { + fn transpose(&self) -> Self { + let matrix3 = self.matrix3.transpose(); + + Self { + matrix3, + translation: matrix3 * -self.translation, + } + } + + fn inv_x_form(&self, in_vec: Vec3A) -> Vec3A { + self.matrix3.mul_transpose_vec3a(in_vec - self.translation) + } +} diff --git a/rocketsim/src/bullet/linear_math/mod.rs b/rocketsim/src/bullet/linear_math/mod.rs index c8ccaf72..c3dba07e 100644 --- a/rocketsim/src/bullet/linear_math/mod.rs +++ b/rocketsim/src/bullet/linear_math/mod.rs @@ -1,106 +1,11 @@ -use std::f32::consts::FRAC_1_SQRT_2; - -use glam::{Affine3A, Quat, Vec3A, Vec4}; -pub mod angle; -pub mod obb; -pub mod transform_util; - -pub trait AffineExt { - fn transpose(&self) -> Self; - fn inv_x_form(&self, in_vec: Vec3A) -> Vec3A; -} - -impl AffineExt for Affine3A { - fn transpose(&self) -> Self { - let matrix3 = self.matrix3.transpose(); - - Self { - matrix3, - translation: matrix3 * -self.translation, - } - } - - fn inv_x_form(&self, in_vec: Vec3A) -> Vec3A { - self.matrix3.mul_transpose_vec3a(in_vec - self.translation) - } -} - -pub trait QuatExt { - fn from_axis_angle_simd(axis: Vec3A, angle: f32) -> Self; -} - -impl QuatExt for Quat { - #[inline] - /// An implementation of `Quat::from_axis_angle` that leverages simd - fn from_axis_angle_simd(axis: Vec3A, angle: f32) -> Self { - debug_assert!(axis.is_normalized()); - let (s, c) = f32::sin_cos(angle * 0.5); - let v = axis * s; - Self::from_xyzw(v.x, v.y, v.z, c) - } -} - -#[inline] -pub fn interpolate_3(v0: Vec3A, v1: Vec3A, rt: f32) -> Vec3A { - let s = 1.0 - rt; - s * v0 + rt * v1 -} - -pub fn plane_space_2(n: Vec3A) -> (Vec3A, Vec3A) { - if n.z.abs() > FRAC_1_SQRT_2 { - // choose p in y-z plane - let a = n.y * n.y + n.z * n.z; - let k = a.sqrt().recip(); - let p = Vec3A::new(0., -n.z * k, n.y * k); - (p, Vec3A::new(a * k, -n.x * p.z, n.x * p.y)) - } else { - // choose p in x-y plane - let a = n.x * n.x + n.y * n.y; - let k = a.sqrt().recip(); - let p = Vec3A::new(-n.y * k, n.x * k, 0.); - (p, Vec3A::new(-n.z * p.y, n.z * p.x, a * k)) - } -} - -pub fn plane_space_1(n: Vec3A) -> Vec3A { - if n.z.abs() > FRAC_1_SQRT_2 { - // choose p in y-z plane - let a = n.y * n.y + n.z * n.z; - let k = a.sqrt().recip(); - Vec3A::new(0., -n.z * k, n.y * k) - } else { - // choose p in x-y plane - let a = n.x * n.x + n.y * n.y; - let k = a.sqrt().recip(); - Vec3A::new(-n.y * k, n.x * k, 0.) - } -} - -pub fn max_dot(simd_points: &[[Vec4; 3]], points: &[Vec3A], direction: Vec3A) -> Vec3A { - let mut max_dot = f32::NEG_INFINITY; - let mut support_vertex = Vec3A::ZERO; - - let dir_x = Vec4::splat(direction.x); - let dir_y = Vec4::splat(direction.y); - let dir_z = Vec4::splat(direction.z); - - for (i, pts) in simd_points.iter().enumerate() { - let dots = pts[0] * dir_x + pts[1] * dir_y + pts[2] * dir_z; - - let this_max_dot = dots.max_element(); - if this_max_dot > max_dot { - max_dot = this_max_dot; - support_vertex = points[i * 4 + dots.max_position()]; - } - } - - for &point in &points[simd_points.len() * 4..] { - let dot = direction.dot(point); - if dot > max_dot { - support_vertex = point; - max_dot = dot; - } - } - - support_vertex -} +mod affine_ext; +mod obb; +mod quat_ext; +mod transform_util; +mod util; + +pub(super) use affine_ext::AffineExt; +pub(super) use obb::Obb; +pub(super) use quat_ext::QuatExt; +pub(super) use transform_util::{integrate_trans, integrate_trans_no_rot}; +pub(super) use util::{interpolate_3, max_dot, plane_space_1, plane_space_2}; diff --git a/rocketsim/src/bullet/linear_math/quat_ext.rs b/rocketsim/src/bullet/linear_math/quat_ext.rs new file mode 100644 index 00000000..6b5eebc3 --- /dev/null +++ b/rocketsim/src/bullet/linear_math/quat_ext.rs @@ -0,0 +1,16 @@ +use glam::{Quat, Vec3A}; + +pub trait QuatExt { + fn from_axis_angle_simd(axis: Vec3A, angle: f32) -> Self; +} + +impl QuatExt for Quat { + /// An implementation of `Quat::from_axis_angle` that leverages SIMD. + #[inline] + fn from_axis_angle_simd(axis: Vec3A, angle: f32) -> Self { + debug_assert!(axis.is_normalized()); + let (s, c) = f32::sin_cos(angle * 0.5); + let v = axis * s; + Self::from_xyzw(v.x, v.y, v.z, c) + } +} diff --git a/rocketsim/src/bullet/linear_math/util.rs b/rocketsim/src/bullet/linear_math/util.rs new file mode 100644 index 00000000..135611de --- /dev/null +++ b/rocketsim/src/bullet/linear_math/util.rs @@ -0,0 +1,64 @@ +use std::f32::consts::FRAC_1_SQRT_2; + +use glam::{Vec3A, Vec4}; + +#[inline] +pub fn interpolate_3(v0: Vec3A, v1: Vec3A, rt: f32) -> Vec3A { + let s = 1.0 - rt; + s * v0 + rt * v1 +} + +pub fn plane_space_2(n: Vec3A) -> (Vec3A, Vec3A) { + if n.z.abs() > FRAC_1_SQRT_2 { + let a = n.y * n.y + n.z * n.z; + let k = a.sqrt().recip(); + let p = Vec3A::new(0., -n.z * k, n.y * k); + (p, Vec3A::new(a * k, -n.x * p.z, n.x * p.y)) + } else { + let a = n.x * n.x + n.y * n.y; + let k = a.sqrt().recip(); + let p = Vec3A::new(-n.y * k, n.x * k, 0.); + (p, Vec3A::new(-n.z * p.y, n.z * p.x, a * k)) + } +} + +pub fn plane_space_1(n: Vec3A) -> Vec3A { + if n.z.abs() > FRAC_1_SQRT_2 { + let a = n.y * n.y + n.z * n.z; + let k = a.sqrt().recip(); + Vec3A::new(0., -n.z * k, n.y * k) + } else { + let a = n.x * n.x + n.y * n.y; + let k = a.sqrt().recip(); + Vec3A::new(-n.y * k, n.x * k, 0.) + } +} + +pub fn max_dot(simd_points: &[[Vec4; 3]], points: &[Vec3A], direction: Vec3A) -> Vec3A { + let mut max_dot = f32::NEG_INFINITY; + let mut support_vertex = Vec3A::ZERO; + + let dir_x = Vec4::splat(direction.x); + let dir_y = Vec4::splat(direction.y); + let dir_z = Vec4::splat(direction.z); + + for (i, pts) in simd_points.iter().enumerate() { + let dots = pts[0] * dir_x + pts[1] * dir_y + pts[2] * dir_z; + + let this_max_dot = dots.max_element(); + if this_max_dot > max_dot { + max_dot = this_max_dot; + support_vertex = points[i * 4 + dots.max_position()]; + } + } + + for &point in &points[simd_points.len() * 4..] { + let dot = direction.dot(point); + if dot > max_dot { + support_vertex = point; + max_dot = dot; + } + } + + support_vertex +} diff --git a/rocketsim/src/bullet/linear_math/angle.rs b/rocketsim/src/shared/angle.rs similarity index 100% rename from rocketsim/src/bullet/linear_math/angle.rs rename to rocketsim/src/shared/angle.rs diff --git a/rocketsim/src/shared/mod.rs b/rocketsim/src/shared/mod.rs index bca083a5..71c3c8a4 100644 --- a/rocketsim/src/shared/mod.rs +++ b/rocketsim/src/shared/mod.rs @@ -1,4 +1,5 @@ mod aabb; +mod angle; pub(crate) mod bvh; mod quad_ray; pub(crate) mod quantize; @@ -6,4 +7,5 @@ pub mod rand; pub mod rsmath; pub use aabb::*; +pub use angle::Angle; pub use quad_ray::*; diff --git a/rocketsim/src/sim/ball/base.rs b/rocketsim/src/sim/ball/base.rs index 3de992f9..130f30e6 100644 --- a/rocketsim/src/sim/ball/base.rs +++ b/rocketsim/src/sim/ball/base.rs @@ -18,10 +18,10 @@ use crate::{ discrete_dynamics_world::DiscreteDynamicsWorld, rigid_body::{ActivationState, CollisionFlags, RigidBody, RigidBodyConstructionInfo}, }, - linear_math::angle::Angle, }, consts::{BT_TO_UU, UU_TO_BT, dropshot, heatseeker, snowday}, get_neighbor_indices_1, get_neighbor_indices_2, get_tile_pos, + shared::Angle, sim::{UserInfoTypes, consts}, }; From b2d14d0f746c1e5e53ac3ea842997a93ce1d4ce5 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 15:14:01 -0500 Subject: [PATCH 11/14] Restrict internal physics modules --- rocketsim/src/bullet/collision/dispatch/mod.rs | 4 ++-- rocketsim/src/bullet/collision/narrowphase/mod.rs | 2 +- rocketsim/src/bullet/collision/shapes/mod.rs | 6 +++--- rocketsim/src/bullet/dynamics/constraint_solver/mod.rs | 6 +++--- rocketsim/src/bullet/dynamics/mod.rs | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/rocketsim/src/bullet/collision/dispatch/mod.rs b/rocketsim/src/bullet/collision/dispatch/mod.rs index 574e6df3..4102ebe7 100644 --- a/rocketsim/src/bullet/collision/dispatch/mod.rs +++ b/rocketsim/src/bullet/collision/dispatch/mod.rs @@ -1,5 +1,5 @@ mod box_box_detector; -pub mod collision_dispatcher; +pub(super) mod collision_dispatcher; mod collision_obj_wrapper; pub mod collision_world; mod compound_collision_alg; @@ -11,4 +11,4 @@ mod obb_obb_collision_alg; pub mod quad_ray_callbacks; mod sphere_concave_collision_alg; mod sphere_obb_collision_alg; -pub mod tri_bvh_util; +pub(super) mod tri_bvh_util; diff --git a/rocketsim/src/bullet/collision/narrowphase/mod.rs b/rocketsim/src/bullet/collision/narrowphase/mod.rs index 3eede27a..18d8dfd0 100644 --- a/rocketsim/src/bullet/collision/narrowphase/mod.rs +++ b/rocketsim/src/bullet/collision/narrowphase/mod.rs @@ -1,3 +1,3 @@ -pub mod gjk; +pub(super) mod gjk; pub mod manifold_point; pub mod persistent_manifold; diff --git a/rocketsim/src/bullet/collision/shapes/mod.rs b/rocketsim/src/bullet/collision/shapes/mod.rs index 03f40234..fae556f2 100644 --- a/rocketsim/src/bullet/collision/shapes/mod.rs +++ b/rocketsim/src/bullet/collision/shapes/mod.rs @@ -9,8 +9,8 @@ mod optimized_bvh; mod polyhedral_convex_shape; pub mod sphere_shape; pub mod static_plane_shape; -pub mod triangle_callback; -pub mod triangle_info_map; +pub(super) mod triangle_callback; +pub(super) mod triangle_info_map; pub mod triangle_mesh; mod triangle_mesh_shape; -pub mod triangle_shape; +pub(super) mod triangle_shape; diff --git a/rocketsim/src/bullet/dynamics/constraint_solver/mod.rs b/rocketsim/src/bullet/dynamics/constraint_solver/mod.rs index c4f760a0..a67de7b3 100644 --- a/rocketsim/src/bullet/dynamics/constraint_solver/mod.rs +++ b/rocketsim/src/bullet/dynamics/constraint_solver/mod.rs @@ -1,6 +1,6 @@ -pub mod contact_constraint; -pub mod contact_solver_info; +pub(super) mod contact_constraint; +pub(super) mod contact_solver_info; mod jacobian_entry; -pub mod seq_impulse_constraint_solver; +pub(super) mod seq_impulse_constraint_solver; pub mod solver_body; mod solver_constraint; diff --git a/rocketsim/src/bullet/dynamics/mod.rs b/rocketsim/src/bullet/dynamics/mod.rs index d866e85d..603db4f0 100644 --- a/rocketsim/src/bullet/dynamics/mod.rs +++ b/rocketsim/src/bullet/dynamics/mod.rs @@ -1,4 +1,4 @@ -pub mod constraint_solver; +pub(super) mod constraint_solver; pub mod discrete_dynamics_world; pub mod rigid_body; pub mod vehicle; From cbe43e25227446ad5cfea9f6dce0cb4fb1ec3248 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 15:15:50 -0500 Subject: [PATCH 12/14] Remove unused second plane-space vector --- .../bullet/collision/narrowphase/manifold_point.rs | 4 ++-- .../collision/narrowphase/persistent_manifold.rs | 4 ++-- rocketsim/src/bullet/linear_math/mod.rs | 2 +- rocketsim/src/bullet/linear_math/util.rs | 14 -------------- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/rocketsim/src/bullet/collision/narrowphase/manifold_point.rs b/rocketsim/src/bullet/collision/narrowphase/manifold_point.rs index 0fb267a8..65465068 100644 --- a/rocketsim/src/bullet/collision/narrowphase/manifold_point.rs +++ b/rocketsim/src/bullet/collision/narrowphase/manifold_point.rs @@ -1,7 +1,7 @@ use glam::Vec3A; use crate::bullet::{ - dynamics::constraint_solver::solver_body::SolverBody, linear_math::plane_space_2, + dynamics::constraint_solver::solver_body::SolverBody, linear_math::plane_space_1, }; #[derive(Debug, Default, Copy, Clone)] @@ -55,7 +55,7 @@ impl ManifoldPoint { if lat_rel_vel > f32::EPSILON { self.lateral_friction_dir_1 *= 1.0 / lat_rel_vel.sqrt(); } else { - self.lateral_friction_dir_1 = plane_space_2(self.normal_world_on_b).0; + self.lateral_friction_dir_1 = plane_space_1(self.normal_world_on_b); } } } diff --git a/rocketsim/src/bullet/collision/narrowphase/persistent_manifold.rs b/rocketsim/src/bullet/collision/narrowphase/persistent_manifold.rs index 6c8af4f4..7869140a 100644 --- a/rocketsim/src/bullet/collision/narrowphase/persistent_manifold.rs +++ b/rocketsim/src/bullet/collision/narrowphase/persistent_manifold.rs @@ -4,7 +4,7 @@ use glam::{Vec3A, Vec4}; use super::manifold_point::ManifoldPoint; use crate::bullet::{ dynamics::rigid_body::{CollisionFlags, RigidBody}, - linear_math::{AffineExt, plane_space_2}, + linear_math::{AffineExt, plane_space_1}, }; pub trait ContactAddedCallback { @@ -201,7 +201,7 @@ impl PersistentManifold { new_pt.combined_friction = Self::calculate_combined_friction(body0, body1); new_pt.combined_restitution = Self::calculate_combined_restitution(body0, body1); - new_pt.lateral_friction_dir_1 = plane_space_2(new_pt.normal_world_on_b).0; + new_pt.lateral_friction_dir_1 = plane_space_1(new_pt.normal_world_on_b); let insert_idx = self.add_manifold_point(new_pt); diff --git a/rocketsim/src/bullet/linear_math/mod.rs b/rocketsim/src/bullet/linear_math/mod.rs index c3dba07e..74fc1dc1 100644 --- a/rocketsim/src/bullet/linear_math/mod.rs +++ b/rocketsim/src/bullet/linear_math/mod.rs @@ -8,4 +8,4 @@ pub(super) use affine_ext::AffineExt; pub(super) use obb::Obb; pub(super) use quat_ext::QuatExt; pub(super) use transform_util::{integrate_trans, integrate_trans_no_rot}; -pub(super) use util::{interpolate_3, max_dot, plane_space_1, plane_space_2}; +pub(super) use util::{interpolate_3, max_dot, plane_space_1}; diff --git a/rocketsim/src/bullet/linear_math/util.rs b/rocketsim/src/bullet/linear_math/util.rs index 135611de..19fa765e 100644 --- a/rocketsim/src/bullet/linear_math/util.rs +++ b/rocketsim/src/bullet/linear_math/util.rs @@ -8,20 +8,6 @@ pub fn interpolate_3(v0: Vec3A, v1: Vec3A, rt: f32) -> Vec3A { s * v0 + rt * v1 } -pub fn plane_space_2(n: Vec3A) -> (Vec3A, Vec3A) { - if n.z.abs() > FRAC_1_SQRT_2 { - let a = n.y * n.y + n.z * n.z; - let k = a.sqrt().recip(); - let p = Vec3A::new(0., -n.z * k, n.y * k); - (p, Vec3A::new(a * k, -n.x * p.z, n.x * p.y)) - } else { - let a = n.x * n.x + n.y * n.y; - let k = a.sqrt().recip(); - let p = Vec3A::new(-n.y * k, n.x * k, 0.); - (p, Vec3A::new(-n.z * p.y, n.z * p.x, a * k)) - } -} - pub fn plane_space_1(n: Vec3A) -> Vec3A { if n.z.abs() > FRAC_1_SQRT_2 { let a = n.y * n.y + n.z * n.z; From 9c11e82b65b4bb05a3315f1ece40f474d710fa4e Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 15:29:33 -0500 Subject: [PATCH 13/14] Upgrade syn to 3.0 --- rocketsim_derive/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketsim_derive/Cargo.toml b/rocketsim_derive/Cargo.toml index 4a98d1de..c2e02419 100644 --- a/rocketsim_derive/Cargo.toml +++ b/rocketsim_derive/Cargo.toml @@ -8,4 +8,4 @@ proc-macro = true [dependencies] quote = "1.0.44" -syn = { version = "2.0.114", features = ["full"] } +syn = { version = "3.0", features = ["full"] } From c55e0cd747dd0f5a8824f156f7607eb4052cc626 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 22 Jul 2026 16:34:14 -0500 Subject: [PATCH 14/14] Add arena memory usage modes --- README.md | 12 +++ rocketsim/examples/stress_common/mod.rs | 12 +++ rocketsim/examples/stress_v3.rs | 95 +++++++++++-------- .../collision/broadphase/grid_broadphase.rs | 22 +++++ rocketsim/src/sim/arena/arena_config.rs | 49 ++++++++++ rocketsim/src/sim/arena/base.rs | 9 +- 6 files changed, 154 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 1df2768a..bd15f135 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,15 @@ It does not care for Bullet compatibility, and removes as many abstrations as po This project is currently a massive WIP, and highly experimental. Lots of features found in RocketSim are currently missing. + +## Arena memory modes + +`ArenaConfig::mem_weight_mode` controls the broadphase memory/performance tradeoff. In the `stress_v3` benchmark: + +| Mode | 1v1 memory per arena | 1v1 performance | 3v3 memory per arena | 3v3 performance | +| --- | ---: | ---: | ---: | ---: | +| `Heavy` (default) | ~915 KiB | Baseline | ~946 KiB | Baseline | +| `Balanced` | ~108 KiB | ~1–2% slower | ~123 KiB | ~3% slower | +| `Light` | ~66 KiB | ~9% slower | ~91 KiB | ~12% slower | + +Balanced reduces marginal memory while retaining most of Heavy's performance. Light effectively disables grid partitioning by using a single broadphase cell, prioritizing minimum memory over performance. Results may vary by platform and workload. diff --git a/rocketsim/examples/stress_common/mod.rs b/rocketsim/examples/stress_common/mod.rs index 0672dffa..94655919 100644 --- a/rocketsim/examples/stress_common/mod.rs +++ b/rocketsim/examples/stress_common/mod.rs @@ -21,12 +21,24 @@ pub enum GameModeArg { TheVoid, } +#[derive(Clone, Copy, Debug, ValueEnum)] +#[clap(rename_all = "kebab-case")] +pub enum MemWeightModeArg { + Light, + Balanced, + Heavy, +} + #[derive(Parser)] pub struct Args { #[arg(short, long, default_value_t = NUM_CARS)] pub num_cars: u8, #[arg(short, long, value_enum, default_value_t = GameModeArg::Soccar)] pub game_mode: GameModeArg, + #[arg(long, value_enum, default_value_t = MemWeightModeArg::Heavy)] + pub mem_weight_mode: MemWeightModeArg, + #[arg(long, default_value_t = 1)] + pub num_arenas: usize, } #[derive(Clone, Copy, Debug, Default)] diff --git a/rocketsim/examples/stress_v3.rs b/rocketsim/examples/stress_v3.rs index 59772b30..29c26521 100644 --- a/rocketsim/examples/stress_v3.rs +++ b/rocketsim/examples/stress_v3.rs @@ -4,12 +4,13 @@ use clap::Parser; use fastrand::Rng; use glam::Vec3A; use rocketsim::{ - Arena, ArenaConfig, ArenaEvent, BallState, CarBodyConfig, CarControls, CarState, GameMode, - Team, consts, init_from_default, + Arena, ArenaConfig, ArenaEvent, ArenaMemWeightMode, BallState, CarBodyConfig, CarControls, + CarState, GameMode, Team, consts, init_from_default, }; use stress_common::{ - Args, BotBallState, BotCarState, BotControls, GameModeArg, NUM_EPISODE, NUM_EPISODE_TICKS, - UPDATE_CHANCE, VEL_ADD_MAG, calc_bot_controls, print_results, rand_axis_val, rand_chance, + Args, BotBallState, BotCarState, BotControls, GameModeArg, MemWeightModeArg, NUM_EPISODE, + NUM_EPISODE_TICKS, UPDATE_CHANCE, VEL_ADD_MAG, calc_bot_controls, print_results, rand_axis_val, + rand_chance, }; mod stress_common; @@ -27,6 +28,16 @@ impl From for GameMode { } } +impl From for ArenaMemWeightMode { + fn from(value: MemWeightModeArg) -> Self { + match value { + MemWeightModeArg::Light => Self::Light, + MemWeightModeArg::Balanced => Self::Balanced, + MemWeightModeArg::Heavy => Self::Heavy, + } + } +} + fn bot_car_state(car_state: &CarState) -> BotCarState { BotCarState { pos: car_state.phys.pos, @@ -63,57 +74,59 @@ fn main() { let cli = Args::parse(); init_from_default(true).unwrap(); - let mut arena = Arena::new_with_config(ArenaConfig { - rng_seed: Some(0), - ..ArenaConfig::new(cli.game_mode.into()) - }); - - let mut rng = Rng::with_seed(0); - let mut ids = Vec::with_capacity(cli.num_cars as usize); - for i in 0..cli.num_cars { - let id = arena.add_car(Team::try_from(i % 2).unwrap(), CarBodyConfig::OCTANE); - ids.push(id); - } + let arena_config = ArenaConfig::new(cli.game_mode.into()) + .with_rng_seed(0) + .with_mem_weight_mode(cli.mem_weight_mode.into()); + let mut arenas: Vec<_> = (0..cli.num_arenas) + .map(|arena_idx| { + let mut arena_config = arena_config.clone(); + arena_config.rng_seed = Some(arena_idx as u64); + let mut arena = Arena::new_with_config(arena_config); + let ids: Vec<_> = (0..cli.num_cars) + .map(|car_idx| { + arena.add_car(Team::try_from(car_idx % 2).unwrap(), CarBodyConfig::OCTANE) + }) + .collect(); + (arena, ids, Rng::with_seed(arena_idx as u64)) + }) + .collect(); let mut total_ball_touches = 0; let start = Instant::now(); for _ in 0..NUM_EPISODE { - { - // Set up new episode - // Reset to kickoff + for (arena, _, rng) in &mut arenas { arena.reset_to_random_kickoff(None); - // Accelerate the ball randomly let mut ball_state = *arena.get_ball_state(); - ball_state.phys.vel += Vec3A::new( - rand_axis_val(&mut rng), - rand_axis_val(&mut rng), - rand_axis_val(&mut rng), - ) * VEL_ADD_MAG; + ball_state.phys.vel += + Vec3A::new(rand_axis_val(rng), rand_axis_val(rng), rand_axis_val(rng)) + * VEL_ADD_MAG; arena.set_ball_state(ball_state); } for _ in 0..NUM_EPISODE_TICKS { - let ball_state = bot_ball_state(arena.get_ball_state()); - for &idx in &ids { - let car_state = arena.get_car_state(idx); - - if rand_chance(&mut rng, UPDATE_CHANCE) { - let controls = calc_bot_controls( - &mut rng, - bot_car_state(car_state), - ball_state, - consts::car::MAX_SPEED, - ); - arena.set_car_controls(idx, car_controls(controls)); + for (arena, ids, rng) in &mut arenas { + let ball_state = bot_ball_state(arena.get_ball_state()); + for &idx in ids.iter() { + let car_state = arena.get_car_state(idx); + + if rand_chance(rng, UPDATE_CHANCE) { + let controls = calc_bot_controls( + rng, + bot_car_state(car_state), + ball_state, + consts::car::MAX_SPEED, + ); + arena.set_car_controls(idx, car_controls(controls)); + } } - } - arena.step_tick(); - for event in arena.get_last_step_events() { - if let ArenaEvent::CarHitBall(_car_hit_ball_event) = event { - total_ball_touches += 1; + arena.step_tick(); + for event in arena.get_last_step_events() { + if let ArenaEvent::CarHitBall(_car_hit_ball_event) = event { + total_ball_touches += 1; + } } } } diff --git a/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs b/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs index 241a51c1..66b15387 100644 --- a/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs +++ b/rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs @@ -200,6 +200,11 @@ impl GridBroadphase { } } + #[cfg(test)] + pub(crate) const fn num_cells(&self) -> USizeVec3 { + self.cell_grid.num_cells + } + pub fn set_aabb(&mut self, col_obj: &RigidBody, proxy_idx: usize, aabb: Aabb) { let sbp = &mut self.handles[proxy_idx]; sbp.aabb = aabb; @@ -346,3 +351,20 @@ impl GridBroadphase { } } } + +#[cfg(test)] +mod tests { + use glam::{USizeVec3, Vec3A}; + + use super::GridBroadphase; + + #[test] + fn arena_sized_cell_disables_grid_partitioning() { + let min_pos = Vec3A::new(-5600.0, -6000.0, 0.0); + let max_pos = Vec3A::new(5600.0, 6000.0, 2200.0); + let cell_size = (max_pos - min_pos).max_element(); + let broadphase = GridBroadphase::new(min_pos, max_pos, cell_size, 1); + + assert_eq!(broadphase.num_cells(), USizeVec3::ONE); + } +} diff --git a/rocketsim/src/sim/arena/arena_config.rs b/rocketsim/src/sim/arena/arena_config.rs index 6923666e..0be91ca3 100644 --- a/rocketsim/src/sim/arena/arena_config.rs +++ b/rocketsim/src/sim/arena/arena_config.rs @@ -6,6 +6,7 @@ use crate::{BoostPadConfig, GameMode, MutatorConfig}; pub enum ArenaMemWeightMode { #[default] Heavy, + Balanced, Light, } @@ -52,4 +53,52 @@ impl ArenaConfig { ..Self::DEFAULT } } + + #[must_use] + pub fn with_mutators(mut self, mutators: MutatorConfig) -> Self { + self.mutators = mutators; + self + } + + #[must_use] + pub fn with_mem_weight_mode(mut self, mem_weight_mode: ArenaMemWeightMode) -> Self { + self.mem_weight_mode = mem_weight_mode; + self + } + + #[must_use] + pub fn with_min_pos(mut self, min_pos: Vec3A) -> Self { + self.min_pos = min_pos; + self + } + + #[must_use] + pub fn with_max_pos(mut self, max_pos: Vec3A) -> Self { + self.max_pos = max_pos; + self + } + + #[must_use] + pub fn with_max_aabb_len(mut self, max_aabb_len: f32) -> Self { + self.max_aabb_len = max_aabb_len; + self + } + + #[must_use] + pub fn with_no_ball_rot(mut self, no_ball_rot: bool) -> Self { + self.no_ball_rot = no_ball_rot; + self + } + + #[must_use] + pub fn with_custom_boost_pads(mut self, custom_boost_pads: Vec) -> Self { + self.custom_boost_pads = Some(custom_boost_pads); + self + } + + #[must_use] + pub fn with_rng_seed(mut self, rng_seed: u64) -> Self { + self.rng_seed = Some(rng_seed); + self + } } diff --git a/rocketsim/src/sim/arena/base.rs b/rocketsim/src/sim/arena/base.rs index 04de9601..dff57380 100644 --- a/rocketsim/src/sim/arena/base.rs +++ b/rocketsim/src/sim/arena/base.rs @@ -60,15 +60,16 @@ impl Arena { } pub fn new_with_config(config: ArenaConfig) -> Self { - let (cell_size_multiplier, initial_handle_size) = match config.mem_weight_mode { - ArenaMemWeightMode::Light => (3.0, 1), - ArenaMemWeightMode::Heavy => (1.0, 8), + let (cell_size, initial_handle_size) = match config.mem_weight_mode { + ArenaMemWeightMode::Light => ((config.max_pos - config.min_pos).max_element(), 1), + ArenaMemWeightMode::Balanced => (config.max_aabb_len * 3.0, 1), + ArenaMemWeightMode::Heavy => (config.max_aabb_len, 8), }; let broadphase = GridBroadphase::new( config.min_pos * UU_TO_BT, config.max_pos * UU_TO_BT, - config.max_aabb_len * UU_TO_BT * cell_size_multiplier, + cell_size * UU_TO_BT, initial_handle_size, );