Skip to content

Commit cf20027

Browse files
authored
Pursche/indirect UI renderer (#65)
* Make CanvasRenderer use IndirectDraw Merge Panel and Text into single renderpipeline Add CPU sorting of CanvasRenderer drawcalls Move CanvasRenderer to indirect drawcalls Considered using GPU sorting but CPU was faster Add sorting test to UI/Demo.luau Fix key mapping mixup in Input.luau Fix hardcoded Light color Add GPU RadixSort utils, currently unused Get rid of old GPU FFX ParallelSort * Update Engine submodule
1 parent 460e01e commit cf20027

28 files changed

Lines changed: 1980 additions & 1280 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@ CMakeSettings.json
3939

4040
# Exceptions
4141
.cache/
42-
*.patch
42+
*.patch
43+
.claude/
44+
images/

Source/Game-Lib/Game-Lib/ECS/Components/UI/Widget.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ namespace ECS::Components::UI
3939
WidgetFlags flags = WidgetFlags::Default;
4040
u32 worldTransformIndex = std::numeric_limits<u32>().max();
4141

42+
// Packed draw-order sortkey computed by CanvasRenderer. See CanvasRenderer::DfsAssignSortKey for the layout.
43+
// Sibling-order tiebreaker lives on SceneNode2D as siblingIndex (monotonic per-parent).
44+
u32 sortKey = 0;
45+
4246
Scripting::UI::Widget* scriptWidget = nullptr;
4347

4448
// Non mutable helper functions
@@ -55,4 +59,12 @@ namespace ECS::Components::UI
5559
struct DirtyWidgetClipper {};
5660
struct DirtyWidgetWorldTransformIndex {};
5761
struct DestroyWidget {};
62+
63+
// Marks a canvas whose widget subtree needs its sortKeys recomputed by CanvasRenderer.
64+
struct DirtyCanvasSort {};
65+
66+
// Registry-context singleton: set when the SET of canvases (or a canvas's layer) changes,
67+
// so CanvasRenderer knows it needs to re-rank canvasOrder before re-running DfsAssignSortKey.
68+
// Cleared inside CanvasRenderer::Update after RebuildCanvasOrder runs.
69+
struct DirtyCanvasOrderFlag {};
5870
}

Source/Game-Lib/Game-Lib/ECS/Util/Transform2D.h

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,10 @@ namespace ECS::Components
287287
prevSibling->nextSibling = nextSibling;
288288
nextSibling->prevSibling = prevSibling;
289289

290+
// If we were the head of the list, the new head is the next sibling
291+
// (which preserves insertion order: the second-inserted child becomes first).
290292
if (parent->firstChild == this)
291-
parent->firstChild = prevSibling;
293+
parent->firstChild = nextSibling;
292294
}
293295

294296
nextSibling = nullptr;
@@ -312,14 +314,20 @@ namespace ECS::Components
312314
}
313315
else
314316
{
315-
//insert after the firstchild
316-
nextSibling = newParent->firstChild->nextSibling;
317-
prevSibling = newParent->firstChild;
317+
// Append to the END of the circular sibling list (i.e. insert just before firstChild).
318+
// This makes iteration order match insertion order, so siblings are drawn in the order they were created.
319+
nextSibling = newParent->firstChild;
320+
prevSibling = newParent->firstChild->prevSibling;
318321

319322
prevSibling->nextSibling = this;
320323
nextSibling->prevSibling = this;
321324
}
322325
parent = newParent;
326+
327+
// Assign a unique-within-current-siblings index. Using a monotonic counter on
328+
// the parent rather than parent->children guarantees uniqueness even after
329+
// detach+reattach cycles (where children decrements but nextSiblingIndex does not).
330+
siblingIndex = newParent->nextSiblingIndex++;
323331
}
324332

325333
//updates transform matrix of the children. does not recalculate matrix
@@ -385,6 +393,21 @@ namespace ECS::Components
385393
SceneNode2D* nextSibling{};
386394
SceneNode2D* prevSibling{};
387395
i32 children{ 0 };
396+
397+
// Monotonic per-parent counter. Bumped each time a child is attached; used
398+
// to assign a unique siblingIndex that never collides with concurrent siblings,
399+
// even after detach/reattach cycles on the same parent. u32 so wraparound is
400+
// irrelevant at any realistic UI churn rate.
401+
u32 nextSiblingIndex{ 0 };
402+
// Unique index within this node's current parent. Set by SetParent. Used as
403+
// the tiebreaker when two siblings have the same Z in the draw sort.
404+
u32 siblingIndex{ 0 };
405+
406+
public:
407+
u32 GetSiblingIndex() const
408+
{
409+
return siblingIndex;
410+
}
388411
};
389412
}
390413

Source/Game-Lib/Game-Lib/ECS/Util/UIUtil.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,42 @@ namespace ECS::Util
3434
{
3535
namespace UI
3636
{
37+
entt::entity FindOwningCanvas(entt::registry* registry, entt::entity entity)
38+
{
39+
if (entity == entt::null)
40+
return entt::null;
41+
42+
auto* widget = registry->try_get<ECS::Components::UI::Widget>(entity);
43+
if (!widget)
44+
return entt::null;
45+
46+
if (widget->type == ECS::Components::UI::WidgetType::Canvas)
47+
return entity;
48+
49+
if (widget->scriptWidget)
50+
return widget->scriptWidget->canvasEntity;
51+
52+
return entt::null;
53+
}
54+
55+
void MarkCanvasSortDirty(entt::registry* registry, entt::entity canvasEntity)
56+
{
57+
if (canvasEntity == entt::null)
58+
return;
59+
registry->emplace_or_replace<ECS::Components::UI::DirtyCanvasSort>(canvasEntity);
60+
}
61+
62+
void MarkAllCanvasSortDirty(entt::registry* registry)
63+
{
64+
registry->view<ECS::Components::UI::Canvas>().each([&](entt::entity canvasEntity, auto&)
65+
{
66+
registry->emplace_or_replace<ECS::Components::UI::DirtyCanvasSort>(canvasEntity);
67+
});
68+
// The canvas SET changed -> canvasOrder ranking is stale; gates the (relatively
69+
// expensive) RebuildCanvasOrder pass next time CanvasRenderer::Update runs.
70+
registry->ctx().emplace<ECS::Components::UI::DirtyCanvasOrderFlag>();
71+
}
72+
3773
entt::entity GetOrEmplaceCanvas(Scripting::UI::Widget*& widget, entt::registry* registry, const char* name, vec2 pos, ivec2 size, bool isRenderTexture)
3874
{
3975
ECS::Singletons::UISingleton& uiSingleton = registry->ctx().get<ECS::Singletons::UISingleton>();
@@ -109,6 +145,11 @@ namespace ECS::Util
109145
registry->emplace<ECS::Components::UI::CanvasRenderTargetTag>(entity);
110146
}
111147

148+
// A new canvas entering the system shifts canvasOrder for everyone;
149+
// mark every canvas (including this one) so all widget sortKeys get their
150+
// canvasOrder bits refreshed on the next CanvasRenderer::Update tick.
151+
MarkAllCanvasSortDirty(registry);
152+
112153
return entity;
113154
}
114155

@@ -201,6 +242,9 @@ namespace ECS::Util
201242
eventInputInfo.onFocusEndEvent = panelTemplateComp.onFocusEndEvent;
202243
eventInputInfo.onFocusHeldEvent = panelTemplateComp.onFocusHeldEvent;
203244

245+
// New widget entering the tree -> owning canvas needs sort-key rebuild.
246+
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, parent));
247+
204248
return entity;
205249
}
206250

@@ -285,6 +329,9 @@ namespace ECS::Util
285329
eventInputInfo.onFocusEndEvent = textTemplate.onFocusEndEvent;
286330
eventInputInfo.onFocusHeldEvent = textTemplate.onFocusHeldEvent;
287331

332+
// New widget entering the tree -> owning canvas needs sort-key rebuild.
333+
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, parent));
334+
288335
return entity;
289336
}
290337

@@ -311,6 +358,9 @@ namespace ECS::Util
311358
widgetComp.type = ECS::Components::UI::WidgetType::Widget;
312359
widgetComp.scriptWidget = widget;
313360

361+
// New widget entering the tree -> owning canvas needs sort-key rebuild.
362+
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, parent));
363+
314364
return entity;
315365
}
316366

@@ -319,6 +369,10 @@ namespace ECS::Util
319369
if (!registry->all_of<ECS::Components::UI::Widget>(entity))
320370
return false;
321371

372+
// Widgets leaving the tree changes the sibling set in their owning canvas.
373+
// Mark it dirty BEFORE we mutate the scriptWidget or clear the parent, so FindOwningCanvas still resolves.
374+
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, entity));
375+
322376
auto& transform2DSystem = Transform2DSystem::Get(*registry);
323377
transform2DSystem.ClearParent(entity);
324378

@@ -382,6 +436,10 @@ namespace ECS::Util
382436
CallLuaEvent(eventInputInfo->onFocusBeginEvent, Scripting::UI::UIInputEvent::FocusBegin, widget.scriptWidget);
383437
}
384438
}
439+
440+
// Focus affects sortKey (priority bits), so both the previously focused and the newly focused widget's canvases need their sortKeys rebuilt.
441+
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, oldFocus));
442+
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, entity));
385443
}
386444

387445
entt::entity GetFocusedWidgetEntity(entt::registry* registry)

Source/Game-Lib/Game-Lib/ECS/Util/UIUtil.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ namespace ECS::Util
3636
void FocusWidgetEntity(entt::registry* registry, entt::entity entity);
3737
entt::entity GetFocusedWidgetEntity(entt::registry* registry);
3838

39+
// Returns the canvas entity that owns the given widget entity (the widget itself if it IS a canvas).
40+
// Walks the scriptWidget->canvasEntity chain; returns entt::null if the entity has no Widget component.
41+
entt::entity FindOwningCanvas(entt::registry* registry, entt::entity entity);
42+
43+
// Mark a single canvas as needing its widget sort-keys recomputed (by CanvasRenderer::Update next frame).
44+
// Safe to call with entt::null; becomes a no-op.
45+
void MarkCanvasSortDirty(entt::registry* registry, entt::entity canvasEntity);
46+
47+
// Mark every canvas in the registry as needing sort-keys recomputed. Used when the set of canvases itself
48+
// changes (new canvas, canvas SetLayer) so that canvasOrder bits are refreshed everywhere.
49+
void MarkAllCanvasSortDirty(entt::registry* registry);
50+
3951
void RefreshText(entt::registry* registry, entt::entity entity, std::string_view newText);
4052
void RefreshTemplate(entt::registry* registry, entt::entity entity, ECS::Components::UI::EventInputInfo& eventInputInfo);
4153
void RefreshClipper(entt::registry* registry, entt::entity entity);

0 commit comments

Comments
 (0)