dimforge / rapier

2D and 3D physics engines focused on performance.
https://rapier.rs
Apache License 2.0
3.77k stars 235 forks source link

Bug: Incrementally-updated `QueryPipeline` memory usage linearly grows over time. #617

Open finnbear opened 2 months ago

finnbear commented 2 months ago

Using Rapier 0.17.2 in a game where objects spawn and despawn (but the total number remains roughly constant), I observe a linear increase in total QueryPipeline memory usage over time (ultimately harming bandwidth usage).

Here I plot serialized size, in bytes, of the QueryPipeline (y axis) against game tick # (x axis):

I was able to reproduce this on Rapier 0.18.0 (warning: don't mouse over the testbed window as it will crash):

[package]
name = "rapier-mem"
version = "0.1.0"
edition = "2021"

[dependencies]
rapier3d = {version = "0.18", features = ["enhanced-determinism", "serde-serialize"] }
rapier_testbed3d = "0.18"
bincode = "1.3"
rand = "0.8.4"
use std::collections::VecDeque;
use rand::thread_rng;
use rand::Rng;
use rapier3d::prelude::*;
use rapier_testbed3d::Testbed;
use rapier_testbed3d::TestbedApp;

pub fn main() {
    let builders: Vec<(_, fn(&mut Testbed))> = vec![
        ("Bug", init_world),
    ];
    let testbed = TestbedApp::from_builders(0, builders);
    testbed.run()
}

pub fn init_world(testbed: &mut Testbed) {
    let bodies = RigidBodySet::new();
    let colliders = ColliderSet::new();
    let impulse_joints = ImpulseJointSet::new();
    let multibody_joints = MultibodyJointSet::new();

    testbed.set_world(bodies, colliders, impulse_joints, multibody_joints);
    testbed.look_at(point![100.0, 100.0, 100.0], Point::origin());

    let mut i = 0;
    let mut rng = thread_rng();
    let mut handles = VecDeque::new();
    testbed.add_callback(move |mut graphics, state, _, _| {
        let rigid_body = RigidBodyBuilder::dynamic().translation(vector![
            rng.gen_range(0.0..100.0),
            rng.gen_range(0.0..100.0),
            rng.gen_range(0.0..100.0)
        ]);
        let handle = state.bodies.insert(rigid_body);
        let collider = ColliderBuilder::cuboid(1.0, 1.0, 1.0);
        state.colliders.insert_with_parent(collider, handle, &mut state.bodies);

        let color0 = [0.7, 0.5, 0.9];
        graphics.as_mut().unwrap().set_body_color(handle, color0);

        handles.push_back(handle);

        while handles.len() > 10 {
            let remove = handles.pop_front().unwrap();
            state.bodies.remove(remove, &mut state.islands, &mut state.colliders, &mut state.impulse_joints, &mut state.multibody_joints, true);
        }

        if i % 100 == 0 {
            assert!(state.bodies.len() <= 10);
            assert!(state.colliders.len() <= 10);
            let incremental_bytes = bincode::serialized_size(&state.query_pipeline).unwrap();

            let mut new_qp = QueryPipeline::new();
            new_qp.update(&state.bodies, &state.colliders);
            let new_bytes = bincode::serialized_size(&new_qp).unwrap();

            println!("{i}, {incremental_bytes}, {new_bytes}");
        }
        i += 1;
    });
}

At first I thought the refit_and_rebalance algorithm wasn't running but, as far as I can tell, it is.

Workaround

Applications can occasionally reset the query pipeline:

state.query_pipeline.update(&state.bodies, &state.colliders);