Skip to content

Release 0.3.5 candidate - #197

Draft
whoisaldo wants to merge 339 commits into
mainfrom
release/0.3.5-candidate
Draft

whoisaldo wants to merge 339 commits into
mainfrom
release/0.3.5-candidate

Conversation

@whoisaldo

Copy link
Copy Markdown
Member

Draft that runs the full CI matrix on the combined 0.3.5 candidate. CI only runs on pull requests, so this checks the merged PRs together, not just one at a time. It stays a draft until 0.3.5 is ready for a stable release.

Merged into the candidate so far:

@whoisaldo whoisaldo added the 0.3.5 After 0.3.4 label Nov 7, 2026
whoisaldo and others added 27 commits November 7, 2026 17:38
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.
The virtual display has no hardware pointer, so Windows draws the
pointer into its image: on the reference PC every pointer move on the
second screen was a new image, and Desktop Duplication kept reporting
the pointer visible at one stale position. An iPad drawing the pointer
there would show that stale pointer too. On the second screen the host
now keeps the pointer in the video as before and tells the iPad it is
hidden; Mirror sends it apart.
Listing adapters and making a device, where a driver call can stall, ran on
the window's thread, so one stuck call froze the window for good. Now they
run on their own thread (winit::Answer), which the window polls each frame.
One that hasn't answered in 10 s counts as a failed try for the backoff and
give-up, and no other starts while it is out; a late device is still taken.
The window's thread keeps only the surface and its swapchain.

New test faults, debug and fault-injection builds only:
device-lost-later-fail and device-lost-later-hang fail or hang every
replacement, and device-lost-early-fail loses the device at the first frame.
The failure was recorded only when a replacement chose its adapter. If every
replacement failed before that, as when the window's surface can't be made,
the painter gave up and a normal quit then cleared the start marker without
recording anything. The painter now counts the devices it drops
(DEVICES_LOST), and the window records the backend it started on as failed,
once, at the next frame after a drop.
…x/gpu-detect-timebox-035

# Conflicts:
#	host/src/supervisor.rs
The candidate's log-capturing tests call prepare_log_capture first, so a
stall line another test's thread logged before any subscriber still
reaches the capture. The detection test asserts that line too.
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.
sws_frame_start in FFmpeg 7.1 takes a reference to the source before it
references the output, and sws_scale_frame returns at once if that second
step fails, leaving the source held until the next conversion. For a
borrowed capture buffer that reference must not outlive the call, so a
failed sws_scale_frame now also calls sws_frame_end.
Review r1, P1: an Enable task the previous host gave up on can still be queued or running in Task Scheduler while the device reads disabled. Startup recorded the display as off and ran nothing; the Enable then added the monitor, and every later Disable, the quit's included, returned "already off", leaving a phantom monitor.

The startup cleanup now decides on its own turn, after handing the turn back to startup. It reads Task Scheduler first: if either driver task has an instance queued or running, or Task Scheduler can't say, the state stays unknown and the Disable runs as before, behind that instance. Only with nothing pending does it read the device, and only problem 22 counts as off. The task's run step is now shared with every other request. The COM query runs on the startup thread, not the window's.

Test: an Enable left running by the previous host ends after startup, and the quit's Disable still turns the display off. A native, ignored test checks the Task Scheduler query on Windows.
Review r1, P2: UTF-32's little-endian byte order mark (FF FE 00 00) starts like UTF-16's. The file decoded as UTF-16 full of NULs, did not parse, and a driver Setup installed with a card to name had the whole file replaced.

Both UTF-32 byte order marks are now refused before UTF-16 is tried. Decoded text holding a NUL, which XML never allows (UTF-32 without a byte order mark reads as UTF-8 that way), is refused too. Such files are left byte for byte, whoever installed the driver, with or without a card to name. The UTF-16 decoder now uses as_chunks, which clippy asks for.
A threaded swscale context can fail to initialise where a single-threaded
one doesn't, and that failure made the encoder fail to open. The scaler
now logs it once and opens on one thread, as before threading.
On macOS, YUV420P input is converted by Accelerate, and swscale is only
its fallback. Its context now opens on one thread there, so the threaded
one keeps no workers waiting. conversion_context takes the thread count.
While the PC is restarting the stream, stopped by the user, or stopped after
repeated failures, it now answers the iPad's HELLO with Restarting, Stopped or
Failed (with a short reason) instead of staying silent. A small standby
responder serves the stream port and USB tunnels between pipeline runs, and the
bound socket is handed over without closing, so no HELLO is lost across a
restart. Nothing starts streaming by itself after Stop.

New iPads advertise a feature bit for these answers, pad their HELLO2 so every
answer fits, wait with the right card and connect as soon as the PC streams
again. A standby or stopping-stream answer is never larger than the HELLO it
answers. The standby follows changes to the chosen network and to Listen on.
Old iPads keep today's behaviour.

Supersedes #85.
releases_past_the_hard_limit_still_release_everything waited with finish(),
which now queues a release of everything first. That reset passed the test
even with the overflow's releases lost. It now waits with wait_idle(), which
releases nothing. With the overflow marker disabled, the last thing Windows
gets is a press.
The panic hook turns the virtual display off for a panic on any background
thread, and the window's device replacement thread is one, so a panic there
could end a Second screen stream although the window only counts a failed
try. The thread is now named (REPLACEMENT_THREAD) and the hook only logs a
panic on it. The hook's body moved to on_panic so a test can run it.
It checks that polling a stuck replacement never waits for it; a slow
scheduler alone must not fail it.
OwedKeyframe::next dropped its kept picture only after take_within had
returned the newer frame and released the mailbox. An encoder thread
descheduled in between held both, capture could publish a third frame,
and the synthetic source's next render found no free buffer.

take_within now takes the kept picture and drops it under the mailbox
lock before the newer frame leaves the mailbox. A test-only hook runs in
the window after a take; the regression test publishes the third buffer
there and checks that capture still has one free.
The 500 ms deadline for giving AMF its last picture again was taken after
send_frame and the receive loop, so time spent in them was not counted.
OwedKeyframe::after_encode now sets it from the moment the keyframe was
submitted, and keeps it across the retries that follow.
The deadline test waited 20 ms against a 60 ms deadline and then slept
past it, so a slow scheduler could fail it. It is now two tests: the
picture is given again under a deadline a minute away, and is let go at
once under one that has already passed.
The test installed its own hook around on_panic, which skipped the
installer and the hook it chains to, and raced the other test that changes
the process-wide hook. Now the test binary runs itself again with
EM_TEST_PANIC_HOOK_CHILD, installs the host's panic hook there with a
virtual display stand-in that notes its thread, and checks that a panic on
the replacement thread left it alone while another background thread's
turned it off. The polling test drops its wall-clock bound.
An Automatic BGRA open that outlived the first picture's wait finished
through EncoderState::opened directly, so a BGRA failure after the wait
skipped open_at_input's YUV420 retry and left NVENC remembered as failed
for the launch. The background open now retries at YUV420 in the
background, the pipeline keeps YUV420, and the failure is handed back,
to be remembered, only if YUV420 fails too.
A background session was taken without looking at the codec negotiated
since it started, so an HEVC open could replace libx264 after HEVC was
turned off or a client that decodes only H.264 connected. Such an open is
now dropped each frame before it can be taken, and the session that
streams goes on. The frame's preferred encoder comes from one function,
as before.
While the PC isn't streaming, the standby responder answered only HELLO2.
An iPad that found its saved PC at a new address checks it with IDENTIFY
first, and got no answer until streaming started again, so it said the PC
didn't answer instead of showing that the PC is stopped.

The standby responder now hands IDENTIFY to the session too, which answers
it exactly as while streaming: the same proof, the same limits per address
and in all, no address list older than its freshness, Wi-Fi only, and an
answer no larger than the request. The responder also lists the home
network addresses when it binds every address, as the stream does.
whoisaldo and others added 30 commits November 8, 2026 11:57
…ules it counts

