|
| 1 | +# Babylon Native -- Debuggability Reference |
| 2 | + |
| 3 | +Read this **before** debugging Babylon Native crashes, hangs, asserts, or visual |
| 4 | +test failures. The Playground app has dedicated diagnostic infrastructure |
| 5 | +that turns common silent failures into searchable banners with stack traces. |
| 6 | +Don't add new ad-hoc logging or `MessageBox` prompts when the existing facilities |
| 7 | +already cover the case. |
| 8 | + |
| 9 | +## When to Use |
| 10 | + |
| 11 | +Read this when the user mentions: Playground, validation tests, BabylonNative |
| 12 | +crash, headless tests, `BX_ASSERT`, "MS C++ runtime dialog", call stack, |
| 13 | +`config.json` test, `excludeFromAutomaticTesting`, pixel diff failure, |
| 14 | +`captureNextFrame`, RenderDoc capture from Playground, or asks to debug a |
| 15 | +specific BN test by name or index. |
| 16 | + |
| 17 | +For pure RenderDoc CLI usage (any app, not BN-specific), see |
| 18 | +`renderdoc/renderdoc-gpu-debug.instructions.md` and |
| 19 | +`renderdoc/rdc-commands-reference.instructions.md`. |
| 20 | + |
| 21 | +## Layout |
| 22 | + |
| 23 | +| File / Dir | Role | |
| 24 | +|---|---| |
| 25 | +| `Apps/Playground/Shared/CommandLine.{h,cpp}` | Argument parser. Single source of truth for supported flags. | |
| 26 | +| `Apps/Playground/Shared/Diagnostics.{h,cpp}` | Crash handler, `DumpFailure`, finish-line, exit-code tracking. | |
| 27 | +| `Apps/Playground/Shared/AppContext.cpp` | Wires `UnhandledExceptionHandler` + `console.error` into `DumpFailure`; injects `_playgroundOptions` into JS. | |
| 28 | +| `Apps/Playground/Scripts/validation_native.js` | Test runner. Reads `_playgroundOptions`, picks tests, calls `TestUtils.captureNextFrame()`. Reference-image load failures arrive via `BABYLON.Tools.LoadFile`'s `onLoadFileError` and are tagged with `MISSING_REFERENCE_IMAGE:`. | |
| 29 | +| `Apps/Playground/Scripts/config.json` | Test catalog. Each entry has `title`, `playgroundId`/`scriptToRun`, `referenceImage`, optional `excludeFromAutomaticTesting`/`reason`/`onlyVisual`/`renderCount`/`capture`/`threshold`/`errorRatio`. | |
| 30 | +| `Plugins/TestUtils/Source/TestUtils.cpp` | Native side of `TestUtils.captureNextFrame()` -- calls `m_deviceContext.RequestCaptureNextFrame()`. | |
| 31 | +| `Core/Graphics/Source/BgfxCallback.cpp` | bgfx trace/fatal sink. Routes bgfx output to stdout in headless mode and to `OutputDebugString` always. | |
| 32 | + |
| 33 | +## Built-in features -- do NOT re-implement |
| 34 | + |
| 35 | +These are already wired up. Use them; don't add parallel mechanisms. |
| 36 | + |
| 37 | +### 1. Crash + assert handler with native call stacks |
| 38 | +`Diagnostics::InstallCrashHandler()` runs from `Diagnostics::Initialize()`, |
| 39 | +which is called at the **top** of `wWinMain` / `main`. It: |
| 40 | +- installs `bx::installExceptionHandler()` (SEH on Windows, signals elsewhere) |
| 41 | + so unhandled exceptions print a banner + native callstack to **stderr** |
| 42 | + before terminating with exit code **3**; |
| 43 | +- on Windows: suppresses the modal "Microsoft Visual C++ Runtime Library" |
| 44 | + assertion dialog, the `abort()` retry dialog, and the invalid-parameter |
| 45 | + dialog -- all are routed through `DumpFailure` instead; |
| 46 | +- only calls `bx::debugBreak()` when a debugger is attached, so the breakpoint |
| 47 | + doesn't escape and corrupt the exit code in CI; |
| 48 | +- maps `BX_ASSERT` / `BX_CHECK` failures into the same banner path (the bx |
| 49 | + default assert handler delegates to `DumpFailure`). |
| 50 | + |
| 51 | +### 2. `DumpFailure(category, file, line, skipFrames, fmt, ...)` |
| 52 | +Reusable, formatted banner emitter. Output looks like: |
| 53 | +``` |
| 54 | +--- <CATEGORY> --- |
| 55 | +<file>(<line>): <printf-formatted message body> |
| 56 | +
|
| 57 | +Callstack (N): |
| 58 | + #: File --- Line PC --- Function --- |
| 59 | + 0: foo.cpp 123 0x00007ff6... foo::bar |
| 60 | + ... |
| 61 | +
|
| 62 | +Build info: |
| 63 | + Compiler: ..., CPU: ..., Arch: ..., OS: ..., CRT: ..., C++: ..., Date: ..., Time: ... |
| 64 | +
|
| 65 | +--- END --- |
| 66 | +``` |
| 67 | +Every line is grep-able and includes a callstack down to the failure site. |
| 68 | + |
| 69 | +Pre-wired call sites: |
| 70 | +| Category | Triggered by | |
| 71 | +|---|---| |
| 72 | +| `ASSERT` | `BX_ASSERT` failures | |
| 73 | +| `ABORT` | `SIGABRT` / runtime invalid-parameter / `_CRT_ERROR` | |
| 74 | +| `CRASH` | unhandled SEH / `bx::installExceptionHandler` path | |
| 75 | +| `UNCAUGHT JS ERROR` | `Babylon::AppRuntime::UnhandledExceptionHandler` | |
| 76 | +| `JS CONSOLE ERROR` | every `console.error(...)` from JS (Babylon.js routes recoverable errors through this) | |
| 77 | + |
| 78 | +When you see `[Error] Error: Cannot load X` in stdout, **scroll up** -- |
| 79 | +`DumpFailure` prints the full banner + JS + native stacks **before** the |
| 80 | +short error line. The short line is kept so legacy log scrapers still match. |
| 81 | + |
| 82 | +### 3. JS stack on every `console.error` |
| 83 | +`AppContext.cpp`'s `Console::Initialize` callback calls |
| 84 | +`Babylon::Polyfills::Console::CaptureCurrentJsStack(env)` on every |
| 85 | +`LogLevel::Error` message and appends the captured stack to the |
| 86 | +`DumpFailure` banner body. The capture is best-effort -- if the JS engine |
| 87 | +can't produce a stack (no JS context active, etc.) the helper returns an |
| 88 | +empty string and the banner just shows the message. |
| 89 | +Do not add per-callsite stack capture -- it's automatic. |
| 90 | + |
| 91 | +### 4. Colored finish line |
| 92 | +`Playground: Finished in <time>. (exit <code>)` is printed once at every exit |
| 93 | +path (atexit + at_quick_exit + explicit calls). Green on success, red on |
| 94 | +failure. Use `Diagnostics::SetExitCode(N)` before any `std::quick_exit`/`_Exit` |
| 95 | +so the line colorizes correctly. |
| 96 | + |
| 97 | +### 5. Headless mode + stdout routing |
| 98 | +`--headless` runs without a visible window (still creates an HWND for input |
| 99 | +plumbing) and **routes all `console.log`/`console.error`, bgfx trace, and |
| 100 | +diagnostic banners to stdout** -- without it, output goes to OutputDebugString |
| 101 | +only and is invisible from a console run. Use `--headless --once` for any |
| 102 | +scripted/CI run. |
| 103 | + |
| 104 | +### 6. RenderDoc capture trigger (already integrated) |
| 105 | +`TestUtils.captureNextFrame()` JS API -> `DeviceContext::RequestCaptureNextFrame()` |
| 106 | +-> on next bgfx frame, `BGFX_FRAME_DEBUG_CAPTURE` flag -> bgfx's |
| 107 | +`renderDocTriggerCapture()` -> `s_renderDoc->TriggerCapture()`. bgfx loads |
| 108 | +`renderdoc.dll` automatically if it's reachable via PATH/LoadLibrary at |
| 109 | +init. **Don't add a parallel RenderDoc integration.** Use the `--capture=N` |
| 110 | +CLI flag (see below) for ad-hoc capture; see |
| 111 | +`playground/playground-renderdoc-capture.instructions.md` for the |
| 112 | +test-capture recipe and gotchas. |
| 113 | + |
| 114 | +## Command-line flags (Playground) |
| 115 | + |
| 116 | +Reference, not a guess -- `--help` prints the same. **Only one short alias |
| 117 | +per flag** (intentional design -- long-name aliases were removed). |
| 118 | + |
| 119 | +``` |
| 120 | +-h, --help Show help and exit (0) |
| 121 | +-l, --list List configured tests as TSV and exit (0) |
| 122 | + --headless Don't show window; route all logs to stdout |
| 123 | + --break-on-fail Trigger debugger break on a failing test |
| 124 | + --generate-references Save rendered images as new reference PNGs |
| 125 | + --once Run only the first matching test, then exit |
| 126 | + --include-excluded Force-run tests with excludeFromAutomaticTesting / |
| 127 | + onlyVisual / excludedGraphicsApis set in config.json |
| 128 | + --save-results=BOOL Override saving of result PNGs (default true) |
| 129 | + --debug-trace=BOOL Enable/disable Babylon::DebugTrace |
| 130 | + --perf-trace=LEVEL Set Babylon::PerfTrace level (None / Log) |
| 131 | + --capture=N Trigger RenderDoc capture on the Nth rendered frame |
| 132 | + of every executed test. Auto-extends each test's |
| 133 | + render budget so the .rdc finalizes; pixel compare |
| 134 | + still runs on the test's original renderCount, so |
| 135 | + pass/fail is unaffected. Output: <cwd>/temp/ |
| 136 | + bgfx_frame<N>.rdc. Combine with --once / --test / |
| 137 | + --test-index for a single capture. Requires |
| 138 | + renderdoc.dll to be loaded into the process; easiest |
| 139 | + is to launch via `renderdoccmd capture -w` or |
| 140 | + `rdc capture --trigger -w`, both of which inject |
| 141 | + the paired DLL before main. |
| 142 | +-t, --test=PATTERN Run tests whose title contains PATTERN (substring, |
| 143 | + case-insensitive). Repeatable; multiple = OR. |
| 144 | + --test-index=LIST Run only the listed indices. |
| 145 | + LIST: '3' or '3,5,7' or '3-6' or '3,5-7,9'. |
| 146 | + -- End of options; everything after is a script path. |
| 147 | +``` |
| 148 | + |
| 149 | +## Exit codes |
| 150 | + |
| 151 | +| Code | Meaning | |
| 152 | +|---:|---| |
| 153 | +| `0` | success / `--help` / `--list` | |
| 154 | +| `1` | uncaught JS exception | |
| 155 | +| `2` | command-line parse error | |
| 156 | +| `3` | hard crash (assert / SIGABRT / unhandled exception) | |
| 157 | +| `-1` | pixel-diff comparison failure | |
| 158 | + |
| 159 | +PowerShell shows `-1` as `4294967295` after unsigned cast in some contexts. |
| 160 | + |
| 161 | +## Common debugging scenarios |
| 162 | + |
| 163 | +### Run one test by name |
| 164 | +```powershell |
| 165 | +.\Playground.exe --headless --once --test "MultiRenderTarget" ` |
| 166 | + app:///Scripts/validation_native.js |
| 167 | +``` |
| 168 | +Substring match, case-insensitive, against `test.title`. |
| 169 | + |
| 170 | +### Run one test by index (when titles collide or you only have a survey row) |
| 171 | +```powershell |
| 172 | +.\Playground.exe --headless --once --test-index=N ` |
| 173 | + app:///Scripts/validation_native.js |
| 174 | +``` |
| 175 | +Use `--list` to find the index for a title. Indices are not stable across |
| 176 | +config.json edits; always re-resolve when the test list changes. |
| 177 | + |
| 178 | +### Run a quarantined test without editing config.json |
| 179 | +```powershell |
| 180 | +.\Playground.exe --headless --once --include-excluded --test "Decal Map" ` |
| 181 | + app:///Scripts/validation_native.js |
| 182 | +``` |
| 183 | +Bypasses `excludeFromAutomaticTesting`, `onlyVisual`, and |
| 184 | +`excludedGraphicsApis` filters. Use this whenever you need to *debug* a |
| 185 | +quarantined test -- never edit config.json's exclusion flags as part of |
| 186 | +debugging. |
| 187 | + |
| 188 | +### Discover what tests exist |
| 189 | +```powershell |
| 190 | +.\Playground.exe --list |
| 191 | +``` |
| 192 | +Prints TSV with index, title, referenceImage, exclusionReason. Pipe to |
| 193 | +`findstr` / `Select-String` for filtering. |
| 194 | + |
| 195 | +### Pixel-diff investigation |
| 196 | +1. Run the failing test with `--headless --once --include-excluded`. |
| 197 | +2. Stdout will contain `First pixel off at <byteOffset>: Value: (R,G,B) - Expected: (R,G,B)` for the first divergent pixel. |
| 198 | +3. Result PNG is at `<exe-parent>/Results/<referenceImage>` (e.g. |
| 199 | + `Apps/Playground/Results/instancecolors.png`). |
| 200 | +4. Diff overlay (red where pixels differ) is at `<exe-parent>/Errors/<referenceImage>`. |
| 201 | +5. Reference is `<exe-dir>/ReferenceImages/<referenceImage>`. |
| 202 | +6. Compare visually first. If geometry/transforms look right but colors are |
| 203 | + off, the bug is shader/texture/uniform; if geometry is wrong, it's |
| 204 | + vertex/index/transform. Then capture with RenderDoc -- see the dedicated |
| 205 | + recipe. |
| 206 | + |
| 207 | +### Reference image missing |
| 208 | +If the test runner emits a line containing `MISSING_REFERENCE_IMAGE:` then the |
| 209 | +reference asset for that test wasn't usable. Two distinct sub-cases: |
| 210 | + |
| 211 | +``` |
| 212 | +MISSING_REFERENCE_IMAGE: Test 'X' has no 'referenceImage' field in config.json - cannot run pixel comparison. |
| 213 | +MISSING_REFERENCE_IMAGE: Test 'X' failed to load reference at app:///ReferenceImages/<name>.png. <exception> |
| 214 | +``` |
| 215 | + |
| 216 | +The first is a catalog/config error (fixed by editing `config.json`). The |
| 217 | +second is a runtime asset-missing case -- the XHR for the reference PNG fired |
| 218 | +its `error` event (e.g. the file isn't on disk, or the local-file load failed |
| 219 | +otherwise), `BABYLON.Tools.LoadFile`'s `onLoadFileError` ran, the runner |
| 220 | +tagged the message with the grep-able token and `failTest`ed. Exit code is |
| 221 | +**-1** (test failure); the run summary increments a dedicated counter |
| 222 | +(`missingRef=N`). RenderDoc capture is meaningless for these tests -- there |
| 223 | +are zero draws because the runner never reaches `loadPlayground`. The check |
| 224 | +is a no-op for `--generate-references` runs (refs don't exist by design) and |
| 225 | +for `onlyVisual` tests (which don't compare pixels). |
| 226 | + |
| 227 | +### RenderDoc capture (one-liner) |
| 228 | +```powershell |
| 229 | +# Launch under renderdoccmd: it injects the paired renderdoc.dll into the |
| 230 | +# Playground process before main, so bgfx::findModule adopts it. Version |
| 231 | +# always matches what `rdc open` accepts -- no PATH-ordering bugs. |
| 232 | +& "<renderdoc-py-dir>\renderdoccmd.exe" capture -w .\Playground.exe ` |
| 233 | + --headless --once --test "<test title or substring>" --include-excluded --capture=5 ` |
| 234 | + app:///Scripts/validation_native.js |
| 235 | +# .rdc lives at <cwd>\temp\bgfx_frame5.rdc |
| 236 | +``` |
| 237 | +Equivalent with the rdc-cli wrapper (inject-only, no auto-capture handshake): |
| 238 | +```powershell |
| 239 | +& rdc capture --trigger --wait-for-exit -- .\Playground.exe ` |
| 240 | + --headless --once --test "<test title or substring>" --include-excluded --capture=5 ` |
| 241 | + app:///Scripts/validation_native.js |
| 242 | +``` |
| 243 | +Verify `renderdoc.dll` is loaded: |
| 244 | +```powershell |
| 245 | +(Get-Process Playground).Modules | Where-Object ModuleName -eq 'renderdoc.dll' |
| 246 | +``` |
| 247 | +Pixel pass/fail is unchanged by `--capture`. See |
| 248 | +`playground/playground-renderdoc-capture.instructions.md` for the full recipe |
| 249 | +(launcher selection, multiple captures, `rdc` CLI inspection). |
| 250 | + |
| 251 | +### "App crashed but I only see one short line" |
| 252 | +Scroll **up** from the error line. The full `--- CRASH ---` / |
| 253 | +`--- ASSERT ---` / `--- UNCAUGHT JS ERROR ---` banner with both JS and |
| 254 | +native stacks is printed *before* the legacy one-liner. If that banner is |
| 255 | +missing, the process probably terminated through a path that bypasses |
| 256 | +`Diagnostics::Initialize()` (e.g. a static-init crash before |
| 257 | +`InstallCrashHandler()` ran). |
| 258 | + |
| 259 | +### bgfx assert during test (`bgfx.cpp:NNNN: ...`) |
| 260 | +The bgfx callback in `Core/Graphics/Source/BgfxCallback.cpp` mirrors trace |
| 261 | +output to `Diagnostics`'s sink. `BX_ASSERT` failures show up as `--- ASSERT ---` |
| 262 | +banners. The actual assert message is in the second-to-last line of the |
| 263 | +banner body; bx's "STR=" prefix indicates the underlying string passed to |
| 264 | +the assert. |
| 265 | + |
| 266 | +### Config-driven knobs per test |
| 267 | +| Field | Effect | |
| 268 | +|---|---| |
| 269 | +| `excludeFromAutomaticTesting: true` | Skipped unless `--include-excluded` | |
| 270 | +| `onlyVisual: true` | Render but skip pixel comparison (use `--include-excluded` to force pixel compare path; result is still saved) | |
| 271 | +| `excludedGraphicsApis: ["D3D11"]` | Skipped on listed APIs unless `--include-excluded` | |
| 272 | +| `renderCount: N` | Run scene N frames before pixel comparison (default 1) | |
| 273 | +| `capture: true` | Call `TestUtils.captureNextFrame()` once (when `renderCount === 1`) -- triggers bgfx's RenderDoc capture. **Prefer the `--capture=N` CLI flag** for ad-hoc capture; this config knob is kept for legacy tests that hard-pin a per-test capture. | |
| 274 | +| `threshold: N` | Per-channel tolerance for pixel comparison (default 25) | |
| 275 | +| `errorRatio: F` | % of pixels allowed to differ (default 2.5) | |
| 276 | +| `replace: "src,dst,..."` | String-replace pairs applied to the playground source before `eval` | |
| 277 | +| `specificRoot: "..."` | Override `BABYLON.Tools.BaseUrl` for asset loads | |
| 278 | + |
| 279 | +## Adding new diagnostic output -- guidelines |
| 280 | + |
| 281 | +- Use `Diagnostics::DumpFailure` for any *failure* that would otherwise be a |
| 282 | + one-line log entry hard to track down. It gives a searchable banner + |
| 283 | + callstack + build info for free. |
| 284 | +- Use `Babylon::Polyfills::Console::Initialize`'s `debugLog` callback for |
| 285 | + general informational logs (it's already wired to stdout in headless |
| 286 | + mode and OutputDebugString always). |
| 287 | +- For new command-line flags, add them only to `CommandLine.cpp`, follow the |
| 288 | + `match("--long", "-short", FlagKind::...)` pattern, and update `PrintUsage` |
| 289 | + in the same file. **No long-name aliases**: pick one canonical long name. |
| 290 | +- Update this instructions file when adding flags or new diagnostic |
| 291 | + categories so future agents find them. |
| 292 | + |
| 293 | +## Building |
| 294 | + |
| 295 | +The branch builds via the standard CMake setup. After source changes that |
| 296 | +touch `Apps/Playground/Scripts/*.js` or `Apps/Playground/Scripts/config.json`, |
| 297 | +the CMake `Apps/Playground/CMakeLists.txt` copies them to |
| 298 | +`<build>/Apps/Playground/<config>/Scripts/`. For *transient* runtime tweaks |
| 299 | +(e.g. flipping `capture: true` for one debug run), edit the build-dir copy |
| 300 | +directly and revert when done -- that avoids accidental commits to |
| 301 | +`config.json` and avoids a rebuild round-trip. |
0 commit comments