Skip to content

cvcGL wasm: the WebAssembly app contract (state shim, FrameYield, cvcgl_wasm_app, glsync) - #537

Merged
transfix merged 10 commits into
masterfrom
feat/cvcgl-wasm-app
Oct 2, 2026
Merged

transfix merged 10 commits into
masterfrom
feat/cvcgl-wasm-app

Conversation

@transfix

@transfix transfix commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

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.

Cost Fix How an app opts in
Synchronous GL state queries: ImGui's OpenGL3 backend (compiled into libcvcGL, drawn through ImGuiOverlay and Ariadne's ImGuiBackend) backs up and restores ACTIVE_TEXTURE, VIEWPORT, SCISSOR_BOX, BLEND_*, isEnabled and isProgram around every frame, and VTK re-reads READ_BUFFER / MAX_DRAW_BUFFERS. In a browser several of these block until the GPU process drains its queue. webgl_state_shadow.js, a --pre-js that answers them from a client-side shadow cvcgl_wasm_app(<target>)
A second yield per frame: VTK 9.5's vtkWebAssemblyOpenGLRenderWindow::Frame() calls emscripten_sleep(0) inside every render, so a loop that already yields once per frame pays the trip twice. cvc::gl::FrameYield view.setFrameYield(SceneRenderer::FrameYield::App)
dlmalloc's global lock, contended by threads on wasm-mt -sMALLOC=mimalloc cvcgl_wasm_app (AUTO: wasm-mt only)

What the branch adds

  • WebGL state shim (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. Modes on / 0|off / norb / verify, chosen by ?glshim= > Module.glStateShadow > Module.glStateShadowDefault > on, so a URL can A/B any page. An unrecognized mode still means on, with a console warning. window.__cvcGlShadow.stats records version, mode, source and served / verified / mismatch counts.
  • FrameYield (inc/cvc/gl/FrameYield.h, SceneRenderer / ViewportManager): setFrameYield, frameYield, lockFrameYield, frameYieldLocked, feature macros CVC_GL_HAS_FRAME_YIELD(_LOCK). App turns the window's DoubleBuffer off (only on wasm with Asyncify and VTK < 9.6; a no-op elsewhere) and restores the value it found. ?frameyield=vtk|app overrides for an A/B. A watchdog (a StartEvent observer 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() (or Module.cvcglFrameYieldLocked = 1) makes App one-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 to lib/cmake/cvcGL/ and included by cvcGLConfig.cmake): STATE_SHIM, STATE_SHIM_MODE, MIMALLOC AUTO|ON|OFF, FRAME_YIELD_LOCKED AUTO|ON|OFF. A no-op outside Emscripten. AUTO lock follows -sASYNCIFY_IGNORE_INDIRECT=1 on 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 any find_dependency. The JS installs to share/cvcGL/wasm/; the cvcgl and cvcgl-cuda recipes pack share/cvcGL/.
  • Examples. All 9 gallery demos are cvcgl_wasm_apps with FrameYield::App. nav_fog_ghost's paused path now yields (it used to continue past the loop's only yield). ariadne_hello builds for the browser as an extra demo (wasm-extra-demos, not in the gallery), with App on its interactive loop only.
  • Devtools. 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 a raf frame clock for pages without frame-timing console lines. serve.py gains --glsync[=PATH], --prof-param NAME and --js-profiling, all off by default. The cvcgl-examples bundle installs the census beside serve.py.
  • Docs + CI. docs/CVCGL_WASM.md. src/cvcGL/wasm/run-js-tests.sh runs the JS tests with the Emscripten SDK's node only. ctest registers cvcgl_webgl_state_shadow and cvcgl_glsync (label js) only when an emsdk node is found. New cvcgl-wasm-js job in ci.yml (cvcpkg emsdk, no apt; skipped when the browser-side files did not change). publish-cvcgl-wasm.yml runs the JS tests and fails a bundle without the shim, without cvcgl_wasm_app(), without the -pthread record, or with a runner path in its CMake files, and fails the gallery if nav_city_drive.js lacks the shim.
  • pycvc wasm host. A TODO in link-host.sh: when it gets a browser page, link the shim and (wasm-mt) mimalloc.

How an app opts in

find_package(cvcGL CONFIG REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE cvc::cvcGL)
if(COMMAND cvcgl_wasm_app)   # older cvcGL bundles do not ship it
  cvcgl_wasm_app(my_app)     # no-op outside Emscripten
endif()
cvc::gl::SceneRenderer view(sg, w, h, /*offscreen=*/false, "main");
view.setFrameYield(cvc::gl::SceneRenderer::FrameYield::App); // no-op natively
while (!view.windowClosed()) {
  view.render();
#ifdef __EMSCRIPTEN__
  emscripten_sleep(0); // the ONE yield per frame, on every path that renders
#endif
}

An app linked with -sASYNCIFY_IGNORE_INDIRECT=1 gets the FrameYield::App lock from cvcgl_wasm_app automatically (or calls lockFrameYield()), and should work through the checklist in docs/CVCGL_WASM.md first. A non-CMake app passes --pre-js <prefix>/share/cvcGL/wasm/webgl_state_shadow.js, plus -sMALLOC=mimalloc on wasm-mt.

Verification

  • Native, standalone cvcGL at the branch head (cmake -S src/cvcGL, Release, ImGui + SDL3) against a native deps prefix, with libcvc built from this branch's base: builds, and ctest passes 51/51, including cvcgl_frame_yield and the two js tests (cvcgl_webgl_state_shadow, cvcgl_glsync) under the Emscripten SDK's node 22.16.0. The install has share/cvcGL/wasm/webgl_state_shadow.js and lib/cmake/cvcGL/cvcGLWasm.cmake, and its CMake files name no build-tree path.
  • Native examples: the same build reconfigured with -DCVC_BUILD_EXAMPLES=ON compiles 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), including ariadne_hello's native sleep_for loop and the setFrameYield calls. Built, not run.
  • src/cvcGL/wasm/run-js-tests.sh with the emsdk's node: state shim 31/31, glsync 18/18, serve.py 20/20.
  • wasm-mt (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.js and -sMALLOC=mimalloc, and every module JS carries __cvcGlShadow. --target wasm-extra-demos builds ariadne_hello into bin/extra/, with the shim.
  • Intermediate commits: the three commits that change the cvcGL library's C++/CMake (FrameYield, 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.
  • Not run locally: the single-threaded gh-pages gallery. deploy-pages.yml builds it only on a push to master, so the shim link in the single-threaded build, and MIMALLOC AUTO leaving it on dlmalloc, are first exercised after merge.
  • git clang-format-18 is clean over the branch and per commit, on the C/C++ pathspec the CI format job checks.
  • SWIG: the cvcpkg SWIG 4.4.1 generates the pycvc_gl wrapper with FrameYield_Vtk / FrameYield_App and the four FrameYield methods on both SceneRenderer and ViewportManager (wrapper generation only; pycvc itself was not rebuilt for this PR).

Follow-ups

  • Recipe revision bumps: now in this PR. cvcpkg/recipes/cvcgl 15 -> 18, cvcpkg/recipes/cvcgl-cuda 1 -> 3, cvcpkg/recipes/cvcgl-examples 3 -> 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 new share/cvcGL/, cvcGLWasm.cmake and 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.
  • Browser gates still open (Firefox and Chromium):
    • ?glshim=verify with __cvcGlShadow.stats.mismatches === 0 on every gallery demo after exercising menus, panels, camera and resize;
    • Firefox ?glsync: no getParameter(ACTIVE_TEXTURE), SCISSOR_BOX or BLEND_* [S] keys with the shim, and they return with &glshim=0;
    • nav_fog_ghost pause / resume in App mode with __cvcglFrameYield.trips === 0;
    • a deliberately non-yielding debug build trips the watchdog once and keeps running; a locked (-sASYNCIFY_IGNORE_INDIRECT) one prints the cvcGL: ERROR: line and never traps;
    • the Austin nav demos for a few minutes at MAXIMUM_MEMORY=4GB with mimalloc before a publish ships it.
  • pycvc_gl wasm host: link the shim and mimalloc when it gets a browser page (TODO in link-host.sh).
  • Merging with the open cvcGL test PRs (the fast-draw mapper and the shadow-casters macOS fix). Each adds a block at the end of 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 the RESOURCE_LOCK cvcgl_gl_context block. Add cvcgl_webgl_state_shadow and cvcgl_glsync to that block's headless REMOVE_ITEM list, because neither opens a GL context. cvcgl_frame_yield renders, so it stays locked.

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
transfix merged commit d0b9e05 into master Oct 2, 2026
14 checks passed
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.
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.

1 participant