Skip to content

Playground validation: make a full 720-test sweep possible - #1830

Merged
bkaradzic-microsoft merged 4 commits into
BabylonJS:masterfrom
bkaradzic-microsoft:pr/playground-full-sweep
Aug 12, 2026
Merged

Playground validation: make a full 720-test sweep possible#1830
bkaradzic-microsoft merged 4 commits into
BabylonJS:masterfrom
bkaradzic-microsoft:pr/playground-full-sweep

Conversation

@bkaradzic-microsoft

@bkaradzic-microsoft bkaradzic-microsoft commented Aug 12, 2026

Copy link
Copy Markdown
Member

Two changes that together make a complete run of the 720-test validation suite possible and order-independent. Two commits, reviewable separately.

1. --keep-going, so a sweep can finish

The runner exits on the first failing test, so a run only ever reports one failure and everything after it goes unmeasured. There's no way to answer "how many of the 720 tests pass today?" without repeatedly excluding whatever failed and re-running.

--keep-going / -k records the failure, continues, and lists every failing title in the summary. The run still exits non-zero if anything failed, so CI semantics are unchanged. Without the flag, behaviour is exactly as before.

Run complete. ran=3 passed=1 failed=2 missingRef=0 skipped=0
Failed tests (2):
  - Gaussian Splatting Compressed ply SH
  - Gaussian Splatting Update Data

Verified with two tests pointed at a mismatched reference image plus one control:

result exit
without --keep-going ran=1 passed=0 failed=1 -1
with --keep-going ran=3 passed=1 failed=2 -1

Framebuffer pool floor 512 -> 2048. Running to completion for the first time immediately exhausted the bgfx framebuffer pool around the FrameGraph tests (bgfx::createFrameBuffer returned invalid handle). 512 was only ever sufficient because the run stopped early. The existing comment in Dependencies/CMakeLists.txt already anticipated this ("combined with V8 GC pacing this can exceed the default limit during long playground sweeps"); this raises the floor to cover a full run.

2. Cross-test state leaks

