Skip to content

Stream screens with an odd width or height - #180

Merged
whoisaldo merged 2 commits into
release/0.3.5-candidatefrom
fix/odd-capture-size
Nov 3, 2026
Merged

whoisaldo merged 2 commits into
release/0.3.5-candidatefrom
fix/odd-capture-size

Conversation

@whoisaldo

Copy link
Copy Markdown
Member

What

The Windows host now encodes a screen with an odd width or height at the even size below it, leaving out the odd last column or row. It crops and doesn't scale, so every other pixel stays exact.

Why

A user's PC with a 1680×945 display couldn't stream. The iPad connected, but no picture arrived and the PC showed "Streaming is off". Their graphics card has no encoder EternalMonitor can use, so the host fell back to libx264, which refuses odd sizes:

ERROR [libx264] height not divisible by 2 (1680x945)
ERROR Encoder failed repeatedly (6 restarts in 60s)

At 1366×768 it streamed. 4:2:0 needs even dimensions. Before this change, nothing made the encoder's size even.

How

  • encoder::encoded_size rounds the capture's width and height down to even numbers. EncoderState::new opens at that size and logs one info line when it crops (capture_size=1680x945 encoded_size=1680x944). The two fields are on the redaction allowlist and covered by its test.
  • EncoderState::convert now does all the conversion the encode loop used to do inline. It reads the top-left region of the capture at the capture's own row stride, on every input path: swscale to YUV420P/NV12 (convert_packed_bgra and its padding-free copy now take the row stride), Accelerate on macOS, and the BGRA copy NVENC reads.
  • accepts_frame compares encoded sizes, so an odd capture doesn't reopen the encoder on every frame.
  • PipelineStats::capture_resolution stores the encoded size, so the stream config the iPad gets, the GUI and the periodic stats summary all report what is actually streamed. session_summary carries no size.
  • Input: the iPad normalizes touches and Pencil over the picture it shows, and the host maps them onto the whole capture. With a cropped picture, the centre of each displayed pixel lands on that same desktop pixel, and the picture's far edge reaches the row or column that was left out, so it stays reachable. No iPad change was needed: it uses the stream config size only for display in Settings.
  • scripts/e2e_ios.sh expects the even decoded size when EM_SIZE is odd.

Testing

  • New encoder::tests::odd_screen_sizes_encode_at_the_even_size_below: opens libx264 at 1680×945, 1365×768 and 641×361, encodes 10 frames each, then decodes them at the even size and reads back the synthetic frame counter. On the base, libx264 fails to open at 1680×945 ("Generic error in an external library").
  • New encoder::tests::cropping_changes_no_pixel_of_the_picture: for 1365×945, 1365×944 and 1366×945, on YUV420P, NV12, BGRA and Accelerate, with and without padding after the last row, converting the odd capture gives byte-identical planes to converting its top-left region on its own. For BGRA it also matches the source pixels directly.
  • New input::tests::a_cropped_picture_maps_each_pixel_onto_itself: on a 1365×945 output, every displayed pixel centre maps onto itself, and the edge maps to the column and row left out.
  • New e2e_synthetic::an_odd_screen_streams_cropped_and_input_still_reaches_its_edge: the real supervisor with a 641×361 synthetic capture and libx264. Heartbeats report 640×360, decoded pictures are 640×360 with rising counters, a touch on the last displayed pixel lands there, and the far edge reaches pixel (640, 360). On the base it fails: libx264 can't open, and the encoder restarts six times.
  • iOS 18.6 simulator, EM_SIZE=641x361 scripts/e2e_ios.sh (Wi-Fi): 240 frames decoded at 640×360 and 60 fps, with no drops, and the pattern check passed. InputGestureTests/testVideoGesturesAndKeyboard against a 641×361 host: all five taps landed within one pixel of their targets on the 641×361 desktop.
  • cargo fmt --check, cargo clippy --workspace --all-targets -D warnings and cargo test --workspace pass on macOS.
  • Not run natively on Windows: the change touches only shared code, with no Windows-only lines. The Rust Windows CI job builds and tests it.

Release notes: Screens with an odd number of pixels, such as 1680×945, now stream instead of stopping.

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.
@whoisaldo whoisaldo added the 0.3.5 After 0.3.4 label Nov 3, 2026
@whoisaldo
whoisaldo marked this pull request as ready for review November 3, 2026 23:07
@whoisaldo
whoisaldo merged commit 477f3ad into release/0.3.5-candidate Nov 3, 2026
8 checks passed
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