Repository navigation
Recover capture after sleep, lost displays and repeated access loss - #205
Merged
Merged
Conversation
The watchdog compares stage heartbeats with the host clock, and that clock counts the time a PC is asleep. At the supervisor's first look after waking, before capture has run again, every heartbeat looked as old as the sleep, and the pipeline restarted under the viewer. 7 of the 10 "capture loop silent" values in a day of field logs were 184 s to 12,886 s: PCs waking up. The supervisor looks every 250 ms. A gap of more than 2 s between two looks means nothing in the process ran in between, so the watchdog restamps the heartbeats it watches and gives the stages a fresh window. A real wedge still shows 3 s later. A generation start does not count as a gap: joining the old one can take seconds.
No display (NoDisplay, 12 events on 8 installs in a day): when
enumeration listed no output, capture opened the detection adapter's
first output by the ordinal it had at startup. That ordinal goes stale
after a dock, lid, sleep or driver change; the call never captured
anything, and its error ("there is no adapter with the specified
ordinal") hid what was wrong. Capture now opens the adapter and output
objects from the enumeration that listed them, and reports "no display
is attached to the desktop" when none is. While no display is on, the
retry wait looks for one every 250 ms instead of sleeping up to 5 s,
and the iPad shows "Your PC has no active display. Turn on a screen to
continue." through the heartbeat notice (new NOTICE_NO_DISPLAY = 2;
English only, awaiting translation). Older iPads ignore the value.
A lost duplication on a display that moved: outputs now carry their
adapter's LUID. After ACCESS_LOST, when the target resolves to another
output or adapter than the one being duplicated (a display unplugged or
back, a driver restart, a dock), the stage opens the target afresh
instead of re-creating the duplication on the old output. Before, a
display that came back stayed replaced by the primary.
Access lost (297 events on 51 installs): it is routine with the lock
screen, UAC prompts and mode changes, so it is logged at info, and a
loss that is not back within a second warns once. On a 0.3.4 PC whose
desktop was taken again every 1 to 4 s for about an hour, the retry
ladder was held for 5 s, so every loss waited the full 2 s step and the
iPad got 3 to 17 fps. A duplication that worked for 250 ms is now
re-created at once, so quick repeated losses cost tens of milliseconds,
and re-creations stay under four a second.
The same PC's capture loop also exited seven times with ACCESS_LOST
(0x887A0026), each a pipeline restart. Only AcquireNextFrame's
ACCESS_LOST reached the handler; ReleaseFrame and the staging Map
returned theirs with `?` and ended the loop. They now go to the same
handler as a lost acquire, and the stage waits out any ACCESS_LOST that
still leaves the loop as a display change instead of failing.
A static second screen also keeps sending its cached picture after the
duplication is re-created at the same size: keepalives waited for a new
image that an unchanged screen never brings, and the watchdog restarted
the pipeline 5 s later.
Blocked retries: the retry pace never started over within an app run,
so a PC locked for the fourth time waited up to 5 s after it was
unlocked. The pace restarts once capture has worked again.
After a gap in the watchdog's looks (a PC that slept), the heartbeats were restamped and a capture loop still silent 3 s later was restarted. Waking displays and drivers can hold capture inside a call for longer than that while nothing is wrong. The stages now get 10 s after such a gap; a capture loop still silent when it ends is restarted then, and a wedge once capture runs again is caught 3 s later, as before.
…ld picture Review r1 of #205. After ACCESS_LOST the handler listed every adapter and output from a new DXGI factory, with no time limit, at every loss: up to four times a second during mode changes and wake-ups. It now asks the duplicated output for its own description first, and lists the outputs only when that output left the desktop or was renamed, while capture runs on the primary for a missing display, or after a re-creation failed. Both calls run under a new stall limit, list_outputs (1 s warning, 4 s limit, as for device creation), and keep the picture alive while they wait. A call that does not return in time counts as no move. Each listing logs how long it took. That check looked the target up through resolve_target, which warns that the chosen display is missing: one warning per loss while it was. It has its own lookup now, which logs nothing. A moved display was reopened through the blocked pace, 500 ms later. The loop now ends with Reopen, and the stage opens the display again at once; a second Reopen within a second waits at the blocked pace, so a layout that keeps changing cannot reopen it in a loop. Resending the cached picture after a re-created duplication hid a virtual display that sends no frames any more: the picture would stay frozen for good. A second screen now resends it for at most 15 s from the first re-creation without a new image. Then the stage reopens it without the cached picture, and the watchdog restarts the pipeline 5 s later if it is still dark, as before the cached picture was kept. The mirror keeps resending. The no-display wait test allows 4 s instead of 2.5 s on a loaded test machine; the wait it checks is 5 s.
…x/capture-recovery # Conflicts: # host/src/supervisor.rs
…paused clocks Review r2 of #205. A second screen reopened by the stage (a display that moved, or one that showed no new image for 15 s after its duplication came back) was reconciled again, and reconciling started a new proof. While it proved itself the watchdog skipped its missing-frame check, and 5 s later the proof failed back to the mirror, so the restart meant for a display gone dark never came. The generation whose second screen captured its first image is now remembered: reconciling again in that generation does not prove it again. A new generation, or a display attached anew, proves itself as before. A reopen during the first proof was noted as a capture error, which fails the proof: a display that moved while the second screen started went back to the mirror. The stage now notes a reopen as an interruption only; the proof keeps its deadline. The no-display wait and the stage's reopen spacing take their clock (and the wait its sleep) as arguments. Their tests run on paused clocks: the display comes back between two looks and is seen at the next one, at 750 ms; a reopen 999 ms after the last waits at the blocked pace, and reopens 1,000 ms or more apart do not.
Review r3 of #205. A proven second screen stayed proven for its whole capture generation, whatever display it was. A virtual display back under the same name on another adapter made capture reopen it; the reconciliation matched it by name and skipped its proof, so a dark replacement would have cost a watchdog restart. The proof now records the output it ran on: the adapter's LUID and the device name. Reconciling the same output again in the same generation keeps it, as a recovery reopen should. Another adapter or name proves itself again, and so does a display that was seen off the desktop in between, by the reconciliation or by capture's check of the duplicated output, even when it comes back under the same name without a new attachment.
…x/capture-recovery # Conflicts: # ios/EternalMonitor/Resources/Localizable.xcstrings # ios/EternalMonitorTests/LocalizationTests.swift
Review r4 of #205. When the duplicated output still described itself as attached but a re-creation failed, capture listed the outputs and could find the virtual display gone, yet kept its proof. The same display back as the same output before the next reconciliation then inherited it, and a dark one would have cost a watchdog restart. The listing is now judged in one place: a listing without the output as it was (absent, or its name under another adapter) clears the proof, and only a listing that still shows the same output keeps it.
…x/capture-recovery # Conflicts: # host/src/capture/second_screen.rs
…x/capture-recovery # Conflicts: # ios/EternalMonitorTests/LocalizationTests.swift
Review r5 of #205. The reopen's first listing could find no output at all and end capture, and nothing cleared the second screen's proof: the same display back before the next reconciliation inherited it, as after r3 and r4's listings. Each listing path had to remember to judge what it saw. A new capture::listing module is now the only way to list the outputs, and it judges every listing: one that does not show the proven output as it was (absent, or its name under another adapter) clears the proof. The DXGI enumerator takes a token only that module can make, so a new listing path cannot skip it. The capture loop's listing (which a reopen makes first), the check after a lost duplication, the no-display wait, reconciliation, the virtual display worker and its mode settling, and the Settings page's display list all go through it. Only the startup banner, before any host state exists, and a native test that prints the outputs list without one. A test holds every remaining way of seeing outputs to those places.
Review r6 of #205. The startup banner's listing was a public function that made its own token, and the backstop scan matched calls, so a `use … as` alias of it listed outputs unseen. Visibility can't be narrowed to the banner: it is in the binary crate, and `pub(in …)` only names ancestor modules. So the unjudged listing is gone: the banner gets only its display label from `startup_display_label`, and the native test that prints the outputs lists them through the judge with its own host state. The debug-build fake display's outputs now take the same token as the DXGI enumerator, so the compiler keeps both to `capture::listing`. The scan matches identifiers outside comments and string literals, so aliases are seen, and has fixtures for an alias, a grouped import, a removed name, a comment, a string and a longer name. `observe_listing` names `OutputInfo` by path: the module imports it only for Windows, test and debug builds, and a macOS release build failed.
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.
What and why
Windows capture recovery, from the field logs: round 1 rows 2, 9 and 14, and round 2 row 1. Reviews r1 to r3 are folded in as additive commits.
No display (NoDisplay, 12 events on 8 installs in a day)
When enumeration listed no output, capture opened the detection adapter's first output by the ordinal it had at startup. That ordinal goes stale after a dock, lid, sleep or driver change, the call never captured anything, and the error hid what was wrong.
NoDisplay("no display is attached to the desktop") when none is attached.NOTICE_NO_DISPLAY = 2, which older iPads ignore. The string is English only and awaits translation.A display that moved
Outputs carry their adapter's LUID. After a lost duplication, capture first asks the duplicated output for its own description. It lists all outputs only in three cases: the output left the desktop or was renamed, capture runs on the primary for a missing display, or re-creating the duplication failed.
list_outputs(1 s warning, 4 s limit), and keep the picture alive while they wait.Reopenand the stage reopens the display at once. A secondReopenwithin 1 s waits at the 500 ms blocked pace.Sleep (13 "capture loop silent" restarts on 11 installs; 7 of 10 were PCs waking after 184 s to 12,886 s)
The watchdog compared heartbeats with a clock that counts the time asleep.
Access lost (297 events on 51 installs, and one 0.3.4 PC that lost it every 1 to 4 s for an hour)
ReleaseFrameand the stagingMapreturned ACCESS_LOST with?, which ended the loop and restarted the pipeline (seven times on that PC). They now go to the same handler as a lost acquire. The stage also waits out any ACCESS_LOST that still leaves the loop.Second screen after a lost duplication
capture::listing, which clears the proof when the listing doesn't show that output as it was. The DXGI enumerator and the debug-build fake display take a token only that module can make, so the compiler keeps listings there. No public listing skips the judge: the startup banner gets only its display label. A test holds the raw Windows enumeration and removed names to known files, aliases included. It must prove itself again in four cases:Blocked retries
The retry pace never started over within an app run, so a PC locked for the fourth time waited up to 5 s after it was unlocked. The pace restarts once capture has worked again.
How it was tested
-D warningsand fmt are clean, and thecapture::lib tests passed (162). macOS: the host lib passed (1,108), and clippy is clean.cargo test -p eternal-host -p eternal-wire --lockedpassed all 32 suites (1,067 host lib tests).cargo clippy --workspace --all-targets --locked -- -D warningsandcargo fmt --all -- --checkare clean.hello_standby,listen_onandamf_encoderpass.amf_encoderand one transport summary test each failed once in a run, then passed on reruns, and on the candidate alone.listingfails to compile (E0603, checked by hand).SETUP.md: no change. It doesn't describe the locked-PC or no-display messages.Native reproductions (Windows 11, RTX 5080, isolated test copies)
Sleep. A headless host on synthetic capture was suspended for 30 s. Its capture thread was released 6 s after the rest, as when capture is still inside a display call after waking.
Pipeline stage failed — automatic restart scheduled stage=Capture reason="encoder silent for 3048ms while capture advances". An earlier base run released capture 2 s after the rest and logged"capture loop silent for 30056ms".The host did not run for a while (the PC slept?) — restarting the pipeline watchdog's clock gap_ms=30305, and no restart.No display. Real capture in a session with no desktop attached:
TooManyCaptureApps(0x887A0022).No display is attached to the desktop … adapters=3, thenblock=NoDisplay detail=no display is attached to the desktop (3 graphics adapters listed).Topology, refresh and virtual display changes. These ran on real displays: two monitors, with a refresh-rate change on the second one and that monitor switched off and back. They ran under the display and measure locks. The layout, vdd_settings.xml and the VDD state were restored and checked.
Measured: repeated access loss. Each refresh change on the second monitor took the duplication away. The test hosts captured the primary monitor; see below.
WARN Desktop duplication access lost, and the recoveries climbed the ladder attook_ms=2, 101, 251, 501, 1001, 2001. That is the field PC's 2 s step.INFO, and every recovery tooktook_ms=0to7. Switching the second monitor off and back cost 3 ms before and 37 ms after.Not measured on real displays:
list_outputstimings.The test settings file lacked fields the host requires (
start_on_boot), so each host captured the primary monitor instead of the second one or the virtual display. The fix's cheap output check found the primary still attached, so no listing ran. Without a virtual display, Second screen never started.These paths are covered by the unit tests above (target move, silent lookup, immediate reopen, picture hold, proof binding, listing), not by a native run.