Skip to content

Refactor material graph generation around polymorphic nodes - #34

Closed
MrChampz wants to merge 81 commits into
feature/mat-systemfrom
feature/mat-nodes
Closed

MrChampz wants to merge 81 commits into
feature/mat-systemfrom
feature/mat-nodes

Conversation

@MrChampz

Copy link
Copy Markdown
Owner

Summary

  • Replace material graph node data and switch-based emission with polymorphic material node classes
  • Add node-owned input definitions, validation, and HLSL emission
  • Update default and particle materials to construct and connect typed nodes
  • Centralize graph validation and preserve channel type coercion

Testing

  • Not run (not requested)

…sizing

Pulls together the four architecture points from the GUI review:

- Hit-testing & input routing: Widget gains HitTest/GetChildCount/GetChildAt
  (ForEachChild is now built on these, non-virtual) and handlers return
  SInputReply instead of void, so Manager can bubble a press/release/move
  along the actual top-to-bottom hit path with mouse capture, instead of
  broadcasting to every widget under the cursor. EVisibility grows
  HitTestInvisible/SelfHitTestInvisible/Collapsed alongside Visible/Hidden.
- Measure pass: ComputeDesiredSize now takes the available space and is
  driven through a cached, non-virtual Widget::Measure, so containers no
  longer re-measure clean subtrees on every arrange. TextBlock/FontManager
  gain wrap-aware measurement (ETextOverflow, MeasureWrapped).
- Z-order runs: RenderBatch::Sort groups commands into per-type SBatchRun
  ranges after sorting by ZOrder, and Renderer walks them in that order
  (SDrawItem/AppendRange/firstInstance) instead of one pass per command
  type, so quads and text interleave correctly by z instead of text always
  drawing on top.
- Typed slot sizing: LayoutSlot's m_FillRatio becomes SSizeParam
  (Auto/Fill/Fixed) with corrected space distribution (fixed space first,
  remaining space split by fill ratio), EHorizontalAlignment/
  EVerticalAlignment gain Fill for the cross axis, and Panel/TPanel<TSlot>
  replace the untyped Ref<Slot> vector + static_pointer_cast in every
  container with a typed slot list (TPanel<LayoutSlot>/TPanel<CanvasSlot>,
  explicitly instantiated and exported so client code can derive from them
  across the DLL boundary).

Test fixtures (WidgetTestUtils, DrawCacheTest, ForEachChildTest,
InvalidationTest, DirtyTrackingTest, WidgetLifetimeTest) updated to match:
new ComputeDesiredSize signature, TPanel<LayoutSlot> instead of hand-rolled
Panel subclasses, GetSlotCount() instead of GetSlots(), and slot-level
SetFillSize() instead of the removed per-panel SetStretching().
- Clip stack: Widget::ClipsChildren (default false) and a clipRect param
  threaded through CollectDrawCommands, intersected at RenderBatch::Append
  time rather than baked into BuildDrawCommands - so scrolling a ScrollBox
  doesn't have to invalidate every descendant's cached draw commands, only
  reassemble the frame. SRect::Intersect added to Definitions.h as the
  supporting primitive.
- ScrollBox (new): a ContentWidget that clips its content, measures it
  unconstrained on the scrolling axis/axes and capped to the viewport on
  the other, clamps the scroll offset to content-minus-viewport, and draws
  an optional track+thumb scrollbar. Routes MouseScrolledEvent through the
  same SInputReply convention as the rest of the input handlers.
- Popup layers: Manager gains a layer stack (SLayer) above the always-
  present root layer - PushPopup/PopPopup/ClearPopups, hit-tested and
  dismissed independently (click outside closes it), arranged and rendered
  through the same per-subtree z-banding as everything else so a popup
  always ends up on top without a magic z offset. A layer-stack version
  counter covers the case a freshly opened popup starts dirty by
  construction without ever calling MarkLayoutDirty.
The if-init-statement's condition was `++i`, incrementing the loop
counter a second time (the for-loop already does it) and using the
post-increment's truthiness as the guard instead of checking whether
GetChildAt actually returned a widget. Net effect: every other child
was skipped, and a null child would never have been filtered anyway.
Drop the second increment and check the child itself.
- ClipStackTest: clip only propagates from a ClipsChildren() container,
  nested clips intersect rather than replace, an inherited clip narrows
  an existing ad-hoc scissor instead of overwriting it, and a direct
  regression test that ScrollBox actually scissors oversized content end
  to end via AssembleFrame (ClipsChildren() on it was silently false at
  one point, with nothing catching it).
- ScrollBoxTest: desired size never exceeds the configured viewport but
  shrinks to smaller content, LayoutChildren offsets content by the
  scroll offset, SetScrollOffset clamps both directions, and
  HandleMouseScrolled moves the offset within bounds (Handled) or
  defers to an ancestor at the edge (Unhandled).
- PopupLayerTest: push/pop/clear track the popup count correctly
  (including no-op pop on an empty stack), a freshly pushed popup is
  arranged immediately against the last known extent, its draw commands
  sort strictly above the root layer's after AssembleFrame - the core
  guarantee of the popup stack - and a clip on one layer doesn't leak
  into another.
- WidgetLifetimeTest: RemoveChild is protected on Widget/Panel with no
  outside production caller; the one test exercising it now drives it
  through a local TPanel<LayoutSlot> test double that promotes it via
  `using`, the same idiom ForEachChildTest already uses, instead of
  relying on VerticalBox exposing it publicly.
…ren does

VerticalBox/HorizontalBox::ComputeDesiredSize summed each child's raw
Measure() result along the main axis regardless of its slot's sizing rule,
while LayoutChildren correctly used the slot's configured pixel size for
Fixed children and gave Fill children no intrinsic contribution. A Fixed
child whose content measured smaller than its configured size made the
container under-report its own desired size.

Found by live-debugging a real symptom: a ScrollBox wrapping a VerticalBox
of fixed-height rows measured the list as shorter than its actual laid-out
height, concluded nothing overflowed the viewport, and silently refused to
scroll or show a scrollbar - even though the rows were visibly clipped.

Fix: ComputeDesiredSize now switches on GetSizeRule() the same way
LayoutChildren already does. Regression tests confirm the old code was
wrong (3 of 4 new tests fail against it) and pass against the fix.
… space

- TPanel<TSlot>::AddChild is now virtual, so a concrete container (Canvas)
  can override it instead of just hiding the base via name lookup.
- Canvas/ScrollBox's configured-size setter is now SetSize on both (was
  SetDesiredSize), matching the rest of the widget setter naming; backing
  fields renamed to m_Size for the same reason. Canvas's default changed
  from 800x600 to 100x100.
- Canvas::ComputeDesiredSize now clamps to glm::min(m_Size, availableSize)
  instead of returning m_Size unconditionally - Canvas was the one
  container that never checked whether the parent actually had room,
  unlike ScrollBox which already followed this rule.

Test fixtures and the Editor's example dropdown menu updated for the
renamed setter; CanvasTest gains coverage for the default size, the
setter (including the same-value no-invalidate guard), and the new
available-space clamp - confirmed the clamp test fails without the fix.
…etry

Outline previously expanded past the widget's own m_Geometry in the
vertex shader and drew in the [0, thickness] band outside the shape,
so two Fill-sized siblings with zero gap between them would have their
borders bleed into each other.

Flip the outline band to [-thickness, 0] (inside the shape) and draw
it after the fill/inset-shadow so it sits on top like a real border,
and stop expanding the quad for it in the vertex shader - only drop
shadow still needs that. Widget::Measure now budgets outline thickness
into the desired size it reports, so a widget that wants a 10x10
content box with a 1px outline actually occupies 12x12 when auto-sized,
instead of the outline eating into its own content.
Content was always measured and arranged at the ScrollBox's full
width/height, never leaving room for the scrollbar it draws over the
edge - so content with a background reaching that edge (e.g. a
section header) rendered on top of the scrollbar instead of under it,
and z-order meant it always would, since scrolled content is a child
and children draw above their parent's own commands.

