Uno Platform (C# / .NET)
A re-implementation of the WinUI 3 / Windows App SDK API surface over Skia, with a family of native "Skia desktop" hosts — genuine, hand-written X11, Linux DRM/framebuffer, Win32, and AppKit backends — that each own window creation, the event loop, and input, then hand pixels to a shared Skia compositor.
| Field | Value |
|---|---|
| Version/commit studied | df5d18a850248cb8c2ccb34032b4ebeb54dc8283 (6.7-dev, June 2026) |
| Language | C# (.NET 8/9); macOS host has an Objective-C companion library (UnoNativeMac) |
| License | Apache-2.0 |
| Repository | unoplatform/uno |
| Documentation | Using the Skia Desktop |
| Category | WinUI-over-Skia framework (cross-platform application framework, not a thin windowing library) |
| Platforms covered | X11 (Linux), Linux framebuffer/DRM, Win32 (Windows), macOS/AppKit; Android & iOS via separate Skia heads; Wayland only via XWayland (no native client) |
| Loop ownership | Hybrid, per-platform: X11 & Win32 — Uno owns the loop; macOS — AppKit (NSApplicationMain) owns it |
| Repo paths (platform) | src/Uno.UI.Runtime.Skia.X11/, src/Uno.UI.Runtime.Skia.Linux.FrameBuffer/, src/Uno.UI.Runtime.Skia.Win32/, src/Uno.UI.Runtime.Skia.MacOS/ |
Overview
What it solves
Uno Platform lets a single C#/XAML codebase that targets Microsoft's WinUI API run on Linux, Windows, macOS, the browser (WebAssembly), Android, and iOS. For the desktop, the interesting artifact is the Skia desktop host family introduced in Uno 5.2 (April–May 2024), which replaced the old GTK+3-based Linux/macOS heads with backends that talk to each platform's window system directly. The official docs state the scope plainly:
The currently supported targets and platforms are: Linux X11, Linux Framebuffer, Windows (Using Win32 shell), macOS (Using an AppKit shell).
What makes Uno unusual in this survey is that the X11 host is a from-scratch X11 client written entirely in managed C# — XOpenDisplay, XCreateWindow, XI2, XRandR, ICCCM/EWMH, XDND, and even an IBus/Fcitx IME client are all P/Invoked from Uno.UI.Runtime.Skia.X11. The Win32 host is likewise a managed Win32 client (via CsWin32-generated P/Invoke), and the macOS host is a thin Objective-C library (UnoNativeMac) driven by managed callbacks. There is no SDL, GLFW, or GTK underneath the desktop hosts.
NOTE
This survey is scoped to the windowing layer (window lifecycle, event loop, input, presentation hand-off). Uno's WinUI control library, XAML layout, and the Skia render pipeline are out of scope except where they constrain windowing. For layout see the companion ui-layout catalog; for event-loop/async-runtime overlap see async-io.
Design philosophy
- One WinUI API, many heads. Each host implements the same internal contracts —
IXamlRootHost,INativeWindowWrapper(NativeWindowWrapperBase),INativeOverlappedPresenter,IUnoKeyboardInputSource,IUnoCorePointerInputSource,IDisplayInformationExtension,IClipboardExtension,IDragDropExtension— and registers them throughApiExtensibility.Registerin the host's static constructor (e.g.X11ApplicationHost'sstaticctor,src/Uno.UI.Runtime.Skia.X11/Hosting/X11ApplicationHost.cs:44). The WinUI-facing code is platform-agnostic; the host supplies the window-system glue. - Managed where possible, native where required. X11 and Win32 are reachable purely through P/Invoke, so those hosts are pure C#. AppKit's object model (
NSWindowsubclassing,NSTextInputClient,sendEvent:overrides) cannot be expressed from managed code, so macOS ships a small Objective-C shim and calls into it. - Skia is the single rasterizer; the host only owns the surface. Every host creates a window, picks a rendering surface (Vulkan → OpenGL/GLX/EGL → software, or Metal on macOS), and lets the shared
Uno.UI.Runtime.Skiacompositor draw into it. The host's job is window + loop + input, not drawing. - Degrade, don't crash. The X11 host's renderer selection tries Vulkan, then GLX, then EGL/GLES, then software, catching exceptions at each step (
X11XamlRootHost.Initialize,src/Uno.UI.Runtime.Skia.X11/Hosting/X11XamlRootHost.cs:383-460); macOS falls back from Metal to software.
How it works
Each Window maps to one host object that implements IXamlRootHost. The lifecycle differs sharply by platform because loop ownership differs.
The three loop-ownership models
SkiaHost (shared base) exposes two overridable hooks, Initialize() and RunLoop(). Each host fills them differently:
- X11 spins up a managed
EventLoopthread (the WinUI dispatcher thread), plus a dedicated X11-event thread per window, plus a render thread; the "main" thread is just a keep-alive sleeper.X11ApplicationHost.RunLoop(src/Uno.UI.Runtime.Skia.X11/Hosting/X11ApplicationHost.cs:210):
// src/Uno.UI.Runtime.Skia.X11/Hosting/X11ApplicationHost.cs
protected override Task RunLoop()
{
Thread.CurrentThread.Name = "Main Thread (keep-alive)";
_eventLoop.Schedule(StartApp);
while (!ShouldExit())
{
Thread.Sleep(100);
}
return Task.CompletedTask;
}- Win32 runs a classic message pump on the calling thread, draining the queue with
GetMessage/PeekMessageuntil all windows close (Win32Host.RunLoop,src/Uno.UI.Runtime.Skia.Win32/Hosting/Win32Host.cs:150). - macOS hands the loop to AppKit and never returns until quit (
MacSkiaHost.RunLoop,src/Uno.UI.Runtime.Skia.MacOS/Hosting/MacSkiaHost.cs:74):
// src/Uno.UI.Runtime.Skia.MacOS/Hosting/MacSkiaHost.cs
protected override unsafe Task RunLoop()
{
NativeUno.uno_set_application_start_callback(&StartApp);
// `argc` and `argv` parameters are ignored by macOS
_ = NativeMac.NSApplicationMain(argc: 0, argv: nint.Zero);
return Task.CompletedTask;
}The dispatcher bridge
WinUI's CoreDispatcher is redirected per host so RunAsync/Dispatch lands on the right thread:
- X11:
CoreDispatcher.DispatchOverride = (a, p) => _eventLoop.Schedule(a)— a managed work queue on the "Uno Event Loop" thread. - Win32:
CoreDispatcher.DispatchOverride = Win32EventLoop.Schedule—PostMessageto a hiddenHWND_MESSAGEwindow. - macOS:
CoreDispatcher.DispatchOverride = MacOSDispatcher.DispatchNativeSingle—dispatch_async_fonto the GCD main queue.
Input arrives on the host's native event thread, but is marshalled to the dispatcher thread before touching the visual tree. On X11 every handler does X11XamlRootHost.QueueAction(this, …), which is host.RootElement?.Dispatcher.RunAsync(CoreDispatcherPriority.High, …) (src/Uno.UI.Runtime.Skia.X11/Hosting/X11XamlRootHost.x11events.cs:315).
1. Window creation & lifecycle
| Platform | Create call | Wrapper / host |
|---|---|---|
| X11 | XOpenDisplay + two XCreateWindow (root anchor + top render window) | X11WindowWrapper / X11XamlRootHost |
| Win32 | CreateWindowEx(WS_OVERLAPPEDWINDOW) | Win32WindowWrapper (is itself the host) |
| macOS | [[UNOWindow alloc] initWithContentRect:styleMask:…] (NSWindow subclass) | MacOSWindowHost + native UNOWindow |
| Framebuffer | DRM/KMS or /dev/fb0 — no window manager at all | FramebufferHost |
X11's two-window design is the most distinctive. X11XamlRootHost.Initialize creates a software-only root window that "does nothing but act as an anchor for children" plus a top window (on a second XOpenDisplay connection) that owns the GL/Vulkan context and receives input (src/Uno.UI.Runtime.Skia.X11/Hosting/X11XamlRootHost.cs:378-460). CreateGLXWindow matches a 32-bit XVisualInfo, creates a GLX context, and calls XCreateWindow with a CWColormap to dodge a GLX error on some drivers (issue #21285, cited inline at :534). The window is mapped only on ShowCore via XMapWindow on both windows.
Window-attributes model. The cross-platform surface is WinUI's OverlappedPresenter / FullScreenPresenter, mapped to native hints per host:
| Attribute | X11 | Win32 | macOS |
|---|---|---|---|
| Title | XStoreName/XFetchName | SetWindowText/GetWindowText | NSWindow.title |
| Size / position | XResizeWindow / XMoveWindow | SetWindowPos | setFrame: / setFrameOrigin: |
| Min/max size | XSetWMNormalHints (PMinSize/PMaxSize) | WM_GETMINMAXINFO (ptMinTrackSize) | window.minSize / window.maxSize |
| Decorations / titlebar | Motif _MOTIF_WM_HINTS (SetMotifWMDecorations) | WM_NCCALCSIZE strips non-client area | toggle NSWindowStyleMaskTitled |
| Resizable / min / max | Motif _MOTIF_WM_FUNCTIONS | grey-out via WM_GETMINMAXINFO | toggle style-mask bits |
| Always-on-top | _NET_WM_STATE_ABOVE (EWMH) | (SetWindowPos HWND_TOPMOST path) | NSWindow.level = NSStatusWindowLevel |
| Fullscreen | _NET_WM_STATE_FULLSCREEN | SW_MAXIMIZE + strip WS_DLGFRAME | toggleFullScreen: |
| Transparency / backdrop | (limited) | DWM dark-mode attr | NSVisualEffectView (Mica/Acrylic mapping) |
The X11 overlapped presenter is candid that decoration control is non-standard:
What works is using the Motif WM hints, which aren't standardized or documented anywhere
—
src/Uno.UI.Runtime.Skia.X11/UI/Windowing/X11NativeOverlappedPresenter.cs:17
It also notes that SetIsResizable "doesn't prevent resizing using xlib calls (e.g. XResizeWindow)" (:20), and that SetIsModal is a stub // TODO: modal windows (:23-26).
Initial-frame handling is the no-buffer-no-window story inverted: X11 and Win32 map immediately and the renderer fills the first frame (Win32 forces a synchronous render in WM_ERASEBKGND to avoid a white flash, Win32WindowWrapper.cs:306-323). macOS creates the window hidden (makeKeyWindow then orderOut:) and shows it on activate (UNOWindow.m:320-324). There is no native Wayland client, so the Wayland-specific deferred-first-buffer dance never appears.
Surface/handle exposure. NativeWindow returns a platform handle wrapper: X11NativeWindow(window), Win32NativeWindow(hwnd), or the NSWindow*. macOS additionally exposes Metal handles for Skia via uno_window_get_metal_handles(window, &device, &queue) (UNOWindow.m:986).
Destruction ordering is treated as a hazard. X11 stops the render thread before destroying the window — "otherwise we might end up in a situation where the render thread is trying to render on a destroyed window" (X11XamlRootHost.cs:334-339) — and SynchronizedShutDown flushes both X connections ten times and waits via SpinWait before XDestroyWindow, commented "This is extremely extremely delicate" (:640-675).
2. Event loop
Loop ownership is the spine of the design and is deliberately different per platform because the native constraints differ.
X11 — Uno owns it, with a thread per window connection. Each window starts two background threads (Run(RootX11Window) and Run(TopX11Window), X11XamlRootHost.x11events.cs:36-48). Each multiplexes its X connection fd with poll(2) on a 1-second timeout, then drains events under an XLock:
// src/Uno.UI.Runtime.Skia.X11/Hosting/X11XamlRootHost.x11events.cs
fds[0].fd = XLib.XConnectionNumber(x11Window.Display);
fds[0].events = X11Helper.POLLIN;
while (true)
{
var ret = X11Helper.poll(fds, 1, 1000); // timeout every second to see if the window is closed
if (Closed.IsCompleted) { SynchronizedShutDown(x11Window); return; }
if (ret == 0) continue; // timeout: re-check closed flag
// ... XPending / XNextEvent loop, then QueueAction(...) to the dispatcher
}This is a readiness-style fd poll, not a completion model. User-event injection from other threads is the EventLoop.Schedule queue (dispatcher thread); cross-thread wakeups to the X thread are unnecessary because that thread polls on a timeout.
Win32 — the standard message pump, plus a hidden dispatcher window. Win32EventLoop.RunOnce prioritizes input messages, then blocks on GetMessage (src/Uno.UI.Runtime.Skia.Win32/Native/Win32EventLoop.cs:110):
// src/Uno.UI.Runtime.Skia.Win32/Native/Win32EventLoop.cs
if (PInvoke.PeekMessage(out var msg, HWND.Null, 0, 0, PM_REMOVE | PM_QS_INPUT)
|| PInvoke.GetMessage(out msg, HWND.Null, 0, 0).Value != -1)
{
PInvoke.TranslateMessage(msg);
PInvoke.DispatchMessage(msg);
}Cross-thread dispatch posts a privately RegisterWindowMessage-d message (UnoWin32DispatcherMsg) to a hidden HWND_MESSAGE window whose WndProc dequeues and invokes the action (:71-101). The standard Win32 caveat applies: a modal resize/move loop (DefWindowProc's internal loop for WM_SYSCOMMAND/SC_SIZE) blocks Uno's own pump; Uno mitigates the resulting blank frames by rendering synchronously on WM_MOVE/WM_SIZE (Win32WindowWrapper.cs:275-291).
macOS — AppKit owns it. NSApplicationMain runs the CFRunLoop; Uno only supplies callbacks. Window/screen/input notifications come through AppKit delegate methods on UNOWindow/UNOApplicationDelegate, then call managed function pointers (uno_set_window_events_callbacks, UNOWindow.m:895).
Frame pacing & vsync. Uno uses a shared, self-correcting software pacer rather than a hard vsync lock. FramePacer (src/Uno.UI.Runtime.Skia/Hosting/FramePacer.cs) tracks absolute target timestamps so "overshoot from one frame is absorbed by shortening the next wait." On X11 the render thread waits on an AutoResetEvent that the pacer sets; the target FPS is updated to the monitor's actual refresh rate read from XRandR (mode_refresh in X11DisplayInformationExtension.cs:458-500, fed to UpdateRenderTimerFps). On macOS the MTKView with enableSetNeedsDisplay = YES lets MetalKit drive drawInMTKView: (UNOMetalViewDelegate.m:36), so AppKit/Core Animation does the pacing. Redraw coalescing is via IXamlRootHost.InvalidateRender() → _framePacer.RequestFrame() (X11) / view.needsDisplay = true (macOS).
3. Input
Keyboard
All hosts converge on WinUI's VirtualKey + a separate text/character channel — i.e. a scancode → virtual-key split.
- X11 uses core-protocol
KeyPress/KeyRelease(XI2 is used only for pointer events; see theEventsHandledByXI2Maskcomment,X11XamlRootHost.cs:47).ProcessKeyboardEventcallsXLookupStringto get both a keysym and the UTF-8 text, maps the keysym to aVirtualKeyviaX11KeyTransform.VirtualKeyFromKeySym, and carries the X11 keycode as the scancode (src/Uno.UI.Runtime.Skia.X11/Devices/Input/X11KeyboardInputSource.cs:107-200). Layout/keysym translation is owned by Xlib (XLookupStringhonours the server's layout); Uno does not embed xkbcommon. - Win32 reads the
VirtualKeystraight fromwParamand peeks the queue for a followingWM_CHARto separate the character from the key, deliberately documenting the implementation-detail reliance (Win32WindowWrapper.Keyboard.cs:21-49). Scancode is(lParam >> 16) & 0xFF. - macOS maps the AppKit
keyCode(a positional scancode) through a big hand-writtenget_virtual_keyswitch and obtains the character viaCGEventKeyboardGetUnicodeString(UNOWindow.m:703-872). Modifier keys are synthesized fromNSEventTypeFlagsChangeddeltas (processModifiers:,:1393).
Key repeat is server/OS-driven on all three (X server autorepeat, Win32 repeat in lParam, AppKit repeat) — none of these is the Wayland model where the client must run its own repeat timer, because Uno has no Wayland client.
IME / text input (studied closely)
This is where the hosts diverge most, and each picks the platform-idiomatic protocol:
- X11 → D-Bus IBus/Fcitx, not XIM.
X11InputMethodDetector.DetectAsyncprobesUNO_IM_MODULE/GTK_IM_MODULE/QT_IM_MODULE/XMODIFIERS, then checks the session bus fororg.freedesktop.portal.IBus/org.freedesktop.portal.Fcitx/org.fcitx.Fcitxowners and builds a D-Bus IME client (src/Uno.UI.Runtime.Skia.X11/IME/X11InputMethodDetector.cs). Key events are forwarded to the IME viaHandleKeyEventAsync(with a 100 ms timeout so a slow daemon can't stall input), and if the IME consumes the key Uno does not dispatchKeyDown/KeyUp(X11KeyboardInputSource.cs:113-156). Pre-edit text flows back throughOnDBusImePreeditChanged→OnPreeditChanged. Using the modern D-Bus IME portals instead of legacy XIM is a notable, deliberate choice for a new X11 client. - Win32 → IMM32 (legacy), not TSF.
Win32ImeTextBoxExtensionis documented as "Win32 IMM32-based" and handlesWM_IME_STARTCOMPOSITION/WM_IME_COMPOSITION/WM_IME_ENDCOMPOSITION(src/Uno.UI.Runtime.Skia.Win32/UI/Xaml/Controls/TextBox/Win32ImeTextBoxExtension.cs:13-17). The committed string is read withGCS_RESULTSTR; candidate-window positioning usesImmSetCompositionWindow+ImmSetCandidateWindow(Win32ImeCaretManager.cs:96-109). - macOS →
NSTextInputClient. The rendering view (UNOMetalFlippedView) implements the protocol —setMarkedText:/insertText:/unmarkText/markedRange. Crucially it overridesinputContextbecause "MTKView(a rendering view) may not provide one by default, which would causeinterpretKeyEvents:to bypass the input method entirely" (UNOWindow.m:120-131). Candidate-window positioning isfirstRectForCharacterRange:actualRange:, which queries the managed caret rect and converts it to screen coordinates (:244-256). Key events are routed throughNSTextInputContext handleEvent:while IME is active; if the IME doesn't consume the key, processing falls through to normal key handling (UNOWindow.m:1230-1264).
Pointer
- X11 uses XI2 (≥ 2.2) for all pointer/touch/scroll and core protocol for the rest. High-resolution scroll is handled by reading
XIScrollClassInfovaluators, diffing the absolute scroll "position" against the previous value (touchpads report position, not delta), and dividing by the scrollIncrement(X11PointerInputSource.XInput.cs:221-253). Emulated core button-4/5/6/7 scroll events are ignored via theXIPointerEmulatedflag (:557-565). Touchpad-vs-mouse is detected by probing libinput/synaptics device properties (IsTouchpad,:630-728). Cursor is set withXCreateFontCursorfrom the X cursor font/theme (X11PointerInputSource.cs:56-61) — nocursor-shape-v1because there's no Wayland. - Win32 opts into the unified pointer stack:
RegisterTouchWindow+EnableMouseInPointer(true)(Win32WindowWrapper.cs:207-210), then handlesWM_POINTERDOWN/UP/WHEEL/HWHEEL/ENTER/LEAVE/UPDATEandWM_POINTERCAPTURECHANGED(:360-367). This gives mouse, pen, and touch through oneWM_POINTER*path. - macOS routes everything through
UNOWindow.sendEvent:, classifyingNSEventType*into aMouseEventsenum (UNOWindow.m:1174-1391). Momentum/precise scrolling usesevent.hasPreciseScrollingDeltasto scale: "trackpad / magic mouse sends about 10x more events than a normal (PC) mouse … line scroll versus a pixel scroll" — so non-precise deltas are multiplied by 10 (:1362-1373). Pen pressure/tilt come fromNSEvent.tilt/.pressure.
Touch: X11 via XI2 XI_TouchBegin/Update/End; Win32 via WM_POINTER*; macOS via NSEventTypeDirectTouch. High-level gestures are synthesized by Uno's cross-platform gesture recognizer, not the window system.
4. Wayland specifics
IMPORTANT
Uno has no native Wayland client. A full-tree search of the Uno.UI.Runtime.Skia.* hosts at this commit finds exactly one mention of "Wayland", and it is in the framebuffer host detecting that a display server holds the DRM master:
The Linux framebuffer host detected a DRM device, but another process (most likely a running X11 or Wayland display server) currently holds the DRM master.
—
src/Uno.UI.Runtime.Skia.Linux.FrameBuffer/Builder/FramebufferHostBuilder.cs:81
There are zero references to wl_surface, wl_display, xdg_toplevel, xdg_shell, zwp_text_input_v3, xdg-decoration, fractional-scale-v1, viewporter, libdecor, or layer-shell anywhere in the desktop hosts. On Wayland systems Uno runs its X11 host under XWayland, which the maintainers acknowledge in docs and discussions:
- The official docs warn that "When running using X11 Wayland compatibility (e.g. recent Ubuntu releases), DPI scaling cannot be determined in a reliable way" (Using the Skia Desktop).
- A long-running discussion catalogs OpenGL/rendering crashes specifically under Wayland, with the X11/XWayland route as the workaround (discussion #13641).
So every Wayland dimension this survey asks about — server-side vs client-side decorations, libdecor, protocol coverage, frame-callback vsync, compositor-specific workarounds — is not applicable: those concerns are delegated to the XWayland server, which presents a legacy X11 surface to Uno. This is the sharpest contrast in this catalog with toolkits like GTK or winit that ship a real Wayland client. (Recording an absent feature is itself a finding: a managed-code, from-scratch Wayland client would be a genuinely novel piece of work, and Uno has not built one.)
5. DPI & scaling
Uno's coordinate model is WinUI's: layout is in logical (view) pixels, and each host computes a RasterizationScale (= RawPixelsPerViewPixel) that the compositor multiplies into physical pixels. So the API-native unit is logical / scale-factor-based, with physical pixels at the surface boundary.
- X11 computes scale from XRandR 1.3+ when available, otherwise falls back to an
xdpyinfo-style DPI estimate, and always letsXft.dpi(read fresh from the X resource database) or theUNO_DISPLAY_SCALE_OVERRIDEenv var win (X11DisplayInformationExtension.cs:212-249,:454). It picks the CRTC that overlaps the window the most for multi-monitor correctness, and reads the refresh rate from the CRTC mode. When X resources change at runtime it reacts to aPropertyNotifyonRESOURCE_MANAGERand re-publishes DPI (X11XamlRootHost.x11events.cs:157-169). The created-at-wrong-scale problem is handled by re-querying on these events; the Wayland-via-X11 DPI caveat above is the known weak spot. - Win32 is per-monitor-v2 aware: the wrapper calls
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)beforeCreateWindowEx(Win32WindowWrapper.cs:91) and performs theWM_DPICHANGEDdance — on the message it updates display info, thenSetWindowPosto the suggestedRECTthe OS provides (OnWmDpiChanged,:440-449). The constructor even resizes the window after creation ifRasterizationScale != 1to compensate for a startup sizing bug (#20021, cited at:124-128). - macOS uses
NSScreen.backingScaleFactor. The native side fireswindowDidChangeScreenNotification:andapplicationDidChangeScreenParametersNotification:(UNOWindow.m:55-77), reporting raw-pixel screen size and backing scale to managed code;windowWillStartLiveResize:/windowDidEndLiveResize:keeplayer.contentsScalein sync so dragging a window between a Retina and non-Retina display rescales correctly (:1422-1444).
Mixed-DPI multi-monitor migration is therefore handled natively on Win32 (WM_DPICHANGED) and macOS (windowDidChangeScreen), and on X11 by re-evaluating the best-overlap CRTC.
6. Multi-window & popups
SupportsMultipleWindows => true on every desktop host's INativeWindowFactoryExtension (e.g. X11NativeWindowFactoryExtension.cs:16), and each new WinUI Window builds a fresh native window (and, on X11, a fresh root+top pair on new connections). Uno 5.2's release notes list multi-window as a headline feature.
Popups, menus, and tooltips are not native top-level windows. Uno renders flyouts, ComboBox drop-downs, tooltips, and ContextMenu in an in-canvas light-dismiss popup layer inside the same window's visual tree — the cross-platform WinUI behaviour — rather than as xdg_popup grabs or X11 override-redirect windows. The only place the X11 host touches override-redirect is AttachSubWindow, which reparents an embedded native child (e.g. a hosted GTK WebView or media surface) and sets CWOverrideRedirect so the WM doesn't detach it (X11XamlRootHost.cs:622-638). Modal dialogs are likewise drawn in-tree; the native modal hooks are stubs (X11NativeOverlappedPresenter.SetIsModal is a TODO; macOS uno_window_set_modal "is a read-only property so we simply log if we can't change it", UNOWindow.m:654-658).
Parent/child stacking and window groups are thus mostly delegated to WinUI semantics plus per-platform always-on-top/level hints, not to a windowing-system grab model.
7. Threading
The thread model is forced by the platforms and differs per host:
- macOS — main-thread AppKit, as always. All
NSWindowwork happens on the AppKit main thread;MacSkiaHostsets_isDispatcherThread = trueon the thread that callsNSApplicationMain, and cross-thread work isdispatch_async_fto the GCD main queue (MacOSDispatcher.cs:34-42). The classic "AppKit is main-thread-only" constraint is the reason the macOS dispatcher is GCD-based. - X11 — a dedicated managed UI thread, not the X-event thread. The "Uno Event Loop" thread is the dispatcher/UI thread; X events arrive on separate per-window threads and are
QueueAction-ed onto it; rendering happens on yet another thread ("X11RenderThread",AboveNormalpriority). So rendering does happen off the event thread, guarded byXLockand the render-thread-stop-before-destroy ordering.XInitThreadsis called once at startup, "necessary to run on WSL" (X11ApplicationHost.cs:46-48). - Win32 — single-threaded pump + a render timer. The window must be created and pumped on the dispatcher thread; the
FramePacertimer triggers rendering.OleInitializeis called on both the static ctor thread and the dispatcher thread for OLE/clipboard/drag-drop (Win32Host.cs:49,:123).
A recurring rule: windows are created on, and events delivered to, the dispatcher thread; only rendering may run elsewhere (X11) or is driven by the platform (macOS).
8. Clipboard & DnD
- X11 implements the ICCCM selection model by hand in
X11ClipboardExtension(1077 lines). It takesCLIPBOARDownership viaXSetSelectionOwner, answersSelectionRequestforTARGETSandMULTIPLE("To be ICCCM-compliant, we need to support TARGETS and MULTIPLE at the very least",:559), tracks an ownership timestamp to reject stale requests, and reads viaXConvertSelection+SelectionNotify. INCR (chunked large transfers) is implemented on the read side — it watches for theINCRatom and accumulates chunks (:829-890, with a note crediting xsel/xclip and observing "Avalonia doesn't implement it") — but the write side carries an explicit// TODO: implement INCR(:506). DnD is a hand-written XDND drop handler (X11DragDropExtension, deriving from edrosten'sx_clipboardsample), wired throughClientMessageevents forXdndEnter/XdndPosition/XdndLeave/XdndDrop(X11XamlRootHost.x11events.cs:190-197). - Win32 uses OLE:
OleInitializeat startup, aWin32ClipboardExtension, and aWin32DragDropExtensionoverIDropTarget. (Win32 delayed-rendering /WM_RENDERFORMATis the platform's mechanism; Uno's clipboard goes through the OLE data-object path.) - macOS uses
NSPasteboard generalPasteboardfor text/HTML/RTF/URL/file-URL/image MIME types (UNOClipboard.m:11-67) and AppKit drag-and-drop.
MIME negotiation is therefore native to each platform's idiom: X11 atoms + TARGETS, Win32 clipboard formats via OLE, macOS UTI pasteboard types.
9. Escape hatches
When the WinUI abstraction is insufficient, Uno exposes the native handle:
INativeWindowWrapper.NativeWindowreturnsX11NativeWindow(theWindowXID),Win32NativeWindow(theHWND), or theNSWindow*— so apps can P/Invoke directly against the real window.- macOS exposes the Metal
device/queueviauno_window_get_metal_handlesfor custom GPU work, andINativeOpenGLWrapper(X11NativeOpenGLWrapper,Win32NativeOpenGLWrapper,MacOSNativeOpenGLWrapper) for native GL interop. - Native element hosting is the deepest hatch:
INativeElementHostingExtensionreparents a real native child into the Uno window. On X11 this isAttachSubWindow(override-redirect reparent + XI2 input registration viaRegisterInputFromNativeSubwindow,X11XamlRootHost.x11events.cs:347-367); on Win32/macOS the equivalent embeds anHWND/NSView. This is how WebView2/WKWebView and the VLC media surface are embedded. - Win32 routes raw messages through a single
WndProcper window (Win32WindowWrapper.WndProcInner); aDwmDefWindowProcpre-pass andWM_NCHITTESThandling expose custom-titlebar control. There is no public per-message hook, but the handle is reachable so an app can subclass.
These hatches reveal the known leaks: decoration control (Motif hints on X11), modal windows (stubbed everywhere), and Wayland (no native client at all).
10. History, redesigns & known regrets
- 2024 (Uno 5.2): the from-scratch Skia desktop hosts. The headline windowing-layer redesign replaced the GTK+3-based Skia.GTK head with native X11, Linux-framebuffer, Win32, and macOS/AppKit hosts, selectable at runtime via
UnoPlatformHostBuilder(.UseX11().UseLinuxFrameBuffer().UseMacOS().UseWin32()). The stated motivation was startup speed and footprint: removing GTK+3 "removes about 200MB of binaries from Gtk+3 on Windows or macOS" (Uno 5.2 announcement, InfoQ coverage, April–May 2024). This is what makes Uno a managed X11/Win32 client today. - GTK head deprecated. The older
Uno.UI.Runtime.Skia.Gtk/XamlHost.Skia.Wpfheads still exist (src/Uno.UI.Runtime.Skia.Wpf,Uno.UI.XamlHost.Skia.Wpf) for migration, but the Skia desktop hosts are the recommended path; a migration guide exists. - Wayland is the standing gap. Native Wayland support remains unimplemented; users run under XWayland and hit the DPI-detection caveat and OpenGL/rendering issues (#13641). This is the clearest "known regret"-shaped item: a managed Wayland client is the obvious missing head.
- Decoration handling is explicitly non-standard. The code itself documents that resize/min/max control relies on undocumented Motif WM hints (
X11NativeOverlappedPresenter.cs:17) and that several presenter operations are best-effort "hints" each WM interprets differently (:28-31). - Delicate X11 shutdown. The two-connection design forces the "extremely extremely delicate" synchronized-shutdown dance (
X11XamlRootHost.cs:640), and key-input handling neededXSetLocaleModifiers/setlocaleworkarounds ("keyboard input fails without this, not sure why this works but Avalonia and xev make similar calls",X11ApplicationHost.cs:50) and a top-window event-mask fix (#19310, cited atX11XamlRootHost.cs:29). - GLX colormap and DPI bugs. Concrete fixes cited in-source: the GLX colormap workaround (#21285) and the Win32 startup-scale resize (#20021).
Strengths
- A real, auditable managed X11/Win32 client. No SDL/GLFW/GTK dependency on those platforms; the entire window-system integration is readable C# you can step through.
- Platform-idiomatic input/IME per host. D-Bus IBus/Fcitx on X11, IMM32 on Win32,
NSTextInputClienton macOS — each picks the right protocol rather than a lowest-common-denominator shim. - Correct modern DPI on Win32 and macOS. Per-monitor-v2 awareness with the
WM_DPICHANGEDdance;backingScaleFactorwith live-resize rescaling. - Graceful renderer fallback. Vulkan → GLX → EGL/GLES → software on X11; Metal → software on macOS, all exception-guarded.
- Clean separation of concerns.
IXamlRootHost+ the extension registry let one WinUI implementation serve very different windowing backends. - Self-correcting frame pacing that tracks the monitor refresh rate from XRandR.
Weaknesses
- No native Wayland client. Runs via XWayland with a documented DPI-detection caveat and rendering issues; this is the biggest gap versus
winit/GTK/Qt. - Decoration control is undocumented/fragile (Motif hints) and modal windows are stubbed on every host.
- Clipboard INCR write-side is unimplemented (large X11 clipboard writes can fail).
- Heavyweight per-window threading on X11 (two X connections + multiple threads per window) makes the shutdown path "extremely delicate."
- Popups are in-canvas, not native, so they can't escape the window bounds the way
xdg_popup/override-redirect menus can — a constraint inherited from the WinUI model. - macOS requires a native Objective-C shim, so that host is not pure managed code.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| From-scratch managed X11/Win32 clients (drop GTK+3) | ~200 MB smaller, faster start, fewer native deps, full control | Must re-implement ICCCM/EWMH/XI2/XRandR/XDND by hand; Wayland left unbuilt |
| Per-platform loop ownership (Uno on X11/Win32, AppKit on macOS) | Honour each platform's natural model (NSApplicationMain is mandatory) | Three different threading/dispatch implementations to maintain |
| Two X11 windows (root anchor + top render) on two connections | Isolate GL context & input from the anchor; embed native children cleanly | "Extremely delicate" synchronized shutdown; double the X resources/threads |
| D-Bus IBus/Fcitx IME on X11 instead of XIM | Modern, async, matches GTK/Qt env-var conventions | Depends on a running D-Bus IME daemon; 100 ms key-forward timeout band-aid |
| IMM32 (not TSF) for Win32 IME | Simpler, sufficient for TextBox composition + candidate window | Misses TSF features (rich text services, some advanced IMEs) |
Software self-correcting FramePacer (refresh-rate-tracked) | Portable across X11/Win32; absorbs timer jitter; no per-backend vsync plumbing | Not a true vsync lock; relies on accurate refresh-rate detection (unreliable under XWayland/VMs) |
| In-canvas popups/flyouts instead of native sub-windows | Identical WinUI behaviour on every platform | Popups can't extend beyond the window; no compositor grab semantics |
| Skia as the sole rasterizer; host only owns the surface | One render pipeline; hosts stay small | Host can't expose platform-native drawing without an escape hatch |
Verdict: what a new framework should steal / avoid
Steal:
- The
IXamlRootHost+ extension-registry seam — one app/UI layer, swappable windowing backends registered in a host's static ctor — is a clean way to support many window systems without#ifsoup. - Per-platform IME via the platform-idiomatic protocol (D-Bus IBus/Fcitx,
NSTextInputClient, IMM/TSF) rather than a shared shim; theinputContextoverride for a non-text rendering view (MTKView) is a reusable macOS lesson. - Renderer fallback chains with exception guards, and a refresh-rate-tracking software frame pacer for portability.
- Native-element hosting via override-redirect reparenting (X11) as a concrete embedding recipe.
Avoid / be wary of:
- Skipping Wayland. Running everything through XWayland is a legitimate bootstrap, but it leaks (DPI, GL crashes); a serious Linux story needs a native Wayland client eventually.
- Two windows on two X connections unless you truly need the isolation — it multiplies thread count and makes shutdown perilous.
- Undocumented Motif hints for decoration control and stubbed modal windows — plan these in from the start.
- In-canvas-only popups if your app needs menus/tooltips that escape the window.
Open questions I could not resolve (with where the answer likely lives)
- Will a native Wayland (managed) client land, and with what decoration strategy (libdecor vs own CSD)? Likely in future
src/Uno.UI.Runtime.Skia.Wayland/plus the discussion/issue tracker (#13641). - Exact frame-pacing behaviour under a compositor with adaptive sync / under XWayland, where refresh-rate detection is "nondeterministic" (
X11DisplayInformationExtension.cs:456). Answer would come from runtime tracing on real hardware. - Whether clipboard INCR write-side will be implemented (currently
// TODO,X11ClipboardExtension.cs:506) — watch that file / the issue tracker. - Win32 modal resize/move-loop blank-frame behaviour beyond the
WM_MOVE/WM_SIZEsynchronous-render mitigation — likely needs aWM_ENTERSIZEMOVE/timer experiment.
Sources
- unoplatform/uno — main repository; all quoted file paths are at commit
df5d18a. - X11 host:
X11XamlRootHost.cs,X11XamlRootHost.x11events.cs,X11ApplicationHost.cs,X11NativeOverlappedPresenter.cs,X11DisplayInformationExtension.cs,X11KeyboardInputSource.cs,X11PointerInputSource.XInput.cs,X11InputMethodDetector.cs,X11ClipboardExtension.cs. - Win32 host:
Win32Host.cs,Win32EventLoop.cs,Win32WindowWrapper.cs,Win32WindowWrapper.Keyboard.cs,Win32ImeTextBoxExtension.cs. - macOS host:
MacSkiaHost.cs,MacOSDispatcher.cs, nativeUNOApplication.m,UNOWindow.m,UNOMetalViewDelegate.m. - Shared:
FramePacer.cs; framebufferFramebufferHostBuilder.cs. - Official docs & releases: Using the Skia Desktop, Uno 5.2 announcement, InfoQ on Uno 5.2, migration guide.
- Issues/discussions: #13641 (OpenGL/Wayland), #19310, #20021, #21285.
- Cross-references: shared vocabulary in concepts; layout in ui-layout; event-loop/async overlap in async-io.