Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 12 additions & 0 deletions rocketsim/examples/stress_common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
95 changes: 54 additions & 41 deletions rocketsim/examples/stress_v3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +28,16 @@ impl From<GameModeArg> for GameMode {
}
}

impl From<MemWeightModeArg> 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,
Expand Down Expand Up @@ -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;
}
}
}
}
Expand Down
28 changes: 23 additions & 5 deletions rocketsim/src/bullet/collision/broadphase/broadphase_proxy.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor};

use glam::USizeVec3;

use crate::shared::Aabb;

pub enum CollisionFilterGroups {
Expand Down Expand Up @@ -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 {
Expand All @@ -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::<BroadphaseProxy>(), 64);
assert_eq!(size_of::<TriangleShape>(), 80);
assert_eq!(size_of::<ManifoldPoint>(), 128);
assert_eq!(size_of::<PersistentManifold>(), 560);
}
}
52 changes: 41 additions & 11 deletions rocketsim/src/bullet/collision/broadphase/grid_broadphase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,23 +200,30 @@ 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;

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::<false>(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::<false>(
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::<true>(proxy_idx, new_indices);
}
Expand All @@ -239,12 +246,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 {
Expand Down Expand Up @@ -278,7 +285,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];

Expand Down Expand Up @@ -331,10 +338,33 @@ 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);
}
}
}
}

#[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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::bullet::{

pub struct HashedOverlappingPairCache {
overlapping_pair_array: Vec<BroadphasePair>,
hash_table: FxHashMap<(u32, u32), usize>,
hash_table: FxHashMap<u64, usize>,
}

impl Default for HashedOverlappingPairCache {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading