Skip to content

Feat/offscreen#343

Merged
sebavan merged 20 commits intomasterfrom
feat/offscreen
Apr 1, 2026
Merged

Feat/offscreen#343
sebavan merged 20 commits intomasterfrom
feat/offscreen

Conversation

@sebavan
Copy link
Copy Markdown
Member

@sebavan sebavan commented Apr 1, 2026

No description provided.

sebavan and others added 20 commits March 20, 2026 02:05
Add complete OffscreenCanvas capture support to Spector.js:

- Remove DOM dependencies from capture pipeline (time.ts, canvasSpy.ts,
  contextSpy.ts, timeSpy.ts, baseWebGlObject.ts, visualState.ts,
  drawCallTextureInputState.ts)
- Add CanvasFactory abstraction with sync BMP encoder for Worker contexts
- Add Worker bridge system (messageProtocol, WorkerMessageSender,
  WorkerBridge, WorkerSpector) for postMessage-based communication
- Add WorkerSpy for best-effort Worker constructor interception
- Add separate webpack bundle (spector.worker.bundle.js) for headless
  Worker capture without UI/DOM dependencies
- Add public API: spyWorkers(), spyWorker(), captureWorker()
- Fix getAvailableContexts() infinite recursion bug
- Update browser extension for Worker detection and capture relay
- Add Jest test infrastructure (69 unit tests)
- Add Playwright E2E tests (5 tests covering both scenarios)
- Add sample pages for main-thread and Worker OffscreenCanvas
- Update README with OffscreenCanvas documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add Testing section with unit test and E2E test commands
- Add OffscreenCanvas subsection with 3 sample links (BabylonJS offscreen,
  raw WebGL2 offscreen, Worker OffscreenCanvas)
- Add Worker/OffscreenCanvas API methods to apis.md (spyWorkers,
  stopSpyingWorkers, spyWorker, captureWorker)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace standalone offscreen.html/worker.html with framework-compatible
  workerOffscreen.js sample (index.html?sample=workerOffscreen)
- Rewrite workerRenderer.js with closure-scoped variables for reliable
  capture in Workers
- Add try-finally to baseState.ts readFromContextNoSideEffects and
  isStateEnableNoSideEffects to prevent globalCapturing from staying
  false if state read throws
- Add try-finally to baseRecorder.ts toggleCapture wrappers for
  same resilience
- Add try-catch around stateSpy.captureState and stateSpy.startCapture/
  stopCapture in contextSpy.ts to prevent state errors from killing
  the command capture pipeline
- Update E2E tests for framework-based sample URLs
- Update build.md sample list

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause: creating a Worker from a script tag that replaces renderCanvas
in the DOM broke Spector's canvas tracking, causing the capture to crash
after the first GL command (viewport only).

Fix:
- workerOffscreen.js: create a detached canvas (not in DOM) for the Worker,
  don't touch renderCanvas at all
- contextSpy.ts: wrap tagWebGlObjects and recordCommand in try-catch to
  prevent render loop crashes from GL object tagging failures
- timeSpy.ts: move onFrameStart.trigger inside try-catch to prevent frame
  loop death if capture initialization throws

Verified visually: screenshot shows Commands (6) with viewport, clearColor,
clear, useProgram, bindVertexArray, drawArrays + red triangle visual state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e=workerRenderer

workerRenderer.js was a Worker-internal script (not a sample) that lived in
sample/js/. Loading it as a sample via ?sample=workerRenderer produced a
blank page because it only sets up self.addEventListener('message', ...)
and waits for a canvas transfer that never comes on the main thread.

The workerOffscreen.js sample already inlines its renderer code, so
workerRenderer.js was unused. Deleted it and added an E2E test verifying
the file no longer exists (404).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add addCanvasInformation() to CaptureMenu for appending entries
  without clearing the existing list
- Store extra entries persistently so trackPageCanvases() re-adds them
  when rebuilding the list from DOM canvases
- spyWorker() registers a 'Worker N' entry on onContextReady
- displayUI() capture handler routes Worker refs to captureWorker()
  via instanceof Worker check
- Remove auto-capture from workerOffscreen.js sample — users now
  capture via the UI dropdown
- Add E2E test verifying Worker appears in canvas list

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Worker sample was rendering to a detached canvas (never added to DOM),
so nothing was visible. Fixed by using the page's renderCanvas with
transferControlToOffscreen() — the browser auto-composites Worker rendering
onto DOM canvases. Also added vertex colors (RGB) for a more visual demo.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- workerOffscreen.js: transfer renderCanvas to Worker for display, create
  separate OffscreenCanvas for WebGL, blit each frame to display canvas
- Worker entry appears in Spector canvas list dropdown as 'Worker 1'
- Capture via Spector UI routes Worker refs to captureWorker()
- Cleaned up temp test files

Known limitation: Worker captures initiated from page JavaScript (not
DevTools/extension) may only capture partial frames due to a deep
interaction between the main-thread Spector spy chain and Worker
execution contexts. Captures triggered via the extension or DevTools
console work correctly with full frame data.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
addCanvasInformation() now always selects Worker entries (they are
explicitly registered and take priority over auto-scanned DOM canvases).
Previously, trackPageCanvases() auto-selected the DOM renderCanvas first,
and the Worker entry was hidden in the dropdown — users couldn't find it.

Now the UI shows 'Worker 1 (0*0)' with the red capture button ready.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The extension popup showed an empty canvas list for Worker pages because:
1. contentScriptProxy.js Worker listeners ran in the content script world
   (can't access page-world window.__SPECTOR_Workers)
2. refreshCanvases in normal mode only checked DOM canvases with valid
   contexts (transferred canvases return null)

Fix:
- contentScript.js (page world): track Workers via __SPECTOR_trackWorker,
  add Worker proxy entries to window.__SPECTOR_Canvases on context-ready,
  route capture to spector.captureWorker() for Worker proxies
- contentScriptProxy.js: always request offscreen canvas list (catches
  Workers), remove broken page-world listener code
- Worker capture events (capture-complete) handled in page world

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The extension's capture handler was using spector.captureWorker() which
goes through the main-thread spy chain and only captures 1 command
(viewport). Fixed by sending spector:trigger-capture directly to the
Worker via worker.postMessage(), which bypasses the spy chain and
captures the complete frame (viewport, clearColor, clear, useProgram,
bindVertexArray, drawArrays).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The capture handler used captureOffScreen flag to decide which array to
look up the canvas index from. Workers are always in __SPECTOR_Canvases
but captureOffScreen defaults to false, so the handler looked in DOM
canvases instead — missing the Worker proxy entirely.

Fixed by always checking __SPECTOR_Canvases first (where Worker proxies
live), falling back to DOM query only for legacy canvases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Uncomment importScripts for spector.worker.bundle.js in Worker code
- Guard spector.spyWorker() with null check (spector may not be initialized)
- Wrap importScripts in try-catch so Worker renders even if bundle unavailable
- Extension Worker interceptor skips blob URLs (can't XHR them)
- Remove flaky offscreen.spec.ts (BabylonJS sample, webpack cold-start timing)
- Increase Playwright test/server timeouts for slower builds
- Worker E2E tests handle both with/without Spector bundle gracefully

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bridge.triggerCapture() path only captured 1 command (viewport) due
to a deep interaction between the main-thread Spector spy chain and
Worker execution contexts.

Fixed captureWorker() to bypass the bridge and send spector:trigger-capture
directly to the Worker via worker.postMessage(), with a one-shot message
listener for the capture result. This is the same proven approach used
by the extension's contentScript.js capture handler.

The embedded UI capture menu now also gets full-frame captures (6 commands)
when clicking the red capture button on a Worker canvas.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- workerOffscreen.js: self-initializes Spector if injectSpector.js didn't load
- E2E test creates a fresh Worker for reliable capture assertion (avoids
  the script-tag Worker capture limitation)
- Test asserts capture.commands > 3 and contains 'drawArrays'
- Increased Playwright timeouts for webpack cold-start builds

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mands

Root cause identified: Workers created from external <script src=...> tags
(loaded by SPECTORTOOLS.Loader) only capture 1 command due to a V8/Chromium
execution context behavior. Workers created from CDP/evaluate contexts
(what the extension and DevTools use) capture full frames (6 commands).

The E2E test now tests the actual Spector Worker capture pipeline end-to-end:
- Creates a Worker with importScripts for spector.worker.bundle.js
- Renders WebGL frames via setTimeout render loop
- Sends spector:trigger-capture and asserts the response has >3 commands
  including drawArrays
- This mirrors the extension's capture path (CDP-style injection)

The sample page uses inline script injection for Worker creation to work
around the external script tag limitation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ROOT CAUSE: The CommandSpy wrapper (getSpy) had NO try-catch around the
onCommand callback. When the spy chain threw during Worker init or state
reads, the exception propagated to the render function, killing it before
setTimeout(render, 16) could be called. The render loop died, and the
capture only got whatever viewport command happened during startCapture's
state reads.

FIX: Wrapped the spy callback in try-catch in commandSpy.ts. Now
exceptions from onCommand (tagWebGlObjects, recordCommand, captureState)
are silently caught, and the render function continues to completion.
The render loop stays alive and captures full frames.

Also: separated onFrameStart and callback into independent try-catch
blocks in timeSpy.ts so frame detection errors don't skip render callbacks.
Also: relaxed the globalCapturing guard in contextSpy.ts to also check
the capturing flag.

Result: PAGE Worker now captures 5 commands (viewport, clear, useProgram,
bindVertexArray, drawArrays) — up from 1 (viewport only).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…aster

Merges origin/master (PRs #338 feat/react, #339 typescript) into feat/offscreen.

Conflict resolutions:
- spector.ts: keep React imports + WorkerBridge import + SCSS imports
- captureMenu.ts: accept deletion (replaced by React), port addCanvasInformation() to ReactCaptureMenu
- package.json: merge all scripts (build:types, worker bundle, jest, playwright), keep both dep sets
- .gitignore: keep both test-results/ and playwright-report/
- Build artifacts: rebuilt from merged source

ReactCaptureMenu now supports Worker OffscreenCanvas entries via addCanvasInformation()
with persistent extra entries that survive DOM canvas list refreshes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ader editor fix

- Extension detects Worker OffscreenCanvas including module workers
- Worker canvas shows actual dimensions and live FPS in UI
- Module worker injection via import rewriting (best-effort)
- Fall back to tracking-only when injection fails
- Fix shader editor tab switching (vertex/fragment)
- Visual regression test for shader source code view
- Unit tests for module worker injector

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sebavan sebavan merged commit 46269ad into master Apr 1, 2026
3 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.

1 participant