Files
rapier/src_testbed/ui.rs
Sébastien Crozet 0b7c3b34ec feat: migrate to glam whenever relevant + migrate testbed to kiss3d instead of bevy + release v0.32.0 (#909)
* feat: migrate to glam whenever relevant + migrate testbed to kiss3d instead of bevy

* chore: update changelog

* Fix warnings and tests

* Release v0.32.0
2026-01-09 17:26:36 +01:00

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);
});
}
}