Your game's interface in HTML and CSS, rendered entirely off the game thread.
You author screens as .rml / .rcss documents — the web languages, not a UMG graph — and the plugin runs them on its own UI thread: layout, styling and draw-command recording all happen away from the game thread, which only enqueues input and reads back a snapshot. Full-screen HUD, menu, or a panel on a quad in the world.

One UI thread owns every document: layout, styling and draw-command recording. The render thread replays the resulting command buffer into a persistent render target, and Slate composites that target every engine frame. What is left on the game thread is enqueueing input and reading one published snapshot.
Publication is withheld when a frame's content hash is unchanged and no resource traffic occurred, so a static UI stops costing anything to draw: the static M1 HUD published 1 frame in 6,910, and the glass demo 0 of 3,363. The reference HUD below publishes 100% of frames by design — it animates every one of them, which is why it is the worst case.
Every figure here is measured and re-runnable in your own project: vacuus.RefHud to load the scene, vacuus.M1HUD.PerfLog 1 to print the timings and the publish ratio.
| Game thread / frame budget 0.10 ms · ~8–11× | 0.012 ms dev 0.008–0.009 ms shipping |
|---|---|
| Composite only, idle UI budget 0.05 ms · ~12–25× | 0.002 ms shipping 0.003 ms dev |
| UI thread, update + record budget 1.2 ms steady avg | 1.050 ms shipping 1.077 ms dev |
| Added RAM budget 32 MB · A/B median | +14.3 MB |
| Added disk, shipping budget 10 MB · Linux proxy | +3.22 MiB |
.rml is the markup, .rcss the styles, and both are plain files on disk. There is no editor asset to open and no graph to wire — your edit is complete when the file is saved, and it diffs, reviews and merges like the rest of your source.
Screens live under Content/DevUI: the plugin's own tree ships the demos, and your project's tree is an extension point beside it, not an override.
Content/DevUI. vacuus.ReloadUI does the same thing from the console. The entire UI surface is text an agent can read and write. That is the easy half. The hard half is that most authoring mistakes here produce no error and a screen that renders — just not the one you asked for. A human notices immediately; an agent, which cannot see the screen, does not.
So the package ships the other half too: a front door the agent finds on its own, a generated ground truth to check itself against, and a verification loop that works without eyes.
UI is VaCuus (HTML/CSS off the game thread). Before touching anything under Content/DevUI, read Plugins/VaCuus/docs/buyer/ai-guide.md and Plugins/VaCuus/docs/buyer/rcss-matrix.md.
At the package root, where an agent looks first. Short by design: the five things that would otherwise cost the session, and links to the real pages.
The full brief. Which mistakes warn, which are drawn wrong in silence, and how to tell the two apart.
99 properties, 20 shorthands, 16 decorators — parsed out of the exact RmlUi vendored in the package, not out of a model's training data. If a property is not in that file, it does not exist.
A declaration RmlUi cannot parse logs a warning naming file:line, in every configuration including Shipping. Plus a headless 1920×1080 render recipe and the 227-test suite, so an agent can check its own work.
Data models bind straight from your UPROPERTY fields. The plugin reads them through the engine's own reflection — no mirror struct to keep in sync, no serialisation step between your game state and the document.
JavaScript is optional, not assumed. QuickJS-ng is vendored in-tree for the projects that want scripting, and a TypeScript/Preact workflow sits on top of it when you want that too.



:hover/:active/:focus, a wheel-scroll list, a text field with tab order, and a region that lets clicks reach the game No <a href> navigation, no CSS Grid, no fetch, no DOM library you remember. Navigation is the host calling LoadDocument; layout is flexbox and absolute positioning.
It is RmlUi's style language: a deliberate subset with its own additions. The generated matrix is the exact difference, keyed to the engine in your package — read it before you write a property.
A few parse and are then not rendered — box-shadow is the headline case, and it warns per view and names a substitute. The matrix marks them; nothing here is a surprise you find at ship time.
There is no Binaries/ directory and that is deliberate. A Blueprint-only project has no toolchain to compile the plugin with — converting takes two minutes: Tools → New C++ Class → None.