f4c3a5d0c3
apple / swift (push) Successful in 1m7s
ci / web (push) Successful in 1m9s
ci / docs-site (push) Successful in 1m18s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m41s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m43s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m11s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m34s
ci / bench (push) Successful in 6m12s
apple / screenshots (push) Successful in 5m18s
docker / deploy-docs (push) Successful in 27s
flatpak / build-publish (push) Failing after 5m25s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m50s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m35s
arch / build-publish (push) Successful in 19m55s
deb / build-publish (push) Successful in 18m59s
ci / rust (push) Successful in 20m9s
android / android (push) Successful in 20m11s
Live-diagnosed on the RTX box during the adaptive-bitrate A/B: 2 of 3 streams died with VK_ERROR_DEVICE_LOST at stream start or at a mid-stream encoder rebuild, then zombied — the demote-to-software path rebuilt the decoder against the same dead device, FFmpeg wedged inside the rebuild, and the pump flushed a never-draining backlog every 2 s forever. No OS TDR: the client's own Vulkan misuse. Two causes: 1. The presenter creates ONE graphics-family queue and hands FFmpeg's AVVulkanDeviceContext the same family (nb_graphics_queues=1 ⇒ queue 0) for its transfer/compute prep — so the pump thread and the presenter thread submitted to the SAME VkQueue with no shared lock, violating vkQueueSubmit's external-sync rule exactly when FFmpeg puts work on the graphics queue (decoder open / frames-context rebuild = stream start and every ABR encoder re-target). New guard-less QueueLock (FFmpeg's lock_queue/unlock_queue callbacks are a raw pair) shared by all four queue users: FFmpeg (callbacks installed via user_opaque), the presenter's submit/present/wait-idle, and the Skia overlay's flushes. 2. Swapchain recreation destroyed the old swapchain + render semaphores after ONE fence cycle — the fence proves our submit, not the presentation engine's semaphore consumption (VUID-vkDestroySemaphore-05149 + VUID-vkDestroySwapchainKHR-01282 on every recreate). Recreate now drains the queue (vkQueueWaitIdle under the shared lock — safe now that FFmpeg honours it) and destroys immediately; the deferred DisplayGarbage machinery is gone. Resilience: VK_ERROR_DEVICE_LOST anywhere in a present error chain is now fatal — the run loop fails the session loudly instead of demoting to software on a dead device (the zombie path). Verified on the RTX box (RTX 4090 → host .21) under VK_LAYER_KHRONOS_validation: 3/3 stream start/stop cycles clean, then 8 mid-stream encoder rebuilds in one session (4 ABR down-steps under 10% induced loss + 4 clean-link recovery up-steps — the exact scenarios that previously killed the device): 0 device losses, 0 wedges, both recreate VUIDs gone (previously fired on every path). Remaining validation messages are FFmpeg's own video-session VUIDs, untouched by this change. Linux: clippy -D warnings + tests green (home-worker-2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
131 lines
5.9 KiB
Rust
131 lines
5.9 KiB
Rust
//! The presenter↔console-UI contract (punktfunk-planning
|
||
//! `linux-client-rearchitecture.md` §6.1): the presenter exposes its device and
|
||
//! composites at most ONE sampled RGBA quad per frame; the overlay implementation
|
||
//! (pf-console-ui, Skia) fills offscreen images on its own damage-driven schedule. No
|
||
//! Skia type crosses this line — everything here is ash — and a `frame()` returning
|
||
//! `None` costs the hot path nothing (the quad isn't even recorded).
|
||
|
||
use ash::vk;
|
||
use pf_client_core::gamepad::{MenuEvent, MenuPulse};
|
||
use punktfunk_core::config::GamepadPref;
|
||
|
||
/// The presenter's device, shared with the overlay so its renderer (Skia's
|
||
/// `DirectContext`) creates resources on the same VkDevice/queue. Handles stay valid for
|
||
/// the presenter's lifetime — the overlay must be dropped before it (the run loop owns
|
||
/// both and drops the overlay first).
|
||
pub struct SharedDevice {
|
||
pub entry: ash::Entry,
|
||
pub instance: ash::Instance,
|
||
pub physical_device: vk::PhysicalDevice,
|
||
pub device: ash::Device,
|
||
pub queue: vk::Queue,
|
||
pub queue_family_index: u32,
|
||
/// External-sync lock for `queue` — FFmpeg's decode prep submits to the same queue
|
||
/// from the pump thread, so every overlay flush/submit must hold it (the presenter
|
||
/// and FFmpeg's `lock_queue` callbacks serialize on this same lock).
|
||
pub queue_lock: std::sync::Arc<pf_client_core::video::QueueLock>,
|
||
}
|
||
|
||
/// What the overlay may draw this frame — composed by the run loop from session state.
|
||
/// Milestone 1 (OSD/HUD) is text-shaped; the console library replaces this with a
|
||
/// richer scene enum when it moves in.
|
||
pub struct FrameCtx<'a> {
|
||
/// Swapchain size in pixels — the overlay renders 1:1.
|
||
pub width: u32,
|
||
pub height: u32,
|
||
/// Multi-line stats OSD (top-left panel); `None` = hidden.
|
||
pub stats: Option<&'a str>,
|
||
/// The capture hint (bottom-center pill, "click to capture…"); `None` = hidden.
|
||
pub hint: Option<&'a str>,
|
||
/// The active gamepad's name (the console library's controller chip).
|
||
pub pad: Option<&'a str>,
|
||
/// The active pad's resolved kind — drives the console UI's button glyphs
|
||
/// (PlayStation shapes for DualSense/DualShock, ABXY letters otherwise).
|
||
pub pad_pref: Option<GamepadPref>,
|
||
/// Every connected pad (the console settings' "Use controller" row).
|
||
pub pads: &'a [pf_client_core::gamepad::PadInfo],
|
||
}
|
||
|
||
/// One overlay image ready to composite: RGBA, PREMULTIPLIED alpha, already in
|
||
/// `SHADER_READ_ONLY_OPTIMAL`, sized `width`×`height` (normally the `FrameCtx` size; a
|
||
/// stale size during a resize just stretches for a frame).
|
||
pub struct OverlayFrame {
|
||
pub image: vk::Image,
|
||
pub view: vk::ImageView,
|
||
pub width: u32,
|
||
pub height: u32,
|
||
}
|
||
|
||
/// An action the overlay raises out of its input handling (browse mode). Only actions
|
||
/// the RUN LOOP must act on live here — starting/canceling sessions and quitting; data
|
||
/// work (pairing, discovery, library fetches…) rides the console command bus instead.
|
||
pub enum OverlayAction {
|
||
/// Start a session on this host. `launch` carries a library title id on the Hello
|
||
/// (`None` streams the desktop); `title` is display-only (window title).
|
||
Launch {
|
||
addr: String,
|
||
port: u16,
|
||
fp_hex: String,
|
||
launch: Option<String>,
|
||
title: String,
|
||
},
|
||
/// Abort an in-flight connect (B while Connecting) — the console keeps browsing.
|
||
/// The run loop stops the pump; a dial that already won the race is quit-closed.
|
||
CancelConnect,
|
||
/// Quit the launcher (B at the root) — ends the process, Gaming Mode returns.
|
||
Quit,
|
||
}
|
||
|
||
/// Session lifecycle notifications into the overlay (browse mode drives its scenes off
|
||
/// these; the OSD/HUD ignore them).
|
||
pub enum SessionPhase<'a> {
|
||
/// A launch action was accepted — the connect is in flight.
|
||
Connecting,
|
||
/// Connected; frames are coming.
|
||
Streaming,
|
||
/// The connect failed (browse mode returns to the library with this message).
|
||
Failed(&'a str),
|
||
/// The session ran and ended (`Some` = abnormal reason for the status strip).
|
||
Ended(Option<&'a str>),
|
||
}
|
||
|
||
/// The console-UI side. Object-safe; the session binary passes
|
||
/// `Option<Box<dyn Overlay>>` (None = the Skia-free power-user build).
|
||
pub trait Overlay {
|
||
/// One-time setup on the presenter's device.
|
||
fn init(&mut self, shared: &SharedDevice) -> anyhow::Result<()>;
|
||
|
||
/// Input routing, before capture sees the event. `true` = consumed (the library or
|
||
/// a menu is up) — the event must not reach capture/forwarding.
|
||
fn handle_event(&mut self, event: &sdl3::event::Event) -> bool;
|
||
|
||
/// Gamepad menu-mode navigation (browse mode; the run loop drains the service's
|
||
/// menu channel). Returns a haptic pulse to play on the menu pad, if any.
|
||
fn handle_menu(&mut self, _event: MenuEvent) -> Option<MenuPulse> {
|
||
None
|
||
}
|
||
|
||
/// Drain one pending action raised by handled input. Called once per loop
|
||
/// iteration; return `None` when idle.
|
||
fn take_action(&mut self) -> Option<OverlayAction> {
|
||
None
|
||
}
|
||
|
||
/// A session lifecycle edge (browse mode scene driving).
|
||
fn session_phase(&mut self, _phase: SessionPhase) {}
|
||
|
||
/// True while a text field is being edited — the run loop starts/stops SDL text
|
||
/// input to match (IME + `Event::TextInput` delivery on desktop; under gamescope
|
||
/// this is also what lets Steam's on-screen keyboard type into the app).
|
||
fn text_input_active(&self) -> bool {
|
||
false
|
||
}
|
||
|
||
/// Once per presenter iteration. Damage-driven: re-render (flush + transition to
|
||
/// SHADER_READ_ONLY) only when the content or size changed, else return the previous
|
||
/// image. `None` = nothing to composite. The returned image must stay untouched
|
||
/// until `frame()` runs again (the presenter runs one frame in flight and the
|
||
/// implementation keeps a ring of two, so alternating satisfies this).
|
||
fn frame(&mut self, ctx: &FrameCtx) -> anyhow::Result<Option<OverlayFrame>>;
|
||
}
|