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.

معلومات المصدر

المستودع
RandyGaul/nudge
آخر نشاط في المصدر
٢٠ أبريل ٢٠٢٦ في ٠٣:١٧
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٣٤
التفرعات
٢

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
2 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub