Skip to content

docs: plan multi-output windows for LED sphere + roadmap#205

Open
Hackshaven wants to merge 8 commits into
mainfrom
claude/multi-monitor-output-feature-CeQo2
Open

docs: plan multi-output windows for LED sphere + roadmap#205
Hackshaven wants to merge 8 commits into
mainfrom
claude/multi-monitor-output-feature-CeQo2

Conversation

@Hackshaven

@Hackshaven Hackshaven commented Jun 16, 2026

Copy link
Copy Markdown
Member

Summary

Adds docs/MULTI_MONITOR_PLAN.md — a design doc for spawning
secondary Tauri webview windows whose sole job is to render a
projection-correct view of the same globe state the operator
is driving in the control window. Primary v1 target is an SOS
LED sphere fed via HDMI from a non-primary monitor; the same
architecture extends to fisheye domes, multi-projector
edge-blended arrays, and presenter "mirror this onto the wall"
modes in Phases 2–5.

This PR is the plan only — no code yet. It establishes the
constraints, the architecture, the delivery ladder, the
acceptance smoke checklist, and closes the open questions so
that the implementation PRs that follow can each be small,
reviewable, and independently revertable.

Key architectural decisions

  • Parallel Three.js renderer per output, not capturing the
    control window. MapLibre's globe is Mercator-derived and only
    shows one hemisphere at a time — capture-and-warp would leave
    the far half of the LED sphere as undefined garbage. Each
    output composites a fresh photorealEarth.ts scene + dataset
    overlay + layer stack and renders to an equirectangular
    framebuffer via a single fragment shader (one ray-march per
    output pixel from a configurable camera offset).
  • Operator camera tracking + sphere-split are v1 behavior,
    not Phase 2 forward-compat — matches existing SOS LED sphere
    conventions. Zooming on the control window concentrates pixels
    around the area of focus on the LED sphere; split mode mirrors
    the AOI to the antipodal hemisphere so visitors on either side
    see the same content.
  • Borderless fullscreen everywhere that needs capture-clean
    output.
    Output windows always launch decorationless + full
    screen; control window gains a Tools toggle, an F11 shortcut,
    and a --kiosk / TERRAVIZ_KIOSK=1 launch flag for
    unattended installations.
  • Three-region playback sync algorithm (tolerance / soft
    playbackRate nudge / hard seek) with hysteresis, so HLS
    decoders across windows stay within ~200 ms without visible
    drift correction.
  • Six designed-for failure modes: output crash, HLS error,
    IPC silence, monitor unplug, GPU context loss, manager crash
    with outputs alive. Bounded auto-recovery (3 retries, 30–60 s
    timeouts), with the LED-sphere audience never seeing
    control-side failures.
  • Narrow output capability: full JSON enumeration of
    capabilities/output.json — events + minimal window controls
    • https://* HTTP only, with localhost explicitly denied. No
      core:default, no filesystem, no asset protocol, no Tauri
      command invoke, no window creation. Includes a 10-item
      PR-time security-review checklist.
  • Phase 2 borders committed to Three.js LineSegments on a
    sphere shell (vector approach) over pre-rendered raster
    overlays.
  • Calibration tooling ships in v1: a procedural test-pattern
    pseudo-dataset (~80 LOC GLSL, no network) and a per-output
    rotation offset for installations whose sphere is mechanically
    rotated relative to canonical 0° prime meridian.
  • Telemetry decided. Three new Tier A events on the control
    window (output_added, output_removed, output_failure)
    plus a perf_sample extension. Output windows themselves
    emit nothing — matches capture-clean policy.

Delivery ladder (14 commits, scoped for git bisect)

  1. Scaffold + protocol types
  2. Equirect RTT shader + visual fixture
  3. Output window entry + Three.js scene scaffold
  4. Layer stack + dataset overlay
  5. Narrow Tauri capability for output-* windows
  6. State aggregator + protocol implementation
  7. Emit dataset:loaded + layer events from main.ts
  8. Wire MultiOutputManager into boot
  9. Tools → Outputs panel (first user-reachable)
  10. Persist + restore outputs across launches
  11. Per-output debug overlay + framebuffer resolution picker
  12. Fullscreen + kiosk launch + F11
  13. Failure recovery (crashes, stalls, GPU loss, monitor unplug)
  14. Calibration tooling (test pattern + rotation offset)

Each commit type-checks, builds both dist/index.html and
dist/output.html, and reverts cleanly without breaking the
prior commit's coverage.

Completeness audit (addressed in this PR)

# Item Where
1 Playback drift-correction algorithm spelled out §3 "Playback sync algorithm"
2 Failure recovery for all six modes §3 "Failure recovery"
3 Output capability JSON enumeration §3 "Output capability spec"
4 Smoke-test checklist for commits 9–14 Appendix B
5 Calibration tooling §3 "Calibration tooling", commit 14
6 Tour engine integration (closed Open Q 6) §3 "Tour engine interaction"
7 VR/AR coexistence §3 "VR / AR coexistence"
8 Telemetry decision (closed Open Q 3) Open Questions §3
11 Stale §2.4 cross-reference fixed §3 per-state-change flow

Deferred to follow-up: operator-facing setup guide (separate
doc), auto-updater behavior with outputs open (one-liner),
CLAUDE.md module-map row additions (lands with the first code
commit per the module-map coverage rule).

Test plan

  • Plan reviewers walk Appendix B (smoke-test checklist)
    mentally against each architectural decision and confirm
    each step is executable as written.
  • Security review of the §3 "Output capability spec" using
    the embedded 10-item PR-time checklist.
  • Confirm the "must verify on Linux compositor X" items in
    Open Questions §1 (now cross-linked to the monitor-unplug
    failure case) are tractable on the planned install
    hardware.
  • Analytics reviewer checks the new output_added /
    output_removed / output_failure events + perf_sample
    extension against docs/ANALYTICS_CONTRIBUTING.md's
    reviewer checklist (none require hashing or sanitization;
    tier choice is Essential).

No code in this PR — no type-check, test, build to run
yet. Implementation PRs will follow the ladder above.

https://claude.ai/code/session_01WE6wQwDKWg2LLy8myuq88i

claude added 8 commits May 8, 2026 04:27
Draft plan for driving secondary fullscreen output windows
on adjacent monitors, targeting Science On a Sphere LED
globes as v1 (planetarium domes, multi-projector arrays,
and presenter / mirrored modes are designed-for-but-deferred).

Architecture: a parallel headless Three.js scene in each
output window mirrors the control window's globe state and
renders directly to a 2:1 equirectangular framebuffer via a
single fragment-shader pass. Reuses photorealEarth.ts (already
shared between VR and the Orbit character page) plus a
multi-layer shell stack for stacked datasets. Forward-compat
with off-center camera "zoom" (Phase 2 dome / Phase 4
presenter) via a shader uniform pinned to vec3(0) in v1.

Desktop only (Tauri WebviewWindow). Web fallback via
window.open + BroadcastChannel is a Phase 5 concern.

No code lands here.

Signed-off-by: Claude <noreply@anthropic.com>
Tightens the Phase 2 borders paragraph to commit to a
Three.js LineSegments mesh on a sphere shell, and notes
the rejected raster-overlay alternative with reasoning
(no zoom-aware thinning, no dynamic styling, no per-dataset
highlight). The line-geometry approach is the natural
extension point for those features when they're needed.

Signed-off-by: Claude <noreply@anthropic.com>
Corrects a wrong premise: an SOS LED sphere is not held to be a
strict 1:1 surface representation of Earth. Operator zoom is
expected behavior — visitors read the area of interest filling
more of the physical sphere as "the operator zoomed in," not as
broken geography. Existing SOS installations have done this for
over a decade.

Concretely:

- §3.5 renamed and rewritten. Was framed as "forward-compat,
  shader uniform exists but pinned to (0,0,0) in v1." Now
  framed as the primary v1 mode for SOS LED sphere outputs:
  manager translates the operator's MapLibre camera into a
  cameraOffset and broadcasts it. Same off-center-camera math,
  but as the default rather than as a future hook.

- Adds split mode. Existing SOS spheres expose a toggle that
  mirrors the area of focus to the antipodal hemisphere of the
  physical sphere so visitors on either side see the same
  feature. Modeled as a `uniform bool uSplit` and a `view.split`
  protocol field with a per-output toggle in the Outputs panel.

- §3 'what gets mirrored' updated. cameraOffset row no longer
  says "always (0,0,0)"; new view.split row added. The "Not
  mirrored: operator camera" paragraph is replaced by a
  paragraph explaining that v1 tracks the operator's MapLibre
  camera by default for SOS outputs, and an operator who wants
  the strict 1:1 idle display can flip "Track operator camera"
  off.

- Per-mode defaults table reframed: SOS LED sphere has
  tracking on by default + split available; dome has tracking
  on (Phase 2 adds smoothing); presenter/mirrored mode keeps
  tracking always on but pins split off (a flat-screen audience
  would be confused by the antipodal mirror).

- §7 Phase 2 reframed. The "expose camera tracking as a
  per-output toggle" framing was wrong — v1 already does that.
  Phase 2's add over v1 is just a smoothing filter on the
  broadcast cameraOffset (the LED sphere's physical surface and
  visitor distance hide jitter; a flat dome doesn't).

- §10 limitations: removed the "operator camera not mirrored"
  bullet (it IS mirrored). Replaced with a Phase 2 polish item
  about smoothing.

- Persistence schema gains trackOperatorCamera (default true)
  and split (default false) per output.

- Outputs panel description in the module map mentions both
  toggles.

- mapRenderer.ts gains a debounced camera:moved event in the
  modified-modules table — the manager subscribes to derive
  cameraOffset.

- equirectRtt.ts module description updated: explicitly the
  v1 ray-march from cameraOffset, with uSplit support.

Signed-off-by: Claude <noreply@anthropic.com>
The application title bar and window border leak into any
signal that captures a monitor as input — common when the
operator's machine drives an SOS sphere, projector, or LED
wall over an HDMI capture card. The output windows already
launched fullscreen + decorationless (§3 boot flow step 3),
but the control window had no equivalent and there was no
unattended-launch path.

Adds §3.6 covering four mechanisms that ship in v1:

- Output windows: always fullscreen + decorationless on
  spawn (no change — already specified).
- Control window: Tools → Fullscreen toggle that calls
  setFullscreen + setDecorations together so the title bar
  disappears with the chrome. Persisted to
  localStorage['sos-control-fullscreen'].
- Kiosk-launch flag: --kiosk argv flag and TERRAVIZ_KIOSK=1
  env var, parsed in src-tauri/src/main.rs, applied before
  first paint. Drops the title bar and goes fullscreen on
  the main window. Aimed at .desktop autostart entries for
  unattended installations.
- F11 on every window: control + output windows wire a
  keydown handler that toggles setFullscreen. Output
  windows already start fullscreen so this is the "show
  the title bar to drag the window" escape hatch operators
  occasionally need during calibration.

Cursor auto-hide after 3 s of idle in fullscreen for the
control window. Output windows already hide the cursor
entirely per §5. Both matter for capture — a stationary
cursor is exactly the artifact operators try to avoid.

Adds delivery commit #12 to the ladder:
"multi-output: fullscreen toggle + kiosk launch + F11 on
every window." Backout plan updated: reverting #12 leaves
the LED-sphere capture path unaffected because the output
windows' fullscreen is wired in commit #3.

Capabilities note: core:window:allow-set-decorations is
NOT in the default permission set (only allow-is-decorated
is). v1 needs to add it explicitly so the runtime
fullscreen toggle can drop the title bar. allow-set-
fullscreen is already granted.

Web build (no Tauri) falls back to the standard Fullscreen
API — covers browser-source capture (OBS, vMix) for
self-hosted Cloudflare deployments.

Signed-off-by: Claude <noreply@anthropic.com>
Constraint #4 promised "re-sync if drift > 200 ms" without
saying how. The per-state-change flow referenced "drift-
correction logic from §2.4" — a section that doesn't exist
(constraints are sub-sections of §1, not §2). Both gaps are
filled here.

Adds a new architecture section "Playback sync algorithm"
in §3 that specifies a three-region state machine with
hysteresis:

- Tolerance band (< 100 ms) — no action; within decoder
  jitter and below perceptual threshold.
- Soft nudge (100 ms – 2 s) — playbackRate = 0.95 (output
  ahead) or 1.05 (output behind), held until drift drops
  under the inner hysteresis bound (50 ms), then released
  to 1.0. The 5 % rate cap is below the typical perceptual
  threshold and converges in 2-20 s for the operating range.
- Hard seek (≥ 2 s) — videoEl.currentTime = broadcast value.
  Visible jump but the only sane choice; soft catch-up at
  5 %/s would take 40+ s to clear a multi-second drift.

Outer / inner hysteresis pair (100 ms enter, 50 ms exit)
prevents flapping between the tolerance and soft-nudge
regions. Constants are exported as named values so unit
tests can drive each transition deterministically.

Bypass cases for the algorithm:

- paused → videoEl.pause() immediately, correction suspended
- operator scrub (discontinuity in broadcast currentTime)
  → hard seek
- dataset change → tear down HLS, sync once 'canplay' fires
- HLS stall (readyState < 3) → freeze drift state to avoid
  spurious "behind" readings during buffering

Notes what the algorithm is not responsible for: A/V sync
within a single decoder (browser handles it) and cross-output
coherence (each output syncs to control independently;
worst-case 200 ms apart on physically-separated outputs).

Stale reference fix (#11 from the audit): the per-state-change
flow now points to "Playback sync algorithm" by name instead
of the non-existent §2.4. Constraint #4's overview points at
the algorithm section for the detailed thresholds.

Acceptance criteria for the delivery ladder gain a new
datasetMirror unit test line covering each region transition,
each hysteresis bound, pause/unpause, and the hard-seek
discontinuity case.

Lands #1 + #11 from the completeness audit; the remaining
must-have items (failure recovery, capability enumeration,
smoke-test checklist) are separate commits.

Signed-off-by: Claude <noreply@anthropic.com>
Until now the plan assumed the happy path: outputs spawn, IPC
works, HLS streams cleanly, monitors stay plugged in, the GPU
context lives forever, and the control window doesn't crash.
A long-running installation hits all six of those edge cases
sooner or later. This commit defines what happens for each.

Adds a new §3 "Failure recovery" section with six designed-for
modes:

1. Output webview crash — manager detects via window-destroy
   without graceful close ping; logs + toast + remove the
   record. No auto-respawn (would mask recurring crashes).
   3-strikes-per-monitor crash storm guard for the rest of
   the session.

2. HLS stream errors — fatal HLS.js error triggers 3×
   exponential backoff retry (1, 2, 4 s) with the texture
   frozen on the last good frame during retry. After
   exhaustion, emit output_dataset_stalled, badge in the
   Outputs panel, manual operator reload required.

3. IPC silence — 5 s no diff puts the output into stale
   state (last good content keeps rendering). 60 s of
   silence with no manager response → output considers
   itself orphaned but does NOT self-destruct: audience-
   facing imagery stays on the LED sphere. Manager comes
   back via case 6 boot scan.

4. Monitor unplug — manager polls availableMonitors() at
   2 s. Don't auto-destroy the window; OS handles
   placement (varies by platform — see Open Question 1
   cross-link). Toast immediately, close prompt after
   60 s if still gone. Reconnect re-positions to the
   persisted {x,y}.

5. GPU context loss — output canvas listens for
   webglcontextlost / webglcontextrestored. On loss,
   preventDefault to allow restoration. On restore,
   rebuild scene from a fresh manager-pushed snapshot
   (same code path as boot, no window recreation).
   30 s no-restore timeout falls through to remove + log.

6. Manager / control-window crash — outputs detect via
   case 3 IPC silence and keep rendering. Manager's boot
   path scans WebviewWindow.getAll() for surviving
   output-* labels, sends reattach pings, reattaches
   responsive ones, destroys unresponsive ones. Audience-
   visible imagery is preserved across a control-window
   relaunch.

Common policy:

- Bounded auto-recovery (3 retries, 30-60 s timeouts);
  beyond that, escalate to the operator. Avoids flapping
  installs that mask deeper issues.
- Audience-visible separation: only output-side failures
  (crash, GPU loss) reach the LED sphere directly. IPC and
  manager failures preserve the last good state.
- All failures route through errorCapture.ts with stable
  signatures; one Tier A control-window event
  (output_failure with { label, kind, retries }) per
  occurrence. Output windows themselves stay silent —
  matches §3.6 capture-clean policy.

Updates to supporting tables:

- Modified modules: errorCapture.ts gains the
  output_failure event path.
- New modules: manager.ts now lists the watchdog,
  monitor poll, boot scan responsibilities; output/main.ts
  lists the context-loss listener and IPC-silence
  watchdog; output/datasetMirror.ts lists the HLS retry
  loop; outputUI.ts lists the per-output health badges.

Delivery ladder gains commit #13 covering the recovery
plumbing. Backout plan updated — reverting commit 13 takes
the install back to "happy-path only" without affecting
the basic state-mirroring path. Acceptable to ship without
under deadline pressure but not for unattended installs.

Risks gain a "silent installation degradation" item that
points at the recovery section as the mitigation, with the
honest note that some unanticipated failure class could
still reach the audience as black or stale content —
observability via the new Tier A event is the backstop.

Open Question 1 (Linux compositor behavior) cross-links to
case 4 (monitor unplug) since both depend on the same
compositor-specific behavior — to be tested in the same
qualification pass.

Lands #2 from the completeness audit.

Signed-off-by: Claude <noreply@anthropic.com>
Two #3 + #4 audit items, landed together because they're both
"things a security reviewer / installation operator needs that
were promised in the plan but not actually written down."

The capability promise was "narrow capability set scoped to
output-* labels: HTTP for HLS / image fetch, window controls,
no filesystem, no keychain, no IPC commands." That sentence is
not a security review; it's a hand-wave. The smoke-test promise
was "manual smoke on a dual-monitor Linux box: add an output,
load a CONUS-bbox dataset, load a video dataset to verify HLS
sync." That's also not a checklist; it's a tweet.

Both are now concrete.

§3 "Output capability spec" (new):

- Full JSON enumeration of capabilities/output.json with
  windows: ["output-*"] glob.
- Allowed: event listen / unlisten / emit / emit-to;
  current-monitor / is-decorated / is-fullscreen /
  set-fullscreen / set-decorations / close; HTTP on
  https://* only.
- Explicitly denied: localhost/127.0.0.1 (no LLM phone-home
  from outputs).
- Excluded with reasons: core:default (would grant invoke),
  core:window:default (would grant set-size/position drift),
  webview-create (outputs cannot spawn windows), updater
  (main-window concern), all fs/path/shell/dialog/clipboard,
  asset protocol (outputs require network in v1; offline
  cached datasets are control-window-only — Phase 5 polish
  if installs need it).
- IPC direction matrix: manager→output via emit_to;
  output→manager via emit + manager listen. Inter-output
  IPC is not v1 (matches failure-recovery non-goal "cross-
  output coherence — outputs sync independently to control").
- Security-review checklist (10 items) for PR-time gating.

The new-modules row for output.json now itemizes what's
allowed and what's excluded instead of saying "narrow."

Appendix B "smoke-test checklist" (new):

40 numbered steps grouped by commit (9 / 10 / 11 / 12 / 13)
plus a cross-platform parity section. Each commit inherits
the prior commit's coverage. Acceptance gates: every step
passes, no errors in the manager log, sync delta p95 <
200 ms, exactly one output_failure Tier A telemetry event
per failure-recovery action.

Coverage:

- Commit 9 (steps 4–18): panel basics, single-monitor
  guard, spawning, photoreal idle, global video sync, CONUS
  bbox alignment ≤1 px @ 4K (Open Q 7), multi-layer z-order,
  camera tracking on/off, split sphere on/off, clean teardown.
- Commit 10 (steps 19–21): persistence including the
  monitor-disconnected-at-restore case.
- Commit 11 (steps 22–24): debug overlay + framebuffer
  resolution picker including the 8K → 1080p downsample
  workflow.
- Commit 12 (steps 25–30): control-window fullscreen
  toggle + persistence, F11 on both window classes,
  --kiosk launch, cursor auto-hide.
- Commit 13 (steps 31–38): each of the six failure cases
  with concrete reproduction steps (kill -9 for crash,
  iptables / unplug for HLS fail, devtools pause for IPC
  silence, devtools "Disable WebGL" for GPU loss, cable
  unplug for monitor-gone, kill -9 manager for orphan +
  reattach).

The "Acceptance for each commit" inline bullets are
reduced to a pointer at Appendix B.

Stale Appendix A example config updated: the
trackOperatorCamera and split fields added in the LED-sphere-
zoom commit (cbbf5a4) are now reflected, with cross-refs to
§3.5 for their semantics.

Cross-platform parity check (steps 39–40) is split out:
Linux is the install gate, Windows + macOS get a recheck
of steps 4–18 before declaring v1 release-ready (WebView2
crash storm behaviour and WKWebView monitor-unplug auto-
move differ from X11/Wayland). Steps 4–24 are flagged as
Playwright-automation candidates; failure-recovery cases
stay manual.

Lands #3 + #4 from the completeness audit.

Signed-off-by: Claude <noreply@anthropic.com>
…emetry

Lands #5#8 from the completeness audit as a single coherent
"production-deployment readiness" pass.

#5 Calibration tooling. New §3 section. Two primitives ship
in v1:

- Test-pattern pseudo-dataset, selectable from a per-output
  Calibration submenu. Rendered shader-side (~80 LOC GLSL),
  no network: 8-step grayscale ramp at the equator, RGB
  color bars at lat ±30°, lat/lon graticule with
  color-coded equator + prime meridian, named anchor
  crosshairs at the eight cardinal points, N/S pole
  labels, live resolution counter. Track-camera + split +
  rotation-offset all apply to the pattern, so an operator
  can verify each primitive while looking at the same
  reference content.

- Per-output rotation offset (degrees), persisted with the
  rest of the output config. Applied as a longitude
  rotation in equirectRtt before the camera-offset
  ray-march. Lets installations whose LED sphere is
  mechanically rotated relative to canonical 0° prime
  meridian (or installs that want the prime meridian to
  face the museum entrance) align the sphere physically
  without modifying source data.

PersistedOutputConfig gains rotationOffsetDeg (default 0).
Appendix A example config updated. New commit #14 added to
the delivery ladder.

#6 Tour engine integration. Open Question 6 closed with the
right framing: tours mutate control-window globe state;
outputs mirror that state via the normal state-diff path.
No tour-aware code in outputs. setEnvView / unloadDatasetAt /
loadDataset(worldIndex) reach outputs indirectly because
they change which dataset/layers/view the primary panel is
showing. Direct tour-→output tasks (setOutput) deferred to
Phase 4. Rapid tour-driven layout swaps coalesce in the
manager's 30 ms debounce so the LED sphere doesn't flicker.

#7 VR/AR coexistence. New §3 sub-section. Each output runs
its own Three.js renderer + scene; VR session runs its own
on the control window's DOM via renderer.xr. No shared GL
context, no scene-graph coupling. Three.js is a single lazy
chunk shared as code. Operator entering VR while outputs
are running keeps everything running concurrently. The
shared-<video>-element trick from VR (constraint #4) does
not extend to outputs — each output runs its own decoder.

#8 Telemetry — Open Question 3 closed. Output windows emit
nothing (capture-clean policy match). Three new Tier A
events on the control window:

- output_added { mode, framebuffer_bucket, monitor_index }
- output_removed { mode, reason }
- output_failure { kind, retries, recovered }

Plus a perf_sample extension when outputs are active:
output_count + sync_delta_p95_ms fields on the existing
60 s sampler — no new event type. Per the analytics
contributing guide: no hashing needed (no free text), no
sanitisation needed (categorical only), tier choice is
essential (installation health is the motivation),
throttling built in (one event per discrete action).

Stale references corrected:

- Failure-recovery section incorrectly routed output_failure
  through errorCapture.ts. output_failure is a categorical
  event with no stack trace or free text — fires via
  analytics/emitter.ts directly. errorCapture.ts is not
  installed in output bundles; unhandled output errors hit
  the local console only. Fixed both the prose and the
  delivery ladder #13 description.

- Modified-modules table: errorCapture.ts row replaced with
  a TelemetryEvent union extension in src/types/index.ts
  and a perf_sample extension in src/analytics/perfSampler.ts.

Lands #5#8.

Signed-off-by: Claude <noreply@anthropic.com>
@Hackshaven Hackshaven self-assigned this Jun 16, 2026
@Hackshaven Hackshaven changed the title Claude/multi monitor output feature ce qo2 docs: plan multi-output windows for LED sphere + roadmap Jun 16, 2026
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.

2 participants