Add ScrollBox::CrossAxisSpace, subtracting the scrollbar's thickness
from whichever axis it actually occupies whenever m_ShowScrollbar is
set, and use it in both ContentMeasureConstraint (what content thinks
it has) and LayoutChildren (what it's actually arranged into) - the
second one was missing before, so even content that measured itself
narrower was stretched back out to the full width at arrange time.
m_DPIScale was stored but never read - QuadRenderPass and
TextRenderPass both scale cmd.Geometry.Position/Size by it before
building vertex data, but DebugRenderPass used the raw logical-point
geometry, so a debug rect (e.g. the focus ring) landed at half its
intended screen position on a 2x display.
Widget gains an IsFocusable/SetFocusable flag, independent of mouse-
click focus (Manager::ProcessMousePress already focuses whatever
widget consumes a mouse-down regardless of this flag) - it only
controls membership in the keyboard-navigable Tab order. TextField
opts itself in, since it already reacts visibly to focus.

Manager::HandleKeyPressed intercepts Tab/Shift+Tab and Escape before
bubbling to the focused widget, not after: TextField::HandleKeyPressed
unconditionally returns Handled() for every key code, so a focused
field would swallow Tab silently forever if this checked the bubble
result first. FocusNext/FocusPrevious walk a lazily-rebuilt, depth-
first focus order cached behind the same dirty-epoch + layer-stack-
version key NeedsRebuild already uses, scoped to the topmost popup
layer so Tab never escapes an open popup into whatever is underneath
it - and deliberately don't clear focus when that scoped order is
empty, so a popup with no focusable content of its own can't silently
steal focus from the layer below.

The focus ring itself reuses AddDebugRect's existing DEBUG_Z_ORDER
sentinel (always-on-top, immune to any ScrollBox/popup clip in
effect) rather than inventing a second z-layering mechanism alongside
the popup stack's.

(Widget::SetFocusable's definition landed in the prior outline-fix
commit by accident - a `git commit <pathspec>` pathspec commits the
working tree content of that path, not what was staged via
`git apply --cached`, so it pulled in more than intended. No
functional impact, just a messier split than planned.)

Promotes SetFocusedWidget/ProcessMousePress/HandleKeyPressed from
private to protected on Manager so tests can drive them directly,
matching the existing AssembleFrame/NeedsRebuild/MarkRebuilt pattern
in ManagerTestUtils.h, and adds FocusTest.cpp covering Tab ordering,
wrap-around both directions, Escape, popup scoping (both directions -
staying inside an open popup, and not leaking out when it has nothing
focusable), and the pre-existing click-outside-clears-focus behavior
as a regression guard.
Focus visualization is a per-widget concern, not something the
Manager should impose uniformly: a widget that wants to show its own
focus state already has everything it needs (IsFocused(), already
invalidated on change via HandleFocus/HandleLostFocus calling
MarkRenderDirty) to render it inside its own BuildDrawCommands, the
same way Button already reacts to m_Hovered - color swap, outline,
background texture, whatever fits the widget. No opt-in/opt-out flag
on Widget either: a widget that doesn't render anything for its own
focused state simply has no focus visual, rather than the base class
carrying a default every widget has to actively suppress.
Splits the background texture into m_NormalBackground/
m_FocusedBackground and adds a separate m_FocusedOutline, then
BuildDrawCommands picks between them based on m_Focused - the same
per-widget pattern Button already uses for m_Hovered, and the first
real consumer of the "no default focus visual" principle from the
prior commit: TextField opts in by drawing something specific for its
own focused state, texture or outline, instead of relying on any
Manager-level fallback.
…sabled)

Adds StyleSet on the Widget base: four style layers (Normal < Hovered
< Pressed < Disabled) composed field-by-field, so a later active layer
only overrides what it explicitly declares - a Pressed override that
changes just the color still shows whatever Hovered or Normal set for
texture, borders, corner radius, etc. BackgroundTexture uses
optional<Ref<Texture2D>> specifically so a layer can distinguish "I
didn't touch this" from "clear the inherited texture" (a non-null but
empty Ref).

GetInteractionState()/GetResolvedStyle() live on Widget as protected,
built from the widget's own IsHovered/IsPressed/IsEnabled - available
to any concrete widget's BuildDrawCommands, not just Button. The
mutators (SetStyle/ClearStyle/SetBackgroundColor/SetForegroundColor/
SetBackgroundTexture/ClearBackgroundTexture) are public on Widget,
following the same precedent as SetOutline/SetInsetShadow/
SetDropShadow already being public and simply inert on widgets that
never draw with them.

Widget also gains IsEnabled/SetEnabled: disabling marks render-dirty,
cancels an in-progress press, and blocks HandleMouseDown/HandleClick
- without touching EVisibility or layout.

Outline/InsetShadow/DropShadow stay single-value widget properties
(via the pre-existing SetOutline/SetInsetShadow/SetDropShadow) rather
than becoming per-layer overrides: GetResolvedStyle() overlays them
from the widget's own getters after resolving, so they don't vary by
interaction state. Avoids a second source of truth for the same
value, at the cost of those three fields in SStyleOverride never
actually taking effect if set through SetStyle directly.

Migrates Button off its old m_NormalColor/m_HoverColor/
m_NormalBackground fields onto this API, keeping only its own thin
wrappers (SetCornerRadius, SetBackgroundBorders, SetTextColor) on top
of the inherited one; no compatibility shims kept, since the only
three call sites (Application.cpp) get updated directly in the same
change. Button::HandleMouseDown also gains the IsEnabled() check it
was missing before.

Adds StyleTest.cpp: StyleSet::Resolve in isolation (Normal-only,
partial Hovered override, Pressed-over-Hovered, Disabled-over-both,
Disabled falling back when it declares nothing, explicit texture
clear, an inactive layer never leaking into the result) plus Widget's
public surface (style/hover/press/enabled changes mark render-dirty;
disabling blocks input that would otherwise be handled).
…errides

GetResolvedStyle() previously overlaid Outline/InsetShadow/DropShadow
from Widget's own single-value getters after resolving m_Styles, so
those three SStyleOverride fields never actually took effect - setting
them through SetStyle/SetOutline/SetInsetShadow/SetDropShadow was a
silent no-op. Removes the single-value m_Outline/m_InsetShadow/
m_DropShadow fields and their old setters/getters entirely; every
setter (SetOutline, SetOutlineColor, SetOutlineThickness,
SetInsetShadow and its per-component variants, SetDropShadow and its
per-component variants) now takes an EStyleLayer, reads the layer's
current override, patches just the one field it owns, and writes it
back - so a widget can genuinely give Hovered a different outline
than Normal. Also adds SetCornerRadius/SetBackgroundBorders directly
on Widget for the same reason Button no longer needs its own copies
of them.

Migrates the only call sites (Application.cpp, Button's own
SetTextColor wrapper) to the new per-layer signatures.
Both had their own single-value background/corner-radius/text-color
fields, parallel to (and now stale next to) the base Widget's
StyleSet - Panel::BuildDrawCommands and TextField::BuildDrawCommands
were still reading m_Outline/m_InsetShadow/m_DropShadow directly,
which the previous commit emptied out entirely (those became per-
layer-only), so any Outline set through the new API on a Canvas,
Overlay or TextField was silently never drawn.

Panel (base of Canvas/Overlay/HorizontalBox/VerticalBox) drops its own
m_Background/m_CornerRadius and their accessors; BuildDrawCommands now
resolves through GetResolvedStyle() like Button already does.

TextField drops m_TextColor/m_BackgroundColor/m_CornerRadius/
m_BackgroundBorders/m_NormalBackground and their setters, resolving
the same way for its Normal/Hovered/Pressed/Disabled state; keeps
m_FocusedBackground/m_FocusedOutline as its own fields, since Focused
isn't one of StyleSet's four layers (see EStyleLayer) - the resolved
style still supplies background color/corner radius/borders/shadows
even while focused, only the texture/outline branch differs. Keeps a
thin SetTextColor(layer, color) wrapper renaming SetForegroundColor,
matching Button's own precedent.

Fixes the three existing tests that called the old single-argument
Widget::SetBackground.
Precedence becomes Normal < Hovered < Pressed < Focused < Disabled -
Disabled still wins even over a widget that stays focused after being
disabled (nothing clears focus just because IsEnabled() went false).
Widget::GetInteractionState() now includes IsFocused().

TextField drops m_FocusedBackground/m_FocusedOutline and their setters
entirely: the constructor sets a default outline on the Focused layer
via SetStyle the same way it already does for Normal, and
BuildDrawCommands loses its whole if (m_Focused) {...} else {...}
branch - GetResolvedStyle() already picks the right layer on its own.
Background color, corner radius, borders and shadows stay whatever
Normal/Hovered/Pressed resolved to while focused, since Focused's
default only declares Outline.
Widget::SetEnabled(false) has no way to clear Manager::m_FocusedWidget
- Widget has no back-reference to the Manager - so a widget disabled
while focused stays focused, just Disabled-styled. That left two real
gaps in its "stops accepting input events" contract:

- Manager::HandleKeyPressed/HandleKeyTyped kept bubbling from
  m_FocusedWidget regardless of IsEnabled(), so a disabled TextField
  that was focused before being disabled would keep taking keystrokes.
- Manager::CollectFocusOrder never checked IsEnabled(), so Tab could
  land inside a disabled widget even though a mouse click already
  can't (Widget::HandleMouseDown's own IsEnabled() check).

Both now guard on IsEnabled(). FocusNext/FocusPrevious already handle
the resulting case correctly for free: once a focused widget drops out
of the (now enabled-only) order, they treat it as "not in the order"
and jump to the first entry instead of stepping from a stale position.

Verified both new FocusTest cases actually catch the bug: reverted the
Manager.cpp guards, confirmed both failed, restored the fix.
…alette

Give Button and TextField a real default visual identity across every
StyleSet layer (Normal/Hovered/Focused/Disabled) instead of placeholder
dev colors, matching chakra-ui.com's "subtle" zinc button. Disabled uses
SColor alpha directly since the engine has no render-blended opacity.
Panel is intentionally left untouched - it backs Canvas/Overlay/H-V Box
as transparent layout scaffolding across the Editor.
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 139 files, which is 39 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 81fe0185-4b12-4e09-badd-0cfa2d171f70

📥 Commits

Reviewing files that changed from the base of the PR and between 35c6f38 and f2be3cc.

⛔ Files ignored due to path filters (10)
  • Shaders/Aether/Mesh.vs.hlsl is excluded by !**/*.hlsl
  • Shaders/Aether/Ribbon.vs.hlsl is excluded by !**/*.hlsl
  • Shaders/Aether/Sprite.ps.hlsl is excluded by !**/*.hlsl
  • Shaders/Aether/Sprite.vs.hlsl is excluded by !**/*.hlsl
  • Shaders/Material/Material.ps.hlsl is excluded by !**/*.hlsl
  • Shaders/Material/ParticleMesh.ps.hlsl is excluded by !**/*.hlsl
  • Shaders/Material/ParticleMesh.vs.hlsl is excluded by !**/*.hlsl
  • Shaders/Material/ParticleRibbon.ps.hlsl is excluded by !**/*.hlsl
  • Shaders/Material/ParticleRibbon.vs.hlsl is excluded by !**/*.hlsl
  • Shaders/Material/ParticleSprite.ps.hlsl is excluded by !**/*.hlsl
📒 Files selected for processing (139)
  • AGENTS.md
  • Assets/.gitignore
  • Assets/VFX/FireAndFireworks.json
  • Assets/VFX/RainStorm.json
  • Assets/VFX/RibbonGarden.json
  • Assets/VFX/RibbonVortex.json
  • Dissolve/Source/Dissolve.cpp
  • Dissolve/Source/Dissolve.h
  • Elixir/.clang-tidy
  • Elixir/Source/Engine.h
  • Elixir/Source/Engine/Aether/Core/ColorCurveStore.cpp
  • Elixir/Source/Engine/Aether/Core/ColorCurveStore.h
  • Elixir/Source/Engine/Aether/Core/CurveStore.cpp
  • Elixir/Source/Engine/Aether/Core/CurveStore.h
  • Elixir/Source/Engine/Aether/Core/ParameterStore.cpp
  • Elixir/Source/Engine/Aether/Core/ParameterStore.h
  • Elixir/Source/Engine/Aether/Core/Particle.h
  • Elixir/Source/Engine/Aether/Core/ParticleStateLayout.cpp
  • Elixir/Source/Engine/Aether/Core/ParticleStateLayout.h
  • Elixir/Source/Engine/Aether/Core/ResourceAllocation.h
  • Elixir/Source/Engine/Aether/Effect.h
  • Elixir/Source/Engine/Aether/Effect/Effect.cpp
  • Elixir/Source/Engine/Aether/Effect/Effect.h
  • Elixir/Source/Engine/Aether/Effect/MaterialDescription.h
  • Elixir/Source/Engine/Aether/Effect/MaterialFactory.cpp
  • Elixir/Source/Engine/Aether/Effect/MaterialFactory.h
  • Elixir/Source/Engine/Aether/Effect/MaterialResolver.cpp
  • Elixir/Source/Engine/Aether/Effect/MaterialResolver.h
  • Elixir/Source/Engine/Aether/Emitter.cpp
  • Elixir/Source/Engine/Aether/Emitter.h
  • Elixir/Source/Engine/Aether/FrameSubmission.h
  • Elixir/Source/Engine/Aether/Manager.cpp
  • Elixir/Source/Engine/Aether/Manager.h
  • Elixir/Source/Engine/Aether/Modules/Modules.cpp
  • Elixir/Source/Engine/Aether/Modules/Modules.h
  • Elixir/Source/Engine/Aether/Particle.h
  • Elixir/Source/Engine/Aether/ParticleResourcePool.h
  • Elixir/Source/Engine/Aether/ParticleStateLayout.h
  • Elixir/Source/Engine/Aether/Renderer.cpp
  • Elixir/Source/Engine/Aether/Renderer.h
  • Elixir/Source/Engine/Aether/Rendering/FrameSubmission.h
  • Elixir/Source/Engine/Aether/Rendering/Renderer.cpp
  • Elixir/Source/Engine/Aether/Rendering/Renderer.h
  • Elixir/Source/Engine/Aether/Rendering/SystemInstanceRenderProxy.cpp
  • Elixir/Source/Engine/Aether/Rendering/SystemInstanceRenderProxy.h
  • Elixir/Source/Engine/Aether/Rendering/SystemInstanceRetirementQueue.h
  • Elixir/Source/Engine/Aether/Runtime/InstanceRegistry.cpp
  • Elixir/Source/Engine/Aether/Runtime/InstanceRegistry.h
  • Elixir/Source/Engine/Aether/Simulation/RenderFrame.h
  • Elixir/Source/Engine/Aether/Simulation/ResourcePool.cpp
  • Elixir/Source/Engine/Aether/Simulation/ResourcePool.h
  • Elixir/Source/Engine/Aether/Simulation/Simulator.cpp
  • Elixir/Source/Engine/Aether/Simulation/Simulator.h
  • Elixir/Source/Engine/Aether/System.cpp
  • Elixir/Source/Engine/Aether/System.h
  • Elixir/Source/Engine/Aether/SystemInstance.cpp
  • Elixir/Source/Engine/Aether/SystemInstance.h
  • Elixir/Source/Engine/Core/Application.cpp
  • Elixir/Source/Engine/Core/Application.h
  • Elixir/Source/Engine/Core/Core.h
  • Elixir/Source/Engine/Core/UUID.h
  • Elixir/Source/Engine/Graphics/GraphicsContext.h
  • Elixir/Source/Engine/Material/DefaultMaterials.cpp
  • Elixir/Source/Engine/Material/DefaultMaterials.h
  • Elixir/Source/Engine/Material/Material.cpp
  • Elixir/Source/Engine/Material/Material.h
  • Elixir/Source/Engine/Material/MaterialCompilationCache.cpp
  • Elixir/Source/Engine/Material/MaterialCompilationCache.h
  • Elixir/Source/Engine/Material/MaterialCompiler.cpp
  • Elixir/Source/Engine/Material/MaterialCompiler.h
  • Elixir/Source/Engine/Material/MaterialFrameTable.cpp
  • Elixir/Source/Engine/Material/MaterialFrameTable.h
  • Elixir/Source/Engine/Material/MaterialGraph.cpp
  • Elixir/Source/Engine/Material/MaterialGraph.h
  • Elixir/Source/Engine/Material/MaterialInstance.cpp
  • Elixir/Source/Engine/Material/MaterialInstance.h
  • Elixir/Source/Engine/Material/MaterialNode.cpp
  • Elixir/Source/Engine/Material/MaterialNode.h
  • Elixir/Source/Engine/Material/MaterialRegistry.cpp
  • Elixir/Source/Engine/Material/MaterialRegistry.h
  • Elixir/Source/Engine/Material/MaterialRenderProxy.cpp
  • Elixir/Source/Engine/Material/MaterialRenderProxy.h
  • Elixir/Source/Engine/Material/MaterialRenderScene.cpp
  • Elixir/Source/Engine/Material/MaterialRenderScene.h
  • Elixir/Source/Engine/Material/MaterialRenderer.cpp
  • Elixir/Source/Engine/Material/MaterialRenderer.h
  • Elixir/Source/Engine/Material/MaterialResolver.h
  • Elixir/Source/Engine/Material/MaterialSystem.cpp
  • Elixir/Source/Engine/Material/MaterialSystem.h
  • Elixir/Source/Engine/Material/MaterialTextureRegistry.cpp
  • Elixir/Source/Engine/Material/MaterialTextureRegistry.h
  • Elixir/Source/Engine/Material/Nodes/BinaryOperationNode.h
  • Elixir/Source/Engine/Material/Nodes/CheckerboardNode.h
  • Elixir/Source/Engine/Material/Nodes/ComponentMaskNode.h
  • Elixir/Source/Engine/Material/Nodes/ConstantNode.h
  • Elixir/Source/Engine/Material/Nodes/FresnelNode.h
  • Elixir/Source/Engine/Material/Nodes/LerpNode.h
  • Elixir/Source/Engine/Material/Nodes/PannerNode.h
  • Elixir/Source/Engine/Material/Nodes/ParameterNode.h
  • Elixir/Source/Engine/Material/Nodes/RadialGradientExponentialNode.h
  • Elixir/Source/Engine/Material/Nodes/TexCoordNode.h
  • Elixir/Source/Engine/Material/Nodes/TextureSampleNode.h
  • Elixir/Source/Engine/Material/Nodes/TimeNode.h
  • Elixir/Source/Engine/Material/Nodes/UnaryOperationNode.h
  • Elixir/Source/Graphics/Vulkan/VulkanDescriptorPool.cpp
  • Elixir/Source/Graphics/Vulkan/VulkanDescriptorPool.h
  • Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.cpp
  • Elixir/Source/Graphics/Vulkan/VulkanGraphicsContext.h
  • Elixir/Tests/Engine/Aether/Effect/MaterialResolverTest.cpp
  • Elixir/Tests/Engine/Aether/FrameSubmissionTest.cpp
  • Elixir/Tests/Engine/Aether/Rendering/FrameSubmissionPublisherTest.cpp
  • Elixir/Tests/Engine/Aether/Rendering/FrameSubmissionTest.cpp
  • Elixir/Tests/Engine/Aether/Rendering/RendererTest.cpp
  • Elixir/Tests/Engine/Aether/Rendering/SystemInstanceRetirementQueueTest.cpp
  • Elixir/Tests/Engine/Aether/Runtime/InstanceRegistryTest.cpp
  • Elixir/Tests/Engine/Aether/Simulation/RenderFrameTest.cpp
  • Elixir/Tests/Engine/Aether/Simulation/ResourcePoolTest.cpp
  • Elixir/Tests/Engine/Aether/Simulation/SimulatorTest.cpp
  • Elixir/Tests/Engine/Aether/SystemInstanceTest.cpp
  • Elixir/Tests/Engine/Aether/SystemTest.cpp
  • Elixir/Tests/Engine/Aether/TestInstanceRegistry.h
  • Elixir/Tests/Engine/Aether/TestMaterialResolver.h
  • Elixir/Tests/Engine/Material/MaterialCompilationCacheTest.cpp
  • Elixir/Tests/Engine/Material/MaterialCompilerTest.cpp
  • Elixir/Tests/Engine/Material/MaterialFrameTableTest.cpp
  • Elixir/Tests/Engine/Material/MaterialGraphTest.cpp
  • Elixir/Tests/Engine/Material/MaterialRegistryTest.cpp
  • Elixir/Tests/Engine/Material/MaterialRenderProxyTest.cpp
  • Elixir/Tests/Engine/Material/MaterialRenderSceneTest.cpp
  • Elixir/Tests/Engine/Material/MaterialRendererTest.cpp
  • Elixir/Tests/Engine/Material/MaterialTest.cpp
  • Elixir/Tests/Engine/Material/MaterialTextureRegistryTest.cpp
  • Elixir/Tests/Graphics/Vulkan/VulkanBufferTest.cpp
  • Elixir/Tests/Graphics/Vulkan/VulkanImageTest.cpp
  • Elixir/Tests/Graphics/Vulkan/VulkanTestContext.h
  • Elixir/Vendor/SPIRV-Cross
  • Shaders/.gitignore
  • Shaders/Shaders.cmake
  • Vendor/vcpkg

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MrChampz
MrChampz changed the base branch from main to feature/material August 30, 2026 22:01
@MrChampz
MrChampz changed the base branch from feature/material to feature/mat-system August 30, 2026 22:01
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

