From 4dd74ab743f8904e6142c9df1b747bed109d8162 Mon Sep 17 00:00:00 2001
From: wenkai fan
Date: Mon, 20 Jul 2026 21:32:02 -0400
Subject: [PATCH 1/9] =?UTF-8?q?feat(windows):=20P0=20=E2=80=94=20plan,=20s?=
=?UTF-8?q?pikes=20(7/7=20PASS),=20gate=20green?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
specs/windows-port/PLAN.md: the full port plan (verified-vs-assumed table,
architecture, phases, security deltas, open decisions).
specs/windows-port/SPIKES.md: Phase-0 spike report — S1 cross-process GPU
handle->ANGLE/Flutter (tearing 1.81%, copy ~0.5ms), S2 bootstrapc sandbox
model, S3 CDP io-pipes, S4 handle-identity laws (external-begin-frame
refuted on CEF 144), S5 WebAuthn viable in OSR, S6 toolchain recipe.
.gitattributes: LF-pin native/** + *.sh so cef_host_hash.sh digests match
across OSes. PORTING.md: correct producer-allocates inversion,
--remote-debugging-io-pipes, and the CEF 144 bootstrap sandbox model.
Co-Authored-By: Claude Fable 5
---
.gitattributes | 8 +
PORTING.md | 13 +-
specs/windows-port/PLAN.md | 902 +++++++++++++++++++++++++++++++++++
specs/windows-port/SPIKES.md | 179 +++++++
4 files changed, 1098 insertions(+), 4 deletions(-)
create mode 100644 .gitattributes
create mode 100644 specs/windows-port/PLAN.md
create mode 100644 specs/windows-port/SPIKES.md
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fddab01
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,8 @@
+# Native sources are hashed by tool/cef_host_hash.sh to key the prebuilt
+# cef_host artifact in GCS. The hash is over RAW BYTES, so a Windows checkout
+# with core.autocrlf=true would compute a different digest than the macOS
+# publisher for the same commit — forever missing the prebuilt. Pin LF for
+# everything the hash covers (and shell scripts, which break under CRLF).
+packages/**/native/** text eol=lf
+packages/**/tool/*.sh text eol=lf
+*.sh text eol=lf
diff --git a/PORTING.md b/PORTING.md
index 4665088..19027c2 100644
--- a/PORTING.md
+++ b/PORTING.md
@@ -57,7 +57,12 @@ lives in the Flutter app's process and:
- Spawns **one `cef_host` subprocess per profile** (`--ipc --cdp-port
--profile-dir --allowed-schemes --ephemeral` as argv), then creates **N
browsers in that process** via `opCreateBrowser` frames carrying `url, width,
- height, dpr, iosurface-id` + a `browserId`. `--profile-dir` is always passed; a
+ height, dpr` + a `browserId`. **The create carries no surface id** — the
+ surface model is *producer-allocates*: `cef_host` mints (and on resize
+ re-mints) the shared surface itself and announces it to the host via
+ `opPresent` frames carrying `{surface-id, srcW, srcH}` (the host promotes a
+ frame only when its dims match the expected size — see
+ `docs/OSR_SCALE_MISMATCH.md`). `--profile-dir` is always passed; a
persistent (named) profile points it at a stable cache dir, while an ephemeral
profile points it at a unique throwaway temp dir and adds `--ephemeral` (so the
host's CDP / mock-keychain guards fire only for a real persistent profile —
@@ -85,11 +90,11 @@ reused verbatim. Only these seams are macOS-specific (file:line are into
| Seam | macOS reference | What your platform provides |
| --- | --- | --- |
-| **Shared surface** — receive painted frames and present them to the host texture. CEF delivers either software `OnPaint` (CPU buffer) or `OnAcceleratedPaint` (a shared GPU texture handle). | `OnPaint` / `OnAcceleratedPaint` + the `IOSurface*` ops (~lines 346–450); `g_surface` (~133). macOS uses an IOSurface-backed `CVPixelBuffer`. | **Windows**: a shared D3D11 texture / DXGI keyed-mutex handle. **Linux**: shared memory or a DMA-buf, presented via the platform texture. Look the surface up by the `--iosurface-id`-equivalent handle the host passes. |
+| **Shared surface** — receive painted frames and present them to the host texture. CEF delivers either software `OnPaint` (CPU buffer) or `OnAcceleratedPaint` (a shared GPU texture handle). | `OnPaint` / `OnAcceleratedPaint` + the `IOSurface*` ops (~lines 346–450); `g_surface` (~133). macOS uses an IOSurface-backed `CVPixelBuffer`. | **Windows**: a shared D3D11 texture. Note (verified against CEF 144 `cef_types_win.h` + Flutter engine `external_texture_d3d.cc`): CEF's `OnAcceleratedPaint` delivers an **NT** handle (no keyed mutex, non-owning, pool-released when the callback returns — open it synchronously via `ID3D11Device1::OpenSharedResource1`), while Flutter's ANGLE compositor requires a **legacy** `D3D11_RESOURCE_MISC_SHARED` handle (`EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE`); the two are mutually exclusive on one texture, so exactly one `CopyResource` NT→legacy is structurally mandatory (macOS also blits — `CompositeMetalLocked`). **Linux**: shared memory or a DMA-buf, presented via the platform texture. Either way the model is **producer-allocates**: `cef_host` mints the shared surface and announces it via `opPresent` — the host does NOT pass a surface id in the create (the macOS field name differs too: `shared_texture_io_surface` vs Windows `shared_texture_handle`, so the accelerated-paint glue takes a per-OS `#if`, not a typedef). |
| **IPC transport** — a framed bidirectional byte stream to the host. Wire format: 4-byte big-endian length prefix, then `[u32 browserId][opcode][payload]` (`browserId 0` = process-level: `opReady`, process logs, `opShutdown`). | `WriteAll`/`ReadAll` (207–235), `SendFrame` (~237–249), `ConnectUnixSocket` (1341+), the read loop (~1140). Unix domain socket. | **Windows**: a named pipe. **Linux**: a Unix domain socket (reuse as-is). Keep the same length-prefixed framing and the `browserId` dimension. |
| **App / run loop** — give CEF a host application + message loop, and a per-profile cache dir. | `CefHostApplication : NSApplication` (304–330); `@autoreleasepool` + `sharedApplication` (1420+); `--profile-dir` → `settings.root_cache_path` cache; `_NSGetExecutablePath` (1335). | **Windows/Linux**: the platform's CEF message-loop integration (`CefRunMessageLoop` or OS loop) and the caller-supplied `--profile-dir` (per-profile, persistent) as the cache path. |
-| **Agent-control CDP pipe (optional)** — for `agentControl`, the host launches `cef_host` so Chromium's `--remote-debugging-pipe` (NUL-framed CDP JSON) rides **inherited file descriptors 3=read / 4=write** instead of a TCP port. | `CefProfileHost.launchViaPosixSpawn` (Swift): two `pipe()` pairs `dup2`'d onto fds 3/4 via `posix_spawn_file_actions`, parent ends marked `FD_CLOEXEC`; `cef_host` appends `remote-debugging-pipe` in `OnBeforeCommandLineProcessing`. The relay (`CdpRelay.swift`) + per-tile Target filter are platform-agnostic; only the fd placement is OS-specific. | **Linux**: reuse the `posix_spawn_file_actions` fd-3/4 recipe verbatim. **Windows**: a different model — `CreatePipe` + `STARTUPINFOEX` `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` with `bInheritHandles`, and Chromium takes the two pipe **HANDLEs** as `--remote-debugging-pipe` args (not fds 3/4). The default (non-agent) launch also needs a native `exec`/`CreateProcess` (Foundation.Process is macOS-only). |
-| **Sandbox** — bring the child processes into the Chromium sandbox in a signed/release build. | `process_helper.mm`: `CefScopedSandboxContext` (release only); `settings.no_sandbox` toggled by `CEF_HOST_ADHOC` (1426/1433). | **Windows**: link the `cef_sandbox` static lib + `CefScopedSandboxContext`. **Linux**: the SUID / user-namespace sandbox helper. |
+| **Agent-control CDP pipe (optional)** — for `agentControl`, the host launches `cef_host` so Chromium's `--remote-debugging-pipe` (NUL-framed CDP JSON) rides **inherited file descriptors 3=read / 4=write** instead of a TCP port. | `CefProfileHost.launchViaPosixSpawn` (Swift): two `pipe()` pairs `dup2`'d onto fds 3/4 via `posix_spawn_file_actions`, parent ends marked `FD_CLOEXEC`; `cef_host` appends `remote-debugging-pipe` in `OnBeforeCommandLineProcessing`. The relay (`CdpRelay.swift`) + per-tile Target filter are platform-agnostic; only the fd placement is OS-specific. | **Linux**: reuse the `posix_spawn_file_actions` fd-3/4 recipe verbatim. **Windows**: a different model — `CreatePipe` + `SetHandleInformation(HANDLE_FLAG_INHERIT)` + `STARTUPINFOEX` `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`, and Chromium takes the two inherited pipe **HANDLE values** via the separate switch `--remote-debugging-io-pipes=,` (uint32-serialized HANDLEs, passed **in addition to** the bare `--remote-debugging-pipe` — verified against `content_switches.cc` at chromium 144.0.7559.254; `--remote-debugging-pipe` itself takes no arguments on any platform). The default (non-agent) launch also needs a native `exec`/`CreateProcess` (Foundation.Process is macOS-only). |
+| **Sandbox** — bring the child processes into the Chromium sandbox in a signed/release build. | `process_helper.mm`: `CefScopedSandboxContext` (release only); `settings.no_sandbox` toggled by `CEF_HOST_ADHOC` (1426/1433). | **Windows**: CEF 144 ships **no `cef_sandbox.lib`** — the current model (`cef_sandbox_win.h`, CEF issue #3824) is: build your app as a **DLL** exporting `RunWinMain`/`RunConsoleMain` and launch it through the prebuilt `bootstrap.exe`/`bootstrapc.exe` from the CEF distribution, which establishes the sandbox before entering your code. **Linux**: the SUID / user-namespace sandbox helper. |
| **Framework / resource path** — point CEF at the CEF binary distribution. | `CefScopedLibraryLoader::LoadInMain`/`LoadInHelper`; `framework_dir_path` / `main_bundle_path` (1453/1466). | The equivalent paths for your bundle layout. |
| **Build + bundle + sign** | `native/build_cef_host.sh` (CMake, `CEF_MULTI_PROCESS` / `CEF_HOST_ADHOC` flags), `tool/bundle_cef_host.sh`. | A platform build that produces `cef_host` + the CEF runtime, and a bundling step into the host app. |
diff --git a/specs/windows-port/PLAN.md b/specs/windows-port/PLAN.md
new file mode 100644
index 0000000..b418a8f
--- /dev/null
+++ b/specs/windows-port/PLAN.md
@@ -0,0 +1,902 @@
+# flutter_cef — Windows port
+
+Goal: `CefWebView` renders, composites, and behaves on Windows as it does on
+macOS — same app-facing API, same method-channel contract, same wire protocol
+(with one lockstep bump), same test gates — via a new endorsed
+`flutter_cef_windows` package and a Windows build of `cef_host`. This is a
+second platform, not a porting chore.
+
+## 1. Bottom line
+
+**Feasible. No fundamental blocker.** Every load-bearing unknown is either
+CONFIRMED against primary sources (§2) or has a named Phase-0 spike with a
+documented fallback (§3).
+
+- **Cost: ~30 engineer-weeks to full parity** (honest range 26–34; the three
+ competing plans priced 23.5/26/31.5 and the low ones underpriced their
+ riskiest phases). One senior C++/Win32/D3D11 engineer ≈ 6–8 months calendar;
+ a second engineer parallelizes only after first pixel (~4.5–5.5 months for
+ two).
+- **First demonstrable value at ~week 5**: a live, interactive web page inside
+ a Flutter `Texture` widget on Windows, software-rendered, macOS untouched by
+ construction. That milestone is the go/no-go checkpoint; abandoning there
+ costs ~6 weeks and leaves macOS byte-identical.
+- **Reuse ledger** (wc -l, verified): Dart carries over ~wholesale — 2,287
+ lines with ~250 lines of platform-conditional edits. Native host: ~2,100 of
+ main.mm's 2,924 lines are already plain C++ and move into a shared core;
+ ~800–900 lines get a Windows twin. The 4,226 lines of Swift plugin/relay
+ (CefProfileHost 1,710 + CdpRelay 919 + FlutterCefPlugin 780 + CefWebSession
+ 727 + policies 90) do **not** carry — Windows plugins are C++; expect
+ ~5,500–6,500 C++ lines out (EncodableMap extraction is 3–6 lines where Swift
+ needs 1). All ~1,050 lines of build/bundle/sign tooling (podspec, codesign,
+ fetch/publish scripts) are macOS-shaped and get Windows rewrites.
+- **One genuinely novel problem with zero prior art anywhere**: the shared GPU
+ texture crosses *three* processes (CEF GPU proc → cef_host → Flutter/ANGLE).
+ Every existing Windows CEF-in-Flutter project (webview_cef,
+ flutter-webview-windows, CefSharp, JCEF) runs the browser in-process. macOS
+ gets the extra hop free via `IOSurfaceIsGlobal` (main.mm:773); Windows has no
+ equivalent primitive that is both cross-process and acceptable to ANGLE.
+ Spiked in week 1 with an **animated** pass criterion and a pre-designed
+ fallback (§3 S1).
+
+Before committing, answer the buy-vs-build question in §10 honestly: if
+Windows users only need "a webview", WebView2 delivers 80% of the value for
+~5% of this cost. This port is justified only by what WebView2 cannot do:
+multi-tile OSR compositing into Flutter's scene, per-tile CDP token isolation,
+out-of-process crash isolation, shared profile hosts.
+
+## 2. Verified vs. assumed
+
+Every claim below was checked against primary sources (CEF branch 7559 =
+CEF 144, Chromium tag 144.0.7559.254, the CEF builds CDN, ANGLE extension
+specs, local Flutter engine source). Anything UNVERIFIED maps to a §3 spike.
+
+| # | Claim | Status | Evidence |
+|---|---|---|---|
+| 1 | The pinned CEF build (`144.0.27+g3fae261+chromium-144.0.7559.254`, build_cef_host.sh:17) ships windows64/windowsarm64/windows32 minimal+standard tarballs, channel stable | **CONFIRMED** | cef-builds.spotifycdn.com index.json; windows64_minimal built 2026-06-03 |
+| 2 | Windows `OnAcceleratedPaint` delivers an **NT** `HANDLE`, created **without a keyed mutex**, non-owning, pool-released when the callback returns; must be opened via `ID3D11Device1::OpenSharedResource1` synchronously inside the callback | **CONFIRMED** | include/internal/cef_types_win.h + include/cef_render_handler.h (branch 7559), verbatim; CEF #4027 |
+| 3 | macOS struct field is *named differently* (`shared_texture_io_surface` vs `shared_texture_handle`) — a `#if` on the field is mandatory, a typedef is not enough | **CONFIRMED** | cef_types_mac.h vs cef_types_win.h |
+| 4 | CEF-143 null-shared-handle regression (#4057) is absent from our 144.0.27 pin | **CONFIRMED** | issue closed 2025-12-19 via PR #4059; pinned build dated 2026-06-03 |
+| 5 | Flutter's DXGI path binds via `EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE`, which requires a **legacy** `D3D11_RESOURCE_MISC_SHARED` handle; NT and legacy are mutually exclusive on one texture ⇒ **exactly one `CopyResource` is structurally mandatory** (note: macOS is not zero-copy either — CompositeMetalLocked blits, main.mm:850-985) | **CONFIRMED** | engine external_texture_d3d.cc:98-106 (local source); ANGLE EGL_ANGLE_d3d_share_handle_client_buffer spec |
+| 6 | `kFlutterDesktopGpuSurfaceTypeD3d11Texture2D` is unusable from a plugin (ANGLE rejects textures not created on its own, unexposed, device) | **CONFIRMED** | ANGLE EGL_ANGLE_d3d_texture_client_buffer spec; Renderer11::getD3DTextureInfo |
+| 7 | `TextureRegistrar::UnregisterTexture` is **asynchronous** on Windows; the engine may invoke the surface callback after it returns | **CONFIRMED** | flutter_texture_registrar.h:165 + flutter_windows_texture_registrar.cc:79-101 |
+| 8 | `release_callback` fires **every frame**; EGL surface is re-imported only when `handle` changes; omitted `struct_size` ⇒ SAFE_ACCESS reads `handle` as null ⇒ silent black texture | **CONFIRMED** | external_texture_d3d.cc:83-115 |
+| 9 | If ANGLE fails to init, `RegisterTexture` still returns a valid id and the callback is never invoked (blank, no log) | **CONFIRMED (substance)** | flutter_windows_texture_registrar.cc:26-30 guards only `!gl_` |
+| 10 | CEF 144 ships **no `cef_sandbox.lib`** for Windows; the model is a client DLL exporting `RunWinMain`/`RunConsoleMain` + prebuilt `bootstrap.exe`/`bootstrapc.exe`; PORTING.md:92 and specs/cross-platform/PLAN.md:53 are **stale** | **CONFIRMED (header) / corroborated (tarball)** | cef_sandbox_win.h declares the bootstrap entry points; CEF #3824. Tarball enumeration = spike S2 |
+| 11 | `--remote-debugging-pipe` takes **no** handle arguments; the handle-passing switch is the separate `--remote-debugging-io-pipes=,` (uint32-serialized inheritable HANDLEs), used in addition. **PORTING.md:91 is REFUTED** | **CONFIRMED** | content_switches.cc at tag 144.0.7559.254 |
+| 12 | Windows OSCrypt = DPAPI-wrapped AES-256-GCM key in Local State (CEF: `LocalPrefs.json` in the cache dir); always available, signing-independent, decryptable by **any same-user process**. The macOS ad-hoc→mock-keychain→ephemeral-downgrade rail (main.mm:1598-1599, :2761-2768) has nothing to trigger on Windows | **CONFIRMED (substance)** | components/os_crypt behavior; `--use-mock-keychain` is Apple-only, `--password-store` Linux-only |
+| 13 | `kOpPresent` is already 12 bytes `{u32 sid}{u32 srcW}{u32 srcH}` — the OSR_SCALE_MISMATCH size-gate (Fix-1) is **landed**; and `kOpCreateBrowser` carries **no** surface id (producer-allocates). PORTING.md's seam table says the opposite and would lead a porter into a deadlocking consumer-allocates model | **CONFIRMED** | main.mm:654-665, main.mm:137 |
+| 14 | No `.gitattributes` exists; `tool/cef_host_hash.sh` hashes raw bytes ⇒ an `autocrlf=true` Windows checkout computes a different prebuilt digest than the macOS publisher forever | **CONFIRMED** | repo inspection |
+| 15 | A legacy `MISC_SHARED` handle minted in cef_host opens via ANGLE in the **separate** Flutter process, tear-acceptably, at 12 tiles/60fps | **UNVERIFIED — no prior art exists** | → spike **S1** |
+| 16 | `OnBeforeCommandLineProcessing` lands before Chromium reads `--remote-debugging-io-pipes`; a bootstrap-launched process inherits pipe handles + forwards argv into `RunConsoleMain` | **UNVERIFIED (inferred from the working macOS `--cdp-pipe` pattern)** | → spike **S3** |
+| 17 | CEF's shared handle value stability across frames/resizes; whether mutating descriptor `handle` reproduces flutter/flutter#154716's engine GPU leak; whether size-bucketing suppresses it | **UNVERIFIED** | → spike **S4** |
+| 18 | WebAuthn/passkeys complete in a windowless browser on Windows (webauthn.dll's Windows Hello modal takes an HWND; OSR `GetWindowHandle()` is NULL; main.mm:1059-1067 deliberately exempts WebAuthn from the permission gate) | **RESOLVED (SPIKES.md S5): the native passkey modal appears even with `SetAsWindowless(nullptr)` — Windows brokers WebAuthn UI out-of-process via CredentialUIBroker.exe. v1 supports passkeys; plumb the runner HWND into `SetAsWindowless` for dialog placement** | spike **S5** — PASS |
+| 19 | `PluginRegistrarWindows::GetGraphicsAdapter` exists at plugin-registrar level | **VERSION-DEPENDENT** — 3.38.8+ only; on 3.32.8 it is `FlutterView::GetGraphicsAdapter`. CI pins 3.38.8 (ci.yaml:20); local box is 3.32.8 | → all spikes standardize on 3.38.8 |
+| 20 | CEF headers compile under Flutter's `apply_standard_settings` (`_HAS_EXCEPTIONS=0` + `/W4 /WX`); plugin symlinks + out-of-tree native sources resolve under `generated_plugins.cmake` | **UNVERIFIED (webview_cef's workaround taken on faith)** | → spike **S6** |
+| 21 | A Windows build of the consumer's custom Flutter engine (run_conformance_oracle.sh:25 → `work_canvas/scripts/campus-flutter-engine.sh`; the ci.yaml 3.38.8 pin rationale) exists, and whether its patches touch ExternalTextureD3d/ANGLE | **UNVERIFIED — gates the entire oracle strategy** | → §10 decision D5, resolved before P8 |
+
+## 3. Phase 0 — spikes (throwaway code, outside the package tree)
+
+All spikes on Flutter **3.38.8** + VS2022. Output is a written PASS/FAIL
+report per spike with the fallback promoted into the architecture on FAIL.
+Repo changes in P0 are limited to: `.gitattributes` (`packages/**/native/**
+text eol=lf`), and the PORTING.md corrections (seam-table producer-allocates
+inversion; line drift; the :91 CDP claim).
+
+- **S1 — Cross-process legacy handle → ANGLE, ANIMATED.** Process A creates
+ an `ID3D11Texture2D` (B8G8R8A8, `MISC_SHARED`) on the LUID adapter reported
+ by the Flutter engine, renders a **rolling frame counter + moving pattern at
+ 60fps across 12 tiles**, ships `IDXGIResource::GetSharedHandle()` values
+ over a pipe. Process B is a minimal Flutter app feeding the raw u64 into
+ `FlutterDesktopGpuSurfaceDescriptor.handle`. Also: feed a real CEF 144
+ `OnAcceleratedPaint` NT handle → `OpenSharedResource1` → `CopyResource` →
+ legacy handle, measuring copy cost. **PASS = per-frame content assertion
+ holds, a measured tearing/partial-frame rate is reported (not just "it lit
+ up"), and a destroy-while-bound probe demonstrates what the consumer
+ observes when the producer frees the texture** (this drives the lifetime
+ protocol, §4.4). *Fallback A*: plugin-side bridge — cef_host
+ `DuplicateHandle`s the NT handle into the plugin (plugin passes its process
+ HANDLE at spawn); plugin opens + copies (one extra hop, a D3D device in the
+ plugin). *Fallback B*: permanent software path only (works — shipped in P3
+ anyway — more CPU, no 12-tile/60fps bar).
+- **S2 — Sandbox/bootstrap model + tarball enumeration.** Download the actual
+ windows64 minimal tarball for the pin; enumerate it (confirm no
+ cef_sandbox.lib, confirm bootstrap.exe/bootstrapc.exe, read
+ cef_sandbox_win.h as shipped). Build the smallest sandboxed OSR frame as
+ cef_host.dll + `RunConsoleMain` + bootstrapc.exe; verify inherited HANDLEs
+ and argv survive the bootstrap entry; verify the `icacls … *S-1-15-2-2`
+ LPAC grant requirement. *Fallback*: `no_sandbox=true` v1 with the posture
+ bit surfaced (§7) and a hard pre-GA deadline.
+- **S3 — CDP io-pipes.** `CreatePipe` ×2 + `SetHandleInformation` +
+ `STARTUPINFOEX` `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` + `CreateProcessW`;
+ child injects `--remote-debugging-pipe` **and**
+ `--remote-debugging-io-pipes=,` in `OnBeforeCommandLineProcessing`;
+ drive one NUL-framed `Target.getTargets` round trip, including through the
+ bootstrap entry from S2. *Fallback*: CRT lpReserved2 fd-3/4 block (last
+ resort — Chromium issue 40259890 documents file corruption when fds 3/4 are
+ not real pipes); second fallback: loopback `--remote-debugging-port` behind
+ the relay, accepting the unauthenticated-port window.
+- **S4 — Handle identity + resize.** Log CEF's `shared_texture_handle` across
+ 5,000 frames and across resizes at 12 tiles (stable / recycled / aliased?).
+ Determine whether mutating the descriptor `handle` reproduces
+ flutter/flutter#154716's GPU leak and whether a 64-px size lattice (crop via
+ `visible_width/height`) suppresses it. *Fallback*: media_kit's
+ unregister/re-register-new-texture-id strategy — which needs a new
+ `textureChanged` event, since Dart ignores `resize`'s return today
+ (cef_web_controller.dart:873-879).
+- **S5 — WebAuthn/passkeys.** Load a WebAuthn test page in a windowless CEF
+ 144 browser with (a) a hidden `WS_CHILD` parent HWND, (b) a visible owner.
+ Does the Windows Hello modal appear, and parented to what? *Fallback*:
+ document passkeys as unsupported on Windows v1 (they are already
+ entitlement-gated and out of scope on macOS per
+ specs/persistent-profiles/PLAN.md:7-9), or require a visible owner HWND
+ plumbed from the runner.
+- **S6 — Toolchain probes** (half-day each): compile one CEF header under
+ `apply_standard_settings` (`_HAS_EXCEPTIONS=0` `/W4 /WX`; **RESULT
+ (SPIKES.md S6a): headers compile CLEAN under stock flags — the entire fix
+ is `NOMINMAX` per target + wrapper rebuilt per-config with
+ `-DCEF_RUNTIME_LIBRARY_FLAG=/MD` (/MDd for Debug); no `cxx_std_20`, no
+ warning relaxations**); `add_subdirectory` through
+ `flutter/ephemeral/.plugin_symlinks` into out-of-tree native sources
+ (Developer-Mode symlink requirement?); confirm native tar.exe (bsdtar)
+ decompresses the .tar.bz2 on the CI image; `Get-Command cmake` on a stock
+ VS2022 box (it is NOT on PATH — the dev-setup doc must say so).
+
+**P0 gate:** spike report committed to `specs/windows-port/SPIKES.md` with a
+verdict per spike and, for every FAIL, the fallback promoted into §4;
+`.gitattributes` landed; PORTING.md corrected; empty diff elsewhere.
+
+## 4. Architecture
+
+### 4.1 Dart (`packages/flutter_cef_windows` + shared-Dart edits)
+
+New package mirrors flutter_cef_macos exactly: `implements: flutter_cef`,
+`platforms: windows: {pluginClass: FlutterCefPlugin, dartPluginClass:
+FlutterCefWindows}`, a 19-line `registerWith()` setting
+`FlutterCefPlatform.instance = MethodChannelFlutterCef()`. Root pubspec gains
+`windows: default_package: flutter_cef_windows` + path dep (today only
+`macos:` is declared — a Windows app gets `MissingPluginException` on every
+call). `FlutterCefPlatform` stays channel-only; no new interface members.
+
+Shared-Dart changes (app-facing API frozen; all additive/conditional):
+
+- **`CefKeyProfile`** (`enum {mac, win}` in platform_interface, derived from
+ `defaultTargetPlatform`, `@visibleForTesting` override so both suites run on
+ one host). Drives:
+ - Accelerator: `isMetaOnly` (cef_web_view.dart:499-555) → `isAcceleratorOnly`
+ keyed on the profile — Ctrl on Windows for copy/cut/paste/selectAll/undo,
+ Ctrl+Y **and** Ctrl+Shift+Z for redo, Ctrl+`=`/`-`/`0` zoom, Ctrl+F. The
+ ⌃⌘Space branch (:487-491) and `_seedCaretRect` become mac-only. Do **not**
+ set `kCefEventFlagCommandDown` from `isMetaPressed` on Windows (:358-366)
+ — the Win key must not present as `metaKey`.
+ - Key codes: new `cefWinNativeKeyCode(PhysicalKeyboardKey, {repeat})`
+ returning a WM_KEYDOWN-shaped lParam (scan code bits 16-23, extended bit
+ 24) — the only value from which Blink derives DOM `event.code`
+ (`ui::KeycodeConverter::NativeKeycodeToDomCode`); `character = 0` for
+ editing/navigation keys (no NSEvent 0xF700 codepoints). Extend
+ `cefWindowsKeyCode` (cef_input.dart:180-188) for F1-F12, `VK_OEM_*`,
+ Insert, numpad — those return 0 today on **both** platforms.
+- **`loadFile` bug** (cef_web_controller.dart:812): `file://C:\path` →
+ `Uri.file(absolutePath, windows: …)` = `file:///C:/dev/page.html`.
+- **Create-failure state**: `_ensureSession` awaits `create()` inside an
+ un-awaited post-frame callback (cef_web_view.dart:259-267, :305-306); wrap
+ it so `MissingPluginException`/native errors render a failure widget rather
+ than a permanent placeholder.
+- **`CefSurfaceInfo.surfaceId`**: no shape change (Dart int is 64-bit).
+ Re-document (cef_events.dart:82-100 + cef_web_controller.dart:139-146) as
+ "an opaque platform surface token — global IOSurface id on macOS, DXGI
+ legacy share handle on Windows".
+- Multi-click thresholds: `GetDoubleClickTime`/`SM_CXDOUBLECLK` via a one-shot
+ `getInputMetrics` channel call on Windows; macOS keeps its constants.
+- `showEmojiPicker` → `PlatformException(MethodNotImplemented)` on Windows
+ (Win+`.` is shell-owned; `SendInput` synthesis is fragile and steals
+ focus). `performSelector` documented mac-only. The example app's emoji
+ button (example/lib/main.dart:189-193) is hidden per-profile.
+- **IME**: keep `enableDeltaModel: true`; the Win32 embedder never emits
+ deltas, so Windows lands on the `updateEditingValue` fallback (:802-823) —
+ currently untested against the deliberately-empty scratch buffer. P7
+ rebuilds it as a diff-against-last-known-value strategy selected by
+ `CefKeyProfile`, retaining `setComposingRect`/`setEditableSizeAndTransform`
+ (they drive `ImmSetCandidateWindow`); the caret-seed and re-click `show()`
+ AppKit hacks become mac-only.
+
+### 4.2 Windows host plugin (C++, `packages/flutter_cef_windows/windows/`)
+
+`flutter::Plugin` + `PluginRegistrarWindows` + `MethodChannel`
+/ `StandardMethodCodec`. Infrastructure that must exist before anything else
+compiles (Windows plugins get **no** task runner; `InvokeMethod` and
+`MarkTextureFrameAvailable` are platform-thread-only):
+
+- `platform_dispatcher` (~120 LOC): message-only HWND (`HWND_MESSAGE`) +
+ `PostMessage` draining a mutex-guarded deque — replaces ~40
+ `DispatchQueue.main.async` sites.
+- `delayed_scheduler` (~150 LOC): thread + `priority_queue` +
+ `condition_variable::wait_until` — replaces the 10
+ `DispatchQueue.global().asyncAfter` timers in CefProfileHost.swift.
+- `named_pipe_transport` (~300 LOC): `\\.\pipe\fcef-<128 random bits>`,
+ `PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE`
+ (defeats squatting), `PIPE_TYPE_BYTE | PIPE_READMODE_BYTE |
+ PIPE_REJECT_REMOTE_CLIENTS`, explicit current-user-SID DACL (**never** a
+ NULL SD; never JCEF's `D:(A;;FA;;;WD)`); client opens with
+ `SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS`. Reader restructured around
+ OVERLAPPED + `CancelIoEx` + `WaitForMultipleObjects` (there is no
+ `shutdown()`-wakes-the-reader on named pipes); the macOS "never close a
+ handle on join timeout — deliberately leak" rule
+ (CefProfileHost.swift:999-1002) carries over verbatim.
+- `job_guard` (~60 LOC): `CreateJobObject` + `JOB_OBJECT_LIMIT_KILL_ON_JOB_
+ CLOSE` + `AssignProcessToJobObject` — kernel-guaranteed no orphaned
+ cef_host even under `TerminateProcess` of the app (strictly stronger than
+ macOS's `willTerminateNotification`), plus a `RegisterTopLevelWindowProc
+ Delegate` on `WM_ENDSESSION`. Retires the kqueue `WatchParentDeath` on this
+ side.
+
+Discipline for the CefProfileHost/CefWebSession rewrite — the highest-variance
+work in the port (all three adversarial reviews independently named it, not
+graphics):
+
+- `shared_ptr` + `enable_shared_from_this` **mandatory**, private
+ constructors so raw-pointer storage cannot compile; the ~30 `[weak self]`
+ captures become `weak_ptr::lock()`.
+- The documented lock order (`writeLock → bufferLock`; `browsersLock` /
+ `presentLock` never nested), never-block-the-reader-on-a-write, and
+ callbacks-outside-locks (CefProfileHost.swift:975-980, :1270-1275,
+ :1454-1457, :1569, :1582) are encoded as a debug lock-order verifier, not
+ comments. `assert(GetCurrentThreadId() == platform_thread_id_)` at every
+ site macOS asserts `dispatchPrecondition(.onQueue(.main))`.
+- Built under **Application Verifier + ASan from the first commit**; the P6
+ gate is a soak (cascade probe ×50 + TerminateProcess-mid-resize fault
+ injection), not one green run.
+- **Fix, don't port**: `pendingCreates` is declared writeLock-guarded
+ (CefProfileHost.swift:129) but cleared under browsersLock at :968/:1262 —
+ masked by Swift CoW, a real race in `std::vector`. Unify on writeLock (per
+ the declaration) at all five sites, on macOS too.
+- `UnregisterTexture` is async (§2 #7): the session is kept alive by a
+ `shared_ptr` captured in the completion callback — a correctness rewrite of
+ CefWebSession.swift:479-492's synchronous assumption, not a translation.
+- Process lifecycle simplifies: `CreateProcessW` (with CommandLineToArgvW-
+ correct quoting, ~40 LOC) + held `hProcess`; the entire H5 pid-ownership
+ dance (:1250-1256, :1301-1326) deletes — a HANDLE is not a recyclable
+ global name. No SIGTERM: graceful = `opShutdown` + bounded wait, escalation
+ = `TerminateProcess`. Profile lock = `CreateFileW` with `dwShareMode=0`,
+ preserving the cross-layer contract **exit code 2 + literal log
+ `profile-locked`** (main.mm:2786-2806; keyed on at FlutterCefPlugin.swift:521).
+- Profile dirs: `SHGetKnownFolderPath(FOLDERID_LocalAppData)`; 0700 becomes a
+ **protected** DACL (`D:P(A;OICI;FA;;;)`) passed to
+ `CreateDirectoryW` at creation (no inherited-ACL window) +
+ `SetNamedSecurityInfoW(…, PROTECTED_DACL_SECURITY_INFORMATION)` for an
+ existing leaf. The sanitizer (FlutterCefPlugin.swift:711-718) ports verbatim
+ **plus** Windows-only rules: CON/PRN/AUX/NUL/COM1-9/LPT1-9, trailing
+ dots/spaces, `:` (ADS), MAX_PATH.
+
+### 4.3 cef_host native — two-stage host, converged early
+
+**Stage 1 (P2–P4): a small greenfield `win/host_main.cc`, additive opcodes,
+macOS untouched.** The Windows host starts as ~700 lines hand-copying only
+what it needs, with the duplication tracked in `PORTING_DEBT.md` and a hard
+rule that the window **closes at P5 — immediately after the interactive
+milestone, NOT after verb parity** (converging two full 3,000-line hosts
+never happens in practice). It presents via a **new opcode** `kOpPresentV2 =
+0x1e` (verified unused in the table at main.mm:111-164), so `main.mm` is not
+edited, `kCefHostProtocolVersion` stays 3, and macOS regression is impossible
+by construction. The wire framing (`[u32 bodyLen][u32 browserId][u8 op]`,
+≥5/≤64 MiB guard), BE codecs, and opcode payloads are copied verbatim.
+
+Non-negotiables carried from day one: **external begin frames OFF on Windows**
+— S4 (SPIKES.md) refuted the CefSharp-#2675-era assumption: on CEF 144,
+`windowless_frame_rate=60` alone delivers 60.3 fps × 12 concurrent browsers,
+while `external_begin_frame_enabled=1` + a 16 ms `SendExternalBeginFrame` pump
+makes **only the first browser in the process ever paint** (fatal for the
+one-host-N-browsers model). The macOS `PumpBeginFrame` pacer (main.mm:295-316)
+must NOT be carried to Windows as-is; host-driven cadence, if ever wanted,
+gets its own per-browser re-spike first;
+`SetAsWindowless` on one hidden `WS_CHILD` HWND per host process (a null
+parent degrades dialogs/menus/DPI/IMM and
+browser_platform_delegate_native_win.cc bails on `!msg.hwnd`);
+`CefRunMessageLoop` on the main thread + a reader `std::thread` (same model
+as macOS, ports as-is); a `CefExecuteProcess` early-return replaces the
+5-helper-app fan-out (Windows relaunches the same exe with `--type=`;
+process_helper.mm's 85 lines collapse). The **must-port checklist** of
+security-relevant lines a greenfield host would silently drop: the
+CDP-on-persistent-profile refusal (main.mm:2757-2760), the deny-default
+`HostPermissionHandler` (:1068-1089), `disable-blink-features=
+AutomationControlled` + the `--cdp-pipe` injection pattern (:1651-1662), the
+profile flock semantics (:2786-2806), and the exit-code-2 contract.
+
+Software present path (P3, permanent): CEF software `OnPaint` → BGRA→RGBA
+swizzle **producer-side** → shared-memory section → `flutter::
+PixelBufferTexture` (engine does packed-RGBA `glTexImage2D`; no format/stride
+fields). Hardened against the flaws found in review:
+
+- **Name**: `Local\fcef-<128-bit CSPRNG hex>` communicated over the
+ authenticated pipe — never a predictable pid/bid/gen name (squattable).
+- **ACL**: explicit current-user-SID SD on `CreateFileMappingW`;
+ `GetLastError() == ERROR_ALREADY_EXISTS` → abort (squat detection).
+- **Sync**: a ring of **2–3 sections** with the presented index carried in
+ `kOpPresentV2` — a plugin-local `std::mutex` excludes nothing across
+ processes (the camera_windows `unique_lock::release()` idiom is
+ in-process-only and covers only the engine↔plugin edge, where it is still
+ used).
+- **Validation**: consumer `VirtualQuery`s the mapped extent and rejects any
+ present whose `srcH*stride` exceeds it — the section is not self-describing
+ the way an IOSurface is.
+- New section per size (never mutate a live one — JCEF's name-encodes-size
+ trick), old sections retired only after the consumer acks adoption.
+
+This path ships forever as the `IsGpuCompositingDisabled()` / ANGLE-init-
+failed fallback and as the permanent "is it CEF or is it Flutter" bisector,
+with a runtime kill-switch `FLUTTER_CEF_FORCE_SOFTWARE=1` (extending the
+repo's env-knob culture).
+
+**Stage 2 (P5): `core/platform.h`, seam-by-seam, oracle after every seam.**
+This is the split specs/cross-platform/PLAN.md P3 deferred "until two
+consumers exist" — at P5 the second consumer exists and its seam is
+empirically known. Extraction order = decreasing certainty, macOS conformance
+oracle run after **every** seam, not once at phase end:
+
+1. `PlatformStream` — `Read/Write/Shutdown` (5 POSIX calls total on macOS;
+ framing/codecs/opcode switch already pure C++).
+2. `PlatformPaths` — exe/resource/cache dirs (~40 LOC; Windows: flat layout,
+ `GetModuleFileNameW`, no `CefScopedLibraryLoader`, no framework paths).
+3. `PlatformProcessGuard` — parent-death watch, profile lock, rlimit
+ (deleted on Windows).
+4. `PlatformApp` — run loop + sandbox init (the 16-line NSApplication
+ subclass deletes on Windows).
+5. `PlatformSurface` — **last**, when best understood: `Ensure(w,h)→Token`,
+ `Lock/Unlock` (+base/stride), `BlitFromShared(const
+ CefAcceleratedPaintInfo&)` (takes the CEF struct so the mac/win
+ field-name divergence stays inside the platform layer; must complete
+ before returning — same pool contract as CompositeMetalLocked's
+ `waitUntilCompleted`), `Token()`, `Release()`. `Slot`'s two macOS members
+ (`IOSurfaceRef surface`, `id dst_mtl` — referenced from five
+ external sites at main.mm:781/927/1522/1802/1850) collapse to an opaque
+ handle.
+
+Then the win host is rebased onto `core/`, deleting the Stage-1 duplicates,
+and the wire converges at **protocol v4** on both platforms in lockstep
+(main.mm:108 ↔ CefProfileHost.swift:42 — the skew check is the point; never
+fork the wire per-OS). The v4 present payload is **tagged and
+variable-length** so a Linux port does not force v5 (its
+`plane_count/modifier/planes[]` dma-buf provably does not fit a u64):
+
+```
+kOpPresent v4: {u8 kind}{u32 srcW}{u32 srcH}{payload}
+ kind 0 = u32 IOSurface global id (macOS)
+ kind 1 = u64 DXGI legacy share handle (Windows GPU)
+ kind 2 = u8 len + utf8 shm name, u32 stride, u8 ringIndex (Windows software)
+ kind 3 = reserved (multi-plane dma-buf, Linux)
+```
+
+Landing tree: repo-root `native/{cef_host/{core,platform/mac,platform/win},
+cef_core/{policy,cdp_relay,test}}`, with build_cef_host.sh, the podspec
+`prepare_command`/`:after_compile`, and `cef_host_hash.sh`'s find-root
+re-pointed (one deliberate prebuilt-cache miss). CocoaPods can't glob across
+package boundaries; use one-line `#include` shim TUs under `Classes/shim/`.
+Also in stage 2: DevTools needs `window_info.SetAsPopup(NULL, "DevTools")`
+(a default CefWindowInfo is not a top-level window on Windows, unlike
+main.mm:2144-2145's comment); `GetScreenPoint` returns **device px** on
+Windows vs DIP on macOS; `RealScreenDip` → `MonitorFromWindow` +
+`GetMonitorInfoW` (top-left already, no Cocoa flip) with a per-platform
+`kChromeH` (87 is "~ real Chrome on macOS" and leaks a wrong `outerHeight`
+fingerprint otherwise); `DoKey`'s always-set `character` workaround
+(main.mm:2297-2303, a macOS-Blink dedupe) goes behind the platform seam and
+is settled empirically in P7 (risk: doubled Backspace/arrows).
+
+### 4.4 GPU surface path (P8) — producer-allocates, with a real lifetime protocol
+
+Retained from macOS: cef_host mints the bridge texture and publishes a token;
+the size-tagged present + promote-only-on-size-match semantics (already
+landed, §2 #13) carry over unchanged. The chain:
+
+1. Plugin reads the engine adapter LUID
+ (`PluginRegistrarWindows::GetGraphicsAdapter`, 3.38.8+) and passes
+ `--adapter-luid=,` on cef_host argv; cef_host creates its
+ `ID3D11Device` on that adapter (`D3D_DRIVER_TYPE_UNKNOWN`). Without this,
+ hybrid-GPU laptops fail the open intermittently.
+2. `OnAcceleratedPaint`: `OpenSharedResource1(info.shared_texture_handle)` →
+ `CopyResource` into the slot's bridge texture (B8G8R8A8, `RENDER_TARGET |
+ SHADER_RESOURCE`, **legacy `MISC_SHARED`**) → `Flush` — synchronously
+ inside the callback (§2 #2). Bridge textures are allocated on a 64-px
+ lattice with `visible_width/height` cropping so the published handle
+ rarely changes (S4 validates this against flutter/flutter#154716).
+3. `kOpPresent(kind=1)` publishes `IDXGIResource::GetSharedHandle()`.
+4. **Lifetime protocol — the fix for the fatal flaw in the winning design.**
+ A legacy share handle is a bare u64 the consumer cannot refcount by
+ itself; without a protocol, cef_host destroying a bridge texture on
+ resize while ANGLE binds asynchronously on the raster thread is a
+ use-after-free (best case black tile; worst case the driver recycles the
+ value onto a *different* live texture — cross-tile content leak). Two
+ belts, both mandatory:
+ - The plugin owns a small D3D11 device (same LUID) and, on each **new**
+ token, `OpenSharedResource`s it itself, holding the ComPtr — a
+ cross-process kernel reference that keeps the resource alive
+ independent of the producer, and a validity probe (a failed open =
+ reject the present, keep the current texture, log).
+ - A new opcode `kOpSurfaceAck {u64 token}` (plugin → host): cef_host
+ keeps every superseded bridge texture alive until the plugin acks
+ adoption of its successor (first successful engine populate with the
+ new handle). Mirrors the macOS retain semantics of
+ `adoptSurfaceLocked`'s CVPixelBuffer.
+5. Consumer: `flutter::GpuSurfaceTexture(kFlutterDesktopGpuSurfaceType
+ DxgiSharedHandle, cb)` in a `map>`
+ (registrar stores a raw pointer — the variant outlives registration);
+ descriptor with `struct_size` set, `visible_*` from the present,
+ `format = kFlutterDesktopPixelFormatNone`; a heap `DescriptorHolder`
+ holding the ComPtr, released in `release_callback` (fires every frame).
+ Startup probe: null `GetGraphicsAdapter()` ⇒ software path + loud log;
+ plus webview_cef's 5-second "no accelerated frame arrived" warning task.
+
+**Tearing is accepted as a structural v1 limitation, stated, not
+discovered**: ANGLE's legacy-handle path excludes keyed mutex
+(`MISC_SHARED_KEYEDMUTEX` is mutually exclusive; the engine never queries
+`EGL_ANGLE_keyed_mutex`), so GPU mutual exclusion is unreachable through
+Flutter's texture API. This is *parity*: README's Roadmap already documents
+"the IOSurface is single-buffered, so very fast-updating pages can tear
+slightly" on macOS. A 2-slot bridge ring is evaluated only against S1's
+measured tearing rate — it pays `eglCreatePbufferFromClientBuffer` +
+`eglBindTexImage` every frame and may hit #154716; adopt on measurement, not
+principle.
+
+### 4.5 CDP relay — one audited boundary, Windows-first validation
+
+Shared C++ `native/cef_core/cdp_relay`: the pure policy
+(`filterClientToPipe`, `filterPipeToClient`, `demuxPipeToClient`,
+`rewriteOutgoingId`, `tokenAcceptable` — already factored `internal` so a
+socket-free test can drive them), inverted from side-effecting to
+value-returning (`struct FilterResult {optional to_pipe;
+vector to_client; optional self_command;}` — ~1 day, makes
+the core strictly more testable than the Swift), plus HTTP head parse,
+RFC-6455 framing, SHA-1/CSPRNG behind a BCrypt/CommonCrypto shim, NUL pipe
+framing, behind an ~8-function socket backend (`posix.cc` / `winsock.cc`).
+JSON via CEF's `cef_parser.h` (already linked; no new supply chain). All 60+
+CdpRelayFilterTests assertions (incl. the 18 token edge cases and the
+`pipeId = (relayId<<21)|seq` bit-layout cases) transcribe into a ctest target
+that links neither CEF nor D3D, wired into **both** CI jobs — a CDP
+regression once shipped precisely because CI wasn't running the filter suite
+(ci.yaml:33-36).
+
+Winsock redesigns (not translations): `SO_EXCLUSIVEADDRUSE` replaces
+`SO_REUSEADDR` (CdpRelay.swift:161 — on Windows the latter is a port-hijack
+primitive); `SO_RCVTIMEO`/`SO_SNDTIMEO` take a `DWORD` of **milliseconds**,
+not a `timeval` (a verbatim port silently disables the slowloris and
+stuck-client timeouts the write-isolation design depends on); `shutdown()`
+does not wake a listening `accept()` (`WSAENOTCONN`) — teardown becomes
+`WSAEventSelect(FD_ACCEPT)` + a manual-reset stop event +
+`WSAWaitForMultipleEvents`; the per-relay writer queue is **bounded** (the
+Swift DispatchQueue is unbounded).
+
+Spawn: the S3 recipe (`CreatePipe` ×2, un-inherit parent ends,
+`PROC_THREAD_ATTRIBUTE_HANDLE_LIST`, both `--remote-debugging-pipe` and
+`--remote-debugging-io-pipes`); the fd-collision relocation guard
+(CefProfileHost.swift:386-411) **deletes** — handle values are opaque.
+
+Sequencing is the point: Windows validates the C++ core end-to-end
+(multiview_probe + a real Playwright `connectOverCDP` drive) in P9; macOS is
+cut over to it in a **separate, later, independently gated phase** (P10) so
+the shipping security boundary is never touched by unproven code — and then
+CdpRelay.swift is deleted so the deny-by-default filter exists exactly once.
+
+### 4.6 Build, bundle, sign, distribute
+
+- Plugin CMake: `add_library(flutter_cef_windows_plugin SHARED …)`,
+ `apply_standard_settings`, link `flutter flutter_wrapper_plugin d3d11 dxgi
+ dxguid ws2_32 shlwapi imm32`; S6's `_HAS_EXCEPTIONS`/`/MD` fixes applied.
+ Bundling: `set(flutter_cef_windows_bundled_libraries … PARENT_SCOPE)` →
+ `install(FILES)` next to the runner exe; `locales/` via explicit
+ `install(DIRECTORY … DESTINATION ${INSTALL_BUNDLE_DATA_DIR})`; the ~250
+ flat CEF files (libcef.dll, chrome_elf.dll, ANGLE/Vulkan DLLs, icudtl.dat,
+ v8_context_snapshot.bin, *.pak). **Stamp-guarded install** so the ~500 MB
+ copy does not re-run on every `flutter build windows`
+ (`CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD` is 1 in the app template).
+ **License files ship**: CEF/Chromium LICENSE + third-party manifest +
+ the Microsoft-redistributable terms for d3dcompiler_47.dll — a legal
+ ship-blocker, owner assigned in §10.
+- Prebuilt fetch: no `pod install` hook exists — `fetch_cef_host.ps1` runs at
+ CMake configure via `execute_process`, cached under
+ `%LOCALAPPDATA%\flutter_cef`, hash-stamp short-circuit **load-bearing**
+ (configure runs every build). Failure policy matches macOS: **fail-open**
+ to build-from-source when the sidecar is unreachable, **fail-closed** on
+ digest/signature mismatch (delete the cached archive). The §2 #14
+ `.gitattributes` fix is the precondition for the shared content-hash key.
+- Sandbox (P11, per S2): cef_host becomes `cef_host.dll` exporting
+ `RunConsoleMain` + CEF's prebuilt `bootstrapc.exe`, `icacls /grant
+ *S-1-15-2-2:(OI)(CI)(RX)` LPAC grant in the install step. Sandbox ON
+ unconditionally once landed — it is signing-independent on Windows (the
+ inverse of macOS).
+- Signing/verification: Authenticode has no `--deep` tree seal and no
+ requirement DSL, so 913f0f0's fail-closed property is rebuilt as:
+ `WinVerifyTrust(WINTRUST_ACTION_GENERIC_VERIFY_V2)` (exactly-0 = success) +
+ hand-rolled **leaf-thumbprint pinning** on **every PE**, + a signed
+ catalog (or Authenticode-signed SHA-256 manifest) covering the non-PE
+ assets, + rejection of **unexpected** files in the staging dir (per-PE
+ verification says nothing about files an attacker *added*).
+ `CreateProcessW` never consults SmartScreen/MotW — exactly as
+ `posix_spawn` bypasses Gatekeeper — so this gate is the only root of
+ trust. Publisher runs in CI with an HSM/Key-Vault identity (CA/B rules
+ since 2023-06 rule out a local P12; no `security find-identity`
+ equivalent). DLL hardening in cef_host/bootstrap entry:
+ `SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32)` first statement,
+ absolute-path `LoadLibraryExW`, `SetProcessMitigationPolicy
+ (ProcessImageLoadPolicy)` — the fetch dir is user-writable, making
+ search-order hijack of libcef.dll live in dev/CI.
+- CI: a `windows-latest` job pinned to Flutter 3.38.8 — analyze ×5 packages,
+ `flutter test`, the cef_core ctest suite, a cached cef_host build. Note
+ GitHub-hosted Windows runners have **no GPU**: the conformance oracle runs
+ on a self-hosted/dev-box runner or as a documented pre-release ritual; CI
+ proper gates on the GPU-free suites. AV/Defender note in the dev docs:
+ unsigned exe + child spawning + RWX JIT + named-pipe IPC is the textbook
+ EDR silhouette; document Defender exclusions for dev and expect
+ enterprise-EDR reports as a support class.
+
+## 5. Phases
+
+Each phase ends at an explicit green gate; macOS gates are re-run at every
+phase that touches shared code (verify, don't assume).
+
+- **P0 — Spikes + repo prep** (§3). ~3 wks.
+ *Gate:* SPIKES.md verdicts committed; fallbacks promoted; `.gitattributes`
+ + PORTING.md corrections landed; empty diff elsewhere.
+- **P1 — Package skeleton + Dart conditionals + CI.** New
+ `flutter_cef_windows` (stub plugin answering `create` with a visible
+ error), root endorsement, `flutter create --platforms=windows example/`
+ (runner committed: main.cpp, flutter_window.cpp, Runner.rc, manifest —
+ decide per-monitor-V2 DPI opt-in here), the §4.1 Dart changes, Windows
+ twins of the ⌘-shortcut/key-table test groups, `/tmp` hardcodes in probes →
+ `Directory.systemTemp`, windows-latest CI job. ~1.5 wks.
+ *Gate:* analyze clean ×5 packages on both OSes; full Dart suite green on
+ both (incl. new twins); `flutter build windows` of example launches and
+ fails loudly, not silently; macOS example renders (oracle green).
+- **P2 — cef_host.exe renders a page, zero Flutter.** Standalone CMake
+ project; fetch the pinned windows64 minimal dist; `no_sandbox=true`
+ (posture bit set, §7); windowless + hidden WS_CHILD HWND + external
+ begin-frame pump; software `OnPaint` writes a swizzled PPM of a hardcoded
+ URL at frame 30. ~1.5 wks.
+ *Gate:* `cef_host.exe https://flutter.dev` writes a correct PPM at the
+ requested size. All CEF-on-Windows build archaeology (S6 fixes, /MD,
+ bsdtar) retired before any Flutter integration exists.
+- **P3 — THE SLICE: software pixels in a Flutter Texture.** Named-pipe IPC
+ (verbatim framing), six opcodes (`kOpCreateBrowser/Shutdown/Resize` in;
+ `kOpReady/Log/PresentV2` out), the hardened shm ring (§4.3), dispatcher +
+ Job Object + one reader thread + one `PixelBufferTexture`; single
+ hardcoded session. ~2 wks.
+ *Gate:* `flutter run -d windows` shows a live, correctly-scaled,
+ correctly-colored page in a `Texture` widget, **with an animated-content
+ check across a resize drag** (the ring, not just dimensions). macOS
+ re-verified: main.mm unedited, protocol still 3.
+- **P4 — Interactive.** Pointer/key/navigate/reload/stop/back/forward/
+ setVisible/invalidate opcodes; wire encodings verbatim from
+ cef_web_controller.dart:885-927; the resize supersede/coalesce machine
+ (resizeGen + 80 ms invalidate re-kick) + size gate on `srcW/srcH`
+ (promote only on match ±1px); title/url/loadState/pageStart/pageFinish/
+ progress/loadErr/console/cursor events; wheel magnitude tuned against
+ `WHEEL_DELTA` + `SPI_GETWHEELSCROLLLINES` (the sign negation at
+ cef_web_view.dart:434-436 holds; the magnitude does not). ~1.5 wks.
+ *Gate:* a human browses: links, typing, scroll, window resize across a DPI
+ boundary, Ctrl+C/V, Ctrl+±/0; zoom_soak shows no stuck-scale frames.
+ **This is the go/no-go milestone (~week 7).**
+- **P5 — Converge: core/platform.h + shared policies + protocol v4** (§4.3
+ stage 2). Seam-by-seam with the macOS oracle after every seam; win host
+ rebased onto core (PORTING_DEBT.md emptied); tagged v4 present, versions
+ bumped in lockstep both sides; the three resize/liveness policies + their
+ ~30 test vectors → `native/cef_core/policy` ctest (skip the dead
+ `shouldForcePromote`); macOS cut over to the policy lib only after ctest +
+ full macOS gates green (Swift copies + swiftc runners then deleted). ~2.5
+ wks.
+ *Gate:* macOS conformance oracle + leak soak + all four policy suites
+ green from the refactored tree; Windows P4 gate re-passes on core;
+ `git diff` shows moves + seam indirection, no logic change (except the
+ pendingCreates lock fix, called out).
+- **P6 — Multi-browser lifecycle.** One host per profile key multiplexing N
+ browsers; real sessionId routing; dispose/created/createFailed; the create
+ pacer (window=3, 8 s backstop, 6-stable-frames/0.4 s dual release),
+ first-paint watchdog, liveness sweep, and targetId resolve/epoch/retry
+ implemented **in shared `core/`** (they reason about facts cef_host
+ observes directly — OnAfterCreated, presents, the DevTools observer;
+ consumer-side placement is why they need IPC backstops). Windows uses the
+ core-side machinery from day one (greenfield validation); macOS keeps its
+ Swift pacer untouched behind the existing wire until the P10 flag-flip.
+ `processGone` reasons; `paintStalled`; the failHost-must-dispose-session
+ ordering (fix macOS C-2 here: FlutterCefPlugin.swift:496-499 leaks the
+ texture per host crash). ASan/AppVerifier soak. ~2.5 wks.
+ *Gate:* Windows cascade probe: 12/12 tiles `paints>0` on one shared host,
+ run ×50; TerminateProcess-mid-resize fault injection: every session gets
+ `processGone`, zero texture/handle leak, zero crash; second app on a named
+ profile → `processGone('locked')`. macOS unchanged.
+- **P7 — Verb parity + input truth + IME.** Cookies, JS channels (per-session
+ OnQuery routing), evalReturning, JS dialogs, find, zoom, editCommand,
+ loadTrusted, downloads; the three string-packings byte-for-byte
+ (`"id:json"`, `"name:message"`, cookies JSON). `CefDialogHandler::
+ OnFileDialog` + new opcode (windowless = NULL owner HWND; ``/downloads otherwise fail silently — new on both platforms).
+ DevTools `SetAsPopup`. Empirically settle: (a) DoKey's always-set
+ `character` on Windows (doubled edits?); (b) whether Blink applies editor
+ commands from raw key events on Windows OSR or the explicit editCommand
+ path stays required; (c) the early-character fallback
+ (cef_web_view.dart:598-600) vs WM_CHAR ordering. IME non-delta strategy
+ (§4.1); verify the Win32 TextInputPlugin honors a non-implicit `viewId`.
+ Native OAuth popup: `CreateWindowExW(WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU)`
+ + WndProc mirroring `windowShouldClose:`, `SetAsChild(HWND, CefRect)`,
+ `SetWindowTextW`, deferred destroy via `CefPostTask(TID_UI)`; forward
+ runner messages per docs.flutter.dev/platform-integration/windows/extern_win;
+ note `SetForegroundWindow` refusal means the popup may open un-foregrounded
+ — mitigate with `AllowSetForegroundWindow` from the plugin before spawn and
+ flash fallback. ~3 wks.
+ *Gate:* channel_probe + channel_probe_shared write `pass:true` on Windows;
+ cef_web_controller_test's 73 contract tests pass against the real host;
+ manual matrix: each editing key applies exactly once, MS-IME + a CJK IME
+ compose with the candidate window at the caret, `event.code` correct,
+ file-picker and a download complete, Google `signInWithPopup` completes.
+- **P8 — GPU path + Windows gates.** §4.4 wholesale, per S1/S4 verdicts:
+ bridge textures, `--adapter-luid`, the two-belt lifetime protocol
+ (plugin-side open+ref, `kOpSurfaceAck` deferred-destroy), lattice sizing,
+ software fallback + `FLUTTER_CEF_FORCE_SOFTWARE`. Conformance oracle
+ rebuilt as a **Dart driver** (one runner both OSes) over the unchanged
+ conformance_harness.dart; Windows host emits the identical `diagpx
+ wire= painted=WxH want=WxH content=` + FIRSTPAINT/ADOPT/blitmismatch
+ grammar (hard requirement — the oracle is a parser over it; note the
+ Windows sampler is a staging-texture `D3D11_MAP` readback, so keep
+ `FLUTTER_CEF_DIAGPX_EVERY` debug-gated). Leak soak: `WorkingSet64` ceiling
+ + `HandleCount` + a host-emitted live-shared-texture counter replacing
+ `ioreg -c IOSurface`. ~3 wks.
+ *Gate:* Windows oracle exits 0 on all five hard rules (NO-RENDER /
+ NO-ADOPT / BLIT-CROP / BLANK / NO-DIAG) at HARNESS_N=12 HARNESS_HARD=1
+ through all six storms; leak soak green; a forced
+ `--disable-gpu-compositing` run still renders via software; measured CPU/
+ frame drop vs P3. macOS oracle still green.
+- **P9 — CDP relay on Windows** (§4.5). Shared core + winsock backend + S3
+ spawn recipe + `--cdp-io-pipes` flag; ctest filter suite in both CI jobs.
+ macOS stays on CdpRelay.swift. ~3 wks.
+ *Gate:* multiview_probe `pass:true` on Windows (two grants, distinct
+ ports+tokens, cross-token rejected, per-tile getTargets scoped, disable
+ kills one, re-enable mints fresh); real Playwright `connectOverCDP` drives
+ one tile without seeing a sibling.
+- **P10 — macOS cutover: relay + pacer flag.** Retire CdpRelay.swift behind
+ a thin ObjC++ shim onto the shared core; flip macOS to the core-side pacer/
+ watchdogs (P6) behind its flag; delete the Swift copies + runners. ~1.5
+ wks.
+ *Gate:* macOS multiview_probe + run_channel_integration + cascade probe +
+ oracle + leak soak green; 16 s-idle run proves no false wedge flag; net
+ LOC deleted — one security boundary, one pacer. Tagged revert point.
+- **P11 — Sandbox, signing, distribution, docs.** §4.6: bootstrapc.exe
+ restructure per S2, LPAC ACLs, DLL hardening, posture bit1→0, Authenticode
+ gate + signed manifest, fetch/publish scripts, license bundling, README
+ security rewrite (§7), CHANGELOG + pubspec graph/publish-ordering update
+ (root note at pubspec.yaml:22-33 gains the fourth package), Windows
+ dev-setup doc (cmake/ninja PATH, Defender exclusions,
+ FLUTTER_CEF_HOST probe order on Windows: env var → next to runner exe →
+ `data/`). ~3 wks.
+ *Gate:* `flutter build windows --release` on a clean machine yields a
+ running, signed, **sandboxed** app (restricted-token renderers verified in
+ Process Explorer); a tampered PE *and* a tampered .pak are rejected
+ fail-closed with the cached archive deleted; identical prebuilt hash from
+ a Windows and a macOS checkout of the same commit.
+- **P12 — Windows-only hardening + folded probes.** TDR/`DXGI_ERROR_DEVICE_
+ REMOVED` fault injection with a recreate-device/bridge/texture recovery
+ path + a new `processGone` reason `deviceLost` (and `hostLaunchFailed` for
+ missing VC++ runtime / AV-quarantined libcef — today indistinguishable
+ from `crashed`); per-monitor-V2 DPI drag between differently-scaled
+ monitors (the C-1f "dpr change while culled" case made routine); sleep/
+ wake; software-fallback under hide/resize; single-renderer death in a
+ shared host; slot/wire-id reuse across respawn under load. Fold the manual
+ visual probes (cull_wedge, zoom_soak, crispness, interaction_soak,
+ realsite_soak, sharedhost_html) into conformance_harness as **asserted**
+ storm phases on both OSes; re-derive the Windows per-host browser ceiling
+ via the PROBE_N bracket. ~3 wks.
+ *Gate:* forced TDR recovers every tile to a painting texture within 5 s;
+ a cross-DPI monitor drag leaves no wedged/wrong-scale tile; all folded
+ storm phases assert green on both platforms; full parity matrix (§8)
+ green.
+
+Total ≈ 30 engineer-weeks. Pixels at ~week 5 (P3); go/no-go at ~week 7 (P4).
+
+## 6. Deliberately NOT ported in v1
+
+- **`showEmojiPicker`** → `MethodNotImplemented` (no supported Win32 API;
+ example button hidden). **`performSelector`** documented mac-only.
+- **The ad-hoc→mock-keychain→ephemeral downgrade rail** — inert on Windows
+ (§2 #12); replaced by the posture-bit scheme in §7, not silently dropped.
+- **Trackpad pan-zoom path + `_kTrackpadScrollGain=3.0`** — the Win32
+ embedder delivers precision-touchpad scroll as PointerScrollEvent; the
+ handler stays inert and the gain constant is never reused.
+- **`CEF_MULTI_PROCESS=OFF`** — documented macOS-only (`--single-process` is
+ Chromium-debug-only on Windows and forfeits OnAcceleratedPaint).
+- **5-helper-app fan-out, `CefScopedLibraryLoader`, NSApplication subclass,
+ rlimit bump, SIGPIPE guards, Mach-port peer-validation bypass, kqueue
+ parent watch** — deleted on Windows (Job Object + `--type=` relaunch model).
+- **A C++ rewrite of the macOS Swift plumbing** (CefProfileHost/
+ CefWebSession transport + lifecycle). Deliberate: six mutexes with a
+ documented lock order, ~30 ARC weak captures, and the wake/join/leak-on-
+ timeout teardown are the highest-variance code in the repo; the decisions
+ are shared (policies, pacer in core, relay), the OS-shaped plumbing is
+ written twice on purpose.
+- **HTML5 drag-and-drop, printing, context menus, accessibility** — missing
+ on macOS too (no CefDragHandler/PrintHandler/ContextMenuHandler/
+ AccessibilityHandler anywhere); parity means matching macOS, not fixing it.
+ If added later, they belong in `core/`.
+- **Tear-free GPU sync** — structurally unreachable through Flutter's
+ Windows texture API (§4.4); parity with macOS's documented single-buffer
+ tearing. Buffer-ring only on S1 measurement.
+- **Windows-on-ARM** — deferred pending §10 D4 (the windowsarm64 CEF dist
+ exists; the cost is fetch-arch plumbing, a second prebuilt, and CI runner
+ availability).
+- **WebAuthn/passkeys** — per S5's verdict; already out of scope on macOS
+ (specs/persistent-profiles/PLAN.md:7-9).
+
+## 7. Security contract deltas on Windows (stated, so nothing weakens silently)
+
+1. **At-rest encryption.** macOS: OSCrypt key in the login Keychain,
+ ACL-bound to the signing identity, real crypto only in signed builds.
+ Windows: DPAPI-wrapped AES-256-GCM, **always on, signing-independent**,
+ key inside the profile dir (`LocalPrefs.json`) — but decryptable by **any
+ same-user process** with no prompt (the classic cookie-stealer path;
+ Chrome's App-Bound Encryption needs a SYSTEM elevation COM service CEF
+ does not ship). Net: better than macOS-ad-hoc, weaker same-user isolation
+ than macOS-signed. README's Secrets-at-rest section gets a platform
+ split, not a find-and-replace. Profile-dir ACL + BitLocker are the
+ backstops.
+2. **Posture bits — extended, never redefined.** The `opReady` handshake
+ grows (under the v4 bump, where skew is caught loudly): **bit0 keeps its
+ existing meaning** (secrets-at-rest unavailable / ad-hoc; Windows
+ truthfully reports 0 — DPAPI is real crypto, so the named-profile
+ downgrade rail correctly never fires). **New bit1 = "sandbox
+ unavailable"**, set by the Windows host until P11 lands and thereafter
+ whenever sandbox init fails, surfaced to Dart as a machine-readable
+ `sandboxed` posture (create-result field + doc) so a consumer can refuse
+ untrusted content on an unhardened host. No silent hardcoding: an
+ unsandboxed build must not report a clean posture.
+3. **Sandbox.** macOS: coupled to Developer-ID signing; off in ad-hoc.
+ Windows: signing-independent — **on unconditionally** once P11 lands
+ (dev and CI included), via the bootstrapc.exe model + LPAC ACL. Until
+ P11, renderers run with the user's full token — worse than macOS's worst
+ posture (no TCC/Gatekeeper second line) — which is why bit1 exists and
+ why P11 must not slip past GA.
+4. **README.md:321 is retracted for Windows.** "Even a same-UID process
+ can't connect [to the relay]" rests on macOS denying `task_for_pid`.
+ Windows grants the owning user `PROCESS_VM_READ` (token readable from the
+ relay heap) and `PROCESS_DUP_HANDLE` (CDP pipe handles duplicable,
+ bypassing token *and* filter) by default; PPL needs a Microsoft cert. The
+ Windows claim is: defeats network/port-scanning and cross-user access —
+ **same-user is not a boundary on Windows.** Documentation, because no
+ engineering fixes it.
+5. **Relay transport.** Token auth, constant-time compare, deny-by-default
+ filter, discovery-without-token all port unchanged. New Windows rules:
+ `SO_EXCLUSIVEADDRUSE` (never `SO_REUSEADDR`); DWORD-ms timeouts;
+ loopback-TCP retained for the relay (Playwright can't speak pipes);
+ AppContainer/MSIX packaging would break loopback without an exemption
+ (noted for packagers).
+6. **IPC + shm surfaces.** Named pipe: random 128-bit name,
+ `FILE_FLAG_FIRST_PIPE_INSTANCE`, explicit user-SID DACL,
+ `PIPE_REJECT_REMOTE_CLIENTS`, client `SECURITY_SQOS_PRESENT |
+ SECURITY_ANONYMOUS` (a squatting server must not impersonate us).
+ Software-path sections: CSPRNG names over the authenticated pipe,
+ explicit user-SID DACL, `ERROR_ALREADY_EXISTS` → abort, extent-validated
+ before trusting wire dims. (Note `Local\` objects are per-session while
+ the profile lock is per-`%LOCALAPPDATA%`: RDP/fast-user-switching makes
+ the `locked` path routine where macOS rarely sees it — the exit-2
+ contract covers it.)
+7. **Binary trust.** No Gatekeeper/notarization/entitlements. Replacement:
+ per-PE WinVerifyTrust + pinned leaf thumbprint + signed manifest/catalog
+ for non-PE assets + unexpected-file rejection + DLL search-order
+ hardening (§4.6). `CreateProcessW` bypasses SmartScreen exactly as
+ posix_spawn bypasses Gatekeeper — the explicit gate is the only root of
+ trust, and it stays fail-closed.
+8. **Command lines are logged on Windows** (4688/Sysmon/EDR persist argv to
+ disk). Nothing profile-identifying or secret ever goes on argv; pipe
+ handle values are not secrets (useless without PROCESS_DUP_HANDLE) but
+ the rule is stated.
+9. **CDP-on-persistent-profile refusal** (main.mm:2757-2760) and the
+ deny-default permission handler are on the must-port checklist (§4.3) —
+ they live in `core/` after P5 so no greenfield host can drop them again.
+
+## 8. Parity test plan
+
+The oracle strategy: one log grammar, one harness, two platforms. The
+Windows host **must** emit the identical `diagpx wire= painted=WxH
+want=WxH content=` / FIRSTPAINT / `ADOPT psid` / blitmismatch lines under
+`FLUTTER_CEF_DEBUG=1` — every gate below is a parser over that grammar.
+
+- **Tier 0 (P1, CI both OSes):** `flutter test` — the 73 controller-contract
+ tests, ~150 input/widget tests + Windows twins of the ⌘/NSEvent groups;
+ example integration smoke.
+- **Tier 1 (P5/P9, CI both OSes, no GPU needed):** `native/cef_core` ctest —
+ the 3 resize/liveness policies (~30 vectors) + the CDP
+ filter/token/demux suite (60+ vectors incl. token edge cases and pipeId
+ bit layout), transcribed verbatim from the Swift tests.
+- **Tier 2 (P8, self-hosted/dev-box — GitHub Windows runners have no GPU):**
+ the conformance oracle as a cross-platform Dart driver over the unchanged
+ conformance_harness.dart — five hard rules (NO-RENDER / NO-ADOPT /
+ BLIT-CROP / BLANK / NO-DIAG), convergence WARN-only, HARNESS_N=12,
+ HARNESS_HARD=1, all six storms.
+- **Tier 3 (P6-P9):** leak soak (recreates_total>0; WorkingSet64 ≤1.5×
+ baseline; HandleCount + host-emitted live-shared-texture counter flat —
+ the direct analogue of the IOSurface ledger); cascade probe (12/12 wires
+ `paints>0`, ×50); channel gates rebuilt as a Dart runner (one runner both
+ OSes) asserting each probe's `pass:true` JSON — channel_probe,
+ channel_probe_shared, multiview_probe — with the Windows analogue of the
+ `FLUTTER_CEF_ALLOW_INSECURE_PROFILE` masking trap handled explicitly (an
+ unsandboxed host must not silently downgrade named profiles or the
+ shared-host probes pass vacuously).
+- **Tier 4 (P12, Windows-first):** TDR/device-removed fault injection with
+ recovery assertion; cross-DPI monitor drag; sleep/wake; software fallback
+ under hide/resize; renderer-death-in-shared-host; respawn-under-load
+ wire-id reuse; the folded (now asserted) visual probes; PROBE_N ceiling
+ bracket.
+- **Manual acceptance:** the example app feature matrix (PORTING.md
+ checklist) on Windows — pointer/keyboard/IME/navigation/allowlist/
+ profiles/dialogs/downloads/OAuth popup.
+
+## 9. Invariants
+
+- **macOS builds, renders, and passes its full gate set at every phase**
+ (verify, don't assume — the oracle runs after every P5 seam, not per
+ phase).
+- **App-facing API frozen**: `CefWebView` / `CefWebController` / exported
+ types unchanged; `package:flutter_cef/flutter_cef.dart` imports keep
+ working; `profile:` semantics identical.
+- **Wire changes are additive until P5, then lockstep**: no edit to
+ `main.mm` or the v3 protocol before the P5 convergence; from P5 the
+ protocol version bumps on *both* sides in one change or not at all — never
+ a per-OS fork, never a silent byte-reinterpretation (posture bits are
+ *added*, not redefined).
+- **No shared component reaches shipping macOS before Windows has validated
+ it** (policies: ctest + full macOS gates; pacer: P6 Windows → P10 flag;
+ relay: P9 Windows → P10 cutover, separately gated, tagged revert point).
+- **The duplication window is bounded and ledgered**: PORTING_DEBT.md tracks
+ every line copied into the Stage-1 win host; the ledger must be empty at
+ the P5 gate.
+- **Security posture is never silently weakened**: every delta in §7 is
+ either surfaced machine-readably (posture bits) or documented in the
+ platform-split README before GA; the fetch stays fail-closed on mismatch;
+ the CDP filter exists exactly once after P10.
+- **Cancellation is cheap by construction**: through P4 the macOS tree is
+ byte-identical (additive opcode, no protocol bump); killing the port at
+ the week-7 go/no-go costs ~6 weeks and zero macOS risk.
+- **The diagpx/FIRSTPAINT/ADOPT log grammar is byte-identical across
+ platforms** — it is the contract every automated gate parses.
+- **cef_host pin moves in lockstep**: one CEF version string
+ (build_cef_host.sh:17) for both platforms; a bump lands with both
+ prebuilts republished and both oracle runs green.
+
+## 10. Open decisions for the maintainer
+
+- **D1 — Buy vs build (answer first).** If Windows users need "a webview",
+ WebView2 via an existing plugin is 2–4 weeks and OS-maintained — but has
+ no OSR multi-tile compositing, no per-tile CDP isolation, no shared
+ profile hosts. This port is only justified by the agent-browser/multi-tile
+ product on Windows. Who is the Windows user, and which do they need?
+- **D2 — Agent control in Windows v1?** Cutting P9+P10 saves ~4.5 weeks;
+ `enableAgentControl` already surfaces a clean `PlatformException`
+ (cef_web_controller.dart:594-604). If cut, the shared-relay work still
+ eventually happens — decide whether v1 ships without it.
+- **D3 — Sandbox timing.** Block GA on P11 (recommended; unsandboxed
+ Windows is worse than macOS's worst posture), or ship a bounded
+ internal-preview window with bit1 surfaced and a hard deadline?
+ Unsandboxed-forever is not an option.
+- **D4 — Windows-on-ARM in scope?** Affects fetch arch detection, GCS
+ object naming, a second prebuilt + publisher, and CI (no GitHub-hosted
+ ARM64 Windows runners).
+- **D5 — The oracle's engine.** run_conformance_oracle.sh:25 prefers the
+ consumer's custom engine (`work_canvas/scripts/campus-flutter-engine.sh`);
+ ci.yaml pins 3.38.8 for the same reason. Does a **Windows** build of that
+ engine exist, and do its patches touch ExternalTextureD3d/ANGLE? The
+ Windows gate is authoritative against whichever engine ships — resolve in
+ writing before P8.
+- **D6 — Signing identity.** Does the org hold (or must it procure) an
+ HSM/Key-Vault-backed Authenticode cert? Procurement lead time can exceed a
+ phase; start at P0. OV suffices (EV no longer buys SmartScreen reputation).
+- **D7 — Licensing owner.** Who reviews/ships the CEF+Chromium license set
+ and the Windows-SDK redistribution terms for d3dcompiler_47.dll? Legal
+ ship-blocker; not an engineering task to discover at P11.
+- **D8 — Staffing.** This needs Win32 + D3D11 + C++-concurrency fluency; the
+ failure modes (torn frames, black textures with no error path,
+ teardown races) are undiagnosable without it. If unavailable, add 30–50%
+ to every estimate.
+- **D9 — Kill criteria (agree up front).** Abandon/descope if: S1 fails AND
+ software rendering can't meet the product's tile/fps bar; or the P4
+ interactive milestone slips past week 10; or D1's demand hasn't
+ materialized by milestone time; or D8 can't be staffed. Each caps the loss
+ at ~6 weeks with macOS untouched.
+- **D10 — Ongoing cost sign-off.** Post-GA, every CEF bump lands twice (two
+ prebuilts, two signing pipelines, two oracle runs); the macOS host is
+ under active churn (the last four commits touched render/screen/popup code
+ — each would have needed a Windows twin). Budget ~30–50% ongoing overhead
+ on this subsystem, or the platforms drift.
diff --git a/specs/windows-port/SPIKES.md b/specs/windows-port/SPIKES.md
new file mode 100644
index 0000000..7cca8fc
--- /dev/null
+++ b/specs/windows-port/SPIKES.md
@@ -0,0 +1,179 @@
+# Windows port — Phase 0 spike report
+
+Executed 2026-07-20 on the reference box (Win11, VS2022 17.14 / MSVC 19.44,
+Win10 SDK 10.0.26100, Intel Arc iGPU, Flutter 3.38.8 + system 3.32.8, CEF
+`144.0.27+g3fae261+chromium-144.0.7559.254` windows64_minimal). Spike code is
+throwaway, lives outside the repo at `C:\dev\flutter_cef_spikes\{s1..s6b}`
+(producers, consumers, logs, screenshots preserved there). Verdicts are
+against the pass criteria in [PLAN.md §3](PLAN.md).
+
+**Result: 7/7 PASS. No fallback promoted. The GPU path, the bootstrap sandbox
+model, the CDP pipe transport, and the plugin/toolchain assumptions all
+survived. Three PLAN.md claims are corrected below (one refuted outright).**
+
+| Spike | Verdict | One-line result |
+| --- | --- | --- |
+| S1 GPU handle → ANGLE | **PASS** | Cross-process legacy `MISC_SHARED` handle → Flutter `GpuSurfaceTexture` works, animated, 12 tiles; tearing 1.81%; NT→legacy copy mean 0.48–0.59 ms @720p |
+| S2 sandbox/bootstrap | **PASS** | Sandboxed render via `bootstrapc.exe` + client DLL `RunConsoleMain`; renderer at UNTRUSTED IL; argv + inherited HANDLEs survive; LPAC icacls grant NOT needed for default config |
+| S3 CDP io-pipes | **PASS** | `Target.getTargets` → `attachToTarget` → `Runtime.evaluate` over inherited pipe handles, 3/3 runs, ~2–3 s; also through `bootstrapc.exe` |
+| S4 handle identity | **PASS** | Handle **value** is fresh-per-callback and aliases across sizes/browsers — never key on it; true pool = 2–3 kernel objects/browser, never reused across sizes; #154716 leak probe **clean** on 3.38.8 |
+| S5 WebAuthn OSR | **PASS** | Native passkey modal (CredentialUIBroker) appears **even with `SetAsWindowless(nullptr)`** — support passkeys in v1 |
+| S6a CEF headers/flags | **PASS** | CEF 144 headers compile **clean** under stock `apply_standard_settings`; entire fix = `NOMINMAX` + per-config `/MD` wrapper builds |
+| S6b symlinks/OOT | **PASS** | Out-of-tree `../native/` sources build + incremental-rebuild through `.plugin_symlinks`; `bundled_libraries` lands extra DLLs beside the exe. Requires Developer Mode (true SYMLINKD, not junction, on 3.38.8) |
+
+## S1 — cross-process shared texture (the novel risk): retired
+
+- **Producer** 12× `ID3D11Texture2D` B8G8R8A8 `D3D11_RESOURCE_MISC_SHARED`,
+ rolling frame counter in 8×8 corner blocks, 59.5–60.0 fps over 15,300
+ frames.
+- **Raw ANGLE consumer** (separate process): all 12 handles open via
+ `eglCreatePbufferFromClientBuffer` + `EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE`,
+ 0 failures; counters advance 350 consecutive reads/tile; 1,178 reads/s.
+ **Tearing 1.81%** (76/4200 reads, diagonal-corner counter disagreement) —
+ the number that prices the future 2-slot ring; acceptable for v1.
+- **Real Flutter consumer** (3.38.8 release): 12 `flutter::GpuSurfaceTexture`
+ (`kFlutterDesktopGpuSurfaceTypeDxgiSharedHandle`) live-animate in a 4×3
+ grid; 5 s vs 15 s screenshots differ 64.4–65.8% of sampled pixels on every
+ tile.
+- **Destroy-while-bound**: consumer's held reference keeps the kernel object
+ alive after producer `Release()` — counter freezes at last value, glError 0,
+ no device-removed. **Belt-1 (plugin holds a ref) is the safety requirement;
+ belt-2 (`kOpSurfaceAck` deferred destroy) is a visual-quality optimization**
+ and may ship after P8 if schedule demands (PLAN §4.4 refined).
+- **CEF leg**: `OnAcceleratedPaint` NT handle (`MiscFlags=0x802`) →
+ `OpenSharedResource1` → `CopyResource` → legacy bridge: mean **0.594 ms**
+ (p95 1.56) on example.com, **0.483 ms** (p95 1.51) on a CSS-animation page
+ @1280×720 — 3–10% of a 60 fps frame budget. The structurally-mandatory copy
+ is not a perf concern.
+- Caveats: single-GPU box — hybrid-GPU/cross-adapter (`--adapter-luid`)
+ remains an assumption; handle-value-recycled-onto-different-texture worst
+ case not exercised (covered instead by S4's identity findings + belt-1).
+- Note: Flutter ships **no standalone** `libEGL.dll`/`libGLESv2.dll` in 3.38.8
+ engine artifacts (ANGLE is statically linked into `flutter_windows.dll`);
+ the raw-ANGLE leg used CEF's shipped ANGLE. Both providers accept the same
+ cross-process legacy handle.
+
+## S2 — bootstrap sandbox: the shipping contract
+
+- Invocation (empirical; not in CEF's README): client DLL must sit **in the
+ bootstrap exe's own directory**; select with `--module=`
+ (positional is a FATAL "Missing module name"), or **rename the bootstrap
+ exe to `.exe`** — the rename route is what we should ship (clean
+ Task Manager names). Neither `bootstrapc.exe` nor `libcef.dll` is
+ Authenticode-signed, so unsigned dev DLLs load fine.
+- `RunConsoleMain(argc, argv, sandbox_info, version_info)`: forward
+ `sandbox_info` to both `CefExecuteProcess` and `CefInitialize`. With
+ `no_sandbox=0`: 6-process tree, renderers at **UNTRUSTED** integrity, GPU at
+ LOW, page renders correctly (PPM inspected), clean exit.
+- argv (`--marker=xyz123`) and an inherited pipe HANDLE (via
+ `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`) both pass through the bootstrap entry
+ unchanged.
+- **LPAC `icacls` S-1-15-2-2 grant is NOT required** for CEF 144's default
+ sandbox (no AppContainer children; network I/O works ungrated). Only the
+ opt-in `NetworkServiceSandbox` feature needs it (and more than the exe-dir
+ grant — not fully chased). Document as future hardening, not setup.
+- Build detail: client DLL must link `delayimp.lib` (CEF's `/DELAYLOAD` list).
+
+## S3 — CDP over inherited pipe handles: ports directly
+
+- `CreatePipe` ×2 + `HANDLE_FLAG_INHERIT` + `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`;
+ child translates a private `--cdp-io-pipes=,` into
+ `--remote-debugging-pipe` **plus** `--remote-debugging-io-pipes=,` in
+ `OnBeforeCommandLineProcessing` (browser process only) — the exact macOS
+ `--cdp-pipe` pattern (main.mm:~1640) with handle values instead of fds 3/4.
+- Full sequence green 3/3: `Target.getTargets` (answers **before** any
+ browser exists — the Windows relay can open its transport at spawn, don't
+ wait for create) → `attachToTarget(flatten:true)` → sessioned
+ `Runtime.evaluate` = 2. NUL-framed UTF-8 JSON, same as macOS.
+- Same sequence green through `bootstrapc.exe` (composition with S2 proven).
+ Not yet exercised: io-pipes with the sandbox *enabled* (S3 ran
+ `no_sandbox=1`; S2 proved handle inheritance under the sandbox separately —
+ compose in P2).
+
+## S4 — handle identity: the law of the pool
+
+- The `shared_texture_handle` **value** is a fresh NT handle every callback,
+ closed by CEF on return. Values recycle and **alias**: 185 values seen at
+ >1 coded-size, 441 values seen from >1 browser. **Never key identity or
+ change-detection on the handle value.** Consumption must be
+ open+copy-inside-the-callback (confirms PLAN §4.4 step 2 as mandatory and
+ sufficient).
+- True identity (`CompareObjectHandles` over retained dups): **2–3 kernel
+ objects per browser** in strict A/B alternation (+1 burst buffer);
+ per-browser pools; **zero** cross-browser sharing; **zero** null handles in
+ 11,842 paints.
+- Resize: every `WasResized` (even ±1 px) discards the whole pool; kernel
+ objects are never reused across coded sizes. 1–3 late callbacks per step
+ still deliver the **old** size after `WasResized` — and
+ `info.extra.coded_size` correctly describes each delivered frame, so the
+ OSR_SCALE_MISMATCH size-gate oracle works on Windows as designed.
+- flutter/flutter#154716 leak probe: **flat** GPU memory over ~6,700
+ handle-changing imports on 3.38.8 → the 64-px size lattice is a churn
+ optimization, not a leak requirement; media_kit-style re-register +
+ `textureChanged` event **not needed**. (Skia/ANGLE backend; re-run if the
+ engine flips to Impeller-default.)
+- **REFUTES PLAN §4.3's "non-negotiable"** external-begin-frame rule — see
+ Plan corrections below.
+
+## S5 — WebAuthn: supported in v1 (decision flipped)
+
+- The native passkey modal (`Credential Dialog Xaml Host`, owner
+ `CredentialUIBroker.exe`) appears in **all three** configs, including
+ `SetAsWindowless(nullptr)` — Windows brokers WebAuthn UI out-of-process and
+ does not need our HWND. With a visible owner HWND the dialog tracks the
+ window's position; with nullptr it centers on the desktop.
+- `isUserVerifyingPlatformAuthenticatorAvailable()` → true in every config.
+ Live-confirmed end-to-end to the phone leg: the caBLE QR flow reached a
+ real phone during the run (maintainer observation).
+- v1 action: plumb the runner's top-level HWND into `SetAsWindowless` for
+ dialog placement (small refinement, not a blocker). Re-confirm the
+ biometric sub-UI on a Hello-enrolled box (this box has no NGC container).
+
+## S6 — toolchain: smaller delta than budgeted
+
+- `apply_standard_settings` verbatim (3.32.8 template; 3.38.8 byte-identical):
+ `cxx_std_17`, `/W4 /WX /wd4100`, **`/EHsc` already present** (folklore
+ wrong), `_HAS_EXCEPTIONS=0`. CEF 144 headers + wrapper subclasses +
+ `CefMessageRouterBrowserSide` compile **clean** under stock flags. The
+ entire real-world fix:
+ - `NOMINMAX` per target (`cef_types_win.h` pulls `windows.h`; else
+ `std::min` → C2589), and
+ - wrapper built per-config with `-DCEF_RUNTIME_LIBRARY_FLAG=/MD` (Debug
+ auto-appends `d`; mixing configs → LNK2038 RuntimeLibrary /
+ `_ITERATOR_DEBUG_LEVEL` mismatch). Prebuilt tooling must ship /MD **and**
+ /MDd wrapper libs.
+ No `cxx_std_20`, no `/wd` additions, no exception-model change, no
+ `USING_CEF_SHARED` macro.
+- `.plugin_symlinks` on 3.38.8 is a **true SYMLINKD** (reparse tag
+ 0xa000000c), not a junction → **Developer Mode (or elevation) is a
+ dev/CI-image requirement**; document in setup. Out-of-tree
+ `../native/shared/*.cc` sources compile, incremental rebuild propagates
+ through the symlink (edit → 7.9 s rebuild, change present),
+ `_bundled_libraries PARENT_SCOPE` lands an out-of-tree DLL beside
+ `app.exe` — the shared-core layout of PLAN §4.3 is buildable exactly as
+ drawn.
+
+## Plan corrections applied
+
+1. **PLAN §4.3 external-begin-frame "non-negotiable" — REFUTED and
+ inverted.** On CEF 144/Windows, `windowless_frame_rate=60` **without**
+ external begin frames delivers 60.3 fps × 12 concurrent browsers. With
+ `external_begin_frame_enabled=1` + a 16 ms `SendExternalBeginFrame` pump to
+ all 12, **only the first browser ever paints** — fatal for the
+ one-host-N-browsers model. Windows default: external begin frames OFF;
+ host-driven cadence (the macOS pacer) must be re-spiked per-browser before
+ any P6 use. (CefSharp #2675 evidently fixed/changed by 144.)
+2. **PLAN §3 S6 expected-fix list** replaced by the two-line S6a recipe
+ above.
+3. **PLAN §2 claim #18 (WebAuthn)** resolved: UI appears; v1 supports
+ passkeys; `SetAsWindowless(hwnd)` refinement noted.
+4. (P0 scope, already landed with this report: `.gitattributes` LF-pins
+ `packages/**/native/**` + `*.sh`; PORTING.md's producer-allocates
+ inversion, `--remote-debugging-io-pipes`, and bootstrap-DLL sandbox model
+ corrected.)
+
+## P0 gate: **GREEN**
+
+All six spike questions answered with measurements; no fallback needed
+anywhere; PLAN.md §2/§3/§4 updated where evidence contradicted it. Next: P1
+(package skeleton + Dart conditionals + example/windows runner + CI).
From df34eabb3bb73b715dd1e2fd12e8dcbfe88c7ac5 Mon Sep 17 00:00:00 2001
From: wenkai fan
Date: Mon, 20 Jul 2026 23:26:57 -0400
Subject: [PATCH 2/9] =?UTF-8?q?feat(windows):=20P1-P4=20vertical=20slice?=
=?UTF-8?q?=20=E2=80=94=20the=20example=20app=20browses=20on=20Windows?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
New federated package packages/flutter_cef_windows:
- windows/ plugin (C++): channel verb dispatch, GpuSurfaceTexture fed by the
host's legacy MISC_SHARED bridge handles (belt-1 ComPtr ref held), named-pipe
IPC + reader-thread -> platform-thread marshal, cef_host spawn with Job
Object reaping, editCommand/zoom/find relays.
- native/cef_host: Windows host as cef_host.dll + CEF bootstrapc.exe renamed
cef_host.exe (RunConsoleMain). OnAcceleratedPaint per the spike laws:
OpenSharedResource1 + CopyResource inside the callback, size-gated presents
{u64 bridgeHandle}{u32 srcW}{u32 srcH}, external begin frames OFF
(windowless_frame_rate drives 60fps; S4 refuted the begin-frame pump on
CEF 144). PROTOCOL.md transcribes the full opcode contract from main.mm.
- Dart: Windows endorsement (default_package), platform-aware accelerators
(Ctrl on Windows, unchanged Cmd on macOS), Windows VK native keycodes,
loadFile drive-letter handling, example/windows runner; 96 lines of new
Windows-locked widget tests. flutter analyze + 156/156 tests green;
packages/flutter_cef_macos untouched.
Verified live on-machine: browsing, link clicks, scroll, typing, backspace,
Ctrl accelerators, resize, clean shutdown — and a real end-to-end passkey
login (WebAuthn via CredentialUIBroker works in OSR; macOS never had this).
Slice posture: no_sandbox=1, ephemeral profiles only; profiles/JS-bridge/
cookies/IME-composition/CDP relay/sandbox = P5-P12.
Co-Authored-By: Claude Fable 5
---
.gitattributes | 4 +
example/.metadata | 30 +
example/lib/main.dart | 16 +-
example/pubspec.lock | 7 +
example/windows/.gitignore | 17 +
example/windows/CMakeLists.txt | 108 +
example/windows/flutter/CMakeLists.txt | 109 +
.../flutter/generated_plugin_registrant.cc | 14 +
.../flutter/generated_plugin_registrant.h | 15 +
.../windows/flutter/generated_plugins.cmake | 24 +
example/windows/runner/CMakeLists.txt | 40 +
example/windows/runner/Runner.rc | 121 ++
example/windows/runner/flutter_window.cpp | 71 +
example/windows/runner/flutter_window.h | 33 +
example/windows/runner/main.cpp | 43 +
example/windows/runner/resource.h | 16 +
example/windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes
example/windows/runner/runner.exe.manifest | 14 +
example/windows/runner/utils.cpp | 65 +
example/windows/runner/utils.h | 19 +
example/windows/runner/win32_window.cpp | 288 +++
example/windows/runner/win32_window.h | 102 +
lib/flutter_cef.dart | 9 +-
lib/src/cef_web_controller.dart | 10 +-
lib/src/cef_web_view.dart | 51 +-
packages/flutter_cef_windows/.gitignore | 3 +
packages/flutter_cef_windows/CHANGELOG.md | 7 +
packages/flutter_cef_windows/LICENSE | 21 +
packages/flutter_cef_windows/README.md | 35 +
.../lib/flutter_cef_windows.dart | 19 +
.../native/cef_host/CMakeLists.txt | 88 +
.../native/cef_host/PROTOCOL.md | 239 +++
.../native/cef_host/build_cef_host.bat | 45 +
.../native/cef_host/cef_host_protocol.h | 139 ++
.../native/cef_host/cef_host_win.cc | 1768 +++++++++++++++++
.../native/cef_host/fetch_cef.ps1 | 37 +
.../native/cef_host/test/pipe_probe.cc | 341 ++++
packages/flutter_cef_windows/pubspec.yaml | 37 +
.../windows/CMakeLists.txt | 148 ++
.../windows/flutter_cef_plugin.cpp | 1031 ++++++++++
.../windows/flutter_cef_plugin.h | 157 ++
.../windows/host_process.cpp | 120 ++
.../windows/host_process.h | 71 +
.../flutter_cef_windows/flutter_cef_plugin.h | 29 +
.../flutter_cef_windows/windows/ipc_pipe.cpp | 232 +++
.../flutter_cef_windows/windows/ipc_pipe.h | 112 ++
.../windows/texture_bridge.cpp | 194 ++
.../windows/texture_bridge.h | 83 +
pubspec.yaml | 7 +
specs/windows-port/SPIKES.md | 5 +
test/cef_web_controller_test.dart | 14 +
test/cef_web_view_test.dart | 96 +
52 files changed, 6283 insertions(+), 21 deletions(-)
create mode 100644 example/.metadata
create mode 100644 example/windows/.gitignore
create mode 100644 example/windows/CMakeLists.txt
create mode 100644 example/windows/flutter/CMakeLists.txt
create mode 100644 example/windows/flutter/generated_plugin_registrant.cc
create mode 100644 example/windows/flutter/generated_plugin_registrant.h
create mode 100644 example/windows/flutter/generated_plugins.cmake
create mode 100644 example/windows/runner/CMakeLists.txt
create mode 100644 example/windows/runner/Runner.rc
create mode 100644 example/windows/runner/flutter_window.cpp
create mode 100644 example/windows/runner/flutter_window.h
create mode 100644 example/windows/runner/main.cpp
create mode 100644 example/windows/runner/resource.h
create mode 100644 example/windows/runner/resources/app_icon.ico
create mode 100644 example/windows/runner/runner.exe.manifest
create mode 100644 example/windows/runner/utils.cpp
create mode 100644 example/windows/runner/utils.h
create mode 100644 example/windows/runner/win32_window.cpp
create mode 100644 example/windows/runner/win32_window.h
create mode 100644 packages/flutter_cef_windows/.gitignore
create mode 100644 packages/flutter_cef_windows/CHANGELOG.md
create mode 100644 packages/flutter_cef_windows/LICENSE
create mode 100644 packages/flutter_cef_windows/README.md
create mode 100644 packages/flutter_cef_windows/lib/flutter_cef_windows.dart
create mode 100644 packages/flutter_cef_windows/native/cef_host/CMakeLists.txt
create mode 100644 packages/flutter_cef_windows/native/cef_host/PROTOCOL.md
create mode 100644 packages/flutter_cef_windows/native/cef_host/build_cef_host.bat
create mode 100644 packages/flutter_cef_windows/native/cef_host/cef_host_protocol.h
create mode 100644 packages/flutter_cef_windows/native/cef_host/cef_host_win.cc
create mode 100644 packages/flutter_cef_windows/native/cef_host/fetch_cef.ps1
create mode 100644 packages/flutter_cef_windows/native/cef_host/test/pipe_probe.cc
create mode 100644 packages/flutter_cef_windows/pubspec.yaml
create mode 100644 packages/flutter_cef_windows/windows/CMakeLists.txt
create mode 100644 packages/flutter_cef_windows/windows/flutter_cef_plugin.cpp
create mode 100644 packages/flutter_cef_windows/windows/flutter_cef_plugin.h
create mode 100644 packages/flutter_cef_windows/windows/host_process.cpp
create mode 100644 packages/flutter_cef_windows/windows/host_process.h
create mode 100644 packages/flutter_cef_windows/windows/include/flutter_cef_windows/flutter_cef_plugin.h
create mode 100644 packages/flutter_cef_windows/windows/ipc_pipe.cpp
create mode 100644 packages/flutter_cef_windows/windows/ipc_pipe.h
create mode 100644 packages/flutter_cef_windows/windows/texture_bridge.cpp
create mode 100644 packages/flutter_cef_windows/windows/texture_bridge.h
diff --git a/.gitattributes b/.gitattributes
index fddab01..105d8dc 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -6,3 +6,7 @@
packages/**/native/** text eol=lf
packages/**/tool/*.sh text eol=lf
*.sh text eol=lf
+# Windows batch files MUST be CRLF (cmd.exe misparses LF-only .bat, dropping
+# characters). This overrides the native/** LF pin above for *.bat only — the
+# hash key stays deterministic because CRLF is pinned on BOTH platforms.
+*.bat text eol=crlf
diff --git a/example/.metadata b/example/.metadata
new file mode 100644
index 0000000..9547b20
--- /dev/null
+++ b/example/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "bd7a4a6b5576630823ca344e3e684c53aa1a0f46"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
+ base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
+ - platform: windows
+ create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
+ base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 52b0982..5e76614 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,3 +1,5 @@
+import 'dart:io' show Platform;
+
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cef/flutter_cef.dart';
@@ -248,11 +250,15 @@ and committed text — including emoji — should appear intact.
tooltip: 'openDevTools()',
onPressed: _controller.openDevTools,
),
- IconButton(
- icon: const Icon(Icons.emoji_emotions_outlined),
- tooltip: 'showEmojiPicker() (focus a field first)',
- onPressed: _emojiPicker,
- ),
+ // macOS-only: showEmojiPicker drives the AppKit Character
+ // Palette; there is no supported Win32 equivalent (PLAN §6),
+ // so the button is hidden per-platform.
+ if (Platform.isMacOS)
+ IconButton(
+ icon: const Icon(Icons.emoji_emotions_outlined),
+ tooltip: 'showEmojiPicker() (focus a field first)',
+ onPressed: _emojiPicker,
+ ),
IconButton(
icon: const Icon(Icons.keyboard),
tooltip: 'Load the IME / text-input test page',
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 07319cc..6183241 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -91,6 +91,13 @@ packages:
relative: true
source: path
version: "0.1.3"
+ flutter_cef_windows:
+ dependency: transitive
+ description:
+ path: "../packages/flutter_cef_windows"
+ relative: true
+ source: path
+ version: "0.1.0"
flutter_driver:
dependency: transitive
description: flutter
diff --git a/example/windows/.gitignore b/example/windows/.gitignore
new file mode 100644
index 0000000..ec4098a
--- /dev/null
+++ b/example/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/ephemeral/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt
new file mode 100644
index 0000000..689a8de
--- /dev/null
+++ b/example/windows/CMakeLists.txt
@@ -0,0 +1,108 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.14)
+project(flutter_cef_example LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "flutter_cef_example")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(VERSION 3.14...3.25)
+
+# Define build configuration option.
+get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+if(IS_MULTICONFIG)
+ set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
+ CACHE STRING "" FORCE)
+else()
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+ endif()
+endif()
+# Define settings for the Profile build mode.
+set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
+set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
+set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
+set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
+
+# Use Unicode for all projects.
+add_definitions(-DUNICODE -D_UNICODE)
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_17)
+ target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
+ target_compile_options(${TARGET} PRIVATE /EHsc)
+ target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
+ target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
+
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# Support files are copied into place next to the executable, so that it can
+# run in place. This is done instead of making a separate bundle (as on Linux)
+# so that building and running from within Visual Studio will work.
+set(BUILD_BUNDLE_DIR "$")
+# Make the "install" step default, as it's required to run.
+set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+ install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
+
+# Copy the native assets provided by the build.dart from all packages.
+set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
+install(DIRECTORY "${NATIVE_ASSETS_DIR}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ CONFIGURATIONS Profile;Release
+ COMPONENT Runtime)
diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt
new file mode 100644
index 0000000..efb62eb
--- /dev/null
+++ b/example/windows/flutter/CMakeLists.txt
@@ -0,0 +1,109 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.14)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+
+# Set fallback configurations for older versions of the flutter tool.
+if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
+ set(FLUTTER_TARGET_PLATFORM "windows-x64")
+endif()
+
+# === Flutter Library ===
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "flutter_export.h"
+ "flutter_windows.h"
+ "flutter_messenger.h"
+ "flutter_plugin_registrar.h"
+ "flutter_texture_registrar.h"
+)
+list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
+add_dependencies(flutter flutter_assemble)
+
+# === Wrapper ===
+list(APPEND CPP_WRAPPER_SOURCES_CORE
+ "core_implementations.cc"
+ "standard_codec.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
+ "plugin_registrar.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
+list(APPEND CPP_WRAPPER_SOURCES_APP
+ "flutter_engine.cc"
+ "flutter_view_controller.cc"
+)
+list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
+
+# Wrapper sources needed for a plugin.
+add_library(flutter_wrapper_plugin STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+)
+apply_standard_settings(flutter_wrapper_plugin)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ POSITION_INDEPENDENT_CODE ON)
+set_target_properties(flutter_wrapper_plugin PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
+target_include_directories(flutter_wrapper_plugin PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_plugin flutter_assemble)
+
+# Wrapper sources needed for the runner.
+add_library(flutter_wrapper_app STATIC
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
+apply_standard_settings(flutter_wrapper_app)
+target_link_libraries(flutter_wrapper_app PUBLIC flutter)
+target_include_directories(flutter_wrapper_app PUBLIC
+ "${WRAPPER_ROOT}/include"
+)
+add_dependencies(flutter_wrapper_app flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
+set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+ ${PHONY_OUTPUT}
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
+ ${FLUTTER_TARGET_PLATFORM} $
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+ ${CPP_WRAPPER_SOURCES_CORE}
+ ${CPP_WRAPPER_SOURCES_PLUGIN}
+ ${CPP_WRAPPER_SOURCES_APP}
+)
diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000..2f708aa
--- /dev/null
+++ b/example/windows/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,14 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+#include
+
+void RegisterPlugins(flutter::PluginRegistry* registry) {
+ FlutterCefPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FlutterCefPlugin"));
+}
diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000..dc139d8
--- /dev/null
+++ b/example/windows/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void RegisterPlugins(flutter::PluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake
new file mode 100644
index 0000000..2960d35
--- /dev/null
+++ b/example/windows/flutter/generated_plugins.cmake
@@ -0,0 +1,24 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+ flutter_cef_windows
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt
new file mode 100644
index 0000000..2041a04
--- /dev/null
+++ b/example/windows/runner/CMakeLists.txt
@@ -0,0 +1,40 @@
+cmake_minimum_required(VERSION 3.14)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME} WIN32
+ "flutter_window.cpp"
+ "main.cpp"
+ "utils.cpp"
+ "win32_window.cpp"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+ "Runner.rc"
+ "runner.exe.manifest"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the build version.
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
+target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
+
+# Disable Windows macros that collide with C++ standard library functions.
+target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
+
+# Add dependency libraries and include directories. Add any application-specific
+# dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
+target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc
new file mode 100644
index 0000000..fc0a490
--- /dev/null
+++ b/example/windows/runner/Runner.rc
@@ -0,0 +1,121 @@
+// Microsoft Visual C++ generated resource script.
+//
+#pragma code_page(65001)
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\r\n"
+ "\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APP_ICON ICON "resources\\app_icon.ico"
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Version
+//
+
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
+#else
+#define VERSION_AS_NUMBER 1,0,0,0
+#endif
+
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
+#else
+#define VERSION_AS_STRING "1.0.0"
+#endif
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION VERSION_AS_NUMBER
+ PRODUCTVERSION VERSION_AS_NUMBER
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "com.example" "\0"
+ VALUE "FileDescription", "flutter_cef_example" "\0"
+ VALUE "FileVersion", VERSION_AS_STRING "\0"
+ VALUE "InternalName", "flutter_cef_example" "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
+ VALUE "OriginalFilename", "flutter_cef_example.exe" "\0"
+ VALUE "ProductName", "flutter_cef_example" "\0"
+ VALUE "ProductVersion", VERSION_AS_STRING "\0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp
new file mode 100644
index 0000000..c819cb0
--- /dev/null
+++ b/example/windows/runner/flutter_window.cpp
@@ -0,0 +1,71 @@
+#include "flutter_window.h"
+
+#include
+
+#include "flutter/generated_plugin_registrant.h"
+
+FlutterWindow::FlutterWindow(const flutter::DartProject& project)
+ : project_(project) {}
+
+FlutterWindow::~FlutterWindow() {}
+
+bool FlutterWindow::OnCreate() {
+ if (!Win32Window::OnCreate()) {
+ return false;
+ }
+
+ RECT frame = GetClientArea();
+
+ // The size here must match the window dimensions to avoid unnecessary surface
+ // creation / destruction in the startup path.
+ flutter_controller_ = std::make_unique(
+ frame.right - frame.left, frame.bottom - frame.top, project_);
+ // Ensure that basic setup of the controller was successful.
+ if (!flutter_controller_->engine() || !flutter_controller_->view()) {
+ return false;
+ }
+ RegisterPlugins(flutter_controller_->engine());
+ SetChildContent(flutter_controller_->view()->GetNativeWindow());
+
+ flutter_controller_->engine()->SetNextFrameCallback([&]() {
+ this->Show();
+ });
+
+ // Flutter can complete the first frame before the "show window" callback is
+ // registered. The following call ensures a frame is pending to ensure the
+ // window is shown. It is a no-op if the first frame hasn't completed yet.
+ flutter_controller_->ForceRedraw();
+
+ return true;
+}
+
+void FlutterWindow::OnDestroy() {
+ if (flutter_controller_) {
+ flutter_controller_ = nullptr;
+ }
+
+ Win32Window::OnDestroy();
+}
+
+LRESULT
+FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ // Give Flutter, including plugins, an opportunity to handle window messages.
+ if (flutter_controller_) {
+ std::optional result =
+ flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
+ lparam);
+ if (result) {
+ return *result;
+ }
+ }
+
+ switch (message) {
+ case WM_FONTCHANGE:
+ flutter_controller_->engine()->ReloadSystemFonts();
+ break;
+ }
+
+ return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
+}
diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h
new file mode 100644
index 0000000..28c2383
--- /dev/null
+++ b/example/windows/runner/flutter_window.h
@@ -0,0 +1,33 @@
+#ifndef RUNNER_FLUTTER_WINDOW_H_
+#define RUNNER_FLUTTER_WINDOW_H_
+
+#include
+#include
+
+#include
+
+#include "win32_window.h"
+
+// A window that does nothing but host a Flutter view.
+class FlutterWindow : public Win32Window {
+ public:
+ // Creates a new FlutterWindow hosting a Flutter view running |project|.
+ explicit FlutterWindow(const flutter::DartProject& project);
+ virtual ~FlutterWindow();
+
+ protected:
+ // Win32Window:
+ bool OnCreate() override;
+ void OnDestroy() override;
+ LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
+ LPARAM const lparam) noexcept override;
+
+ private:
+ // The project to run.
+ flutter::DartProject project_;
+
+ // The Flutter instance hosted by this window.
+ std::unique_ptr flutter_controller_;
+};
+
+#endif // RUNNER_FLUTTER_WINDOW_H_
diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp
new file mode 100644
index 0000000..59586f7
--- /dev/null
+++ b/example/windows/runner/main.cpp
@@ -0,0 +1,43 @@
+#include
+#include
+#include
+
+#include "flutter_window.h"
+#include "utils.h"
+
+int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
+ _In_ wchar_t *command_line, _In_ int show_command) {
+ // Attach to console when present (e.g., 'flutter run') or create a
+ // new console when running with a debugger.
+ if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
+ CreateAndAttachConsole();
+ }
+
+ // Initialize COM, so that it is available for use in the library and/or
+ // plugins.
+ ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+
+ flutter::DartProject project(L"data");
+
+ std::vector command_line_arguments =
+ GetCommandLineArguments();
+
+ project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
+
+ FlutterWindow window(project);
+ Win32Window::Point origin(10, 10);
+ Win32Window::Size size(1280, 720);
+ if (!window.Create(L"flutter_cef_example", origin, size)) {
+ return EXIT_FAILURE;
+ }
+ window.SetQuitOnClose(true);
+
+ ::MSG msg;
+ while (::GetMessage(&msg, nullptr, 0, 0)) {
+ ::TranslateMessage(&msg);
+ ::DispatchMessage(&msg);
+ }
+
+ ::CoUninitialize();
+ return EXIT_SUCCESS;
+}
diff --git a/example/windows/runner/resource.h b/example/windows/runner/resource.h
new file mode 100644
index 0000000..ddc7f3e
--- /dev/null
+++ b/example/windows/runner/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by Runner.rc
+//
+#define IDI_APP_ICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6
GIT binary patch
literal 33772
zcmeHQc|26z|35SKE&G-*mXah&B~fFkXr)DEO&hIfqby^T&>|8^_Ub8Vp#`BLl3lbZ
zvPO!8k!2X>cg~Elr=IVxo~J*a`+9wR=A83c-k-DFd(XM&UI1VKCqM@V;DDtJ09WB}
zRaHKiW(GT00brH|0EeTeKVbpbGZg?nK6-j827q-+NFM34gXjqWxJ*a#{b_apGN<-L_m3#8Z26atkEn&
ze87Bvv^6vVmM+p+cQ~{u%=NJF>#(d;8{7Q{^rWKWNtf14H}>#&y7$lqmY6xmZryI&
z($uy?c5-+cP
nt2%)R&(KIWEXww>Cnz{OUpT>W$CbO$h1=
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
literal 0
HcmV?d00001
diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest
new file mode 100644
index 0000000..4b962bb
--- /dev/null
+++ b/example/windows/runner/runner.exe.manifest
@@ -0,0 +1,14 @@
+
+
+
+
+ PerMonitorV2
+
+
+
+
+
+
+
+
+
diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp
new file mode 100644
index 0000000..259d85b
--- /dev/null
+++ b/example/windows/runner/utils.cpp
@@ -0,0 +1,65 @@
+#include "utils.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+void CreateAndAttachConsole() {
+ if (::AllocConsole()) {
+ FILE *unused;
+ if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
+ _dup2(_fileno(stdout), 1);
+ }
+ if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
+ _dup2(_fileno(stdout), 2);
+ }
+ std::ios::sync_with_stdio();
+ FlutterDesktopResyncOutputStreams();
+ }
+}
+
+std::vector GetCommandLineArguments() {
+ // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
+ int argc;
+ wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ if (argv == nullptr) {
+ return std::vector();
+ }
+
+ std::vector command_line_arguments;
+
+ // Skip the first argument as it's the binary name.
+ for (int i = 1; i < argc; i++) {
+ command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
+ }
+
+ ::LocalFree(argv);
+
+ return command_line_arguments;
+}
+
+std::string Utf8FromUtf16(const wchar_t* utf16_string) {
+ if (utf16_string == nullptr) {
+ return std::string();
+ }
+ unsigned int target_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ -1, nullptr, 0, nullptr, nullptr)
+ -1; // remove the trailing null character
+ int input_length = (int)wcslen(utf16_string);
+ std::string utf8_string;
+ if (target_length == 0 || target_length > utf8_string.max_size()) {
+ return utf8_string;
+ }
+ utf8_string.resize(target_length);
+ int converted_length = ::WideCharToMultiByte(
+ CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
+ input_length, utf8_string.data(), target_length, nullptr, nullptr);
+ if (converted_length == 0) {
+ return std::string();
+ }
+ return utf8_string;
+}
diff --git a/example/windows/runner/utils.h b/example/windows/runner/utils.h
new file mode 100644
index 0000000..3f0e05c
--- /dev/null
+++ b/example/windows/runner/utils.h
@@ -0,0 +1,19 @@
+#ifndef RUNNER_UTILS_H_
+#define RUNNER_UTILS_H_
+
+#include
+#include
+
+// Creates a console for the process, and redirects stdout and stderr to
+// it for both the runner and the Flutter library.
+void CreateAndAttachConsole();
+
+// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
+// encoded in UTF-8. Returns an empty std::string on failure.
+std::string Utf8FromUtf16(const wchar_t* utf16_string);
+
+// Gets the command line arguments passed in as a std::vector,
+// encoded in UTF-8. Returns an empty std::vector on failure.
+std::vector GetCommandLineArguments();
+
+#endif // RUNNER_UTILS_H_
diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp
new file mode 100644
index 0000000..b5ba2a0
--- /dev/null
+++ b/example/windows/runner/win32_window.cpp
@@ -0,0 +1,288 @@
+#include "win32_window.h"
+
+#include
+#include
+
+#include "resource.h"
+
+namespace {
+
+/// Window attribute that enables dark mode window decorations.
+///
+/// Redefined in case the developer's machine has a Windows SDK older than
+/// version 10.0.22000.0.
+/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+
+constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
+
+/// Registry key for app theme preference.
+///
+/// A value of 0 indicates apps should use dark mode. A non-zero or missing
+/// value indicates apps should use light mode.
+constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
+constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
+
+// The number of Win32Window objects that currently exist.
+static int g_active_window_count = 0;
+
+using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
+
+// Scale helper to convert logical scaler values to physical using passed in
+// scale factor
+int Scale(int source, double scale_factor) {
+ return static_cast(source * scale_factor);
+}
+
+// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
+// This API is only needed for PerMonitor V1 awareness mode.
+void EnableFullDpiSupportIfAvailable(HWND hwnd) {
+ HMODULE user32_module = LoadLibraryA("User32.dll");
+ if (!user32_module) {
+ return;
+ }
+ auto enable_non_client_dpi_scaling =
+ reinterpret_cast(
+ GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
+ if (enable_non_client_dpi_scaling != nullptr) {
+ enable_non_client_dpi_scaling(hwnd);
+ }
+ FreeLibrary(user32_module);
+}
+
+} // namespace
+
+// Manages the Win32Window's window class registration.
+class WindowClassRegistrar {
+ public:
+ ~WindowClassRegistrar() = default;
+
+ // Returns the singleton registrar instance.
+ static WindowClassRegistrar* GetInstance() {
+ if (!instance_) {
+ instance_ = new WindowClassRegistrar();
+ }
+ return instance_;
+ }
+
+ // Returns the name of the window class, registering the class if it hasn't
+ // previously been registered.
+ const wchar_t* GetWindowClass();
+
+ // Unregisters the window class. Should only be called if there are no
+ // instances of the window.
+ void UnregisterWindowClass();
+
+ private:
+ WindowClassRegistrar() = default;
+
+ static WindowClassRegistrar* instance_;
+
+ bool class_registered_ = false;
+};
+
+WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
+
+const wchar_t* WindowClassRegistrar::GetWindowClass() {
+ if (!class_registered_) {
+ WNDCLASS window_class{};
+ window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
+ window_class.lpszClassName = kWindowClassName;
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = GetModuleHandle(nullptr);
+ window_class.hIcon =
+ LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
+ window_class.hbrBackground = 0;
+ window_class.lpszMenuName = nullptr;
+ window_class.lpfnWndProc = Win32Window::WndProc;
+ RegisterClass(&window_class);
+ class_registered_ = true;
+ }
+ return kWindowClassName;
+}
+
+void WindowClassRegistrar::UnregisterWindowClass() {
+ UnregisterClass(kWindowClassName, nullptr);
+ class_registered_ = false;
+}
+
+Win32Window::Win32Window() {
+ ++g_active_window_count;
+}
+
+Win32Window::~Win32Window() {
+ --g_active_window_count;
+ Destroy();
+}
+
+bool Win32Window::Create(const std::wstring& title,
+ const Point& origin,
+ const Size& size) {
+ Destroy();
+
+ const wchar_t* window_class =
+ WindowClassRegistrar::GetInstance()->GetWindowClass();
+
+ const POINT target_point = {static_cast(origin.x),
+ static_cast(origin.y)};
+ HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
+ UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
+ double scale_factor = dpi / 96.0;
+
+ HWND window = CreateWindow(
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
+ Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
+ Scale(size.width, scale_factor), Scale(size.height, scale_factor),
+ nullptr, nullptr, GetModuleHandle(nullptr), this);
+
+ if (!window) {
+ return false;
+ }
+
+ UpdateTheme(window);
+
+ return OnCreate();
+}
+
+bool Win32Window::Show() {
+ return ShowWindow(window_handle_, SW_SHOWNORMAL);
+}
+
+// static
+LRESULT CALLBACK Win32Window::WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ if (message == WM_NCCREATE) {
+ auto window_struct = reinterpret_cast(lparam);
+ SetWindowLongPtr(window, GWLP_USERDATA,
+ reinterpret_cast(window_struct->lpCreateParams));
+
+ auto that = static_cast(window_struct->lpCreateParams);
+ EnableFullDpiSupportIfAvailable(window);
+ that->window_handle_ = window;
+ } else if (Win32Window* that = GetThisFromHandle(window)) {
+ return that->MessageHandler(window, message, wparam, lparam);
+ }
+
+ return DefWindowProc(window, message, wparam, lparam);
+}
+
+LRESULT
+Win32Window::MessageHandler(HWND hwnd,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept {
+ switch (message) {
+ case WM_DESTROY:
+ window_handle_ = nullptr;
+ Destroy();
+ if (quit_on_close_) {
+ PostQuitMessage(0);
+ }
+ return 0;
+
+ case WM_DPICHANGED: {
+ auto newRectSize = reinterpret_cast(lparam);
+ LONG newWidth = newRectSize->right - newRectSize->left;
+ LONG newHeight = newRectSize->bottom - newRectSize->top;
+
+ SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
+ newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+
+ return 0;
+ }
+ case WM_SIZE: {
+ RECT rect = GetClientArea();
+ if (child_content_ != nullptr) {
+ // Size and position the child window.
+ MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
+ rect.bottom - rect.top, TRUE);
+ }
+ return 0;
+ }
+
+ case WM_ACTIVATE:
+ if (child_content_ != nullptr) {
+ SetFocus(child_content_);
+ }
+ return 0;
+
+ case WM_DWMCOLORIZATIONCOLORCHANGED:
+ UpdateTheme(hwnd);
+ return 0;
+ }
+
+ return DefWindowProc(window_handle_, message, wparam, lparam);
+}
+
+void Win32Window::Destroy() {
+ OnDestroy();
+
+ if (window_handle_) {
+ DestroyWindow(window_handle_);
+ window_handle_ = nullptr;
+ }
+ if (g_active_window_count == 0) {
+ WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
+ }
+}
+
+Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
+ return reinterpret_cast(
+ GetWindowLongPtr(window, GWLP_USERDATA));
+}
+
+void Win32Window::SetChildContent(HWND content) {
+ child_content_ = content;
+ SetParent(content, window_handle_);
+ RECT frame = GetClientArea();
+
+ MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
+ frame.bottom - frame.top, true);
+
+ SetFocus(child_content_);
+}
+
+RECT Win32Window::GetClientArea() {
+ RECT frame;
+ GetClientRect(window_handle_, &frame);
+ return frame;
+}
+
+HWND Win32Window::GetHandle() {
+ return window_handle_;
+}
+
+void Win32Window::SetQuitOnClose(bool quit_on_close) {
+ quit_on_close_ = quit_on_close;
+}
+
+bool Win32Window::OnCreate() {
+ // No-op; provided for subclasses.
+ return true;
+}
+
+void Win32Window::OnDestroy() {
+ // No-op; provided for subclasses.
+}
+
+void Win32Window::UpdateTheme(HWND const window) {
+ DWORD light_mode;
+ DWORD light_mode_size = sizeof(light_mode);
+ LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
+ kGetPreferredBrightnessRegValue,
+ RRF_RT_REG_DWORD, nullptr, &light_mode,
+ &light_mode_size);
+
+ if (result == ERROR_SUCCESS) {
+ BOOL enable_dark_mode = light_mode == 0;
+ DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
+ &enable_dark_mode, sizeof(enable_dark_mode));
+ }
+}
diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h
new file mode 100644
index 0000000..49b847f
--- /dev/null
+++ b/example/windows/runner/win32_window.h
@@ -0,0 +1,102 @@
+#ifndef RUNNER_WIN32_WINDOW_H_
+#define RUNNER_WIN32_WINDOW_H_
+
+#include
+
+#include
+#include
+#include
+
+// A class abstraction for a high DPI-aware Win32 Window. Intended to be
+// inherited from by classes that wish to specialize with custom
+// rendering and input handling
+class Win32Window {
+ public:
+ struct Point {
+ unsigned int x;
+ unsigned int y;
+ Point(unsigned int x, unsigned int y) : x(x), y(y) {}
+ };
+
+ struct Size {
+ unsigned int width;
+ unsigned int height;
+ Size(unsigned int width, unsigned int height)
+ : width(width), height(height) {}
+ };
+
+ Win32Window();
+ virtual ~Win32Window();
+
+ // Creates a win32 window with |title| that is positioned and sized using
+ // |origin| and |size|. New windows are created on the default monitor. Window
+ // sizes are specified to the OS in physical pixels, hence to ensure a
+ // consistent size this function will scale the inputted width and height as
+ // as appropriate for the default monitor. The window is invisible until
+ // |Show| is called. Returns true if the window was created successfully.
+ bool Create(const std::wstring& title, const Point& origin, const Size& size);
+
+ // Show the current window. Returns true if the window was successfully shown.
+ bool Show();
+
+ // Release OS resources associated with window.
+ void Destroy();
+
+ // Inserts |content| into the window tree.
+ void SetChildContent(HWND content);
+
+ // Returns the backing Window handle to enable clients to set icon and other
+ // window properties. Returns nullptr if the window has been destroyed.
+ HWND GetHandle();
+
+ // If true, closing this window will quit the application.
+ void SetQuitOnClose(bool quit_on_close);
+
+ // Return a RECT representing the bounds of the current client area.
+ RECT GetClientArea();
+
+ protected:
+ // Processes and route salient window messages for mouse handling,
+ // size change and DPI. Delegates handling of these to member overloads that
+ // inheriting classes can handle.
+ virtual LRESULT MessageHandler(HWND window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Called when CreateAndShow is called, allowing subclass window-related
+ // setup. Subclasses should return false if setup fails.
+ virtual bool OnCreate();
+
+ // Called when Destroy is called.
+ virtual void OnDestroy();
+
+ private:
+ friend class WindowClassRegistrar;
+
+ // OS callback called by message pump. Handles the WM_NCCREATE message which
+ // is passed when the non-client area is being created and enables automatic
+ // non-client DPI scaling so that the non-client area automatically
+ // responds to changes in DPI. All other messages are handled by
+ // MessageHandler.
+ static LRESULT CALLBACK WndProc(HWND const window,
+ UINT const message,
+ WPARAM const wparam,
+ LPARAM const lparam) noexcept;
+
+ // Retrieves a class instance pointer for |window|
+ static Win32Window* GetThisFromHandle(HWND const window) noexcept;
+
+ // Update the window frame's theme to match the system theme.
+ static void UpdateTheme(HWND const window);
+
+ bool quit_on_close_ = false;
+
+ // window handle for top level window.
+ HWND window_handle_ = nullptr;
+
+ // window handle for hosted content.
+ HWND child_content_ = nullptr;
+};
+
+#endif // RUNNER_WIN32_WINDOW_H_
diff --git a/lib/flutter_cef.dart b/lib/flutter_cef.dart
index cc7c7b3..c5236bb 100644
--- a/lib/flutter_cef.dart
+++ b/lib/flutter_cef.dart
@@ -1,10 +1,11 @@
/// flutter_cef — embed a live Chromium (CEF) browser as a Flutter widget.
///
/// The page renders off-screen in a `cef_host` subprocess into a shared
-/// IOSurface and is shown as a [Texture] (so it composites/transforms/clips
-/// like any widget). Pointer + keyboard input is forwarded by coordinate, and
-/// the page cursor drives a [MouseRegion]. macOS only, for now (the package is
-/// federated — see `flutter_cef_macos` and `PORTING.md`).
+/// surface (an IOSurface on macOS, a DXGI shared texture on Windows) and is
+/// shown as a [Texture] (so it composites/transforms/clips like any widget).
+/// Pointer + keyboard input is forwarded by coordinate, and the page cursor
+/// drives a [MouseRegion]. macOS and Windows (the package is federated — see
+/// `flutter_cef_macos`, `flutter_cef_windows`, and `PORTING.md`).
library;
export 'package:flutter_cef_platform_interface/flutter_cef_platform_interface.dart'
diff --git a/lib/src/cef_web_controller.dart b/lib/src/cef_web_controller.dart
index a6733dd..dcdb669 100644
--- a/lib/src/cef_web_controller.dart
+++ b/lib/src/cef_web_controller.dart
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
+import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
@@ -808,8 +809,13 @@ class CefWebController {
///
/// Host-trusted content: rendered regardless of the view's `allowedSchemes`
/// (so `file:` need not be in the allowlist to use this).
- Future loadFile(String absolutePath) =>
- _loadTrusted('file://$absolutePath');
+ Future loadFile(String absolutePath) => _loadTrusted(
+ // A Windows path (drive letter + backslashes, e.g. `C:\a\b.html`) needs
+ // Uri.file to become a valid `file:///C:/a/b.html`; the POSIX branch keeps
+ // the historical byte-identical `file://` form macOS ships today.
+ defaultTargetPlatform == TargetPlatform.windows
+ ? Uri.file(absolutePath, windows: true).toString()
+ : 'file://$absolutePath');
/// Set the page content zoom. `level` is a Chromium zoom *level*; the zoom
/// *factor* is `1.2^level` (0 = 100%, 1 ≈ 120%, -1 ≈ 83%).
diff --git a/lib/src/cef_web_view.dart b/lib/src/cef_web_view.dart
index 031efe3..5f03a12 100644
--- a/lib/src/cef_web_view.dart
+++ b/lib/src/cef_web_view.dart
@@ -1,6 +1,7 @@
import 'dart:async';
+import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
@@ -25,7 +26,7 @@ const double _kZoomMax = 4.0;
/// The page renders off-screen in a `cef_host` subprocess and is shown here as
/// a texture (so it composites, transforms, and clips like any widget — unlike
/// a platform view). Pointer + keyboard input is forwarded by coordinate, and
-/// the page's cursor drives a [MouseRegion]. macOS only.
+/// the page's cursor drives a [MouseRegion]. macOS and Windows.
///
/// Keyboard input reaches the page as real `keydown → keypress → keyup` events,
/// so a focused control activates from the keyboard (Enter submits / clicks,
@@ -97,7 +98,8 @@ class CefWebView extends StatefulWidget {
/// Shown until the first frame arrives. Defaults to a dark blank box.
final Widget? placeholder;
- /// Invoked when the user presses ⌘F in the focused view. The view has no
+ /// Invoked when the user presses ⌘F (Ctrl+F on Windows) in the focused view.
+ /// The view has no
/// find-bar UI of its own — a host that wants find-in-page provides this to
/// open its own bar (which then drives [CefWebController.find] / `stopFind`
/// and reads `onFindResult`). When null, ⌘F falls through to the page as an
@@ -164,6 +166,13 @@ class CefWebView extends StatefulWidget {
class _CefWebViewState extends State
implements DeltaTextInputClient {
+ /// Windows uses Ctrl for the browser accelerators (Ctrl+C/V/X/A/Z, Ctrl+/-/0,
+ /// Ctrl+F) where macOS uses ⌘, and its key events carry Windows codes rather
+ /// than macOS NSEvent codes. Everything else in this file is cross-platform.
+ /// (In `flutter test` [defaultTargetPlatform] is android, which takes the
+ /// non-Windows — i.e. existing macOS — branches.)
+ static bool get _isWindows =>
+ defaultTargetPlatform == TargetPlatform.windows;
late final CefWebController _controller =
widget.controller ?? CefWebController(profile: widget.profile);
bool _ownsController = false;
@@ -483,8 +492,11 @@ class _CefWebViewState extends State
// the picker never opens. Flutter's own plugin documents this exact case.
// skipRemainingHandlers stops Flutter ancestors from eating it but still
// hands it to the platform; don't forward it to the page either.
+ // macOS-specific: on Windows Ctrl+Win+Space is not an emoji-picker chord
+ // (the Win key is an OS-level modifier), so it takes the ordinary key path.
final keys = HardwareKeyboard.instance;
- if (event.logicalKey == LogicalKeyboardKey.space &&
+ if (!_isWindows &&
+ event.logicalKey == LogicalKeyboardKey.space &&
keys.isControlPressed &&
keys.isMetaPressed) {
return KeyEventResult.skipRemainingHandlers;
@@ -495,11 +507,16 @@ class _CefWebViewState extends State
// ones to explicit controller calls so a focused webview behaves like a real
// browser (⌘C/X/V/A/Z, ⌘+/-/0). Handled on key-down; zoom also on repeat
// (hold to keep zooming). Returning `handled` keeps the raw combo off the page
- // AND off Flutter's own shortcuts.
- final isMetaOnly = keys.isMetaPressed &&
- !keys.isControlPressed &&
- !keys.isAltPressed;
- if (isMetaOnly && (event is KeyDownEvent || event is KeyRepeatEvent)) {
+ // AND off Flutter's own shortcuts. The accelerator modifier is ⌘ on macOS
+ // and Ctrl on Windows (Ctrl+C/V/X/A/Z, Ctrl+/-/0, Ctrl+F).
+ final isAccelOnly = _isWindows
+ ? (keys.isControlPressed &&
+ !keys.isMetaPressed &&
+ !keys.isAltPressed)
+ : (keys.isMetaPressed &&
+ !keys.isControlPressed &&
+ !keys.isAltPressed);
+ if (isAccelOnly && (event is KeyDownEvent || event is KeyRepeatEvent)) {
final k = event.logicalKey;
// Editing commands: key-down only (repeat would re-cut/re-paste).
if (event is KeyDownEvent && !keys.isShiftPressed) {
@@ -530,6 +547,14 @@ class _CefWebViewState extends State
unawaited(_controller.redo());
return KeyEventResult.handled;
}
+ // Windows convention: Ctrl+Y is redo (alongside Ctrl+Shift+Z above).
+ if (_isWindows &&
+ event is KeyDownEvent &&
+ !keys.isShiftPressed &&
+ k == LogicalKeyboardKey.keyY) {
+ unawaited(_controller.redo());
+ return KeyEventResult.handled;
+ }
// Content zoom (⌘+/-/0). `=`/`+` in, `-` in, `0` reset. Repeat-friendly.
if (k == LogicalKeyboardKey.equal || k == LogicalKeyboardKey.add) {
_applyZoom((_zoomLevel + _kZoomStep).clamp(_kZoomMin, _kZoomMax));
@@ -559,12 +584,18 @@ class _CefWebViewState extends State
// native_key_code MUST be the macOS keycode for the physical key — CEF on
// macOS keys editing/navigation off it. Deriving it from the Windows VK
// collides (e.g. 0 -> VK 0x30 == macOS keycode 48 == Tab, moving focus).
- final nkc = cefMacNativeKeyCode(event.physicalKey) ?? wkc;
+ // On Windows CEF keys everything off windows_key_code, so the VK is the
+ // right native code there (and the macOS table would be wrong).
+ final nkc =
+ _isWindows ? wkc : (cefMacNativeKeyCode(event.physicalKey) ?? wkc);
final ch = event.character;
final isText = ch != null && _isPrintable(ch);
// Editing / navigation keys MUST carry the macOS NSEvent character or CEF
// OSR double-applies them (one Backspace deletes two, one arrow moves two).
- final keyChar = cefMacCharForKey(event.logicalKey);
+ // macOS-only: the table holds NSEvent codepoints (incl. private-use
+ // 0xF7xx function-key values) that would corrupt a Windows key event —
+ // Windows CEF derives the character from the VK itself, so send 0 there.
+ final keyChar = _isWindows ? 0 : cefMacCharForKey(event.logicalKey);
// The page should see keydown→keypress→keyup for every character, like a
// browser. We always send RAWKEYDOWN/KEYUP; the keypress (CHAR) is
// synthesized by [_commitText] when the IME's insertText delivers a typed
diff --git a/packages/flutter_cef_windows/.gitignore b/packages/flutter_cef_windows/.gitignore
new file mode 100644
index 0000000..c822747
--- /dev/null
+++ b/packages/flutter_cef_windows/.gitignore
@@ -0,0 +1,3 @@
+# Standalone cef_host build output (build_cef_host.bat's default BUILD_DIR;
+# the flutter-driven build uses the example's binary dir instead).
+native/cef_host/build/
diff --git a/packages/flutter_cef_windows/CHANGELOG.md b/packages/flutter_cef_windows/CHANGELOG.md
new file mode 100644
index 0000000..5706268
--- /dev/null
+++ b/packages/flutter_cef_windows/CHANGELOG.md
@@ -0,0 +1,7 @@
+# 0.1.0
+
+- Initial Windows package skeleton (Phase 1 of the Windows-port vertical
+ slice): endorsed federated plugin, stub C++ plugin answering every
+ `flutter_cef` channel verb, `cef_host` Windows host skeleton
+ (pipe connect + `kOpReady`), and `native/cef_host/PROTOCOL.md` — the
+ transcribed wire/channel contract builders implement against.
diff --git a/packages/flutter_cef_windows/LICENSE b/packages/flutter_cef_windows/LICENSE
new file mode 100644
index 0000000..3f83b7d
--- /dev/null
+++ b/packages/flutter_cef_windows/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 the flutter_cef authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/flutter_cef_windows/README.md b/packages/flutter_cef_windows/README.md
new file mode 100644
index 0000000..e3dc0f2
--- /dev/null
+++ b/packages/flutter_cef_windows/README.md
@@ -0,0 +1,35 @@
+# flutter_cef_windows
+
+The endorsed Windows implementation of [`flutter_cef`](../../README.md): a
+live Chromium (CEF) browser rendered off-screen in a `cef_host` subprocess
+and composited into the Flutter scene as a `Texture` (DXGI shared-texture
+path).
+
+**Status: Phase-1 skeleton of the Windows-port vertical slice.** The plugin
+registers the `flutter_cef` channel and answers every verb (stubbed), the
+`cef_host` skeleton connects the IPC pipe and announces `kOpReady`, and the
+example app builds/runs with a blank tile. The authoritative plan is
+`specs/windows-port/PLAN.md` + `SPIKES.md`; the wire/channel contract the
+implementation follows is [`native/cef_host/PROTOCOL.md`](native/cef_host/PROTOCOL.md).
+
+## Layout
+
+- `lib/flutter_cef_windows.dart` — `registerWith()` endorsing the shared
+ method-channel platform implementation.
+- `windows/` — the C++ plugin: channel dispatch (`flutter_cef_plugin.*`),
+ `texture_bridge.*` (Flutter texture side), `host_process.*` (cef_host
+ spawn/lifecycle), `ipc_pipe.*` (named-pipe framing).
+- `native/cef_host/` — the standalone CEF OSR host: `cef_host_win.cc`
+ builds as `cef_host.dll` (exports `RunConsoleMain`), shipped beside CEF's
+ `bootstrapc.exe` renamed to `cef_host.exe`. `build_cef_host.bat` builds it
+ (driven from the plugin CMake during `flutter build windows`);
+ `fetch_cef.ps1` resolves the pinned CEF distribution.
+
+## Build prerequisites (dev box)
+
+- VS2022 (MSVC + the bundled CMake/Ninja; they are NOT on PATH — the build
+ scripts use absolute paths), Flutter 3.38.8, Developer Mode ON (plugin
+ symlinks are true symlinks).
+- The pinned CEF distribution
+ `cef_binary_144.0.27+g3fae261+chromium-144.0.7559.254_windows64_minimal`,
+ resolved via the `CEF_ROOT` env var (see `native/cef_host/fetch_cef.ps1`).
diff --git a/packages/flutter_cef_windows/lib/flutter_cef_windows.dart b/packages/flutter_cef_windows/lib/flutter_cef_windows.dart
new file mode 100644
index 0000000..eb11ee5
--- /dev/null
+++ b/packages/flutter_cef_windows/lib/flutter_cef_windows.dart
@@ -0,0 +1,19 @@
+import 'package:flutter_cef_platform_interface/flutter_cef_platform_interface.dart';
+
+/// The Windows implementation of `flutter_cef`.
+///
+/// The Windows plugin is native-only: the C++ `FlutterCefPlugin` (see
+/// `windows/`) spawns and talks to a per-profile `cef_host` subprocess over a
+/// named-pipe IPC and answers the `flutter_cef` method channel. This Dart
+/// class exists only to **endorse** the default method-channel platform
+/// instance at registration time; there is no Windows-specific Dart behavior.
+///
+/// Registered via `dartPluginClass: FlutterCefWindows` in this package's
+/// pubspec — the Flutter tool calls [registerWith] during plugin registration.
+class FlutterCefWindows {
+ /// Sets the [FlutterCefPlatform] instance to the method-channel
+ /// implementation (the contract the native Windows plugin speaks).
+ static void registerWith() {
+ FlutterCefPlatform.instance = MethodChannelFlutterCef();
+ }
+}
diff --git a/packages/flutter_cef_windows/native/cef_host/CMakeLists.txt b/packages/flutter_cef_windows/native/cef_host/CMakeLists.txt
new file mode 100644
index 0000000..2986b06
--- /dev/null
+++ b/packages/flutter_cef_windows/native/cef_host/CMakeLists.txt
@@ -0,0 +1,88 @@
+# cef_host (Windows) — standalone CEF OSR host, built OUTSIDE the Flutter
+# plugin build (driven by build_cef_host.bat, which the plugin CMakeLists
+# invokes via add_custom_command).
+#
+# Ship shape (LAW 8, S2): the host is a client DLL `cef_host.dll` exporting
+# RunConsoleMain, loaded by CEF's prebuilt bootstrap. We ship bootstrapc.exe
+# RENAMED to cef_host.exe beside the DLL (the rename selects the module —
+# no --module flag needed; clean Task Manager names). The slice runs
+# no_sandbox=1 (sandbox is P11), but the bootstrap layout is the shipping
+# contract from day one so P11 is a settings flip, not a restructure.
+#
+# CEF's own cmake config (find_package(CEF)) drives compiler flags (/MT):
+# this target never links against Flutter, so the plugin's /MD rule (S6a)
+# does not apply here. The client DLL must link delayimp.lib (CEF's
+# /DELAYLOAD list — SPIKES.md S2).
+
+cmake_minimum_required(VERSION 3.21)
+project(cef_host_win LANGUAGES CXX)
+
+# CEF_ROOT: -DCEF_ROOT=... > env CEF_ROOT > the dev-box spike extraction.
+if(NOT DEFINED CEF_ROOT)
+ if(DEFINED ENV{CEF_ROOT})
+ set(CEF_ROOT "$ENV{CEF_ROOT}")
+ else()
+ set(CEF_ROOT "C:/dev/flutter_cef_spikes/cef/cef_binary_144.0.27+g3fae261+chromium-144.0.7559.254_windows64_minimal")
+ endif()
+endif()
+if(NOT EXISTS "${CEF_ROOT}/cmake")
+ message(FATAL_ERROR
+ "CEF_ROOT not found at '${CEF_ROOT}'. Run fetch_cef.ps1 or set CEF_ROOT "
+ "(env or -DCEF_ROOT=) to the extracted cef_binary_144.0.27 windows64_minimal dir.")
+endif()
+list(APPEND CMAKE_MODULE_PATH "${CEF_ROOT}/cmake")
+find_package(CEF REQUIRED)
+
+# libcef_dll_wrapper: reuse a prebuilt (/MT, matching CEF's default flags)
+# when provided, else build it in-tree. -DCEF_WRAPPER_LIB=.
+if(NOT DEFINED CEF_WRAPPER_LIB)
+ set(_default_prebuilt
+ "C:/dev/flutter_cef_spikes/cefbuild/libcef_dll_wrapper/libcef_dll_wrapper.lib")
+ if(EXISTS "${_default_prebuilt}")
+ set(CEF_WRAPPER_LIB "${_default_prebuilt}")
+ endif()
+endif()
+if(DEFINED CEF_WRAPPER_LIB AND EXISTS "${CEF_WRAPPER_LIB}")
+ set(WRAPPER_LIB "${CEF_WRAPPER_LIB}")
+else()
+ add_subdirectory("${CEF_LIBCEF_DLL_WRAPPER_PATH}" libcef_dll_wrapper)
+ set(WRAPPER_LIB libcef_dll_wrapper)
+endif()
+
+# The client DLL (loaded by the renamed bootstrapc.exe).
+add_library(cef_host SHARED
+ cef_host_win.cc
+ cef_host_protocol.h
+)
+SET_LIBRARY_TARGET_PROPERTIES(cef_host)
+# NOMINMAX: cef_types_win.h pulls windows.h; without it std::min/max -> C2589
+# (S6a). CEF's own configs may set it too — harmless twice.
+target_compile_definitions(cef_host PRIVATE NOMINMAX)
+target_include_directories(cef_host PRIVATE ${CEF_ROOT})
+target_link_libraries(cef_host
+ ${WRAPPER_LIB}
+ "${CEF_ROOT}/Release/libcef.lib"
+ delayimp.lib
+ # The OnAcceleratedPaint NT->legacy bridge blit (S1 pixel path).
+ d3d11.lib
+ dxgi.lib
+)
+
+# ---- pipe_probe: standalone IPC gate test (native/cef_host/test/) ----
+# Plain Win32 + cef_host_protocol.h, NO CEF link. Acts as the plugin side of
+# the named pipe: spawns cef_host.exe, drives kOpCreateBrowser/kOpNavigate/
+# kOpShutdown and asserts kOpReady/kOpCreated/kOpPresent/events. Built on
+# demand (`--target pipe_probe`); build_cef_host.bat builds only cef_host, so
+# plugin builds never compile this.
+add_executable(pipe_probe EXCLUDE_FROM_ALL test/pipe_probe.cc)
+target_include_directories(pipe_probe PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
+target_compile_definitions(pipe_probe PRIVATE NOMINMAX)
+
+# Stage the ship pair beside the build output: cef_host.dll + cef_host.exe
+# (bootstrapc.exe renamed — LAW 8).
+add_custom_command(TARGET cef_host POST_BUILD
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ "${CEF_ROOT}/Release/bootstrapc.exe"
+ "$/cef_host.exe"
+ COMMENT "Staging bootstrapc.exe as cef_host.exe beside cef_host.dll"
+)
diff --git a/packages/flutter_cef_windows/native/cef_host/PROTOCOL.md b/packages/flutter_cef_windows/native/cef_host/PROTOCOL.md
new file mode 100644
index 0000000..0c85cfd
--- /dev/null
+++ b/packages/flutter_cef_windows/native/cef_host/PROTOCOL.md
@@ -0,0 +1,239 @@
+# flutter_cef Windows wire + channel contract (slice)
+
+TRANSCRIBED from the macOS reference implementation — do not invent. Sources
+(line numbers as of branch `feat/windows-port-p0`):
+
+- `packages/flutter_cef_macos/native/cef_host/main.mm` — opcode table
+ (`kOp*`, main.mm:111-164), framing (main.mm:40-45, 416-446), read-loop
+ payload decoding (main.mm:2338-2603).
+- `packages/flutter_cef_macos/macos/Classes/FlutterCefPlugin.swift` — channel
+ verb dispatch (`handle`, FlutterCefPlugin.swift:111-242) and native->Dart
+ events (`emit` sites, FlutterCefPlugin.swift:359-430, 490, 521, 531, 541,
+ 554-558).
+- `packages/flutter_cef_macos/macos/Classes/CefWebSession.swift:28-73` and
+ `CefProfileHost.swift:21-42` — the Swift copies of the opcode table (must
+ match main.mm; they do).
+- `lib/src/cef_web_controller.dart` — the exact channel-arg maps Dart sends
+ (cited per verb below).
+
+The Windows host + plugin speak EXACTLY this protocol with **one payload
+difference**: `kOpPresent` (see below). Opcode numbers, framing, byte order,
+and every other payload are copied verbatim. Protocol version byte = **3**
+(main.mm:108, CefProfileHost.swift:42).
+
+---
+
+## 1. IPC framing (byte stream over the named pipe)
+
+Identical to macOS (main.mm:40-45, SendFrame main.mm:416-446, reader
+main.mm:2338-2357):
+
+```
+[u32 bodyLen BE][u32 browserId BE][u8 opcode][payload...]
+```
+
+- `bodyLen` = 4 (browserId) + 1 (opcode) + payloadLen — counts every byte
+ after the length prefix.
+- Guard on read: `5 <= bodyLen <= 64 MiB`, else the stream is desynced —
+ log + tear down the whole process (main.mm:2343-2351).
+- `browserId` is the PLUGIN-assigned wire id (>= 1). `browserId 0` =
+ process/profile level (`kOpReady`, process-level `kOpLog`, inbound
+ `kOpShutdown`) (main.mm:41-43).
+- ALL multi-byte integers are BIG-ENDIAN, including `f64` (IEEE-754 double,
+ BE byte order — `ReadF64BE`/`WriteF64BE`, see main.mm ReadU32BE:476).
+- Writes are assembled into one contiguous frame and written atomically
+ under a write mutex, so a partial write never desyncs the peer
+ (main.mm:431-445).
+- Transport on Windows: named pipe `\\.\pipe\flutter_cef__`,
+ `PIPE_TYPE_BYTE | PIPE_READMODE_BYTE`, single instance; plugin is the
+ server (`CreateNamedPipeW`), cef_host connects with `CreateFileW`
+ (+ `SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS`). Same framing on top.
+- **OVERLAPPED I/O is REQUIRED on any pipe end that reads and writes from
+ different threads** (empirical, cef_host + pipe_probe 2026-07-20): Windows
+ serializes I/O on a synchronous pipe file object, so a dedicated reader
+ thread's pending blocking `ReadFile` makes every `WriteFile` on the same
+ handle queue behind it. In cef_host that froze the CEF UI thread inside
+ `SendFrame(kOpCreated)`, stalled `CefRunMessageLoop`, and every Chromium
+ child process died with "Terminating current process after 15 seconds with
+ no connection" (no renderer/GPU/network, browsers never created). Fix:
+ open/create the handle with `FILE_FLAG_OVERLAPPED` and run all reads AND
+ writes as event-based overlapped ops (`cef_host_win.cc` `OverlappedIo`).
+ The plugin's server end (`CreateNamedPipeW` + reader thread + writes from
+ the platform thread) has the identical shape and MUST also pass
+ `FILE_FLAG_OVERLAPPED`. A Unix socket fd is full-duplex, so the macOS
+ reference never encounters this.
+- Unknown opcode at either end: log ONCE per opcode value and drop the
+ frame — never kill the stream (main.mm:2583-2598).
+
+## 2. Opcode table (numbers verbatim from main.mm:111-164)
+
+Direction `H<-C` = cef_host -> plugin (event), `H->C` = plugin -> cef_host
+(command). Payload layouts are exactly the macOS ones except where marked
+**WINDOWS**.
+
+### cef_host -> plugin (0x01-0x1d)
+
+| Op | Name | Payload | Source |
+|---|---|---|---|
+| 0x01 | kOpPresent | **WINDOWS**: `{u64 bridgeHandle BE}{u32 srcW BE}{u32 srcH BE}` = 16 bytes. bridgeHandle = the DXGI **legacy** shared handle (`IDXGIResource::GetSharedHandle`) of the host-minted `MISC_SHARED` bridge texture; srcW/srcH = the PHYSICAL px dims of the frame actually composited (the size-gate signal). macOS reference is 12 bytes `{u32 iosurfaceId}{u32 srcW}{u32 srcH}` — same semantics, different token width. | main.mm:654-665 (semantics + size gate), SPIKES.md S1/S4, LAW 10 |
+| 0x02 | kOpReady | `{u8 readyFlags}{u8 protocolVersion}` on browserId 0. readyFlags bit0 = ad-hoc/mock-keychain build (macOS-only concern; Windows sends 0). protocolVersion = 3. Sent from `OnContextInitialized`, BEFORE any browser exists. | main.mm:1664-1682 |
+| 0x03 | kOpCursor | `{u32 cef_cursor_type_t}` | main.mm:1456-1466 |
+| 0x04 | kOpLog | `{utf8 message}` (browserId 0 = process-level) | main.mm:448-450 |
+| 0x05 | kOpLoadState | `{u8 loading}{u8 canGoBack}{u8 canGoForward}` | main.mm:115, 456-462 |
+| 0x06 | kOpTitle | `{utf8 title}` | main.mm:116 |
+| 0x07 | kOpUrl | `{utf8 main-frame url}` | main.mm:117 |
+| 0x08 | kOpLoadErr | `{u32 code}{utf8 "url\ntext"}` | main.mm:118, 464-474 |
+| 0x09 | kOpConsole | `{u32 level}{utf8 "source:line\tmsg"}` | main.mm:119 |
+| 0x0a | kOpPageStart | `{utf8 url}` main frame load started | main.mm:120 |
+| 0x0b | kOpPageFinish | `{utf8 url}` main frame load finished | main.mm:121 |
+| 0x0c | kOpProgress | `{u32 percent 0-100}` | main.mm:122, 1396 |
+| 0x0d | kOpNewWindow | `{utf8 url}` popup / target=_blank | main.mm:123 |
+| 0x0e | kOpFindResult | `{u32 count}{u32 activeOrdinal}{u8 final}` = 9 bytes | main.mm:124, 1274 |
+| 0x0f | kOpJsDialog | `{u32 id}{u32 type}{u32 msgLen}{msg utf8}{defaultText utf8}` | main.mm:125, 1300 |
+| 0x16 | kOpEvalResult | `{utf8 "id:json"}` (runJavaScriptReturningResult) | main.mm:126 |
+| 0x17 | kOpChannelMsg | `{utf8 "name:message"}` JS channel -> host | main.mm:127 |
+| 0x18 | kOpDownload | `{utf8 suggestedName}` a download started | main.mm:128 |
+| 0x19 | kOpImeBounds | `{u32 x}{u32 y}{u32 w}{u32 h}` caret rect (DIP) | main.mm:129, 1050 |
+| 0x1a | kOpCookies | `{u32 id}{utf8 json-array}` visitAllCookies result | main.mm:130 |
+| 0x1b | kOpTargetId | `{utf8 targetId}` this browser's CDP targetId (reply to 0x36) | main.mm:131 |
+| 0x1c | kOpCreated | `{}` OnAfterCreated — browser is up (create pacer advance) | main.mm:132, 1406 |
+| 0x1d | kOpCreateFailed | `{}` async CreateBrowser dispatch failed — drop the session | main.mm:133, 1705 |
+
+### plugin -> cef_host (0x10-0x38)
+
+Payload minimums are enforced host-side exactly as the macOS read loop does
+(cited); short frames are dropped per-op, not fatal.
+
+| Op | Name | Payload | Source |
+|---|---|---|---|
+| 0x10 | kOpPointer | `{u8 type}{u8 button}{u8 clickCount}{u8 pad}{u32 modifiers}{f64 x}{f64 y}{f64 dx}{f64 dy}` = 40 bytes. type: 0=move 1=down 2=up 3=wheel 4=leave; button: 0=left 1=middle 2=right. x/y logical (DIP). | main.mm:2560-2570 |
+| 0x11 | kOpResize | `{u32 w}{u32 h}[{f64 dpr}]` — plen>=8; dpr present iff plen>=16, `0`/absent = unchanged; guard `0 < dpr <= 8` else treat as 0. Producer-allocates: no surface id. EVERY WasResized discards CEF's frame pool (LAW 4). | main.mm:135, 2384-2394 |
+| 0x12 | kOpKey | `{u8 type}{u8 pad×3}{u32 modifiers}{u32 windowsKeyCode}{u32 nativeKeyCode}{u32 character}` = 20 bytes. type: 0=rawkeydown 2=keyup 3=char. wkc/nkc are i32 stored as u32 BE. | main.mm:136, 2571-2582 |
+| 0x13 | kOpCreateBrowser | `{u32 w}{u32 h}{f64 dpr}{utf8 url}` (plen>=16); frame browserId = the NEW wire id; producer-allocates (no surface id); empty url -> about:blank; guard `0 < dpr <= 8` else 1.0. | main.mm:137, 2364-2376 |
+| 0x14 | kOpShutdown | `{}` tear down the whole PROCESS (all browsers); browserId 0. | main.mm:138, 2381-2383 |
+| 0x15 | kOpDisposeBrowser | `{}` close ONE browser (target = frame browserId); process survives. | main.mm:139, 2377-2380 |
+| 0x20 | kOpNavigate | `{utf8 url}` — do NOT require a bound slot; resolve by wire id on the UI thread (a nav right behind a queued create must not drop). | main.mm:140, 2395-2402 |
+| 0x21 | kOpReload | `{}` | main.mm:141 |
+| 0x22 | kOpStop | `{}` | main.mm:142 |
+| 0x23 | kOpBack | `{}` | main.mm:143 |
+| 0x24 | kOpForward | `{}` | main.mm:144 |
+| 0x25 | kOpExecuteJs | `{utf8 code}` | main.mm:145 |
+| 0x26 | kOpSetZoom | `{f64 level}` (factor = 1.2^level) | main.mm:146, 2434-2439 |
+| 0x27 | kOpFind | `{u8 fwd}{u8 matchCase}{u8 findNext}{utf8 text}` (plen>=3) | main.mm:147, 2452-2459 |
+| 0x28 | kOpStopFind | `{u8 clearSelection}` (absent = 1) | main.mm:148, 2460-2465 |
+| 0x29 | kOpJsDialogResp | `{u32 id}{u8 ok}{utf8 text}` (plen>=5) | main.mm:149, 2466-2474 |
+| 0x2a | kOpEvalReturning | `{u32 id}{utf8 code}` (plen>=4) | main.mm:150, 2475-2482 |
+| 0x2b | kOpAddChannel | `{utf8 name}` — do NOT require a bound slot (registers process-global; injected on load). | main.mm:151, 2483-2494 |
+| 0x2c | kOpSetCookie | `{utf8 url\0name\0value\0domain\0path}` (NUL-separated, pad missing fields to 5) | main.mm:152, 2495-2510 |
+| 0x2d | kOpClearCookies | `{}` delete all cookies | main.mm:153 |
+| 0x2e | kOpVisitCookies | `{u32 id}{utf8 url}` enumerate (url empty = all) | main.mm:154, 2515-2522 |
+| 0x2f | kOpDeleteCookie | `{utf8 url\0name}` delete one | main.mm:155, 2523-2531 |
+| 0x30 | kOpImeSetComp | `{utf8 text}` IME composition update | main.mm:156 |
+| 0x31 | kOpImeCommit | `{utf8 text}` commit composed text | main.mm:157 |
+| 0x32 | kOpImeCancel | `{}` cancel composition | main.mm:158 |
+| 0x33 | kOpShowDevTools | `{}` open DevTools in a window | main.mm:159 |
+| 0x34 | kOpLoadTrusted | `{utf8 url}` host content-load, exempt from allowlist; do NOT require a bound slot (same as 0x20). | main.mm:160, 2403-2411 |
+| 0x35 | kOpSetVisible | `{u8 visible}` (absent = 1) -> `WasHidden(!visible)` | main.mm:161, 2446-2451 |
+| 0x36 | kOpResolveTargetId | `{}` resolve this browser's CDP targetId -> kOpTargetId | main.mm:162 |
+| 0x37 | kOpInvalidate | `{}` force a repaint (`Invalidate(PET_VIEW)`) to re-kick a stalled first frame | main.mm:163 |
+| 0x38 | kOpEditCommand | `{u8 cmd}` focused-frame edit command: 0=copy 1=cut 2=paste 3=selectAll 4=undo 5=redo | main.mm:164, 2440-2445 |
+
+Reserved (do NOT reuse): `0x1e` was earmarked `kOpPresentV2` by PLAN §4.3
+stage-1; the slice instead reuses `kOpPresent 0x01` with the Windows payload
+(LAW 10) because the Windows plugin is the only peer of the Windows host.
+
+## 3. Method-channel verbs (Dart -> plugin), channel `flutter_cef`
+
+Verb names + arg keys verbatim from FlutterCefPlugin.swift:113-241 and
+cef_web_controller.dart (invokeMethod sites). Every arg map carries
+`sessionId` (String). SLICE = must be functional for the vertical slice;
+STUB = reply success/null + `OutputDebugString` warning, never an error.
+
+| Verb | Args (beyond sessionId) | Returns | Maps to | Slice? | Source |
+|---|---|---|---|---|---|
+| create | url:String, width:int, height:int, dpr:double, allowedSchemes:String? (csv, omit-when-empty), enableCdp:bool? (omit-when-false), agentControl:bool? (omit-when-false), profile:String? (omit-when-empty) | `{textureId:int, width:int, height:int, cdpPort:int}` | spawn host (if needed) + kOpCreateBrowser 0x13 | SLICE | Swift:255-446, controller:506-523 |
+| navigate | url:String | null | 0x20 | SLICE | Swift:617-622, controller:566 |
+| loadTrusted | url:String | null | 0x34 | SLICE (stub-ok) | Swift:626-631, controller:634 |
+| resize | width:int, height:int, dpr:double | `{textureId:int}` (or null if unknown session) | 0x11 | SLICE | Swift:633-641, controller:874 |
+| getFrameSurface | — | `{surfaceId:int, width:int, height:int}` (physical px) or null | plugin-local | STUB | Swift:649-658 |
+| dispose | — | null | 0x15 (last browser: 0x14 + host teardown) | SLICE | Swift:660-663, controller:530/948 |
+| pointer | type:int, button:int, clickCount:int, modifiers:int, x:double, y:double, dx:double, dy:double | null | 0x10 | SLICE | Swift:730-740, controller:895 |
+| key | type:int, modifiers:int, windowsKeyCode:int, nativeKeyCode:int, character:int | null | 0x12 | SLICE | Swift:742-752, controller:919 |
+| reload | — | null | 0x21 | SLICE | Swift:122 |
+| stop | — | null | 0x22 | SLICE | Swift:123 |
+| goBack | — | null | 0x23 | SLICE | Swift:124 |
+| goForward | — | null | 0x24 | SLICE | Swift:125 |
+| executeJavaScript | code:String | null | 0x25 | SLICE | Swift:126-130 |
+| setZoomLevel | level:double | null | 0x26 | STUB | Swift:131-133 |
+| editCommand | command:int | null | 0x38 | STUB | Swift:134-136 |
+| setVisible | visible:bool | null | 0x35 | SLICE | Swift:137-139 |
+| find | text:String, forward:bool, matchCase:bool, findNext:bool | null | 0x27 | STUB | Swift:140-148 |
+| stopFind | clearSelection:bool | null | 0x28 | STUB | Swift:149-151 |
+| respondJsDialog | id:int, ok:bool, text:String | null | 0x29 | STUB | Swift:152-158 |
+| evalReturning | id:int, code:String | null | 0x2a | STUB | Swift:159-165 |
+| addJavaScriptChannel | name:String | null | 0x2b | STUB | Swift:166-170 |
+| setCookie | url, name, value, domain, path : String | null | 0x2c | STUB | Swift:171-179 |
+| clearCookies | — | null | 0x2d | STUB | Swift:180-182 |
+| visitCookies | id:int, url:String | null | 0x2e | STUB | Swift:183-188 |
+| deleteCookie | url:String, name:String | null | 0x2f | STUB | Swift:189-194 |
+| showDevTools | — | null | 0x33 | STUB | Swift:195-197 |
+| enableAgentControl | — | `{wsUrl, token, port}` or FlutterError | CDP relay (P9) | STUB (null) | Swift:198-217 |
+| disableAgentControl | — | null | CDP relay (P9) | STUB | Swift:218-225 |
+| showEmojiPicker | — | null | macOS-only (Character Palette) | STUB | Swift:226-230 |
+| imeSetComposition | text:String | null | 0x30 | STUB | Swift:231-233 |
+| imeCommitText | text:String | null | 0x31 | STUB | Swift:234-236 |
+| imeCancelComposition | — | null | 0x32 | STUB | Swift:237-239 |
+
+macOS replies `FlutterMethodNotImplemented` for unknown verbs
+(Swift:240); the WINDOWS SLICE deviates deliberately: unknown/unimplemented
+verbs reply success(null) + `OutputDebugString` so the example app never sees
+a MissingPluginException-style error (slice contract).
+
+## 4. Events (plugin -> Dart), channel `flutter_cef`
+
+Method names + payload keys verbatim from the Swift `emit` sites. Every
+payload carries `sessionId:String`. `invokeMethod` MUST run on the platform
+thread (marshal from the reader thread).
+
+| Method | Payload (beyond sessionId) | From opcode | Source |
+|---|---|---|---|
+| cursor | cursor:int (cef_cursor_type_t) | 0x03 | Swift:359-361 |
+| loadingState | isLoading:bool, canGoBack:bool, canGoForward:bool | 0x05 | Swift:362-367 |
+| title | title:String | 0x06 | Swift:368-370 |
+| url | url:String | 0x07 | Swift:371-373 |
+| loadError | code:int, url:String, text:String (split payload at first '\n') | 0x08 | Swift:374-378 |
+| consoleMessage | level:int, message:String | 0x09 | Swift:379-383 |
+| pageStarted | url:String | 0x0a | Swift:384-386 |
+| pageFinished | url:String | 0x0b | Swift:387-389 |
+| progress | progress:int | 0x0c | Swift:390-392 |
+| newWindow | url:String | 0x0d | Swift:393-395 |
+| findResult | count:int, activeMatchOrdinal:int, isFinal:bool | 0x0e | Swift:396-401 |
+| jsDialog | id:int, type:int, message:String, defaultText:String | 0x0f | Swift:402-407 |
+| evalResult | payload:String ("id:json") | 0x16 | Swift:408-410 |
+| channelMessage | payload:String ("name:message") | 0x17 | Swift:411-413 |
+| download | suggestedName:String | 0x18 | Swift:414-416 |
+| imeCompositionBounds | x:int, y:int, w:int, h:int | 0x19 | Swift:417-421 |
+| cookies | id:int, json:String | 0x1a | Swift:422-424 |
+| onSurface | surfaceId:int, width:int, height:int (physical px) — Windows: surfaceId = the bridge-handle token as int64 | 0x01 (on surface (re)alloc) | Swift:425-433 |
+| processGone | reason:String — "crashed" \| "locked" (host exit code 2) \| "createFailed" (0x1d) \| "respawnFailed" \| "protocolMismatch(host=vN)" | host death / 0x1d / handshake | Swift:490, 521, 531, 541, 601 |
+| paintStalled | — | watchdog (no 0x01 after create + 0x37 re-kick) | Swift:554-558 |
+
+## 5. Handshake + lifecycle rules (carry-over)
+
+- Plugin sends NOTHING until it receives `kOpReady`; it then checks
+ `protocolVersion == 3` and refuses (teardown + `processGone
+ protocolMismatch`) on skew (main.mm:100-108, Swift:528-533).
+- Host exit code 2 after a `kOpLog "profile-locked"` = profile already open
+ elsewhere -> `processGone reason:"locked"` (main.mm:2786-2806, Swift:521).
+- Present size-gate (LAW 4): the plugin promotes a presented bridge
+ handle to the Flutter texture ONLY when `{srcW,srcH}` matches the expected
+ `round(logical*dpr)` for the current size (±1 px); until then it keeps
+ serving the previous texture (main.mm:640-665 rationale).
+- Bridge-handle identity (LAW 3): the host-minted legacy handle is the
+ identity Flutter sees; never key anything on CEF's per-callback
+ `shared_texture_handle` values (SPIKES.md S4).
+- The plugin holds an opened `ID3D11Texture2D` ComPtr on the current bridge
+ handle for as long as it feeds it to Flutter (LAW 6 / S1 belt-1).
+- cef_host args (slice): `--ipc=` `--profile-dir=`
+ `--ephemeral` (cf. macOS args main.mm:32-38; `--cdp-port`/`--cdp-pipe`/
+ `--allowed-schemes` are post-slice).
diff --git a/packages/flutter_cef_windows/native/cef_host/build_cef_host.bat b/packages/flutter_cef_windows/native/cef_host/build_cef_host.bat
new file mode 100644
index 0000000..8729ac3
--- /dev/null
+++ b/packages/flutter_cef_windows/native/cef_host/build_cef_host.bat
@@ -0,0 +1,45 @@
+@echo off
+rem build_cef_host.bat — build cef_host.dll + stage cef_host.exe (renamed
+rem bootstrapc.exe, LAW 8). Invoked standalone by developers AND from the
+rem plugin's windows/CMakeLists.txt add_custom_command during
+rem `flutter build windows`.
+rem
+rem Usage: build_cef_host.bat [BUILD_DIR] [OUT_DIR]
+rem BUILD_DIR cmake binary dir (default: %~dp0build)
+rem OUT_DIR where cef_host.dll + cef_host.exe land (default: BUILD_DIR)
+rem Env overrides:
+rem CEF_ROOT extracted cef_binary_144.0.27 windows64_minimal dir
+rem CEF_WRAPPER_LIB prebuilt /MT libcef_dll_wrapper.lib to reuse
+rem
+rem Deterministic + incremental-safe: configure only when build.ninja is
+rem missing; ninja no-ops when nothing changed.
+
+setlocal
+set SRC_DIR=%~dp0
+if "%~1"=="" (set BUILD_DIR=%SRC_DIR%build) else (set BUILD_DIR=%~1)
+if "%~2"=="" (set OUT_DIR=%BUILD_DIR%) else (set OUT_DIR=%~2)
+
+rem MSVC env (cmake/ninja ship with VS2022 but are NOT on PATH — see
+rem specs/windows-port dev notes).
+call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul
+if errorlevel 1 exit /b 1
+set CMAKE="C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
+set NINJA_DIR=C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja
+set PATH=%NINJA_DIR%;%PATH%
+
+if not exist "%BUILD_DIR%\build.ninja" (
+ %CMAKE% -G Ninja -DCMAKE_BUILD_TYPE=Release -S "%SRC_DIR%." -B "%BUILD_DIR%"
+ if errorlevel 1 exit /b 2
+)
+%CMAKE% --build "%BUILD_DIR%" --target cef_host
+if errorlevel 1 exit /b 3
+
+if /i not "%OUT_DIR%"=="%BUILD_DIR%" (
+ if not exist "%OUT_DIR%" mkdir "%OUT_DIR%"
+ copy /y "%BUILD_DIR%\cef_host.dll" "%OUT_DIR%\cef_host.dll" >nul
+ if errorlevel 1 exit /b 4
+ copy /y "%BUILD_DIR%\cef_host.exe" "%OUT_DIR%\cef_host.exe" >nul
+ if errorlevel 1 exit /b 5
+)
+echo cef_host staged: %OUT_DIR%\cef_host.dll + cef_host.exe
+exit /b 0
diff --git a/packages/flutter_cef_windows/native/cef_host/cef_host_protocol.h b/packages/flutter_cef_windows/native/cef_host/cef_host_protocol.h
new file mode 100644
index 0000000..8d58f31
--- /dev/null
+++ b/packages/flutter_cef_windows/native/cef_host/cef_host_protocol.h
@@ -0,0 +1,139 @@
+// flutter_cef Windows — wire protocol constants + big-endian codecs.
+//
+// Shared by the cef_host process (native/cef_host/cef_host_win.cc) and the
+// Flutter plugin (windows/ipc_pipe.cpp): plain C++/Win32, no CEF includes, so
+// both builds can consume it.
+//
+// THE CONTRACT IS PROTOCOL.md (this directory) — transcribed from the macOS
+// reference `packages/flutter_cef_macos/native/cef_host/main.mm:111-164`.
+// Opcode numbers are copied VERBATIM from main.mm. Framing:
+// [u32 bodyLen BE][u32 browserId BE][u8 opcode][payload]
+// bodyLen = 4 + 1 + payloadLen; guard 5 <= bodyLen <= 64 MiB; browserId 0 =
+// process-level (kOpReady, process-level kOpLog, inbound kOpShutdown).
+//
+// ONE payload difference vs macOS (LAW 10): kOpPresent on Windows carries
+// {u64 bridgeHandle BE}{u32 srcW BE}{u32 srcH BE} (16 bytes)
+// instead of macOS's {u32 iosurfaceId}{u32 srcW}{u32 srcH} (12 bytes).
+// bridgeHandle is the DXGI LEGACY shared handle (IDXGIResource::
+// GetSharedHandle) of the host-minted D3D11_RESOURCE_MISC_SHARED bridge
+// texture — the identity Flutter sees (LAW 3). srcW/srcH are the PHYSICAL
+// pixel dims of the frame actually composited (the size-gate signal, LAW 4).
+
+#ifndef FLUTTER_CEF_WINDOWS_NATIVE_CEF_HOST_CEF_HOST_PROTOCOL_H_
+#define FLUTTER_CEF_WINDOWS_NATIVE_CEF_HOST_CEF_HOST_PROTOCOL_H_
+
+#include
+#include
+
+namespace flutter_cef {
+
+// ---- Wire protocol version ----
+// Announced in kOpReady's payload byte 1 (byte 0 is the ready-flags byte).
+// Must stay equal to main.mm:108 kCefHostProtocolVersion and
+// CefProfileHost.swift:42 — the wire is shared cross-platform.
+constexpr uint8_t kCefHostProtocolVersion = 3;
+
+// Framing guard (main.mm:2347): minimum body = 4 (browserId) + 1 (op).
+constexpr uint32_t kMinBodyLen = 5;
+constexpr uint32_t kMaxBodyLen = 64u << 20; // 64 MiB
+
+// ---- Opcodes: cef_host -> plugin (main.mm:111-133) ----
+constexpr uint8_t kOpPresent = 0x01; // WINDOWS: {u64 bridgeHandle}{u32 srcW}{u32 srcH}
+constexpr uint8_t kOpReady = 0x02; // {u8 readyFlags}{u8 protocolVersion} browserId 0
+constexpr uint8_t kOpCursor = 0x03; // {u32 cef_cursor_type_t}
+constexpr uint8_t kOpLog = 0x04; // {utf8}
+constexpr uint8_t kOpLoadState = 0x05; // {loading,back,forward : u8}
+constexpr uint8_t kOpTitle = 0x06; // {utf8}
+constexpr uint8_t kOpUrl = 0x07; // {utf8} main-frame address
+constexpr uint8_t kOpLoadErr = 0x08; // {code:u32}{utf8 "url\ntext"}
+constexpr uint8_t kOpConsole = 0x09; // {level:u32}{utf8 "source:line\tmsg"}
+constexpr uint8_t kOpPageStart = 0x0a; // {utf8 url} main frame load started
+constexpr uint8_t kOpPageFinish = 0x0b; // {utf8 url} main frame load finished
+constexpr uint8_t kOpProgress = 0x0c; // {u32 percent 0-100}
+constexpr uint8_t kOpNewWindow = 0x0d; // {utf8 url} popup / target=_blank
+constexpr uint8_t kOpFindResult = 0x0e; // {u32 count}{u32 activeOrdinal}{u8 final}
+constexpr uint8_t kOpJsDialog = 0x0f; // {u32 id}{u32 type}{u32 msgLen}{msg}{default}
+constexpr uint8_t kOpEvalResult = 0x16; // {utf8 "id:json"} runJavaScriptReturningResult
+constexpr uint8_t kOpChannelMsg = 0x17; // {utf8 "name:message"} JS channel -> host
+constexpr uint8_t kOpDownload = 0x18; // {utf8 suggestedName} a download started
+constexpr uint8_t kOpImeBounds = 0x19; // {u32 x}{u32 y}{u32 w}{u32 h} caret rect (DIP)
+constexpr uint8_t kOpCookies = 0x1a; // {u32 id}{utf8 json-array} visitAllCookies result
+constexpr uint8_t kOpTargetId = 0x1b; // {utf8 targetId} this browser's CDP targetId
+constexpr uint8_t kOpCreated = 0x1c; // {} OnAfterCreated — browser is up
+constexpr uint8_t kOpCreateFailed = 0x1d; // {} async CreateBrowser dispatch failed
+
+// ---- Opcodes: plugin -> cef_host (main.mm:134-164) ----
+constexpr uint8_t kOpPointer = 0x10; // {u8 type}{u8 btn}{u8 clicks}{u8 pad}{u32 mods}{f64 x}{f64 y}{f64 dx}{f64 dy}
+constexpr uint8_t kOpResize = 0x11; // {u32 w}{u32 h}{f64 dpr} — producer-allocates: no sid
+constexpr uint8_t kOpKey = 0x12; // {u8 type}{pad*3}{u32 mods}{u32 wkc}{u32 nkc}{u32 char}
+constexpr uint8_t kOpCreateBrowser = 0x13; // {u32 w}{u32 h}{f64 dpr}{utf8 url}; frame browserId = NEW id
+constexpr uint8_t kOpShutdown = 0x14; // {} tear down the whole PROCESS; frame browserId 0
+constexpr uint8_t kOpDisposeBrowser = 0x15; // {} close ONE browser; process survives
+constexpr uint8_t kOpNavigate = 0x20; // {utf8 url}
+constexpr uint8_t kOpReload = 0x21;
+constexpr uint8_t kOpStop = 0x22;
+constexpr uint8_t kOpBack = 0x23;
+constexpr uint8_t kOpForward = 0x24;
+constexpr uint8_t kOpExecuteJs = 0x25; // {utf8 code}
+constexpr uint8_t kOpSetZoom = 0x26; // {f64 level} (factor = 1.2^level)
+constexpr uint8_t kOpFind = 0x27; // {u8 fwd}{u8 matchCase}{u8 findNext}{utf8}
+constexpr uint8_t kOpStopFind = 0x28; // {u8 clearSelection}
+constexpr uint8_t kOpJsDialogResp = 0x29; // {u32 id}{u8 ok}{utf8 text}
+constexpr uint8_t kOpEvalReturning = 0x2a; // {u32 id}{utf8 code}
+constexpr uint8_t kOpAddChannel = 0x2b; // {utf8 name} register a JS channel
+constexpr uint8_t kOpSetCookie = 0x2c; // {utf8 url\0name\0value\0domain\0path}
+constexpr uint8_t kOpClearCookies = 0x2d; // {} delete all cookies
+constexpr uint8_t kOpVisitCookies = 0x2e; // {u32 id}{utf8 url} enumerate (url empty = all)
+constexpr uint8_t kOpDeleteCookie = 0x2f; // {utf8 url\0name} delete one
+constexpr uint8_t kOpImeSetComp = 0x30; // {utf8 text} IME composition update
+constexpr uint8_t kOpImeCommit = 0x31; // {utf8 text} commit composed text
+constexpr uint8_t kOpImeCancel = 0x32; // {} cancel composition
+constexpr uint8_t kOpShowDevTools = 0x33; // {} open DevTools in a window
+constexpr uint8_t kOpLoadTrusted = 0x34; // {utf8 url} host content-load, exempt from allowlist
+constexpr uint8_t kOpSetVisible = 0x35; // {u8 visible} -> CefBrowserHost::WasHidden(!visible)
+constexpr uint8_t kOpResolveTargetId = 0x36;// {} resolve CDP targetId -> kOpTargetId
+constexpr uint8_t kOpInvalidate = 0x37; // {} force a repaint (re-kick a stalled first frame)
+constexpr uint8_t kOpEditCommand = 0x38; // {u8 cmd} 0=copy 1=cut 2=paste 3=selectAll 4=undo 5=redo
+
+// 0x1e is RESERVED (PLAN §4.3's kOpPresentV2 earmark) — do not assign.
+
+// ---- Big-endian codecs (mirror main.mm ReadU32BE/WriteU32BE/ReadF64BE) ----
+
+inline uint32_t ReadU32BE(const uint8_t* p) {
+ return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) |
+ (uint32_t(p[2]) << 8) | uint32_t(p[3]);
+}
+
+inline void WriteU32BE(uint8_t* p, uint32_t v) {
+ p[0] = static_cast((v >> 24) & 0xff);
+ p[1] = static_cast((v >> 16) & 0xff);
+ p[2] = static_cast((v >> 8) & 0xff);
+ p[3] = static_cast(v & 0xff);
+}
+
+inline uint64_t ReadU64BE(const uint8_t* p) {
+ return (uint64_t(ReadU32BE(p)) << 32) | uint64_t(ReadU32BE(p + 4));
+}
+
+inline void WriteU64BE(uint8_t* p, uint64_t v) {
+ WriteU32BE(p, static_cast(v >> 32));
+ WriteU32BE(p + 4, static_cast(v & 0xffffffffu));
+}
+
+inline double ReadF64BE(const uint8_t* p) {
+ uint64_t bits = ReadU64BE(p);
+ double d;
+ static_assert(sizeof(d) == sizeof(bits), "double must be 64-bit");
+ std::memcpy(&d, &bits, sizeof(d));
+ return d;
+}
+
+inline void WriteF64BE(uint8_t* p, double d) {
+ uint64_t bits;
+ std::memcpy(&bits, &d, sizeof(bits));
+ WriteU64BE(p, bits);
+}
+
+} // namespace flutter_cef
+
+#endif // FLUTTER_CEF_WINDOWS_NATIVE_CEF_HOST_CEF_HOST_PROTOCOL_H_
diff --git a/packages/flutter_cef_windows/native/cef_host/cef_host_win.cc b/packages/flutter_cef_windows/native/cef_host/cef_host_win.cc
new file mode 100644
index 0000000..70787e4
--- /dev/null
+++ b/packages/flutter_cef_windows/native/cef_host/cef_host_win.cc
@@ -0,0 +1,1768 @@
+// cef_host (Windows) — a standalone CEF off-screen-rendering subprocess.
+//
+// The Flutter plugin (packages/flutter_cef_windows/windows/) spawns one
+// cef_host per profile and drives N browsers in it over a named-pipe IPC.
+// The wire contract is PROTOCOL.md + cef_host_protocol.h in this directory —
+// transcribed verbatim from the macOS reference
+// packages/flutter_cef_macos/native/cef_host/main.mm, with ONE payload
+// difference (LAW 10): kOpPresent carries {u64 bridgeHandle BE}{u32 srcW BE}
+// {u32 srcH BE} where bridgeHandle is the DXGI LEGACY shared handle of the
+// host-minted D3D11_RESOURCE_MISC_SHARED bridge texture.
+//
+// Ship shape (LAW 8, SPIKES.md S2): this builds as cef_host.dll exporting
+// RunConsoleMain, loaded by CEF's prebuilt bootstrapc.exe shipped RENAMED to
+// cef_host.exe beside it. sandbox_info is forwarded to BOTH CefExecuteProcess
+// and CefInitialize. The slice runs settings.no_sandbox = 1 (sandbox is P11).
+//
+// THE LAWS this file keeps (specs/windows-port/SPIKES.md):
+// 1. external_begin_frame_enabled = FALSE; windowless_frame_rate = 60.
+// (S4: with the external pump only the FIRST browser in the process ever
+// paints. The macOS PumpBeginFrame pacer is NOT ported; everywhere main.mm
+// calls SendExternalBeginFrame we use Invalidate(PET_VIEW) — with the
+// internal frame timer ON that is sufficient to drive a repaint.)
+// 2. OnAcceleratedPaint's NT handle is valid ONLY inside the callback:
+// OpenSharedResource1 + CopyResource to the legacy bridge INSIDE the
+// callback, synchronously. The NT handle is never stored.
+// 3. Identity is never keyed on shared_texture_handle VALUES (they alias
+// across sizes and browsers). The bridge texture handle WE mint is the
+// identity Flutter sees.
+// 4. Every WasResized discards CEF's pool; late frames at the OLD size still
+// arrive — every kOpPresent carries the TRUTHFUL composited dims
+// (srcW/srcH) so the plugin's size-gate (the macOS SendPresentLocked
+// consumer contract, PROTOCOL.md §5) can refuse stale-size frames.
+// 5. Bridge textures are D3D11_RESOURCE_MISC_SHARED (LEGACY handle via
+// IDXGIResource::GetSharedHandle, not NT) — what ANGLE/Flutter accepts.
+// 6. (Plugin-side; supported here by keeping the retired bridge alive until
+// the present announcing its replacement has been written to the pipe.)
+//
+// Args (per-PROCESS / per-profile, mirroring main.mm:32-38):
+// --ipc= the plugin's already-created named pipe
+// --profile-dir= -> settings.root_cache_path
+// --ephemeral marks the profile dir throwaway
+// --allowed-schemes= optional navigation scheme allowlist
+// (empty/omitted = allow all; main.mm:2727-2737)
+
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include