Files
rapier/src_testbed/ui.rs
Sébastien Crozet 48de83817e feat: persistent islands + manifold reduction (#895)
* feat: initial implementation of contact manifold reduction

* feat: try bepu-like manifold reduction

* feat: simplification of the constraints counting and indexing logic

* feat: add concept of incremental islands with a single awake island

More islands manager fixes

* feat: start adding support for multiple awake islands

* feat: add more timings

* feat: implement incremental island split & merge

* chore: refactor islands manager into multiple files

* chore: refactor manifold reduction to its own file + add naive reduction method

* feat: add islands manager validation checks

* fix various bugs in the new islands system

* chore: remove redundant active_set_offset field
2026-01-09 17:04:02 +01:00

539 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use rapier::counters::Counters;
use rapier::math::Real;
use crate::debug_render::DebugRenderPipelineResource;
use crate::harness::{Harness, RapierBroadPhaseType};
use crate::testbed::{
PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR, RunMode, TestbedActionFlags,
TestbedState, TestbedStateFlags,
};
pub use bevy_egui::egui;
use crate::PhysicsState;
use crate::settings::SettingValue;
use bevy_egui::EguiContexts;
use bevy_egui::egui::{ComboBox, Slider, Ui, Window};
use web_time::Instant;
#[cfg(feature = "dim3")]
use rapier::dynamics::FrictionModel;
pub(crate) fn update_ui(
ui_context: &mut EguiContexts,
state: &mut TestbedState,
harness: &mut Harness,
debug_render: &mut DebugRenderPipelineResource,
) {
#[cfg(feature = "profiler_ui")]
{
profiler_ui(ui_context);
}
example_settings_ui(ui_context, state);
Window::new("Parameters").show(ui_context.ctx_mut(), |ui| {
if state.backend_names.len() > 1 && !state.example_names.is_empty() {
let mut changed = false;
ComboBox::from_label("backend")
.width(150.0)
.selected_text(state.backend_names[state.selected_backend])
.show_ui(ui, |ui| {
for (id, name) in state.backend_names.iter().enumerate() {
changed = ui
.selectable_value(&mut state.selected_backend, id, *name)
.changed()
|| changed;
}
});
if changed {
state
.action_flags
.set(TestbedActionFlags::BACKEND_CHANGED, true);
}
ui.separator();
}
ui.horizontal(|ui| {
if ui.button("<").clicked() && state.selected_example > 0 {
state.selected_example -= 1;
state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, true)
}
if ui.button(">").clicked() && state.selected_example + 1 < state.example_names.len() {
state.selected_example += 1;
state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, true)
}
let mut changed = false;
egui::ComboBox::from_label("example")
.width(150.0)
.selected_text(state.example_names[state.selected_example])
.show_ui(ui, |ui| {
for (id, name) in state.example_names.iter().enumerate() {
changed = ui
.selectable_value(&mut state.selected_example, id, *name)
.changed()
|| changed;
}
});
if changed {
state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
}
});
ui.separator();
ui.collapsing("Scene infos", |ui| {
scene_infos_ui(ui, &harness.physics);
});
ui.collapsing("Profile infos", |ui| {
ui.horizontal_wrapped(|ui| {
profiling_ui(ui, &harness.physics.pipeline.counters);
});
});
ui.collapsing("Serialization infos", |ui| {
ui.horizontal_wrapped(|ui| {
ui.label(serialization_string(
harness.state.timestep_id,
&harness.physics,
))
});
});
let integration_parameters = &mut harness.physics.integration_parameters;
if state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
|| state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
{
ui.add(
Slider::new(&mut integration_parameters.num_solver_iterations, 0..=10)
.text("pos. iters."),
);
} else {
// Broad-phase.
let mut changed = false;
egui::ComboBox::from_label("broad-phase")
.width(150.0)
.selected_text(format!("{:?}", state.broad_phase_type))
.show_ui(ui, |ui| {
let broad_phase_type = [
RapierBroadPhaseType::BvhSubtreeOptimizer,
RapierBroadPhaseType::BvhWithoutOptimization,
];
for sty in broad_phase_type {
changed = ui
.selectable_value(&mut state.broad_phase_type, sty, format!("{sty:?}"))
.changed()
|| changed;
}
});
if changed {
harness.physics.broad_phase = state.broad_phase_type.init_broad_phase();
// Restart the simulation after a broad-phase changes since some
// broad-phase might not support hot-swapping.
state.action_flags.set(TestbedActionFlags::RESTART, true);
}
// Friction model.
#[cfg(feature = "dim3")]
egui::ComboBox::from_label("friction-model")
.width(150.0)
.selected_text(format!("{:?}", integration_parameters.friction_model))
.show_ui(ui, |ui| {
let friction_model = [FrictionModel::Simplified, FrictionModel::Coulomb];
for model in friction_model {
changed = ui
.selectable_value(
&mut integration_parameters.friction_model,
model,
format!("{model:?}"),
)
.changed()
|| changed;
}
});
// Solver iterations.
ui.add(
Slider::new(&mut integration_parameters.num_solver_iterations, 0..=10)
.text("num solver iters."),
);
ui.add(
Slider::new(
&mut integration_parameters.num_internal_pgs_iterations,
1..=40,
)
.text("num internal PGS iters."),
);
ui.add(
Slider::new(
&mut integration_parameters.num_internal_stabilization_iterations,
0..=100,
)
.text("max internal stabilization iters."),
);
ui.add(
Slider::new(&mut integration_parameters.warmstart_coefficient, 0.0..=1.0)
.text("warmstart coefficient"),
);
let mut substep_params = *integration_parameters;
substep_params.dt /= substep_params.num_solver_iterations as Real;
let curr_erp = substep_params.contact_softness.erp(substep_params.dt);
let curr_cfm_factor = substep_params
.contact_softness
.cfm_factor(substep_params.dt);
ui.add(
Slider::new(
&mut integration_parameters.contact_softness.natural_frequency,
0.01..=120.0,
)
.text(format!("contacts Hz (erp = {curr_erp:.3})")),
);
ui.add(
Slider::new(
&mut integration_parameters.contact_softness.damping_ratio,
0.01..=20.0,
)
.text(format!("damping ratio (cfm-factor = {curr_cfm_factor:.3})",)),
);
}
#[cfg(feature = "parallel")]
{
let mut num_threads = harness.state.num_threads();
ui.add(
Slider::new(&mut num_threads, 1..=num_cpus::get_physical()).text("num. threads"),
);
harness.state.set_num_threads(num_threads);
}
ui.add(
Slider::new(&mut integration_parameters.max_ccd_substeps, 0..=10).text("CCD substeps"),
);
ui.add(
Slider::new(&mut integration_parameters.min_island_size, 1..=10_000)
.text("min island size"),
);
ui.add(Slider::new(&mut state.nsteps, 1..=100).text("sims. per frame"));
let mut frequency = integration_parameters.inv_dt().round() as u32;
ui.add(Slider::new(&mut frequency, 0..=240).text("frequency (Hz)"));
let mut gravity_y = harness.physics.gravity.y;
if ui
.add(Slider::new(&mut gravity_y, 0.0..=-200.0).text("Gravity"))
.changed()
{
harness.physics.gravity.y = gravity_y;
}
integration_parameters.set_inv_dt(frequency as Real);
let mut sleep = state.flags.contains(TestbedStateFlags::SLEEP);
let mut draw_surfaces = state.flags.contains(TestbedStateFlags::DRAW_SURFACES);
// let mut contact_points = state.flags.contains(TestbedStateFlags::CONTACT_POINTS);
// let mut wireframe = state.flags.contains(TestbedStateFlags::WIREFRAME);
ui.checkbox(&mut sleep, "sleep enabled");
// ui.checkbox(&mut contact_points, "draw contacts");
// ui.checkbox(&mut wireframe, "draw wireframes");
ui.checkbox(&mut draw_surfaces, "surface render enabled");
ui.checkbox(&mut debug_render.enabled, "debug render enabled");
state.flags.set(TestbedStateFlags::SLEEP, sleep);
state
.flags
.set(TestbedStateFlags::DRAW_SURFACES, draw_surfaces);
// state
// .flags
// .set(TestbedStateFlags::CONTACT_POINTS, contact_points);
// state.flags.set(TestbedStateFlags::WIREFRAME, wireframe);
ui.separator();
let label = if state.running == RunMode::Stop {
"Start (T)"
} else {
"Pause (T)"
};
if ui.button(label).clicked() {
if state.running == RunMode::Stop {
state.running = RunMode::Running
} else {
state.running = RunMode::Stop
}
}
if ui.button("Single Step (S)").clicked() {
state.running = RunMode::Step;
}
if ui.button("Take snapshot").clicked() {
state
.action_flags
.set(TestbedActionFlags::TAKE_SNAPSHOT, true);
}
if ui.button("Restore snapshot").clicked() {
state
.action_flags
.set(TestbedActionFlags::RESTORE_SNAPSHOT, true);
}
if ui.button("Restart (R)").clicked() {
state.action_flags.set(TestbedActionFlags::RESTART, true);
}
});
}
fn scene_infos_ui(ui: &mut Ui, physics: &PhysicsState) {
ui.label(format!("# rigid-bodies: {}", physics.bodies.len()));
ui.label(format!("# colliders: {}", physics.colliders.len()));
ui.label(format!("# impulse joint: {}", physics.impulse_joints.len()));
// ui.label(format!(
// "# multibody joint: {}",
// physics.multibody_joints.len()
// ));
}
fn profiling_ui(ui: &mut Ui, counters: &Counters) {
egui::CollapsingHeader::new(format!(
"Total: {:.2}ms - {} fps",
counters.step_time_ms(),
(1000.0 / counters.step_time_ms()).round()
))
.id_salt("total")
.show(ui, |ui| {
egui::CollapsingHeader::new(format!(
"Collision detection: {:.2}ms",
counters.collision_detection_time_ms()
))
.id_salt("collision detection")
.show(ui, |ui| {
ui.label(format!(
"Broad-phase: {:.2}ms",
counters.broad_phase_time_ms()
));
ui.label(format!(
"Final broad-phase: {:.2}ms",
counters.cd.final_broad_phase_time.time_ms()
));
ui.label(format!(
"Narrow-phase: {:.2}ms",
counters.narrow_phase_time_ms()
));
});
egui::CollapsingHeader::new(format!("Solver: {:.2}ms", counters.solver_time_ms()))
.id_salt("solver")
.show(ui, |ui| {
ui.label(format!(
"Velocity assembly: {:.2}ms",
counters.solver.velocity_assembly_time.time_ms()
));
ui.label(format!(
"> Velocity assembly - solver bodies: {:.2}ms",
counters
.solver
.velocity_assembly_time_solver_bodies
.time_ms()
));
ui.label(format!(
"> Velocity assembly - constraints init: {:.2}ms",
counters
.solver
.velocity_assembly_time_constraints_init
.time_ms()
));
ui.label(format!(
"Velocity resolution: {:.2}ms",
counters.velocity_resolution_time_ms()
));
ui.label(format!(
"Velocity integration: {:.2}ms",
counters.solver.velocity_update_time.time_ms()
));
ui.label(format!(
"Writeback: {:.2}ms",
counters.solver.velocity_writeback_time.time_ms()
));
});
egui::CollapsingHeader::new(format!("CCD: {:.2}ms", counters.ccd_time_ms()))
.id_salt("ccd")
.show(ui, |ui| {
ui.label(format!("# of substeps: {}", counters.ccd.num_substeps));
ui.label(format!(
"TOI computation: {:.2}ms",
counters.ccd.toi_computation_time.time_ms(),
));
ui.label(format!(
"Broad-phase: {:.2}ms",
counters.ccd.broad_phase_time.time_ms()
));
ui.label(format!(
"Narrow-phase: {:.2}ms",
counters.ccd.narrow_phase_time.time_ms(),
));
ui.label(format!(
"Solver: {:.2}ms",
counters.ccd.solver_time.time_ms()
));
});
ui.label(format!(
"Island computation: {:.2}ms",
counters.island_construction_time_ms()
));
ui.label(format!(
"Active constraints collection: {:.2}ms",
counters.stages.island_constraints_collection_time.time_ms()
));
ui.label(format!("Mprops update: {:.2}ms", counters.update_time_ms()));
ui.label(format!(
"User changes: {:.2}ms",
counters.stages.user_changes.time_ms()
));
ui.label(format!("Debug timer: {:.2}ms", counters.custom.time_ms()));
});
}
fn serialization_string(timestep_id: usize, physics: &PhysicsState) -> String {
let t = Instant::now();
// let t = Instant::now();
let bf = bincode::serialize(&physics.broad_phase).unwrap();
// println!("bf: {}", Instant::now() - t);
// let t = Instant::now();
let nf = bincode::serialize(&physics.narrow_phase).unwrap();
// println!("nf: {}", Instant::now() - t);
// let t = Instant::now();
let bs = bincode::serialize(&physics.bodies).unwrap();
// println!("bs: {}", Instant::now() - t);
// let t = Instant::now();
let cs = bincode::serialize(&physics.colliders).unwrap();
// println!("cs: {}", Instant::now() - t);
// let t = Instant::now();
let js = bincode::serialize(&physics.impulse_joints).unwrap();
// println!("js: {}", Instant::now() - t);
let serialization_time = Instant::now() - t;
let hash_bf = md5::compute(&bf);
let hash_nf = md5::compute(&nf);
let hash_bodies = md5::compute(&bs);
let hash_colliders = md5::compute(&cs);
let hash_joints = md5::compute(&js);
format!(
r#"Serialization time: {:.2}ms
Hashes at frame: {}
|_ Broad phase [{:.1}KB]: {}
|_ Narrow phase [{:.1}KB]: {}
|_ &RigidBodySet [{:.1}KB]: {}
|_ Colliders [{:.1}KB]: {}
|_ Joints [{:.1}KB]: {}"#,
serialization_time.as_secs_f64() * 1000.0,
timestep_id,
bf.len() as f32 / 1000.0,
format!("{hash_bf:?}").split_at(10).0,
nf.len() as f32 / 1000.0,
format!("{hash_nf:?}").split_at(10).0,
bs.len() as f32 / 1000.0,
format!("{hash_bodies:?}").split_at(10).0,
cs.len() as f32 / 1000.0,
format!("{hash_colliders:?}").split_at(10).0,
js.len() as f32 / 1000.0,
format!("{hash_joints:?}").split_at(10).0,
)
}
fn example_settings_ui(ui_context: &mut EguiContexts, state: &mut TestbedState) {
if state.example_settings.is_empty() {
// Dont show any window if there is no settings for the
// example.
return;
}
Window::new("Example settings").show(ui_context.ctx_mut(), |ui| {
let mut any_changed = false;
for (name, value) in state.example_settings.iter_mut() {
let prev_value = value.clone();
match value {
SettingValue::Label(value) => {
ui.label(format!("{name}: {value}"));
}
SettingValue::F32 { value, range } => {
ui.add(Slider::new(value, range.clone()).text(name));
}
SettingValue::U32 { value, range } => {
ui.horizontal(|ui| {
if ui.button("<").clicked() && *value > *range.start() {
*value -= 1;
}
if ui.button(">").clicked() && *value <= *range.end() {
*value += 1;
}
ui.add(Slider::new(value, range.clone()).text(name));
});
}
SettingValue::Bool { value } => {
ui.checkbox(value, name);
}
SettingValue::String { value, range } => {
ComboBox::from_label(name)
.width(150.0)
.selected_text(&range[*value])
.show_ui(ui, |ui| {
for (id, name) in range.iter().enumerate() {
ui.selectable_value(value, id, name);
}
});
}
}
any_changed = any_changed || *value != prev_value;
}
if any_changed {
// The value changed, request a restart.
state.action_flags.set(TestbedActionFlags::RESTART, true);
}
});
}
#[cfg(feature = "profiler_ui")]
fn profiler_ui(ui_context: &mut EguiContexts) {
let window = egui::Window::new("Profiling");
let window = window.default_open(false);
#[cfg(feature = "unstable-puffin-pr-235")]
{
use std::sync::Once;
static START: Once = Once::new();
fn set_default_rapier_filter() {
let mut profile_ui = puffin_egui::PROFILE_UI.lock();
profile_ui
.profiler_ui
.flamegraph_options
.scope_name_filter
.set_filter("Harness::step_with_graphics".to_string());
}
START.call_once(|| {
set_default_rapier_filter();
});
window.show(ui_context.ctx_mut(), |ui| {
if ui.button("🔍 Rapier filter").clicked() {
set_default_rapier_filter();
}
puffin_egui::profiler_ui(ui);
});
}
#[cfg(not(feature = "unstable-puffin-pr-235"))]
window.show(ui_context.ctx_mut(), |ui| {
puffin_egui::profiler_ui(ui);
});
}