A 3D UI system for Unreal Engine 5.8 — widgets that live in the world, widget Blueprints you subclass, and a designer built to feel like UMG's.
Install · vs UMG · vs upstream · Status · License
DreamGUI is a fork of LGUI / LexUI by Lex Liu, MIT licensed. It is not a drop-in replacement for it — see how it differs.
A UI widget here is a UObject with a rect, a pivot and anchors, arranged into a tree and drawn by
a canvas that batches the whole tree into as few draw calls as it can. That is Unity's uGUI shape
rather than Slate's, and it buys three things UMG cannot do as directly:
- UI in the world. A canvas can render in screen space or sit on a surface in the level, at any angle, lit or unlit, with correct hit testing either way.
- A tree that is a class. A UI tree is authored in a widget Blueprint (
UDreamWidgetBlueprint) and reused by subclassing and by nesting one widget class inside another, the way UMG does it. - Per-widget perspective. A widget can establish a perspective its subtree is foreshortened
into, the way CSS
perspectiveworks.
It costs you Slate's ecosystem: none of UMG's widgets, styles or bindings apply.
The runtime is split into modules by layer. A module depends only on modules in the layers below its
own, never on a sibling in its layer, and Tools/Tests/static_checks.py fails an include that goes the
other way (rule layering). Every runtime module loads at PostConfigInit, as the core does, so its
types and .dui tags are in place before anything compiles.
L4 DreamGUISamples
L3 DreamGUIControls DreamGUIExtensions
L2 DreamGUIInput
L1 DreamGUI (core)
L0 DreamGUIRenderer DreamTween
------------------------------------------------------------
DreamGUIEditor, DreamGUIK2Nodes (uncooked only), DreamGUITests (editor only)
| Module | Layer | Holds |
|---|---|---|
DreamGUIRenderer |
below the core | The view extension that draws DreamUI and the render command that draws a render-target canvas, its shaders, the vertex and index formats, the material proxies a canvas answers its parameters through, the post-process proxies with the screen reads and writes the effects share, and the stage timing behind DreamUI.Stats. It knows nothing of widgets: the core registers what it asks for |
DreamGUI |
core | Widgets, visuals, canvas batching, layout, text and .dui, animation, the event contracts, the render root that holds a canvas's sections for the renderer, and the PNG capture |
DreamGUIInput |
above the core | The input system: the event systems and their preset actors, the raycasters and input modules, the action router, navigation, drag and drop, tooltips and modals, the selectable base the controls are built on, and the game viewport client |
DreamGUIControls |
above the input system | The control library: the Dream* controls (button, toggle, slider, lists, dialog, tab view, ...), the UI* behaviours they are built from, the action bar, style sheets and the UMG interop |
DreamGUIExtensions |
above the input system | 2D lines, polygons and rings, the static-mesh visual, the retainer box and the render-target helpers, lyrics, the concrete mesh modifiers, and the background blur, pixelate and pixel sort effects |
DreamGUISamples |
above the controls | The showcase and the controls gallery |
DreamTween |
independent | Tweens |
DreamGUIEditor, DreamGUIK2Nodes |
uncooked only | The designer and the asset tools; the Blueprint nodes. Loaded by every process that runs uncooked content, a game started from the editor (-game, Standalone Game) included: an editor build drops a Blueprint's saved bytecode on load and rebuilds it from the Blueprint, and the widget Blueprint class and its compiler live here. Outside the editor only the compiler starts |
DreamGUITests |
editor | The automation suite |
C++ that uses a type from a split-off module adds that module to its Build.cs. Assets need nothing:
every type that moved still loads under its old name (see below).
Besides the Build.cs line, a few includes and calls changed. Every other header kept its path.
| Include that was | Is now |
|---|---|
Core/DreamUIRender/*, Core/DreamUIMesh/DreamUIGizmoMesh.h, Core/DreamUIMeshVertex.h, Core/DreamUIMeshIndex.h, Core/DreamUIBlendMode.h, Core/DreamVisualPostProcessRenderProxy.h |
DreamUIRender/ and the same file name |
Extensions/DreamGameViewportClient.h |
Event/DreamGameViewportClient.h |
Extensions/DreamUMGWidget.h, Extensions/DreamUMGWidgetInteraction.h |
UMG/ and the same file name |
Core/DreamUIEachAdapter.h |
Binding/DreamUIEachAdapter.h |
Core/Components/DreamBackgroundBlur.h, Core/Components/DreamBackgroundPixelate.h, Core/Components/DreamPixelSort.h |
Extensions/Effects/ and the same file name |
| Call that was | Is now |
|---|---|
UUITextInput::RouteCharacterInputToActiveInput |
DreamUITextInputRouter::RouteViewportCharacter, called before the base class |
UDreamCanvas::CalculateRenderScaledSize |
FDreamUIRenderer::CalculateRenderScaledSize |
DreamPixelSort::ResolveRegionSize |
DreamUIPostProcessEffects::ResolvePixelSortRegionSize (DreamUIRender/DreamUIPostProcessEffects.h) |
UDreamUIManagerWorldSubsystem's event-system registry and player interaction: GetEventSystemByUserIndex, GetMapUserIndexToEventSystem, AddEventSystem, RemoveEventSystem, EnsureInteractionForPlayer, GetInteractionHost |
The same names on UDreamUIInputSubsystem (Event/DreamUIInputSubsystem.h); UDreamUIInputSubsystem::Get(WorldContext) finds it |
UDreamUIManagerWorldSubsystem::AddSelectable, RemoveSelectable and GetAllSelectableArray, with UUISelectable |
The same, with UDreamUIBehaviour |
UDreamGUISettings::DefaultStyleSheet as a UDreamUIStyleSheet |
A TSoftObjectPtr<UDataAsset>; UDreamUIStyleSheet::GetProjectSheet() does the cast |
What changed for C++ since the split — the material callback a visual overrode, the input and renderer calls that went with the old code paths — is in Docs/Migration.md.
The renderer logs to LogDreamGUIRenderer; stat DreamGUI still shows its counters.
Requires Unreal Engine 5.8. Clone into your project's Plugins/ directory:
git clone https://github.com/TypeDreamMoon/DreamGUI.git Plugins/DreamGUIRegenerate project files and build. That is the whole install for a fresh project.
Important
Engine 5.8, a launcher install included. The plugin compiles against the engine's public headers
only: the renderer reads the scene's depth through the public scene-texture API, and the static check
engine-private-path fails any include path into Runtime/Renderer/Private or Internal.
msdfgen, which the glyph rasteriser compiles into its own translation unit, used to need a source
build: upstream generates its single-file copy rather than committing it, so a launcher install has
only Engine/Source/ThirdParty/msdfgen/msdfgen.tps. The plugin now carries its
own generated copy under ThirdParty/ — see ThirdParty/README.md. A plain clone or a zip download
builds it as-is; the ThirdParty/msdfgen submodule is only what Tools/UpdateMsdfgen.ps1 regenerates
that copy from.
A DreamGUI text field is not a Slate widget, so it is never on the keyboard focus path, and the
engine's only landing place for a platform character in a game is the virtual
UGameViewportClient::InputChar — which has no delegate to subscribe to. Without an owner for that
function, UUITextInput falls back to its own FKey → character table, and that table is only
correct on US QWERTY: AZERTY, QWERTZ, Dvorak, Cyrillic, dead keys and AltGr all type the wrong
character. (IME users are unaffected — composition text arrives through TSF, not through the table.)
Pick whichever of these fits the project. The field logs one warning the first time it is edited if neither is in place.
1. Use the plugin's viewport client. In Config/DefaultEngine.ini:
[/Script/Engine.Engine]
GameViewportClientClassName=/Script/DreamGUIInput.DreamGameViewportClient2. Keep your own viewport client. Either derive it from UDreamGameViewportClient instead of
UGameViewportClient, or keep its base and hand the character to DreamGUI from its InputChar
override (module DreamGUIInput, header Interaction/DreamUITextInputTarget.h) — after the console,
and before the base class:
bool UMyGameViewportClient::InputChar(FViewport* InViewport, int32 ControllerId, TCHAR Character)
{
FString CharacterString;
CharacterString += Character;
// An open console takes every character.
if (ViewportConsole && ViewportConsole->InputChar(FInputDeviceId::CreateFromInternalId(ControllerId), CharacterString))
{
return true;
}
// Before the base class: in a play-in-editor viewport it answers true for every character, so a
// field asked after it never sees one there.
if (!IgnoreInput() && DreamUITextInputRouter::RouteViewportCharacter(this, ControllerId, Character))
{
return true;
}
return Super::InputChar(InViewport, ControllerId, Character);
}DreamUITextInputRouter::RouteViewportCharacter is the entire contract — it hands the character to
the field the typing player (the one ControllerId is) is editing, or, when no field takes it, to
what that player has focused as a key character, and returns whether either took it. From the first
character that arrives this way, the FKey table stops synthesising printable characters in that
world, so the two roads never double-type.
By default DreamGUI hears input through its preset event system actor's bindings on the player
controller, so it hears nothing in the engine's own UI-only input mode: SetInputMode(FInputModeUIOnly())
makes the game viewport ignore input, and the controller never sees it. Turn on Project Settings →
Plugins → Dream GUI → Input → Use Slate Input Source and DreamGUI hears the mouse, touch, keys and
sticks from Slate itself instead — an input pre-processor, ahead of the game viewport — in every input
mode. The preset actors stand down while it is on, so nothing arrives twice.
Slate Input Consume Policy decides what the UI keeps from the game: Never (the default; the game
hears everything, as it always has), WhenOverUI (a press, release or wheel turn over DreamGUI UI,
and any key the UI took) or WhenHandled (only a press on a widget that handles presses, and a key the
UI took). A key typed into a field being edited is always kept. The source is off by default for now,
and becomes the default in a later version.
The whole move — what to take out first, what loads by itself, what no longer exists, what to check afterwards and what changed for C++ — is written up in Docs/Migration.md. In short:
They reference the old class names and the old /LGUI/ mount, so they need CoreRedirects — and
the plugin ships them: the [CoreRedirects] block in
Config/DefaultDreamGUI.ini is mounted as the plugin's own config
branch, and the engine applies every branch's redirects before the first asset loads. Nothing to copy.
It covers the LGUI/LexUI rename, the prefab-vocabulary rename that the class model replaced, the
control renames (UIButtonComponent → UIButton and its siblings), and the module split: a type that
moved out of the core into another of the plugin's runtime modules is still found under its old
/Script/DreamGUI name.
If you copied the block into your project's Config/DefaultEngine.ini for an earlier version,
delete that copy. Earlier versions shipped it as a template, Config/DefaultEngine.ini, on the
belief that a plugin's config is read too late for redirects; it is not. Two redirects for one old
name with different new names are an error, and the copy is older than the file that ships.
Removed in this version. These have no redirect, because there is nothing left to point at:
- the root Blueprints
WorldSpaceRoot_DreamRenderer,WorldSpaceRoot_UERenderer,ScreenSpaceRootandDreamWorldSpaceRaycasterSource_Mouse; UDreamWidgetPresenterComponent— the abstract baseUDreamWidgetPresenterComponentBasestays, andUDreamWorldWidgetComponentis what you place now;- the
UDreamWorldSpaceRaycasterBase,UDreamWorldSpaceRaycasterForWorldTriggerandUDreamWorldSpaceRaycasterSourcefamily —UDreamWorldSpaceRaycasterabsorbed all of it; - the plugin settings'
ScreenSpaceRootClass,WorldSpaceRootClass,WorldSpaceUERendererRootClass,WorldSpaceRaycasterSourceClassandbLegacyTouchPointerIds; - the console variables
r.DreamUI.MaterialWrappersandr.DreamUI.RTDrawer, and the renderer behaviour they put back.
A level that still holds one of those Blueprints drops that actor on load — its class no longer resolves, so the whole export is discarded and a warning is logged naming it. Nothing is left behind to fix up: drag the widget Blueprint into the level again.
Worth knowing before you commit to either, because the difference is structural rather than cosmetic.
| UMG / Slate | DreamGUI | |
|---|---|---|
| Widget | SWidget, retained-mode Slate |
UObject in a component-like tree |
| Sizing | Content-sized: a widget's size is its desired size | Box-first: you author a rect, content is arranged inside it |
| Text | The box grows to the text | The text is aligned in the box, and may overflow it |
| Placement | Slot-relative | Anchors + pivot, resolution-independent |
| Reuse | Widget Blueprint subclassing | Widget Blueprint subclassing, plus named slots for content |
| In-world | WidgetComponent, a rendered quad |
A first-class render mode |
The text difference is the one that surprises people. In UMG a TextBlock cannot overflow, because
its box is derived from the text; you control wrapping instead. Here the rect is authored, so text
can overflow it, and you get controls UMG has no need for — Margin, LineHeightPercentage,
WrapTextAt and Best Fit (shrink the font until it fits, which neither UMG nor Slate offers).
The structure differs; the vocabulary deliberately does not. A control here answers to the name its
UMG counterpart uses, with the meaning it has there: ScrollWidgetIntoView takes a destination and a
padding, a scroll box has bFrontPadScrolling and WheelScrollMultiplier, a spin box has
MinSliderValue and ClearMaxValue, a panel slot has Nudge and bForceNewLine, every widget has
SetIsEnabled, SetRenderShear and a FlowDirectionPreference. Every knob the details panel can
turn is one a Blueprint can turn at runtime, through a setter that pushes the change instead of
writing a field nothing reads again.
Where a name could not be kept, that is written down rather than left to be discovered.
Resources/UMGParity holds one table per UMG class -- 58 of them, some 960 Blueprint-facing members
-- and each row says one of three things: adopt (same name, same meaning), map (here under
another name, or on another type, and which), or reject (deliberately absent, with the reason:
there is no immediate-mode paint context to draw into, a rect block has no per-instance material,
and so on). The automation suite holds those tables against UMG's own reflection, in both
directions: a member the engine gains in an upgrade turns up as a row that does not exist, and a row
naming something this plugin has since renamed turns up as a name that does not resolve.
Two differences are worth knowing in advance. A new knob defaults to what the control already
did, not to UMG's default, so that existing content does not move -- ScrollWhenFocusChanges is
AnimatedScroll here and NoScroll there, and the tables record each such case. And appearance
lives in a control's style struct while behaviour lives on the control, so a few UMG properties are
one level down: EntrySpacing is Style.RowSpacing.
Docs/Reference is the property and function reference, one page per class, each ending with that
class's UMG comparison. It is printed from reflection rather than written, so it cannot fall behind
the headers:
UnrealEditor-Cmd.exe <project>.uproject -run=DreamGUIReferenceDocs
Forked from upstream LexUI/5.7 at 765efeaf1 (2026-07-13), and the upstream commits up to 97d281376 (2026-07-21) were rebased in afterwards: 97d281376 is the upstream this code starts from, and its content is this repository's 5b48c42a. Diff against that, not against 765efeaf1 or a merge base, or the rebased commits count as this fork's changes. Upstream fixes after it are ported by hand.
Upstream is actively developed, but the two branches can no longer be merged cheaply:
| Upstream | Here | |
|---|---|---|
| Engine | UE 5.7 — no 5.8 branch on the LexUI line | UE 5.8 |
| Layout | FlexBox + Grid family, still being developed | Family deleted; UMG-shaped panels only |
| Editor | — | Designer largely rebuilt |
The layout split is the sharpest of these. Upstream's 2026-08-08 fix for an infinite loop in
ULexLayoutContainerFlexBox has no meaning here, because that class no longer exists.
Layout, along the lines Blink and Yoga use. Measurement is const and separated from
application; panels arrange into an immutable fragment that is committed in one write; desired size
is memoised for the duration of a pass; invalidation carries a reason, so moving a widget no longer
re-measures the whole ancestor chain. The legacy Lex layout family
(ULexLayoutContainerFlexBox, ULexLayoutContainerGrid, ULexLayoutSelfFlexBox,
ULexLayoutSelfGrid, and the ELexUILayoutMode switch) was deleted.
The designer, reviewed against UMG's widget designer. Viewport picking is by widget rect rather than by rendered triangles — layout-only panels have no mesh, so a raycast could never hit them, which made panels unclickable and undroppable. Added since: hover feedback, per-axis resize handles, an anchor medallion, marquee selection, drag-to-reparent on the canvas, Content-Browser drops onto the design surface, palette favourites, type-aware search, and undo coverage for create, paste and drop.
Text, with the four controls listed above.
Perspective, per widget and inherited by its subtree. Requires a screen-space canvas with a perspective projection; inert otherwise.
Render transform, widened to three dimensions, so a widget can be animated inside a layout without the layout fighting it.
Input, per player. Each local player has its own pointers, focus and text target, so a split screen's second player hovers, focuses and types on its own; the mouse, fingers and scripted pointers have id ranges of their own and no longer share an id. An optional Slate input source hears input before the viewport, so the UI keeps working in the engine's UI-only input mode.
Rendering. A canvas walks only the widgets that asked to change, patches in place the sections whose geometry changed rather than rebuilding them, and answers its materials' parameters through render-thread proxies instead of a material instance per draw call. A render-target canvas is drawn by a render command of its own, so it updates whether or not anything renders its world. The screen effects share one way to read, crop and write back the screen, on the render graph's textures.
A UI tree is a class, not an asset you instance. Authoring one gives you a
UDreamWidgetBlueprint whose generated class is a UDreamUserWidget; you subclass it, drop it
inside another tree, and bind to its named children by name — the same shape as UMG's widget
Blueprints, compiled by DreamGUI's own FDreamWidgetBlueprintCompilerContext.
The designer edits a preview instance of that class and writes back to the class, so there is no
apply step and no per-instance override list to reconcile. Content a parent supplies to a child goes
through UDreamNamedSlotHost.
Note
The prefab asset model this forked from is gone, along with SavePrefab, Apply,
ClearLoadedPrefab and Save on Apply. Assets saved against the old class names are covered by
the redirects in Config/DefaultDreamGUI.ini.
Content/Samples/HelloDreamGUI.dui is the smallest .dui
that is still a real screen — an anchored root, an overlay, a card, a vertical column of text. The
file's own header says what to do with it: make a Dream Widget Blueprint, point it at the file with
Pick Text Source in the designer toolbar, compile, and add it to the viewport with
UDreamUIBPLibrary::AddWidgetOfClassToViewport. That is the whole path from text to a UI on screen,
and nothing else needs configuring — the screen root, the raycaster and the event system are created
on demand.
Copy it into your own project before editing it; a plugin update overwrites the copy in the plugin folder.
The same class can be a surface in the level instead of a layer on the screen. Drag a widget
Blueprint from the Content Browser into a level, or place a DreamUI World Widget Actor from the
Place Actors panel: either way you get an ADreamWorldWidgetActor, whose entire content is a single
UDreamWorldWidgetComponent. Move it, rotate it and attach it like any other scene component.
The component hosts the tree; it does not redefine it. The Blueprint's own root Canvas remains the truth about how the UI is built; the component writes render mode, sort order and trace channel down onto it and leaves the rest alone.
WidgetClass— the widget Blueprint class to load.Backend—DreamUIRendererdraws the tree with DreamGUI's own renderer: flat, unlit and untouched by post process.UERenderersends it through the engine's pipeline instead, so it takes post process and depth like any other mesh in the level.bUseDesignSize/DrawSize— the size the tree lands at. On by default, following the size the Blueprint was designed at; turn it off to give this actor a size of its own.Pivot— where the actor's origin sits within that rectangle.SortOrder— order among world-space canvases.TraceChannel— the channel this canvas answers on. A raycaster only sees canvases whose channel matches its own.
Interaction needs no setup. On BeginPlay the component asks for an event system and a
UDreamWorldSpaceRaycaster for each local player and supplies whichever is missing, so pressing Play
is enough to click a button hanging in the world. The raycaster points either from the cursor or from
the middle of the screen (PointerSource), and bOccludeByWorld makes solid geometry block a click
the way it blocks a line trace. It is on by default: whatever blocks the raycaster's TraceChannel
(Visibility) stops the pointer, and the actor it hits is handed the pointer's events through the
pointer interfaces, which is also how a render-target surface on a mesh is clicked. To click through
walls instead, untick it on a raycaster of your own, or call SetOccludeByWorld(false) on the
player's. Put a raycaster of your own on any actor with the same user index and nothing is added on
top of it — the test is for one that exists, not for one this plugin made.
From code it is the two calls that were already there: ConstructWidget, then
AttachWidgetToSceneComponent on whatever component should carry the tree.
ADreamWorldWidgetActor can be possessed by a Level Sequence directly, and WidgetOpacity,
WidgetOffset and bWidgetVisible on the component are keyable from it.
A timeline block is an animation the language owns:
timeline Pulse {
duration = 0.6
loop = PingPong
Icon.RenderScale : 0.0 = (1, 1, 1), 0.3 = (1.25, 1.25, 1) ease InOutQuad, 0.6 = (1, 1, 1)
Row/Title.RenderTranslation : 0.0 = (-40, 0, 0), 0.2 = (0, 0, 0) ease OutCubic
@0.3 -> Landed
}
One line per track: a path of node ids, the property it drives, and the keys. A line with no path at
all (RenderScale : ...) drives the widget the animation lives on. The path is the same display-name
path an animation binding already resolves through; the values are spelled the way they are
everywhere else.
What a track may drive is exactly what the animation editor offers: a property marked Interp,
named either by its own name (RenderScale, Color) or by the label on its row (Width,
Height -- those are the Animatable* mirrors that exist so the anchor block can be keyed, since
a struct has no property track). One list, so a line that compiles is a track you can see. Note the
asymmetry with assignment, which is deliberate: Width = 400 is still DUI4001 pointing at
AnchorData.SizeDelta, because an assignment writes a property and a timeline drives a track.
Easing is a name out of EDreamTweenEase -- one word list for the whole plugin -- never a
tangent quadruple: tangents are stored data, and a text form that expressed them would be
unwritable by hand and lossy to read back. @<time> -> Name is a key on the block's event track,
broadcast through the component's OnAnimationEvent.
The block compiles into a UDreamWidgetAnimation in the root's animation component, gets its class
member variable like any other animation, and plays through the same entry points. Because the FILE
owns it, every compile rebuilds it and the animation editor opens it read-only -- edit the
.dui, or hand the animation to Sequencer for good:
timeline Celebrate external
external builds nothing. It is a manifest entry: the animation lives in the asset, Sequencer edits
it freely, and the file still lists it -- which is what makes "what animations does this class have"
answerable by reading the file. Material-parameter tracks, hand-shaped curves and anything else layer
one cannot express stay external by design; a compile warns when an animation is not listed, and
when a listed one does not exist.
One mechanism, spelled the same way everywhere: a route.
Confirm : UIButton {
OnClick -> HandleConfirm
}
EventName -> Handler names a function on the user widget — the class the tree compiles into —
which is where UMG puts event handling too. The compiler resolves every route into
UDreamWidgetBlueprint::EventBindings and UDreamUserWidget::BindEventBindings attaches them at
Initialize. In the designer the same thing is the Events section of the details panel: the +
creates a custom event with the right signature and the route that names it.
A route reaches both kinds of event this plugin has — the BlueprintAssignable dynamic multicast
delegates the Controls/ family declares, and the FDreamUIEventDelegate properties the older
Interaction/ behaviours declare.
Note
FDreamUIEventDelegate's own per-instance event list is legacy and read-only. Bindings saved
in it still fire, and can still be removed from the panel, but new ones are not authored there: a
binding of that kind calls a function on an arbitrary object with a literal argument, which UMG has
no equivalent of and the .dui has no syntax for. Opening a panel that holds one logs a warning
naming it. Nothing is rewritten automatically — turning "call Foo on that behaviour with this
value" into "call a handler on the user widget" would change what the game does — so re-author
those as routes when you touch them.
Asset format, all additive and back-compatible: FDreamUIEventDelegateData gained
HelperComponentIndex (a behaviour's position in the widget's component array, which is now the key
— HelperComponentName is still read for assets saved before it and the position is recorded the
first time that name resolves), and StructValue + StructValueType for the new Struct
parameter type, which carries any USTRUCT as exported text. Older assets load unchanged.
Two console commands answer both questions without a debugger:
DreamUI.Capture [Directory]writes a PNG of the world's viewport and of every root canvas that renders into a target — by default into a new folder underSaved/DreamUI/Captures. The same thing is a Blueprint library,UDreamUICaptureLibrary:SaveViewportToPng,SaveCanvasToPng,SaveRenderTargetToPng,CaptureAll. A canvas drawn straight onto the screen has no picture of its own; capture the viewport it is on.DreamUI.Statsprints what the frames since the lastDreamUI.Statscost, stage by stage — the UI manager's tick, canvas updates, batching, draw-call submission, the render thread's recording — and how many batches, vertices and bytes went to the GPU. Each stage is also a named scope in Unreal Insights (DreamUI_*).
A canvas answers the parameters it gives its own materials through render-thread proxies of the material — it makes no material instance per draw call — and a render-target canvas is drawn by a render command of its own, whether or not anything renders its world. The switches that put the old ways back for a while are gone with them.
For a report of something drawn wrong or not drawn, r.DreamUI.VerifyPartialPrepare 1 checks every
prepare a canvas makes from its last one against a prepare of every widget, and a difference is an
ensure that names the canvas. r.DreamUI.VerifyKeptPointers 1 does the same for the objects DreamGUI
keeps instead of looking them up every frame -- a widget's canvas, a canvas's render layers, the UI
manager's canvases, an animated property's object: each use looks the object up as well, and a
disagreement is an ensure that says which. The test suite runs with both on.
Tools/Bench holds the benchmarks the performance work was measured with -- a screen of 5000 turning
buttons and a level of 2688 world-space panels -- in PIE or in a -game process, with scripts that
read the CSV profiles and traces they leave (Tools/Bench/README.md).
What is claimed and what has been run are different lists, so both are here.
| Builds | Verified | |
|---|---|---|
| Win64 | yes | yes — the editor and the whole automation suite |
| Mac, Linux | yes | no — no machine here to run them on |
| iOS, Android | yes | no — never run on a device |
| Dedicated server | see below | no — this project has no server target to build |
The per-module PlatformAllowList and the descriptor's SupportedTargetPlatforms name the five
platforms the code is written for, and they are kept in step with each other by a test. They are a
statement about what compiles and what gets cooked, not a claim that anyone has shipped on them.
Mobile is better supported than "untested" suggests, and worse than "supported" would: touch is
routed end to end (BindTouch → the standalone input module), the virtual keyboard is implemented
(FPlatformApplicationMisc::RequiresVirtualKeyboard → ShowVirtualKeyboard), and the one live
platform branch in the renderer flips culling for Android GLES. MSAA is not available on GLES, and
the renderer now falls back rather than pretending — see AntiAliasingMethod.
Dedicated server. There is no server target in this project, so the honest statement is that the
server configuration has never been compiled. What has been done is the part that can be checked by
reading: Config/DefaultEngine.ini-style guesses are not involved, the #if !UE_SERVER blocks are
balanced, and — the thing that actually breaks a server build — no member or function referenced
outside a server guard is declared inside one. UDreamUMGWidget is the only class with UE_SERVER
blocks, and every field they touch (SlateWindow, SlateWidget, WidgetRenderer) is declared
unconditionally. WITH_FREETYPE=0 / WITH_HARFBUZZ=0 are handled the same way: every function the
text path exposes is defined unconditionally with the body guarded, so nothing goes undefined at
link time. Six interaction subsystems already decline to exist on a server
(ShouldCreateSubsystem → !IsRunningDedicatedServer()).
1480 automation tests are declared — run them with Automation RunTests DreamGUI, or a preset of
Tools/Tests/Invoke-DreamGUITests.ps1. There were none before this fork.
Known gaps:
LineHeightPercentageandWrapTextAtare only reachable through a real font asset, so they are not covered by tests.- A panel that offers a wrapping text no width -- a horizontal box measuring along its own axis -- is answered with the paragraph's one-line width; the text wraps once it is arranged narrower than that. (Offered a width, as a vertical box offers its own, a wrapping text answers with its height at that width.)
- One content asset still carries
Lexin its name (Content/Blueprints/LexEventSystemActor_EnhancedInput). Renaming a.uassetfile does not rename the object inside it, so only an editor-side rename can change it; the code points at what is actually on disk. - The editor work is verified by tests, not by eye. Expect rough edges in the designer.
MIT — see LICENSE.
Copyright (c) 2026-present TypeDreamMoon Copyright (c) 2019-present Lex Liu
Substantial portions of this software remain the work of Lex Liu and are used under the MIT terms of the original project. The MIT notice must travel with any copy or substantial portion of this code, including yours.