fix(vdisplay): preserve FramePublisher across swap-chain reassign (sibling-join freeze)
windows-drivers / probe-and-proto (push) Successful in 28s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
windows-drivers / driver-build (push) Successful in 1m35s
apple / swift (push) Successful in 4m34s
ci / bench (push) Successful in 5m39s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6m50s
docker / deploy-docs (push) Successful in 33s
windows-host / package (push) Successful in 10m8s
android / android (push) Successful in 12m24s
arch / build-publish (push) Successful in 16m41s
deb / build-publish (push) Successful in 17m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m13s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m21s
ci / rust (push) Successful in 23m52s
apple / screenshots (push) Successful in 20m9s
windows-drivers / probe-and-proto (push) Successful in 28s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
windows-drivers / driver-build (push) Successful in 1m35s
apple / swift (push) Successful in 4m34s
ci / bench (push) Successful in 5m39s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6m50s
docker / deploy-docs (push) Successful in 33s
windows-host / package (push) Successful in 10m8s
android / android (push) Successful in 12m24s
arch / build-publish (push) Successful in 16m41s
deb / build-publish (push) Successful in 17m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m13s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m21s
ci / rust (push) Successful in 23m52s
apple / screenshots (push) Successful in 20m9s
When a second client got its own virtual display mid-stream, the FIRST client's IDD-push stream froze (video only; `new_fps=0 repeat_fps=240` forever). Adding/removing/resizing a sibling display re-commits the CCD topology, which makes the OS unassign+reassign the first monitor's IddCx swap chain. `unassign_swap_chain` dropped the SwapChainProcessor, dropping `run_core`'s local FramePublisher and closing the sealed-channel handles. The fresh worker then polled the frame-channel stash — but that stash is consumed once at session open, and the host only re-delivers on a ring recreate (a descriptor change). The first monitor's descriptor didn't change and WUDFHost stayed alive, so no watchdog fired: the driver drained the swap chain without publishing and the host repeated its last frame indefinitely. Confirmed twice on the .173 box (host.log 21:12 & 21:15). Preserve the live FramePublisher across the flap instead of dropping it: the host-owned ring (header/event/textures) it holds stays valid — only the swap chain died. - frame_transport.rs: FramePublisher records its render-adapter LUID + exposes render_adapter(). - monitor.rs: MonitorObject.preserved_publisher + preserve_publisher() (mirrors set_frame_channel) + take_preserved_publisher() (mirrors take_frame_channel). Monitor teardown drops the stashed publisher and closes its ring handles, so nothing leaks. - swap_chain_processor.rs run_core: after SetDevice OK, re-adopt a preserved publisher ONLY when the new swap chain renders on the same LUID (same pooled Direct3DDevice → its context + opened textures are valid); on loop exit, stash the publisher back on the monitor. Safe: the old worker is fully joined (drop-outside-lock discipline) before the new one runs, so no concurrent context use; a stale re-adopted publisher is superseded by the existing is_stale() + has_frame_channel() newest-wins checks at the loop top. Verified clippy -D warnings clean on rustc 1.96.0 via a faithful mock crate (the real crate needs the WDK to compile). Needs a driver rebuild + reinstall on the host to take effect; not yet hardware-validated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,17 @@ pub struct MonitorObject {
|
||||
/// closes an unconsumed channel's handles via [`FrameChannel`]'s `Drop`, so no delivery can leak
|
||||
/// handles in the WUDFHost table whatever the monitor's fate.
|
||||
pub frame_channel: Option<crate::frame_transport::FrameChannel>,
|
||||
/// A live [`FramePublisher`](crate::frame_transport::FramePublisher) preserved across a swap-chain
|
||||
/// unassign→reassign flap (STEP 6 sibling-join fix). The OS unassigns a monitor's swap-chain
|
||||
/// whenever a SIBLING display churns the desktop topology (a second client joining / leaving /
|
||||
/// resizing), which drops the swap-chain worker — but the HOST-owned ring (header / event /
|
||||
/// textures) the publisher holds stays valid, and the host only re-delivers the frame channel on a
|
||||
/// ring RECREATE (a descriptor change), so a fresh worker had nothing to re-attach from and the
|
||||
/// first client's stream froze (repeat frames forever). The exiting worker stashes its still-live
|
||||
/// publisher here ([`preserve_publisher`]); the next worker on the SAME render adapter takes it back
|
||||
/// ([`take_preserved_publisher`]) and resumes publishing into the same ring. Dropped with the
|
||||
/// `MonitorObject` on teardown (closing its ring handles) if no worker ever reclaims it.
|
||||
pub preserved_publisher: Option<crate::frame_transport::FramePublisher>,
|
||||
/// When the entry was created — the watchdog skips still-initializing monitors.
|
||||
pub created_at: Instant,
|
||||
}
|
||||
@@ -318,6 +329,49 @@ pub fn has_frame_channel(target_id: u32) -> bool {
|
||||
.any(|m| m.target_id == target_id && m.frame_channel.is_some())
|
||||
}
|
||||
|
||||
/// Stash a swap-chain worker's still-live [`FramePublisher`](crate::frame_transport::FramePublisher) on
|
||||
/// its monitor across a swap-chain unassign→reassign flap (STEP 6 sibling-join fix; see the field docs
|
||||
/// on [`MonitorObject::preserved_publisher`]). Called from the EXITING worker thread — the caller must
|
||||
/// NOT hold `MONITOR_MODES` (this locks it), matching the same drop-outside-the-lock discipline the
|
||||
/// processor teardown paths use. Returns `Err(publisher)` when no monitor with `target_id` exists (a
|
||||
/// genuine teardown, not a flap: the entry was already removed) so the caller drops it, closing the ring
|
||||
/// handles. Replacing an already-stashed publisher (should not happen — one worker exits at a time)
|
||||
/// drops the old one, so it can never accumulate. Returning the publisher in the `Err` makes the
|
||||
/// `Result` itself `#[must_use]`, so a caller can't silently drop the not-preserved publisher.
|
||||
pub fn preserve_publisher(
|
||||
target_id: u32,
|
||||
publisher: crate::frame_transport::FramePublisher,
|
||||
) -> Result<(), crate::frame_transport::FramePublisher> {
|
||||
if target_id == 0 {
|
||||
return Err(publisher);
|
||||
}
|
||||
let mut lock = lock_monitors();
|
||||
if let Some(m) = lock.iter_mut().find(|m| m.target_id == target_id) {
|
||||
m.preserved_publisher = Some(publisher);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(publisher)
|
||||
}
|
||||
}
|
||||
|
||||
/// Take (remove) a preserved [`FramePublisher`](crate::frame_transport::FramePublisher) for a freshly-
|
||||
/// (re)assigned swap-chain worker (STEP 6 sibling-join fix). The caller re-adopts it ONLY when the new
|
||||
/// swap-chain's render adapter matches the publisher's ([`FramePublisher::render_adapter`]) — same
|
||||
/// pooled device, so its context + opened ring textures are still valid; on a mismatch the caller drops
|
||||
/// it and waits for a fresh channel delivery instead. `None` until a worker has stashed one.
|
||||
pub fn take_preserved_publisher(
|
||||
target_id: u32,
|
||||
) -> Option<crate::frame_transport::FramePublisher> {
|
||||
if target_id == 0 {
|
||||
return None;
|
||||
}
|
||||
lock_monitors()
|
||||
.iter_mut()
|
||||
.find(|m| m.target_id == target_id)?
|
||||
.preserved_publisher
|
||||
.take()
|
||||
}
|
||||
|
||||
/// Install a swap-chain processor on the monitor whose handle matches, returning any PREVIOUS processor
|
||||
/// for the caller to drop OUTSIDE the lock. Dropping a processor RAII-joins its worker thread, so it must
|
||||
/// never happen while holding `MONITOR_MODES` (the worker would block the whole control plane / risk a
|
||||
@@ -406,6 +460,7 @@ pub fn create_monitor(
|
||||
adapter_luid_high: 0,
|
||||
swap_chain_processor: None,
|
||||
frame_channel: None,
|
||||
preserved_publisher: None,
|
||||
created_at: Instant::now(),
|
||||
});
|
||||
id
|
||||
|
||||
Reference in New Issue
Block a user