Playground validation: make a full 720-test sweep possible - #1830
Merged
bkaradzic-microsoft merged 4 commits intoAug 12, 2026
Merged
Conversation
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
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
Contributor
There was a problem hiding this comment.
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/-kCLI flag plumbing throughPlaygroundOptionsinto the JS validation runner. - Update
validation_native.jsto accumulate failing test titles and only exit after completing the sweep when--keep-goingis enabled. - Raise the enforced minimum
BGFX_CONFIG_MAX_FRAME_BUFFERSfloor 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.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60c2ec68-6de1-445d-9fc9-b699db737eae
ryantrem
approved these changes
Aug 12, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 finishThe 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/-krecords 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.Verified with two tests pointed at a mismatched reference image plus one control:
--keep-goingran=1 passed=0 failed=1--keep-goingran=3 passed=1 failed=2Framebuffer 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 inDependencies/CMakeLists.txtalready 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.BaseTexture._getFromCachekeys onurl/noMipmap/isCubeonly -- not on load-time options. A test that leaves a reference behind (e.g. assigning one texture to bothscene.environmentTextureand a material'sreflectionTexture) keeps its internal texture cached acrossscene.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
createScenepromise that never resolves currently hangs the entire suite, because the existingonReadyTimeoutnet lives insideprocessCurrentSceneand 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.