return float4(surface.BaseColor + surface.Emissive, surface.Opacity);

P1 Badge Apply the simulated particle color in particle shaders

When an emitter uses SetColor or ColorOverLife, this return path ignores input.Color and outputs only the static material values. The same omission exists in the ribbon and mesh material templates, so the migrated effects no longer display their per-particle RGB transitions or alpha fade-outs; combine input.Color.rgb and input.Color.a with the graph surface output.


return { "pow(saturate(1.0 - dot(N, V)), 5.0)", EMaterialValueType::Float };

P2 Badge Define Fresnel inputs for particle shader permutations

When a material containing FresnelNode enables any particle usage, runtime compilation injects this expression into a particle pixel template where neither N nor V is declared. MaterialCompiler::Compile therefore succeeds for the surface stage and then fails in DXC for the particle stage, making the material unusable; emit an expression based on inputs available in every permutation or reject unsupported usages before invoking DXC.


static constexpr std::array components{ ".x", ".y", ".z", ".w" };

P2 Badge Widen scalar inputs before applying a component mask

When ComponentMaskNode is connected to a scalar-producing node and selects Y, Z, or W, this generates an invalid scalar swizzle such as (n1).w, causing runtime DXC compilation to fail. The graph API permits this connection and the component index constructor accepts all four components, so the input should be widened to Float4 before selecting the requested component.


