#1 - quicr module

This commit is contained in:
Martin Slachta
2026-07-22 17:34:44 +02:00
parent a04f0dc262
commit f4174eb0c7
177 changed files with 5309 additions and 2265 deletions
+5 -1
View File
@@ -14,8 +14,12 @@ public:
JPH::Vec3 m_desired_velocity;
// JPH::Vec3's default constructor leaves the value uninitialised, and the
// desired velocity is accumulated across steps, so it has to start at zero
// or the first step feeds garbage into the character's velocity.
CharacterBody(JPH::CharacterVirtual* character) :
m_character(character)
m_character(character),
m_desired_velocity(JPH::Vec3::sZero())
{ }
};
+42 -12
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <chrono>
#include <glm/glm.hpp>
@@ -11,6 +12,7 @@ namespace tw {
* Controls the character's body
*/
class CharacterController {
private:
float m_speed;
using Clock = std::chrono::steady_clock;
@@ -18,7 +20,16 @@ class CharacterController {
HistoryBuffer<Clock::time_point, glm::vec3> m_history;
HistoryBuffer<Clock::time_point, glm::vec3> m_position_history;
glm::vec3 m_input;
// Frame-indexed input ring, capacity 64
struct InputSlot {
uint32_t frame;
glm::vec3 input;
bool valid;
};
std::array<InputSlot, 64> m_input_ring;
uint32_t m_last_input_frame;
glm::vec3 m_last_input;
uint32_t m_frame_idx;
@@ -31,12 +42,26 @@ public:
m_speed(speed),
m_history(Clock::now(), glm::vec3(), 10 * 20),
m_position_history(Clock::now(), glm::vec3(), 10 * 20),
m_last_input_frame(0),
m_last_input(0.0f),
m_frame_idx(0)
{ }
{
// Initialize input ring
for(auto& slot : m_input_ring) {
slot.frame = 0;
slot.input = glm::vec3(0.0f);
slot.valid = false;
}
}
void set_input(uint32_t frame_idx, glm::vec3 input) {
// m_history.set(frame_idx, input);
m_input = input;
size_t idx = frame_idx % m_input_ring.size();
m_input_ring[idx].frame = frame_idx;
m_input_ring[idx].input = input;
m_input_ring[idx].valid = true;
m_last_input_frame = frame_idx;
m_last_input = input;
}
void set_frame_idx(uint32_t idx) {
@@ -44,18 +69,23 @@ public:
}
glm::vec3 input() const {
return m_input;
// return m_history.values()[m_history.values().size() - 1];
return m_last_input;
}
// Returns the input for the specified frame, or falls back to the most recently set input
// if the frame slot has been overwritten or never written
glm::vec3 input(uint32_t frame_idx) const {
return m_input;
// auto value = m_history.get(frame_idx);
// if(value.has_value()) {
// return *value.value();
// }
size_t idx = frame_idx % m_input_ring.size();
const auto& slot = m_input_ring[idx];
// return glm::vec3();
// If slot contains the exact frame we're looking for, return it
if(slot.valid && slot.frame == frame_idx) {
return slot.input;
}
// Otherwise, fall back to the most recent input
// This handles dropped packets (slot never written) or wraparound (slot overwritten)
return m_last_input;
}
};
+79 -23
View File
@@ -40,7 +40,6 @@ JoltPhysicsWorld::JoltPhysicsWorld(World* world) :
temp_allocator(std::make_unique<JPH::TempAllocatorImpl>(10 * 1024 * 1024)),
job_system(JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers, std::thread::hardware_concurrency() - 1),
m_world(world),
m_history(0, std::move(std::make_unique<JPH::StateRecorderImpl>()), 10),
m_latest_frame(0),
broad_phase_layer_interface(),
object_vs_broadphase_layer_filter(),
@@ -50,6 +49,11 @@ JoltPhysicsWorld::JoltPhysicsWorld(World* world) :
contact_listener(),
m_character_vs_character_collision()
{
// Initialize snapshot ring
for(auto& snapshot : m_snapshot_ring) {
snapshot.frame = 0;
snapshot.valid = false;
}
// for(int i = 0; i < m_thread_pool.size(); i++) {
// temp_allocator.emplace_back(std::make_unique<JPH::TempAllocatorImpl>(10 * 1024 * 1024));
// }
@@ -92,6 +96,27 @@ JoltPhysicsWorld::JoltPhysicsWorld(World* world) :
// body_interface().SetLinearVelocity(sphere_id, JPH::Vec3(0.0f, -5.0f, 0.0f));
}
void JoltPhysicsWorld::save_snapshot(uint32_t frame) {
size_t idx = frame % m_snapshot_ring.size();
auto& snapshot = m_snapshot_ring[idx];
// Clear and set up the slot
snapshot.recorder.Clear();
snapshot.frame = frame;
snapshot.valid = true;
snapshot.desired_velocities.clear();
// Save the physics world state
physics_system.SaveState(snapshot.recorder);
// Save character states in deterministic order
auto view = m_world->registry().view<CharacterController, CharacterBody>();
view.each([&](const CharacterController& controller, CharacterBody& rb) {
rb.m_character->SaveState(snapshot.recorder);
snapshot.desired_velocities.push_back(rb.m_desired_velocity);
});
}
void JoltPhysicsWorld::update(uint32_t frame_idx, double delta_time) {
auto view = m_world->registry().view<CharacterController, CharacterBody>();
@@ -117,7 +142,9 @@ void JoltPhysicsWorld::update(uint32_t frame_idx, double delta_time) {
: vel;
// True if the player intended to move
mAllowSliding = input.length() < 0.01f; // allow sliding when idle, prevent when moving
// glm::vec3::length() is the static component count (3), not the
// magnitude, so this has to go through glm::length.
mAllowSliding = glm::length(input) < 0.01f; // allow sliding when idle, prevent when moving
}
JPH::Vec3 current_vertical_velocity = character->GetLinearVelocity().Dot(character->GetUp()) * character->GetUp();
JPH::Vec3 ground_velocity = character->GetGroundVelocity();
@@ -181,16 +208,15 @@ void JoltPhysicsWorld::update(uint32_t frame_idx, double delta_time) {
});
physics_system.Update(delta_time, 1, temp_allocator.get(), &job_system);
auto stateRecorder = std::make_shared<JPH::StateRecorderImpl>();
physics_system.SaveState(*stateRecorder.get());
m_history.set(frame_idx, stateRecorder);
}
void JoltPhysicsWorld::step(uint32_t frame_idx, double delta_time) {
void JoltPhysicsWorld::step(uint32_t frame_idx, double delta_time, bool record_snapshot) {
update(frame_idx, delta_time);
if(record_snapshot) {
save_snapshot(frame_idx);
}
m_world->registry().view<Transform, CharacterBody>()
.each([&](Transform& ts, CharacterBody& rb) {
auto character = rb.m_character;
@@ -199,22 +225,52 @@ void JoltPhysicsWorld::step(uint32_t frame_idx, double delta_time) {
memcpy(&ts.transform, &transform, sizeof(glm::mat4));
});
// copy values
// m_world->registry().view<Transform, RigidBody>()
// .each([&](Transform& ts, RigidBody& rb) {
// auto transform = body_interface().GetWorldTransform(rb.id);
// memcpy(&ts.transform, &transform, sizeof(glm::mat4));
// });
//
// m_world->registry().view<Transform, CharacterBody>()
// .each([&](Transform& ts, CharacterBody& rb) {
// auto character = rb.m_character;
//
// auto transform = character->GetWorldTransform();
// memcpy(&ts.transform, &transform, sizeof(glm::mat4));
// });
m_latest_frame = std::max(m_latest_frame, frame_idx);
}
bool JoltPhysicsWorld::rollback(uint32_t frame) {
if(frame == 0) return true;
size_t idx = frame % m_snapshot_ring.size();
auto& snapshot = m_snapshot_ring[idx];
// Check if snapshot exists and matches the requested frame
if(!snapshot.valid || snapshot.frame != frame) {
return false;
}
// Restore the physics world state
snapshot.recorder.Rewind();
physics_system.RestoreState(snapshot.recorder);
// Restore character states in the same deterministic order
size_t char_idx = 0;
auto view = m_world->registry().view<CharacterController, CharacterBody>();
view.each([&](const CharacterController& controller, CharacterBody& rb) {
rb.m_character->RestoreState(snapshot.recorder);
if(char_idx < snapshot.desired_velocities.size()) {
rb.m_desired_velocity = snapshot.desired_velocities[char_idx];
}
char_idx++;
});
return true;
}
void JoltPhysicsWorld::apply_rollback(uint32_t frame) {
// Replay frames from frame+1 through m_latest_frame inclusive
for(uint32_t i = frame + 1; i <= m_latest_frame; i++) {
update(i, FIXED_DELTA_TIME);
save_snapshot(i);
}
// Sync transforms from characters to match the current state
m_world->registry().view<Transform, CharacterBody>()
.each([&](Transform& ts, CharacterBody& rb) {
auto character = rb.m_character;
auto transform = character->GetWorldTransform();
memcpy(&ts.transform, &transform, sizeof(glm::mat4));
});
}
}
+20 -15
View File
@@ -5,6 +5,7 @@
#include <print>
#include "metrics/HistoryBuffer.hpp"
#include <array>
#include <glm/glm.hpp>
#include <Jolt/Jolt.h>
@@ -189,7 +190,14 @@ private:
JPH::CharacterVsCharacterCollisionSimple m_character_vs_character_collision;
HistoryBuffer<int, std::shared_ptr<JPH::StateRecorderImpl>> m_history;
struct Snapshot {
uint32_t frame;
bool valid;
JPH::StateRecorderImpl recorder;
std::vector<JPH::Vec3> desired_velocities;
};
std::array<Snapshot, 64> m_snapshot_ring;
inline JPH::BodyInterface &body_interface() {
return physics_system.GetBodyInterface();
@@ -203,7 +211,11 @@ private:
void update(uint32_t frame_idx, double delta_time);
void save_snapshot(uint32_t frame);
public:
static constexpr double FIXED_DELTA_TIME = 1.0 / 20.0;
JoltPhysicsWorld(World* world);
RigidBody create_dynamic_rigid_body(JPH::Shape* shape, glm::vec3 position) {
@@ -242,24 +254,17 @@ public:
body_interface().AddForce(rigidbody.id, JPH::RVec3(force.x, force.y, force.z));
}
void step(uint32_t frame_idx, double delta_time);
void step(uint32_t frame_idx, double delta_time, bool record_snapshot = false);
void rollback(uint32_t frame) {
if(frame == 0) return;
auto snapshot = m_history.get(frame);
bool rollback(uint32_t frame);
if(!snapshot.has_value()) {
throw std::runtime_error("Failed to restore history");
}
void apply_rollback(uint32_t frame);
auto snapshot_value = snapshot.value()->get();
physics_system.RestoreState(*snapshot_value);
}
uint32_t latest_frame() const { return m_latest_frame; }
void apply_rollback(uint32_t frame) {
for(uint32_t i = frame; i < m_latest_frame; i++) {
update(i, 1000.0f / 20.0f);
}
bool has_snapshot(uint32_t frame) const {
size_t idx = frame % m_snapshot_ring.size();
return m_snapshot_ring[idx].valid && m_snapshot_ring[idx].frame == frame;
}
};