Repository navigation
Draw the pointer on the iPad, apart from the video - #192
Merged
Merged
Conversation
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
With the iPad drawing the pointer, pointer-only updates are released without a frame. A moving pointer keeps Desktop Duplication from timing out, so the idle keepalive and keyframe answers, which ran only on a timeout, stopped: on the reference PC the watchdog restarted the stream after 5 s of pointer motion. Pointer-only updates now run the same keepalive check as a timeout.
On the reference PC the last position of each burst of pointer updates went 12 to 25 ms after the burst, not within the 8 ms budget: Tokio's timer wakes on Windows' 15.6 ms tick. The budget's end now waits on the transport's high-resolution deadline thread, as pen spacing does. The 100 ms repeat stays on Tokio's timer.
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.
IDENTIFY and IDENTITY took packet types 0x1A and 0x1B, so the pointer messages move to 0x1C (POINTER_SHAPE) and 0x1D (POINTER_STATE); their golden vectors follow, and the base's vectors are unchanged. The transport loop marks a Pointer phase for its watchdog.
…the picture The virtual display driver leaves its hardware cursor off unless its settings file turns it on, and the file this host writes had no <cursor> block, so Windows drew the pointer into the second screen's image. The host now writes the driver's own cursor block (hardware cursor on, 128x128, alpha and XOR support), and the second screen sends the pointer apart whenever the file the driver reads enables it, as Mirror does. A user's own file without it keeps the pointer in the picture. On the iPad, a late chunk of an older pointer image no longer replaces the image held, and both pointer passes are clipped to the picture so a pointer past its edge stays out of Fit's bars.
…at/ipad-drawn-cursor
…at/ipad-drawn-cursor # Conflicts: # host/src/capture/synthetic.rs # ios/EternalMonitorTests/WireProtocolGoldenTests.swift # proto/testdata/v2_vectors.txt # proto/tests/golden.rs
… image chunks Pointer messages now wait until the iPad has answered, as audio does: until then nothing confirms it received its HELLO_ACK. Capture keeps the pointer in the video for it meanwhile, so it always has one. Over USB the tunnel refuses a datagram when its control queue (64) is full. An image of more chunks than that lost the rest and still counted as sent. The sender now keeps the image's next chunk, resumes there 5 ms after a refusal, and counts the image sent once every chunk went.
…or only when the driver read it When Windows draws the pointer into the picture (the second screen without the driver's hardware cursor, or an image larger than that cursor takes), capture no longer draws the pointer Desktop Duplication reports there, which stayed at one spot. The hardware cursor now counts only when an Enable started the driver from off with a settings file that turns it on, read as the driver reads it, from a driver that reads <cursor> (24.10.27 on) or one this host installed; a driver already on keeps what it read when it started. An iPad that draws the pointer gets it shown, and capture stops drawing it, once the iPad's report names the image it holds whole.
The pointer feed outlives a capture, and a second screen starts from a mirror of the main screen. A position from the output captured before must not carry over to the new one if Desktop Duplication reports no pointer on its first frame. Each capture now starts with the pointer hidden; Desktop Duplication reports the output's own pointer once it is on it.
…at/ipad-drawn-cursor # Conflicts: # host/src/transport/mod.rs
The host now records what each driver start gave the pointer, and capture
trusts that record only for the same start.
- An Enable that started the virtual display driver records it on disk,
in vdd-cursor.json in the app data folder:
- the boot id;
- the device's last arrival (DEVPKEY_Device_LastArrivalDate), which
changes at every driver start;
- the SHA-256 of the settings file it started with;
- the cursor that file gives it, read as the driver reads it.
- Each second screen capture compares the running driver's boot and
arrival with the record. A record of another start, or none, means
Unknown. This covers AlreadyDone, a relaunch with the driver left on,
and a restart by another program.
- With a hardware cursor, each pointer image is classed against the caps
that start used, such as the user's 64 or 256, not a fixed 128.
- Unknown is handled as in 0.3.4:
- capture draws the pointer Desktop Duplication reports;
- no image and no shown position go to the iPad;
- an iPad that was drawing the pointer before gets hidden positions.
- A registry override, a file the driver can't read as a full hardware
cursor, or a driver version that doesn't tell also count as Unknown.
- The pointer e2e now fails on a hidden state after the pointer went
apart, and on any pointer message to an older client.
…at/ipad-drawn-cursor
…at/ipad-drawn-cursor # Conflicts: # host/src/transport/mod.rs
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.
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.
…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.
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.
Fixes #183
What changes
The iPad draws the PC's pointer itself, from small messages sent apart from the video. On a still desktop, pointer motion no longer needs a video frame, in Mirror and on the Second screen.
Negotiation. The iPad sets HELLO2 feature bit 5 (
FEATURE_DRAWS_POINTER). The host answers with host capability bit 6 (HOSTCAP_POINTER). Only when both are set does the host leave the pointer out of the video and send it separately. Each side ignores the other's unknown bit, so an older iPad or an older host gets the previous behaviour. Pointer messages start once the iPad has answered, as audio does. The pointer stays in the video, and the positions sent say hidden, until the iPad's report names the pointer image it holds whole. Then capture stops drawing it, once per session.Wire. Two new control types, each with a body version byte, append-only like the rest:
POINTER_SHAPE(0x1C): the pointer image, in chunks of up to 1,358 bytes. It is premultiplied BGRA plus, when any pixel inverts the picture under it, a 1-bit invert mask. Each image has an id, a hotspot, and its original Windows type.POINTER_STATE(0x1D): position (top-left of the image, in video pixels, may be negative), visibility, image id, and the host's read time. The newest wins, bymsg_seq.RECEIVER_REPORTgains an appendedpointer_shape_id, naming the image the iPad holds complete. When a report sent at least 250 ms after an image went still names another id, the host sends the image again. The image also goes at every session start and IDR.Host.
Second screen.
<cursor>block, so Windows drew the pointer into the second screen's image.vdd-cursor.jsonin the app data folder:DEVPKEY_Device_LastArrivalDate). On the reference PC it changed at each driver start and stayed the same while the driver was off;trueturns a switch on, numbers asstd::stoireads them, and comments don't count. A character or entity reference inside a cursor setting makes it unknown.VDDPATHunder its registry key) is set to anything butC:\VirtualDisplayDriver, ignoring case and a trailing backslash;<cursor>) or doesn't read as a version. That version is the installer's, so it isn't taken to mean the running driver has no hardware cursor.iPad.
PointerOverlayreassembles images. It ignores chunks of an image older than the one held or assembling (comparison that survives the id wrapping), and keeps the newest position.MetalRendererdraws the image over the video in the same pass: alpha "over", then a second pass that inverts the picture where the mask is set (blend(1 - dst)·src + dst·(1 - src)). Both passes are clipped to the picture, so a pointer past its edge stays out of Fit's bars.ContentRectMapper, the same rectangle touches are normalized against, in Fit and Fill.Compatibility
HOSTCAP_POINTER; the iPad never draws)FEATURE_DRAWS_POINTER; no pointer messages sent)Measurements
Windows 11 reference PC with an RTX 5080, H.264 NVENC, 120 fps target.
Mirror, 3440x1440:
Second screen, the virtual display at 2732x2048, with the driver's hardware cursor in this build:
With a 160 px pointer on the second screen (larger than the hardware cursor takes), Windows drew it into the picture: about 114 frames/s while it moved, and every position sent said hidden.
Second screen with the driver-start record (this build, three cold starts, a client that draws the pointer):
App relaunch with the driver left on:
While only the pointer moves and the iPad draws it, the encoder takes only the idle keepalive (about 1.3 frames/s).
Tests
Wire (Rust and Swift, shared golden vectors): new vectors
hello2_draws_pointer,hello_ack_pointer,pointer_shape,pointer_state,pointer_hidden,receiver_report_pointer. Every truncation and the bad ids, sizes, lengths and versions are rejected on both sides. An older parser reads the extended report unchanged. The base's vectors are unchanged.Host unit tests:
client_draws_pointer.VDDPATHor the default folder in any case and with a trailing backslash; any other folder, a failed read, or any cursor value present or unreadable is unknown.std::stoinumbers, missing sizes, alpha and XOR levels, white space and comments; references inside a cursor setting are unknown.iPad unit tests:
ContentRectMapperand the video quad in Fit and Fill.ControlChannel.Host end to end (
e2e_synthetic, real pipeline over UDP, synthetic still picture with the pointer moving at 60 fps, 3 s): a client that draws the pointer got 181 shown pointer states and 3 video frames; hidden states came only before the first shown one (25), and any hidden state after it fails the test. An older client on the same host got 179 frames, and any pointer message to it fails the test.iPad end to end (
scripts/e2e_pointer.sh, Simulator, 10 s):EM_DRAWS_POINTER=0(as older apps)