Repository navigation
Conversation
libx264 refuses an odd width or height (4:2:0 needs even dimensions), so a 1680x945 screen with no usable hardware encoder never streamed: the encoder failed to open on every restart until the pipeline gave up. The encoder now opens at the even size below the capture and drops the odd last column or row, on every input path (swscale to YUV420P/NV12, Accelerate on macOS, the BGRA copy NVENC reads). The crop reads the capture's top-left region in place, so no pixel is resampled. The size check compares encoded sizes, so an odd capture does not reopen the encoder every frame. The stream config, GUI and stats report the encoded size; one info line per open names both sizes. Input still maps onto the whole capture: each pixel of the cropped picture lands on itself and the picture's far edge reaches the row or column left out.
Stream screens with an odd width or height
On a hybrid laptop, Windows can put the desktop on the virtual display alone when it turns on. The host only logged that, waited 30 s for an attach that could not happen, then turned the display off, after which Windows reported no screen at all and refused every layout until a restart. The display guard now acts while the virtual display is still on. A turn on that leaves it alone, where the PC had a real screen before, switches Windows to Extend, else re-applies the saved layout, else PC screen only. A turn off that finds it alone puts a real screen back first, and does not run if nothing does. A start whose display was left alone falls back to the mirror at once with second_screen_fallback vdd_only, remembered like an attach timeout.
…lock An iPad whose HELLO never reaches the PC says "Your PC didn't answer" while the PC says it is ready, and neither side could say why. - Setup now deletes every inbound rule for EternalMonitor-host.exe before it adds its Allow rule. Windows adds inbound Block rules for an app when its "allow access" prompt is dismissed, and a Block rule beats an Allow rule, so running Setup again, the fix the iPad suggests, left the block in place. - The host reads the firewall policy at startup and every minute and logs the profiles in use, the Allow and Block rules for its own exe and a verdict. The Stream page warns while a Block rule or "Blocks all incoming connections" keeps iPads out. - The network line names the adapter kinds that hold a home network address, which tells a PC on a cable from one on Wi-Fi.
Review fixes for the Windows Firewall check: - A Block rule counts only when it reaches the stream's port. One scoped to remote or local addresses, remote ports, interfaces or a service is logged as scoped_block_rules and never shown. - Each profile in use keeps its own verdict. The Stream page warns only when every profile blocks and the PC has a home network address; a block on one of two networks is logged as partly_blocked. - The warning stays hidden while any iPad is connected, over Wi-Fi or the cable. - A failed read clears the last reading, so an old block can't stay on screen. - The line's keys are diagnostic keys: a PC or user named like a token no longer changes it in uploads.
…t read Review fixes for the Windows Firewall check: - A Block rule with no program blocks every app, ours included. It is now judged like ours when it covers the stream's port, logged as all_programs_block_rules, and the Stream page points to Windows Security instead of Setup, which deletes only this exe's rules. - A rule field that could not be read is unknown, not "any". A rule with an unknown scope or program is logged as scoped and never makes a block certain. Rules are read through a small trait, so tests can fail each getter.
Review fixes for the Windows Firewall check: - Rules are also read through INetFwRule3: an app package, a user owning the process, authorized local or remote users or machines, or IPsec SecureFlags limit whose traffic a rule is for. Any of them, set or unread, makes a rule scoped, so it never makes a block certain. - The Stream page warns only for this exe's own unscoped Block rule, which Setup removes, or "Blocks all incoming connections" on every profile in use. A Block rule for every program is logged (blocked_all_programs) and never shown. - An enabled Allow rule for this exe or every program that asks for IPsec may override Block rules for authenticated traffic. Any such rule makes a block uncertain: verdict=uncertain, bypass_rules=N, no warning.
Say when Windows Firewall keeps iPads out, and let Setup remove the block
FFmpeg 7.1 only uses swscale's threads through sws_scale_frame, so the encoder's scaler is now built with a thread count (half the logical processors, at most four) and converts capture's rows where they lie through sws_scale_frame, with a buffer that never frees them. YUV420 input, which AMF, Quick Sync, x264 and PCs that kept YUV420 from 0.3.4 use, took about 14 ms per 2732x2048 frame on one thread. The swscale line in the log now says how many threads it uses.
On an AMD hybrid laptop, the virtual display's arrival takes the built-in panel away below the layout: Windows lists the panel's target as unavailable, refuses every layout that names it, and only a restart gives it back. The guard now tells that case apart on a PC with more than one graphics card (the virtual display on, the panel off, no built-in target offered) and makes no SetDisplayConfig call. Every turn off stays held, at start-up too, so the desktop stays on the display the iPad shows; only Windows ending the session lets the turn off run, so the restart starts without the display. Second screen falls back at once with second_screen_fallback="panel_lost" and leaves the display on. The failure is saved until Second screen is chosen again: no wait, update, or blocked mirror tries it by itself. It is saved as an attach_timeout with panel_lost set, which older versions read as a timeout. The Stream page says to save work on the iPad and restart.
When a saved PC's HELLO2s go unanswered, look it up by its Bonjour name. If exactly one PC claims that name at another address, connect there and send the saved pairing. The PC must give back the same name and token, otherwise the iPad leaves it and keeps its pairing. Addresses the user typed and host names are never looked up. A home network entry gives way to the new address; VPN and Tailscale entries stay. Recent entries now keep the PC's name and whether the address was typed. The abandoned attempt's summary reads outcome=no_reply end=superseded. If the new address does not answer either, the card says where the iPad found the PC. scripts/e2e_moved_pc.sh streams from 127.0.0.1, moves the host, and expects the app to stream again by itself.
…rt loop Quitting, Stop and the storm brake ended the PC's stream without a word to the iPad. A quit or a self-restart showed up on the iPad as a lost signal or an unplugged cable. After Stop or Failed the session even stayed open with nothing ticking it: no heartbeats, no liveness expiry, and new HELLOs went unanswered until the next Start or quit. - Session::shut_down now ends the session with a goodbye that the transport sends on its way out, over UDP or the USB tunnel. Quit and Stop say user_disconnect, so the iPad shows "Your PC ended the session" and stays away. A restart or update handoff, and the storm brake, say error, so the iPad reconnects at once. - The supervisor ends the session when it stops for a manual Start (Stopped, Failed). A backoff or a held port keeps it, as before. - A goodbye left while no transport ran is dropped when the next one starts. - The transport loop marks a heartbeat with the step it is in. The supervisor's thread logs one line (stall="transport_loop" phase=...) when the mark is 2 s old, and one when the loop moves again. A gap in the supervisor's own checks (PC asleep) is not counted.
…, Apple Devices is missing or the PC is old - No complete frame 2 s after video began: the reassembler names the missing fragments of the latest incomplete frames and the largest fragment received, the app logs it and asks the PC for a fresh keyframe with reason Startup, and again every 2 s while video still arrives. Startup passes the 500 ms request spacing, which the dropped frames' own requests keep busy. The PC's packet size has no control message, so the iPad can't ask for smaller datagrams. - A Wi-Fi link failure with no network ends the attempt as noNetwork, summarized as failure=no_network instead of other. The card says "Wi-Fi is off". - A PC whose HELLO_ACK lacks the USB capability can't reach Apple's USB service: the USB card says Apple Devices isn't running there. - The update check keeps the manifest's Windows version. A PC found by Bonjour with an older version gets "Update EternalMonitor on your PC" under its row.
Turning pairing off is the user's setting, so the startup line is info, not a warning. The host re-advertises mDNS every minute and logged each one; it now logs a re-advertisement when its addresses change and otherwise once an hour, with how many went unlogged.
An iPad that says it draws the pointer (HELLO2 feature bit 5) gets the pointer image and its position as control messages from a host that says it sends them (host capability bit 6). The host then leaves the pointer out of the video, and a pointer-only update needs no frame, so pointer motion no longer waits for the encoder. - POINTER_SHAPE (0x1A) carries the image in chunks: premultiplied BGRA and, when a pixel inverts the picture under it, an invert mask. Monochrome, color and masked-color Windows pointers convert to it. - POINTER_STATE (0x1B) carries the position and visibility, at most one per frame budget, the newest, and once more 100 ms after the last. - Receiver reports append the image id the iPad holds, so a lost chunk is sent again; the image also goes with every session start and IDR. - The iPad draws the image over the video in the Metal pass, through the same content rect the touch relay uses, with a second pass that inverts the picture where the mask says. - iPads and hosts without the bits behave as before. Fixes #183
…ice is lost
egui-wgpu 0.36 panicked on the frame after a device loss ("Failed to create
staging buffer for index data"), which ended the app. The vendored copy now
skips that frame, drops the lost device and builds a new one, and the window
reloads its logo on the new device. A replacement device leaves the launch's
crash-avoidance marker alone.
While the lock screen or a permission prompt is up, Windows lets nothing capture the screen, and the iPad showed a frozen picture without a word (SecureDesktop: 22 events on 13 installs in a day, 116 resumes on 28). The heartbeat gets an appended notice byte, sent only while there is a notice. The PC sets it from the capture block: PC locked for SecureDesktop, nothing for the rest, and it clears when capture works again. Control bodies are append-only and both parsers ignore trailing bytes, so older iPads read the same heartbeat as before, and a plain heartbeat is byte-identical. New golden vector heartbeat_pc_locked. The iPad shows a quiet pill over the held picture, "Your PC is locked. Unlock it to continue.", until a heartbeat says capture works again. The string is English only, in the catalog and awaiting translation.
… stops in summaries Review round 1 of the stop goodbyes. - The transport no longer ends when the encoder closes its video channel. The encoder thread drops it before its failure report reaches the supervisor, so the transport used to exit before the storm brake queued its goodbye; the next transport then dropped it as stale. Now the loop stops reading video and runs until `running` goes false. - After Session::shut_down, HELLOs are refused with Busy until a transport resumes. A HELLO before the loop saw `running=false` opened a session that nothing served. One place decides it: closed_to_clients. - Summaries tell stops apart with existing tokens: Stop is end=user, the storm brake is end=error with failure=capture_failed, encoder_failed or other. A quit stays end=shutdown. SupervisorState::Failed keeps the stage. - On the iPad, the error goodbye's card is titled "The connection to your PC ended", true for a PC that restarts by itself as well as for a fault. "Your PC ended the session" stays for Stop and quit. SETUP.md lists both. - stalled_ms is a diagnostic key. The stall test checks for a free port on every address, as the transport binds, and fails fast if the transport ends early.
At 4-22% WiFi loss each replacement keyframe arrived incomplete: one round of requests and repairs leaves some of its 80-300 fragments missing, and the iPad dropped it at its 25 ms deadline because the small frames after it, which need it, were already complete. The picture then froze until the iPad reconnected. An incomplete keyframe is now held at its deadline while the frames behind it complete, until it completes, a newer keyframe completes or a second passes. It asks again every RTT + 5 ms once something has arrived since its last request. Swift and the Rust mirror change together, with shared traces. The WiFi repair simulation at 10% loss each way now completes 397 of 399 keyframes instead of 148 of 390, and no freeze lasts a second (35 did, up to 3.4 s). A measurement test runs the same loss through the real encoder and transport.
Log pairing-off at info and collapse unchanged mDNS re-advertisements
Before reconnecting at a new address, the iPad now checks that it is the same PC. It sends a token-less IDENTIFY with a random nonce, and a PC on 0.3.5 or later answers with HMAC-SHA256 keyed with its pairing token over the nonce and a fixed context. Only a matching answer lets the normal HELLO2 go to the new address. A PC that doesn't answer, as before 0.3.5, means no move. The messages are versioned (0x1A, 0x1B) and older apps and PCs ignore them. A saved PC moves only if all of these hold: - the iPad holds a pairing for it; - its entry came from Bonjour's list (a new found mark; older, typed and scanned entries never move); - both the old and the new address are home network addresses. Bonjour services still resolving count when two PCs claim one name. Try again after a refused move goes back to the saved address. The new address becomes the target only once the PC is accepted. The old entry and pairing stay until the new pairing is saved and read back. The e2e now pairs for real at the Mac's home network address (read at run time) and moves the PC to another port. Test fixtures use documentation addresses.
Startup reopened to clients and then, in a second lock, dropped any queued goodbye as stale. A Stop between the two ended the session and queued a current goodbye, which startup then dropped: the iPad timed out instead of showing the card. Session::transport_resumed now retires a stale goodbye and reopens to clients in one call, under the lock the transport reads `running` with. stop_session queues its goodbye and clears `running` under that same lock. A transport that starts after a stop and sees the stream stopping keeps the goodbye and sends it on its way out. One that starts first leaves later goodbyes alone.
… note to its PC, and wait out a keyframe still arriving
Review fixes:
- A waiting connection now carries whether iPadOS called its path
unsatisfied with no usable network. Only that is the new wifiOff
problem ("Wi-Fi is off", failure=no_network). Every other path
failure, no route to the PC or its network included, keeps the card
and failure=other it had before.
- The Apple Devices note is kept with the PC's target and the time it
was seen, dropped when an attempt goes to another PC, and stops
applying after 30 minutes. The card gives it as what happened last
time and what to try if the cable doesn't connect.
- The no-first-frame watchdog stays quiet while a frame has been
arriving in order for over 100 ms and still is, as a large keyframe
does over the cable.
A held keyframe asked for its missing fragments at each hold expiry as well as on its retry timer, and the hold neither moved the timer nor waited for traffic. At 1 ms RTT the same fragment went out 1 ms after a retry, inside the PC's 5 ms resend limit; at 200 ms RTT the holds asked at 26 and 51 ms, before any repair could return. A keyframe's hold now only moves its deadline. Every repeated request goes through one retry: due RTT + 5 ms after the last one, and after the first only once something arrived since the last request, on a later tick. Shared traces cover a hold and retry on the same tick, silence, and 1 ms and 200 ms RTT.
…loop Windows' input calls wait while it reconfigures displays. On a test PC, every virtual display change held SendInput for 50-130 ms and pen injection for about 40 ms, on the transport loop. In the field one such wait lasted about 53 s: the iPad heard nothing, new HELLOs went unanswered, and the stopped stream kept its port from the next one. - The transport queues input; an input thread owns the relay and hands it to Windows in order. It also sends pen frames held back for their spacing and refreshes a still contact, which the loop did before. - The queue is bounded. While Windows takes nothing, a pointer or Pencil move replaces the move queued just before it. Input that waited over a second lands only as a release of what the PC holds, as late input from the iPad does. Ends, key releases and resets are never dropped. - A stopped stream drops its handle at once. The thread then releases what the PC holds whenever Windows answers. - A Windows input call over 250 ms is logged, and so is a backlog once it clears. - input_soak, a fake iPad that streams harmless input while connecting and leaving, reproduces the waits on a PC set to Second screen.
Review fix: the 30-minute limit was only checked when SwiftUI redrew the USB card, so an idle Connect screen kept the hint. The sighting now carries its expiry, a cancellable task clears that sighting and publishes the change when it comes, and coming back to the foreground checks it against the clock again. The clock and the lifetime can be injected for tests.
Show the mirror before the moved-screen test's second screen attaches
A driver update or reset, the case this window recovery is for, can give an adapter a new LUID. The adapter that drew was then no longer found, so the choice fell back to any adapter, and adapters left out came back in. Now an adapter that isn't listed as it was is found as the only listed one that differs in the LUID alone; two such are no match. The Vulkan LUID is read only when the device, not just the instance, is Vulkan 1.1+.
…at/ipad-drawn-cursor # Conflicts: # host/src/transport/mod.rs
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.
Four more cases now count as Unknown, where the host can't be sure what the running driver read. - The driver reads its settings folder from VDDPATH under its registry key. Unless that value is unset or names C:\VirtualDisplayDriver (ignoring case and a trailing backslash), the file this host writes isn't the driver's, and the cursor is Unknown. - Registry reads count as "no override" only when they find no key or no value. Any other error means Unknown. - The driver's state before an Enable is read as running, off, or unreadable. A record is kept only for a driver seen off before the Enable and running after it. - Software now comes only from a file that a driver which reads <cursor> started with. An older or unreadable installer version, which may not be the running driver's, is Unknown. - A character or entity reference inside a cursor setting is Unknown: XmlLite expands it into the text, while quick-xml reports it apart.
Keep the window and the stream running when the GPU device is lost
… a slow driver Third review of #206 found five more gaps; this closes them. - The watcher counted a minimized window as stuck: eframe runs no UI pass for one, so the renderer never saw it minimized, and a healthy host got the driver notice 4 s after Win+D. The watcher now checks the window live (IsIconic, or the move/size and menu loops Windows runs while the title bar is held), and counts the 4 s from the latest of its start, the last UI pass and the last time it saw the window held. - Confirming the window could still pass with nothing presented: after a failed surface recreation egui-wgpu's paints return early with no surface error. The renderer now asks for a screenshot once the window has drawn long enough, and confirms only when it arrives; egui-wgpu takes one only from a frame it acquired, drew and submitted. - A driver that always answers after 4 s never opened the window: each try was given up on and started again. The list and the probe are now one call each, waited for as long as they run; the notice goes up at their limit, and a late answer opens the window. - Tests whose steps answer use 60 s limits, and the watcher's panics reach the test. - The notice box closes on a thread of its own, so the window's thread never waits on it.
…x/gpu-detect-timebox-035 # Conflicts: # host/src/gui/renderer.rs
Give the startup and quit device read a 2 s limit
When the PC says it sent video and the iPad still has no picture, the iPad asks for smaller media datagrams (1,200, then 576 bytes, each with a fresh keyframe). If that doesn't help, it asks the PC to sign its other home network addresses with the pairing, confirms the same PC at one of them, and reconnects there. The address that worked is tried first next time. The size request is a field appended to KEYFRAME_REQUEST, and the frame count is appended to HEARTBEAT. Both are gated on FEATURE_MEDIA_SIZE and HOSTCAP_MEDIA_SIZE, so older PCs and iPads see the same bytes as before. A PC session that sent video and ended with the iPad's error goodbye without a counted frame now summarizes as no_video or no_frame instead of other.
The scan of root devices used to drop a device it couldn't locate, or
whose hardware IDs it couldn't read. A failed read could then leave no
device of the driver, which counted as off. The next successful read
would record a start for a driver that was already running.
- The scan now keeps every present root device as one of:
- not the driver's: its hardware IDs were read and don't name it, or
it has none (CR_NO_SUCH_VALUE);
- the driver's, with its status;
- unread.
- Any unread device, or a driver device whose status can't be read,
makes the state unreadable.
- A pure run_from classifies the scan, so a test can inject a failed
read: the state is unreadable and no record is kept.
A settings file saved in the system's ANSI code page, as Notepad does, was not UTF-8 or UTF-16, so the host kept the driver's old modes and skipped the graphics card pin. The driver reads the file with XmlLite, which takes UTF-8 and UTF-16 but no double-byte code page: declared, it reads nothing; undeclared or declared UTF-8, it stops at the first non-ASCII byte. The host now decodes such a file in the code page its declaration names, or in the system's ANSI code page, and writes it back as UTF-8 with a matching declaration, which the driver reads to the end. The original is kept as vdd_settings.xml.bak-<date> first. Bytes that are no character in that code page leave the file alone, as before.
Recover capture after sleep, lost displays and repeated access loss
- A remembered address gets HELLO2 and the token only after the PC there proves it holds the pairing, as a move with the same-PC checks. Without the proof, the entry is forgotten and the attempt goes to the address tapped. Entries are bound to a digest of the pairing token. - The other-address step only tries addresses on the iPad's own subnet, as a PC that Bonjour moved does. - Only the first two smaller sizes in a session skip the keyframe rate limit; later ones are ordinary keyframe requests. - The iPad's summary for a move after no picture says no_frame when some video arrived, as the PC's does.
…at/ipad-drawn-cursor # Conflicts: # host/src/capture/dxgi.rs # host/src/capture/mod.rs
When the pointer's answer changes, the cached frame is rebuilt from the staging texture. If that copy hit ACCESS_LOST, the error ended the capture loop. The stage then opened capture afresh after at least 500 ms, which dropped the cached picture and skipped the quick re-creation of the duplication. - ACCESS_LOST from the copy now goes to the loop's existing handler, as one from ReleaseFrame or the readback does. The cache is kept. - While a loss is pending, the rebuild waits until the handler has run. - Any other error, such as DEVICE_REMOVED, still ends the loop, so the pipeline re-creates the device. - The decision is redraw_cached, generic over the error, so a test can inject the ACCESS_LOST.
…x/no-video-path-fallback # Conflicts: # ios/EternalMonitor/Network/WireProtocol.swift
- Before the identity check, a remembered address must be on this iPad's own subnet, as the other-address step requires; otherwise the entry is forgotten and the attempt goes to the address tapped. - When the attempt at a remembered address fails after its proof (no HELLO2 answer, no picture before the timeout, or another PC by name or pairing), the entry is forgotten and a new attempt goes to the address tapped. - Try again and a resume go to the address tapped while the remembered one is checked. - A golden vector covers a heartbeat that carries both the no-display notice and the frame count.
…in the remembered-address test
…x/gpu-detect-timebox-035 # Conflicts: # host/src/gui/mod.rs # host/src/main.rs # host/src/stall.rs
A settings file read from a code page is now written back as UTF-8 even when its graphics card stays, so the driver reads all of it. UTF-8 with stray bytes is no longer read in the system's code page when it starts with a byte order mark, or, in a single-byte code page, when it still holds UTF-8 beyond ASCII; its UTF-8 text would be garbled. A second conversion the same day keeps its original as vdd_settings.xml.bak-<date>-<n>. Each conversion is logged once. Tests cover the system code page through apply_pin and write_with_pin.
Fourth review of #206 found three more gaps; this closes them. - A window whose paints return without a surface keeps running UI passes, so the watcher's 4 s without a pass never came, and its screenshots were asked for again and again with no notice. The watcher now also raises the notice when the window has run UI passes for 12 s without being confirmed drawing (a working one is at about 3.5 s). Both bounds count from the latest of the start and the last time the window was held, so a minimized or dragged window suspends them. - Any screenshot marked as the proof was accepted, also a late one from before a surface error or a new device. Each ask now carries its span (a new one after each surface error, skipped frame or device) and a request id, and only an answer to the current span's asks confirms. - The watcher's decisions are a step function the tests call with each state in turn, instead of a thread they hoped had looked.
Translate the no-display notice
The mixed UTF-8 cases are refused only in a single-byte system code page, so the file-level cases now run only there and both byte orders are tested in code page 1252 directly. The backup test passes its date instead of reading the clock.
Draw the pointer on the iPad, apart from the video
…x/no-video-path-fallback # Conflicts: # ios/EternalMonitor/App/ConnectionManager.swift # ios/EternalMonitor/Network/WireProtocol.swift # ios/EternalMonitorTests/WireProtocolGoldenTests.swift # proto/src/v2/control.rs # proto/tests/golden.rs
Use smaller packets or the PC's other address when no video arrives
Read vdd_settings.xml saved in a Windows code page
Keep a stuck graphics driver from holding the window at start
This branch has not been deployed
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.
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: