10 KiB
UIXrender.dll — rendering & message pump log
Append-only. Do not edit previous entries. Newest first.
2026-07-22 — Landed: real StbTrueType text rendering + backend-agnostic message pump
Built, AOT-published, and tested. Summary of what's now real vs. before.
Text rendering (was: ratio-approximated measurement + E_NOTIMPL rasterization):
Subsystems/Text/LoadedFont.cs— a TrueType face over StbTrueTypeSharp (pure managed, zero GPU). Pins the font bytes (stb stores the data pointer) and exposes scaled advance/kerning/ascent/line-height and glyph coverage rasterization.FontStore.cs— resolves a face name to a font: runtime-registered fonts (SpLoadFontResource, now real — reads the embedded resource and registers it) → platform system-font discovery (Windows/Linux/macOS dirs) → first-available fallback. Cached, thread-safe. Returns null when nothing resolves, so callers degrade to the ratio metrics rather than faking.TextLayout.cs— real measurement over actual glyph advances + kerning, with the old ratioTextMetricskept strictly as the no-font fallback. Word wrap is real either way.GlyphRun.cs— the objecthGlyphRunInfopoints at;Rasterize(color)composites stb coverage bitmaps into a straight-alpha ARGB32 buffer (BGRA-in-memory, matchingSurfaceFormat.ARGB32).TextBitmapowns that buffer;SpFreeDib(repointed toHandleTable.Free) releases it.- Wired:
SpSimpleTextMeasurenow produces a real glyph run + real geometry;SpRichTextRasterize(wasE_NOTIMPL) composites it to a bitmap;SpRichTextMeasure/SpRichTextGetNaturalBounds/SpSimpleTextMeasurePossibleuse the real font. Outline/shadow passes and rich-text multi-runrrcbremain TODO.
Message pump (was: PeekMessage→0, WaitMessage→Thread.Sleep, Invoke(null)→no-op):
Subsystems/Os/MessagePump.cs— aConcurrentQueue<Action>+ManualResetEventSlim, with a lost-wakeup guard (reset only when empty, re-check after). No windowing/GPU dependency.IWindowMessageSourceis the pluggable seam for a host/optional backend to inject OS messages during Peek.- Wired through
EngineService:PeekMessage→ drain+run queued work, report ProcessedMessage(1)/None(0);WaitMessage→ real bounded wait that returns early on a post;Invoke(null ptr)→PostWake(the InterThreadWake fix — a blockedWaitMessagenow actually wakes); asyncInvoke(real ptr)→ deferred onto the pump thread;SetWindowMessageSourceexposes the seam.SpPostDeferredImeMessagenow posts its dispatch onto the pump (genuinely deferred).SpCreateNotifyWindownow returnsS_OK+ a token (wasE_NOTIMPL, which threw insideUIForm.Initialize'sIFC).
Verified: dotnet build UIXrender -f net8.0 clean; whole solution builds except the
pre-existing SimpleDebugClient/SimpleIrisApp errors; NativeAOT linux-x64 publish
succeeds with zero trim/AOT warnings (StbTrueTypeSharp is pure-managed, AOT-safe) and
exports exactly 193/193 (surface unchanged). Tests/UIXrender.Engine.Tests extended
to 66 checks, all passing, exercising real behaviour: proportional metrics ('WWW' >
'iii', which the ratio path could never produce), word-wrap growth, rasterized ink
(non-zero alpha pixels — glyphs actually drawn), deferred-work execution on Peek,
InterThreadWake unblocking a blocked WaitMessage in ~50ms (not the 5s timeout),
WaitMessage honouring its timeout, and PeekMessage reporting the right WorkResult.
Still open / not done (flagged, not hidden):
- No on-screen compositing / real window. By design (backend-agnostic): text
measurement + CPU rasterization is complete, but presenting pixels to a screen needs a
window+GPU backend, which stays out of core.
IWindowMessageSourceis the seam for a later optional backend package.SpPeekMessagetherefore never returnsNewUserMessage(2)(no HWND to Win32-dispatch). - Text: DPI scaling (fontHeightPts treated as pixels for now), complex-script shaping (stb is Latin-oriented), and outline/shadow/underline rendering passes.
- Font family matching is filename-heuristic, not the TTF
nametable / fontconfig — a bold/italic style request currently resolves the family's plain face.
2026-07-22 — Course-correction: no GLFW/SDL; backend-agnostic pump + pluggable window seam
What happened: I asked the user (AskUserQuestion) how to back the message pump and they picked "Silk.NET.Windowing real window". While setting that up the user pushed back: "why are you adding a dependency on GLFW? That's not backend-agnostic." Correct.
The constraint, stated plainly: Silk.NET.Windowing is an abstraction over IWindow,
but there is no backend-agnostic way to create a real OS window — Window.Create()
needs a concrete platform provider (GLFW or SDL) registered via a backend package at
runtime, or it throws "no platform registered". So "real window" unavoidably pulls in
GLFW or SDL, which violates the backend-agnostic requirement that has governed this whole
effort (and matches FullSurface.md decision 2: UIXrender is loaded into a host that
already owns its window). Nothing windowing-related had been added to the real project
yet — only a throwaway scratchpad probe — so there was nothing to revert.
Revised design (what's actually being built):
- Message pump = a backend-agnostic message queue owned by the render-session thread.
This is exactly and only what the render engine's loop uses the pump for, verified from
the call sites (
UIX.RenderApi/.../Internal/RenderEngine.cs):IRenderEngine.WaitForWork→SpWaitMessage(timeout)(block until work or timeout);IRenderEngine.ProcessNativeEvents→SpPeekMessage(drain)(drain + report a WorkResult);IRenderEngine.InterThreadWake→SpInvoke(ctx, NULL, NULL, false)(wake a blocked wait from another thread). AConcurrentQueue+ a wait handle implements all three correctly with no windowing/GPU dependency at all. ProcessNativeEvents'sNewUserMessage(2) path is deliberately never taken. That branch forwards a Win32MSGtoRenderWindow.ForwardWindowMessagethen callsWin32Api.TranslateMessage/DispatchMessage— genuine Win32/HWND dispatch that only exists on Windows with a real window. The backend-agnostic pump has no HWND, soSpPeekMessagereturnsProcessedMessage(1) when it ran queued work, else0(none), and leavesout msgdefault — which routesProcessNativeEventsdown its non-Win32 branch (return TestFlag(nResult, 1)), exactly right for a windowless pump.- Pluggable window seam, no dependency: a small
IWindowMessageSourceinterface plusEngineService.SetWindowMessageSource(...)lets a host that owns a window (or an optional, separate Silk.NET.Windowing backend package that never ships in UIXrender core) inject OS window/input messages into the pump later. The pump harvests from the source during Peek if one is registered. This preserves the "windowing can plug in" intent of the user's original menu choice while keeping UIXrender core free of GLFW/SDL.
Deferred-invoke semantics fixed at the same time: SpInvoke with a null function
pointer is InterThreadWake → post a wake to the pump (this is the fix the user's "must
implement a pump" requirement was about: a blocked SpWaitMessage now actually wakes). A
non-null async SpInvoke enqueues the callback to run on the pump thread during the
next Peek (real "deferred invoke to the render thread" semantics), not on a random
threadpool thread as the interim wiring did; a non-null sync SpInvoke runs inline.
2026-07-22 — Plan: text rendering (StbTrueTypeSharp) + message pump
Task (user): implement rendering, specifically text rendering, and — mandatory — a
message pump so SpPeekMessage/SpWaitMessage work correctly. Backend-agnostic; pause
and ask if no suitable abstraction exists (which is why the font backend was an
AskUserQuestion — Silk.NET has no font/text abstraction, and the community option
SilkyNvg was rejected for being OpenGL-only).
Font backend chosen (user): StbTrueTypeSharp — pure-managed stb_truetype port,
zero-GPU, NativeAOT-clean, pairs with the StbImageSharp decoder already referenced. Gives
glyph metrics (real measurement) and CPU glyph rasterization (real SpRichTextRasterize).
No complex-script shaping — line/word layout is built here (the ratio-based TextMetrics
already did the layout; only the per-glyph width becomes real).
Text subsystem shape (Subsystems/Rendering/Text/):
FontStore— resolves a TextStyle font-face name to a loaded font: registered fonts (viaSpLoadFontResource), then platform system-font discovery (WindowsFonts, Linux/usr/share/fonts& friends, macOS), then a first-available fallback. Cached, thread-safe. When nothing resolves, callers fall back to the existing ratio metrics and rasterization reportsE_NOTIMPL— honest degradation, not a fake.LoadedFont— wrapsstbtt_fontinfo+ v-metrics; scaled advance/kerning/ascent/line height, and glyph coverage-bitmap rasterization.TextLayout— real measurement (word wrap over real advances) and a measured run.GlyphRun— a measured run stored behind aHandleTablehandle (this becomes thehGlyphRunInfothe managed side round-trips from measure to rasterize).
Rasterize lifecycle (read off the managed consumers, not guessed):
SpRichTextRasterize returns phTextBitmap + ppvBits + size, which
Microsoft.Iris.Drawing.RichText.Rasterize wraps in a Dib
(UIX/.../RenderAPI/Drawing/Dib.cs) that frees phTextBitmap via SpFreeDib. So the
rasterizer allocates one unmanaged ARGB buffer, returns a HandleTable handle as
phTextBitmap and the buffer pointer as ppvBits, and SpFreeDib is repointed to
HandleTable.Free (its previous body referenced a since-removed GDI helper and only ever
mattered on Windows; the only producer of "DIBs" in this reimplementation is this
rasterizer). SpRichTextDestroyGlyphRunInfo already frees the glyph-run handle.