Skip to content

Flutter embedder step 3a: live texture bridge into the GUI - #31

Merged
xvrh merged 27 commits into
masterfrom
claude/happy-cannon-aa0b9f
May 15, 2026
Merged

Flutter embedder step 3a: live texture bridge into the GUI#31
xvrh merged 27 commits into
masterfrom
claude/happy-cannon-aa0b9f

Conversation

@xvrh

@xvrh xvrh commented May 15, 2026

Copy link
Copy Markdown
Owner

Summary

Step 3a of the experimental Flutter engine embedder: the embedded engine's live, animated, interactive output is now displayed inside the flutterware desktop GUI via an external Texture.

  • Out-of-process guest — the C host (app/native/) became a long-lived guest process: runs the engine with the software renderer, copies each composited frame into shared IOSurfaces, and speaks a framed wire protocol over a Unix domain socket (SurfacesAllocated/FrameReady/Resize/PointerEvent/KeyEvent/Shutdown). Split into host.c + surface.c + ipc.c + input.c.
  • GUI sideEmbeddedEngine builds/spawns the guest and bridges the protocol; a macOS plugin (EmbedderTexturePlugin.swift) registers an external FlutterTexture and wraps the IOSurfaces as CVPixelBuffers; a dev-harness screen (main_embedder_dev.dart) hosts the live Texture with resize and pointer/keyboard forwarding.
  • The panel is resizable and interactive; the guest runs out-of-process so a guest crash leaves the GUI alive.

Built per the design + plans under docs/superpowers/ (2026-05-15-flutter-embedder-step3a-*). Two later steps remain: 3b (Metal/zero-copy) and 4 (hot reload).

Test plan

  • cd app && dart test test/embedder integration_test/embedder — 18 tests pass (protocol_test, raw_frame_test, flutter_cache_test, compiler_test, live_bridge_test)
  • flutter analyze — clean workspace-wide
  • dart run app/tool/embedder/run.dart — headless smoke renders the scene to a PNG
  • GUI harness runs: live animated scene in the texture, click increments the counter, resize works at every size
  • Verify on a fresh checkout (first run downloads FlutterEmbedder.framework, ~31 MB)
  • Visual confirmation by a reviewer — the automated env had no Screen Recording permission, so display was verified by pipeline (no Metal/IOSurface errors) rather than a screenshot

Known limitations (scoped out of 3a)

  • Transient flicker during an active resize drag (full software relayout per resize; frame-perfect resize sync is out of scope for 3a).
  • macOS only; software renderer; no hot reload.

🤖 Generated with Claude Code

xvrh and others added 27 commits May 15, 2026 17:21
Out-of-process guest engine renders an animated, interactive scene; the
flutterware GUI displays it live in a Texture via a shared IOSurface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SCM_RIGHTS transfers file descriptors, not mach ports. IOSurfaces are shared
across processes by their global IOSurfaceID via IOSurfaceLookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The guest dumps its first frame as a step-2-format raw file via --capture-raw,
so run.dart can encode a PNG without reading an IOSurface from Dart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two plans: Plan 1 builds the headless out-of-process guest and wire protocol;
Plan 2 adds the macOS texture plugin and dev-harness GUI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The field has no zero enumerator (kFlutterKeyEventDeviceTypeKeyboard = 1);
leaving it zero-initialized makes the engine reject key events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The StreamController is closed via the socket's onDone handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Side effect of running `flutter build macos`: deployment target bump to
10.15, @main + applicationSupportsSecureRestorableState, scheme/Podfile.lock
refresh. Committed separately to keep the embedder change focused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A macOS app launched by `flutter run` has a stripped environment and a `/`
working directory, so FlutterSdkPath.findSdks() and Directory.current both
fail. Take FLUTTERWARE_APP_ROOT and FLUTTER_SDK_ROOT as dart-defines, with
SDK discovery as a fallback and a clear error screen when unset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runtime fixes found by running the GUI harness:
- surface.c: mark IOSurfaces global so the GUI process can resolve them
  with IOSurfaceLookup (cross-process lookup fails otherwise).
- EmbedderTexturePlugin: create the CVPixelBuffer with the Metal
  compatibility key, so the Flutter engine's Metal texture cache accepts
  it (was failing per-frame with kCVReturnPixelBufferNotMetalCompatible).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- FrameReady now carries the surface-ring generation; EmbeddedEngine
  discards frames composited against superseded surfaces, honoring the
  spec's resize-race contract.
- EmbeddedEngine.start races the socket accept against guest exit, so a
  guest that dies before connecting surfaces an error instead of hanging.
- Fix a stale build_guest.dart doc comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resizing the harness window left it white at some sizes and flashed at
others. Root causes and fixes:

- surface.c: stop forcing kIOSurfaceBytesPerRow to width*4. That stride is
  not always validly aligned, so IOSurfaceCreate failed for some widths and
  surface_ring_init tore down the ring with no replacement. Let IOSurface
  choose an aligned stride (downstream reads IOSurfaceGetBytesPerRow).
- surface.c: build the new ring into temporaries and swap atomically, so a
  failed re-allocation keeps the existing ring presentable; reject zero-size.
- embedded_engine.dart: handle socket messages strictly in arrival order via
  a future chain instead of unawaited concurrent handlers, so the generation
  gate sees an up-to-date generation and stops discarding valid frames.
- embedder_harness_screen.dart: skip degenerate zero-size resize events.

Transient flicker during an active resize drag remains (a full software
relayout per resize); frame-perfect resize sync is out of scope for 3a.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xvrh
xvrh merged commit ab3aa78 into master May 15, 2026
1 check passed
@xvrh
xvrh deleted the claude/happy-cannon-aa0b9f branch May 15, 2026 19:25
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