Start with
architecture-principles.md— the governing policy for layout, scaling, themes and component ownership. The guides below describe the implemented system and its current limits.
RmlUi provides the client's game UI through the SDL_GPU renderer. The legacy UI migration is complete: RML/RCSS owns presentation and layout, while C++ owns game behavior, view models and engine integration. Use this directory to maintain existing windows, build new ones, or edit a theme.
The game UI uses one RmlUi context, shared components and two built-in themes, legacy and
modern. The class map records which components replaced the retired native toolkits.
Game windows integrate through mu::ui::window::CObject and are drawn by RmlUi in both themes.
The CWin toolkit,
the sprite widgets, the CUIControl toolkit and the shared item camera (C3DRenderMng) are
deleted. That covers the login and character-select scene, the HUD (one theme-placed unit in the
workspace), the inventory family, the docked panels, the dialogs and options, the social windows,
the event, siege, duel and NPC windows, and the world labels (names, balloons, bars, ground items)
through the world-label layer. migration-ledger.md maps each original
class to what replaced it.
Themes own placement. Documents sit on .stage,
.hud-board or a workspace slot, RmlUi hit-tests them, and CManager gives native code one
measuring space (layout-and-scaling.md's "Units and placement"). Only
infrastructure may touch the active transform; tools/check_layout_transform_users.py keeps it to
that.
Stays native on purpose: live 3D content (item grids, equipped items, item and character
previews — a RenderTarget shows one inside a document, as the potions, the letter portrait, the
character-creation preview and the event previews do), the mouse cursor, developer overlays, and
the equipment paperdoll's background, durability tint and drag highlight, which paint behind the
equipped item's 3D icon.
The migration is complete, but the following customization and validation limits remain.
- No user-override layer or theme inheritance (principles §18–19). Themes are two directories
selected by name. The supported first-party set is
legacyandmodern; their different structures and the contract guard help detect presentation coupling (§25, §28). Revisit the resource hierarchy when mod support is prioritised. - Drag positions are session-only. Dialogs and social windows retain positions while running; inventory placement resets when its workspace is arranged again. See Dragging and saved positions.
- Validation is uneven across windows. Headless layout tests cover resolution and OS display
scale for the party list, trade, event entry, the personal shops and the MU Helper; other
windows have the UI-scale sweep (
layout-and-scaling.md). Drag state across a scale change and theme changes while open are checked when a window is touched. MiniMaplays out in physical pixels: its art turns 45° with no reference-px space (UI/HUD/MiniMapLayout.cpp). Trigger: a map redesign needing theme-owned map geometry.- A new siege command's pulse binds
rgb(255, pulse, pulse): RCSS cannot mix a bound fraction into a colour. Trigger: a theme wanting another pulse palette. CCryWolfpicks its sprite files and texel rects in C++ (SyncResult(),SyncHud()); a theme can hide or rearrange that art, not replace its C++ selection. Revisit when another event-art set is required.- The title scene's loading bar is pushed in real
px, because its background is still native sprites on an 800x600 per-axis scaledpcannot reproduce. Keep the gauge aligned with those sprites until the title background also uses RmlUi layout.
Render/RmlUi: what RmlUi needs from any host engine. The runtime (lifetime, contexts, the frame hook, SDL input, IME), the render and system interfaces, custom decorators. No themes and no game state.UI/RmlBridge: what game windows share. Themed loading and theme switching, the model helpers, visibility and stacking, the shared tooltip, dragging, text-size policy, and the adapters between legacy reference coordinates and RmlUi (root transform, panel readback, the keyboard claim). No widget classes and no event or layout system of its own: use RmlUi's.- A window: its model fields, its event callbacks and its game behaviour. Layout, units, colours and placement stay in RML/RCSS.
The runtime renders, updates and routes input; the game adds the rest through
RmlUiRuntimeHooks. How native 3D shares the frame with RmlUi is in
Frame lifecycle below.
- Building New UI — the implementation workflow: choose a component
and owner, define its model and documents, connect lifecycle and input, and validate both themes.
Read before starting anything under
UI/. - Component Catalog — reusable RmlUi/RCSS primitives that exist, and concepts without a shared primitive. Check before adding a new mechanism.
- Layout, Anchoring & Scaling — the UI-scale (
dp) mechanism, the anchor/stretch/center utility classes, and the scale sweep for hit boxes read from RCSS. - Window Placement — the theme-owned workspace: regions, docks, slots, the HUD in the workspace, dragging, and theme recipes.
- Theming & Modding — the theme mechanism, adding a theme, and the modding constraints.
- Engine Findings — empirical gotchas of this RmlUi build and the
CObject/CManagermachinery. Check before assuming a bug is new. - Migration Ledger — where each original window class went; check
here for "what replaced
X?".
RmlUi renders through its own vendored SDL_GPU backend
(src/ThirdParty/RmlUi/Backends/RmlUi_Renderer_SDL_GPU.cpp) rather than a bridge against
IMuRenderer's game-oriented primitives. RmlUiRenderInterface
(Render/RmlUi/RmlUiRenderInterface.cpp/.h) subclasses it, overriding only
LoadTexture/ReleaseTexture so game assets (.OZT/.OZJ) route through CGlobalBitmap.
The submodule points at the fork nitoygo/RmlUi (integration/sdl-gpu-parity), which carries
the renderer work upstream doesn't have yet; renderer changes are committed there first, then the
submodule pin is moved.
transform, gradients, box-shadow and backdrop-filter: blur() render; other filters and
mask-image have limited verification — engine-findings.md has the current list. Test a new visual property in isolation rather
than trusting the parser.
Texture-lifetime rule: CGlobalBitmap's numbered slots get force-reassigned across scene
transitions, while RmlUi caches a texture handle indefinitely. Any RmlUi-loaded texture must use
CGlobalBitmap::LoadImageExclusive(), never the shared LoadImage() — sharing a slot crashes on
scene re-entry.
Native drawing and RmlUi share one frame, and each step draws over everything before it. Hexagons are native 3D, rounded boxes are RmlUi contexts:
flowchart TD
scene["World and legacy 2D"]
subgraph loop["CManager::Render(): windows in layer-depth order"]
win{{"Each window's native Render(): state for its document,<br/>what little still draws natively"}}
end
rtt{{"Offscreen pass: RenderTarget drawers into textures:<br/>the item windows' items and effects, the cash shop, potions,<br/>letter portrait, character creation, item previews, the item on the cursor"}}
main("Pre-submit: the main RmlUi context, every game document;<br/>render-target textures show here as images;<br/>then the debug context ($details, $glstats)")
post["Post-RmlUi pass: the cursor"]
scene --> win --> rtt --> main --> post
A flush draws what has been recorded so far (FlushRenderCommands()), so a context rendered right
after it lands over that and under whatever is recorded next. The seams are on IMuRenderer and
registered once in Winmain.cpp.
- The main context renders after ordinary native drawing (
SetPreSubmitCallback,RmlUiRuntime::RenderFrame()). The post-RmlUi pass (SetPostRmlUiCallback) draws the cursor above the documents and also runsCSystem::SyncMainSceneHudVisibility()every frame. - The debug overlay has a context of its own (
RmlUiRuntime::GetDebugContext(), chosen fordiagnostics.rmlinRmlThemedView.cpp). It updates and draws in theOverlaypass, so the RmlUi update and render times it shows leave the overlay out. Window content uses document render targets so other windows can cover it. - Native drawing inside one document goes into a
UI::RmlBridge::RenderTarget(SetOffscreenRenderCallback,component-catalog.md), so it sits at its element's depth: live 3D, and native 2D effects that belong to the window (SetOffscreen2DRect()).
UI::RmlBridge::RmlModelBinder<Model> (UI/RmlBridge/RmlModelBinder.h) wraps a per-window
Rml::DataModel: game state → SyncRmlModel() → a plain data struct → bound via
Rml::DataModelConstructor. The model must be created before the document that references it
is loaded — a model created after LoadDocument() is too late and every binding shows its
literal source text. Clicks go through data-event-click → BindEventCallback (right-click
through data-event-mouseup, button 1). A window with no dynamic state can skip the binder and
use AddEventListener.
A window holds its binder and documents through UI::RmlBridge::ThemedView<Model>
(UI/RmlBridge/RmlThemedView.h), which creates the model before loading the documents, rebuilds
both on a theme switch and tears them down; the component catalog explains its use; the header defines its options.
A theme is a folder name — adding one needs no recompile. A window's ThemedView loads its
.rml (through ThemedDocumentLoader, UI/RmlBridge/RmlTheme.h/.cpp) against a synthetic
themes/<active-theme>/ URL, so its <link href> pulls that theme's .rcss; a theme can also
fork the .rml itself (themes/<theme>/<name>.rml).
Two built-in themes, legacy (original sprite art and compatibility layout) and modern,
are maintained together. Update both when a window's content or behavior changes.
Shared rules live in themes/<name>/base.rcss (.btn, .checkbox-box, .hidden, the
mandatory body { pointer-events: none; } reset). Full mechanism in
Theming & Modding.
Any interaction more than one window wants belongs in UI::RmlBridge as a shared primitive.
MakeDraggable(handle, panel, onMove) (UI/RmlBridge/RmlDraggable.h/.cpp) builds on RmlUi's drag
events; the handle needs pointer-events: auto, and it moves both axes. Dialogs and the friend
family drag; inventory dragging lasts until the workspace arranges it again. Positions are not
saved across sessions. See component-catalog.md's
"Dragging" and window-placement.md for the scope.
The login- and character-scene windows (g_*Win globals) are CObjects owned by
CSceneUICoordinator, which calls each one's Release() by name on every scene transition. A
document is created once and reused, and RmlUi renders last, so each such window's Release()
must hide its document (CLoginWin::Release()), or it paints over the next scene.
pointer-eventstrap:Context::IsMouseInteracting()is true over any RmlUi hover target, so a full-window document with defaultpointer-eventsswallows every click. Every document needsbody { pointer-events: none; }, opted back in per interactive element (base.rcssdoes this for every window that links it).- Mouse input is handled directly, not via RmlUi's
RmlSDL::InputEventHandler, which callsSDL_CaptureMouse()on every click and double-scales motion by pixel density. Wheel, key and text events use the vendored handler. .hiddenvs.disabled:.disabledkeeps an element in layout (dimmed);.hidden(display: none) removes it. Use.hiddenwhen something doesn't apply to the current scene at all — a dimmed-but-present button once overlapped its neighbour inCSysMenuWin.- Paint order: a parent's background paints before its children, and children paint in document order. Move the visually topmost element later in the document rather than fighting with z-index.
| Subsystem | Key files |
|---|---|
| Runtime lifecycle | Render/RmlUi/RmlUiRuntime.h/.cpp |
| Render interface | Render/RmlUi/RmlUiRenderInterface.h/.cpp |
| System interface | Render/RmlUi/RmlUiSystemInterface.h/.cpp |
| Renderer seams | Render/Renderer/MuRenderer.h |
| Input gating | Core/Input/UiInputRouter.cpp (IsMouseOverUI()) |
| Model/binder layer | UI/RmlBridge/RmlModelBinder.h, UI/RmlBridge/RmlThemedView.h |
| Theme framework | UI/RmlBridge/RmlTheme.h/.cpp |
| Draggable helper | UI/RmlBridge/RmlDraggable.h/.cpp |
| Native drawing in a document | UI/RmlBridge/RmlRenderTarget.h/.cpp |
| Workspace placement | UI/Placement/WindowPlacement.h/.cpp |
| Texture lifetime | Render/Sprites/GlobalBitmap.h/.cpp — LoadImageExclusive() |
| RML/RCSS assets | bin/Data/Interface/RmlUi/ — one .rml per window + themes/{legacy,modern}/ |