float roughness = clamp(surface.Roughness, 0.045f, 1.0f);
float3 F0 = lerp(0.04f.xxx, surface.BaseColor, surface.Metallic);
float NdotV = saturate(dot(N, V)) + 1e-4f;
float3 diffuse = SampleIrradiance(N) * surface.BaseColor * (1.0f - surface.Metallic);

P2 Badge Use the graph-provided normal for surface lighting

When a graph drives EMaterialChannel::Normal, the generated body assigns surface.Normal, but every subsequent lighting calculation continues using N, which was initialized from input.Normal before graph evaluation. Normal-map or procedural-normal nodes therefore have no visual effect; derive the lighting normal from surface.Normal after the graph body, including the required space conversion.


// Runs on the application thread before its render task is queued.
// Implementations must not record GPU commands from this method.
virtual void Prepare(Timestep frameTime) {}
virtual void Render(Timestep frameTime) {}

P1 Badge Document the new public application hooks

The newly introduced public Prepare and Render hooks are not documented with Doxygen; the ordinary // text above Prepare does not satisfy the repository requirement, and Render has no documentation at all. Add concise English Doxygen comments that explain when each hook runs and any command-recording constraint.

AGENTS.md reference: AGENTS.md:L9-L9


m_FrameBuffer->UpdateData(
table->GetData().data(),
table->GetData().size() * sizeof(SMaterialFrameData)
);

P1 Badge Keep material frame data separate for in-flight frames

When two frames are in flight and their material values differ, this overwrites the one persistently mapped m_FrameBuffer even though the previous submission can still be reading that same buffer. VulkanGraphicsContext allows two concurrent submissions and waits only when reusing a frame slot, so the earlier frame can observe the later frame's material table or race a host write; use frame-indexed material buffers/ranges or defer the update until every prior user has completed.


const auto& material = item.Material;
if (material)
table->Add(*material);

P2 Badge Handle material-table overflow before building the snapshot

When a scene contains more unique material proxies than InitialFrameCapacity (256 in Application), MaterialFrameTable::Add returns nullopt here and the result is discarded. The snapshot then lacks those materials, and Render asserts and skips every affected draw, so sufficiently material-heavy particle scenes disappear partially without a usable failure result; grow the storage or explicitly reject/report the snapshot before rendering.


bool MaterialGraph::Validate(const IMaterialNodeValidationContext& parameters, std::string* error) const
{
for (const auto& [id, graphNode] : m_Nodes)

P2 Badge Validate graph connections and channel roots

When an editor or deserializer supplies a stale source ID, a missing channel node, or a cycle, this validation checks only each node's parameter references and returns success. EmitNode then silently substitutes 0.0 for the invalid topology, so MaterialCompiler::Build accepts a corrupted graph and produces visibly wrong material output instead of diagnostics; validation should traverse channel roots and reject dangling connections and cycles.


if (renderMode == Core::EParticleRenderMode::Sprite && !desc.BaseColorTexturePath.empty())

P2 Badge Apply authored textures to ribbon and mesh materials

When an effect-authored ribbon or mesh material includes a texture field, the parser stores it in BaseColorTexturePath, but this sprite-only condition discards it while still successfully loading the effect. Both particle ribbon and mesh shader templates support material textures, so these render modes unexpectedly show only the constant base color; construct the texture parameter and sample graph for every supported render mode.


return "dxc"; // rely on PATH

P1 Badge Bundle or remove the runtime DXC dependency

When a built application runs on a machine without the Vulkan SDK or dxc on PATH, every material compilation invokes a nonexistent executable and fails, so effect registration cannot produce renderable materials. The build copies the HLSL templates but does not package DXC, making the new runtime material path unusable in an ordinary deployment; ship a compiler, compile/cache permutations during the build, or resolve a packaged compiler path.


if (!proxy || !proxy->GetCompiledMaterial()->SupportsUsage(
Effect::GetMaterialUsage(m_RenderMode)
))
EE_CORE_ERROR(
"Aether emitter '{}' material does not support its render mode.",

P2 Badge Reject systems whose emitter material cannot render its mode

When an emitter is assigned a material that fails compilation or does not support its selected render mode, this only logs an error and leaves SCompiledEmitter::Material null. System::Compile still returns a compiled system and Manager::Add reports success, but the renderer later skips the emitter entirely; propagate this failure so registration or recompilation returns false instead of accepting an invisible system.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@MrChampz MrChampz closed this Aug 30, 2026
@MrChampz
MrChampz deleted the feature/mat-nodes branch September 1, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants