Skip to content

feat(studio): guided setup on first launch - #7

Merged
kixelated merged 13 commits into
mainfrom
quest/m0/guided-setup
Nov 3, 2026
Merged

kixelated merged 13 commits into
mainfrom
quest/m0/guided-setup

Conversation

@kixelated

@kixelated kixelated commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The first launch of moq-studio opened an empty 1080p30 studio. To go live, a creator had to find their devices, guess a canvas their machine and uplink can carry, and read the docs. This finishes quest/m0/guided-setup.md.

Approach

When the scene file does not exist, the window opens on a setup screen instead of the studio. It runs three checks in the background, and each failure is shown with its reason:

  • Devices: the existing device listing, moved from ui.rs to devices.rs with typed fields so setup and the Devices panel share it.
  • Encoders: for each candidate canvas (1080p60, 1080p30, 720p60, 720p30, 360p30), moq_video::render::export::Exporter::new, the same probe the program uses at startup. It records the encoder it settled on, whether it reads the GPU, and the bitrate. Then it times a 15-frame burst through that exporter and a fresh encoder: a panning picture is copied, waited on, and encoded, as the program's encode thread does. The first frame (a keyframe) is untimed, and the burst stops once its average can no longer come in under budget.
  • Uplink (uplink.rs): a probe publish at the --broadcast path, using one-shot connections:
    • A second connection subscribes to the probe back through the relay, so the relay pulls it.
    • A bandwidth reservation sized to the largest candidate canvas's program (divided by the headroom) sets the transport's probe target.
    • Filler follows the reservation's grant.
    • After a 6 s window, the congestion controller's send-rate estimate is the measurement. If the connection reports no estimate (WebSocket), the check fails rather than guessing.

The proposal, explained in plain words beside each choice:

  • Scene: a scene named Main with the first display filling the canvas, the first camera in the bottom-right corner over it (or filling the canvas alone), and the first audio input. Each can be swapped for another device or set to None.
  • Canvas: among canvases whose encoder keeps up (a frame copied and encoded within 75% of its period), the largest whose video plus audio fits the uplink's headroom, or the cheapest when none fits. Without a measurement it is the default 1080p30 when that keeps up. When no encoder keeps up, it is the canvas closest to keeping up, and the explanation says to expect dropped frames.

Start with an empty scene opens a new collection on the chosen canvas. Start with this scene opens the same collection with the proposal as its first edit: a Draft labelled "Set up scene Main", applied and recorded in the history as any panel edit is, so undo returns to the empty scene. Both start the engine with a fresh Autosave on the scene file, whose first save writes the scene file and its history at once. Nothing opens or writes the file before that, so quitting during setup leaves no file and setup runs again. A start refuses a file that appeared during setup (another studio, say) instead of overwriting it: the autosave's own read is the check, and its first save of a file it found missing refuses one created since (persist_noclobber). A failed start shows its error and can be retried with other choices.

Engine::start now takes a Launch (GPU, relay, broadcast path, on-demand), the Autosave, and the autosave's Loaded collection and history, so setup can start it later. The save on quit runs only once an engine exists.

The encoder probes never overlap the program's or the sources' encode sessions: each canvas's exporter and encoder are dropped before the next opens, and the start buttons appear only once every probe has finished, so the program opens its encoder at startup, ahead of the sources, as on main.

Impact

  • Opening a scene file that does not exist starts guided setup instead of an empty studio. An existing file opens as before.
  • Setup creates the scene file when either start button is pressed, not at launch. The proposal is the history's first entry, so it can be undone.
  • The autosave's first write of a scene file that did not exist fails, rather than replacing it, if another program created the file meanwhile; the status bar says to restart.
  • With --connect, setup publishes a probe broadcast (track probe, filler bytes) at the --broadcast path for about 6 s and subscribes to it through the relay.
  • Setup encodes a short burst per canvas. Measured on an Apple M4, release build: the encoder check took about 0.46 s with VideoToolbox and 0.40 s with openh264, against 0.12 s for opening the exporters alone, so the burst adds about 0.3 s ahead of the 6 s uplink window. A slow encoder is cut off at 15 frames' budget: at most about 0.19 s at 60 fps and 0.38 s at 30 fps per canvas.
  • The Output panel gains a Canvas row.
  • README: new "First launch" section; the quickstart bullet and Output panel row are updated.

No scene-file format change.

Decisions

  1. 2026-11-02, maintainer: time a short encode burst in the per-canvas Exporter::new probe, and propose only canvases whose encoder keeps up with real time. This replaces the follow-up "the encoder probe proves an encoder opens, not that it keeps up". It supersedes the quest Plan's "no separate benchmark": the burst runs through the exporter the probe already opened, not a separate test.

Choices made without asking

  1. Output settings are the canvas only. The bitrate stays the encoder's default for the canvas (0.07 bits per pixel), so the scene file gets no new field. Live bitrate belongs to encode-control.
  2. First launch means "the scene file does not exist". There is no --setup flag. Picking a new file path reruns setup.
  3. "Changed later": sources and scenes through their panels, and the canvas by editing the file and restarting (live resize is scene-editing's). The Output panel shows the canvas in use.
  4. The probe uses the --broadcast path, since that is the path a relay token lets the studio publish at. Following only the probe's own epoch (follow(path, Some(epoch))) never resolved through the test relay, so the viewer follows the path. A collision there is the same one the program itself would hit.
  5. The estimate is read at the end of the window, not as a peak. On loopback it climbs well past the probe target (about 688 Mbps), from burst samples. The controller's estimate is already a windowed maximum.
  6. Uplink headroom is two thirds of the measured uplink, read from Health's degraded threshold (hang::health::Thresholds::default().transport.headroom.degraded, 1.5), so a proposed canvas does not show as degraded in the Health panel the moment it goes live. It was 75%, which Health would grade degraded. The window stays 6 s (the transport probe's rise budget is 5 s).
  7. The camera rectangle is 16:9, a quarter of the canvas wide, inset by a fortieth of the width. The compositor stretches sources to their rectangle, so a 4:3 camera is stretched until per-item fit lands.
  8. Windows and system audio are not proposed. The quest names cameras, microphones, and displays. On macOS, system audio is in the audio list and can be picked.
  9. An empty start creates the file, as choice 9 anticipated once autosave landed: its first save writes the collection, with the chosen canvas and no sources, at once, so the next launch opens the studio rather than setup.
  10. Encoder pace: 75% of the frame period, over 15 timed frames of a panning gradient. The remaining quarter is for sources and the compositor sharing the machine. The picture pans 4 px a frame so the encoder searches motion as for live content, and is a gradient-and-pattern image rather than noise, which no camera or screen produces. A pipelined encoder that holds every exporter slot is flushed and the copy retried, and the burst ends with a flush, so held frames count.
  11. No status-bar message after setup. The autosave's own error line covers a failed write, so the "set up; saved" message and Studio::new's message parameter are gone.
  12. The proposal is one undoable edit. Undoing it leaves the empty scene Scene on the chosen canvas, which is what Start with an empty scene opens. Setup builds it through Draft and records it in the Loaded history before the engine starts, rather than committing after, so the autosave's first save already holds it.
  13. Setup's background work, and the Devices panel's listing, run on threads of their own, not the runtime's blocking pool, which dropping the runtime waits out. A probe stuck on a camera permission prompt, or the 6 s uplink window, no longer holds the studio open after its window closes. The listing futures are !Send on macOS, so they cannot be spawned on the runtime (#14 moved the panel to the blocking pool for that).

Verification

  • just ci passes locally on macOS (Apple M4) at the reviewed head, capture features included now that #14 builds them: 71 tests passed.
  • Unit tests on the paused clock: a burst times every frame but the first, and a burst that falls behind stops at the eighth timed frame, within the time a keeping-up burst takes.
  • Unit tests for the pick: headroom boundary at two thirds, cheapest fallback, refused encoders, no measurement, canvases skipped when their encoder is too slow (including when the default is), and the closest-to-keeping-up fallback.
  • Layout test: the proposal is one history entry that undoes to the empty start's collection, on the chosen canvas.
  • autosave::tests::leaves_a_file_created_since_it_found_none: a scene file created after the autosave found none is left as it is, and the error stays shown.
  • setup::tests::times_every_canvas and engine::tests::the_program_reaches_a_relay (ignored, in just render-sw and so the nightly) pass on an Apple M4 with VideoToolbox after the merge.
  • Relay tests from before: the uplink probe gets an estimate through moq_relay::test_relay, and an unreachable relay fails with a message naming the connection.

Alternatives

  • A separate speed test. The Plan rules this out in favour of the existing probes.
  • Timing the burst inside moq-video (Exporter::new). It would need a moq change and a pin bump, and the studio already has every public piece (Exporter::copy, Encoder::encode) to time its own path.
  • A noise picture. Worst case for every encoder, so it would turn down canvases a camera or screen encodes in time.
  • A bitrate field in the scene file, set from the uplink. It is a new user-facing surface, and encode-control will make the bitrate follow the grant live.
  • One connection that publishes and subscribes to itself. A relay may not route a session's own announcement back to it, and two connections avoid depending on that.

Follow-ups

  • origin::Consumer::follow(path, Some(epoch)) did not resolve for a broadcast announced with that epoch through moq_relay::test_relay. This is worth checking in moq: either the relay does not carry the epoch, or the probe used it wrong.

Review

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 3 commits November 2, 2026 14:42
Quest-Claim: 579298a7a5ac44a3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…starting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Guided setup feat(studio): guided setup on first launch Nov 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 2, 2026 23:02
@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2fdd94e2-6338-49ea-a78e-3eb0de261ae7
Commits

Reviewing files that changed from the base of the PR and between 5d0c2a0 and f9db233.

Files selected for processing: 11
  • README.md
  • justfile
  • quest/m0/README.md
  • quest/m0/guided-setup.md
  • rs/moq-studio/src/autosave.rs
  • rs/moq-studio/src/devices.rs
  • rs/moq-studio/src/engine.rs
  • rs/moq-studio/src/main.rs
  • rs/moq-studio/src/setup.rs
  • rs/moq-studio/src/ui.rs
  • rs/moq-studio/src/uplink.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

Setup now starts the engine with an Autosave it opens only once the
creator picks a start, so a missing scene file is never created before
setup finishes. Both starts create the file through the autosave's first
save, and setup refuses to overwrite a file that appeared meanwhile.
The save on quit runs only once an engine exists. The Output panel keeps
main's broadcast and encoding rows plus the Canvas row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kixelated kixelated left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 98187f4

No confirmed actionable defects found. Device and encoder errors and measurement fallback are surfaced. The automatic probe uses the real broadcast path, a collision risk already acknowledged by the PR; relay arbitration was not independently verified.

Verification: static review only; no tests, device checks or live relay checks run.

kixelated and others added 3 commits November 2, 2026 19:13
Each canvas's encoder probe now times a 15-frame burst of a panning
picture through the program's own copy and encode path, and setup
proposes only canvases that copy and encode a frame within three
quarters of its period. When none keeps up, it proposes the one closest
to keeping up. A burst that falls behind stops early, so a slow encoder
costs no more time than a fast one.

The uplink headroom now follows the Health panel's degraded threshold,
so a proposal is not graded degraded the moment it goes live, and rates
use the Health panel's formatting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kixelated kixelated left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: c5a17d4

P2 — Reserve the setup file atomically (rs/moq-studio/src/setup.rs:782–790). try_exists() and Autosave::open() are separate, and the collection returned by open is discarded. If another process creates the file after the check, including before autosave's first write, this setup can overwrite the newly created scene despite the no-overwrite promise. Atomically create/reserve the path or make the initial write create-new and fail on existence; add a concurrent-creation regression test.

The earlier real-broadcast probe collision risk remains unchanged and acknowledged. Verification: static review only; no tests or device checks run.

kixelated and others added 4 commits November 2, 2026 20:19
Setup's proposal is now the scene model's first edit, recorded in the
history, so undo returns to the empty scene and the autosave writes both
files. Engine::start takes the Launch with autosave's Loaded. The first
save of a missing scene file refuses one another program created since.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h the autosave

The autosave's own read is the check, and its first save refuses a file
created after it, so no window remains where setup could replace one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping the runtime waits out its blocking pool, so a probe stuck on a
camera permission prompt held the studio open after its window closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Devices panel keeps main's off-runtime listing for macOS, on a thread
of its own like setup's probes, so a stuck listing cannot hold the studio
open on quit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kixelated kixelated left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: 9d8eb42

The earlier setup-file overwrite finding is addressed: setup checks the autosave's own read, and the first write uses a no-clobber persist. No new actionable issues found in the updated setup and history integration. Moving potentially stuck probes off the runtime's blocking pool is a reasonable way to avoid holding shutdown open. Static review only; I did not independently run device, encoder, or relay tests.

@kixelated
kixelated merged commit 754d3d7 into main Nov 3, 2026
3 checks passed
@kixelated
kixelated deleted the quest/m0/guided-setup branch November 3, 2026 15:08
kixelated added a commit that referenced this pull request Nov 3, 2026
Fits guests to guided setup (#7): the room joins `engine::Launch` as an
`Arc`, since setup clones the launch, and is loaded before setup so bad
guest flags still fail at startup. Guests are followed once the engine
starts, after setup has recorded its scene.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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