feat(display): size virtual displays from the client's image area - #452
Draft
LucasBonafe wants to merge 2 commits into
Draft
feat(display): size virtual displays from the client's image area#452LucasBonafe wants to merge 2 commits into
LucasBonafe wants to merge 2 commits into
Conversation
The stream protocol carries pixels, never physical dimensions, so the host cannot tell a 2504-pixel-wide phone panel from a 2504-pixel-wide monitor. effective_virtual_display_scale_percent() therefore guessed a Windows scale from the short edge alone, and buttons, text, and the cursor landed at a different physical size on the client than they have on the host's monitor. Add dd_virtual_display_image_width_mm: the measured width, in millimetres, of the image area the client actually displays. When set, the virtual display advertises that size through its synthetic EDID and Windows derives DPI from it the same way it does for a real monitor. The Windows scale becomes a value derived from the measurement rather than the primary input, which is both more precise than a picked percentage and closer to how Windows expects to be told about a display. The width describes the image area, not the device. A letterboxed stream is smaller than the panel carrying it, so the height is derived from the width and the requested mode's pixel aspect instead of from the panel's shape. Both halves stay pure functions of the configuration. Reconnecting re-runs them against a display that already carries the previous answer, so neither may read back what it wrote last time; a covering test pins that down. An explicit dd_virtual_display_scale still wins, and 0 still leaves the Windows DPI setting untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LucasBonafe
force-pushed
the
feat/virtual-display-physical-image-size
branch
from
August 23, 2026 04:35
4662b75 to
96ca0e4
Compare
dd_virtual_display_image_width_mm asks for a number nobody knows offhand: the width, in millimetres, of the image area a client actually displays. Working it out means finding a datasheet and dividing, which is enough friction to leave the setting unused and the guess in place. Offer the measurements that have already been worked out. clientImageWidthPresets is a plain table of device, mode, and width; the settings schema hangs it off the field, and the three editors that render numeric fields - global settings, per-client overrides, per-app overrides - expose any field's suggestions through a datalist. The Alienware m16 R2 (345 mm at 2560x1600) and the inner panel of a Galaxy Z Fold8 Ultra (151 mm at 2504x2256) start the table, both from vendor figures the existing tests already assert against. The table carries no behaviour and binds nothing: it is a list of examples, the field still takes any width the host accepts, and a device missing from it is a convenience gap rather than a blocker. Suggestions are a generic field property rather than something keyed on this one setting, so the editors stay free of per-setting special cases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
The problem
The stream protocol carries pixels, never physical dimensions. I checked nvhttp
/launch, the SDPattributes in
src/rtsp.cpp, and moonlight-common-c: nothing anywhere carries a client's screen sizeor DPI. So the host genuinely cannot tell a 2504-pixel-wide phone panel from a 2504-pixel-wide desktop
monitor.
effective_virtual_display_scale_percent()deals with that by guessing from the short edge alone(
src/platform/windows/virtual_display_identity.cpp):Resolution is a poor proxy for density, and the existing tests already show the seam: 3440x1440 and
1440x2560 are very different displays and both resolve to 175%.
The user-visible result is that buttons, text, and the cursor come out a different physical size on the
client than they are on the host's own monitor. #386 is this, from the other end: a user picked 175% in
Vibepollo, Windows recommended 150% "on two different phones", and the two disagreed until they set
Windows by hand.
The approach
Rather than add another picked percentage, this takes the physical route the Apollo maintainer already
argued for in ClassicOldSong#290 (the issue ClassicOldSong#1013 was closed as a duplicate
of). Asked about specifying scaling as a percentage from the client, he answered:
and then:
He also noted that auto-detecting the client's physical size is not feasible and it has to be entered
manually — which matches what the protocol survey above found.
The host already synthesises an EDID physical size in
virtual_display_sunshine.cpp, but it derives themillimetres from the scale (
dpi = 96 * scale / 100, thenmm = px * 25.4 / dpi). This change letsthat run in the honest direction: measure the client once, put the real millimetres in the EDID, and let
Windows derive DPI from them the way it does for any real monitor. The Windows scale becomes a derived
value rather than the primary input.
The setting
One new option,
dd_virtual_display_image_width_mm: the width, in millimetres, of the image area theclient actually displays.
0disables it and nothing changes.It is deliberately the image area, not the device. On a letterboxed panel those differ — a 16:10
stream on the 8.0-inch inner panel of a Galaxy Z Fold8 Ultra occupies about 7.01 inches of it — so a
device diagonal would bake in an error. Height is derived from the width and the requested mode's pixel
aspect, so only one number is configured.
The calculation
Worked through on two measured clients:
345151Both sets of dimensions are from the vendors: the m16 R2 active area is Dell's own manual (188.65 PPI,
which the numbers reproduce exactly), and the Z Fold8 Ultra figures follow from Samsung's 8.0-inch inner
panel spec (421.3 PPI, likewise reproduced). The phone is the case that shows why this matters — the
resolution heuristic is off by 200 percentage points there.
Suggested widths
Nobody knows their client's image width offhand, and a number that needs a datasheet to find is a number
nobody sets. So the two measurements above are also offered in the UI.
src_assets/common/assets/web/configs/clientImageWidthPresets.tsis a plain table of device, mode, andwidth:
The settings schema hangs that table off the field as a generic
presetsproperty, and the threeeditors that render numeric fields - global settings, per-client overrides, per-app overrides - render
any field's suggestions through a native
<datalist>. Nothing is keyed on this particular setting, andno device name reaches the C++ at all: the host still only ever sees a number of millimetres.
The table binds nothing. It is a list of worked examples rather than a supported-device list, the field
still accepts any width the host accepts, and a device missing from it is a convenience gap rather than
a blocker. Adding one is a single row.
Idempotency
Reconnecting re-runs this against a display that already carries the previous answer, so both halves are
pure functions of the configuration and read nothing back:
nothing feeds the previous size back in.
set_display_scale_percent()re-queriesrecommended_indexand recomputesdesired_relativefrom scratch on every call, and short-circuitswhen
current_index == desired_index. The relative index never accumulates.MeasuredImageWidthResolvesIdempotentlycovers both, including feeding a derived scale back in as anexplicit setting.
Precedence
Unchanged for anyone not setting the new option, and the existing semantics still win:
dd_virtual_display_scalepercentage overrides the measurement;0("preserve Windows' choice") still leaves the Windows DPI setting alone;-1) recommendation is refined.The measured EDID size is applied regardless of the scale mode, since reporting a true physical size is
correct even when Vibepollo is not touching the DPI setting.
Scope
Kept small on purpose. No protocol extension, no client auto-detection, no new UI component — the
setting is a plain number field with a native list of suggested widths, and it reuses
effective_virtual_display_scale_percent()and theexisting
config_overridesallowlists rather than adding a parallel path, so it is alreadyper-client and per-app overridable. The scale-snapping helper is extracted once and shared by both the
heuristic and the derived path.
web-legacyis not wired up; it is excluded from packaging incmake/packaging/common.cmake.Testing — please read
Seven test cases were added to
tests/unit/platform/windows/test_virtual_display_sunshine.cpp, in theexisting style, covering the derived scale, the fallback when unset or out of range, explicit-scale
precedence, the derived physical size, the absent cases, and idempotency. Two more were added to
src_assets/common/assets/web/scripts/v2-parity.test.ts, pinning the suggestion table to the field andto the widths the C++ tests and the documentation are written against, so the three cannot drift apart
silently.
I could not run the project's own test suite, and I have not tested this on a real host. Vibepollo
builds under MSYS2 UCRT64 and that toolchain is not installed on my machine; the full build also needs
the virtual display driver SDK (
<virtual_display/driver/control_client.h>). What I did instead:from
virtual_display.handvirtual_display_identity.cpp, with MSVC at/W4 /permissive- /std:c++20, and ran every assertion from the nine scale and size test cases, thetwo pre-existing ones included as regression checks. 35/35 pass, no warnings.
npm run test:v2-parity, 6/6), the TypeScript typecheck(
vue-tsc --noEmit, clean), the production build (npm run build, clean), andprettier --checkover every file touched.
Not verified: that the whole project compiles under the real toolchain, and — most importantly —
that a real client actually ends up with correctly-sized UI. The EDID path in particular needs someone
with the driver and a device to confirm that Windows picks up the advertised size and settles on the
expected scale. I would not merge this without that check.
AI assistance
This change was written with AI assistance (Claude). Per CONTRIBUTING.md I have reviewed the diff and I
understand what it does; the arithmetic above was verified independently against the vendor figures, and
the limits of what was actually executed are stated plainly in the section above rather than implied.
Design discussion
The shape of this — EDID physical size as the input rather than a picked percentage — is open for
discussion in #453, including the alternatives I considered and the questions I'd most like answered
before this goes any further. If you'd rather implement it yourself, everything needed is written up
there and this draft can simply be closed.
Refs ClassicOldSong#290, ClassicOldSong#1013, #386, #453, Nonary/vibeshine#266.