Skip to content

Read the active display, and let a client name one - #19

Draft
rubu wants to merge 1 commit into
mainfrom
feat/framebuffer-display-selection
Draft

rubu wants to merge 1 commit into
mainfrom
feat/framebuffer-display-selection

Conversation

@rubu

@rubu rubu commented Sep 30, 2026

Copy link
Copy Markdown

Why

framebuffer_stream selected its display with:

if displayClass == 0 { return SimDisplayRenderableSurface(surface: renderable, logger: logger) }

first-match, with a comment about skipping tvOS TVOut. A foldable reports an integrated panel per fold state and lights one of them, and the first class-0 panel is not necessarily the lit one. On an iPhone Duo I measured, with the device booted:

primary   (LCD,   1398×2034)  → 100% black
primary-1 (LCD-1, 2007×2853)  → content        ← isActive

and on a different boot of the same device the other panel was the lit one. So the stream can attach to a dark panel and copy black frames for the whole session — which shows up as boot verification timing out with frames arriving and nothing ever non-black.

What

  • Reads the active integrated display by default, which is what SimulatorScreenshotCommands already does. A runtime whose provider cannot report displays keeps the old heuristic, since that is all that was ever available there.
  • Configure lets a client name a display and fix the scale for the stream's lifetime. Accepted at most once and only before the first copy — a client allocates against the geometry it is told, so neither can change under it. Answered with the chosen display's geometry, so it doubles as a probe. Streams that never send one behave exactly as before.
  • list_displays reports what there is to choose from; a unique id is not otherwise discoverable.

Two small visibility changes in FBSimulatorControl: Framebuffer.surface(for:simulator:) exposes the per-display factory while keeping FramebufferSurfaceLocator internal, and activeIntegratedDisplayIfSupported() becomes public — it returns nil exactly when the runtime cannot report displays, which is the fallback signal.

Verified

Against an iPhone Duo on iOS 27.1:

=== list_displays ===
  ACTIVE  integrated 2007x2853 scale=3  LCD-1      0EA097B6-…
          integrated 1398x2034 scale=3  LCD        CED69781-…
          other      0x0       scale=1  Wireless / TVOut / Resizable

stream, no Configure                    -> 2007x2853   (the active panel)
stream, Configure naming LCD            -> 1398x2034   (the other panel)
stream, Configure with a bogus id       -> ERROR: No display with unique id …

Notes

  • Per-screen streams rather than one multiplexed stream: each has its own geometry and buffer, so a client rendering both panels does not have to size every allocation for the larger one.
  • Attachment is now made on the first request that needs it rather than up front, since the display is not known until a Configure names one.
  • If the active display changes mid-stream the geometry changes with it, which this protocol has no way to express — a client that cares should reopen the stream. Screenshots handle the same situation by throwing SimulatorDisplayError.changed.

🤖 Generated with Claude Code

The framebuffer stream picked the first display reporting `displayClass == 0`, a
heuristic written to skip tvOS TVOut rather than to choose between panels. A
foldable reports an integrated panel per fold state and lights one of them, and
which of those two comes first is not the one that is lit -- so the stream could
attach to a dark panel and copy black frames for the life of the boot.

It now reads the active integrated display, which is what the screenshot commands
already do. A runtime whose provider cannot report displays keeps the old
heuristic, since that is all that was ever available there.

`Configure` lets a client name a display instead, and fixes the scale, for the
life of the stream: geometry a client has allocated against cannot change under
it, so the message is accepted at most once and only before the first copy. It is
answered with the chosen display's geometry, so it doubles as a probe. Streams
that never send one behave exactly as before.

`list_displays` reports what there is to choose from, since a unique id is not
something a client can otherwise discover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant