Skip to content

Design Constraints — What a Windowing Framework Cannot Abstract Away

The synthesis leaf of the OS-API empirical matrix: the hard, measured constraints that survive every abstraction attempt. Each section states one constraint, lays out the per-platform evidence (verification tiers as in the matrix legendA[wine] = under Wine, not Windows; A[ssh] = on mac-bsn over SSH, WindowServer-verified but not on-glass), prescribes what a framework must therefore do, and says where the empirical findings confirm or contradict the framework-level study (comparison, recommendations, and the deep-dive verdicts).

Last reviewed: June 11, 2026


1. Threading: four different contracts, one narrowest portable rule

The thread-affinity contract is not one rule but a ladder — macOS crashes, Win32 queues, X11 shards per-Display, Wayland routes per-queue — and only the strictest rung is portable.

PlatformContract (measured)Findings
macOSA[ssh] — strictest: window off-main = deterministic NSInternalInconsistencyException crash (assert captured verbatim, twice per probe); cross-thread setNeedsDisplay: is a silent no-opappkit
Win32A[wine] — creator-thread queue affinity, not main-thread: main saw 0 of 10 messages for a worker-created HWND; two concurrent pumps legal; SendMessage blocks on the receiver's pump (400.6 ms vs a 400 ms gap) — mutual send defused by nonqueued processing, send-to-parked-thread deadlocks; 100/100 cross-thread BitBlt succeededwin32
X11A — no main-thread rule; affinity is per-Display. The classic no-XInitThreads corruption is extinct on libX11 ≥ 1.8 (ELF-constructor self-arms); two readers on one Display serialize and one starves completelyx11
WaylandA — no thread rule at all; per-thread wl_event_queue + proxy wrapper is the designed routing model. But a mispaired read_events (stolen read intent, reader-count −1) silently wedges the connection foreverwayland

What a framework must therefore do. The narrowest portable contract is: every window is owned by one designated thread, and on macOS that thread must be the main thread. A framework may relax per platform (multi-pump multi-window on Win32, fully free-threaded on Wayland) but the relaxation must be capability-queried, not assumed. Two non-negotiables fall out of the measurements: cross-thread calls must marshal through a dispatcher (the macOS silent no-op means a missing marshal is undetectable, not just crashy), and the Wayland event-read path must be owned by exactly one component — exposing raw prepare_read to users is handing them the forever-wedge.

Versus the framework study. Confirms comparison §Dimension 7 ("GUI = main thread, forced by one platform: AppKit") and winit's MainThreadMarker-plus-with_any_thread shape as exactly right. Sharpens it: the field's uniform main-thread-only rule is stricter than three of the four OSes demand — only Smithay's no-rule stance and winit's per-platform relaxation match the measured contracts.

2. The modal loop: the pumped loop stops being yours

On Win32 and macOS, OS-owned modal loops capture the pump, so any redraw driven solely by "my loop iterated" freezes — Linux has no such loop at all.

PlatformEvidence (measured)Findings
Win32A[wine] — modal loop entered programmatically: no-fix freeze max tick gap 1.01 s; with the SetTimer-on-WM_ENTERSIZEMOVE fix, in-modal ticks at 17 ms. winewayland swallows SC_* entirelywin32
macOSA[ssh] — a default-mode timer starves completely during tracking/modal modes (0 fires across a 2.0 s gap); the same timer scheduled in NSRunLoopCommonModes ticks through nested modes (max gap 109.5 ms)appkit
X11A — no modal loop exists: ticks held ≤ 17.8 ms through 50-resize storms, bare and WM-mediatedx11
WaylandA — modal_enter is unrepresentable; max tick gap 17.06 ms through 12 interactive transitions vs 16.80 ms calmwayland

What a framework must therefore do. The redraw path must have a delivery channel that survives loop capture: a WM_TIMER re-entry on Win32 and common-modes (not default-mode) run-loop sources on macOS. Equivalently: the public frame tick must be specified as "fires even while the OS owns the loop", which rules out implementing it as "after my PollEvents returns".

Versus the framework study. Confirms comparison §Dimension 2's survey-wide SetTimer consensus, now with the freeze quantified (1.01 s → 17 ms). Adds what the study only implied: the macOS fix is a different mechanism (run-loop mode placement, the Flutter custom-mode trick) — one "modal loop workaround" abstraction won't compile to both platforms.

3. Decorations: a Wayland-only product decision with a measured price

Only Wayland makes the client responsible for the frame, the responsibility can arrive uninvited (weston offers no negotiation at all), and getting the geometry contract wrong is a connection-fatal protocol error.

PlatformEvidence (measured)Findings
WaylandA — SSD negotiated where offered (sway, tiled and floating); weston has no decoration protocol — the client draws or there is no frame. Hand-rolled minimal CSD cost 1061 LOC vs 295 for the libdecor variant; omitting set_window_geometry (WSI_NO_GEOMETRY=1) = fatal protocol error, connection killed; move/resize grabs require real input serialswayland
X11— N/A by design: the WM reparents and draws the frame (hook notes in the Wayland F13 doc)matrix
Win32— N/A: DWM owns the frame (custom chrome = WM_NCCALCSIZE hooks, out of scope here)matrix
macOS— N/A: NSWindowStyleMask owns the framematrix

What a framework must therefore do. Carry a CSD path unconditionally on Wayland and surface the decoration question as an explicit product decision (SSD-preferred / libdecor / bespoke), never as an internal detail — because the answer differs per compositor at runtime and can flip mid-session (sway honors a runtime set_mode(client_side) when floating, silently ignores it when tiled). The 3.6× LOC ratio is the empirical argument for libdecor as the default tier.

Versus the framework study. Confirms comparison §Fork C ("prefer libdecor over hand-rolled CSD") and SDL3's SSD-first-with-libdecor-fallback verdict, now with the cost measured. Confirms consensus #6 ("SSD is only a hint") in its strongest form: on weston the hint protocol does not even exist. Contradicts nothing — but note GTK 4's KDE-protocol-only binding is exactly the gap our registry probe would expose.

4. Keyboard: ownership of sym/text/repeat/compose moves per platform

No platform delivers the same division of labor between OS and client for key-identity, text, repeat, and compose — the framework must own a unified state machine and per-platform fill in what the OS withholds.

PlatformWho owns what (measured)Findings
WaylandA — protocol ships only evdev codes; the client owns sym, text, repeat (both cancellation paths proven) and compose, via xkbcommon (+8 offset) — verified under sway + wtypewayland
X11A — same xkbcommon state machine via xkbcommon-x11; server owns repeat (detectable-autorepeat is opt-in); live setxkbmap keymap rebuild + client-side compose provenx11
Win32A[wine] — OS owns text and dead-key compose only through TranslateMessage (WM_KEYDOWNWM_CHAR/WM_DEADCHAR chain; skipping it kills text); OS owns repeat. Layout switch under the headless null driver changes the HKL but not the tables — de-DE captures queued for real Windowswin32
macOSA[ssh] — three layers in three places: hardware keyCode, layout engine (UCKeyTranslate over uchr), text layer (interpretKeyEvents:insertText:); OS owns repeat (isARepeat)appkit

What a framework must therefore do. The unified key event needs three independent fields — physical key (scancode/evdev), logical key (layout-resolved sym), and committed text — plus a repeat flag the framework synthesizes on Wayland and translates elsewhere. Compose/dead-key state must live in the framework on Linux and be delegated on Win32/macOS; conflating any two of the three identities is the bug class the framework study catalogs.

Versus the framework study. Confirms comparison Part 3 consensus #1 wholesale (xkbcommon, the +8 offset, client-side Wayland repeat) — our demos are the working proof of each clause. Adds a precision the study lacks: on X11 repeat is the server's, so a framework that re-synthesizes repeat everywhere (for uniformity) must suppress the server's, not stack on top of it.

5. Resize: negotiation on Wayland, notification everywhere else — with fatal enforcement

Wayland resize is a serial-stamped contract the compositor enforces by killing the connection; the other three platforms merely inform you, each on a different clock.

PlatformEvidence (measured)Findings
WaylandA — serial-exact ack_configure before every matching commit proven; --violatefatal invalid_surface_state, connection killed. States arrive as a fresh whole array each configure (F14: v1 sees maximized where v5 sees tiled_*)wayland
X11A — notify-not-negotiate; ≥ 1 stale frame per resize is structural; a WM makes XResizeWindow deniablex11
Win32A[wine] — pure notification: all 14 SetWindowPos sizes granted verbatim; pure shrinks invalidate nothing; WM_SIZING unreachable programmatically. Fullscreen-as-geometry-idiom corrupts the placement normal-rect (F14)win32
macOSA[ssh] — app-decided and fully synchronous: setFrameSize:windowDidResize:drawRect: in one call stack; pixels = points × 2.0 on all 14 events; live-resize never entered programmaticallyappkit

What a framework must therefore do. The frame-lifecycle API must be shaped by the strictest member: a configure-like event the app acknowledges by presenting a buffer of the agreed size. On Wayland that ack is load-bearing protocol (with buffer-size coupling the compositor checks); on X11/Win32/macOS it degrades to "redraw at the new size". An API where resize is a property setter and redraw is independent cannot express the Wayland contract and will ship the connection-killer as a user-reachable state.

Versus the framework study. Confirms comparison §Dimension 1's "model the lifecycle as explicit async state" lesson — and escalates it: the study's evidence was fragility (SDL3's KDE bug tail); ours is a fatal, enforced protocol error, the strongest possible form of the same argument.

6. Frame clocks & visibility: four clocks, four throttle behaviors

There is no portable vsync primitive — the four frame clocks differ in source, in failure mode, and in what happens when the window is not visible — so the framework's frame clock must be its own component with per-platform feeders and a watchdog.

PlatformClock + throttle behavior (measured)Findings
WaylandA — wl_surface.frame: 16.52–16.87 ms over 600 frames (synthetic 60 Hz); minimized ⇒ callbacks stop dead, and there is no client-side un-minimize (F14)wayland
X11A — Present is the only real frame clock (ust/msc); hidden windows never throttle; Xvfb advertises Present 1.2 with a synthetic 60 Hzx11
Win32A[wine] — DwmFlush is a non-blocking S_OK stub under Wine (forced pacing free-runs at p50 522 µs) → the demo's timer fallback carried all 600 frames; no minimized throttlingwin32
macOSA[ssh] — CADisplayLink goes silent under a locked console despite reporting unpaused/visible; the watchdog fell back to a 16 ms NSTimer; real cadence Tier Cappkit

What a framework must therefore do. requestRedraw must be a contract the framework keeps with its own clock, fed by the native vsync source when that source is alive and by a timer watchdog when it is not — because two of the four sources were observed dead-while-claiming-alive (Wine's stub, the locked-console CADisplayLink), one stops by design (Wayland minimized — correct power behavior the framework must surface, not paper over), and one never stops (X11 hidden — the framework must throttle itself or burn CPU).

Versus the framework study. Confirms the comparison delta-table frame-clock row ("one frame-clock abstraction", GdkFrameClock/Slint FrameThrottle as references) and GTK 4's freeze-until-frame-callback verdict. Adds the requirement the study missed: the abstraction needs liveness detection, not just source-folding — no surveyed toolkit documents a watchdog for a lying vsync source.

7. Pointer lock/confine: asymmetric capabilities, asymmetric lifecycles

Pointer lock is async-and-deniable on Wayland, hand-assembled on X11/Win32, and confine simply does not exist publicly on macOS — so capture must be a capability-reported request, not a method that returns void.

PlatformEvidence (measured)Findings
WaylandA — lock granted only on surface entry (async, deniable); oneshot/persistent lifecycles + hint-warp proven; only relative motion flows while locked; deactivation only on focus change (sway)wayland
X11A — lock is assembled from grab+hide+warp (restore exact); confine_to modal but unenforced post-activation on Xvfb; XFixes barriers are ambient and grab-proof; raw events double during the app's own grabsx11
Win32A[wine] — raw WM_INPUT + ClipCursor(1×1) + ShowCursor lock and confine all proven; Wine applies pointer ballistics before the raw stream; focus loss does not clear the clipwin32
macOSA[ssh] — lock = 3 sync calls, no deny path, no read-back getter; warp is silent and fraction-truncating with a 0.25 s event suppression; no public confine — warp-back leaks 1 out-of-bounds event per excursionappkit

What a framework must therefore do. Expose lockPointer() as an async request with grant/deny/revoke events (the Wayland shape, the only one that subsumes the others), and expose confinePointer() behind a capability probe that reports absent on macOS rather than emulating it leakily. Raw-motion delivery must be a separate stream from accelerated motion, with per-platform dedup (the X11 doubling, the Wine pre-ballistics) handled inside.

Versus the framework study. Confirms GTK 4's verdict clause "omitting pointer constraints if any client might be a game" and the comparison capability-probing pattern (TryGetFeature-style) as the right escape shape. Sharpens it: macOS confine is not "unimplemented", it is unimplementable with public API — a framework that promises uniform confine is lying on one platform.

8. IME: a separate stateful channel, gated differently everywhere

Text input is not a key-event refinement but a second, stateful channel whose enable gate, event ordering, and lifetime rules differ per platform — and whose omission or interleaving bugs are unfixable above the windowing layer.

PlatformEvidence (measured)Findings
WaylandA — full CJK round-trip headless (sway + fcitx5-pinyin): zwp_text_input_v3 preedit → commit_string "你好"; serial discipline (one commit per done) proven; weston is v1-onlywayland
X11A — XIM end-to-end incl. headless fcitx5 pinyin commit, destroy/restart callbacks, on-the-spot preedit deltas; the XFilterEvent gate is total — skip it and composition never happensx11
Win32A[wine] — IMM32 choreography fully verified headless (SCS_SETSTR echoes real WM_IME_*); TSF COM bring-up all-S_OK but blocked at ITextStoreACP — the wall every toolkit hitwin32
macOSA[ssh] — full NSTextInputClient (added via runtime class_addProtocol) flips composition on: option-e + e ⇒ setMarkedText:"´"insertText:"é"; Esc commits-then-cancels; focus loss orphans the pre-editappkit

What a framework must therefore do. Model text input as an explicitly enabled, per-window stateful channel (enable/disable, preedit-with-styling, commit, delete-surrounding) that the framework — not the app — wires to the four native contracts, because each has a non-optional structural obligation: the X11 filter gate sits inside the event pump, the Wayland serial discipline sits inside protocol dispatch, the macOS protocol must be on the view receiving keys, and key events must be suppressed/reordered around composition. The focus-loss-orphans-preedit and Esc-ordering findings mean preedit lifetime needs explicit cancel events in the API.

Versus the framework study. Confirms comparison §Dimension 3's two findings — IME is frequently punted, and nobody ships TSF — and our TSF probe explains why empirically: everything up to ITextStoreACP is cheap, the document-model interface is the cliff. Confirms the study's "own the consumer" prescription (winit/SDL3 verdicts) with working evidence that all four consumers are implementable in a few hundred lines each.

9. Popups: compositor-placed vs app-math — the positioner argument

Wayland places popups and dismisses them for you (uncapturably); the other three make the app do all placement math and hold fragile grabs — only a positioner-style declarative API maps onto both.

PlatformEvidence (measured)Findings
WaylandA — positioner solved compositor-side (sway flips, not slides, at edges); outside click is uncapturable — the client only gets popup_done, topmost-first; a stale grab serial is silently granted grab-lesswayland
X11A — popup = override-redirect + session-global grab that starves all other clients; placement and dismissal 100 % app-side; one grab serves the whole submenu chainx11
Win32A[wine] — WS_POPUP + SetCapture; one capture slot + screen-coord hit-testing serves the chain; WM_CAPTURECHANGED is the silent grab-breaker; popups may exceed output boundswin32
macOSA[ssh] — borderless window at level 101; placement/clamping 100 % app math (off-screen frames accepted verbatim); local monitor sees only queued events; Esc arrives via cancelOperation:appkit

What a framework must therefore do. The popup API must be declarative — anchor rect + gravity + constraint-adjustment, i.e. xdg_positioner's vocabulary — with the framework computing geometry on X11/Win32/macOS and delegating on Wayland; and dismissal must be a framework-delivered event (popup_done-shaped), because on Wayland the app cannot observe the outside click and on the others the grab can be broken silently (WM_CAPTURECHANGED, grab steals). "Place this window at (x, y) and watch clicks yourself" is implementable on exactly three of four platforms.

Versus the framework study. Confirms comparison §Dimension 6 ("a popup abstraction cannot be a simple place-at-(x,y) call") and the winit verdict's self-identified gap — winit's punt pushes an obligation onto consumers that our Wayland findings show consumers cannot even discharge (the dismiss event only exists protocol-side). The positioner-style resolution the study inferred from SDL3/Smithay is the one the measurements force.

10. Clipboard & DnD: three transfer models, one shared trap

Clipboard is a negotiated async transfer everywhere, but the ownership model — unified selection machinery (Linux), transactional with delayed rendering (Win32), versioned polling (macOS) — and its coupling to input state cannot be unified into a synchronous getText()/setText().

PlatformEvidence (measured)Findings
WaylandA — clipboard and DnD share one data_source/offer pipe-fd machinery; set_selection is dead without a real input serial — silently ignored; DnD action resolved compositor-sidewayland
X11A — clipboard and XDND are one selection mechanism; INCR chunking proven both directions; full XDND v5 source + target by hand; delayed rendering is the only modex11
Win32A[wine] — delayed rendering timed (WM_RENDERFORMAT only on demand); OLE DnD fully headless (hand-rolled COM to DRAGDROP_S_DROP); host bridging two-way on winex11, absent on winewaylandwin32
macOSA[ssh] — eager + lazy NSPasteboard; changeCount polling is the only change signal; promises die with the source; reading changeCount inside the ownership callback recurses to stack overflowappkit

What a framework must therefore do. The data-transfer API must be: (a) provider-based — the app registers a typed render-on-demand source, never pushes bytes eagerly (delayed rendering is mandatory on X11, the default idiom on Win32/Wayland); (b) async on the read side (pipe-fd / INCR / promises all stream); (c) coupled to input events on WaylandsetClipboard must accept or capture the triggering event's serial, or it silently does nothing; and (d) change-notification must be abstracted as "may be poll-based" (macOS has no push signal). DnD source and target ride the same machinery on Linux and should share the provider type.

Versus the framework study. Confirms comparison §Dimension 8 (async MIME-typed transfer; GTK 4's clipboard as the model; drag-source as the field's weakest corner — our hand-rolled XDND/OLE sources show it is laborious but bounded). Adds the input-serial coupling, which no deep-dive surfaced as an API-shape constraint: it makes "clipboard write" an event-context operation on Wayland, a genuinely new requirement for the API.

11. Loop wakeup: the loop must be composable with external fds

Cross-thread wakeup is cheap everywhere, but the mechanisms are disjoint — a client fd on Wayland (no protocol-level user event exists), ClientMessage or fd on X11, the message queue (with a 63-handle ceiling on the wait) on Win32, run-loop sources on macOS — so loop integration, not wakeup, is the real API.

PlatformEvidence (measured)Findings
WaylandA — no protocol-level user event; wakeup must be a client fd; eventfd median 11 µs via the prepare_read/poll loopwayland
X11A — native user event exists (ClientMessage, p50 71 µs, full server round-trip) but fd-readiness is trivial and 4× faster (eventfd p50 17 µs)x11
Win32A[wine] — PostMessageW/PostThreadMessageW both verified; the "add an arbitrary handle" story is MsgWaitForMultipleObjectsEx with its 63-handle ceiling (probed)win32
macOSA[ssh] — three lanes verified: postEvent:atStart:, a signalled version-0 CFRunLoopSource, and a pipe(2) via CFFileDescriptorappkit

What a framework must therefore do. Offer a wake() (any-thread, coalesced) and an external-source registration that is fd-shaped on Linux and source-shaped on Apple/Win32 — and on Win32 multiplex registered handles itself (the 63-handle ceiling means user handles cannot map 1:1 onto the OS wait).

Versus the framework study. Confirms comparison §Fork A's resolution — the external-fd integration point (winit's calloop/AsFd story) is the load-bearing requirement, and its absence in GLFW/SDL3 the recorded gap. The 11–71 µs measurements show the cost argument is a non-issue; the design argument is everything.

12. Scale is late-bound: the first frame is at the wrong scale by design

On Wayland the first commit is always at scale 1 and the real scale arrives post-map; on Win32 awareness is write-once-per-process; X11 has no live channel at all — so scale must be a revisable event in the API, not a creation-time constant.

PlatformEvidence (measured)Findings
WaylandA — both integer and fractional paths in one binary; live fractional rescale + cross-scale output move on sway; first commit is always at scale 1 — post-map rescale guaranteedwayland
X11A — absence proven live: Xft.dpi 96→144→192 and xrandr --dpi deliver nothing to the window; the only live channel is an opt-in root PropertyNotify on RESOURCE_MANAGER (convention, not protocol)x11
Win32A[wine] — PMv2 surface complete; awareness is write-once (second set ⇒ err=5), thread granularity, virtualization captured; under winewayland WM_DPICHANGED is structurally unreachable — scale arrives as a plain WM_SIZE at DPI 96win32
macOSA[ssh] — all scale sources agree (2.0); convertRectToBacking: exact incl. fractional; wrong-scale buffers fail silently (no error, just blur); headless drawRect: CTM is identity — rasterization Tier Cappkit

What a framework must therefore do. Treat scale as an event stream with a guaranteed initial delivery after the surface maps, and require render-target sizing off the physical/buffer size, never the logical size. Process-level prerequisites (Win32 PMv2 manifest/early SetProcessDpiAwarenessContext) must be handled at init because they are unfixable later (write-once). On X11 the framework must volunteer the RESOURCE_MANAGER listener — no app will.

Versus the framework study. Partially contradicts the study's cleanest prescription: comparison §Dimension 5 holds up Slint's "learn the scale before first paint" as the cure, but on Wayland that is impossible — the first configure cannot carry the real scale and a post-map rescale is guaranteed. Winit's surface_size_writer (make the post-hoc rescale first-class and cheap) matches the measured reality better than learn-it-early does.

13. Output topology: enumeration is portable, change notification is not

Every platform enumerates monitors, but who answers "which output am I on" and whether change arrives by event or by polling differs — and on one verified stack the notification simply never fires.

PlatformEvidence (measured)Findings
WaylandA — wl_output v4 + xdg-output logical/physical split; live hot-add/remove + vanishing-occupied-output captured; occupancy (surface.enter/leave) trails configures by ~200 mswayland
X11A — RandR 1.5 monitors vs 1.2 wiring objects disagree on physical mm; occupancy is app-derived; off-screen windows learn nothingx11
Win32A[wine] — live hotplug captured (~1 s to \\.\DISPLAY2) but Wine fires none of the three notify messages — only polling caught it; rcMonitor all at +0+0 under winewaylandwin32
macOSA[ssh] — dual enumeration (NSScreen + CG) bridged via NSScreenNumber; window.screen answers occupancy directly; a locked session empties only the CG active list; CGDisplayPixelsWide returns pointsappkit

What a framework must therefore do. Maintain its own output model updated by events where they exist and by a low-frequency poll where they provably don't (A[wine] caveat: the missing notifications are a Wine gap — real-Windows behavior queued); report window-to-output occupancy as eventually-consistent (the 200 ms Wayland lag, the X11 derive-it-yourself); and never expose two contradictory physical-size answers (pick the RandR 1.5 / logical view and say so).

Versus the framework study. The study barely touches output topology beyond DPI migration — this constraint is empirical surplus. It reinforces the comparison capability-probe ethos: "monitor change events" is itself a capability that one verified stack lacks.


Constraint summary

#ConstraintBinding platform (why)Framework consequence
1Thread-affinity laddermacOS (off-main = crash; cross-thread call = silent no-op)One owner thread per window, main on macOS; dispatcher marshalling; relax per-platform via capability
2OS-owned modal loops capture the pumpWin32 (1.01 s freeze) + macOS (default-mode starvation)Frame tick must survive loop capture (SetTimer / common-modes sources), never "after my poll returns"
3Client may own the frame; geometry contract is fatalWayland (weston: no SSD protocol; geometry violation = disconnect)Unconditional CSD path, libdecor default (295 vs 1061 LOC), decoration mode surfaced as product decision
4Key sym/text/repeat/compose ownership movesWayland (client owns all) vs Win32/macOS (OS owns text+repeat)Three-identity key event; framework-synthesized repeat on Wayland, suppressed-server-repeat on X11
5Resize is a negotiation with fatal enforcementWayland (ack_configure serial-exact or connection killed)Configure→ack-by-presenting frame lifecycle; size and buffer coupled in the API
6Four frame clocks, four throttle behaviorsAll four differently (stops dead / never stops / stub / silent)Own frame-clock component: native feeder + liveness watchdog + self-throttle
7Pointer lock/confine asymmetrymacOS (no public confine) + Wayland (async-deniable lock)lockPointer as async request with grant/deny/revoke; confine behind a capability probe
8IME is a separate stateful channelAll four (filter gate / serials / ITextStoreACP wall / view protocol)Framework owns all four IME consumers; explicit preedit lifetime incl. cancel
9Popup placement/dismissal ownership splitsWayland (compositor places; outside click uncapturable)Declarative positioner API + framework-delivered dismiss event; never "place at (x,y)"
10Three clipboard models, input-serial couplingWayland (set_selection silently dead without serial) + macOS (poll-only)Provider-based render-on-demand transfer; event-context writes; poll-tolerant change signal
11Wakeup ≠ loop integrationWayland (no protocol user event) + Win32 (63-handle ceiling)Any-thread coalesced wake() + external-source registration; framework multiplexes Win32 handles
12Scale is late-boundWayland (first commit always scale 1) + Win32 (write-once PMv2)Scale as revisable event with guaranteed post-map delivery; size targets off physical size
13Output change notification is not portableWin32/Wine (zero notify messages fired — polling only)Framework-owned output model; events where real, poll fallback; occupancy eventually-consistent

The contradiction worth flagging upward: the framework study's "learn the scale before first paint" (comparison §Dimension 5, via Slint) is unimplementable on Wayland as measured (constraint 12) — the prescription should be winit's shape instead: make the guaranteed post-map rescale first-class and cheap. Everything else in comparison Part 3/4 and the recommendations plan is confirmed, mostly with sharper teeth (fatal enforcement, measured freezes, LOC prices) than the study could claim from source-reading alone.