Skip to content

Fix crashes, rework physics, and add tests, CI, tools and a demo - #1

Merged
RohanFredriksson merged 9 commits into
masterfrom
claude/engine-audit-and-plan
Oct 16, 2026
Merged

RohanFredriksson merged 9 commits into
masterfrom
claude/engine-audit-and-plan

Conversation

@RohanFredriksson

Copy link
Copy Markdown
Owner

Picks the engine up from where the force-generator refactor left it in August 2024. This fixes the memory bugs that refactor introduced and makes the physics hold up for normal game scenes. It also adds tests and CI, finishes the half-built features, and adds a demo sandbox.

Memory-safety fixes

  • Crash on reset or close: resetting or closing any scene with a Gravity component caused a heap-use-after-free, because World deleted force generator components that their entities also deleted. Entities now own them; the world only registers them.
  • Killed bodies left in the grid: a killed rigidbody stayed in the physics spatial hash grid, so later steps used freed memory. Both crashes were reproduced under ASan before the fix.
  • Window lifecycle: scene teardown order is fixed, and the window now deletes the scene, shaders, picking framebuffer and ImGui context on shutdown. The engine can also be started again after stop().

Physics

  • Box/box collision: now a separating-axis test with reference face clipping. The old corner-in-box test missed edge overlaps, so identical boxes stacked on each other fell through.
  • Circle/box collision: handles a circle whose centre has entered the box.
  • Solver: replaced with warm-started sequential impulses. The step order is now velocities → solve → positions; integrating positions before solving made stacks collapse. A 5-row pyramid now stays standing at any frame rate.
  • Mass properties: angular velocity integrates with the moment of inertia where it used mass, and the parallel-axis term uses each collider's own mass where it used the total.
  • Bounds: bounds no longer always include the entity origin.
  • Step limits: fixed steps are capped at 5 per frame.
  • World gravity: the gravity argument was ignored and is now applied, with a per-body gravityScale.
  • Raycasts: box raycasts are rewritten (the old version ignored the ray), circles behind the ray no longer hit, and World::raycast(ray, maxDistance) walks the grid.
  • Debug drawing: off by default. It also used to allocate about 4.8 GB of GPU memory, which is fixed.

Engine and tools

  • Spatial queries: the scene's unused entity spatial grid is now kept up to date and backs getEntitiesIn/Near/At.
  • Scene files: they store gravity and the scene name, load audio from audio (it read fonts), and keep rigidbody velocity on load.
  • Console: argument parsing and commands (help, save, load, reset, debug, gravity, entities, inspector), plus Console::registerCommand and Console::execute.
  • Inspector: a new window where you click an entity to edit its components. Rigidbody has a panel now.
  • Window size and high-DPI: Pancake::size sets the correct size, and the viewport follows the framebuffer size on high-DPI displays.
  • Input: begin-down key and mouse queries last for the whole frame instead of being consumed by the first reader.
  • Per-frame stall: removed a per-frame glReadPixels call.

Build, tests and CI

  • CMake:
    • C++17 is now required explicitly.
    • AddressSanitizer is opt-in through PANCAKE_ASAN; it was always linked into games on Linux.
    • PANCAKE_BUILD_TESTS and PANCAKE_BUILD_EXAMPLES default to on only when Pancake is the top-level project.
  • Tests:
    • Unit tests that need no window: spatial grid, collision, solver (resting, stacking, pyramid, angled drop, ball rest), force generators, raycasts and JSON round trips.
    • An engine test that runs the real loop and covers settle, save, reload, killing a body, resetting with force generators, and console commands.
    • A smoke run of the demo.
  • CI: a GitHub Actions workflow builds with ASan and in Release, then runs everything under Xvfb.
  • Demo: examples/demo.cpp is a physics sandbox.

Behaviour changes for existing games

These are also listed under "Upgrading" in the README.

  • Gravity: world gravity now defaults to (0, -10). A game that added a Gravity component to get global gravity should remove it or set the world gravity to zero.
  • Solver feel: restitution and friction may feel slightly different with the new solver. Bounces slower than 1 unit/s are ignored.
  • Input: isKeyBeginDown and isMouseBeginDown now stay true for the whole frame.

Testing

  • All 6 tests pass in both ASan and Release builds locally, including from a fresh clone of this branch.
  • The demo was checked visually under Xvfb, with screenshots of the pyramid, debug drawing, console and inspector.
  • Leak checking runs for the unit tests but is off for the tests that open a window, because the Mesa driver leaks memory from libraries it unloads, which can't be suppressed by name.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a


Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a
- World no longer deletes ForceGenerator components; entities own them.
  Removing a generator only unregisters it if it is the active one.
- World::remove now removes the rigidbody from the spatial hash grid,
  fixing a use-after-free when a body is killed near another body.
- Static bodies refresh their grid registration when moved by game code.
- Scene teardown deletes entities before the systems they unregister from.
- Window resets its stop flag on start, deletes the scene, shaders,
  picking framebuffer and ImGui context on shutdown, and no longer reads
  a pixel back from the GPU every frame.
- DebugDraw allocated MAX_LINES times too much GPU memory.
- Rigidbody: integrate angular velocity with the moment of inertia, use
  each collider's mass in the parallel axis term, compute bounds without
  always including the origin, and drop force registrations in
  clearForceGenerators.
- Begin-down key/mouse queries now last for one frame instead of being
  consumed by the first reader.
- Entity tolerates having no active scene; define Entity::setId.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a
Collision detection
- Box/box now uses a separating axis test with reference face clipping.
  The old corner-containment test missed edge overlaps, so identical
  boxes stacked on each other fell straight through.
- Circle/box finds the closest point on the box and handles circles
  whose centre has entered the box.
- All features report the full penetration depth with the normal from
  the first collider to the second.

Solver
- Correct positions once per step after the impulse iterations, rather
  than re-applying the correction in every iteration.
- Guard against NaN when contacts are touching with zero depth.
- Cap the number of fixed steps per frame and drop excess time.
- World gravity (previously an unused constructor argument) is applied
  to every dynamic body, scaled by a new per-body gravityScale.

Raycasts
- Box raycasts use a slab test in the box's local space (the old code
  ignored the ray entirely). Circles behind the ray are no longer hit,
  and normals point out of the surface.
- World::raycast(ray, maxDistance) walks the spatial hash grid cells
  along the ray instead of testing every body.

Ownership
- Rigidbodies and force generators remember the World they were added
  to, so they unregister from the right world and work outside of a
  running scene.
- Rigidbody::load reads colliders before velocity and force, which were
  previously dropped because a body without colliders has no mass.

Debug drawing
- Physics debug drawing (bounds, colliders, contacts) is off by default
  and toggled with World::setDebugDraw. DebugDraw can be disabled
  entirely with DebugDraw::setEnabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a
- Scene keeps its (previously unused) entity spatial hash grid in sync
  and offers getEntitiesIn, getEntitiesNear and getEntitiesAt.
- Scene files store world gravity and restore the scene name, read
  audio from the "audio" key instead of "fonts", and report missing
  files correctly.
- The console parses arguments and supports help, quit, clear, reset,
  save, load, debug, gravity, entities and inspector. Games can add
  their own with Console::registerCommand, and run commands with
  Console::execute.
- New Inspector window: click an entity in the game to select it and
  edit its components, or pick it from the entity list. It also edits
  scene gravity and physics debug drawing.
- Pancake::gravity, Pancake::debug and Pancake::inspector facade calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a
The previous solver averaged impulses across contacts and added a
heuristic linear velocity, and the step integrated positions before
solving contacts, so resting bodies sank every step and relied on
position correction. A five row pyramid collapsed within seconds.

- Step order is now: forces, integrate velocities, detect, solve,
  integrate positions, correct positions.
- Contacts accumulate and clamp normal and friction impulses, and are
  warm started from a cache of last step's impulses.
- Pairs are ordered by body id so results don't depend on memory layout.
- Bounces below 1 unit per second are ignored to stop resting jitter.
- Fixed orientation bodies get no rotational response.
- Rigidbody gets an inspector panel.
- Entity::addRotationAround compared the pivot to the world origin
  rather than the entity's own position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a
- Pancake::size set width and height with two resizes, the second using
  a stale width. Before start the size is now used at creation, and at
  runtime both dimensions are applied together.
- The viewport and picking framebuffer follow the framebuffer size,
  which differs from the window size on high DPI displays. readPixel
  converts window coordinates to pixels.
- The console and inspector open in opposite corners.
- Fall back to the default font if a font file cannot be read.
- Remove unused variables and make shader sources constexpr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a
- tests/: a minimal test harness and unit tests for the spatial grid,
  collision detection, the solver (resting, stacking, pyramids, angled
  drops), force generators, raycasts and JSON round trips, plus an
  engine test that runs the real loop to check saving, reloading,
  killing bodies, resetting scenes with force generators and console
  commands.
- CMake: C++17 is required explicitly, AddressSanitizer moved behind
  PANCAKE_ASAN (it was always linked into games on Linux), and
  PANCAKE_BUILD_TESTS / PANCAKE_BUILD_EXAMPLES default on only when
  Pancake is the top level project.
- GitHub Actions builds with ASan and in Release, and runs all tests
  under Xvfb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a
Spawns boxes and balls, pans and zooms the camera, and binds keys to the
console, inspector and physics debug drawing. --frames N quits after N
frames, which CTest uses as a smoke test of the whole engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a
Document building the engine on its own, the tests and options, the
debugging tools, and upgrade notes for world gravity, the new solver and
per-frame input queries. Fix the README example, which never fell
because nothing applied gravity, and drop the gamepad claim as there is
no gamepad support.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSbmWwU5zh3ujWmjioWg1a
@RohanFredriksson
RohanFredriksson merged commit bf4967b into master Oct 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants