Repository navigation
Linux engine: 2.0 engine contract (Phase C) - #82
Merged
Merged
Conversation
The 2.0 UI follows the Windows engine's policy: Auto means hardware only, and a software replay encoder is accepted only when the user chose Software. The Linux candidate list still appended the software encoders to Auto, so a broken VA-API driver silently produced a CPU-heavy libopenh264 replay that the 2.0 UI then rejected as "hardware encoding unavailable" without the engine saying why. Move the candidate list into a pure encoder_policy unit, make Auto try NVENC, VA-API/AMF and QSV only, and pin it with the linux_encoder_candidates test. nvenc_active now means "a compressed replay encoder is open" (any backend), which is what the 2.0 UI reads it as; before, an AMD VA-API engine reported false. docs/linux.md no longer claims a software fallback.
The 2.0 UI launches the engine with 26 positional arguments and runs `--list-playback-devices` / `--list-microphones` to fill its device pickers. The Linux engine read argv with strtol, so `--list-playback-devices` parsed as fps 0, was clamped to 1 and started a full engine that took over the shared-memory name; argv[24] and [25] were not read at all, and the buffer and bitrate ceilings (300 s, 60 Mbps) were below the 2.0 UI's 900 s and 200 Mbps. - Parse argv in a pure ParseEngineArgs() using the shared ParseU32, with the 2.0 limits, empty-means-default in every slot, and a warning for each invalid value. Unknown codec or encoder values become Auto; the old min(value, 4) turned an unknown encoder value into Software. - Any argument starting with `--` exits before shared memory is touched: the two listing flags (alone) print one schema-1 JSON line, anything else exits 2 with usage. - `--list-playback-devices` lists PulseAudio sinks through libpulse (endpoint_id = sink name, display_name = description, is_default from the server). `--list-microphones` prints an empty list: the Linux engine records no microphone. - argv[24] (playback sink) and argv[6] (byte budget) are carried in CaptureConfig for the following commits; argv[25] (microphone mono) is parsed and ignored. Tests: linux_engine_args, linux_audio_endpoint_json.
argv[24] carries the 2.0 playback endpoint. On Linux it is a PulseAudio sink name: capture that sink's monitor (resolved through the server, so a sink that no longer exists is detected) instead of always the default output. A missing sink is logged and falls back to the default monitor, and only that default case keeps following default-sink changes; an explicitly chosen sink that drops out mid-session is retried every second like any lost stream. Reconfigure keeps the selection.
With 2.0 a replay buffer can be 900 s at up to 200 Mbps, about 22 GB of encoded video held in memory with nothing but the time window to stop it. argv[6] (max_buffer_mb) was ignored. When argv[6] is non-zero the encoded video ring now has a byte budget. Over budget it drops whole GOPs from the front, so the oldest packet is always a keyframe and saves stay keyframe-aligned; the GOP being filled is never dropped, even when it alone exceeds the budget. The first trim per encoder generation is logged with the history it kept, and startup logs the budget against the expected size, warning when the bitrate outruns it. Desktop audio stays time-bounded on purpose: it is ~375 KiB/s whatever the bitrate (~340 MiB at 900 s) and has to cover the whole video window, or a clip starts silent. Note the 2.0 UI still sizes argv[6] as a raw-frame estimate capped at 2048 MiB, so at that cap a 900 s buffer above roughly 18 Mbps is trimmed (and says so in the log).
In 2.0, START_RECORDING with a path in ui_string is the manual (continuous) recording request. The Linux engine treated every START_RECORDING as "resume" and answered RECORDING_STARTED, so the 2.0 UI would believe a recording was running that was never written. START_RECORDING with a path now answers MANUAL_RECORDING_ERROR with a message in engine_string (protected from the health-reason writer until the UI consumes it) and leaves the pause state alone; an empty ui_string still means resume. The 2.0 UI's resume_recording() does not clear ui_string, which still holds the last clip path after a save, so every focus resume after the first save would have been refused. The engine now consumes ui_string when it takes a SAVE_CLIP path (before publishing SAVE_STARTED, so the UI cannot have written a new one yet), which keeps resume working with the UI as it is.
Record argv[24]/[25], the 900 s and argv[6] budget semantics, the listing modes and the `--` exit, the hardware-only encoder policy and what nvenc_active means, and how the Linux engine answers START_RECORDING with and without a path.
The engine can now record a chosen sink's monitor instead of only the default; say so where the Linux guide describes desktop audio.
…report sink-list errors Review follow-ups: - StartForSink treated an unreachable server like a missing sink and fell back to the default monitor for the whole session, so an engine started while pipewire-pulse was still coming up never used the chosen device. Only a server that answered without the sink falls back now; otherwise the capture loop keeps retrying <sink>.monitor. - pa_context_get_sink_info_list reports a server error as eol < 0; that printed an empty, successful device list. It is an error now.
Collaborator
Author
|
Review follow-ups pushed:
Not covered by CTest (needs a PulseAudio server or the shm loop): |
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase C of
docs/linux-2.0-catch-up.md: the Linux capture engine now speaks the 2.0 UI's engine contract. Changes are confined toFTHRcapture_linux/plusdocs/linux.mdanddocs/engine-startup.md. The shared-memory layout and both enums are unchanged (Linuxchar ui_string[1024]/engine_string[2048]).Contract changes
--arguments--list-playback-deviceswas read as fps 0, clamped to 1, and started a full engine that took the shm name--argument exits before shared memory is touched. The two listing flags, used alone, print one schema-1 JSON line. Anything else exits 2 with usage--list-playback-devicesendpoint_id= sink name,display_name= description (falls back to the name),is_defaultfrom server info,"microphones"key as on Windows--list-microphones{"schema_version":1,"microphones":[]}, because the engine does not capture a micstrtol, 24 slotsParseEngineArgs()using the sharedParseU32. 26 slots. An empty string means default in every slot. Each invalid value gets a[Config]warningcapture_config_limits.h)min(v, 4), so 5+ meant Softwarenvenc_active*_nvenc(false on AMD VA-API!)MANUAL_RECORDING_ERRORwith a message inengine_string. An emptyui_stringstill means resumeKept: the Linux
transactional_save.h(fsync before publish).replay_interval.hneeded no changes.Risk: stale
ui_stringon resume (UI side)capture_bridge.resume_recording()writes onlyui_command. After any save,ui_stringstill holds the clip path, so a strict "path means recording" check would have refused every focus resume after the first save (Linux uses focus pause:focus_pause_supported()is true off Windows). To handle this, the engine now clearsui_stringas soon as it copies a SAVE_CLIP path, before publishing SAVE_STARTED. The physical run below exercises exactly this sequence. The UI should still clearui_stringinresume_recording()and keep the Linux manual-recording skip (the UI agent is restoring it). Any other future writer ofui_stringfollowed by START_RECORDING would be read as a recording request.Judgement calls
The budget covers only ~572 s at this bitrate). Raising the cap or sending an encoded-size budget is a UI decision.nvenc_active and codec != 'libopenh264'branch; only H.264 software is labelled as software there.Tests
New CTests:
linux_encoder_candidates(pins Auto = hardware only, software only when explicit, LGPL only),linux_engine_args(26-slot UI launch, empty = default, limits, ParseU32 edge cases,--classification),linux_audio_endpoint_json(exact schema-1 line, escaping, empty description).linux_encoded_ring_durationgains byte-budget cases: whole-GOP drops, keyframe-aligned snapshots after trimming, a single oversized GOP survives, exact byte accounting across time trims andClear().linux_encoder_timestamp/resizealready selected Software explicitly, so they needed no changes.Clean Release build with gcc 16.2.1 and the pinned FFmpeg: 0 warnings, 23/23 tests passed (vaapi encoder probe ran rather than skipped).
tools/verify_engine_response_contract.py: intact. (verify_shared_memory_contract.pystill fails on the Linux UI layout, which is Phase B and the UI agent's work.)Physical evidence (Arch, Hyprland, PipeWire-Pulse, AMD VA-API)
Standalone engine with 26 args, non-default sink in argv[24], 900 s, 30 Mbps, argv[6]=2048, driven over
/dev/shmby a script:Other runs:
Playback device 'no_such_sink_xyz' not found; capturing the default output monitor insteadthenCapturing default output monitor: alsa_output.pci-0000_13_00.6.analog-stereo.monitor.Using: libopenh264.FTHR_VAAPI_DEVICE=/dev/null): tries onlyh264_nvenc,h264_vaapi,h264_amf,h264_qsv, thenAll codecs failed — no encoder available (no automatic software fallback; ...). Recovery is exhausted and the engine exits. libopenh264 is never tried.