cvcGL wasm: the WebAssembly app contract (state shim, FrameYield, cvcgl_wasm_app, glsync) - #537
Merged
Merged
Conversation
webgl_state_shadow.js is an Emscripten --pre-js that answers the GL state queries made around every frame from a client-side shadow instead of a synchronous round trip to the browser's GPU process. The queries come from cvcGL's own code, so every cvcGL ImGui / Ariadne wasm app makes them: - imgui_impl_opengl3 is compiled into libcvcGL, ImGuiOverlay calls ImGui_ImplOpenGL3_RenderDrawData once per frame, and Ariadne's ImGuiBackend draws through ImGuiOverlay. That backend backs up and restores ACTIVE_TEXTURE, VIEWPORT, SCISSOR_BOX, the six BLEND_* values, five isEnabled flags and isProgram around every draw. - VTK re-reads READ_BUFFER after every read-framebuffer bind and MAX_DRAW_BUFFERS on every framebuffer activation, several times a frame. In a browser several of these block until the GPU process drains its command queue. Firefox does not cache ACTIVE_TEXTURE, so there ImGui's first query of each frame waits for all of the frame's queued GPU work. Every setter that can change a shadowed value is wrapped to keep a per-context copy current, and the matching query is answered from it. Anything the shim cannot prove goes to the real call: a rejected setter, a foreign framebuffer, a deleted program, BLEND_COLOR. The copy is dropped on context loss, and in a worker (OffscreenCanvas / PROXY_TO_PTHREAD) the shim does nothing. It patches the WebGL prototypes, so it serves every context on the page. Modes: on (default), 0/off (nothing installed: the A/B baseline), norb (READ_BUFFER goes to the real call) and verify (answer with the real value, compare it to the shadow, count mismatches). The mode comes from the URL (?glshim=), then the page (Module.glStateShadow), then a build-time default (Module.glStateShadowDefault), then on, so a URL can A/B any page. An unrecognized value still means on, but a console.warn names its source and the accepted values, so a mistyped baseline does not silently measure the shim twice. window.__cvcGlShadow.stats records the version, the mode and where it came from, and the served / verified / mismatch counts. src/cvcGL/test/webgl_state_shadow_test.js runs it under node against a mock WebGL with real GL/WebGL semantics: a seeded fuzz of valid and invalid state changes on WebGL1 and WebGL2, clamped and raw viewports, framebuffers and context loss, plus the mode precedence, the version and the typo warning. ctest registers it later in this series.
VTK 9.5's vtkWebAssemblyOpenGLRenderWindow::Frame(), reached from every
Render(), calls emscripten_sleep(0) when DoubleBuffer is on and the build has
Asyncify. An app whose loop already yields once per frame pays that clamped
trip through the event loop twice. An app could only remove it by calling
SetDoubleBuffer(0) on the render window itself; this makes it a cvcGL
setting:
namespace cvc::gl { enum class FrameYield { Vtk, App }; } // FrameYield.h
SceneRenderer / ViewportManager:
using FrameYield = cvc::gl::FrameYield;
void setFrameYield(FrameYield);
FrameYield frameYield() const; // the effective mode
void lockFrameYield(); // App for the life of the window
bool frameYieldLocked() const;
#define CVC_GL_HAS_FRAME_YIELD 1
#define CVC_GL_HAS_FRAME_YIELD_LOCK 1
Vtk stays the default. App is the app's promise that its loop calls
emscripten_sleep(0) once per frame on every path that renders; cvcGL then
turns the window's DoubleBuffer off. That happens only on wasm with Asyncify
and VTK < 9.6: VTK 9.6 dropped the sleep, and natively or without Asyncify
there is none, so there App is a no-op, DoubleBuffer is left alone and
frameYield() reads Vtk. cvcGL changes DoubleBuffer back only if it turned it
off, and then restores the value it found rather than forcing 1, so Vtk mode
never overrides an app's own setting. Pixel readback is unaffected: writePNG
and frameRGB read the back buffer explicitly.
?frameyield=vtk|app on the page URL overrides the app for an A/B, read once
when the window is created.
Watchdog: a microtask-bumped epoch (EM_JS) can only advance once wasm has
returned to the browser. A vtkCommand::StartEvent observer on the window
counts every Render(), whichever path made it: render(), writePNG /
frameRGB, renderWindow()->Render(), a node's fallback render, the
interactor's resize render. If one window renders 8 times in a single epoch,
the loop broke the App contract: cvcGL logs once and restores VTK's yield, so
the page keeps running instead of freezing. Each trip bumps
globalThis.__cvcglFrameYield.trips for browser checks. With VTK >= 9.6 there
is nothing to restore and it only warns. The observer is removed before the
window can outlive the manager.
The lock: under -sASYNCIFY_IGNORE_INDIRECT=1, VTK's in-render sleep is
reached through the virtual Render() and traps, so in such an app neither
?frameyield=vtk nor a watchdog trip may bring it back. lockFrameYield(), or
Module.cvcglFrameYieldLocked = 1 set before the module starts (read once per
window), makes App one-way: ?frameyield=vtk and setFrameYield(Vtk) are
refused and logged, and the watchdog only reports, with a loud
"cvcGL: ERROR:" line. Natively the lock is only recorded.
The enum lives in its own header and is %included in pycvc_gl.i ahead of
SceneRenderer.h, because SWIG does not follow #include.
New headless test cvcgl_frame_yield pins the native contract: App leaves
DoubleBuffer at 1 and reads back Vtk, the facade forwards, Vtk does not reset
an app's own DoubleBuffer, the lock is one-way and only recorded, a closed
renderer throws, and the requires-detection a consumer writes finds the
methods and falls back on a type without them.
A wasm app that links cvcGL gets the browser-side speedups with
cvcgl_wasm_app(<target> [STATE_SHIM ON|OFF]
[STATE_SHIM_MODE on|verify|norb]
[MIMALLOC AUTO|ON|OFF]
[FRAME_YIELD_LOCKED AUTO|ON|OFF])
defined in src/cvcGL/wasm/cvcGLWasm.cmake, included in-tree by
src/cvcGL/CMakeLists.txt and, for installed consumers, by cvcGLConfig.cmake.
Outside Emscripten it returns at once, so an app can call it unconditionally.
Consumers feature-test it with if(COMMAND cvcgl_wasm_app), and later keywords
with CVCGL_WASM_APP_FEATURES. That variable is directory-scoped, so it is set
on every include, ahead of include_guard(GLOBAL): a second find_package(cvcGL)
from a sibling directory sees it too.
- STATE_SHIM (default ON) links webgl_state_shadow.js as a --pre-js and adds
it to LINK_DEPENDS. A STATE_SHIM_MODE other than `on` generates
<target>_glshim_default.js (Module.glStateShadowDefault) ahead of it.
- MIMALLOC AUTO links -sMALLOC=mimalloc iff cvcGL was built -pthread:
dlmalloc serialises every malloc/free on one lock, which only threads
contend on, and mimalloc is larger and uses more memory. A
-sMALLOC=mimalloc the target already links counts as ON; any other
allocator of its own is kept, with a warning. Under -fsanitize=address
AUTO resolves OFF, because emcc refuses mimalloc with ASan, and an explicit
ON warns.
- FRAME_YIELD_LOCKED links a generated <target>_frameyield_lock.js that sets
Module.cvcglFrameYieldLocked = 1, so every cvcGL window of the app keeps
FrameYield::App for good. AUTO locks exactly when the target's own link
line carries -sASYNCIFY_IGNORE_INDIRECT=1: its LINK_OPTIONS,
LINK_FLAGS[_<CFG>], the -s items given to target_link_libraries, and the
global linker flags. Directory options are not read: a target inherits
them only when it is created, so a later add_link_options is not on its
link line. ON is for a flag the check cannot see (a dependency's
INTERFACE_LINK_OPTIONS, a generator expression).
- With CMake >= 3.19 a deferred check on the final link options resolves
AUTO and warns when the target links -sASYNCIFY_IGNORE_INDIRECT=1, louder
with -sFETCH=1; before 3.19 it runs at the call. That flag is deliberately
not an option: it is only correct after an audit of every sleep the app
can reach.
It is a function, not an INTERFACE --pre-js on cvc::cvcGL. An interface
option would reach every static consumer's link: tests, helper tools, and
pages that never asked for a page-global WebGL patch. It could not carry
-sMALLOC either, because dependency link options come after the target's own
and emcc keeps the last -s value.
Nothing absolute is exported. Two GLOBAL properties tell the function where
the JS is and whether cvcGL is wasm-mt:
- in-tree, the source dir and CVC_WASM_PTHREADS;
- installed, @PACKAGE_CVCGL_WASM_DATADIR@ (configure_package_config_file
PATH_VARS, computed from the installed config's own location) and the
-pthread flag the build had.
cvcGLConfig.cmake reads that path right after its package init, before any
find_dependency: on CMake 3.29 and older every dependency config generated
the same way overwrites PACKAGE_PREFIX_DIR with its own prefix, so a later
read hands out the dependency prefix's share/cvcGL/wasm whenever cvc or SDL3
lives in another prefix than cvcGL.
The JS installs to share/cvcGL/wasm/ and the function to lib/cmake/cvcGL/ on
every platform. The cvcgl and cvcgl-cuda recipes pack share/cvcGL/.
All 9 gallery demos (_wasm_demos) now call cvcgl_wasm_app() inside the
existing link-flags foreach. Each links the WebGL state shim as a --pre-js,
and mimalloc as well on the threaded (wasm-mt) build. Each also calls
view.setFrameYield(SceneRenderer::FrameYield::App), because each loop yields
once per frame (emscripten_sleep(0)) on every path that renders. That takes
VTK 9.5's second, in-render yield off each frame. The call is a no-op
natively.
nav_fog_ghost was the one exception. Its paused path,
if (uiPaused) { view.render(); continue; }, skipped the loop's only yield.
It had only ever worked because VTK yielded inside render(); in App mode,
pausing would freeze the page. That path now flushes the publisher
(non-pthread builds) and yields before its continue.
ariadne_hello becomes browser-capable as the first wasm Ariadne app. On
Emscripten its loop yields (flush + emscripten_sleep(0)) instead of
sleep_for, and it gets the same link flags plus cvcgl_wasm_app, so the state
shim answers the queries its ImGuiBackend -> ImGuiOverlay draw makes. It sets
FrameYield::App right before the interactive loop only: its --png /
--offscreen / --frames capture branch renders back to back without yielding,
so it keeps VTK's in-render yield (and cannot trip the watchdog). It is a
_wasm_extra_demo: it builds only on request (wasm-extra-demos, or by name)
into bin/extra/, which build-pages.py does not scan, so the gallery is
unchanged. nav_convoy and nav_compute load .ari documents from disk and stay
native-only.
glsync.js is a Firefox census of synchronous WebGL calls, the tool that shows whether the state shim removes what it should. It wraps every WebGL2RenderingContext method, sorts each call by what Firefox 156 does with it (a synchronous round trip to the GPU process, answered in the content process, queued, or flushed), and models Firefox's async-present flush budget. Once per report window it prints one GLSYNC line. Without ?glsync in the URL it does nothing but that one check. It lives in src/cvcGL/wasm/devtools/ with GLSYNC.md, its node test and a bench, as a developer tool, not part of the SDK install. - A frame clock option, clock=auto|prof|raf. The cvcGL gallery page sends Module.print to a DOM node and the gallery demos print no frame-timing lines, so the census cannot report only after "PROF n=" console lines. raf reports every every=N paints (default 60) of a drawn canvas; f is the number of JS tasks that drew to the canvas, exactly one per frame with FrameYield::App. prof reports after each PROF line, for an app that prints them. auto, the default, uses raf until the first PROF line and prof after it. - The census adds ?prof to the page URL only on request: with ?glsync=prof, or when the server injects window.__glsyncProfParam. A URL that already has prof, profhud or glcount (other switches with which an app may already print PROF lines), or the requested name, is left alone. - The header and GLSYNC.md describe frames and tasks in terms of FrameYield. src/cvcGL/examples/wasm/serve.py gains the injection, behind flags that are all off by default, so the cvcgl-examples-web launcher is unchanged: - --glsync[=PATH] inserts the census <script> before a page's first <script> and serves /glsync.js from PATH (default: the source tree, or devtools/ next to an installed serve.py); - --prof-param NAME injects window.__glsyncProfParam; - --js-profiling sends Document-Policy: js-profiling, for Chromium's JS Self-Profiling API. "serve.py --glsync 8822" takes 8822 as the port. The cvcgl-examples native bundle installs glsync.js and GLSYNC.md to share/cvcgl-examples/devtools/ beside serve.py. Tests: tests/test_glsync.js drives a synthetic heavy-overlay frame under node against a mock WebGL2 and a fake requestAnimationFrame (flush model, frame clocks, URL handling, the prof parameter, FBO-only tasks); its figures are a harness self-test only. tests/test_serve.py covers serve.py's flags, the default path, the CLI, and that no flags mean no injection and no Document-Policy.
docs/CVCGL_WASM.md documents the contract a cvcGL / Ariadne wasm app works under: - cvcgl_wasm_app() and its keywords; - the state shim, its URL modes and their precedence; - FrameYield: the contract, the URL override, the watchdog and what it counts, the lock for -sASYNCIFY_IGNORE_INDIRECT apps, a feature-detection snippet for consumers that also build against an older cvcGL, and the loop audit of the examples; - mimalloc AUTO and why it is threaded-only; - the -sASYNCIFY_IGNORE_INDIRECT checklist (documented, never a default); - glsync and serve.py's profiling flags; - the tests, and what to check in a browser. The JS tests run only under the Emscripten SDK's node, never one from PATH (hermetic toolchain). ctest registers cvcgl_webgl_state_shadow and cvcgl_glsync (label js) with CMAKE_CROSSCOMPILING_EMULATOR under emcmake, and otherwise with CVCGL_EMSDK_NODE, searched only in $CVC_EMSDK_DIR/node/*/bin and /opt/cvc-wasm/emsdk/node/*/bin (NO_DEFAULT_PATH) or given with -D; it looks again when CVC_EMSDK_DIR changes. Without an emsdk node the tests are not registered, and a status line says so. src/cvcGL/wasm/run-js-tests.sh runs the shim, glsync and serve.py tests without CMake, with the emsdk bundle's node (CVC_EMSDK_DIR, or the fleet's /opt/cvc-wasm/emsdk). ci.yml: a new cvcgl-wasm-js job runs it. It does work only when src/cvcGL/wasm/, the shim test, serve.py or ci.yml changed, decided by a git diff against the PR base or the push's before, and provisions node with `cvcpkg install emsdk` (no apt). publish-cvcgl-wasm.yml: - runs run-js-tests.sh once emsdk is provisioned; - fails the SDK bundle when share/cvcGL/wasm/webgl_state_shadow.js is missing, cvcGLWasm.cmake has no cvcgl_wasm_app(), cvcGLConfig.cmake does not record the -pthread build, or cvcGLConfig / cvcGLTargets / cvcGLWasm.cmake name a path on the runner; - fails the cvcgl-examples gallery when nav_city_drive.js carries no __cvcGlShadow.
bindings/pycvc/wasm/link-host.sh links neither of cvcGL's browser-side speedups. That is right while the host is node-only, but nothing tracked it where the work will happen. Leave a TODO in link-host.sh (docs/CVCGL_WASM.md says the same) that when the host gets a browser page, its link must add --pre-js "$INST/share/cvcGL/wasm/webgl_state_shadow.js" and, on wasm-mt, -sMALLOC=mimalloc: what cvcgl_wasm_app does for a CMake app. It links no -sASYNCIFY, so FrameYield and -sASYNCIFY_IGNORE_INDIRECT do not apply until it does.
src/cvcGL/CMakeLists.txt: keep both test-section blocks -- the JS-test registration first, then master's cvcgl_gl_context RESOURCE_LOCK block -- and list the two node tests (cvcgl_webgl_state_shadow, cvcgl_glsync) as headless so they stay unlocked.
…e wasm app contract Next free revisions above master, the low-memory draw PR (#522: cvcgl 17, cvcgl-cuda 2, cvcgl-examples 16) and the highest published on any platform (cvcgl +cvc.16, cvcgl-examples +cvc.15, cvcgl-cuda none), so the publish jobs ship share/cvcGL/, cvcGLWasm.cmake and the examples' devtools instead of skipping an already-published name+version.
transfix
added a commit
that referenced
this pull request
Oct 2, 2026
Conflicts:
- cvcpkg/recipes/{cvcgl,cvcgl-cuda,cvcgl-examples}/recipe.yaml: #537 took
cvcgl 18, cvcgl-cuda 3 and cvcgl-examples 17 for the wasm app contract, above
the revisions this branch had set aside (17, 2, 16). Master's bump comments
are kept; this branch's revisions move to the next number above master's and
above the highest published on any platform (cvcgl 16, cvcgl-examples 15,
cvcgl-cuda none): cvcgl 19, cvcgl-cuda 4, cvcgl-examples 18. libcvc stays at
17 (master 15, published 16).
- src/cvcGL/CMakeLists.txt: both installs kept -- master's wasm state shim and
this branch's THIRD_PARTY_NOTICES.md. The test region merged clean: master's
JS-test registration and its REMOVE_ITEM additions, one GL-test lock block.
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.
cvcGL in the browser: the WebAssembly app contract
A cvcGL / Ariadne app built with Emscripten pays for things a native build never sees. Two of those costs come from cvcGL's own code, so every cvcGL wasm app has them, and a third comes from the allocator on threaded builds. This branch adds one opt-in fix per cost, a CMake one-liner that applies them, a browser-side profiling tool, and the docs and CI that keep them working. The full contract is in
docs/CVCGL_WASM.md.libcvcGL, drawn throughImGuiOverlayand Ariadne'sImGuiBackend) backs up and restoresACTIVE_TEXTURE,VIEWPORT,SCISSOR_BOX,BLEND_*,isEnabledandisProgramaround every frame, and VTK re-readsREAD_BUFFER/MAX_DRAW_BUFFERS. In a browser several of these block until the GPU process drains its queue.webgl_state_shadow.js, a--pre-jsthat answers them from a client-side shadowcvcgl_wasm_app(<target>)vtkWebAssemblyOpenGLRenderWindow::Frame()callsemscripten_sleep(0)inside every render, so a loop that already yields once per frame pays the trip twice.cvc::gl::FrameYieldview.setFrameYield(SceneRenderer::FrameYield::App)-sMALLOC=mimalloccvcgl_wasm_app(AUTO: wasm-mt only)What the branch adds
src/cvcGL/wasm/webgl_state_shadow.js). Wraps every setter that can change a shadowed value; anything it cannot prove goes to the real call; dropped on context loss; a no-op in a worker. Modeson/0|off/norb/verify, chosen by?glshim=>Module.glStateShadow>Module.glStateShadowDefault>on, so a URL can A/B any page. An unrecognized mode still meanson, with a console warning.window.__cvcGlShadow.statsrecords version, mode, source and served / verified / mismatch counts.FrameYield(inc/cvc/gl/FrameYield.h,SceneRenderer/ViewportManager):setFrameYield,frameYield,lockFrameYield,frameYieldLocked, feature macrosCVC_GL_HAS_FRAME_YIELD(_LOCK).Appturns the window'sDoubleBufferoff (only on wasm with Asyncify and VTK < 9.6; a no-op elsewhere) and restores the value it found.?frameyield=vtk|appoverrides for an A/B. A watchdog (aStartEventobserver plus a microtask epoch) catches a loop that renders 8 times without yielding, logs once and restores VTK's yield so the page keeps running.lockFrameYield()(orModule.cvcglFrameYieldLocked = 1) makesAppone-way for apps linked with-sASYNCIFY_IGNORE_INDIRECT=1, where VTK's in-render sleep is reached through a virtual call and would trap. Wrapped for pycvc_gl.cvcgl_wasm_app()(src/cvcGL/wasm/cvcGLWasm.cmake, installed tolib/cmake/cvcGL/and included bycvcGLConfig.cmake):STATE_SHIM,STATE_SHIM_MODE,MIMALLOC AUTO|ON|OFF,FRAME_YIELD_LOCKED AUTO|ON|OFF. A no-op outside Emscripten.AUTOlock follows-sASYNCIFY_IGNORE_INDIRECT=1on the target's own link line; a deferred lint warns about that flag (louder with-sFETCH=1). No absolute path is exported: the JS directory is computed from the installed config's own location, read before anyfind_dependency. The JS installs toshare/cvcGL/wasm/; the cvcgl and cvcgl-cuda recipes packshare/cvcGL/.cvcgl_wasm_apps withFrameYield::App.nav_fog_ghost's paused path now yields (it used tocontinuepast the loop's only yield).ariadne_hellobuilds for the browser as an extra demo (wasm-extra-demos, not in the gallery), withAppon its interactive loop only.src/cvcGL/wasm/devtools/glsync.js+GLSYNC.md: a Firefox census of synchronous WebGL calls with a model of the async-present flush budget, and arafframe clock for pages without frame-timing console lines.serve.pygains--glsync[=PATH],--prof-param NAMEand--js-profiling, all off by default. The cvcgl-examples bundle installs the census besideserve.py.docs/CVCGL_WASM.md.src/cvcGL/wasm/run-js-tests.shruns the JS tests with the Emscripten SDK's node only. ctest registerscvcgl_webgl_state_shadowandcvcgl_glsync(labeljs) only when an emsdk node is found. Newcvcgl-wasm-jsjob inci.yml(cvcpkg emsdk, no apt; skipped when the browser-side files did not change).publish-cvcgl-wasm.ymlruns the JS tests and fails a bundle without the shim, withoutcvcgl_wasm_app(), without the-pthreadrecord, or with a runner path in its CMake files, and fails the gallery ifnav_city_drive.jslacks the shim.link-host.sh: when it gets a browser page, link the shim and (wasm-mt) mimalloc.How an app opts in
An app linked with
-sASYNCIFY_IGNORE_INDIRECT=1gets theFrameYield::Applock fromcvcgl_wasm_appautomatically (or callslockFrameYield()), and should work through the checklist indocs/CVCGL_WASM.mdfirst. A non-CMake app passes--pre-js <prefix>/share/cvcGL/wasm/webgl_state_shadow.js, plus-sMALLOC=mimallocon wasm-mt.Verification
cmake -S src/cvcGL, Release, ImGui + SDL3) against a native deps prefix, with libcvc built from this branch's base: builds, andctestpasses 51/51, includingcvcgl_frame_yieldand the twojstests (cvcgl_webgl_state_shadow,cvcgl_glsync) under the Emscripten SDK's node 22.16.0. The install hasshare/cvcGL/wasm/webgl_state_shadow.jsandlib/cmake/cvcGL/cvcGLWasm.cmake, and its CMake files name no build-tree path.-DCVC_BUILD_EXAMPLES=ONcompiles and links all 14 native example programs without warnings (ariadne_hello,bunny_shadow,lsystem_coast,lsystem_forest,nav_abi_smoke,nav_city_drive,nav_city_swarm,nav_compute,nav_convoy,nav_finale,nav_fog_ghost,terrain_lab,volren_bunny,volslice_bunny), includingariadne_hello's nativesleep_forloop and thesetFrameYieldcalls. Built, not run.src/cvcGL/wasm/run-js-tests.shwith the emsdk's node: state shim 31/31, glsync 18/18, serve.py 20/20.build-wasm-demo.sh --pthread) at the branch head: all 9 gallery demos build and the gallery is assembled. Every demo links--pre-js .../webgl_state_shadow.jsand-sMALLOC=mimalloc, and every module JS carries__cvcGlShadow.--target wasm-extra-demosbuildsariadne_hellointobin/extra/, with the shim.cvcgl_wasm_app(), docs + CI) were each configured, built and tested natively at their pre-rebase equivalents. The examples commit was built only as part of the full builds at the branch head (native examples and wasm-mt), and the rest are JS, docs or comments.deploy-pages.ymlbuilds it only on a push to master, so the shim link in the single-threaded build, andMIMALLOC AUTOleaving it on dlmalloc, are first exercised after merge.git clang-format-18is clean over the branch and per commit, on the C/C++ pathspec the CI format job checks.FrameYield_Vtk/FrameYield_Appand the four FrameYield methods on bothSceneRendererandViewportManager(wrapper generation only; pycvc itself was not rebuilt for this PR).Follow-ups
cvcpkg/recipes/cvcgl15 -> 18,cvcpkg/recipes/cvcgl-cuda1 -> 3,cvcpkg/recipes/cvcgl-examples3 -> 17 (commit 057ef49). Each is the next free revision above the max of master, the pending fast-draw PR cvcGL: fast-draw low-memory mapper (~2/3 fewer GL calls per frame on WebGL2) + reship cvc.16 #522 (cvcgl 17, cvcgl-cuda 2, cvcgl-examples 16) and the highest published on any platform (linux / macos / windows / wasm / wasm-mt: cvcgl +cvc.16, cvcgl-examples +cvc.15, cvcgl-cuda none), so platforms stay monotonic and the publish jobs ship the newshare/cvcGL/,cvcGLWasm.cmakeand the cvcgl-examples devtools instead of skipping an already-published name+version. libcvc is not bumped (this PR does not change libcvc's sources or ABI). If cvcGL: fast-draw low-memory mapper (~2/3 fewer GL calls per frame on WebGL2) + reship cvc.16 #522 lands after this, it must take the next numbers above these.?glshim=verifywith__cvcGlShadow.stats.mismatches === 0on every gallery demo after exercising menus, panels, camera and resize;?glsync: nogetParameter(ACTIVE_TEXTURE),SCISSOR_BOXorBLEND_*[S]keys with the shim, and they return with&glshim=0;nav_fog_ghostpause / resume inAppmode with__cvcglFrameYield.trips === 0;-sASYNCIFY_IGNORE_INDIRECT) one prints thecvcGL: ERROR:line and never traps;MAXIMUM_MEMORY=4GBwith mimalloc before a publish ships it.link-host.sh).src/cvcGL/CMakeLists.txt's test section, where this branch registers its JS tests. The resolution is to keep both: the JS test registration, then theRESOURCE_LOCK cvcgl_gl_contextblock. Addcvcgl_webgl_state_shadowandcvcgl_glsyncto that block's headlessREMOVE_ITEMlist, because neither opens a GL context.cvcgl_frame_yieldrenders, so it stays locked.