Add optional @PPI hint to --display for HiDPI pixel displays

`tart set --display WIDTHxHEIGHTpx` always configured the guest's pixel
framebuffer at a hardcoded 72 PPI, so a macOS guest never exposed a HiDPI
(2x/Retina) mode when the host display was non-Retina — or absent, which
is common on headless/CI hosts. There was no way to ask for a Retina
guest in that case.

Allow an optional "@PPI" pixels-per-inch suffix on --display, e.g.
`--display 3200x1800px@220`. At a Retina-class density the pixel display
then offers a scaling:on (HiDPI) mode. The pixel path never consults
NSScreen, so this works regardless of the host display or whether a user
is logged in on the host.

The hint lives in VMConfig (VMDisplayConfig.ppi) and defaults to the
existing 72 when omitted, so behavior is unchanged for current VMs and
configs. Covered by VMConfigTests (parsing, description round-trip,
effective-PPI default, backward compatibility, malformed input).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eugene Petrenko 2026-07-28 13:43:10 +02:00
parent cbc160a592
commit 521cc16e44
4 changed files with 72 additions and 6 deletions

View File

@ -14,7 +14,7 @@ struct Set: AsyncParsableCommand {
@Option(help: "VM memory size in megabytes")
var memory: UInt64?
@Option(help: "VM display resolution in a format of WIDTHxHEIGHT[pt|px]. For example, 1200x800, 1200x800pt or 1920x1080px. Units are treated as hints and default to \"pt\" (points) for macOS VMs and \"px\" (pixels) for Linux VMs when not specified.")
@Option(help: "VM display resolution in a format of WIDTHxHEIGHT[pt|px][@PPI]. For example, 1200x800, 1200x800pt, 1920x1080px or 3200x1800px@220. Units are treated as hints and default to \"pt\" (points) for macOS VMs and \"px\" (pixels) for Linux VMs when not specified. The optional @PPI (pixels-per-inch) hint applies to pixel-unit displays and, at a Retina-class value such as 220, makes a macOS guest expose a HiDPI (2x) mode regardless of the host display; it defaults to 72 (non-Retina) when omitted.")
var display: VMDisplayConfig?
@Flag(inversion: .prefixedNo, help: ArgumentHelp("Whether to automatically reconfigure the VM's display to fit the window"))
@ -57,6 +57,7 @@ struct Set: AsyncParsableCommand {
vmConfig.display.height = display.height
}
vmConfig.display.unit = display.unit
vmConfig.display.ppi = display.ppi
}
vmConfig.displayRefit = displayRefit
@ -91,6 +92,14 @@ extension VMDisplayConfig: ExpressibleByArgument {
public init(argument: String) {
var argument = argument
var unit: Unit? = nil
var ppi: Int? = nil
// Optional "@PPI" pixels-per-inch hint, e.g. "3200x1800px@220". Parsed
// before the unit suffix since it always trails the whole spec.
if let atIndex = argument.lastIndex(of: "@") {
ppi = Int(argument[argument.index(after: atIndex)...])
argument = String(argument[..<atIndex])
}
if argument.hasSuffix(Unit.pixel.rawValue) {
argument = String(argument.dropLast(Unit.pixel.rawValue.count))
@ -107,6 +116,7 @@ extension VMDisplayConfig: ExpressibleByArgument {
width: parts[safe: 0] ?? 0,
height: parts[safe: 1] ?? 0,
unit: unit,
ppi: ppi,
)
}
}

View File

@ -95,9 +95,11 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
VZMacGraphicsDisplayConfiguration(
widthInPixels: vmConfig.display.width,
heightInPixels: vmConfig.display.height,
// A reasonable guess according to Apple's documentation[1]
// Defaults to 72 a reasonable guess according to Apple's
// documentation[1] unless a --display "@PPI" hint asks for a
// Retina-class density (which yields a HiDPI guest mode).
// [1]: https://developer.apple.com/documentation/coregraphics/1456599-cgdisplayscreensize
pixelsPerInch: 72
pixelsPerInch: vmConfig.display.effectivePixelsPerInch
)
]

View File

@ -41,15 +41,27 @@ struct VMDisplayConfig: Codable, Equatable {
var width: Int = 1024
var height: Int = 768
var unit: Unit?
var ppi: Int?
// Pixels-per-inch handed to the guest's pixel-unit framebuffer. Defaults to
// 72 (non-Retina) when no hint is given, matching Virtualization.framework's
// historical assumption; a Retina-class value (e.g. 220) makes the guest
// expose a HiDPI (scaling:on) mode regardless of the host display.
var effectivePixelsPerInch: Int {
ppi ?? 72
}
}
extension VMDisplayConfig: CustomStringConvertible {
var description: String {
var result = "\(width)x\(height)"
if let unit {
"\(width)x\(height)\(unit.rawValue)"
} else {
"\(width)x\(height)"
result += unit.rawValue
}
if let ppi {
result += "@\(ppi)"
}
return result
}
}

View File

@ -15,4 +15,46 @@ final class VMConfigTests: XCTestCase {
vmDisplayConfig = VMDisplayConfig.init(argument: "1234x5678px")
XCTAssertEqual(VMDisplayConfig(width: 1234, height: 5678, unit: .pixel), vmDisplayConfig)
}
func testEffectivePixelsPerInchUsesConfiguredValue() throws {
let config = VMDisplayConfig(width: 3200, height: 1800, unit: .pixel, ppi: 220)
XCTAssertEqual(220, config.effectivePixelsPerInch)
}
func testEffectivePixelsPerInchDefaultsTo72WhenUnset() throws {
// Upstream behavior: a pixel display with no PPI hint stays non-Retina (72 PPI).
let config = VMDisplayConfig(width: 1234, height: 5678, unit: .pixel, ppi: nil)
XCTAssertEqual(72, config.effectivePixelsPerInch)
}
func testParsesPixelsPerInchSuffix() throws {
let config = VMDisplayConfig(argument: "3200x1800px@220")
XCTAssertEqual(VMDisplayConfig(width: 3200, height: 1800, unit: .pixel, ppi: 220), config)
}
func testWithoutSuffixHasNilPixelsPerInch() throws {
// Backward compatibility: existing "WIDTHxHEIGHT[pt|px]" strings carry no PPI.
XCTAssertEqual(
VMDisplayConfig(width: 1234, height: 5678, unit: .pixel, ppi: nil),
VMDisplayConfig(argument: "1234x5678px"))
}
func testDescriptionRoundTripsPixelsPerInch() throws {
let config = VMDisplayConfig(width: 3200, height: 1800, unit: .pixel, ppi: 220)
XCTAssertEqual("3200x1800px@220", config.description)
XCTAssertEqual(config, VMDisplayConfig(argument: config.description))
}
func testParsesPixelsPerInchWithoutExplicitUnit() throws {
let config = VMDisplayConfig(argument: "3200x1800@220")
XCTAssertEqual(VMDisplayConfig(width: 3200, height: 1800, unit: nil, ppi: 220), config)
}
func testMalformedPixelsPerInchDegradesToNil() throws {
// A non-numeric PPI is ignored (falls back to the 72 default) rather than
// failing the parse consistent with the parser's lenient dimensions.
let config = VMDisplayConfig(argument: "3200x1800px@notanumber")
XCTAssertEqual(VMDisplayConfig(width: 3200, height: 1800, unit: .pixel, ppi: nil), config)
XCTAssertEqual(72, config.effectivePixelsPerInch)
}
}