The suite reuses one engine across all tests, so anything outliving scene.dispose() carries into later tests. The browser harness never sees this because it reloads the page per test.

  • Stray scenes. A test can leave extra scenes registered on the engine (an async load that created its own scene, or one whose creation promise resolved after validation). They stay registered and keep their resources alive.
  • The engine texture cache. BaseTexture._getFromCache keys on url/noMipmap/isCube only -- not on load-time options. A test that leaves a reference behind (e.g. assigning one texture to both scene.environmentTexture and a material's reflectionTexture) keeps its internal texture cached across scene.dispose(), so a later test loading the same URL silently reuses the previous test's texture along with its prefiltering/irradiance settings.
  • SceneLoader.OnPluginActivatedObservable. Global, outlives the scene. Snippets use it to configure the glTF loader (animationStartMode, compileMaterials, ...) and never unregister, so every later glTF test inherits those settings.

Plus a hang guard: a createScene promise that never resolves currently hangs the entire suite, because the existing onReadyTimeout net lives inside processCurrentScene and only applies once the promise has resolved. This converts it to a single failed test. (Documented caveat: it only fires if the JS event loop keeps running.)

This second commit changes no current result -- 302 ran / 299 passed / 3 failed, identical set, before and after. Its value is removing order-dependence that becomes load-bearing as more tests are enabled.

Why it matters

This is what made a complete 720-test sweep possible at all. Measuring the full suite in one pass turned up 47 failures previously hidden behind the first one -- and spot-checking those in isolation shows they pass individually, so they're cross-test state pollution rather than missing functionality. That's a materially different and far more tractable problem than the tally alone suggested, and none of it is visible without this.

The validation runner exits as soon as a test fails, so a run only ever
reports the first failure and everything after it is left unmeasured. That
makes it impossible to answer "how many of the 720 tests actually pass
today?" without repeatedly excluding whatever failed and re-running.

Add --keep-going (-k): a failing test is recorded and the run continues.
The run still exits non-zero if anything failed, so CI semantics are
unchanged for existing invocations, and the summary now lists every failing
test title instead of just the tally.

Default behaviour is untouched -- without the flag the runner still stops on
the first failure.

Running to completion for the first time immediately exhausted the bgfx
framebuffer pool around the FrameGraph tests, so raise the floor from 512 to
2048. 512 was only ever sufficient because the run stopped early; a complete
uninterrupted sweep keeps allocating well past that point.

Verified with two tests pointed at a mismatched reference image plus one
control:

  without --keep-going:  ran=1 passed=0 failed=1   exit -1
  with    --keep-going:  ran=3 passed=1 failed=2   exit -1
                         (both failing titles listed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
@bkaradzic-microsoft
bkaradzic-microsoft requested review from bghgary and ryantrem and a lite review from Copilot August 12, 2026 18:39
…engine

The suite reuses a single engine across all tests, so anything that outlives
scene.dispose() silently carries into later tests. The browser harness never
sees this because it reloads the page per test. Three such leaks:

1. Stray scenes. A test can leave extra scenes registered on the engine (an
   async load that created its own scene, a scene whose creation promise
   resolved after validation). They keep their resources alive. Dispose any
   scene still registered after the test's own scene is disposed.

2. The engine texture cache. BaseTexture._getFromCache keys on url/noMipmap/
   isCube only -- not on the load-time options. A test that leaves a reference
   behind (e.g. assigning one texture to both scene.environmentTexture and a
   material's reflectionTexture) keeps its internal texture cached across
   scene.dispose(), so a later test loading the same URL reuses the *previous*
   test's texture along with its prefiltering/irradiance settings. Release
   what is left so each test loads its own textures.

3. SceneLoader.OnPluginActivatedObservable. This is global and outlives the
   scene. Snippets use it to configure the glTF loader (animationStartMode,
   compileMaterials, ...) and never unregister, so every later glTF test
   inherits those settings.

Also guard a createScene promise that never resolves. The existing
onReadyTimeout safety net lives inside processCurrentScene and only applies
once the promise has resolved, so a pending createScene promise hangs the
whole suite instead of failing one test.

This is a no-op for the currently enabled set: 302 ran / 299 passed / 3 failed
both before and after. It removes order-dependence that becomes load-bearing
as more tests are enabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
@bkaradzic-microsoft bkaradzic-microsoft changed the title Playground validation: add --keep-going so a full sweep can complete Playground validation: make a full 720-test sweep possible Aug 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a --keep-going (-k) mode to the native Playground validation runner so it can continue executing after failures and report a full pass/fail tally, and adjusts bgfx configuration to support long uninterrupted sweeps without exhausting the framebuffer pool.

Changes:

  • Add --keep-going / -k CLI flag plumbing through PlaygroundOptions into the JS validation runner.
  • Update validation_native.js to accumulate failing test titles and only exit after completing the sweep when --keep-going is enabled.
  • Raise the enforced minimum BGFX_CONFIG_MAX_FRAME_BUFFERS floor from 512 to 2048 to avoid pool exhaustion during long runs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Dependencies/CMakeLists.txt Raises the enforced minimum bgfx framebuffer pool size to handle full validation sweeps.
Apps/Playground/Win32/App.cpp Passes the new KeepGoing option into the JS runtime options object.
Apps/Playground/Shared/CommandLine.h Adds KeepGoing to PlaygroundOptions.
Apps/Playground/Shared/CommandLine.cpp Adds --keep-going / -k flag parsing and help text.
Apps/Playground/Scripts/validation_native.js Implements keep-going behavior and prints a failed-titles summary; exits non-zero if any failures occurred.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Dependencies/CMakeLists.txt
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
Comment thread Apps/Playground/Scripts/validation_native.js
Replaces the manual then/catch + `sceneSettled` latch with `await
Promise.race([...])`. `Promise.race` already provides the settle-once
semantics the latch was hand-rolling, so the three duplicated
`if (sceneSettled) { return; }` guards, the second `clearTimeout` call and
the separate rejection path all collapse into the existing try/catch.

The timer is cleared in a `finally` rather than on each settle path: left
pending it would keep the event loop alive for the full 10 minutes after a
scene that resolved normally.

`eval(pgCode)` still runs before the first `await`, so it keeps executing at
the shallow stack depth that the enclosing `setTimeout(..., 0)` exists to
provide for engines with a small C stack (QuickJS). Also hardens the promise
test to `currentScene && currentScene.then` so a snippet returning null hits
the readable "Failed to evaluate playground snippet" path instead of a
TypeError.

Net 23 insertions / 25 deletions.

Verified on Win32 D3D11 with a full headless sweep on the upstream config:
ran=302 passed=299 failed=3 -- identical to the pre-refactor baseline,
including the same three failing titles (those three are artifacts of the
local build, not upstream).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
@bkaradzic-microsoft
bkaradzic-microsoft enabled auto-merge (squash) August 12, 2026 22:30
@bkaradzic-microsoft
bkaradzic-microsoft merged commit 78f1269 into BabylonJS:master Aug 12, 2026
34 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.

4 participants