* feat: migrate to glam whenever relevant + migrate testbed to kiss3d instead of bevy * chore: update changelog * Fix warnings and tests * Release v0.32.0
847 lines
32 KiB
Rust
847 lines
32 KiB
Rust
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, UiTab,
|
|
};
|
|
|
|
pub use egui;
|
|
|
|
use crate::PhysicsState;
|
|
use crate::settings::SettingValue;
|
|
use egui::{ComboBox, RichText, Slider, Ui, Window};
|
|
use web_time::Instant;
|
|
|
|
#[cfg(feature = "dim3")]
|
|
use rapier::dynamics::FrictionModel;
|
|
|
|
/// Sets up a custom warm theme that complements the app's off-white background.
|
|
fn setup_custom_theme(ctx: &egui::Context) {
|
|
use egui::{Color32, CornerRadius, Stroke};
|
|
|
|
let bg_fill = Color32::from_rgb(250, 250, 245);
|
|
let window_fill = Color32::from_rgb(252, 252, 248);
|
|
let faint_bg = Color32::from_rgb(240, 240, 232);
|
|
let extreme_bg = Color32::from_rgb(255, 255, 252);
|
|
|
|
let text_color = Color32::from_rgb(60, 58, 52);
|
|
|
|
let accent = Color32::from_rgb(82, 130, 150);
|
|
let accent_active = Color32::from_rgb(70, 115, 135);
|
|
|
|
let widget_bg = Color32::from_rgb(235, 235, 225);
|
|
let widget_bg_hover = Color32::from_rgb(225, 225, 215);
|
|
let widget_bg_active = Color32::from_rgb(215, 215, 205);
|
|
|
|
let stroke_color = Color32::from_rgb(200, 198, 190);
|
|
let stroke_hover = Color32::from_rgb(180, 178, 170);
|
|
|
|
let rounding = CornerRadius::same(6);
|
|
let small_rounding = CornerRadius::same(4);
|
|
|
|
ctx.style_mut(|style| {
|
|
let v = &mut style.visuals;
|
|
v.dark_mode = false;
|
|
|
|
v.widgets.noninteractive.bg_fill = faint_bg;
|
|
v.widgets.noninteractive.weak_bg_fill = faint_bg;
|
|
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, stroke_color);
|
|
v.widgets.noninteractive.corner_radius = rounding;
|
|
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, text_color);
|
|
|
|
v.widgets.inactive.bg_fill = widget_bg;
|
|
v.widgets.inactive.weak_bg_fill = widget_bg;
|
|
v.widgets.inactive.bg_stroke = Stroke::new(1.0, stroke_color);
|
|
v.widgets.inactive.corner_radius = small_rounding;
|
|
v.widgets.inactive.fg_stroke = Stroke::new(1.0, text_color);
|
|
|
|
v.widgets.hovered.bg_fill = widget_bg_hover;
|
|
v.widgets.hovered.weak_bg_fill = widget_bg_hover;
|
|
v.widgets.hovered.bg_stroke = Stroke::new(1.0, stroke_hover);
|
|
v.widgets.hovered.corner_radius = small_rounding;
|
|
v.widgets.hovered.fg_stroke = Stroke::new(1.5, text_color);
|
|
|
|
v.widgets.active.bg_fill = widget_bg_active;
|
|
v.widgets.active.weak_bg_fill = widget_bg_active;
|
|
v.widgets.active.bg_stroke = Stroke::new(1.0, accent);
|
|
v.widgets.active.corner_radius = small_rounding;
|
|
v.widgets.active.fg_stroke = Stroke::new(2.0, accent_active);
|
|
|
|
v.widgets.open.bg_fill = widget_bg;
|
|
v.widgets.open.weak_bg_fill = widget_bg;
|
|
v.widgets.open.bg_stroke = Stroke::new(1.0, stroke_color);
|
|
v.widgets.open.corner_radius = small_rounding;
|
|
v.widgets.open.fg_stroke = Stroke::new(1.0, text_color);
|
|
|
|
v.selection.bg_fill = accent.gamma_multiply(0.25);
|
|
v.selection.stroke = Stroke::new(1.0, accent);
|
|
|
|
v.hyperlink_color = accent;
|
|
v.faint_bg_color = faint_bg;
|
|
v.extreme_bg_color = extreme_bg;
|
|
v.code_bg_color = Color32::from_rgb(230, 230, 220);
|
|
v.warn_fg_color = Color32::from_rgb(180, 120, 60);
|
|
v.error_fg_color = Color32::from_rgb(180, 70, 70);
|
|
|
|
v.window_corner_radius = CornerRadius::same(8);
|
|
v.window_fill = window_fill;
|
|
v.window_stroke = Stroke::new(1.0, stroke_color);
|
|
|
|
v.panel_fill = bg_fill;
|
|
|
|
v.slider_trailing_fill = true;
|
|
v.handle_shape = egui::style::HandleShape::Circle;
|
|
|
|
style.spacing.item_spacing = egui::vec2(6.0, 3.0);
|
|
style.spacing.window_margin = egui::Margin::same(10);
|
|
style.spacing.button_padding = egui::vec2(6.0, 3.0);
|
|
style.spacing.slider_width = 130.0;
|
|
style.spacing.indent = 14.0;
|
|
style.spacing.interact_size = egui::vec2(32.0, 18.0);
|
|
style.spacing.combo_width = 100.0;
|
|
});
|
|
}
|
|
|
|
pub(crate) fn update_ui(
|
|
ui_context: &egui::Context,
|
|
state: &mut TestbedState,
|
|
harness: &mut Harness,
|
|
debug_render: &mut DebugRenderPipelineResource,
|
|
) {
|
|
setup_custom_theme(ui_context);
|
|
|
|
#[cfg(feature = "profiler_ui")]
|
|
{
|
|
profiler_ui(ui_context);
|
|
}
|
|
|
|
example_settings_ui(ui_context, state);
|
|
|
|
Window::new("Rapier Testbed")
|
|
.default_width(300.0)
|
|
.show(ui_context, |ui| {
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// TAB BAR
|
|
// ═══════════════════════════════════════════════════════════════
|
|
ui.horizontal(|ui| {
|
|
ui.selectable_value(&mut state.selected_tab, UiTab::Examples, "Examples");
|
|
ui.selectable_value(&mut state.selected_tab, UiTab::Settings, "Settings");
|
|
ui.selectable_value(&mut state.selected_tab, UiTab::Performance, "Performance");
|
|
});
|
|
|
|
ui.separator();
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// TAB CONTENT
|
|
// ═══════════════════════════════════════════════════════════════
|
|
egui::ScrollArea::vertical()
|
|
.max_height(400.0)
|
|
.show(ui, |ui| match state.selected_tab {
|
|
UiTab::Examples => {
|
|
examples_tab(ui, state);
|
|
}
|
|
UiTab::Settings => {
|
|
settings_tab(ui, state, harness, debug_render);
|
|
}
|
|
UiTab::Performance => {
|
|
performance_tab(ui, harness);
|
|
}
|
|
});
|
|
|
|
ui.separator();
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// BOTTOM CONTROLS - Always visible
|
|
// ═══════════════════════════════════════════════════════════════
|
|
ui.horizontal(|ui| {
|
|
// Play/Pause
|
|
let (label, hover) = if state.running == RunMode::Stop {
|
|
("Play", "Start simulation (T)")
|
|
} else {
|
|
("Pause", "Pause simulation (T)")
|
|
};
|
|
|
|
if ui.button(label).on_hover_text(hover).clicked() {
|
|
state.running = if state.running == RunMode::Stop {
|
|
RunMode::Running
|
|
} else {
|
|
RunMode::Stop
|
|
};
|
|
}
|
|
|
|
// Step
|
|
if ui.button("Step").on_hover_text("Single step (S)").clicked() {
|
|
state.running = RunMode::Step;
|
|
}
|
|
|
|
// Restart
|
|
if ui
|
|
.button("Restart")
|
|
.on_hover_text("Restart example (R)")
|
|
.clicked()
|
|
{
|
|
state.action_flags.set(TestbedActionFlags::RESTART, true);
|
|
}
|
|
|
|
ui.separator();
|
|
|
|
// Save/Restore
|
|
if ui.button("Save").on_hover_text("Save snapshot").clicked() {
|
|
state
|
|
.action_flags
|
|
.set(TestbedActionFlags::TAKE_SNAPSHOT, true);
|
|
}
|
|
|
|
if ui
|
|
.button("Restore")
|
|
.on_hover_text("Restore snapshot")
|
|
.clicked()
|
|
{
|
|
state
|
|
.action_flags
|
|
.set(TestbedActionFlags::RESTORE_SNAPSHOT, true);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fn examples_tab(ui: &mut Ui, state: &mut TestbedState) {
|
|
// Backend selector (if multiple backends available)
|
|
if state.backend_names.len() > 1 {
|
|
ui.horizontal(|ui| {
|
|
ui.label("Backend:");
|
|
let mut backend_changed = false;
|
|
ComboBox::from_id_salt("backend_combo")
|
|
.width(150.0)
|
|
.selected_text(state.backend_names[state.selected_backend])
|
|
.show_ui(ui, |ui| {
|
|
for (id, name) in state.backend_names.iter().enumerate() {
|
|
backend_changed = ui
|
|
.selectable_value(&mut state.selected_backend, id, *name)
|
|
.changed()
|
|
|| backend_changed;
|
|
}
|
|
});
|
|
|
|
if backend_changed {
|
|
state
|
|
.action_flags
|
|
.set(TestbedActionFlags::BACKEND_CHANGED, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Navigation and backend selector row
|
|
ui.horizontal(|ui| {
|
|
// Previous/Next buttons (navigate in display order)
|
|
if ui
|
|
.add_enabled(state.selected_display_index > 0, egui::Button::new("<"))
|
|
.on_hover_text("Previous example")
|
|
.clicked()
|
|
{
|
|
state.selected_display_index -= 1;
|
|
state
|
|
.action_flags
|
|
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
|
|
}
|
|
|
|
if ui
|
|
.add_enabled(
|
|
state.selected_display_index + 1 < state.examples.len(),
|
|
egui::Button::new(">"),
|
|
)
|
|
.on_hover_text("Next example")
|
|
.clicked()
|
|
{
|
|
state.selected_display_index += 1;
|
|
state
|
|
.action_flags
|
|
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
|
|
}
|
|
|
|
// Current example name
|
|
if let Some(current) = state.examples.get(state.selected_display_index) {
|
|
ui.label(
|
|
RichText::new(format!("[{}] {}", current.group, current.name))
|
|
.strong()
|
|
.italics(),
|
|
);
|
|
}
|
|
});
|
|
|
|
ui.add_space(4.0);
|
|
ui.separator();
|
|
ui.add_space(4.0);
|
|
|
|
// Display examples grouped by their assigned groups
|
|
for group in &state.example_groups.clone() {
|
|
// Collect examples in this group with their display indices
|
|
let examples_in_group: Vec<(usize, &str)> = state
|
|
.examples
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, e)| e.group == *group)
|
|
.map(|(display_idx, e)| (display_idx, e.name))
|
|
.collect();
|
|
|
|
if examples_in_group.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
egui::CollapsingHeader::new(format!("{} ({})", group, examples_in_group.len()))
|
|
.default_open(false)
|
|
.show(ui, |ui| {
|
|
for (display_idx, name) in examples_in_group {
|
|
let is_selected = state.selected_display_index == display_idx;
|
|
let text = if is_selected {
|
|
RichText::new(name).strong()
|
|
} else {
|
|
RichText::new(name)
|
|
};
|
|
|
|
if ui
|
|
.selectable_label(is_selected, text)
|
|
.on_hover_text("Click to run this example")
|
|
.clicked()
|
|
&& !is_selected
|
|
{
|
|
state.selected_display_index = display_idx;
|
|
state
|
|
.action_flags
|
|
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn settings_tab(
|
|
ui: &mut Ui,
|
|
state: &mut TestbedState,
|
|
harness: &mut Harness,
|
|
debug_render: &mut DebugRenderPipelineResource,
|
|
) {
|
|
let integration_parameters = &mut harness.physics.integration_parameters;
|
|
let is_physx = state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
|
|
|| state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR;
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// RENDERING
|
|
// ─────────────────────────────────────────────────────────────────
|
|
ui.label(RichText::new("Rendering").strong());
|
|
ui.add_space(2.0);
|
|
|
|
let mut draw_surfaces = state.flags.contains(TestbedStateFlags::DRAW_SURFACES);
|
|
if ui
|
|
.checkbox(&mut draw_surfaces, "Surfaces")
|
|
.on_hover_text("Render collider surfaces.")
|
|
.changed()
|
|
{
|
|
state
|
|
.flags
|
|
.set(TestbedStateFlags::DRAW_SURFACES, draw_surfaces);
|
|
}
|
|
|
|
ui.checkbox(&mut debug_render.enabled, "Debug render")
|
|
.on_hover_text("Show debug wireframes and contacts.");
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// SIMULATION
|
|
// ─────────────────────────────────────────────────────────────────
|
|
ui.label(RichText::new("Simulation").strong());
|
|
ui.add_space(2.0);
|
|
|
|
// Frequency
|
|
let mut frequency = integration_parameters.inv_dt().round() as u32;
|
|
if ui
|
|
.add(
|
|
Slider::new(&mut frequency, 30..=240)
|
|
.text("Hz")
|
|
.clamping(egui::SliderClamping::Never),
|
|
)
|
|
.on_hover_text("Simulation frequency. Higher = more accurate but slower.")
|
|
.changed()
|
|
{
|
|
integration_parameters.set_inv_dt(frequency as Real);
|
|
}
|
|
|
|
// Steps per frame
|
|
ui.add(Slider::new(&mut state.nsteps, 1..=100).text("Steps/frame"))
|
|
.on_hover_text("Physics steps per rendered frame.");
|
|
|
|
// Gravity
|
|
let mut gravity_y = harness.physics.gravity.y;
|
|
if ui
|
|
.add(Slider::new(&mut gravity_y, 0.0..=-200.0).text("Gravity"))
|
|
.on_hover_text("Gravity (m/s^2). Default: -9.81")
|
|
.changed()
|
|
{
|
|
harness.physics.gravity.y = gravity_y;
|
|
}
|
|
|
|
// Sleep
|
|
let mut sleep = state.flags.contains(TestbedStateFlags::SLEEP);
|
|
if ui
|
|
.checkbox(&mut sleep, "Sleep enabled")
|
|
.on_hover_text("Allow resting bodies to sleep for better performance.")
|
|
.changed()
|
|
{
|
|
state.flags.set(TestbedStateFlags::SLEEP, sleep);
|
|
}
|
|
|
|
ui.add_space(8.0);
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// SOLVER
|
|
// ─────────────────────────────────────────────────────────────────
|
|
ui.label(RichText::new("Solver").strong());
|
|
ui.add_space(2.0);
|
|
|
|
ui.add(Slider::new(&mut integration_parameters.num_solver_iterations, 1..=10).text("Substeps"))
|
|
.on_hover_text("Main solver iterations. Higher = more stable stacking.");
|
|
|
|
if !is_physx {
|
|
ui.add(
|
|
Slider::new(
|
|
&mut integration_parameters.num_internal_pgs_iterations,
|
|
1..=40,
|
|
)
|
|
.text("PGS iters"),
|
|
)
|
|
.on_hover_text("Internal Projected Gauss-Seidel iterations.");
|
|
|
|
ui.add(
|
|
Slider::new(
|
|
&mut integration_parameters.num_internal_stabilization_iterations,
|
|
0..=100,
|
|
)
|
|
.text("Relaxation"),
|
|
)
|
|
.on_hover_text("Position stabilization iterations.");
|
|
|
|
ui.add(
|
|
Slider::new(&mut integration_parameters.warmstart_coefficient, 0.0..=1.0)
|
|
.text("Warmstart"),
|
|
)
|
|
.on_hover_text("Reuse previous impulses for faster convergence.");
|
|
}
|
|
|
|
ui.add_space(8.0);
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// CONTACTS (Rapier only)
|
|
// ─────────────────────────────────────────────────────────────────
|
|
if !is_physx {
|
|
ui.label(RichText::new("Contacts").strong());
|
|
ui.add_space(2.0);
|
|
|
|
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 = 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("Frequency"),
|
|
)
|
|
.on_hover_text(format!(
|
|
"Contact stiffness (Hz). Higher = stiffer.\nERP = {curr_erp:.3}"
|
|
));
|
|
|
|
ui.add(
|
|
Slider::new(
|
|
&mut integration_parameters.contact_softness.damping_ratio,
|
|
0.01..=20.0,
|
|
)
|
|
.text("Damping"),
|
|
)
|
|
.on_hover_text(format!(
|
|
"Contact damping. 1.0 = critical.\nCFM = {curr_cfm:.5}"
|
|
));
|
|
|
|
#[cfg(feature = "dim3")]
|
|
{
|
|
ui.horizontal(|ui| {
|
|
ui.label("Friction model:");
|
|
egui::ComboBox::from_id_salt("friction_model")
|
|
.width(100.0)
|
|
.selected_text(format!("{:?}", integration_parameters.friction_model))
|
|
.show_ui(ui, |ui| {
|
|
for model in [FrictionModel::Simplified, FrictionModel::Coulomb] {
|
|
ui.selectable_value(
|
|
&mut integration_parameters.friction_model,
|
|
model,
|
|
format!("{model:?}"),
|
|
)
|
|
.on_hover_text(match model {
|
|
FrictionModel::Simplified => "Fast friction approximation",
|
|
FrictionModel::Coulomb => "Accurate Coulomb friction",
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
ui.add_space(8.0);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// ADVANCED (Rapier only)
|
|
// ─────────────────────────────────────────────────────────────────
|
|
if !is_physx {
|
|
ui.label(RichText::new("Advanced").strong());
|
|
ui.add_space(2.0);
|
|
|
|
// Broad-phase
|
|
ui.horizontal(|ui| {
|
|
ui.label("Broad-phase:");
|
|
let mut bp_changed = false;
|
|
egui::ComboBox::from_id_salt("broad_phase")
|
|
.width(120.0)
|
|
.selected_text(match state.broad_phase_type {
|
|
RapierBroadPhaseType::BvhSubtreeOptimizer => "BVH (optimized)",
|
|
RapierBroadPhaseType::BvhWithoutOptimization => "BVH (basic)",
|
|
})
|
|
.show_ui(ui, |ui| {
|
|
for (bpt, label) in [
|
|
(RapierBroadPhaseType::BvhSubtreeOptimizer, "BVH (optimized)"),
|
|
(RapierBroadPhaseType::BvhWithoutOptimization, "BVH (basic)"),
|
|
] {
|
|
bp_changed = ui
|
|
.selectable_value(&mut state.broad_phase_type, bpt, label)
|
|
.changed()
|
|
|| bp_changed;
|
|
}
|
|
});
|
|
|
|
if bp_changed {
|
|
harness.physics.broad_phase = state.broad_phase_type.init_broad_phase();
|
|
state.action_flags.set(TestbedActionFlags::RESTART, true);
|
|
}
|
|
});
|
|
|
|
ui.add(
|
|
Slider::new(&mut integration_parameters.max_ccd_substeps, 0..=10).text("CCD substeps"),
|
|
)
|
|
.on_hover_text("Continuous collision detection substeps.");
|
|
|
|
ui.add(
|
|
Slider::new(&mut integration_parameters.min_island_size, 1..=10_000)
|
|
.text("Min island sz."),
|
|
)
|
|
.on_hover_text("Minimum bodies per simulation island.");
|
|
|
|
#[cfg(feature = "parallel")]
|
|
{
|
|
let mut num_threads = harness.state.num_threads();
|
|
if ui
|
|
.add(Slider::new(&mut num_threads, 1..=num_cpus::get_physical()).text("Threads"))
|
|
.on_hover_text("Parallel solver threads.")
|
|
.changed()
|
|
{
|
|
harness.state.set_num_threads(num_threads);
|
|
}
|
|
}
|
|
|
|
ui.add_space(8.0);
|
|
}
|
|
}
|
|
|
|
fn performance_tab(ui: &mut Ui, harness: &Harness) {
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// SCENE INFO
|
|
// ─────────────────────────────────────────────────────────────────
|
|
ui.label(RichText::new("Scene").strong());
|
|
ui.add_space(2.0);
|
|
|
|
egui::Grid::new("scene_grid")
|
|
.num_columns(2)
|
|
.spacing([20.0, 2.0])
|
|
.show(ui, |ui| {
|
|
ui.label("Bodies:");
|
|
ui.label(format!("{}", harness.physics.bodies.len()));
|
|
ui.end_row();
|
|
|
|
ui.label("Colliders:");
|
|
ui.label(format!("{}", harness.physics.colliders.len()));
|
|
ui.end_row();
|
|
|
|
ui.label("Joints:");
|
|
ui.label(format!("{}", harness.physics.impulse_joints.len()));
|
|
ui.end_row();
|
|
});
|
|
|
|
ui.add_space(8.0);
|
|
ui.separator();
|
|
ui.add_space(4.0);
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// TIMING - Full details
|
|
// ─────────────────────────────────────────────────────────────────
|
|
let counters = &harness.physics.pipeline.counters;
|
|
let total_ms = counters.step_time_ms();
|
|
let fps = if total_ms > 0.0 {
|
|
(1000.0 / total_ms).round()
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
ui.label(RichText::new(format!("Total: {:.2}ms - {:.0} FPS", total_ms, fps)).strong());
|
|
ui.add_space(4.0);
|
|
|
|
// Collision detection
|
|
egui::CollapsingHeader::new(format!(
|
|
"Collision detection: {:.2}ms",
|
|
counters.collision_detection_time_ms()
|
|
))
|
|
.id_salt("collision_detection")
|
|
.default_open(false)
|
|
.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()
|
|
));
|
|
});
|
|
|
|
// Solver
|
|
egui::CollapsingHeader::new(format!("Solver: {:.2}ms", counters.solver_time_ms()))
|
|
.id_salt("solver")
|
|
.default_open(false)
|
|
.show(ui, |ui| {
|
|
ui.label(format!(
|
|
"Velocity assembly: {:.2}ms",
|
|
counters.solver.velocity_assembly_time.time_ms()
|
|
));
|
|
ui.label(format!(
|
|
" > Solver bodies: {:.2}ms",
|
|
counters
|
|
.solver
|
|
.velocity_assembly_time_solver_bodies
|
|
.time_ms()
|
|
));
|
|
ui.label(format!(
|
|
" > 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()
|
|
));
|
|
});
|
|
|
|
// CCD
|
|
egui::CollapsingHeader::new(format!("CCD: {:.2}ms", counters.ccd_time_ms()))
|
|
.id_salt("ccd")
|
|
.default_open(false)
|
|
.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()
|
|
));
|
|
});
|
|
|
|
// Other timings
|
|
ui.add_space(4.0);
|
|
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!(
|
|
"Mass properties 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()));
|
|
|
|
ui.add_space(8.0);
|
|
ui.separator();
|
|
ui.add_space(4.0);
|
|
|
|
// ─────────────────────────────────────────────────────────────────
|
|
// SERIALIZATION INFO
|
|
// ─────────────────────────────────────────────────────────────────
|
|
egui::CollapsingHeader::new("Serialization Hashes")
|
|
.default_open(false)
|
|
.show(ui, |ui| {
|
|
ui.label(
|
|
egui::RichText::new(serialization_string(
|
|
harness.state.timestep_id,
|
|
&harness.physics,
|
|
))
|
|
.small()
|
|
.monospace(),
|
|
);
|
|
});
|
|
}
|
|
|
|
fn serialization_string(timestep_id: usize, physics: &PhysicsState) -> String {
|
|
let t = Instant::now();
|
|
let bf = bincode::serialize(&physics.broad_phase).unwrap();
|
|
let nf = bincode::serialize(&physics.narrow_phase).unwrap();
|
|
let bs = bincode::serialize(&physics.bodies).unwrap();
|
|
let cs = bincode::serialize(&physics.colliders).unwrap();
|
|
let js = bincode::serialize(&physics.impulse_joints).unwrap();
|
|
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!(
|
|
"Frame: {} ({:.1}ms)\n\
|
|
Broad: {:.1}KB {}\n\
|
|
Narrow: {:.1}KB {}\n\
|
|
Bodies: {:.1}KB {}\n\
|
|
Colliders: {:.1}KB {}\n\
|
|
Joints: {:.1}KB {}",
|
|
timestep_id,
|
|
serialization_time.as_secs_f64() * 1000.0,
|
|
bf.len() as f32 / 1000.0,
|
|
format!("{hash_bf:?}").split_at(8).0,
|
|
nf.len() as f32 / 1000.0,
|
|
format!("{hash_nf:?}").split_at(8).0,
|
|
bs.len() as f32 / 1000.0,
|
|
format!("{hash_bodies:?}").split_at(8).0,
|
|
cs.len() as f32 / 1000.0,
|
|
format!("{hash_colliders:?}").split_at(8).0,
|
|
js.len() as f32 / 1000.0,
|
|
format!("{hash_joints:?}").split_at(8).0,
|
|
)
|
|
}
|
|
|
|
fn example_settings_ui(ui_context: &egui::Context, state: &mut TestbedState) {
|
|
if state.example_settings.is_empty() {
|
|
return;
|
|
}
|
|
|
|
Window::new("Example Settings")
|
|
.default_width(250.0)
|
|
.show(ui_context, |ui| {
|
|
let mut any_changed = false;
|
|
|
|
for (name, value) in state.example_settings.iter_mut() {
|
|
let prev_value = value.clone();
|
|
match value {
|
|
SettingValue::Label(text) => {
|
|
ui.horizontal(|ui| {
|
|
ui.label(RichText::new(format!("{name}:")).strong());
|
|
ui.label(text.as_str());
|
|
});
|
|
}
|
|
SettingValue::F32 { value, range } => {
|
|
ui.add(Slider::new(value, range.clone()).text(name));
|
|
}
|
|
SettingValue::U32 { value, range } => {
|
|
ui.horizontal(|ui| {
|
|
if ui.small_button("-").on_hover_text("Decrease").clicked()
|
|
&& *value > *range.start()
|
|
{
|
|
*value -= 1;
|
|
}
|
|
if ui.small_button("+").on_hover_text("Increase").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, option) in range.iter().enumerate() {
|
|
ui.selectable_value(value, id, option);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
any_changed = any_changed || *value != prev_value;
|
|
}
|
|
|
|
if any_changed {
|
|
state.action_flags.set(TestbedActionFlags::RESTART, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(feature = "profiler_ui")]
|
|
fn profiler_ui(_ui_context: &egui::Context) {
|
|
#[cfg(feature = "unstable-puffin-pr-235")]
|
|
{
|
|
let window = egui::Window::new("Profiler");
|
|
let window = window.default_open(false);
|
|
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, |ui| {
|
|
if ui.button("Rapier filter").clicked() {
|
|
set_default_rapier_filter();
|
|
}
|
|
puffin_egui::profiler_ui(ui);
|
|
});
|
|
}
|
|
}
|