X11 Demo Bring-up Notes: Making PictureBox, MusicDemo, and SmartWatchDemo Run
If you are debugging Linux hosts, validating the first X11 frame, or trying to make real demos stable enough to capture, this write-up shows you exactly how we worked through it.
Our goal in this round was straightforward:
- bring
PictureBoxDemoup underX11 - bring
MusicDemoup underX11 - bring
SmartWatchDemoup underX11 - preserve reusable screenshot assets for all three
Once we got into it, this turned out to be much more than “does the window open or not?”. Several layers had to line up at the same time:
- which
DISPLAYthe runtime actually connected to - how the root font was attached into the LVGL style tree
- whether the custom glyph bitmap callback respected LVGL’s buffer contract
- whether font pointers read during control creation were stable
- which path was good enough to treat as the screenshot baseline
Lesson 1: Verify the real X11 target first
At first we assumed the demos would use the Xvfb :99 display we prepared for validation.
That assumption was wrong.
The current Linux runtime does not simply trust the process-local DISPLAY value. It also scans candidate X11 sockets and can end up preferring a real desktop display. In this bring-up round, the actual working target was :1, not :99.
That sounds like an operational detail, but it matters a lot:
- the real window inspection tools must point at the actual display
- screenshot capture must target the same display
- otherwise you can end up debugging the wrong environment
That is why every screenshot we kept from this round came from DISPLAY=:1.
Lesson 2: Manually allocating lv_style_t from managed code is fragile
The earliest crash stacks from MusicDemo and SmartWatchDemo both converged on font-related LVGL paths:
lv_font_get_line_heightlv_text_get_sizelv_label_refr_textlv_obj_refresh_style
At first glance that can look like a label-specific bug. But after following the initialization path more carefully, the more suspicious part was how the default font style was installed.
The old Linux default-font path manually allocated an lv_style_t from managed code, then called:
lv_style_initlv_style_set_text_fontlv_obj_add_style
That approach is risky because lv_style_t is a native-owned structure. If managed layout, alignment, or lifecycle assumptions drift even slightly from what the native library expects, the whole path becomes brittle.
We changed [LvglHostDefaults.cs](/home/admin/openclaw/workspace/LVGLSharp.Forms_1/src/LVGLSharp.Runtime.Linux/LvglHostDefaults.cs) to use a simpler and safer path:
lv_obj_set_style_text_font(root, font, 0);
That change removed a whole class of “managed code trying to simulate native style ownership” problems.
Lesson 3: The glyph bitmap callback has to obey LVGL’s memory contract exactly
The next high-probability issue lived inside the custom font manager.
In [SixLaborsFontManager.cs](/home/admin/openclaw/workspace/LVGLSharp.Forms_1/src/LVGLSharp.Core/SixLaborsFontManager.cs), GetGlyphBitmap previously had two dangerous behaviors:
- it returned
draw_bufinstead ofdraw_buf->data - it mixed its own bitmap caching lifetime with the draw buffer that LVGL provided for the current render
That is the kind of bug that often survives the first few steps and only explodes deeper in the render stack. LVGL expects a pointer to glyph pixel data. If it instead receives a struct pointer or a buffer with the wrong ownership model, later blending code becomes vulnerable.
We fixed that by doing less:
- render directly into
draw_buf->data - return
draw_buf->data
In other words, the callback now fills the buffer LVGL asked for and returns the exact address LVGL expects.
After that change, MusicDemo became much more stable and was able to open as a real X11 window and produce a usable screenshot.
Lesson 4: A font pointer returned from style lookup is not always a trusted source
SmartWatchDemo was trickier.
After the first wave of fixes, it no longer failed before window creation, but it still crashed very early during label creation. A lightweight creation trace narrowed the problem down to the first title label. Then gdb showed the critical clue: the “font pointer” passed into lv_font_get_line_height was 0xff.
At that point the issue was no longer “the font file is bad”. It was “something in the style path is feeding LVGL a bogus value as if it were a font pointer”.
The root cause landed in the WinForms-side style helper. During control creation, the helper was querying the current font from the root style and then reapplying that value to newly created controls. That sounds reasonable in theory, but during the current X11 bring-up lifecycle, that query result was not stable enough to trust.
We fixed that by introducing a managed registry for the active runtime font:
- add
[LvglRuntimeFontRegistry.cs](/home/admin/openclaw/workspace/LVGLSharp.Forms_1/src/LVGLSharp.Core/LvglRuntimeFontRegistry.cs) - cache the active font pointer at the moment the Linux runtime installs it
- teach
[ApplicationStyleSet.cs](/home/admin/openclaw/workspace/LVGLSharp.Forms_1/src/LVGLSharp.WinForms/Forms/ApplicationStyleSet.cs)to use that managed pointer instead of querying the root style again during control creation
The key idea is simple:
- the code that installs the font should keep the trusted pointer
- control creation should not depend on a style lookup that can return transient or invalid values
- obvious bad sentinels such as
0xffshould never be allowed to flow deeper into LVGL
Lesson 5: “Stable enough to bring up” and “fully fixed” are not the same thing
By this point we had moved all three demos forward substantially, but one more reality had to be acknowledged:
the custom font path could still crash later in the draw pipeline, especially inside software blending paths such as lv_draw_sw_blend_color_to_rgb565.
That means:
- “the window opens” is not the same as “the full font-rendering pipeline is correct”
- “the first screen appears” is not the same as “every label is stable”
So instead of pretending the custom font renderer was completely solved, this round added an explicit stability fallback:
[X11View.cs](/home/admin/openclaw/workspace/LVGLSharp.Forms_1/src/LVGLSharp.Runtime.Linux/X11View.cs)now supportsLVGLSHARP_DISABLE_CUSTOM_FONT=1
With that switch enabled, the X11 runtime skips the current custom font manager and falls back to LVGL’s built-in font path.
The tradeoff is very clear:
- complex demos become stable enough to launch and capture
- but CJK text can temporarily degrade into missing glyphs or square placeholders
That is not the final state we want, but it is a valid engineering move for bring-up work because it allows screenshots, regression checks, and documentation to keep moving while the deeper renderer issue is still under investigation.
The currently recommended X11 launch command
If your goal right now is “make it run stably first and capture the UI”, the current recommended X11 command is:
DISPLAY=:1 \
LVGLSHARP_LINUX_HOST=x11 \
XDG_SESSION_TYPE=x11 \
WAYLAND_DISPLAY= \
LVGLSHARP_DISABLE_CUSTOM_FONT=1 \
dotnet run -f net10.0 --project src/Demos/SmartWatchDemo/SmartWatchDemo.csproj -p:EnableWindowsTargeting=true
MusicDemo follows the same pattern with its own csproj.
If you are doing layout validation, screenshot collection, or UI regression work, that stable path is the best current baseline. We can keep improving the deeper custom-font path in parallel.
What we have now
We now have three X11 screenshot assets:
PictureBoxDemo:/images/x11-pictureboxdemo.pngMusicDemo:/images/x11-musicdemo.pngSmartWatchDemo:/images/x11-smartwatchdemo.png
They have also been added to the curated screenshot pages:
What you should keep in mind if you are bringing up X11
- X11 bring-up is not a single bug. It is the combined result of display selection, style installation, font ownership, glyph callback correctness, and capture tooling.
- If a native structure ABI is not fully under your control, managed-side manual allocation should be treated with great caution.
- In glyph callbacks, return values and buffer ownership must match LVGL’s contract exactly, or the crash may surface much later in the render stack.
- If style lookups are unstable during early control creation, keeping a trusted active font pointer on the managed side is safer than repeatedly querying the style tree.
- A deliberate stability fallback switch is not a failure. It is often what lets demos, screenshots, validation, and deeper debugging move forward at the same time.
What we still need to finish
We moved the X11 path from “some demos are black or crashing” to “complex demos can launch and be captured”.
But we are not at the finish line yet. If you keep following this path, these are the next items we still need to close:
- fully fix the deeper custom-font rendering crash on X11
- make
MusicDemoandSmartWatchDemostable without disabling the custom font path - restore CJK text quality for views such as
PictureBoxDemo - eventually reduce
LVGLSHARP_DISABLE_CUSTOM_FONT=1from a stability path to a debugging-only switch
If we land that next step, X11 stops being just “something we managed to screenshot once” and becomes a solid engineering-grade validation path for Linux host work.