Added
-
Display zoom (View ▸ Zoom). On a Retina Mac the client asked the guest for
the view's full backing pixel count, so a 1512×982-point window drove the
guest at 3024×1964 — the guest has no idea the Mac is HiDPI, so it rendered
one pixel per pixel and everything came out half size, while the VM pushed
four times the pixels it needed. Zoom is now Z = Mac physical pixels per
guest pixel: the client requestspoints × backing scale ÷ Zand each guest
pixel is drawn as a Z×Z block, so readability and cost improve together.
Shortcuts ⌃⌘+ / ⌃⌘− / ⌃⌘0. -
The zoom level is per-window, and a fixed level is absolute. Each window
is its own session on its own display, so it carries its own level: picking
one in the front window leaves the others alone. The commands live on the
window controller and reach it down the responder chain the wayConnection ▸ Send Ctrl-Alt-Delalready did, so the submenu greys out with no session open;
the preference is only the seed a new window starts from. Nothing but the user
ever changes a level — Automatic is the mode for constant apparent size
across a move. -
A window that changes display re-applies its geometry. At a fixed level
the target guest size ispoints × backing scale ÷ Z, and a move changes the
backing scale out from under it — which is why it used to need a manual nudge
of the window before the guest came out right. All four signals AppKit offers
now drive it, includingNSApplicationDidChangeScreenParametersfor hotplug,
sleep/wake and Displays scaled-mode changes, which resize the window without a
live resize. Requests are coalesced, so four triggers cost at most one guest
mode switch. -
Without
spice-vdagent, zoom and screen changes resize the window
(guest × Z ÷ backing scalepoints) rather than doing nothing — the guest
resolution is fixed, so that is the only side of the equation left. The
geometry is a new dependency-free package,Packages/DisplayScale, with a
21-checkscalecheckrunner wired intomake testand CI.
Changed
- The default zoom is Automatic (Z = the screen's backing scale), which
changes behaviour on upgrade. The guest resolution now tracks the window's
point size instead of its backing-pixel size, so on first connect after
updating a Retina guest drops to roughly half its previous resolution and
everything in it gets twice as big. That is the fix; View ▸ Zoom ▸ 100%
restores the old behaviour. Automatic also means the requested resolution is
the window's point size on any display, so dragging between screens needs no
guest reconfiguration.
Fixed
-
A dead connection no longer hides its own explanation. When a session
ended, the last guest frame stayed frozen over the window (the Metal view is
opaque anddetach()leaves it showing its final texture), so the
"Disconnected — open a fresh .vv file" hint or the failure reason was drawn
invisibly behind it and the window just looked hung. The display view now
hides when the session ends and returns on the next connect, revealing the
centered status message. A specific failure reason (.failed) is also kept
if the generic disconnect callback arrives after the error one, instead of
being downgraded to "Disconnected." -
ISO / French Magic Keyboard
<>key now reaches the guest. The keymap was
ANSI-only and omittedkVK_ISO_Section(0x0A), the key next to Left Shift on
ISO hardware (e.g. French AZERTY</>). Presses produced no guest event.
Map it to PC set-1KEY_102ND(0x56);inputcheckcovers the mapping.
(#4) -
Guest text was blurry at some window sizes and not others, worst on a
normal-DPI monitor. The sampler only went nearest-neighbour at 2× or more, so
a 1:1 presentation — what 100% means on a 1x screen — always took the bilinear
path; and because the requested mode is floored onto the 8-wide/2-high grid
guest drivers want, the centred quad landed on a half-pixel offset
whenever that slack was odd. Nearest now applies at any whole-number
magnification, andviewportOriginnudges the quad onto whole pixels — the
input router subtracts the same origin, so the cursor stays locked. -
Dragging a window between a Retina panel and a 1x monitor did not re-scale
the guest.MTKViewrefreshesdrawableSizelazily, so inside
viewDidChangeBackingProperties— the one moment such a move offers — it
still holds the previous screen's value: on a real 2.0↔1.0 drag the callback
reportsbackingScaleFactor1.0 whiledrawableSizeis still 1800×1200 for a
view that is now 900×600 physical pixels. The fit now measures with
convertToBacking(bounds), which follows the backing store immediately, and
pulls the drawable up to match.
Download
SpiceMac.app.zip — Apple Silicon, macOS 12+. Ad-hoc signed; to open: right-click ▸ Open (macOS 14) or System Settings ▸ Privacy & Security ▸ Open Anyway (macOS 15+), or xattr -dr com.apple.quarantine /Applications/SpiceMac.app.
SHA-256: faeb08e0ef42a4189ed69a4a64cc4c7d3b6e29e511645ccea876eb865551ad76