Documentation
¶
Overview ¶
Package virtualdisplay creates virtual displays on macOS from pure Go, with CGO_ENABLED=0. A display it opens is a real display as far as the rest of the system is concerned: it has a CGDirectDisplayID, it appears in CGGetActiveDisplayList and in System Settings > Displays, the desktop extends onto it, and ordinary applications can be dragged there and captured from there.
⚠ This package uses PRIVATE CoreGraphics API ¶
There is no public macOS API that creates a virtual display. The public route is a DriverKit driver extension, which needs an Apple-granted entitlement. This package instead drives four undocumented Objective-C classes that CoreGraphics has carried for years and that every third-party virtual-display app on macOS uses: CGVirtualDisplayDescriptor, CGVirtualDisplay, CGVirtualDisplaySettings and CGVirtualDisplayMode.
The consequences are not negotiable, so they are stated up front:
- Apple may change, rename or remove these classes in any macOS release, including a point release. Nothing here is covered by any compatibility promise.
- A program that links this package cannot be distributed on the Mac App Store. App Store review rejects private-API use.
- The selectors are reverse-engineered. The argument types below were read off the live runtime's method type encodings on the macOS this package was developed against; a future OS could keep a selector's name and change its signature, which no amount of checking can detect.
Failing loudly rather than crashing ¶
Sending a message to a class that no longer exists, or a selector a class no longer implements, is a hard crash in the Objective-C runtime, not an error return. So this package never sends a message it has not first verified. Available — which Open calls before it allocates anything — looks up every class with objc_getClass and every selector with class_getInstanceMethod, and reports ErrUnavailable naming the first class or selector that has gone missing:
virtualdisplay: the private CoreGraphics virtual-display API is not available: class CGVirtualDisplaySettings does not respond to setModes:
That is the failure mode to expect when a future macOS moves the shape. Treat ErrUnavailable as "this OS release broke it", print it, and carry on without virtual displays. Do not retry.
Lifetime ¶
A virtual display lives exactly as long as the CGVirtualDisplay object that owns it. Display.Close releases that object and the display disappears. Close is idempotent: calling it twice is safe and the second call is a no-op, because a second Objective-C release would be a use-after-free.
The display is also torn down when the creating process dies, including on a crash or SIGKILL — the WindowServer owns the other end of the connection and reclaims it. This was verified deliberately (see the package README): a process that panics mid-flight leaves no phantom display behind. That is a safety property worth relying on, but it is not a licence to skip Close: the display stays on the user's desktop for as long as your process lives.
Usage ¶
if err := virtualdisplay.Available(); err != nil {
log.Printf("no virtual displays on this macOS: %v", err)
return
}
d, err := virtualdisplay.Open(virtualdisplay.Spec{
Name: "XR screen 1",
Width: 1920,
Height: 1080,
HiDPI: true,
})
if err != nil {
return err
}
defer d.Close()
capture(d.ID()) // a CGDirectDisplayID, ready for ScreenCaptureKit
Portability ¶
Every exported symbol exists on every platform so consumers cross-compile. Off darwin the entry points report ErrUnsupported.
Index ¶
- Constants
- Variables
- func ActiveDisplayIDs() ([]uint32, error)
- func Available() error
- func CloseAll() error
- func OpenCount() int
- func WaitGone(timeout time.Duration, ids ...uint32) error
- type ActiveMode
- type Display
- func (d *Display) ActiveMode() ActiveMode
- func (d *Display) AvailableModes() ([]ActiveMode, error)
- func (d *Display) Close() error
- func (d *Display) Closed() bool
- func (d *Display) CurrentMode() (ActiveMode, error)
- func (d *Display) HiDPI() bool
- func (d *Display) ID() uint32
- func (d *Display) Modes() []Mode
- func (d *Display) Name() string
- func (d *Display) Size() (width, height uint32)
- type DisplayInfo
- type Mode
- type Size
- type Spec
Constants ¶
const ( // MinDimension is the smallest pixel width or height a virtual display may // have. Below this the WindowServer rejects the mode. MinDimension = 64 // MaxDimension is the largest pixel width or height this package will ask // for. It is a sanity bound, not a measured hardware limit. MaxDimension = 16384 // MinRefreshRate and MaxRefreshRate bound [Spec.RefreshRate] in Hz. MinRefreshRate = 1 MaxRefreshRate = 240 // DefaultRefreshRate is used when [Spec.RefreshRate] is zero. DefaultRefreshRate = 60 // DefaultDPI is the pixel density used to derive a physical size in // millimetres when [Spec.SizeMM] is left zero. macOS reads the physical // size as an EDID would, and uses it to decide what counts as a sensible // default resolution. DefaultDPI = 96 // DefaultName is the display name used when [Spec.Name] is empty. It is // what System Settings > Displays shows. DefaultName = "Go Virtual Display" // DefaultVendorID and DefaultProductID identify displays this package // creates. They are arbitrary: no real vendor owns them. DefaultVendorID = 0x676F // "go" DefaultProductID = 0x5644 // "VD" )
Limits and defaults.
Variables ¶
var ( // ErrUnsupported is returned by every entry point on a non-darwin platform. ErrUnsupported = errors.New("virtualdisplay: unsupported on this platform (macOS only)") // classes are absent, or present but missing a selector this package needs. // The wrapped message names the first class or selector that is missing, so // a report from a future macOS says exactly what moved. See [Available]. ErrUnavailable = errors.New("virtualdisplay: the private CoreGraphics virtual-display API is not available") // ErrInvalidSpec is returned by [Open] when the [Spec] cannot describe a // display: a dimension out of range, an impossible refresh rate, a mode // larger than the display it belongs to, a name the Objective-C runtime // cannot carry. ErrInvalidSpec = errors.New("virtualdisplay: invalid spec") // ErrRejected is returned when the display object was created but the // WindowServer refused the settings (-[CGVirtualDisplay applySettings:] // returned NO), so no display ever became active. An empty mode list and a // mode larger than the descriptor's maximum both produce this. ErrRejected = errors.New("virtualdisplay: the WindowServer rejected the display settings") // ErrCreateFailed is returned when -[CGVirtualDisplay initWithDescriptor:] // yields nil, i.e. the WindowServer would not hand out a display at all. // The usual cause is a process with no window-server session (a plain ssh // login, a CI runner, a LaunchDaemon). ErrCreateFailed = errors.New("virtualdisplay: the WindowServer would not create a virtual display") // ErrWrongMode is returned when the display came up at a size other than // the one requested and could not be made to come up at the right one. // // macOS remembers, per (VendorID, ProductID, SerialNumber), whichever mode // that monitor was last set to, and restores it. A fresh identity has // nothing remembered and comes up at the requested size, so [Open] retries // once under a fresh identity — but only when it chose the serial number // itself. If [Spec.SerialNumber] was set explicitly, that identity is the // caller's to manage and this error is returned instead of quietly using a // different one. // // The mode CANNOT simply be corrected after the fact: see // [Display.Close]'s documentation on why this package never changes a // display's mode. ErrWrongMode = errors.New("virtualdisplay: the display came up at a size other than the one requested") // ErrModesUnreadable is returned by [Display.AvailableModes], // [Display.CurrentMode] and [DisplayModes] when CoreGraphics will not // report a display's modes to THIS process. // // Measured on macOS 26.6.2: CoreGraphics builds a per-process cache of // display modes the first time a process asks about displays. A virtual // display created after that cache exists is fully real — it is in // CGGetActiveDisplayList, CGDisplayPixelsWide reports its size, other // processes see it completely — but this process can no longer obtain a // CGDisplayMode for it, permanently, and nothing refreshes that: not a // reconfiguration callback, not pumping the run loop. // // It affects only mode reporting, never the display itself, and only the // process that created it. Nothing else in this package depends on being // able to read a mode. To inspect the modes of a display you created — // which is how to confirm HiDPI took effect — read them from another // process, or arrange to create the display before anything in the process // enumerates displays. ErrModesUnreadable = errors.New("virtualdisplay: CoreGraphics will not report this display's modes to this process") // ErrStillPresent is what [WaitGone] returns when macOS is still listing a // display after the time allowed for it to go. // // Reaching it means something holds the display that this package cannot // release: another process created it, or its mode was changed after // creation, which stops a release from removing it (see [Display.Close]). ErrStillPresent = errors.New("virtualdisplay: macOS still lists a display that was released") )
Errors reported by the package. They are stable and may be tested with errors.Is.
Functions ¶
func ActiveDisplayIDs ¶
ActiveDisplayIDs returns the IDs from ActiveDisplays, sorted.
func Available ¶
func Available() error
Available reports whether this macOS still carries the private virtual-display API in the shape this package expects. It returns nil if every class and selector is present, ErrUnavailable naming the first thing missing otherwise, and ErrUnsupported off darwin.
Open calls it first, so calling it yourself is only needed to decide whether to offer the feature at all.
func CloseAll ¶
func CloseAll() error
CloseAll closes every display this process still has open and returns the first error. Use it from a signal handler or a top-level defer so a display never outlives the run that created it.
func OpenCount ¶
func OpenCount() int
OpenCount returns how many displays this process currently has open.
func WaitGone ¶ added in v0.2.0
WaitGone waits until macOS lists none of ids as an active display.
⚠ Releasing a display is ASYNCHRONOUS, and this is the half that says so. Display.Close returns as soon as the CGVirtualDisplay object is released; the WindowServer retires the display a moment later. Measured on macOS 26.6.2, six 1920x1080 displays closed in one go were still in ActiveDisplays 250 ms later and took 1.9 s to all leave — long enough that a caller which closes and immediately looks sees displays that are already dead and reads them as a leak. Open does not return until the display is active; this is the other direction, and a release is not observable until it finishes.
A zero or negative timeout checks once and does not wait. No ids is not an error: nothing is present, so the wait is over before it starts.
Types ¶
type ActiveMode ¶
type ActiveMode struct {
PointsWide, PointsHigh int
PixelsWide, PixelsHigh int
RefreshRate float64
}
ActiveMode is a mode a display can be in. Points are what the desktop measures in; pixels are what a capture of the display produces. They differ by a factor of two on a HiDPI (Retina) mode.
func DisplayModes ¶
func DisplayModes(id uint32) ([]ActiveMode, error)
DisplayModes returns every mode macOS offers for any display, virtual or not. It can report ErrModesUnreadable for a display this same process created; from any other process the same display reads normally.
func (ActiveMode) HiDPI ¶
func (a ActiveMode) HiDPI() bool
HiDPI reports whether this is a Retina mode — more pixels than points.
func (ActiveMode) String ¶
func (a ActiveMode) String() string
String renders the mode as "960x540 points / 1920x1080 pixels @60Hz".
type Display ¶
type Display struct {
// contains filtered or unexported fields
}
Display is a live virtual display. It is safe for concurrent use.
func Open ¶
Open creates a virtual display and returns it. The display is active — it is in CGGetActiveDisplayList and the desktop has extended onto it — by the time Open returns.
⚠ IT LEAVES SOMETHING ON THE MACHINE. macOS remembers every monitor it has seen: opening a display writes an ICC profile into /Library/ColorSync/Profiles/Displays, owned by root, and neither Display.Close nor a reboot removes it. The identity is derived from the name and pixel size (see Spec.SerialNumber), so a program that reuses a name reuses one profile while one that invents names fills somebody's system directory. Measured on one machine: 106 of 235 stored profiles came from this project's own probes and tests. Use few, stable names.
It never becomes the main display, and it never disturbs a display that already existed.
Close it when done. See the package documentation for what happens if you do not.
func (*Display) ActiveMode ¶
func (d *Display) ActiveMode() ActiveMode
ActiveMode returns the mode the display came up in, as read at Open. Its PixelsWide and PixelsHigh are guaranteed to be the requested Spec.Width and Spec.Height — Open fails rather than return a display of another size.
func (*Display) AvailableModes ¶
func (d *Display) AvailableModes() ([]ActiveMode, error)
AvailableModes returns every mode macOS offers for this display. With Spec.HiDPI set the list also holds Retina entries — half the points, the same pixels — which is how to confirm HiDPI took effect.
It is read-only. Selecting one of these modes is not something this package will do; see Display.Close.
It commonly reports ErrModesUnreadable for a display this process created: read that error's documentation before concluding anything about the display.
func (*Display) Close ¶
Close destroys the display. It is idempotent: the second and later calls do nothing and return nil, which matters because the underlying teardown is an Objective-C release and doing it twice would be a use-after-free.
Close returns before macOS has finished retiring the display: the removal is asynchronous, and WaitGone is how to wait for it to be observable.
Why this package never changes a display's mode ¶
Close only works because the display's mode was never changed. Measured on macOS 26.6.2: once a virtual display is switched into a different mode — by CGDisplaySetDisplayMode, or by a CGBeginDisplayConfiguration transaction at any scope, HiDPI or not — releasing its CGVirtualDisplay object no longer removes it. The object is deallocated (its retain count reaches zero) and the display stays on the desktop until the process exits.
So this package chooses the mode by picking a monitor identity macOS has nothing remembered for, and never switches modes afterwards. If something else switches this display's mode, Close stops being able to remove it; that is a property of the private API, not of this code. Display.CurrentMode will show it.
func (*Display) Closed ¶
Closed reports whether Display.Close has already run.
func (*Display) CurrentMode ¶
func (d *Display) CurrentMode() (ActiveMode, error)
CurrentMode reads the mode the display is in right now. It differs from Display.ActiveMode only if something outside this package changed it, which is worth knowing: such a change pins the display for the life of the process (see Display.Close).
It can report ErrModesUnreadable; that is a limit on what this process can see, not on the display.
func (*Display) HiDPI ¶
HiDPI reports whether Retina modes were requested. Whether one was actually selected is ActiveMode.HiDPI on Display.ActiveMode.
func (*Display) ID ¶
ID returns the display's CGDirectDisplayID — the handle every other macOS display API takes, ScreenCaptureKit and CGDisplayStream included. After Display.Close it still returns the ID the display had, which is then stale.
func (*Display) Modes ¶
Modes returns the modes the display advertises, primary first. macOS adds more of its own; this is what was asked for.
type DisplayInfo ¶
type DisplayInfo struct {
// ID is the CGDirectDisplayID.
ID uint32
// Mode is the mode the display is currently in, points and pixels both.
Mode ActiveMode
// X, Y, Width and Height are the display's bounds in the global display
// coordinate space, in points.
X, Y, Width, Height float64
// Main reports whether this is the main display (the one with the menu
// bar).
Main bool
}
DisplayInfo describes one display macOS reports as active, virtual or not.
func ActiveDisplays ¶
func ActiveDisplays() ([]DisplayInfo, error)
ActiveDisplays returns every display macOS currently reports as active, ordered by display ID. It is the outside check on this package's work: call it before and after Open and the difference is the display that was created.
func (DisplayInfo) String ¶
func (d DisplayInfo) String() string
String renders the display as "id=7 800x600 at (-800,0)", noting the pixel size separately when the mode is HiDPI.
type Mode ¶
Mode is one resolution a virtual display advertises. Width and Height are pixels; RefreshRate is Hz, and zero means the display's own rate.
macOS synthesises further modes of its own around the ones given here (a 1920x1080 display is also offered at 1600x900, 1280x720 and so on), so a single mode is usually enough.
type Size ¶
type Size struct{ Width, Height float64 }
Size is a physical size in millimetres, as an EDID would report it.
type Spec ¶
type Spec struct {
// Name is what System Settings > Displays calls the display. Empty means
// [DefaultName].
Name string
// Width and Height are the display's pixel dimensions, and also the
// maximum any [Mode] may have. Required; both must be within
// [MinDimension] and [MaxDimension].
Width, Height uint32
// RefreshRate is the primary mode's rate in Hz. Zero means
// [DefaultRefreshRate].
RefreshRate float64
// HiDPI asks macOS to additionally ADVERTISE Retina modes — each mode at
// half its pixel size in points, with a backing scale factor of 2. With
// HiDPI set, a 1920x1080 display also offers 960x540 points at 1920x1080
// pixels, and [Display.AvailableModes] shows it.
//
// It does not change which mode the display starts in, and this package
// will not switch into one: entering a Retina mode is a mode change, and a
// virtual display whose mode has been changed cannot be removed until the
// process exits (see [Display.Close]). Setting HiDPI makes the Retina modes
// selectable — in System Settings, or by a caller who accepts that
// trade-off — nothing more.
HiDPI bool
// ExtraModes are additional resolutions to advertise, beyond the primary
// Width x Height. None may exceed Width or Height: the WindowServer
// rejects the whole settings object if one does ([ErrRejected]).
ExtraModes []Mode
// SizeMM is the physical size reported to macOS. Zero means derived from
// the pixel size at [DefaultDPI].
SizeMM Size
// VendorID, ProductID and SerialNumber are the identity macOS uses to tell
// displays apart and to remember each one's arrangement and resolution.
// Zero VendorID and ProductID mean [DefaultVendorID] and
// [DefaultProductID]; a zero SerialNumber is derived, deterministically,
// from Name and the pixel size — so reopening the same logical display
// finds the arrangement the user last chose for it, while a differently
// named or sized display is a different monitor. Two displays that share a
// name and a size are therefore ONE monitor as far as macOS is concerned;
// give them different names, or set this field, if you open several at
// once.
VendorID, ProductID, SerialNumber uint32
// OnTerminate, if non-nil, is called when the WindowServer terminates the
// display from its side rather than at your request — a window-server
// restart, for instance. It is NOT called by [Display.Close]. It runs on an
// internal dispatch queue, so it must not block; hand the work to a
// goroutine.
//
// Leave it nil unless you need it. Setting it installs an Objective-C block
// that calls back into Go, and the WindowServer can invoke that block while
// the process is exiting — after the Go runtime has started shutting down,
// which crashes. With OnTerminate nil no block is installed at all and
// there is nothing to call. This was observed as an intermittent crash on
// exit in a process that created a display and exited without closing it.
OnTerminate func()
}
Spec describes a display to create. The only required fields are Width and Height, in pixels.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
vdprobe
command
Command vdprobe is the outside check on github.com/go-macos/virtualdisplay: it enumerates the displays macOS reports, creates a virtual display, enumerates again, destroys it, and enumerates a third time — printing all three lists and asserting the differences.
|
Command vdprobe is the outside check on github.com/go-macos/virtualdisplay: it enumerates the displays macOS reports, creates a virtual display, enumerates again, destroys it, and enumerates a third time — printing all three lists and asserting the differences. |