Skip to main content

physics-debug

Debug physics bugs (collision, solver, joints, integration) using deterministic repro + cdb step-through. Activates when user reports bodies exploding, tunneling, not settling, false contacts, or wrong collision behavior — especially after speculative fixes failed.

Informations de source

Dépôt
RandyGaul/nudge
Dernière activité de la source
20 avril 2026 à 03:17
Langue détectée de SKILL.md
anglais
Étoiles
34
Forks
2

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
2 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
physics-debug
description
Debug physics bugs (collision, solver, joints, integration) using deterministic repro + cdb step-through. Activates when user reports bodies exploding, tunneling, not settling, false contacts, or wrong collision behavior — especially after speculative fixes failed.
version
1.0.0
# Physics Debug — Deterministic Repro + cdb Step-Through Debug physics bugs by watching the code execute one step at a time with a debugger attached, from the frame BEFORE the bug manifests. Not by speculating and patching symptoms. ## When to activate User says things like: - "bodies are exploding / falling through / tunneling / jittering / not settling" - "collision isn't working right / objects fly off the mesh" - "find the collision bug / debug physics / step through the solver" - "why is X happening at frame Y" And **especially** when speculative fixes haven't worked. ## When NOT to activate - General algorithmic questions ("how does SAT work?") — that's research, not debug. - Build errors, syntax errors, non-physics issues. - Symptoms reproducible only in interactive scenes with mouse input (those need `remote-debug` in app mode, sometimes with `playrecording`). ## Trust the solver. Suspect its inputs. Contact constraints in PGS are the most-stable component in the engine: - **Simple**: one inequality row per contact, single lambda clamped ≥ 0 with accumulated-impulse clamping, friction cone capped by the normal lambda. That's it. - **Constantly tested**: every sub-step of every scene exercises thousands of rows across many shape pairs; if a correctness bug lived here, every bench would already be on fire. - **Globally convergent**: contact rows don't need to live in the same manifold to communicate — they share the body's velocity state, and accumulated lambdas converge across the whole island. Bugs almost never live *inside* the solver. Blowups, launches, large angular velocities, tunneling, jitter — PGS is faithfully integrating the inputs it was handed. The actual bug is virtually always upstream: - **Narrowphase** emitting bad contacts: wrong normal direction, inflated penetration, false-positive axis from a degenerate hull, duplicate contacts at the same world point, non-symmetric sampling on a rigid body. - **Warm cache** returning stale lambdas, matching a new contact against a dead feature ID, or handing out a dangling pointer after a map resize. - **Broadphase / integration** missing a frame of contact so the pair re-contacts at large penetration the next frame, or writing a NaN/garbage position the solver then tries to correct. **This is why the skill's core move is to break one physics step *before* the visible anomaly.** You want to watch the bad data get written — not inspect the wreckage after the solver has faithfully propagated it. By the time PGS finishes the anomalous step, the symptom is everywhere and the causal chain is gone. Single-stepping from the prior frame through narrowphase → pre-solve → solver-input buffers catches the exact line where a normal flips sign, a pen goes negative, or a stale lambda leaks in. **Corollary:** never "fix" a blowup by turning `beta`, `max_push_velocity`, `velocity_iters`, `sub_steps`, `contact_hertz`, or `contact_damping_ratio`. The solver is correct; a knob-tune that makes *this* symptom smaller is masking the upstream bug, which will resurface under different inputs. ## Debugging styles — pick the right tool Three styles are available. Prefer them in this order unless the table below says otherwise. | Style | What it is | When it wins | |-------|-----------|-------------| | **`remote-debug` pause points** (`DBG_BREAK`) | TCP-attached viewer inspects live memory via RPM while the test/app is paused at a named breakpoint. Zero rebuild to inspect anything new. | Default. Almost all test-side bugs; any time you'd be tempted to add "just one more printf". | | **cdb / `win-debugger` step-through** | Full debugger with call stack, step-in/step-out, registers, arbitrary expression evaluation, memory writes. | You need to watch logic *inside* a single function execute line-by-line, see the call stack leading to a bad state, or mutate locals to test a hypothesis. | | **printf / conditional logging** | Targeted `printf` or CSV dump compiled into the test. | You need a time series across many frames, aggregate statistics, or a trace to diff between runs. | ### Why pause points beat printf for most investigations - **No rebuild to change what you look at.** Printf commits you to one query; inspecting a different field means edit → rebuild → re-run. A pause point lets you fire `get body_state 2`, `table body_hot`, `contacts 1`, `get ldl_debug_info`, and `warm` in any order against the exact same paused state. - **Full reflected struct access, not a hand-picked view.** `summary` / `get` / `table` traverse the entire reflected type graph. Printfs show only what you remembered to dump — and usually miss the field that actually mattered. - **One run covers many hypotheses.** Each `continue` advances to the next break. No repeated startup cost per experiment. - **State is not transcribed.** Printf output is a text snapshot of what a human formatted at commit time. RPM reads the bytes — no chance of formatting bugs misleading you. - **Works on release code paths.** You don't need to leave formatting code in a hot loop; the pause point is a predicate check only. ### Why printf still wins sometimes - **Temporal patterns.** "When does body B first tunnel?" needs every frame, not one paused frame. Printf writes a line per frame; pausing 600 times is tedious. Use printf when the bug's signature is a *trend over time*. - **Hot-loop statistics.** Histograms, running averages, counters — build them in code, dump a summary line. - **Non-interactive CI / comparisons.** Printf output is diff-able between runs. Pause points require a live process + operator. - **Inspecting state you can't reach via reflection.** Stack-local variables, intermediate expressions in narrowphase SIMD code, control-flow decisions inside a function — printf (or a debugger) gets there; RPM does not. - **You already know exactly what one value to watch.** The printf latency is lower than standing up a viewer for a single `body_state[B].position.y < −0.3` gate (but `DBG_BREAK` gated on the same predicate is usually just as cheap). ### Why cdb step-through is still needed - **"Why did we end up in this branch?"** Reflection sees *state*, cdb sees *control flow + call stack*. - **Breaking one frame before the bug** to watch the BAD frame execute top to bottom. Pause points pause at a known named site; cdb can stop at any instruction given the right breakpoint condition. - **Mutating state to test a hypothesis.** "If I force `best.index1 = -1` here, does the bug vanish?" — only a real debugger does that. - **Bugs in code that never reaches a pause point** (e.g. infinite loops, early-exit returns, crashes inside SIMD intrinsics). A typical session uses all three: pause point to scope the investigation, cdb to step through the bad frame, printf to snapshot a time series across the fix-verification matrix. ### What each tool can actually see The three styles are **not interchangeable** — they inspect different memory regions. Keep this table in mind when the question is "which tool do I need for *this* state?" | State you want to inspect | Viewer (RPM) | cdb / win-debugger | printf | |---------------------------|:------------:|:-------------------:|:------:| | Reflected struct fields on `WorldInternal`, bodies, manifolds, joints, warm cache, LDL debug info, NP_DebugSnapshot | ✅ | ✅ | ✅ (if you printed them) | | Arbitrary memory reads at any address | ✅ | ✅ | ❌ | | **Function-local variables** (e.g. `best`, `face_a`, `edge_q` inside `collide_hull_hull_ex`) | ❌ | ✅ | ✅ (if you printed) | | **Callstack** leading to current location | ❌ | ✅ | ❌ | | **Intermediate expressions** inside a function (mid-SIMD, pre-return) | ❌ | ✅ | ✅ (if you printed) | | **Control-flow decisions** ("did we take this branch?") | ❌ (inferred from state) | ✅ | ✅ | | **Mutating state** to test a hypothesis ("what if this were 0?") | ❌ | ✅ | ❌ | | Multiple hypotheses against same paused moment — no rebuild | ✅ | ✅ | ❌ (rebuild each) | | **Time-series across N frames** | ❌ (one moment at a time) | ❌ (painful) | ✅ | | **Diff-able output between runs** | ❌ | ❌ | ✅ | | **CI / headless** | ❌ (needs operator) | ❌ (needs operator) | ✅ | **Biggest trap:** thinking RPM will show you locals. It won't. The reflected type graph traverses structs reachable from `WorldInternal*`; function locals live on the thread stack at addresses RPM can't easily resolve. If the value you want is a local in a narrowphase or solver function, you need cdb — or you need to publish that local into reflected storage first (see "Deep narrowphase debugging" below). ### Deep narrowphase debugging: cdb + viewer on the same paused process DBG_BREAK parks the calling thread in `Sleep(5)` inside `dbg_break_wait_impl`. The callstack above that sleep is still live, so you can attach cdb to the already-paused process and inspect everything cdb normally can — *plus* the viewer keeps giving you RPM-level state in parallel. Workflow for a deep NP bug (e.g. "SAT picks the wrong axis for this box-hull pair"): 1. **Place a break point inside** (or immediately after) the narrowphase call: ```c // inside collide_hull_hull_ex_dbg, right before the successful return DBG_BREAK("np:hull-hull:wrong-axis", *(World*)&w_handle); ``` Or — since bodies travel by index, not by pointer — use an existing entry point in the dispatch (`np_hull_hull` etc.) and read `c->body_a`/`c->body_b`. 2. **Run** with debug enabled and filtered: ```bash ./build/Debug/nudge_tests.exe --debug --break=np:* --bench-trimesh ``` Stderr shows `[dbg] break "np:hull-hull:wrong-axis" at ... (world=0x...)` when the filter matches. 3. **Attach cdb to the paused PID** (grab the PID from the viewer's connect banner or the stderr line): ``` cdb -p <pid> ~*k # every thread's callstack — find the one parked in dbg_break_wait_impl ~<nn>s # switch to that thread .frame 2 # step up out of Sleep/dbg_break_wait_impl into your NP frame dv # list locals: face_a, face_b, edge_q, ref_face, flip, ... ?? face_a.separation # evaluate any C expression dt FaceQuery @@c++(&face_a) # dump a struct ``` 4. **Run the viewer concurrently** on its own socket for the full world state: ``` > summary > get body_state 42 > get np_debug # <-- reflected SAT intermediates, see below > contacts 42 ``` 5. **Detach cdb** (`q` or `.detach`), then `continue` from the viewer to resume the test. Rule of thumb: when the bug is "which axis did SAT pick and why", cdb wins. When it is "what contact manifold did we hand the solver for body X", the viewer wins. A deep narrowphase session usually needs both. ### `NP_DebugSnapshot`: reflected SAT intermediates (no cdb required) To close the "I need to see `face_a.separation` mid-NP" gap without cdb, narrowphase writes a reflected snapshot to `WorldInternal.np_debug` when enabled. The viewer reads it via `get np_debug`. **Turn it on** (from the viewer): ``` > np-debug 1 42 -1 # capture any pair touching body 42; -1 = any for the other side > step 1 # (app mode) or `continue` past a DBG_BREAK in test mode > get np_debug ``` **What it contains** (per hull-hull pair): - `body_a`, `body_b`, `shape_a_kind`, `shape_b_kind`, `frame` - Full SAT output: `face_a_index` + `face_a_sep`, `face_b_index` + `face_b_sep`, `edge_a_index` + `edge_b_index` + `edge_sep` - `winning_axis`: `0` = face_a, `1` = face_b, `2` = edge, `-1` = separated / no contact produced - Final manifold: `contact_normal`, `contact_count`, `contact_points[0..3]`, `contact_pens[0..3]` **When it suffices (no cdb needed):** - Which SAT axis won, and by how much over the runners-up. - Whether the final normal is plausible (does `contact_normal` point from A to B? Is it unit-length?). - Whether contact count and penetrations look sane. - Comparing two runs: capture on frame 53 vs frame 54 to see the transition. **When you still need cdb:** - SAT picked the right axis but the *support vertex* it found is wrong — that's inside `hull_support` / `sat_eval_face_ex`, not in the snapshot. - Hill-climb got stuck at a local minimum — you need the `sat_hint` path and intermediate face indices, which are locals. - Contact reduction collapsed two points that shouldn't have collapsed — `reduce_contacts` loop internals aren't in the snapshot. - Anything in GJK / EPA / capsule code — the snapshot only covers hull-hull SAT today (the worst offender for local-state-heavy bugs). Extend it if a new hot spot appears. **Caveat:** single-slot snapshot. Under parallel narrowphase (`--threads >1`) the slot is raced; force `--threads 1` when using it. **Zero-cost when off:** the dispatch agents take one predicate branch (`w->np_debug_enabled`) and skip. Safe to ship. ## Core methodology 1. **Deterministic repro first.** Same input + same binary = same numbers every run. 2. **Minimize** to one body, one frame, one anomaly, whenever possible. You want a minimal repro to isolate variables. 3. **Inspect before stepping.** Land a `DBG_BREAK` pause point at/just-before the anomaly, attach the viewer, and read the full paused state via RPM (`summary`, `get`, `table`, `contacts`, `warm`). This is the new default — it replaces most exploratory printfs. 4. **Attach cdb one physics step BEFORE the bug** when you also need to watch *control flow* inside the bad frame. Post-mortem state is already corrupted; watch the bad step unfold. 5. **Step-through with a hypothesis.** Every breakpoint has an expected/actual comparison. 6. **Fix the root cause** in the suspected module. Minimal change, try to isolate variables. You may make surgical changes to gather information to continue root-cause analysis. 7. **Verify with varied inputs**, not just the one repro, but verify with similar setups, variational inputs. ## The five phases ### Phase 1 — Deterministic repro **Rule:** You cannot debug what you cannot reproduce exactly. "Sometimes fails" = undebuggable. - Use a bench / test binary, not an interactive scene. Fixed script, fixed initial state, no mouse. - For nudge: `nudge_tests.exe --bench-trimesh`, `--bench-ragdoll`, etc. Or write a one-off minimal test. - Confirm determinism: run the same command 2× and diff the output. If it differs, fix determinism first. ```bash for run in 1 2; do ./build/Release/nudge_tests.exe --bench-trimesh > run-$run.txt 2>&1 done diff run-1.txt run-2.txt # must be empty ``` **Minimize** until the repro is the smallest configuration that still shows the bug. Bodies: 100→50→10→1. Shapes: mixed→single type. Mesh: simplify until bug disappears, then back off one change. **Output:** a repro that fires the bug at a specific (body B, frame F), with pre-frame state recorded. ### Phase 2 — Instrument to find the moment Pinpoint (body, frame) programmatically. **Default to `DBG_BREAK` pause points.** Reach for printf or conditional CSV dumps only when you need a trend across many frames. #### 2a. Default: guarded pause point (preferred) Insert a `DBG_BREAK` conditional on the anomaly. The first time the predicate trips, the test thread blocks and waits for a viewer. Then you can inspect *anything* about the paused state — no rebuild, no pre-committed print format. ```c // In the test loop, body index `bi`, frame `f` if (wi->body_state[bi].position.y < -0.3f) { DBG_BREAK("tunnel:first", *(World*)&w); // first offender caught } ``` Run the test with the debugger server up: ```bash ./build/Debug/nudge_tests.exe --debug --break=tunnel:* --unit # or for a specific test-only binary that has bench flags: ./build/Debug/nudge_tests.exe --debug --break=tunnel:* --bench-trimesh ``` Attach the viewer from another shell: ```bash ./build/Debug/nudge_viewer.exe localhost 9999 > where # file:line of the pause > summary # frame, body count, contacts, islands > get body_state # positions + rotations of every body > get body_hot 85 # velocities of the target body > contacts 85 # its solver contacts > table body_hot # all velocities side-by-side > continue # resume until next match (or test end) ``` This gives you the full pre-frame picture without committing to any particular printf format ahead of time. #### 2b. Fallback: conditional printf / CSV (trend over time) When the bug's signature is "the value drifts over N frames" or you want to diff two runs, printf wins. Keep it minimal — one event, one line, stop. ```c // In the bench loop if (first_blowup_frame < 0 && wi->body_state[bi].position.y < -0.3f) { first_blowup_frame = f; first_blowup_body = bi; // snapshot prev_pos/prev_vel for the frame BEFORE, then set the flag so // this block doesn't fire again. Avoid flooding stdout — you want one hit. } ``` Iterate the threshold until you catch the earliest manifestation — a body penetrating the mesh by 0.01 is a better target than one already at y=-100. **Output of this phase:** concrete state 1 frame BEFORE the bug, e.g. *"frame
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub