Repository navigation
feat(moq-video): export the studio program to the encoder on the GPU - #6805
Conversation
Quest-Claim: 9bb42c9a3f0daa9f
Adds the UI-free half of moq-studio: a versioned TOML scene collection, live sources (capture, image, color, subscribed broadcast), a compositor with preview, program, cut, and fade, and a pooled program readback. Adds moq-audio's program bus with per-source strips, and moves Level to the crate root so the bus and capture share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One wgpu device serves the sources, the compositor, and the window. A program clock draws the canvas and mixes the bus, and the program is read back and encoded only while a viewer watches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…emains Adds doc/bin/studio.md, the repository map and AGENTS.md rows, and the moq-audio migration note for Level. The app quest now holds only the zero-copy program encode and the macOS and Windows runs, and the studio quests that only needed the app are unblocked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… frame The canvas is black until a source delivers its first picture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collection::validate already refuses a negative or non-finite gain from a scene file, so only a caller bug reaches Strip::set_gain; fail loud there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
app.md keeps only the macOS and Windows runs. Zero-copy program encode and zero-copy source import become quest/m2/studio-gpu, ranked at the top of m2 and first in the studio epic, with a dated note in m1 and the studio README that they drive moq-video improvements ahead of the cut without gating it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moq-video's GPU encode and import API changes land before the cut. The m1 quests are scoped to the API shape and the backends provable now (NVENC, VideoToolbox, Media Foundation, DMA-BUF, Metal); AMD Vulkan encode, Intel VA-API import, and Direct3D11 render import stay m2 follow-ups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- moq-studio enters its runtime before building the QUIC client, which refused to start outside one, so --connect works from the binary. - A failed video or audio output aborts its track with the cause instead of dropping it unfinished, and a clean stop finishes the video. - A broadcast source follows catalog updates, switching a rendition when a better one appears instead of keeping the first catalog's choice. - The compositor forgets an edited source's last picture, so a replaced source never shows the old one's frame. - A criterion bench sweeps the program bus over channel count and resampling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
render::export::Exporter copies a texture into a pooled slot and hands it back as a Frame: an exported Vulkan image on Linux, a readback elsewhere. render::request_device opens a device with the Vulkan extension that shares a timeline semaphore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rendition name always means the same decoder config, so a metadata-only catalog update, such as a republished jitter estimate, no longer restarts the decoder and drops buffered audio. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moq-scene drops its Readback; the studio hands the program to an Exporter, which picks a GPU route the encoder takes and falls back to a readback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Metal draws the program into IOSurface-backed NV12 pixel buffers through a shader conversion; Direct3D12 draws into a texture shared with a Direct3D11 device on the same adapter, where the video processor converts it to NV12. Both are tested on their platform's CI recipe; the NV12 shader is also checked against the CPU matrix in render-sw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unrun Media Foundation hardware encode moves to video hardware validation. Also flushes the Metal texture cache before taking a buffer, so a buffer wgpu has finished with is back in the pool, and asserts every route's exporter and copies cross threads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m1/studio-gpu/encode
…h no fallback
`Exporter::new(device, queue, &mut encode::Config)` probes the encoder and
takes `Route::Gpu` exactly when that encoder reads this platform's GPU
surface (NVENC, VideoToolbox, Media Foundation), `Route::Readback` otherwise.
A device that cannot export the surface its encoder reads is a setup error
naming what is missing and, on Linux, `request_device`; nothing degrades to a
readback after trying the GPU.
Drops `export::Config { zero_copy }`, `Exporter::probe`, `Exporter::zero_copy`
and the public Linux `Exporter::input`; adds `export::Route` and
`Exporter::route`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ading - moq-scene: a broadcast rendition restarts when the track its entry resolves to changes, not its catalog ID, so a publisher that repoints a stable ID at a replacement track (the JS publisher on a codec change) is followed. A catalog offering only renditions from another broadcast fails the source instead of leaving it live and silent. - moq-scene: a picture the compositor cannot draw is reported through Compositor::error, which the studio shows as the source's status. - moq-audio: Channel::write refuses a NaN or infinite sample, which would poison the whole mix through any gain. Bus::capture no longer reopens a device behind the caller's back; it returns the device's error. - moq-studio: program audio and video are stamped on the catalog's clock, so the advertised wall time matches when the program drew them, and the canvas keeps drawing after the encoder stops. - Docs: the scene file can name local paths and devices; Windows system audio waits on the Windows capture quest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… clock The only relay test that reads the host clock is Unix-only, so the module-wide expectation went unfulfilled on Windows and denied warnings failed `just rs windows-test` before it reached the render export tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing on Windows calls it (the Desktop Duplication path it named is gone), so the render export tests `windows-test` now builds with warnings denied would fail on it as dead code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Platform's Windows runner has only WARP, whose D3D11CreateDevice refuses video support (DXGI_ERROR_UNSUPPORTED), so the conversion to NV12 cannot run there. The setup error now names the missing Direct3D11 video device, a runner test pins that refusal on the software adapter every host has, and the round trip moves to the ignored hardware tests in video-hardware validation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ee-agent-a3fba740265e97706 # Conflicts: # rs/moq-scene/src/compositor.rs # rs/moq-studio/src/engine.rs
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d7710fee10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| pub async fn new( | ||
| device: &wgpu::Device, | ||
| queue: &wgpu::Queue, | ||
| config: &mut crate::encode::Config, | ||
| ) -> Result<(Self, hang::catalog::VideoConfig), Error> { |
There was a problem hiding this comment.
Expose exporter initialization through a poll-first API
The new library initialization logic is implemented only as an async fn, with no poll_* operation accepting a &kio::Waiter. This violates the poll-first library API requirement in rs/AGENTS.md and prevents callers from driving initialization through the repository's synchronous polling interface. Implement the initialization state behind a poll operation and make the async constructor its helper; the new render::request_device API needs the same treatment.
AGENTS.md reference: rs/AGENTS.md:L21-L24
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Declining. Neither function has polling logic of its own to expose. Exporter::new awaits encode::Config::probe's machinery (Sink::open and Sink::encode, which hand the codec to its own thread) and render::request_device awaits wgpu's Adapter::request_device; moq-video's encode surface is async-only today. A poll_* here would store those foreign futures in a pinned state machine and poll them, which is the pattern rs/AGENTS.md calls a smell. Both run once at setup, so there is no hot path a poll API would serve. If moq-video's encode gains poll-first Sink operations, the export should follow them then.
(Written by Claude Opus 5.5)
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: d7710fee10b96c316a09937e2c77e705da0001cd
No additional actionable bugs found. The existing poll-first API finding and configured-color concern cover the issues identified; no duplicate inline comments. For the latter, export does infer its color, but the existing probe also constructs an inferred-color I420 sample, so mismatched explicit color may fail during probing rather than only on the first exported frame. A regression test should cover the whole initialization-to-encode path.
Overall direction is sound: platform interop stays below the app, bounded slots limit backlog, and encoder failure leaves preview rendering alive. Explicit export/readback routes avoid silently changing behavior.
Verification: GitHub-only static inspection of the export routes, ownership and slot lifecycle, studio integration, feature gates and tests. No builds, tests, GPU execution or benchmarks run; ignored hardware cases remain unverified.
Exporter::new failed setup under Kind::Auto when it picked NVENC while the wgpu device sat on another GPU, the default on a hybrid laptop or on lavapipe. It now tries the encoders the config allows in priority order, probing each with a frame exported from the device: an encoder that reads the GPU takes that route only when it encodes this device's export, and choosing passes over it for one that reads frames back. A named encoder still fails setup, naming the fix. The settled encoder is pinned in config.kind so the encoder opened next is the one probed. An explicit encode::Config::color the export does not convert to is now refused at setup instead of failing every frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: 810f5868a86b749071a689f992a82051cc9c772c
Reviewed the substantive update since d7710fee10b96c316a09937e2c77e705da0001cd; no new actionable bugs found.
Prior findings: the configured-color concern is addressed by rejecting unsupported color at setup, rather than adding arbitrary-color conversion. The poll-first API discussion is unchanged in code, with the author's rationale for declining it now recorded; not a new finding.
Direction is sound: probing an actual export checks device compatibility, shared candidate ordering avoids a second selection policy, and pinning the successful encoder makes subsequent initialization consistent. Setup rejection is a simpler explicit contract than adding unused per-route color conversions. Named encoders still fail rather than silently falling back.
Verification: GitHub-only static review of the incremental changes and relevant encoder/export lifecycle. No builds, tests or GPU execution run; added ignored hardware tests and cross-device behavior remain unverified.
|
Warning Review limit reachedYou'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 4 minutes. View limit details
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. Comment |
# Conflicts: # doc/bin/studio.md # quest/m1/README.md # quest/m1/studio-gpu/README.md # quest/m1/studio-gpu/import.md # quest/m3/studio/README.md # quest/m3/studio/app.md # rs/justfile # rs/moq-scene/src/compositor.rs # rs/moq-scene/src/lib.rs # rs/moq-studio/Cargo.toml # rs/moq-studio/src/engine.rs # rs/moq-studio/src/ui.rs
|
Merge summary for 9233df0:
(Written by Claude Opus 5.5) |
Resolve against the GPU program export (#6805): `moq play` now opens its device with `render::request_device`, the render docs point Linux callers at it, and both macOS test sets run. With the encode quest finished there, this PR completes the studio-gpu epic, so it is deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finishes quest/m1/studio-gpu/encode.md (deleted here): the studio's program reaches moq-video's encoder as a GPU surface on NVENC, VideoToolbox, and Media Foundation, and takes a counted CPU readback for an encoder that reads frames on the CPU.
Builds on moq-dev/moq#6797, which added
rs/moq-studioandrs/moq-sceneand has merged; this now targetsmain.What landed
moq_video::render::exportis the mirror ofRenderer:Exporter::copysnapshots an RGBA texture into one ofExporter::DEPTH(3) slots and returns at once;Pending::waitblocks on the GPU on the encoding thread and hands back aFrame. A copy that finds every slot busy returnsNone, so a slow encoder drops pictures rather than adding latency.Exporter::new(device, queue, &mut encode::Config)settles the encoder and the route once. It tries the encodersconfig.kindallows in priority order, probing each with a frame exported fromdevice(not a synthetic CPU picture). An encoder that reads this platform's GPU surface (NVENC on Linux, VideoToolbox, Media Foundation) takesRoute::Gpu, and only if it encodes this device's export; every other one (OpenH264, VA-API and V4L2 today) takesRoute::Readback. It leavesconfig.kindasKind::Named(<the encoder it settled on>)and, on Linux,config.inputas that encoder's device, so the encoder opened next is the one probed and the published rendition is true. An encoder never switches routes:Auto,HardwareorSoftware, an encoder that cannot read this device's export (NVENC while wgpu sits on the iGPU, another vendor's GPU, or lavapipe; or any device without the export extensions) is passed over for the next encoder, which reads frames back.Kind::Named("nvenc")with a device on another GPU failsExporter::newwithnvenc cannot encode this device's export (...); if nvenc runs on another GPU, open the device there with moq_video::render::request_device, or pick an encoder that reads CPU frames.Err, never a retry or a degrade.encode::Config::colorother thanColor::infer's is refused at setup: every route converts in the inferred space, so such a config would otherwise fail every frame withColorMismatch.The GPU route on each platform:
Surface::Vulkanthrough the existingframe::vulkanslot contract. A compute shader does the copy because wgpu-hal maps storage usage toGENERAL, the layout the contract requires. The ready value is host-signalled after the copy's submission completes, since wgpu's signal-semaphore hook applies to whatever submits next. The same slot serves VA-API and AMD Vulkan Video later with no API change.CVPixelBufferPoolof IOSurface-backed NV12 buffers, capped at the depth with the pool's allocation threshold.CVMetalTextureCachealiases each plane as a drawable Metal texture. A shader converts RGBA to NV12 inColor::infer's space (the oneSurface::rgbapicks), so the encoder gets the format every other Apple producer hands it. The shader is also checked against the CPU matrix on Linux.Surface::Texture. An event query per slot keeps a slot from being redrawn while the conversion still reads it.render::request_deviceopens a wgpu device with what the zero-copy paths need (the DMA-BUF feature andVK_KHR_external_semaphore_fdon Linux), shared with the import side (moq-dev/moq#6806). The studio opens its device with it, publishes through anExporter, and its Output panel shows frames handed to the encoder and readbacks ("none; the encoder reads the GPU" on the GPU route, known from startup).moq-scene's ownReadbackis gone.Public API and wire impact
renderfeature):render::request_devicerender::export::{Exporter, Pending, Route}, withExporter::{new, route, size, copy, DEPTH}andPending::{wait, timestamp}.Route { Gpu, Readback }, exhaustive, so a third route breaks a caller's match loudly.moq-scene(unpublished, added in moq-dev/moq#6797) dropsReadback,Pending, andError::Gpu; the studio uses the export instead.encode::Configis unchanged.Exporter::newrewrites its existingkindto the settledKind::Named, and on Linux its existinginput.Decisions (maintainer, 2026-10-25)
No silent fallback; fail loud. Removed
export::Config { zero_copy },Exporter::probeand its runtime switch to readback, and everyOk(None)"cannot export, read back instead" path in the Vulkan, Metal and Direct3D12 exporters (each is now an error naming the cause).Run Platform on this branch before marking ready. Results below.
The default configuration must work on hybrid-GPU machines; an explicit mismatch still fails loudly at setup; no runtime fallback. Options considered:
render::request_device(or a new adapter picker) opens the device on the GPU the encoder uses.Picked 2 because it adds no public API:
request_devicetakes the caller's adapter, so 1 needs a new adapter picker that ties the app's whole GPU (window, UI, power) to the encoder and can only learn NVENC's GPU through CUDA. With 2 the caller keeps choosing the adapter (the studio already asks forHighPerformance, which is the NVIDIA GPU on a typical laptop), and a mismatch costs a readback instead of a startup failure. The decision is made once at setup, observable throughExporter::route()and an info log, so it is not a runtime fallback.Choices made
Renderer, not moq-scene, so any wgpu app gets it.Exporter::newtakes theencode::Configand returns the probed rendition, replacingExporter::new+Exporter::probe. The route depends on which encoder the config opens, so a single constructor makes an exporter whose route was never settled unrepresentable. Textures are the config's size, so the two cannot disagree.RoutereplacesExporter::zero_copy() -> bool, since the decision asked for the route. The LinuxExporter::inputgetter is private now;Exporter::newsetsencode::Config::inputitself.vulkancandidates on Linux (NVENC), every hardware encoder on Apple platforms and Windows. A software encoder takes the readback even if it could download a surface, since a readback is cheaper than its per-frame download.download_i420keeps working.d3d11::create_devicetakes an optional adapter;Texture::captureandPlan::captureare compiled forrenderas well ascapture.Kind::Hardwarecounts as choosing among encoders, likeAuto; onlyKind::Namedis an explicit choice.config.kindto the settled name meansEncoder::newno longer logs its own "falling back to software" warning, soExporter::newlogs it instead.the_program_is_exportable: an export now needs an encoder, and the studio's relay test already exports the compositor's program on the host's route.moq-relay's clock-lint expectation and the PipeWire-onlyI420::from_bgra) landed onmainin moq-dev/moq#6821; the merge ofmaintakes its versions, so they are no longer in this diff.Checks
cargo clippy --all-targets -D warningsfor moq-video (render,openh264;--no-default-features --features render; defaults), moq-scene, moq-studio (with and withoutopenh264);cargo nextestfor the three crates; rustdoc with-D warnings;quest check.just check's flake module fails in this sandbox (unshare: write failed /proc/self/uid_map), unrelated. All rerun after the hybrid-GPU change (clippy also withrender,nvidia,openh264).just rs render-swon lavapipe: readback route, route selection (OpenH264 takes the readback), NV12 shader, compositor, and the studio's relay test.choosing_passes_over_an_encoder_that_cannot_read_this_devicefeedsExporter::settlean export-reading candidate that cannot read the device (OpenH264 marked as reading the GPU, standing in for NVENC on another GPU) ahead of a readback one, and asserts the readback route,Kind::Named("openh264")and noinput.a_named_encoder_that_cannot_read_this_device_fails_at_setupkeeps the explicit mismatch loud and checks the error names the fix.refuses_a_color_it_does_not_convert_tocovers the color refusal. On lavapipe the export cannot open (Mesa exports no memory); on RADV and ANV it opens and the encoder refuses the device, the real hybrid case, sosh/rs/gpu.shnow runs both on real drivers.clippy --all-targets): the Metal route onaarch64-apple-darwinand the Direct3D12 route onx86_64-pc-windows-msvc, before the hybrid-GPU change. After it, macOS and Windows are covered by Platform only.videotoolbox_reads_the_export_in_placenow settles through the exported-frame probe and VideoToolbox still encodes in place with the pool cap intact, anda_software_adapter_is_refused_at_setuppasses on WARP.videotoolbox_reads_the_export_in_placeruns on the runner's Metal device, and VideoToolbox encodes the exported pixel buffers in place.D3D11CreateDevicewith video support fails there (DXGI_ERROR_UNSUPPORTED), so the Direct3D12 route cannot run on it (run 40234531745).exports_an_nv12_textureis now ignored and listed in video hardware validation. The newa_software_adapter_is_refused_at_setuppins the fail-loud setup error on WARP, which every Windows host has.moq-tokioflake (quic::driver::tests::stuck_window_evicts_the_lower_priority) that passed on the next two runs.sh/rs/vulkan-cuda.sh); the Direct3D12 round trip and the Media Foundation hardware encode (ignored, listed in video hardware validation).Suggested follow-ups
MFT_ENUM_ADAPTER_LUID) would keep a hardware encode on the GPU route there.vaapioropenh264.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code