The fix now decides from the same verdict the Stream page shows, on the
networks in use with Windows Firewall on. It adds the installer's Allow rule
only when no Allow rule already lets the stream in there, so a narrower rule
the user made stays the only one. It removes only this exe's own Block rules
on those networks, leaving blocks for other networks alone, and reads
everything back through the verdict before it reports success.

Each Block rule is renamed to a fresh name and removed by that name, so no
other rule sharing its name can go with it. The firewall objects are released
before COM is shut down. The Windows prompt belongs to the window that asked
for it, and the page waits for the fix however long it runs, so Try again
never starts a second copy.
Tell the iPad when the PC is restarting its stream
…x/connect-when-pc-returns

# Conflicts:
#	ios/EternalMonitor/App/ConnectionManager.swift
Make four host lib tests independent of the machine's load
#228 added RawFrame::cached. The GDI start picture now sets it: no new
picture is due after it, so an encoder that holds its last frame gets the
shortest wait.

The GDI read now also returns nothing for a duplication rotated 180
degrees (or a square mode turned 90), which the size check missed, and
for an all-black read, as on the secure desktop, so the start picture is
tried again with the next duplication instead of counting as real. A
native test reads the primary display through GDI at its size, and
nothing at another size or rotation.
Brings in the restarting notice (5). The heartbeat says Second screen needs Windows 11 last: after the capture and graphics card notices and after "restarting", so the session decides it next to "restarting" rather than the capture's notice. On the iPad its pill comes after the restarting one and before "restoring the picture". All strings waiting for translation are kept.
Log what started each held picture on the iPad
Say goodbye when the PC sleeps, shuts down or signs out
Measured on NVENC and AMF under injected Wi-Fi loss, the PC answers a
keyframe request within about 15-130 ms, AMF's reopen included, and no
requests pile up. The long recoveries at 3-10% loss were a frame lost
soon after a recovery landed: the iPad sends one request per 500 ms and
the PC grants one per 500 ms, so that loss waited for the decoder's next
try 600 ms later.

The PC now also grants a request that names a frame at or past the first
keyframe it sent since its last grant: the iPad already had that
recovery, so this is a new loss. That grant doesn't move the 500 ms
window, and a request naming an older frame, or none, waits as before.
The PC says so with HOSTCAP_KEYFRAME_PER_LOSS. To such a PC the iPad
sends the first GapLoss after a keyframe inside its spacing, once per
keyframe, without moving the spacing. An older PC gets the same requests
as before.

The loss measurement now reports each recovery, request and keyframe,
models the iPad's request rules, and can model an older iPad.
A rule's %name% path now means what the machine's settings say, from the
known folders and the system environment, never from the environment of the
process, which any user can change for their own programs. The Stream page's
verdict and the elevated fix share this, so another program's Block rule can
never be taken for this exe's and removed. A system value that names another
variable, or one only a user has, stays unknown.

The Windows prompt is now started synchronously, as a thread with no message
loop needs.
Connect by itself when a PC that didn't answer comes back
…path allowed

A system environment value stored to be expanded, such as one built on
%SystemDrive%, is now expanded from the machine's own values too, up to eight
levels deep. A value only a user has, or values that name each other in a
cycle, stay unknown. A REG_SZ value is taken as written, as Windows does.

A Block rule with this exe's file name whose path still can't be resolved may
be this exe's. The verdict now calls that network uncertain instead of
allowed, so no notice claims a block is certain, and the fix never removes
such a rule and reports that it didn't finish instead of success.
…x/amf-test-isolation

# Conflicts:
#	host/tests/amf_encoder.rs
The startup and quit Disable now use the same read as the second screen's attach wait. It confirms there is no device only when every present root device was read and none is the driver's. A device list that can't be read now counts as uncertain there, instead of as no device. So does a device that can't be read, and the Disable task runs for both, as before. The separate absence check is gone.
Start a still mirror from the screen's picture, and wake the idle capture for a viewer
Each machine value is looked up once per expansion, and an expansion stops,
as unknown, after 256 references or 32 KB of text. Values that name each
other many times over can no longer stall the firewall check or the fix.
Run each AMF encoder test alone, from a clean launch
Say plainly on Windows 10 that Second screen needs Windows 11
A laptop powers its discrete card off entirely (D3cold) about 20 s after
its last use: on an AMD hybrid laptop the card and every PCIe port above
it, up to the root port, went to D3. The next D3D11 device or AMF open on
it then took about 800 ms instead of 160, and an AMF session's first
picture after 40 s idle took about 870 ms instead of 130.

An iPad looks for PCs over mDNS on its connect screen, seconds before it
sends HELLO. The host now reads mDNS queries alongside its daemon, and a
PTR or ANY question for the service from another device (not the PC's own
daemon, which probes at each registration and re-advertisement) creates a
D3D11 device on the encoder's adapter on a thread of its own and drops it.

Nothing is kept open, and nothing wakes the card on a timer: a wake comes
at most every 15 s, never while a viewer is connected, and each one no
session followed doubles the wait for the next, up to 10 min.
Answer a Wi-Fi loss after a recovery with its own keyframe at once
Install the virtual display driver on Windows 11 only
- The browse watch read into a 1500-byte buffer and stopped at its first
  receive error: one oversized mDNS datagram (WSAEMSGSIZE) ended warming
  until the app restarted. It now reads up to mDNS's 9000 bytes, goes on
  at once after errors that pass, and waits longer after repeated or
  other errors, up to a minute, without stopping.
- A HELLO could start a session between the browse's check and the wake's
  device. The wake thread now checks again, just before it creates the
  device, that no session has started since (the session count), and
  drops the wake if one has.
- After a long backoff a session didn't restore the short wait until the
  old ten-minute one had passed: the gate checked the wait before
  noticing the session. It now resets the wait first, keeping the 15 s
  minimum.
- Whether to wake now follows the encoder that will stream: the override
  in Settings, and no wake after a fallback to the CPU, nor for an
  encoder on another card.
…d it

The browse callback judged a snapshot of startup: the detected encoder,
the Settings override and the CPU-fallback flag. A hardware fallback
(NVENC or AMF failing over to Quick Sync, which clears that flag) still
woke the card no one used, and a late GPU detection the supervisor
adopted never became wakeable: startup's libx264 stayed in the callback.

SharedControl::wake_adapter now holds the card to wake. Each encoder open,
fallbacks included, sets it from the encoder that opened and the
pipeline's GPU: that GPU's adapter for a hardware encoder of its family,
None for the CPU or another card's encoder. Adopting a late detection
sets it too, until the new pipeline's encoder opens. The callback reads
only that.
Offer a one-click fix for a Windows Firewall block on EternalMonitor
…ere a change reopens (#254)

Most hardware encoder reopens came right after an ABR bitrate step, and each
reopen costs a stutter and an IDR. Many of those steps were ABR moving back
and forth on a link just below the next rung.

- NVENC now changes its bitrate on the open session, through FFmpeg's
  reconfigure path, with the VBV sized as a fresh open would size it. The
  change stands only when FFmpeg reports that the reconfigure ran and did
  not fail; anything else reopens the encoder at the new bitrate, and later
  changes on that card reopen as before.
- FFmpeg's verbose lines reach the encode thread only while it watches for
  that report, through an atomic flag in the log shim; FFmpeg's own log
  level is set once at startup.
- For encoders that still reopen on a bitrate change (AMF, libx265), ABR
  waits 30 s after a change before stepping up, doubling to at most 120 s
  when a step up is undone, and resetting when one holds. Steps down are
  unchanged, and a Max bitrate change with ABR off applies at once.
- Unit tests for the encoder table, NVENC confirmation and refusal on
  forced and periodic IDRs, the log bridge, the ABR holds, and a closed-loop
  simulation; native NVENC and AMF suites.
Wake a laptop's discrete card when an iPad looks for PCs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

0.3.5 After 0.3.4

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants