Shared lazy synchronization: real-game measurements

Current default (2026-09-30)

Lazy synchronization is now enabled by default for guest workers using the shared WebGL renderer. Software, cooperative execution and private executors retain eager synchronization. This rollout supersedes the conservative opt-in decision recorded below; the retained-GDI-pointer limitation is accepted and remains documented, rather than blocking the supported path's default.

Uncheck Lazy sync in the debug toolbar to disable it for running apps and future launches. ?no-lazy-sync starts with it disabled. Existing guest threads receive the live change; threads starting during a change reconcile the setting before execution, and future threads inherit it. Disabling uses the existing fenced setter, so a pending surface is synchronized before the worker returns. Retained GDI/native pointers can still bypass first-touch tracking and produce stale or overwritten pixels; use the opt-out for those applications.

tools/bench-lazy-games.js --shipped-default exercises the real startup setting without forcing the WASM export. Add --no-lazy-sync to test the URL opt-out. The original forced OFF/ON benchmark modes remain available.

Rollout validation: the browser selector regression passes for default-on, URL opt-out, live disable/re-enable and software remaining eager. The thread lifecycle test checks opt-out during spawn, inheritance by later threads and private-executor fallback. Hardware game smoke runs using --shipped-default (no forced export setting) reach MW3's cockpit and GTA2 gameplay with one readback/frame; MW3 with --no-lazy-sync arms no lazy Locks and restores three readbacks/frame. All three runs report zero GPU errors. These 10-second windows validate the setting, not an FPS improvement. Artifacts: build/lazy-games/default-{mw3,gta2_demo,mw3-optout}/.

2026-09-29 session, shared synchronization commit 1730589d. All runs use one frozen runtime and host source set in /home/user/mw3-watch-ab on box8, Ryzen 9950X with hardware ANGLE / Intel UHD 620. The frozen renderer includes the concurrent bounded-readback changes. This compares lazy OFF/ON on that same renderer, not bounded versus full readback. Artifact hashes are in every result.json; each game's runtime and recorded source hashes match across all arms.

Method

tools/bench-lazy-games.js launches a fresh headful browser for each arm and uses the actual shared WebGL renderer. Order: OFF, ON, ON, OFF. Each launch has two 15-second measurement windows. No screenshots, profile sampling or browser-stat polling occur inside those windows. Captures surround them.

Instrumentation wraps every guest worker's native dx_trace: MW3 records presents, GTA2 records flips. Epoch-based timestamps allow all worker records to be inspected together. The harness records persistent thread events from before launch and rejects timing windows with thread creation/exit or more than one active presentation stream; it does not reject multiple idle/audio guest threads. CPU-only peers still contribute lazy-touch counters. Startup thread events remain in the report even when those workers have exited. The added validity checks were also applied offline to the completed MW3 arms.

Example (repeat with fresh output directories in OFF/ON/ON/OFF order):

DISPLAY=:0 CHROME=/usr/bin/google-chrome node tools/bench-lazy-games.js \
  --app=mw3 --seconds=15 --samples=2 --out=build/lazy-games/mw3-off1
DISPLAY=:0 CHROME=/usr/bin/google-chrome node tools/bench-lazy-games.js \
  --app=mw3 --lazy-sync --seconds=15 --samples=2 --out=build/lazy-games/mw3-on1

Independent launches do not simulate identical geometry. Treat throughput as observed behavior, not an isolated percentage speedup. Readback and untouched-Lock counts provide the direct evidence of the optimization. waitMs is guest time waiting on the render transport; syncMs includes readPixels and conversion on the renderer. These overlap and must not be added.

MechWarrior 3 cockpit

All four launches reached the verified cockpit. Reviewed OFF/ON captures retain the terrain, sky, cockpit and HUD. No reported GPU errors occurred. One helper thread was created and exited during startup in each launch; only the main guest worker remained during measurement. This is evidence that short-lived helpers occur in the corpus, not evidence of another thread accessing a pending surface.

Metric Lazy OFF Lazy ON
Aggregate FPS 21.29 22.64
Individual 15-second windows 20.58–22.22 22.44–22.96
Frame time p50 46.03 ms 43.13 ms
Frame time p95 56.01 ms 52.45 ms
Frame time p99 62.34 ms 62.71 ms
Renderer transport wait/frame 15.38 ms 13.25 ms
GPU readbacks/frame 3.016 1.011
Readback/conversion time/frame 9.09 ms 5.77 ms
Lazy Locks armed/frame 0 4.002
Untouched Locks/frame 0 2.000
Triangles/frame 2,122 1,906

Each mode has about 60 measured seconds; OFF has 1,279 frames and ON 1,360. No measured interval exceeded 100 ms. The ON runs submitted roughly 10% fewer triangles, so the observed ~6% FPS increase cannot all be attributed to lazy synchronization. The reduction from three readbacks to one and two untouched Locks per frame is consistent across both ON launches.

Artifacts: build/lazy-games/mw3-{off1,on1,on2,off2}/, with the same paths under the remote test directory. Reports include raw per-worker timestamps, thread histories, counters, CPU time, load, exact source hashes and captures.

GTA2 gameplay with a live helper

All four launches reached the playfield. Reviewed OFF/ON captures retain the player and HUD, with no visible corruption or reported GPU errors. Two guest workers remained live throughout every measurement window. The helper recorded no presentation events or lazy surface touches; this exercises lazy sync with a live helper, but foreign-thread pixel access remains covered synthetically.

Metric Lazy OFF Lazy ON
Aggregate FPS 29.86 29.79
Individual 15-second windows 29.82–29.91 29.63–29.85
Frame time p50 33.31 ms 33.52 ms
Frame time p95 41.15 ms 41.02 ms
Frame time p99 47.16 ms 47.62 ms
Renderer transport wait/frame 6.80 ms 4.26 ms
GPU readbacks/frame 1.002 1.001
Readback/conversion time/frame 3.37 ms 5.70 ms
GPU backend fence calls/frame 1.002 2.001
Lazy Locks armed/frame 0 1.001
Untouched Locks/frame 0 1.001
Triangles/frame 245.34 245.35

Each mode has about 60 measured seconds; OFF has 1,797 frames and ON 1,793. No measured interval exceeded 100 ms. The game leaves the armed Lock untouched, but presentation still requires one readback per frame. Lazy mode adds a GPU backend fence call, not a second guest fence request (see the follow-up below); the lower guest transport wait is not evidence of lower total work. Renderer process CPU time/frame was 30.23 → 32.94 ms, and GPU process CPU time/frame was 53.92 → 55.62 ms. These are process CPU times, can overlap across cores, and must not be treated as a wall-clock frame breakdown.

Reproduce with --app=gta2_demo and the same OFF/ON/ON/OFF sequence above. Artifacts: build/lazy-games/gta2-{off1,on1,on2,off2}/; aggregate values for both games are in build/lazy-games/summary.json.

Initial default decision (before rollout)

The initial decision was to keep global lazy synchronization opt-in. MW3 consistently avoids two readbacks per frame and is a candidate for app-specific enablement. GTA2 has unchanged readback count and essentially unchanged FPS, with an additional backend fence call and higher observed process CPU cost. These measurements do not justify enabling the optimization globally.

Publication waits require cross-thread ordering and must not be removed based on the backend fence counter alone. Retained GDI/native pointers remain the documented limitation; these game captures are visual checks, not byte-exact pixel oracles.

Fence attribution and scoped-barrier correction

A follow-up --trace-fences GTA2 diagnostic recorded 91 frames, 91 publication calls (0x20007) and 91 guest fence calls (0x20001). The merged snapshot's fences field was the GPU backend's counter, overwriting the encoder field. The queued flip calls the backend fence to materialize its pixels before swapping DIBs; the subsequent guest request calls the backend again, with no additional readback. The earlier description of two transport fences was incorrect. transportFences, publications, and publicationWaitMs now keep these costs separate. --trace-fences collects diagnostic call stacks and must not be used for performance comparisons.

The same inspection found a separate redundant guest barrier: when a shared lazy fence synchronizes the requested span but returns 2 because another target is still dirty, d3dim_surface_fence issued the identical request again. It now reuses the completed scoped barrier while retaining pending state for other targets. Software and pending-presentation paths keep their full barriers.

The focused two-target regression fails on the previous implementation with two calls instead of one and passes after the change. It covers both the owner and a foreign instance, plus software fallback; the existing real-Worker contention/read/write tests also pass. Shared-render transport tests verify that publication and explicit-fence counts remain distinct.

Matched before/after validation

Both arms enable lazy sync. Eight launches use BEFORE/AFTER/AFTER/BEFORE per game, two 15-second windows per launch (60 seconds per variant per game). Both artifacts were rebuilt from the same frozen remote sources, differing only in the scoped-barrier correction; host-source hashes match across all arms. The rebuilt baseline is not byte-identical to the older runtime used in the OFF/ON experiment above, so compare only within this new experiment.

Metric MW3 before MW3 after GTA2 before GTA2 after
FPS 22.41 22.97 29.76 29.84
Frame time p95 55.55 ms 53.74 ms 41.50 ms 40.91 ms
Transport fences/frame 3.997 3.004 1.001 1.000
GPU backend fence calls/frame 3.996 3.005 2.001 2.000
GPU readbacks/frame 0.999 1.002 1.001 1.000
Publications/frame 3.997 4.005 1.000 1.000
Publication wait/frame 1.00 ms 1.09 ms 0.64 ms 0.86 ms
Total transport wait/frame 13.51 ms 13.20 ms 4.15 ms 4.86 ms
Triangles/frame 2,162 2,029 245.51 245.04

MW3 avoids one redundant guest fence per frame. GTA2 does not exercise that duplicate, and its request/readback counts stay unchanged. Publication waits are included in total transport wait, not additive. No FPS improvement is claimed: MW3's geometry differs, its windows span 20.18–24.68 FPS before and 21.76–24.64 after, and sampled host load reached 4.45 (above the usual quiet benchmark threshold of 4). The request-count reduction is the direct result.

All eight launches reported zero GPU errors and no measured frame interval over 100 ms. Reviewed before/after captures retain the cockpit or playfield and HUD. GTA2 has two live guest workers in every window; MW3 has one.

Hardware synthetic validation: the complete 64-arm run passes 58 arms and fails the six lazy arms for the three documented retained-GDI-pointer cases (gdi-retained, gdi-retained-write, gdi-blit-retained). The strict run of the 13 supported cases passes 52/52, including real-Worker read/write, write-only, thread lifetime, x87 overlap, backing replacement and release. These limitations are still present; that measurement preceded the default rollout described at the top of this report.

Artifacts: build/lazy-games/fence-{gta2_demo,mw3}-{before1,after1,after2,before2}/, fence-summary.json, gta2-fence-trace/, fence-synthetic/ and fence-synthetic-supported/. Remote artifact binaries are build/lazy-fence-{before,after}.wasm; canonical build artifacts were not replaced. SHA-256 prefixes: before 357ba0bf44f63b23, after 2c3477ace32507c4.