diff --git a/README.md b/README.md
index d0747f5..1a69813 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,32 @@
+
+
# Display Share
-**Use a Windows laptop as a genuine extended second display for a Mac.**
+**Turn a Windows laptop into a real second display for your Mac.**
+
+[](https://github.com/nbkdoesntknowcoding/display-share/actions/workflows/build.yml)
+[](https://github.com/nbkdoesntknowcoding/display-share/releases/latest)
+[](LICENSE)
+[](#requirements)
+[](#requirements)
+
+
A laptop's HDMI port is output-only, so no cable can do this. Display Share
creates a **real virtual display** on the Mac — macOS believes a monitor is
-attached, and you can drag windows onto it, set its resolution, and arrange it in
-System Settings. It then captures that display, encodes it in hardware, and
-streams it over your LAN to a receiver app on the laptop.
+attached, so you can drag windows onto it, set its resolution, and arrange it in
+System Settings. It captures that display, encodes it in hardware, and streams it
+to a receiver app on the laptop.
+
+Not a mirror. Not a remote desktop. A second desktop.
-> **Status: working end to end.** Verified on 18 Aug 2026: a Mac mini streaming
-> to a Windows laptop over the LAN, video confirmed on real Windows hardware.
-> Phases 0–7 of the build plan are implemented. Remaining gaps are listed under
-> [What isn't proven yet](#what-isnt-proven-yet).
+
+
+What the receiver shows: a real macOS desktop, captured and streamed as H.264.
+The menu bar is the giveaway — macOS is drawing a whole second screen, not copying the first.
```
- Mac mini Windows laptop
+ Mac Windows laptop
┌───────────────────────┐ ┌──────────────────────┐
│ CGVirtualDisplay │ │ Tauri receiver │
│ ↓ ScreenCaptureKit │ H.264 over │ ↓ WebCodecs │
@@ -23,12 +35,66 @@ streams it over your LAN to a receiver app on the laptop.
└───────────────────────┘ └──────────────────────┘
```
+```bash
+git clone https://github.com/nbkdoesntknowcoding/display-share.git
+cd display-share && ./install.sh
+```
+
+Then grant Screen Recording when asked, click **Start** in the menu bar, and run
+the receiver on the laptop. It finds the Mac on its own.
+
+---
+
+## Contents
+
+- [How it compares](#how-it-compares)
+- [Two apps, and which one you need](#two-apps-and-which-one-you-need)
+- [Installing](#installing)
+- [Using it](#using-it)
+- [Using a cable instead of Wi-Fi](#using-a-cable-instead-of-wi-fi)
+- [Requirements](#requirements)
+- [Known limits](#known-limits)
+- [What we measured](#what-we-measured)
+- [Troubleshooting](#troubleshooting)
+- [How it works](#how-it-works)
+- [Contributing](#contributing)
+- [What isn't proven yet](#what-isnt-proven-yet)
+- [Licence and credits](#licence-and-credits)
+
+---
+
+## How it compares
+
+There are several ways to get a second screen, and they are not the same thing.
+What matters most is whether the OS believes a display exists — that is the
+difference between dragging a window onto it and watching a copy of a screen you
+already have.
+
+| | Extends the desktop | Runs on | Open source | Needs |
+|---|---|---|---|---|
+| **Display Share** | **Yes** — a real display | Mac → Windows / any browser | **Yes**, GPL-3.0 | Nothing but the two machines |
+| Apple Sidecar | Yes | Mac → iPad | No | An iPad |
+| Duet Display | Yes | Mac → iPad, Android, PC | No | Paid subscription |
+| Luna Display | Yes | Mac → iPad, Mac | No | A hardware dongle |
+| Deskreen | No — mirrors only | Any → browser | Yes, AGPL-3.0 | — |
+| spacedesk | Yes | **Windows** → other devices | No | A display driver, Windows as the source |
+
+The direction matters as much as the feature. Most of this category points *away*
+from a Mac and towards a tablet, or points away from Windows. Display Share
+points a Mac at the Windows laptop you already own, which is the one combination
+that tends to be sitting unused on the same desk.
+
+**Where the others are the better choice:** Sidecar is free, first-party, and
+supports Apple Pencil — if you have an iPad, use it. spacedesk is the right tool
+when Windows is the machine with the screens to give away. Deskreen is excellent
+at what it does, which is showing one screen on many devices.
+
---
## Two apps, and which one you need
-Display Share is a pair. Installing or opening the wrong half is the most
-likely way to get confused, so:
+Display Share is a pair. Opening the wrong half is the most likely way to get
+confused, so:
| | Runs on | What it looks like | You want it if |
|---|---|---|---|
@@ -62,9 +128,37 @@ for malicious software", no trip through Privacy & Security to click *Open
Anyway*. Downloading the `.dmg` is the route that triggers that warning, because
Display Share is open source and does not buy code signing certificates.
-### Or ask your coding agent
+### Or download a build
-Paste this into Claude Code, Cursor, or any agent with shell access:
+[Latest release](https://github.com/nbkdoesntknowcoding/display-share/releases/latest)
+— universal `.dmg` for the Mac, NSIS `.exe` for Windows, with `SHA256SUMS`.
+
+These are **unsigned**, so both systems warn on first launch:
+
+* **macOS** — open it once, then System Settings → Privacy & Security → **Open Anyway**
+* **Windows** — **More info** → **Run anyway**
+
+[docs/distribution.md](docs/distribution.md) explains why, and what each warning
+actually means.
+
+### Manual build
+
+```bash
+# Mac sender
+brew install xcodegen
+cd mac && xcodegen generate
+xcodebuild -scheme DisplayShare -configuration Release -derivedDataPath ./.build build
+
+# Windows receiver (needs Rust + Node 22)
+cd windows && npm ci && npx tauri build
+```
+
+
+Or hand it to a coding agent — a prompt that knows where the human steps are
+
+Paste this into Claude Code, Cursor, or any agent with shell access. The
+important part is that it stops before the permission grant, because no script
+can grant macOS permissions:
````text
Set up Display Share on this machine. It turns a Windows laptop into a real
@@ -95,7 +189,7 @@ VERIFY (after I confirm):
a HUD. It stays black until something is actually on that display — drag a
window onto the new display, then the HUD should show ~58 fps.
A near-black screen with capture near 0 fps is CORRECT for an empty desktop
- (a few fps still trickle in from the menu bar clock) — that is not a bug.
+ — that is not a bug.
WINDOWS RECEIVER (run this part on the Windows laptop):
7. Install Rust (https://rustup.rs) and Node 22+.
@@ -107,8 +201,8 @@ WINDOWS RECEIVER (run this part on the Windows laptop):
USEFUL TO KNOW:
- Ports 8787 (viewer page) and 8788 (video + control) must not be blocked.
-- Keys in the receiver: F11 fullscreen, F8 forward input to the Mac, H toggle
- HUD, A cycle decode mode.
+- Keys in the receiver: F fullscreen, H toggle HUD, K force a keyframe,
+ A cycle decode mode, F8 forward input to the Mac.
- Read README.md "Known limits" before reporting a bug — the 60 Hz cap, the
~1920x1200 geometry ceiling and no-HDCP are properties of Apple's private
API, not defects.
@@ -116,66 +210,78 @@ USEFUL TO KNOW:
rather than guessing.
````
-### Or download a build
+
-[Latest release](https://github.com/nbkdoesntknowcoding/display-share/releases/latest)
-— universal `.dmg` for the Mac, NSIS `.exe` for Windows, with `SHA256SUMS`.
+---
-These are **unsigned**, so both systems warn on first launch:
+## Using it
-* **macOS** — open it once, then System Settings → Privacy & Security → **Open Anyway**
-* **Windows** — **More info** → **Run anyway**
+### Connecting, once
-[docs/distribution.md](docs/distribution.md) explains why, and what each warning
-actually means.
+**On the Mac**
-### Manual build
+1. Launch **Display Share**. It lives in the menu bar — no Dock icon and no
+ window, so look at the top-right of the screen. First run explains the one
+ permission it needs and detects the grant without a restart.
+2. Click **Start**. A second display appears immediately: open System Settings →
+ Displays and it is there, arrangeable like any monitor.
-```bash
-# Mac sender
-brew install xcodegen
-cd mac && xcodegen generate
-xcodebuild -scheme DisplayShare -configuration Release -derivedDataPath ./.build build
+**On the laptop**
-# Windows receiver (needs Rust + Node 22)
-cd windows && npm ci && npx tauri build
-```
+3. Launch the receiver. It finds the Mac over Bonjour, so there is no address to
+ type — pick it from the list. If your network blocks mDNS (guest and
+ AP-isolated Wi-Fi usually do), type the Mac's IP instead.
+4. Enter the 4-digit PIN the Mac is showing. Once per device, then that laptop
+ goes straight through every time.
----
+Press **F** for fullscreen, then drag windows onto the new display from the Mac.
+Nothing else needs configuring — resolution follows the laptop's own panel as
+soon as it connects.
-## Using it
+> If the receiver says **"busy — another receiver is already connected"**, one is
+> already attached. Only one at a time, by design.
-1. Launch **Display Share** on the Mac (`./install.sh` puts it in
- `/Applications` and launches it). First run explains the one permission it
- needs and detects the grant without a restart.
-2. Click **Start** in the menu bar.
-3. Launch the receiver on the laptop. It finds the Mac over Bonjour — no IP
- address to type.
-4. Enter the 4-digit PIN shown on the Mac. This happens once per device.
-5. Press **F11** for fullscreen. Drag windows onto the new display.
+### Keys in the receiver
-**Two directions of control, and they are not the same thing:**
+| Key | Does |
+|---|---|
+| `F` | Fullscreen |
+| `H` | Show or hide the HUD |
+| `K` | Force a fresh keyframe |
+| `A` | Cycle hardware / software decode |
+| `F8` | Forward this laptop's mouse and keyboard to the Mac |
+
+While input forwarding is on, every other key belongs to the Mac — only `F8`
+stays local, so there is always a way back.
+
+### Two directions of control, and they are not the same thing
* **Your Mac's own mouse and keyboard already work on the second screen** — it is
a real display, so the cursor walks onto it exactly like a physical monitor.
Nothing to enable.
-* **To drive the Mac from the laptop's keyboard and trackpad**, press **F8** on
- the receiver. A badge shows while it is live; F8 again releases it. This needs
- **Accessibility** permission on the Mac — without it macOS silently discards
- injected events, so the app reports `input_unavailable` rather than pretending
- to work.
+* **To drive the Mac from the laptop**, press **F8** on the receiver. A badge
+ shows while it is live. This needs **Accessibility** permission on the Mac —
+ without it macOS silently discards injected events, so the app reports
+ `input_unavailable` rather than pretending to work.
* **Push the cursor past the edge** of the second screen and it keeps going onto
the rest of the Mac's desktop. The receiver takes a pointer lock and switches
- to relative motion at that point, because the OS clamps the real pointer at the
- screen edge and an absolute position simply pins at the boundary. Move back
- onto the second screen and control returns automatically. F8, or Esc, hands the
- pointer back at any time.
+ to relative motion there, because the OS clamps the real pointer at the screen
+ edge and an absolute position simply pins at the boundary. Move back and
+ control returns automatically. `F8` or `Esc` hands the pointer back at any
+ time.
+
+### When Netflix or Prime Video refuses to play
-Controlling *Windows applications* from the Mac is **not** in this version. It is
-planned as Phase 8: capturing the real Windows desktop and viewing it on the Mac,
-which needs no display driver and no certificate. The mirror image — making the
-Mac a second display *for* Windows — would need a signed Windows Indirect Display
-Driver, so it is deliberately not planned.
+Protected video checks **every display attached to your Mac**, not just the one
+it is playing on. A virtual display cannot carry the copy protection those
+services require, so they refuse to play *anywhere* while it exists — including
+on the Mac's own built-in screen. Apple's Sidecar behaves the same way, for the
+same reason.
+
+There is no fix, only a choice, so the app makes it an explicit one: the popover
+has a **Release the screen** control that takes the display out of the topology.
+Protected video plays again immediately, your windows move back to the Mac, and
+your laptop stays paired so bringing it back is one click.
### Viewing Windows from the Mac
@@ -187,84 +293,64 @@ on each machine does both; you pick a direction rather than reinstalling.
2. **Mac** — menu bar icon → **View a Windows PC…**. It finds the PC on its own;
if mDNS is blocked, type the address and port `7879`.
-**Only one direction runs at a time.** Both apps refuse the second one rather
-than trusting you not to try it: two machines each capturing and encoding the
-other feeds each screen back into the other, saturating the link, and the
-adaptive bitrate controller assumes a single stream — so it would react to
-congestion it was itself creating.
-
-### Controlling Windows from the Mac
-
-Click **Control this PC** in the viewer window. Your mouse and keyboard then
-drive Windows while the window has focus; click it again to stop. A green badge
-shows while it is on, because forwarding the keyboard is a strong capability and
-should never be ambiguous.
+**Only one direction runs at a time.** Both apps refuse the second rather than
+trusting you not to try it: two machines each capturing and encoding the other
+feeds each screen back into the other, saturating the link — and the adaptive
+bitrate controller would then be reacting to congestion it was itself creating.
-Windows needs no permission for this — there is no equivalent of macOS's
-Accessibility prompt. It does refuse to inject into **windows running as
-administrator**: Task Manager, an elevated PowerShell, and the UAC prompt itself
-will ignore the Mac's keyboard while everything else keeps working. That is a
-deliberate Windows security boundary, not a fault in Display Share. If you need
-it, run the receiver as administrator too.
+Click **Control this PC** in the viewer to drive Windows from the Mac. Windows
+needs no permission for this, but it does refuse to inject into **windows running
+as administrator** — Task Manager, an elevated PowerShell, and the UAC prompt
+itself will ignore the Mac's keyboard while everything else works. That is a
+Windows security boundary, not a fault here. Run the receiver as administrator if
+you need it.
Keys are sent by physical position rather than by the character they produce, so
a UK Mac driving a US Windows machine types what you actually pressed.
### Turning the mirror into a real extra desktop
-By default this duplicates the Windows screen, so the Mac shows whatever the
-Windows monitor shows. Windows cannot be given a *virtual* display without an
-Indirect Display Driver, and that needs a signed driver — see above.
+By default the reverse direction duplicates the Windows screen. Windows cannot be
+given a *virtual* display without a signed Indirect Display Driver, which this
+project deliberately does not ship.
-A **dummy display adapter** (an HDMI or DisplayPort plug that reports a monitor,
-roughly the price of a coffee) sidesteps this entirely. Windows genuinely
-extends onto it, and the display picker next to the share button lets you share
-*that* output instead. The Windows laptop keeps its own screen, and the Mac shows
-a separate desktop. No driver, no certificate.
+A **dummy display adapter** — an HDMI or DisplayPort plug that reports a monitor,
+roughly the price of a coffee — sidesteps it entirely. Windows genuinely extends
+onto it, and the display picker next to the share button lets you share *that*
+output. No driver, no certificate.
-> Unverified: this has not been tried with a real adapter yet. The code path is
-> exercised, the hardware is not.
+> Unverified: the code path is exercised, the hardware is not.
---
## Using a cable instead of Wi-Fi
Wi-Fi is usually the largest source of lag, and not because of bandwidth — a
-1080p stream needs roughly 10-15 Mbps, which any modern link manages. It is
+1080p stream needs roughly 10–15 Mbps, which any modern link manages. It is
**jitter**: frames arrive in clumps, and a clump is felt as a stutter even when
the average frame rate looks perfect. A cable removes it, and costs nothing in
sharpness or frame rate.
-The apps need no configuration for this. Display Share runs over whatever IP link
-exists, and both ends advertise and browse on every interface, so plugging in a
-cable is the entire procedure.
-
-**The HUD names the link it is actually using** — `Ethernet`, `Wi-Fi`, and
-`direct` when the two machines are wired straight to each other. If the link is
-wireless and measurably costing time, the receiver says so once, with the number.
-
-### Which cable
+The apps need no configuration for this. Both ends advertise and browse on every
+interface, so plugging in a cable is the entire procedure. **The HUD names the
+link it is actually using** — `Ethernet`, `Wi-Fi`, and `direct` when the two
+machines are wired straight to each other.
| Setup | Works | Notes |
|---|---|---|
-| Ethernet, both into the router | Yes | Simplest. Removes Wi-Fi from both ends. |
-| Ethernet, machine to machine | Yes | Lowest latency. Modern ports auto-negotiate, so no crossover cable is needed. |
-| Thunderbolt / USB4, machine to machine | Yes, if **both** ends support it | macOS calls this Thunderbolt Bridge. Very fast. |
-| A plain USB-C cable | **No** | USB-C is a connector, not a network. Without Thunderbolt on both ends it carries no IP at all. |
+| Ethernet, both into the router | Yes | Simplest. Removes Wi-Fi from both ends |
+| Ethernet, machine to machine | Yes | Lowest latency. Modern ports auto-negotiate, so no crossover cable |
+| Thunderbolt / USB4, machine to machine | Yes, if **both** ends support it | macOS calls this Thunderbolt Bridge. Very fast |
+| A plain USB-C cable | **No** | USB-C is a connector, not a network. Without Thunderbolt on both ends it carries no IP at all |
-> **Check before buying anything.** On Windows, look for a *Thunderbolt* controller
-> in Device Manager. Many laptops have USB-C ports that do power and DisplayPort
-> but not Thunderbolt, and those cannot bridge. If yours is one of them, two
-> USB-C-to-Ethernet adapters and a cable achieve the same result for very little.
-
-### Wiring the two machines directly
+> **Check before buying anything.** On Windows, look for a *Thunderbolt*
+> controller in Device Manager. Many laptops have USB-C ports that do power and
+> DisplayPort but not Thunderbolt, and those cannot bridge. Two USB-C-to-Ethernet
+> adapters and a cable achieve the same result for very little.
With no router in between, neither machine gets an address from DHCP, so both
-self-assign one in `169.254.x.x`. This is normal and needs no setup — discovery
-works over it, and the HUD shows `direct` when it happens.
-
-The Mac keeps its normal Wi-Fi connection at the same time, so the internet
-carries on working; the cable is used only for the machine-to-machine traffic.
+self-assign one in `169.254.x.x`. That is normal and needs no setup. The Mac
+keeps its Wi-Fi connection at the same time, so the internet carries on working.
---
@@ -276,6 +362,7 @@ carries on working; the cable is used only for the machine-to-machine traffic.
| Receiver | Windows 10/11. Any device with a modern browser also works for testing |
| Network | **5 GHz Wi-Fi or Ethernet.** 2.4 GHz jitter is visible as stutter |
| Permissions | Screen Recording (required), Accessibility (only for remote control) |
+| Ports | 8787 viewer page, 8788 video and control, 7879 for the reverse direction |
---
@@ -288,35 +375,69 @@ These come from the private `CGVirtualDisplay` API and are **not** going away:
| **60 Hz ceiling** | Every mode the virtual display advertises is 60 Hz. Requesting 120 yields ~64 fps because the surface does not update faster |
| **~1920×1200 maximum** | Larger geometries are adopted *wrongly* and silently: 2560×1080 becomes 1280×540, 2560×1440 falls back to 1920×1080. Display Share fits your panel's **aspect ratio** inside the reliable envelope instead — a 2560×1080 panel gets 1920×810, which fills it exactly with no letterboxing |
| **SDR only** | No HDR |
-| **No HDCP** | DRM-protected video (Netflix, Apple TV+) will not play on the virtual display |
+| **No HDCP** | Protected video will not play *on any display* while the virtual one exists. See [above](#when-netflix-or-prime-video-refuses-to-play) |
| **Not on the Mac App Store** | `CGVirtualDisplay` is a private API; App Review rejects private API use outright |
| **Private API risk** | A future macOS release could remove or change `CGVirtualDisplay`. Verified working on macOS 26.2 (25C56) |
---
-## Performance
+## What we measured
+
+Findings from building this, kept here because they cost real time to discover
+and are useful to anyone working in the same area.
+
+**Chrome's hardware H.264 decoder carries a ~69 ms pipeline — 22× worse than
+software decoding the identical stream (~3 ms).** Hardware decode is optimised
+for throughput on long video, not for latency on a live one. The receiver
+defaults to software decode because of it. Press `A` to cycle and measure it on
+your own hardware.
+
+**`CGVirtualDisplay` accepts geometries it then silently gets wrong.** Requests
+above roughly 1920×1200 are sometimes halved and sometimes replaced with
+1920×1080, and the API reports success either way. The only way to know what you
+got is to read it back.
+
+**Protected playback evaluates the entire output topology, not the display in
+use.** One output that cannot carry HDCP refuses playback on all of them. This is
+why Sidecar has the same behaviour, and why filtering what you capture cannot
+help — the trigger is the display existing.
+
+**`kVTVideoEncoderSpecification_EnableLowLatencyRateControl` is an encoder
+*specification*, not a property.** It has to be passed to
+`VTCompressionSessionCreate`; setting it afterwards with `VTSessionSetProperty`
+does nothing, silently. Every other knob on that encoder is a property, which is
+what makes it easy to miss. Read the encoder identifier back to confirm you got
+`…h264.rtvc` rather than `…ave.avc`.
+
+**Network.framework's `.idempotent` send completion installs no handler at all.**
+It means "safe to resend", not "tell me when it is done" — so an encoder using it
+has no back-pressure signal and will run ahead of the socket without limit.
+`.contentProcessed` is the documented mechanism.
+
+**An idle desktop sends nothing, and that breaks things that assume otherwise.**
+ScreenCaptureKit marks frames as carrying no new pixels rather than resending an
+unchanged surface. Anything measuring health by encoded frames — a bitrate
+controller, a watchdog — will read a still desktop as a failing link. Both
+mistakes were shipped here before they were caught.
+
+### Performance
Measured on a Mac mini M4, macOS 26.2, at 1080p60 over **localhost** — so these
exclude LAN transit and are a floor, not a promise:
| | Value |
|---|---|
-| Capture | 57.8 fps (against a 60 fps target) |
+| Capture | 57.8 fps against a 60 fps target |
| H.264 encode | 5.62 ms/frame |
-| Bandwidth | ~1.4 Mbps synthetic content; ~20 Mbps for the MJPEG fallback |
+| Bandwidth | ~1.4 Mbps synthetic content |
| Capture cost | ~1.4% of one core |
| Decode (software) | ~3 ms |
-**On latency, deliberately not a headline number.** One-way sender→paint measured
-**3.1 ms median** in Chromium with software decode on the same machine. That
-figure excludes network transit entirely and was measured in a browser, not in
-the shipping receiver. Real end-to-end latency on a LAN will be higher, and has
-not been measured on real hardware yet.
-
-One finding worth knowing: Chrome's **hardware** H.264 decoder carries a ~69 ms
-pipeline, **22× worse** than software decoding the identical stream. The receiver
-defaults to software decode because of this. Press **A** on the receiver to cycle
-the setting and measure it on your own hardware.
+**On end-to-end latency, deliberately no headline number.** The receiver's HUD
+reports each stage separately — network hand-off, decode, paint, and the wait for
+the compositor — and the honest position is that nobody has yet recorded those on
+real hardware over a real LAN. If you run it, the numbers are on screen under
+`H`, and a report of them is a genuinely useful contribution.
---
@@ -327,43 +448,21 @@ the setting and measure it on your own hardware.
| "Cannot be opened because Apple cannot check it" | Unsigned build | System Settings → Privacy & Security → **Open Anyway**. See [docs/distribution.md](docs/distribution.md) |
| "Windows protected your PC" | Unsigned installer | **More info** → **Run anyway** |
| Menu bar says *Screen Recording permission has not been granted* | macOS TCC | Grant it, then reopen the menu. A freshly built copy is a new identity and needs granting again |
-| **Settings shows Display Share enabled, but the app still asks for permission** | The listed entry belongs to an **older build**. macOS lists apps by name, so a stale entry is indistinguishable from a live one | `install.sh` now signs with a stable local identity, so this should not recur. If you hit it on an older copy: `tccutil reset ScreenCapture in.theboringpeople.displayshare`, then grant again |
-| App says *Not granted* even though you just granted it | `CGPreflightScreenCaptureAccess()` caches its answer for the life of the process, so an app that was already running cannot see a new grant | The app now detects this via a short-lived child process and shows **"Granted — restart"**. Quit and reopen it |
+| **Settings shows Display Share enabled, but the app still asks** | The listed entry belongs to an **older build**. macOS lists apps by name, so a stale entry is indistinguishable from a live one | `install.sh` signs with a stable local identity, so this should not recur. On an older copy: `tccutil reset ScreenCapture in.theboringpeople.displayshare`, then grant again |
+| App says *Not granted* even though you just granted it | `CGPreflightScreenCaptureAccess()` caches its answer for the life of the process | The app detects this via a short-lived child process and shows **"Granted — restart"**. Quit and reopen |
| Receiver shows black, HUD says `capture 0.0 fps` | Nothing is on the virtual display | Drag a window onto it. An idle desktop legitimately sends no frames |
-| Receiver cannot find the Mac | mDNS blocked, or different subnets | Type the Mac's IP manually in the receiver. Guest and AP-isolated Wi-Fi block Bonjour |
-| `busy — another receiver is already connected` | One receiver at a time, by design | Close the other receiver, or wait ~10 s for the socket to drop |
+| Receiver cannot find the Mac | mDNS blocked, or different subnets | Type the Mac's IP manually. Guest and AP-isolated Wi-Fi block Bonjour |
+| `busy — another receiver is already connected` | One receiver at a time, by design | Close the other, or wait ~10 s for the socket to drop |
| Stutter, image goes soft under load | Adaptive bitrate reducing quality | Working as designed: sharpness degrades rather than latency accumulating. Move to 5 GHz or Ethernet |
-| Mouse and keyboard do nothing after pressing F8 | **Accessibility** not granted — it is a SEPARATE permission from Screen Recording, so video can work perfectly while input is blocked | Enable Display Share under Privacy & Security → Accessibility. To check what the app actually sees: `open -a /Applications/DisplayShare.app --args --check-permissions --out /tmp/p.txt && sleep 3 && cat /tmp/p.txt` |
+| Netflix or Prime Video will not play, even on the Mac's own screen | Protected video refuses while any non-HDCP display exists | Use **Release the screen** in the popover. [Why](#when-netflix-or-prime-video-refuses-to-play) |
+| Mouse and keyboard do nothing after `F8` | **Accessibility** not granted — a SEPARATE permission from Screen Recording, so video can work perfectly while input is blocked | Enable it under Privacy & Security → Accessibility. To see what the app sees: `open -a /Applications/DisplayShare.app --args --check-permissions --out /tmp/p.txt && sleep 3 && cat /tmp/p.txt` |
| Windows scattered after a crash | The display was destroyed | Relaunch within ~8 s and the helper re-attaches the *same* display, preserving arrangement |
| Second display gone after Mac sleep | Capture died on wake | Recovers automatically, typically under 0.1 s. If not, toggle Stop then Start |
-| Opened the app on the Mac and got "Enter the Mac's address and press Connect" | That is the **receiver**, not the sender | Close it. The sender is `DisplayShare.app` and appears only in the **menu bar** |
-| Launched the sender and nothing happened | It is a menu bar app — `LSUIElement`, so no Dock icon and no window by design | Look at the top-right menu bar for a display icon and click it |
-
----
-
-## Updates
-
-Both apps check GitHub Releases at launch. **Neither updates silently** — the
-Mac app links you to the release page, and the Windows app downloads only after
-you click *Update and restart*. For an unsigned app, a self-replacing binary is
-the wrong default. See [docs/distribution.md](docs/distribution.md#updates) for
-the rollback path.
+| Launched the sender and nothing happened | It is a menu bar app — `LSUIElement`, so no Dock icon and no window by design | Look at the top-right menu bar for a display icon |
---
-## Privacy
-
-Display Share captures **only the virtual display it creates**, never your real
-screen. macOS makes no such distinction, so the purple recording indicator
-appears exactly as it does for Zoom or OBS.
-
-Video never leaves your LAN — there is no server, no account, no telemetry. A
-receiver must pair with a 4-digit PIN before it gets any video, and input
-forwarding is refused entirely until it does.
-
----
-
-## Repository layout
+## How it works
```
install.sh one-command build + install for the Mac sender
@@ -372,7 +471,6 @@ mac/
DisplayShareCore/ capture, encode, transport, pairing, input
vd_helper/ subprocess that owns the CGVirtualDisplay
Shared/ wire protocol + helper IPC, compiled into both
- spike/ Phase 0 throwaway spike (vdspike)
dsprobe/ dev harness for capture/encode measurements
scripts/ acceptance tests + packaging
windows/
@@ -386,15 +484,25 @@ docs/ findings, distribution
process holding it, so isolating it means a crash in the capture or encode
pipeline does not destroy your window arrangement.
+The wire protocol is specified in [`protocol/SPEC.md`](protocol/SPEC.md), and the
+Swift and TypeScript parsers are tested independently against the same golden
+vectors — so a shared misunderstanding cannot pass.
+
---
-## Development
+## Contributing
+
+Pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers the
+licensing rules that matter here, and the fact that the Xcode project is
+**generated** from [`mac/project.yml`](mac/project.yml) — edit the YAML, not the
+`.xcodeproj`.
```bash
brew install xcodegen
cd mac && xcodegen generate
-xcodebuild -scheme DisplayShareCore -derivedDataPath ./.build test # 50 unit tests
+# 201 Swift tests
+xcodebuild -scheme DisplayShareCore -derivedDataPath ./.build test
./scripts/test-helper-lifecycle.sh # vd_helper lifecycle
python3 scripts/ws-acceptance.py # wire protocol over WebSocket
@@ -405,35 +513,58 @@ python3 scripts/input-acceptance.py # input forwarding + auth gate
python3 scripts/injection-acceptance.py # CGEvent injection vs the real cursor
cd ../windows && npm ci
-node scripts/verify-vectors.mjs # TS parser vs the same golden vectors
+cargo test --manifest-path src-tauri/Cargo.toml # 53 Rust tests
+node scripts/verify-vectors.mjs # TS parser vs golden vectors
+node --experimental-strip-types scripts/verify-timing.mjs
+node --experimental-strip-types scripts/verify-window-states.mjs
npx tauri dev
```
-The Xcode project is generated from [`mac/project.yml`](mac/project.yml) — edit
-the YAML, not the `.xcodeproj`. The wire protocol is specified in
-[`protocol/SPEC.md`](protocol/SPEC.md), and the Swift and TypeScript parsers are
-tested independently against the same golden vectors so a shared misunderstanding
-cannot pass.
+**The most useful contributions right now** are measurements rather than code —
+see [What isn't proven yet](#what-isnt-proven-yet). A HUD screenshot from a real
+Mac-to-Windows session over a real LAN would settle several open questions at
+once.
+
+Found a security issue? Please open a private advisory through GitHub's
+**Security** tab rather than a public issue.
---
## What isn't proven yet
-Stated plainly, because everything above was measured but these were not:
+Stated plainly, because everything above was measured and these were not:
-* **Latency on Windows is unmeasured.** Video is confirmed working there, but no
- one has recorded the numbers on WebView2 — press `H` for the HUD and `A` to
- compare software against hardware decode.
+* **End-to-end latency on real hardware is unmeasured.** Every figure here is
+ localhost. The per-stage instrumentation exists and reports on the HUD; nobody
+ has run it on a real LAN.
* **Cursor roaming past the screen edge is unverified.** The clamping is covered
- by tests, but the pointer-lock handoff has only been reasoned about, not used.
-* **Latency over a real LAN is unmeasured.** All figures above are localhost.
+ by tests, but the pointer-lock handoff has been reasoned about, not used.
* **No real sleep/wake cycle.** The recovery path was exercised through the same
entry point the wake notification calls, not by actually sleeping the Mac.
* **2.4 GHz congestion** was simulated through the control channel, not real RF.
+* **The dummy-adapter path** for extending Windows has not been tried with real
+ hardware.
+
+---
+
+## Privacy
+
+Display Share captures **only the virtual display it creates**, never your real
+screen. macOS makes no such distinction, so the purple recording indicator
+appears exactly as it does for Zoom or OBS.
+
+Video never leaves your LAN — no server, no account, no telemetry. A receiver
+must pair with a 4-digit PIN before it gets any video, and input forwarding is
+refused entirely until it does.
+
+Both apps check GitHub Releases at launch and **neither updates silently** — the
+Mac app links you to the release page, and the Windows app downloads only after
+you click *Update and restart*. For an unsigned app, a self-replacing binary is
+the wrong default.
---
-## Licence
+## Licence and credits
GPL-3.0. See [LICENSE](LICENSE).
diff --git a/docs/images/second-display.png b/docs/images/second-display.png
new file mode 100644
index 0000000..54b1387
Binary files /dev/null and b/docs/images/second-display.png differ