From f2699892e076461bde4f49fe88308688e0e26986 Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 17 Aug 2025 15:42:54 +0200 Subject: [PATCH 001/100] add support for resizing visject curve nodes --- Source/Editor/Surface/Archetypes/Tools.cs | 155 ++++++++++++++++++++-- 1 file changed, 141 insertions(+), 14 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 0e4b567b9..3bca332f8 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -455,9 +455,22 @@ namespace FlaxEditor.Surface.Archetypes private class CurveNode : SurfaceNode where T : struct { + private Rectangle _resizeButtonRect; + private Float2 _startResizingSize; + private Float2 _startResizingCornerOffset; + private bool _isResizing; + private BezierCurveEditor _curve; private bool _isSavingCurve; + private int SizeValueIndex => 29; // Index of the Size stored in Values array + + private Float2 SizeValue + { + get => (Float2)Values[SizeValueIndex]; + set => SetValue(SizeValueIndex, value, false); + } + public static NodeArchetype GetArchetype(ushort typeId, string title, Type valueType, T zero, T one) { return new NodeArchetype @@ -467,7 +480,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new CurveNode(id, context, arch, groupArch), Description = "An animation spline represented by a set of keyframes, each representing an endpoint of a Bezier curve.", Flags = NodeFlags.AllGraphs, - Size = new Float2(400, 180.0f), + Size = new Float2(400, 180), DefaultValues = new object[] { // Keyframes count @@ -491,6 +504,8 @@ namespace FlaxEditor.Surface.Archetypes 0.0f, zero, zero, zero, 0.0f, zero, zero, zero, 0.0f, zero, zero, zero, + + new Float2(400, 180), }, Elements = new[] { @@ -506,6 +521,11 @@ namespace FlaxEditor.Surface.Archetypes { } + public override bool CanSelect(ref Float2 location) + { + return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); + } + /// public override void OnLoaded(SurfaceNodeActions action) { @@ -519,13 +539,131 @@ namespace FlaxEditor.Surface.Archetypes { MaxKeyframes = 7, Bounds = new Rectangle(upperLeft + new Float2(curveMargin, 10.0f), upperRight.X - upperLeft.X - curveMargin * 2.0f, 140.0f), - Parent = this + Parent = this, + AnchorMax = Float2.One, }; _curve.Edited += OnCurveEdited; _curve.UnlockChildrenRecursive(); _curve.PerformLayout(); UpdateCurveKeyframes(); + + // Reapply the curve node size + var size = SizeValue; + if (Surface != null && Surface.GridSnappingEnabled) + size = Surface.SnapToGrid(size, true); + Resize(size.X, size.Y); + + // Ensure the whole curve is shown + _curve.ShowWholeCurve(); + } + + public override void OnValuesChanged() + { + base.OnValuesChanged(); + + var size = SizeValue; + Resize(size.X, size.Y); + + if (!_isSavingCurve) + { + UpdateCurveKeyframes(); + } + } + + protected override void UpdateRectangles() + { + base.UpdateRectangles(); + + const float buttonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; + const float buttonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize; + _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize); + } + + public override void Draw() + { + base.Draw(); + + var style = Style.Current; + if (_isResizing) + { + Render2D.FillRectangle(_resizeButtonRect, style.Selection); + Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); + } + Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); + } + + public override void OnLostFocus() + { + if (_isResizing) + EndResizing(); + + base.OnLostFocus(); + } + + public override void OnEndMouseCapture() + { + if (_isResizing) + EndResizing(); + + base.OnEndMouseCapture(); + } + + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (base.OnMouseDown(location, button)) + return true; + + if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) + { + // Start sliding + _isResizing = true; + _startResizingSize = Size; + _startResizingCornerOffset = Size - location; + StartMouseCapture(); + Cursor = CursorType.SizeNWSE; + return true; + } + + return false; + } + + public override void OnMouseMove(Float2 location) + { + if (_isResizing) + { + var emptySize = CalculateNodeSize(0, 0); + var size = Float2.Max(location - emptySize + _startResizingCornerOffset, new Float2(240, 160)); + Resize(size.X, size.Y); + } + else + { + base.OnMouseMove(location); + } + } + + public override bool OnMouseUp(Float2 location, MouseButton button) + { + if (button == MouseButton.Left && _isResizing) + { + EndResizing(); + return true; + } + + return base.OnMouseUp(location, button); + } + + private void EndResizing() + { + Cursor = CursorType.Default; + EndMouseCapture(); + _isResizing = false; + if (_startResizingSize != Size) + { + var emptySize = CalculateNodeSize(0, 0); + SizeValue = Size - emptySize; + Surface.MarkAsEdited(false); + } } private void OnCurveEdited() @@ -553,17 +691,6 @@ namespace FlaxEditor.Surface.Archetypes _isSavingCurve = false; } - /// - public override void OnValuesChanged() - { - base.OnValuesChanged(); - - if (!_isSavingCurve) - { - UpdateCurveKeyframes(); - } - } - private void UpdateCurveKeyframes() { var count = (int)Values[0]; @@ -1571,7 +1698,7 @@ namespace FlaxEditor.Surface.Archetypes DefaultValues = new object[] { Guid.Empty, - string.Empty + string.Empty, }, Elements = new[] { From 499228cc98b251b1f8f0af3e3ee98ca79e6837f5 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 13 Sep 2025 19:07:41 +0200 Subject: [PATCH 002/100] add curve editor presets --- Source/Editor/GUI/CurveEditor.Contents.cs | 37 +++++++++ Source/Editor/GUI/CurveEditor.cs | 94 +++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/Source/Editor/GUI/CurveEditor.Contents.cs b/Source/Editor/GUI/CurveEditor.Contents.cs index c2831046b..75f37d457 100644 --- a/Source/Editor/GUI/CurveEditor.Contents.cs +++ b/Source/Editor/GUI/CurveEditor.Contents.cs @@ -522,6 +522,16 @@ namespace FlaxEditor.GUI cm.AddButton("Show whole curve", _editor.ShowWholeCurve); cm.AddButton("Reset view", _editor.ResetView); } + cm.AddSeparator(); + var presetCm = cm.AddChildMenu("Apply preset"); + foreach (var value in Enum.GetValues(typeof(CurvePreset))) + { + CurvePreset preset = (CurvePreset)value; + string name = Utilities.Utils.GetPropertyNameUI(preset.ToString()); + var b = presetCm.ContextMenu.AddButton(name, () => _editor.ApplyPreset(preset)); + b.Enabled = !(_editor is LinearCurveEditor && (preset != CurvePreset.Constant && preset != CurvePreset.Linear)); + } + _editor.OnShowContextMenu(cm, selectionCount); cm.Show(this, location); } @@ -619,6 +629,33 @@ namespace FlaxEditor.GUI } } + /// + /// A list of avaliable curve presets for the . + /// + public enum CurvePreset + { + /// + /// A curve where every point has the same value. + /// + Constant, + /// + /// A curve linear curve. + /// + Linear, + /// + /// A curve that starts a slowly and then accelerates until the end. + /// + EaseIn, + /// + /// A curve that starts a steep and then flattens until the end. + /// + EaseOut, + /// + /// A combination of the and preset. + /// + Smoothstep + } + /// public override void OnKeyframesDeselect(IKeyframesEditor editor) { diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index 706d07b32..d91eac18c 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -326,6 +326,28 @@ namespace FlaxEditor.GUI private Color _labelsColor; private Font _labelsFont; + /// + /// Preset values for to be applied to a . + /// + public Dictionary PresetValues = new Dictionary + { + { CurvePreset.Constant, new object[] { true, // LinearTangent + 0f, 0.5f, 0f, 0f, // Time, value, tangent in, tangent out + 1f, 0.5f, 0f, 0f } }, + { CurvePreset.EaseIn, new object[] { false, + 0f, 0f, 0f, 0f, + 1f, 1f, -1.4f, 0f } }, + { CurvePreset.EaseOut, new object[] { false, + 1f, 1f, 0f, 0f, + 0f, 0f, 0f, 1.4f } }, + { CurvePreset.Linear, new object[] { true, + 0f, 0f, 0f, 0f, + 1f, 1f, 0f, 0f } }, + { CurvePreset.Smoothstep, new object[] { false, + 0f, 0f, 0f, 0f, + 1f, 1f, 0f, 0f } }, + }; + /// /// The keyframe UI points. /// @@ -568,6 +590,28 @@ namespace FlaxEditor.GUI /// The list of indices of the keyframes to remove. protected abstract void RemoveKeyframesInternal(HashSet indicesToRemove); + /// + /// Tries to convert a float to the type of the type wildcard of the curve editor. + /// + /// The float. + /// The converted value. + public static object ConvertCurvePresetValueToCurveEditorType(float value) + { + if (typeof(T) == typeof(Float2)) + return new Float2(value); + if (typeof(T) == typeof(Float3)) + return new Float3(value); + if (typeof(T) == typeof(Float4)) + return new Float4(value); + if (typeof(T) == typeof(Vector2)) + return new Vector2(value); + if (typeof(T) == typeof(Vector3)) + return new Vector3(value); + if (typeof(T) == typeof(Vector4)) + return new Vector4(value); + return value; + } + /// /// Called when showing a context menu. Can be used to add custom buttons with actions. /// @@ -752,6 +796,17 @@ namespace FlaxEditor.GUI ShowCurve(false); } + /// + /// Applies a to the curve editor. + /// + /// The preset. + public virtual void ApplyPreset(CurvePreset preset) + { + // Remove existing keyframes + SelectAll(); + RemoveKeyframes(); + } + /// public override void Evaluate(out object result, float time, bool loop = false) { @@ -1580,6 +1635,22 @@ namespace FlaxEditor.GUI base.OnDestroy(); } + + /// + public override void ApplyPreset(CurvePreset preset) + { + base.ApplyPreset(preset); + + object[] data = PresetValues[preset]; + for (int i = 1; i < data.Length; i += 4) + { + float time = (float)data[i]; + object value = ConvertCurvePresetValueToCurveEditorType((float)data[i + 1]); + AddKeyframe(time, value); + } + + ShowWholeCurve(); + } } /// @@ -2396,5 +2467,28 @@ namespace FlaxEditor.GUI base.OnDestroy(); } + + /// + public override void ApplyPreset(CurvePreset preset) + { + base.ApplyPreset(preset); + + object[] data = PresetValues[preset]; + for (int i = 1; i < data.Length; i += 4) + { + float time = (float)data[i]; + object value = ConvertCurvePresetValueToCurveEditorType((float)data[i + 1]); + object tangentIn = ConvertCurvePresetValueToCurveEditorType((float)data[i + 2]); + object tangentOut = ConvertCurvePresetValueToCurveEditorType((float)data[i + 3]); + + AddKeyframe(time, value, tangentIn, tangentOut); + } + + SelectAll(); + if ((bool)data[0]) + SetTangentsLinear(); + + ShowWholeCurve(); + } } } From bb6c3233d2709ba6fbfcce6f3a30b7666794c54b Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 13 Sep 2025 19:22:37 +0200 Subject: [PATCH 003/100] method placement --- Source/Editor/GUI/CurveEditor.cs | 78 ++++++++++++++++---------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index d91eac18c..0bbbb9354 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -1581,6 +1581,22 @@ namespace FlaxEditor.GUI _tangents[i].Visible = false; } + /// + public override void ApplyPreset(CurvePreset preset) + { + base.ApplyPreset(preset); + + object[] data = PresetValues[preset]; + for (int i = 1; i < data.Length; i += 4) + { + float time = (float)data[i]; + object value = ConvertCurvePresetValueToCurveEditorType((float)data[i + 1]); + AddKeyframe(time, value); + } + + ShowWholeCurve(); + } + /// protected override void DrawCurve(ref Rectangle viewRect) { @@ -1635,22 +1651,6 @@ namespace FlaxEditor.GUI base.OnDestroy(); } - - /// - public override void ApplyPreset(CurvePreset preset) - { - base.ApplyPreset(preset); - - object[] data = PresetValues[preset]; - for (int i = 1; i < data.Length; i += 4) - { - float time = (float)data[i]; - object value = ConvertCurvePresetValueToCurveEditorType((float)data[i + 1]); - AddKeyframe(time, value); - } - - ShowWholeCurve(); - } } /// @@ -2383,6 +2383,29 @@ namespace FlaxEditor.GUI } } + /// + public override void ApplyPreset(CurvePreset preset) + { + base.ApplyPreset(preset); + + object[] data = PresetValues[preset]; + for (int i = 1; i < data.Length; i += 4) + { + float time = (float)data[i]; + object value = ConvertCurvePresetValueToCurveEditorType((float)data[i + 1]); + object tangentIn = ConvertCurvePresetValueToCurveEditorType((float)data[i + 2]); + object tangentOut = ConvertCurvePresetValueToCurveEditorType((float)data[i + 3]); + + AddKeyframe(time, value, tangentIn, tangentOut); + } + + SelectAll(); + if ((bool)data[0]) + SetTangentsLinear(); + + ShowWholeCurve(); + } + /// protected override void SetScaleInternal(ref Float2 scale) { @@ -2467,28 +2490,5 @@ namespace FlaxEditor.GUI base.OnDestroy(); } - - /// - public override void ApplyPreset(CurvePreset preset) - { - base.ApplyPreset(preset); - - object[] data = PresetValues[preset]; - for (int i = 1; i < data.Length; i += 4) - { - float time = (float)data[i]; - object value = ConvertCurvePresetValueToCurveEditorType((float)data[i + 1]); - object tangentIn = ConvertCurvePresetValueToCurveEditorType((float)data[i + 2]); - object tangentOut = ConvertCurvePresetValueToCurveEditorType((float)data[i + 3]); - - AddKeyframe(time, value, tangentIn, tangentOut); - } - - SelectAll(); - if ((bool)data[0]) - SetTangentsLinear(); - - ShowWholeCurve(); - } } } From ee6fae8956048442cf1fb1bca0f290129c5d5e6f Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 14 Sep 2025 14:15:35 +0200 Subject: [PATCH 004/100] small polish stuff - Fix keyframe edit popup height - Make keyframe mouse hover color more intense - Increase keyframe size by 1 to prevent accidental miss clicks, resulting in a new keyframe added, when trying to double click to edit a keyframe --- Source/Editor/GUI/CurveEditor.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index 0bbbb9354..ce3845e3d 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -26,11 +26,12 @@ namespace FlaxEditor.GUI private List _keyframeIndices; private bool _isDirty; - public Popup(CurveEditor editor, object[] selection, List keyframeIndices = null, float height = 140.0f) - : this(editor, height) + public Popup(CurveEditor editor, object[] selection, List keyframeIndices = null, float maxHeight = 140.0f) + : this(editor, maxHeight) { _presenter.Select(selection); _presenter.OpenAllGroups(); + Size = new Float2(Size.X, Mathf.Min(_presenter.ContainerControl.Size.Y, maxHeight)); _keyframeIndices = keyframeIndices; if (keyframeIndices != null && selection.Length != keyframeIndices.Count) throw new Exception(); @@ -169,7 +170,7 @@ namespace FlaxEditor.GUI if (IsSelected) color = Editor.ContainsFocus ? style.SelectionBorder : Color.Lerp(style.ForegroundDisabled, style.SelectionBorder, 0.4f); if (IsMouseOver) - color *= 1.1f; + color *= 1.5f; Render2D.FillRectangle(rect, color); } @@ -285,7 +286,7 @@ namespace FlaxEditor.GUI /// /// The keyframes size. /// - protected static readonly Float2 KeyframesSize = new Float2(7.0f); + protected static readonly Float2 KeyframesSize = new Float2(8.0f); /// /// The colors for the keyframe points. From 6810e5f2a442e6773335fa146939349219bc1b1a Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 14 Sep 2025 16:00:07 +0200 Subject: [PATCH 005/100] add moving selected keyframes with arrow keys --- Source/Editor/GUI/CurveEditor.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index ce3845e3d..d9f79b6b9 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -1084,6 +1084,30 @@ namespace FlaxEditor.GUI return true; } + bool left = key == KeyboardKeys.ArrowLeft; + bool right = key == KeyboardKeys.ArrowRight; + bool up = key == KeyboardKeys.ArrowUp; + bool down = key == KeyboardKeys.ArrowDown; + + if (left || right || up || down) + { + bool shift = Root.GetKey(KeyboardKeys.Shift); + bool alt = Root.GetKey(KeyboardKeys.Alt); + float deltaValue = 10f; + if (shift || alt) + deltaValue = shift ? 2.5f : 5f; + + Float2 moveDelta = Float2.Zero; + if (left || right) + moveDelta.X = left ? -deltaValue : deltaValue; + if (up || down) + moveDelta.Y = up ? -deltaValue : deltaValue; + + _contents.OnMoveStart(Float2.Zero); + _contents.OnMove(moveDelta); + _contents.OnMoveEnd(Float2.Zero); + } + return false; } From facd7d39dc86345a9f9457487067dcf163bee926 Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 14 Sep 2025 16:02:48 +0200 Subject: [PATCH 006/100] return true if handled --- Source/Editor/GUI/CurveEditor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index d9f79b6b9..ac3d38353 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -1106,6 +1106,7 @@ namespace FlaxEditor.GUI _contents.OnMoveStart(Float2.Zero); _contents.OnMove(moveDelta); _contents.OnMoveEnd(Float2.Zero); + return true; } return false; From 187592b6734bf8cc73b10bbcf9e100597ee3b544 Mon Sep 17 00:00:00 2001 From: Saas Date: Mon, 15 Sep 2025 22:37:31 +0200 Subject: [PATCH 007/100] add moving visject nodes in smaller increments when holding shift and alt --- Source/Editor/Surface/VisjectSurface.Input.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index 09df195eb..6299c1bb5 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -721,7 +721,12 @@ namespace FlaxEditor.Surface if (HasNodesSelection) { - var keyMoveRange = 50; + var keyMoveDelta = 50; + bool altDown = RootWindow.GetKey(KeyboardKeys.Alt); + bool shiftDown = RootWindow.GetKey(KeyboardKeys.Shift); + if (altDown || shiftDown) + keyMoveDelta = shiftDown ? 10 : 25; + switch (key) { case KeyboardKeys.Backspace: @@ -759,7 +764,7 @@ namespace FlaxEditor.Surface else if (!IsMovingSelection && CanEdit) { // Move selected nodes - var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveRange : keyMoveRange); + var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveDelta : keyMoveDelta); MoveSelectedNodes(delta); } return true; @@ -775,9 +780,8 @@ namespace FlaxEditor.Surface if ((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput)) { if (_selectedConnectionIndex < 0 || _selectedConnectionIndex >= selectedBox.Connections.Count) - { _selectedConnectionIndex = 0; - } + toSelect = selectedBox.Connections[_selectedConnectionIndex]; } else @@ -805,7 +809,7 @@ namespace FlaxEditor.Surface else if (!IsMovingSelection && CanEdit) { // Move selected nodes - var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveRange : keyMoveRange, 0); + var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveDelta : keyMoveDelta, 0); MoveSelectedNodes(delta); } return true; @@ -821,13 +825,9 @@ namespace FlaxEditor.Surface return true; if (Root.GetKey(KeyboardKeys.Shift)) - { _selectedConnectionIndex = ((_selectedConnectionIndex - 1) % connectionCount + connectionCount) % connectionCount; - } else - { _selectedConnectionIndex = (_selectedConnectionIndex + 1) % connectionCount; - } return true; } } From 4d77646f2692b8c9570bd426a8c15cbb2d08b6f3 Mon Sep 17 00:00:00 2001 From: Saas Date: Mon, 15 Sep 2025 22:46:56 +0200 Subject: [PATCH 008/100] fix error when changing selected box with arrow keys --- Source/Editor/Surface/VisjectSurface.Input.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index 6299c1bb5..c24a82f52 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -777,11 +777,8 @@ namespace FlaxEditor.Surface if (selectedBox != null) { Box toSelect = null; - if ((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput)) + if (((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput)) && selectedBox.HasAnyConnection) { - if (_selectedConnectionIndex < 0 || _selectedConnectionIndex >= selectedBox.Connections.Count) - _selectedConnectionIndex = 0; - toSelect = selectedBox.Connections[_selectedConnectionIndex]; } else From 4f3f1cd163b287924cc04948d41c927ac6dbe9b3 Mon Sep 17 00:00:00 2001 From: Saas Date: Mon, 15 Sep 2025 23:09:07 +0200 Subject: [PATCH 009/100] make changing selected box with arrow up/ down loop --- Source/Editor/Surface/VisjectSurface.Input.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index c24a82f52..4ce2025a6 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -754,12 +754,13 @@ namespace FlaxEditor.Surface Box selectedBox = GetSelectedBox(SelectedNodes); if (selectedBox != null) { - Box toSelect = (key == KeyboardKeys.ArrowUp) ? selectedBox?.ParentNode.GetPreviousBox(selectedBox) : selectedBox?.ParentNode.GetNextBox(selectedBox); - if (toSelect != null && toSelect.IsOutput == selectedBox.IsOutput) - { - Select(toSelect.ParentNode); - toSelect.ParentNode.SelectBox(toSelect); - } + int delta = key == KeyboardKeys.ArrowDown ? 1 : -1; + List boxes = selectedBox.ParentNode.GetBoxes().FindAll(b => b.IsOutput == selectedBox.IsOutput); + int selectedIndex = boxes.IndexOf(selectedBox); + Box toSelect = boxes[Mathf.Wrap(selectedIndex + delta, 0, boxes.Count - 1)]; + + Select(toSelect.ParentNode); + toSelect.ParentNode.SelectBox(toSelect); } else if (!IsMovingSelection && CanEdit) { From b38a7e6eb234eaea6dac28a224279818e0458ceb Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 18 Sep 2025 17:54:21 +0200 Subject: [PATCH 010/100] unify both "too many audio listeners"- warnings --- Source/Engine/Audio/AudioListener.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Audio/AudioListener.cpp b/Source/Engine/Audio/AudioListener.cpp index 1921bd373..99f4424ec 100644 --- a/Source/Engine/Audio/AudioListener.cpp +++ b/Source/Engine/Audio/AudioListener.cpp @@ -37,15 +37,14 @@ void AudioListener::OnEnable() { _prevPos = GetPosition(); _velocity = Vector3::Zero; + + ASSERT(!Audio::Listeners.Contains(this)); if (Audio::Listeners.Count() >= AUDIO_MAX_LISTENERS) { - LOG(Error, "Unsupported amount of the audio listeners!"); + LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time."); } else { - ASSERT(!Audio::Listeners.Contains(this)); - if (Audio::Listeners.Count() > 0) - LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time."); Audio::Listeners.Add(this); AudioBackend::Listener::Reset(); AudioBackend::Listener::TransformChanged(GetPosition(), GetOrientation()); From b130b81863e069a9084426cf62a79a246a1d1277 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 20 Sep 2025 15:08:03 +0200 Subject: [PATCH 011/100] add game view and show/ hiding navigation similar to unreal engine --- Source/Editor/Gizmo/TransformGizmo.cs | 13 +++++++++ Source/Editor/Options/InputOptions.cs | 8 +++++ .../Viewport/MainEditorGizmoViewport.cs | 29 +++++++++++++++++++ Source/Engine/Graphics/Enums.h | 5 ++++ 4 files changed, 55 insertions(+) diff --git a/Source/Editor/Gizmo/TransformGizmo.cs b/Source/Editor/Gizmo/TransformGizmo.cs index 8294a4302..d5847c892 100644 --- a/Source/Editor/Gizmo/TransformGizmo.cs +++ b/Source/Editor/Gizmo/TransformGizmo.cs @@ -31,6 +31,7 @@ namespace FlaxEditor.Gizmo private readonly List _selection = new List(); private readonly List _selectionParents = new List(); + private bool _visible = true; /// /// The event to apply objects transformation. @@ -52,6 +53,11 @@ namespace FlaxEditor.Gizmo /// public List SelectedParents => _selectionParents; + /// + /// Gets or sets a value indicating whether this is visible. + /// + public bool Visible { get { return _visible; } set { _visible = value; } } + /// /// Initializes a new instance of the class. /// @@ -272,6 +278,13 @@ namespace FlaxEditor.Gizmo base.OnSelectionChanged(newSelection); } + /// + public override void Draw(ref RenderContext renderContext) + { + if (Visible) + base.Draw(ref renderContext); + } + /// protected override int SelectionCount => _selectionParents.Count; diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index af919c1f3..1c6a8279d 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -387,6 +387,14 @@ namespace FlaxEditor.Options [EditorDisplay("Viewport"), EditorOrder(1760)] public InputBinding ToggleOrthographic = new InputBinding(KeyboardKeys.NumpadDecimal); + [DefaultValue(typeof(InputBinding), "G")] + [EditorDisplay("Viewport"), EditorOrder(1770)] + public InputBinding ToggleGameView = new InputBinding(KeyboardKeys.G); + + [DefaultValue(typeof(InputBinding), "P")] + [EditorDisplay("Viewport"), EditorOrder(1770)] + public InputBinding ToggleNavMeshVisibility = new InputBinding(KeyboardKeys.P); + #endregion #region Debug Views diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index bb01bc863..6f30977e2 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -108,6 +108,12 @@ namespace FlaxEditor.Viewport private EditorSpritesRenderer _editorSpritesRenderer; private ViewportRubberBandSelector _rubberBandSelector; + private bool _gameViewActive; + private ViewFlags _preGameViewFlags; + private bool _gameViewWasGridShown; + private bool _gameViewWasFpsCounterShown; + private bool _gameViewWasNagivationShown; + /// /// Drag and drop handlers /// @@ -259,6 +265,29 @@ namespace FlaxEditor.Viewport InputActions.Add(options => options.FocusSelection, FocusSelection); InputActions.Add(options => options.RotateSelection, RotateSelection); InputActions.Add(options => options.Delete, _editor.SceneEditing.Delete); + InputActions.Add(options => options.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation); + + // Game View + InputActions.Add(options => options.ToggleGameView, () => + { + if (!_gameViewActive) + { + _preGameViewFlags = Task.ViewFlags; + _gameViewWasGridShown = ShowFpsCounter; + _gameViewWasFpsCounterShown = ShowNavigation; + _gameViewWasNagivationShown = Grid.Enabled; + } + + Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.GameView; + ShowFpsCounter = _gameViewActive ? _gameViewWasGridShown : false; + ShowNavigation = _gameViewActive ? _gameViewWasFpsCounterShown : false; + Grid.Enabled = _gameViewActive ? _gameViewWasNagivationShown : false; + + _gameViewActive = !_gameViewActive; + + TransformGizmo.Visible = !_gameViewActive; + SelectionOutline.ShowSelectionOutline = !_gameViewActive; + }); } /// diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h index 6343b827b..9019fc504 100644 --- a/Source/Engine/Graphics/Enums.h +++ b/Source/Engine/Graphics/Enums.h @@ -1082,6 +1082,11 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64 /// Default flags for materials/models previews generating. /// DefaultAssetPreview = Reflections | Decals | DirectionalLights | PointLights | SpotLights | SkyLights | SpecularLight | AntiAliasing | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | Sky, + + /// + /// Default flags for game view. + /// + GameView = AntiAliasing | Shadows | Reflections | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Sky | Fog | SpecularLight | Decals | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | MotionBlur | ContactShadows | DepthOfField, }; DECLARE_ENUM_OPERATORS(ViewFlags); From fd1e0a4e80eabf0c308c29f5357ade9031c626a7 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 20 Sep 2025 16:12:04 +0200 Subject: [PATCH 012/100] add game view to "View" menu, add keyboard new keyboard shortcuts to menu and extra options for view flags and view layers --- Source/Editor/Viewport/EditorViewport.cs | 8 ++- .../Viewport/MainEditorGizmoViewport.cs | 56 ++++++++++++------- Source/Engine/Graphics/Enums.h | 5 ++ 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index b4af43281..ed4832d63 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -864,7 +864,9 @@ namespace FlaxEditor.Viewport } }); viewLayers.AddButton("Reset layers", () => Task.ViewLayersMask = LayersMask.Default).Icon = Editor.Instance.Icons.Rotate32; - viewLayers.AddButton("Disable layers", () => Task.ViewLayersMask = new LayersMask(0)).Icon = Editor.Instance.Icons.Rotate32; + viewLayers.AddSeparator(); + viewLayers.AddButton("Enable all", () => Task.ViewLayersMask = new LayersMask(-1)).Icon = Editor.Instance.Icons.CheckBoxTick12; + viewLayers.AddButton("Disable all", () => Task.ViewLayersMask = new LayersMask(0)).Icon = Editor.Instance.Icons.Cross12; viewLayers.AddSeparator(); var layers = LayersAndTagsSettings.GetCurrentLayers(); if (layers != null && layers.Length > 0) @@ -905,7 +907,9 @@ namespace FlaxEditor.Viewport } }); viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = Editor.Instance.Icons.Rotate32; - viewFlags.AddButton("Disable flags", () => Task.ViewFlags = ViewFlags.None).Icon = Editor.Instance.Icons.Rotate32; + viewFlags.AddSeparator(); + viewFlags.AddButton("Enable all", () => Task.ViewFlags = ViewFlags.All).Icon = Editor.Instance.Icons.CheckBoxTick12; + viewFlags.AddButton("Disable all", () => Task.ViewFlags = ViewFlags.None).Icon = Editor.Instance.Icons.Cross12; viewFlags.AddSeparator(); for (int i = 0; i < ViewFlagsValues.Length; i++) { diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 6f30977e2..f882505b3 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -25,6 +25,7 @@ namespace FlaxEditor.Viewport private readonly Editor _editor; private readonly ContextMenuButton _showGridButton; private readonly ContextMenuButton _showNavigationButton; + private readonly ContextMenuButton _toggleGameViewButton; private SelectionOutline _customSelectionOutline; /// @@ -191,6 +192,7 @@ namespace FlaxEditor.Viewport : base(Object.New(), editor.Undo, editor.Scene.Root) { _editor = editor; + var inputOptions = _editor.Options.Options.Input; DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem); // Prepare rendering task @@ -238,9 +240,14 @@ namespace FlaxEditor.Viewport _showGridButton.CloseMenuOnClick = false; // Show navigation widget - _showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", () => ShowNavigation = !ShowNavigation); + _showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", inputOptions.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation); _showNavigationButton.CloseMenuOnClick = false; + // Game View + ViewWidgetButtonMenu.AddSeparator(); + _toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView); + _toggleGameViewButton.CloseMenuOnClick = false; + // Create camera widget ViewWidgetButtonMenu.AddSeparator(); ViewWidgetButtonMenu.AddButton("Create camera here", CreateCameraAtView); @@ -268,26 +275,7 @@ namespace FlaxEditor.Viewport InputActions.Add(options => options.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation); // Game View - InputActions.Add(options => options.ToggleGameView, () => - { - if (!_gameViewActive) - { - _preGameViewFlags = Task.ViewFlags; - _gameViewWasGridShown = ShowFpsCounter; - _gameViewWasFpsCounterShown = ShowNavigation; - _gameViewWasNagivationShown = Grid.Enabled; - } - - Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.GameView; - ShowFpsCounter = _gameViewActive ? _gameViewWasGridShown : false; - ShowNavigation = _gameViewActive ? _gameViewWasFpsCounterShown : false; - Grid.Enabled = _gameViewActive ? _gameViewWasNagivationShown : false; - - _gameViewActive = !_gameViewActive; - - TransformGizmo.Visible = !_gameViewActive; - SelectionOutline.ShowSelectionOutline = !_gameViewActive; - }); + InputActions.Add(options => options.ToggleGameView, ToggleGameView); } /// @@ -503,6 +491,32 @@ namespace FlaxEditor.Viewport TransformGizmo.EndTransforming(); } + /// + /// Toggles game view view mode on or off. + /// + public void ToggleGameView() + { + if (!_gameViewActive) + { + _preGameViewFlags = Task.ViewFlags; + _gameViewWasGridShown = ShowFpsCounter; + _gameViewWasFpsCounterShown = ShowNavigation; + _gameViewWasNagivationShown = Grid.Enabled; + } + + Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.GameView; + ShowFpsCounter = _gameViewActive ? _gameViewWasGridShown : false; + ShowNavigation = _gameViewActive ? _gameViewWasFpsCounterShown : false; + Grid.Enabled = _gameViewActive ? _gameViewWasNagivationShown : false; + + _gameViewActive = !_gameViewActive; + + TransformGizmo.Visible = !_gameViewActive; + SelectionOutline.ShowSelectionOutline = !_gameViewActive; + + _toggleGameViewButton.Icon = _gameViewActive ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + } + /// public override void OnLostFocus() { diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h index 9019fc504..f61096863 100644 --- a/Source/Engine/Graphics/Enums.h +++ b/Source/Engine/Graphics/Enums.h @@ -1087,6 +1087,11 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64 /// Default flags for game view. /// GameView = AntiAliasing | Shadows | Reflections | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Sky | Fog | SpecularLight | Decals | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | MotionBlur | ContactShadows | DepthOfField, + + /// + /// All flags enabled. + /// + All = None | DebugDraw | EditorSprites | Reflections | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | Decals | DepthOfField | PhysicsDebug | Fog | MotionBlur | ContactShadows | GlobalSDF | Sky | LightsDebug, }; DECLARE_ENUM_OPERATORS(ViewFlags); From 6e9902a9668ec122c45179e78a36a45878d1061a Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 21 Sep 2025 16:49:07 +0200 Subject: [PATCH 013/100] use DefaultGame ViewFlag and some fixes --- Source/Editor/Viewport/MainEditorGizmoViewport.cs | 8 ++++---- Source/Engine/Graphics/Enums.h | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index f882505b3..9e37b3dce 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -499,12 +499,12 @@ namespace FlaxEditor.Viewport if (!_gameViewActive) { _preGameViewFlags = Task.ViewFlags; - _gameViewWasGridShown = ShowFpsCounter; - _gameViewWasFpsCounterShown = ShowNavigation; - _gameViewWasNagivationShown = Grid.Enabled; + _gameViewWasGridShown = Grid.Enabled; + _gameViewWasFpsCounterShown = ShowFpsCounter; + _gameViewWasNagivationShown = ShowNavigation; } - Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.GameView; + Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame; ShowFpsCounter = _gameViewActive ? _gameViewWasGridShown : false; ShowNavigation = _gameViewActive ? _gameViewWasFpsCounterShown : false; Grid.Enabled = _gameViewActive ? _gameViewWasNagivationShown : false; diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h index f61096863..3df3194d4 100644 --- a/Source/Engine/Graphics/Enums.h +++ b/Source/Engine/Graphics/Enums.h @@ -1083,11 +1083,6 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64 /// DefaultAssetPreview = Reflections | Decals | DirectionalLights | PointLights | SpotLights | SkyLights | SpecularLight | AntiAliasing | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | Sky, - /// - /// Default flags for game view. - /// - GameView = AntiAliasing | Shadows | Reflections | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Sky | Fog | SpecularLight | Decals | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | MotionBlur | ContactShadows | DepthOfField, - /// /// All flags enabled. /// From 13dd80d4290fd6743ff3dfadd6c19f973e114309 Mon Sep 17 00:00:00 2001 From: Saas Date: Sun, 21 Sep 2025 16:57:38 +0200 Subject: [PATCH 014/100] add changing and caching Debug ViewMode for Game View --- Source/Editor/Viewport/EditorViewport.cs | 6 +++--- Source/Editor/Viewport/MainEditorGizmoViewport.cs | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index ed4832d63..4502880cb 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -584,7 +584,7 @@ namespace FlaxEditor.Viewport _cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth) { Tag = this, - TooltipText = "Camera Settings", + TooltipText = "Camera Settings.", Parent = _cameraWidget }; _cameraWidget.Parent = this; @@ -593,7 +593,7 @@ namespace FlaxEditor.Viewport _orthographicModeButton = new ViewportWidgetButton(string.Empty, Editor.Instance.Icons.CamSpeed32, null, true) { Checked = !_isOrtho, - TooltipText = "Toggle Orthographic/Perspective Mode", + TooltipText = "Toggle Orthographic/Perspective Mode.", Parent = _cameraWidget }; _orthographicModeButton.Toggled += OnOrthographicModeToggled; @@ -832,7 +832,7 @@ namespace FlaxEditor.Viewport ViewWidgetButtonMenu = new ContextMenu(); var viewModeButton = new ViewportWidgetButton("View", SpriteHandle.Invalid, ViewWidgetButtonMenu) { - TooltipText = "View properties", + TooltipText = "View properties.", Parent = viewMode }; viewMode.Parent = this; diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 9e37b3dce..f9b382a0a 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -111,6 +111,7 @@ namespace FlaxEditor.Viewport private bool _gameViewActive; private ViewFlags _preGameViewFlags; + private ViewMode _preGameViewViewMode; private bool _gameViewWasGridShown; private bool _gameViewWasFpsCounterShown; private bool _gameViewWasNagivationShown; @@ -499,12 +500,14 @@ namespace FlaxEditor.Viewport if (!_gameViewActive) { _preGameViewFlags = Task.ViewFlags; + _preGameViewViewMode = Task.ViewMode; _gameViewWasGridShown = Grid.Enabled; _gameViewWasFpsCounterShown = ShowFpsCounter; _gameViewWasNagivationShown = ShowNavigation; } Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame; + Task.ViewMode = _gameViewActive ? _preGameViewViewMode : ViewMode.Default; ShowFpsCounter = _gameViewActive ? _gameViewWasGridShown : false; ShowNavigation = _gameViewActive ? _gameViewWasFpsCounterShown : false; Grid.Enabled = _gameViewActive ? _gameViewWasNagivationShown : false; From 25744986a3892df0fef0d754c3d9e984a9bdba9e Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 13 Oct 2025 20:40:16 -0500 Subject: [PATCH 015/100] Special query for closest point on height field. --- .../Physics/PhysX/PhysicsBackendPhysX.cpp | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index 8d61e9ba5..4bd82f901 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -2783,6 +2783,70 @@ float PhysicsBackend::ComputeShapeSqrDistanceToPoint(void* shape, const Vector3& { auto shapePhysX = (PxShape*)shape; const PxTransform trans(C2P(position), C2P(orientation)); + + // Special case for heightfield collider + if (shapePhysX->getGeometryType() == PxGeometryType::eHEIGHTFIELD) + { + // Do a bunch of raycasts... because for some reason pointDistance does not support height fields... + PxVec3 origin = C2P(point); + + // Get all unit directions based on resolution value + Array unitDirections; + int32 resolution = 32; + for (int32 i = 0; i <= resolution; i++) { + float phi = PI * (float)i / resolution; + float sinPhi = Math::Sin(phi); + float cosPhi = Math::Cos(phi); + + for (int32 j = 0; j <= resolution; j++) { + float theta = 2.0f * PI * (float)j / resolution; + float cosTheta = Math::Cos(theta); + float sinTheta = Math::Sin(theta); + + PxVec3 v; + v.x = cosTheta * sinPhi; + v.y = cosPhi; + v.z = sinTheta * sinPhi; + + // All generated vectors are unit vectors (length 1) + unitDirections.Add(v); + } + } + + PxReal maxDistance = PX_MAX_REAL; // Search indefinitely + + PxQueryFilterData filterData; + filterData.data.word0 = (PxU32)shapePhysX->getSimulationFilterData().word0; + PxHitFlags hitFlags = PxHitFlag::ePOSITION | PxHitFlag::eNORMAL | PxHitFlag::eMESH_BOTH_SIDES; // Both sides added for if it is underneath the height field + PxRaycastBuffer buffer; + auto scene = shapePhysX->getActor()->getScene(); + + PxReal closestDistance = maxDistance; + PxVec3 tempClosestPoint; + for (PxVec3& unitDir : unitDirections) + { + bool hitResult = scene->raycast(origin, unitDir, maxDistance, buffer, hitFlags, filterData); + if (hitResult) + { + auto& hit = buffer.getAnyHit(0); + if (hit.distance < closestDistance && hit.distance > 0.0f) + { + tempClosestPoint = hit.position; + closestDistance = hit.distance; + } + } + } + + if (closestDistance < maxDistance) + { + *closestPoint = P2C(tempClosestPoint); + return closestDistance; + } + + return -1.0f; + } + + // Default point distance for other collider queries #if USE_LARGE_WORLDS PxVec3 closestPointPx; float result = PxGeometryQuery::pointDistance(C2P(point), shapePhysX->getGeometry(), trans, &closestPointPx); From 887311d1f1257cde43839b7f88db15624a7eeefa Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 13 Nov 2025 23:00:09 +0100 Subject: [PATCH 016/100] replace raw data with (a) struct(s) --- Source/Editor/GUI/CurveEditor.cs | 132 ++++++++++++++++++++++++------- 1 file changed, 105 insertions(+), 27 deletions(-) diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index ac3d38353..4fb727ea1 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -19,6 +19,48 @@ namespace FlaxEditor.GUI /// public abstract partial class CurveEditor : CurveEditorBase where T : new() { + /// + /// Represents a single point in a . + /// + protected struct CurvePresetPoint + { + /// + /// The time. + /// + public float Time; + + /// + /// The value. + /// + public float Value; + + /// + /// The in tangent. Will be ignored in + /// + public float TangentIn; + + /// + /// The out tangent. Will be ignored in + /// + public float TangentOut; + } + + /// + /// A curve preset. + /// + protected struct CurveEditorPreset() + { + /// + /// If the tangents will be linear or smooth. + /// + public bool LinearTangents; + + /// + /// The points of the preset. + /// + public List Points; + } + private class Popup : ContextMenuBase { private CustomEditorPresenter _presenter; @@ -330,23 +372,58 @@ namespace FlaxEditor.GUI /// /// Preset values for to be applied to a . /// - public Dictionary PresetValues = new Dictionary + protected Dictionary Presets = new Dictionary { - { CurvePreset.Constant, new object[] { true, // LinearTangent - 0f, 0.5f, 0f, 0f, // Time, value, tangent in, tangent out - 1f, 0.5f, 0f, 0f } }, - { CurvePreset.EaseIn, new object[] { false, - 0f, 0f, 0f, 0f, - 1f, 1f, -1.4f, 0f } }, - { CurvePreset.EaseOut, new object[] { false, - 1f, 1f, 0f, 0f, - 0f, 0f, 0f, 1.4f } }, - { CurvePreset.Linear, new object[] { true, - 0f, 0f, 0f, 0f, - 1f, 1f, 0f, 0f } }, - { CurvePreset.Smoothstep, new object[] { false, - 0f, 0f, 0f, 0f, - 1f, 1f, 0f, 0f } }, + { CurvePreset.Constant, new CurveEditorPreset + { + LinearTangents = true, + Points = new List + { + new CurvePresetPoint { Time = 0f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 1f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f }, + } + } + }, + { CurvePreset.EaseIn, new CurveEditorPreset + { + LinearTangents = false, + Points = new List + { + new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = -1.4f, TangentOut = 0f }, + } + } + }, + { CurvePreset.EaseOut, new CurveEditorPreset + { + LinearTangents = false, + Points = new List + { + new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 1.4f }, + } + } + }, + { CurvePreset.Linear, new CurveEditorPreset + { + LinearTangents = true, + Points = new List + { + new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f }, + } + } + }, + { CurvePreset.Smoothstep, new CurveEditorPreset + { + LinearTangents = false, + Points = new List + { + new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f }, + } + } + }, }; /// @@ -1612,11 +1689,11 @@ namespace FlaxEditor.GUI { base.ApplyPreset(preset); - object[] data = PresetValues[preset]; - for (int i = 1; i < data.Length; i += 4) + CurveEditorPreset data = Presets[preset]; + foreach (var point in data.Points) { - float time = (float)data[i]; - object value = ConvertCurvePresetValueToCurveEditorType((float)data[i + 1]); + float time = point.Time; + object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value); AddKeyframe(time, value); } @@ -2414,19 +2491,20 @@ namespace FlaxEditor.GUI { base.ApplyPreset(preset); - object[] data = PresetValues[preset]; - for (int i = 1; i < data.Length; i += 4) + CurveEditorPreset data = Presets[preset]; + + foreach (var point in data.Points) { - float time = (float)data[i]; - object value = ConvertCurvePresetValueToCurveEditorType((float)data[i + 1]); - object tangentIn = ConvertCurvePresetValueToCurveEditorType((float)data[i + 2]); - object tangentOut = ConvertCurvePresetValueToCurveEditorType((float)data[i + 3]); + float time = point.Time; + object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value); + object tangentIn = ConvertCurvePresetValueToCurveEditorType((float)point.TangentIn); + object tangentOut = ConvertCurvePresetValueToCurveEditorType((float)point.TangentOut); AddKeyframe(time, value, tangentIn, tangentOut); } SelectAll(); - if ((bool)data[0]) + if (data.LinearTangents) SetTangentsLinear(); ShowWholeCurve(); From 26e24769bea94f4b3d73a283c582a6d2c05c67a5 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 11 Dec 2025 20:44:40 +0100 Subject: [PATCH 017/100] actually set things to the right thing How the hell did this happen?? --- Source/Editor/Viewport/MainEditorGizmoViewport.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index f9b382a0a..66e1b5210 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -499,6 +499,7 @@ namespace FlaxEditor.Viewport { if (!_gameViewActive) { + // Cache flags & values _preGameViewFlags = Task.ViewFlags; _preGameViewViewMode = Task.ViewMode; _gameViewWasGridShown = Grid.Enabled; @@ -506,11 +507,12 @@ namespace FlaxEditor.Viewport _gameViewWasNagivationShown = ShowNavigation; } + // Set flags & values Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame; Task.ViewMode = _gameViewActive ? _preGameViewViewMode : ViewMode.Default; - ShowFpsCounter = _gameViewActive ? _gameViewWasGridShown : false; - ShowNavigation = _gameViewActive ? _gameViewWasFpsCounterShown : false; - Grid.Enabled = _gameViewActive ? _gameViewWasNagivationShown : false; + ShowFpsCounter = _gameViewActive ? _gameViewWasFpsCounterShown : false; + ShowNavigation = _gameViewActive ? _gameViewWasNagivationShown : false; + Grid.Enabled = _gameViewActive ? _gameViewWasGridShown : false; _gameViewActive = !_gameViewActive; From 7445064d9738213785586ad413aea049f44e2ce2 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 14 Dec 2025 20:16:33 +0200 Subject: [PATCH 018/100] Improve installed .NET runtime version detection --- Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs index 28c3958df..d1c24e5cd 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs @@ -282,7 +282,7 @@ namespace Flax.Build dotnetRuntimeVersions = MergeVersions(dotnetRuntimeVersions, GetVersions(Path.Combine(dotnetPath, "shared", "Microsoft.NETCore.App"))); dotnetSdkVersions = dotnetSdkVersions.Where(x => File.Exists(Path.Combine(dotnetPath, "sdk", x, ".version"))); - dotnetRuntimeVersions = dotnetRuntimeVersions.Where(x => File.Exists(Path.Combine(dotnetPath, "shared", "Microsoft.NETCore.App", x, ".version"))); + dotnetRuntimeVersions = dotnetRuntimeVersions.Where(x => File.Exists(Path.Combine(dotnetPath, "shared", "Microsoft.NETCore.App", x, "System.dll"))); dotnetRuntimeVersions = dotnetRuntimeVersions.Where(x => Directory.Exists(Path.Combine(dotnetPath, "packs", "Microsoft.NETCore.App.Ref", x))); dotnetSdkVersions = dotnetSdkVersions.OrderByDescending(ParseVersion); From 5cd27032b41c31b583f967b7f6e938f583a05630 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 14 Dec 2025 20:18:21 +0200 Subject: [PATCH 019/100] Set C# language version to 14 with .NET 10 --- Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs index d1c24e5cd..137a22ccc 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs @@ -166,6 +166,7 @@ namespace Flax.Build /// public string CSharpLanguageVersion => Version.Major switch { + _ when Version.Major >= 10 => "14.0", _ when Version.Major >= 9 => "13.0", _ when Version.Major >= 8 => "12.0", _ when Version.Major >= 7 => "11.0", From ae03bc2dd27c10f93813a23b97bb5de2e7306b8d Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 14 Dec 2025 20:24:39 +0200 Subject: [PATCH 020/100] Improve Linux .NET runtime identifier detection Use the runtime identifier detected during runtime instead of calling the dotnet tool in order to query it. --- .../Flax.Build/Build/DotNet/DotNetSdk.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs index 137a22ccc..de0fa755c 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; using System.Collections.Generic; +using System.Runtime.InteropServices; using Microsoft.Win32; namespace Flax.Build @@ -187,7 +188,7 @@ namespace Flax.Build // Find system-installed SDK string dotnetPath = Environment.GetEnvironmentVariable("DOTNET_ROOT"); - string rid, ridFallback, arch; + string arch; IEnumerable dotnetSdkVersions = null, dotnetRuntimeVersions = null; switch (architecture) { @@ -209,8 +210,6 @@ namespace Flax.Build { case TargetPlatform.Windows: { - rid = $"win-{arch}"; - ridFallback = ""; #pragma warning disable CA1416 if (string.IsNullOrEmpty(dotnetPath)) { @@ -235,16 +234,12 @@ namespace Flax.Build } case TargetPlatform.Linux: { - rid = $"linux-{arch}"; - ridFallback = Utilities.ReadProcessOutput("dotnet", "--info").Split('\n').FirstOrDefault(x => x.StartsWith(" RID:"), "").Replace("RID:", "").Trim(); if (string.IsNullOrEmpty(dotnetPath)) dotnetPath ??= SearchForDotnetLocationLinux(); break; } case TargetPlatform.Mac: { - rid = $"osx-{arch}"; - ridFallback = ""; if (string.IsNullOrEmpty(dotnetPath)) { dotnetPath = "/usr/local/share/dotnet/"; // Officially recommended dotnet location @@ -258,7 +253,6 @@ namespace Flax.Build } if (Flax.Build.Platforms.MacPlatform.BuildingForx64) { - rid = "osx-x64"; dotnetPath = Path.Combine(dotnetPath, "x64"); architecture = TargetArchitecture.x64; } @@ -332,6 +326,20 @@ namespace Flax.Build VersionName = dotnetSdkVersion; RuntimeVersionName = dotnetRuntimeVersion; + string rid, ridFallback = ""; + switch (platform) + { + case TargetPlatform.Windows: rid = $"win-{arch}"; break; + case TargetPlatform.Linux: + { + rid = RuntimeInformation.RuntimeIdentifier; + ridFallback = $"linux-{arch}"; + break; + } + case TargetPlatform.Mac: rid = Flax.Build.Platforms.MacPlatform.BuildingForx64 ? "osx-x64" : $"osx-{arch}"; break; + default: throw new InvalidPlatformException(platform); + } + // Pick SDK runtime if (!TryAddHostRuntime(platform, architecture, rid) && !TryAddHostRuntime(platform, architecture, ridFallback)) { From e6fd761b80e9b020cf2b5bd10ca6d1b0cedf2724 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 17 Jan 2026 14:09:13 -0600 Subject: [PATCH 021/100] Fix missing light shadow resolution serialization. --- Source/Engine/Level/Actors/Light.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Level/Actors/Light.cpp b/Source/Engine/Level/Actors/Light.cpp index ad7fe54a2..6de7977c9 100644 --- a/Source/Engine/Level/Actors/Light.cpp +++ b/Source/Engine/Level/Actors/Light.cpp @@ -107,6 +107,7 @@ void LightWithShadow::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE(ContactShadowsLength); SERIALIZE(ShadowsUpdateRate); SERIALIZE(ShadowsUpdateRateAtDistance); + SERIALIZE(ShadowsResolution); } void LightWithShadow::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) @@ -125,4 +126,5 @@ void LightWithShadow::Deserialize(DeserializeStream& stream, ISerializeModifier* DESERIALIZE(ContactShadowsLength); DESERIALIZE(ShadowsUpdateRate); DESERIALIZE(ShadowsUpdateRateAtDistance); + DESERIALIZE(ShadowsResolution); } From ecfd03f79ca809bae576441b3447706fab642678 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 22 Jan 2026 18:49:47 +0100 Subject: [PATCH 022/100] Fix restoring `NuGet` packages for target with multiple projects Fix restoring `NuGet` packages to run before project build to ensure files are downloaded #3900 --- Source/Tools/Flax.Build/Build/Builder.cs | 61 +++++++++++++++++++ .../Flax.Build/Build/DotNet/Builder.DotNet.cs | 26 +------- .../Build/NativeCpp/BuildOptions.cs | 7 ++- .../Build/NativeCpp/Builder.NativeCpp.cs | 44 +------------ .../Tools/Flax.Build/Utilities/Utilities.cs | 17 ++---- 5 files changed, 77 insertions(+), 78 deletions(-) diff --git a/Source/Tools/Flax.Build/Build/Builder.cs b/Source/Tools/Flax.Build/Build/Builder.cs index 3a4286254..3b00daf72 100644 --- a/Source/Tools/Flax.Build/Build/Builder.cs +++ b/Source/Tools/Flax.Build/Build/Builder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using Flax.Build.Graph; using Flax.Build.NativeCpp; @@ -424,5 +425,65 @@ namespace Flax.Build return failed; } + + private static void DeployFiles(TaskGraph graph, Target target, BuildOptions targetBuildOptions, string outputPath) + { + using (new ProfileEventScope("DeployFiles")) + { + foreach (var srcFile in targetBuildOptions.OptionalDependencyFiles.Where(File.Exists).Union(targetBuildOptions.DependencyFiles)) + { + var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile)); + graph.AddCopyFile(dstFile, srcFile); + } + + if (targetBuildOptions.NugetPackageReferences.Any()) + { + var nugetPath = Utilities.GetNugetPackagesPath(); + var restore = true; + foreach (var reference in targetBuildOptions.NugetPackageReferences) + { + var path = reference.GetLibPath(nugetPath); + if (!File.Exists(path) && restore) + { + RestoreNugetPackages(graph, target, targetBuildOptions); + restore = false; + } + var dstFile = Path.Combine(outputPath, Path.GetFileName(path)); + graph.AddCopyFile(dstFile, path); + } + } + } + } + + private static void RestoreNugetPackages(TaskGraph graph, Target target, BuildOptions targetBuildOptions) + { + // Generate a dummy csproj file to restore package from it + var csprojPath = Path.Combine(targetBuildOptions.IntermediateFolder, "nuget.restore.csproj"); + var dotnetSdk = DotNetSdk.Instance; + var csProjectFileContent = new StringBuilder(); + csProjectFileContent.AppendLine(""); + csProjectFileContent.AppendLine(" "); + csProjectFileContent.AppendLine($" net{dotnetSdk.Version.Major}.{dotnetSdk.Version.Minor}"); + csProjectFileContent.AppendLine(" false"); + csProjectFileContent.AppendLine(" false"); + csProjectFileContent.AppendLine(" false"); + csProjectFileContent.AppendLine(" false"); + csProjectFileContent.AppendLine(" false"); + csProjectFileContent.AppendLine(" false"); + csProjectFileContent.AppendLine($" {dotnetSdk.CSharpLanguageVersion}"); + csProjectFileContent.AppendLine(" 512"); + csProjectFileContent.AppendLine(" true"); + csProjectFileContent.AppendLine(" "); + csProjectFileContent.AppendLine(" "); + foreach (var reference in targetBuildOptions.NugetPackageReferences) + csProjectFileContent.AppendLine($" "); + csProjectFileContent.AppendLine(" "); + csProjectFileContent.AppendLine(""); + Utilities.WriteFileIfChanged(csprojPath, csProjectFileContent.ToString()); + + // Restore packages using dotnet CLI (synchronous to prevent task ordering issues on C# library building) + Log.Info($"Restoring NuGet packages for target {target.Name}"); + Utilities.Run(Utilities.GetDotNetPath(), $"restore \"{csprojPath}\"", null, null, Utilities.RunOptions.DefaultTool); + } } } diff --git a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs index 23646a3d7..54136b490 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs @@ -135,27 +135,7 @@ namespace Flax.Build // Deploy files if (!target.IsPreBuilt) { - using (new ProfileEventScope("DeployFiles")) - { - foreach (var srcFile in targetBuildOptions.OptionalDependencyFiles.Where(File.Exists).Union(targetBuildOptions.DependencyFiles)) - { - var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile)); - graph.AddCopyFile(dstFile, srcFile); - } - - if (targetBuildOptions.NugetPackageReferences.Any()) - { - var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); - foreach (var reference in targetBuildOptions.NugetPackageReferences) - { - var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll"); - if (!File.Exists(path)) - Utilities.RestoreNugetPackages(graph, target); - var dstFile = Path.Combine(outputPath, Path.GetFileName(path)); - graph.AddCopyFile(dstFile, path); - } - } - } + DeployFiles(graph, target, targetBuildOptions, outputPath); } using (new ProfileEventScope("PostBuild")) @@ -301,10 +281,10 @@ namespace Flax.Build // Reference Nuget package if (buildData.TargetOptions.NugetPackageReferences.Any()) { - var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); + var nugetPath = Utilities.GetNugetPackagesPath(); foreach (var reference in buildOptions.NugetPackageReferences) { - var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll"); + var path = reference.GetLibPath(nugetPath); args.Add(string.Format("/reference:\"{0}\"", path)); } } diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs index 13ecd1982..ea1eb1ade 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs @@ -102,6 +102,11 @@ namespace Flax.Build.NativeCpp Version = version; Framework = framework; } + + internal string GetLibPath(string nugetPath) + { + return Path.Combine(nugetPath, Name, Version, "lib", Framework, $"{Name}.dll"); + } } /// @@ -167,7 +172,7 @@ namespace Flax.Build.NativeCpp /// /// The nuget package references. /// - public List NugetPackageReferences = new List(); + public HashSet NugetPackageReferences = new HashSet(); /// /// The collection of defines with preprocessing symbol for a source files of this module. Inherited by the modules that include it. diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs index 4c9d521b4..322f1c3c1 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs @@ -1057,27 +1057,7 @@ namespace Flax.Build // Deploy files if (!buildData.Target.IsPreBuilt) { - using (new ProfileEventScope("DeployFiles")) - { - foreach (var srcFile in targetBuildOptions.OptionalDependencyFiles.Where(File.Exists).Union(targetBuildOptions.DependencyFiles)) - { - var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile)); - graph.AddCopyFile(dstFile, srcFile); - } - - if (targetBuildOptions.NugetPackageReferences.Any()) - { - var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); - foreach (var reference in targetBuildOptions.NugetPackageReferences) - { - var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll"); - if (!File.Exists(path)) - Utilities.RestoreNugetPackages(graph, target); - var dstFile = Path.Combine(outputPath, Path.GetFileName(path)); - graph.AddCopyFile(dstFile, path); - } - } - } + DeployFiles(graph, target, targetBuildOptions, outputPath); } using (new ProfileEventScope("PostBuild")) @@ -1270,27 +1250,7 @@ namespace Flax.Build // Deploy files if (!buildData.Target.IsPreBuilt) { - using (new ProfileEventScope("DeployFiles")) - { - foreach (var srcFile in targetBuildOptions.OptionalDependencyFiles.Where(File.Exists).Union(targetBuildOptions.DependencyFiles)) - { - var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile)); - graph.AddCopyFile(dstFile, srcFile); - } - - if (targetBuildOptions.NugetPackageReferences.Any()) - { - var nugetPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); - foreach (var reference in targetBuildOptions.NugetPackageReferences) - { - var path = Path.Combine(nugetPath, reference.Name, reference.Version, "lib", reference.Framework, $"{reference.Name}.dll"); - if (!File.Exists(path)) - Utilities.RestoreNugetPackages(graph, target); - var dstFile = Path.Combine(outputPath, Path.GetFileName(path)); - graph.AddCopyFile(dstFile, path); - } - } - } + DeployFiles(graph, target, targetBuildOptions, outputPath); } using (new ProfileEventScope("PostBuild")) diff --git a/Source/Tools/Flax.Build/Utilities/Utilities.cs b/Source/Tools/Flax.Build/Utilities/Utilities.cs index 917b8aa77..872b269ee 100644 --- a/Source/Tools/Flax.Build/Utilities/Utilities.cs +++ b/Source/Tools/Flax.Build/Utilities/Utilities.cs @@ -42,21 +42,14 @@ namespace Flax.Build } /// - /// Restores a targets nuget packages. + /// Gets the NuGet packages cache folder path. /// - /// The task graph. - /// The target. - /// The dotnet path. - public static void RestoreNugetPackages(Graph.TaskGraph graph, Target target) + /// The path. + public static string GetNugetPackagesPath() { - var dotNetPath = GetDotNetPath(); - var task = graph.Add(); - task.WorkingDirectory = target.FolderPath; - task.InfoMessage = $"Restoring Nuget Packages for {target.Name}"; - task.CommandPath = dotNetPath; - task.CommandArguments = $"restore"; + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); } - + /// /// Gets the hash code for the string (the same for all platforms). Matches Engine algorithm for string hashing. /// From 6dbfd25bdb219f84750187cc84113bd869dcb1de Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 22 Jan 2026 19:57:16 +0100 Subject: [PATCH 023/100] Fix deploying NuGet packages to include correct library file name #3900 --- Source/Tools/Flax.Build/Build/Builder.cs | 32 +++++++++++++++---- .../Build/NativeCpp/BuildOptions.cs | 17 ++++++++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Source/Tools/Flax.Build/Build/Builder.cs b/Source/Tools/Flax.Build/Build/Builder.cs index 3b00daf72..d09947f09 100644 --- a/Source/Tools/Flax.Build/Build/Builder.cs +++ b/Source/Tools/Flax.Build/Build/Builder.cs @@ -438,18 +438,38 @@ namespace Flax.Build if (targetBuildOptions.NugetPackageReferences.Any()) { + // Find all packages to deploy (incl. dependencies) and restore if needed var nugetPath = Utilities.GetNugetPackagesPath(); - var restore = true; + var restoreOnce = true; + var nugetFiles = new HashSet(); foreach (var reference in targetBuildOptions.NugetPackageReferences) { - var path = reference.GetLibPath(nugetPath); - if (!File.Exists(path) && restore) + var folder = reference.GetLibFolder(nugetPath); + if (!Directory.Exists(folder) && restoreOnce) { + // Package binaries folder is missing so restore packages (incl. dependency packages) RestoreNugetPackages(graph, target, targetBuildOptions); - restore = false; + restoreOnce = false; } - var dstFile = Path.Combine(outputPath, Path.GetFileName(path)); - graph.AddCopyFile(dstFile, path); + + // Deploy library + var path = reference.GetLibPath(nugetPath, folder); + nugetFiles.Add(path); + + // Copy additional files (if included) + path = Path.ChangeExtension(path, "xml"); + if (File.Exists(path)) + nugetFiles.Add(path); + path = Path.ChangeExtension(path, "pdb"); + if (targetBuildOptions.Configuration != TargetConfiguration.Release && File.Exists(path)) + nugetFiles.Add(path); + } + + // Copy libraries from all referenced packages to the output folder + foreach (var file in nugetFiles) + { + var dstFile = Path.Combine(outputPath, Path.GetFileName(file)); + graph.AddCopyFile(dstFile, file); } } } diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs index ea1eb1ade..b0f116673 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs @@ -103,9 +103,22 @@ namespace Flax.Build.NativeCpp Framework = framework; } - internal string GetLibPath(string nugetPath) + internal string GetLibFolder(string nugetPath) { - return Path.Combine(nugetPath, Name, Version, "lib", Framework, $"{Name}.dll"); + return Path.Combine(nugetPath, Name, Version, "lib", Framework); + } + + internal string GetLibPath(string nugetPath, string libFolder = null) + { + if (libFolder == null) + libFolder = GetLibFolder(nugetPath); + var dlls = Directory.GetFiles(libFolder, "*.dll", SearchOption.TopDirectoryOnly); + if (dlls.Length == 0) + { + Log.Error($"Missing NuGet package \"{Name}, {Version}, {Framework}\" binaries (folder: {libFolder})"); + return string.Empty; + } + return dlls[0]; } } From b09fbe2d9be99953554f18cc8707199f086fad6a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 22 Jan 2026 23:39:38 +0100 Subject: [PATCH 024/100] Fix deploying NuGet packages to include dependencies (recursive) #3900 --- Source/Tools/Flax.Build/Build/Builder.cs | 57 +++++++++++++++---- .../Build/NativeCpp/BuildOptions.cs | 29 +++++++++- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/Source/Tools/Flax.Build/Build/Builder.cs b/Source/Tools/Flax.Build/Build/Builder.cs index d09947f09..775bef1dc 100644 --- a/Source/Tools/Flax.Build/Build/Builder.cs +++ b/Source/Tools/Flax.Build/Build/Builder.cs @@ -440,6 +440,7 @@ namespace Flax.Build { // Find all packages to deploy (incl. dependencies) and restore if needed var nugetPath = Utilities.GetNugetPackagesPath(); + Log.Verbose($"Deploying NuGet packages from {nugetPath}"); var restoreOnce = true; var nugetFiles = new HashSet(); foreach (var reference in targetBuildOptions.NugetPackageReferences) @@ -452,17 +453,7 @@ namespace Flax.Build restoreOnce = false; } - // Deploy library - var path = reference.GetLibPath(nugetPath, folder); - nugetFiles.Add(path); - - // Copy additional files (if included) - path = Path.ChangeExtension(path, "xml"); - if (File.Exists(path)) - nugetFiles.Add(path); - path = Path.ChangeExtension(path, "pdb"); - if (targetBuildOptions.Configuration != TargetConfiguration.Release && File.Exists(path)) - nugetFiles.Add(path); + DeployNuGetPackage(nugetPath, targetBuildOptions, nugetFiles, reference, folder); } // Copy libraries from all referenced packages to the output folder @@ -505,5 +496,49 @@ namespace Flax.Build Log.Info($"Restoring NuGet packages for target {target.Name}"); Utilities.Run(Utilities.GetDotNetPath(), $"restore \"{csprojPath}\"", null, null, Utilities.RunOptions.DefaultTool); } + + private static void DeployNuGetPackage(string nugetPath, BuildOptions targetBuildOptions, HashSet nugetFiles, NugetPackage package, string folder = null) + { + // Deploy library + var path = package.GetLibPath(nugetPath, folder); + if (!File.Exists(path)) + return; + Log.Verbose($"Deploying NuGet package {package.Name}, {package.Version}, {package.Framework}"); + nugetFiles.Add(path); + + // Copy additional files (if included) + path = Path.ChangeExtension(path, "xml"); + if (File.Exists(path)) + nugetFiles.Add(path); + path = Path.ChangeExtension(path, "pdb"); + if (targetBuildOptions.Configuration != TargetConfiguration.Release && File.Exists(path)) + nugetFiles.Add(path); + + // Read package dependencies + var nuspecFile = package.GetNuspecPath(nugetPath); + if (File.Exists(nuspecFile)) + { + var doc = System.Xml.Linq.XDocument.Load(nuspecFile); + var root = (System.Xml.Linq.XElement)doc.FirstNode; + var metadataNode = root.Descendants().First(x => x.Name.LocalName== "metadata"); + var dependenciesNode = metadataNode.Descendants().First(x => x.Name.LocalName == "dependencies"); + var groupNode = dependenciesNode.Descendants().FirstOrDefault(x => x.Attribute("targetFramework")?.Value == package.Framework); + if (groupNode == null) + { + Log.Warning($"Cannot find framework {package.Framework} inside NuGet package {package.Name}, {package.Version}"); + return; + } + foreach (var dependency in groupNode.Descendants()) + { + if (dependency.Name.LocalName != "dependency") + continue; + + // Deploy dependency package + var dependencyId = dependency.Attribute("id").Value; + var dependencyVersion = dependency.Attribute("version").Value; + DeployNuGetPackage(nugetPath, targetBuildOptions, nugetFiles, new NugetPackage { Name = dependencyId, Version = dependencyVersion, Framework = package.Framework } ); + } + } + } } } diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs index b0f116673..8251ff186 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs @@ -105,7 +105,34 @@ namespace Flax.Build.NativeCpp internal string GetLibFolder(string nugetPath) { - return Path.Combine(nugetPath, Name, Version, "lib", Framework); + var libFolder = Path.Combine(nugetPath, Name, Version, "lib", Framework); + if (Directory.Exists(libFolder)) + return libFolder; + + // Try to find nearest framework folder + if (Framework.StartsWith("net")) + { + var baseVersion = int.Parse(Framework.Substring(3, Framework.IndexOf('.') - 3)); + for (int version = baseVersion - 1; version >= 5; version--) + { + var framework = $"net{version}.0"; + libFolder = Path.Combine(nugetPath, Name, Version, "lib", framework); + if (Directory.Exists(libFolder)) + { + Framework = framework; + return libFolder; + } + } + } + + Log.Error($"Missing NuGet package \"{Name}, {Version}, {Framework}\" (nuget: {nugetPath})"); + return string.Empty; + } + + internal string GetNuspecPath(string nugetPath) + { + var files = Directory.GetFiles(Path.Combine(nugetPath, Name, Version), "*.nuspec", SearchOption.TopDirectoryOnly); + return files[0]; } internal string GetLibPath(string nugetPath, string libFolder = null) From 004e02af73c88dc08306c3211d47decaf36cad68 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 26 Jan 2026 20:46:49 -0600 Subject: [PATCH 025/100] Fix double scaling in canvas scalar. --- Source/Engine/UI/GUI/CanvasScaler.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/Engine/UI/GUI/CanvasScaler.cs b/Source/Engine/UI/GUI/CanvasScaler.cs index 6bd18ea51..1e30fd22f 100644 --- a/Source/Engine/UI/GUI/CanvasScaler.cs +++ b/Source/Engine/UI/GUI/CanvasScaler.cs @@ -449,8 +449,7 @@ namespace FlaxEngine.GUI /// public override bool RayCast(ref Float2 location, out Control hit) { - var p = location / _scale; - if (RayCastChildren(ref p, out hit)) + if (RayCastChildren(ref location, out hit)) return true; return base.RayCast(ref location, out hit); } From a7016d11867f4ed022234cd0d25c395f84eb52a0 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 26 Jan 2026 21:56:30 -0600 Subject: [PATCH 026/100] Fix issue with tabs not collapsing panel 1 if no tabs on panel 1 --- Source/Editor/GUI/Docking/DockPanel.cs | 51 +++++++++++++++++++++++++- Source/Editor/Modules/WindowsModule.cs | 5 +++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/Source/Editor/GUI/Docking/DockPanel.cs b/Source/Editor/GUI/Docking/DockPanel.cs index bfac161f0..c8900dcba 100644 --- a/Source/Editor/GUI/Docking/DockPanel.cs +++ b/Source/Editor/GUI/Docking/DockPanel.cs @@ -469,7 +469,7 @@ namespace FlaxEditor.GUI.Docking var childPanels = _childPanels.ToArray(); if (childPanels.Length != 0) { - // Move tabs from child panels into this one + // Fallback: move tabs from child panels into this one. DockWindow selectedTab = null; foreach (var childPanel in childPanels) { @@ -490,7 +490,8 @@ namespace FlaxEditor.GUI.Docking { // Unlink splitter var splitterParent = splitter.Parent; - Assert.IsNotNull(splitterParent); + if (splitterParent == null) + return; splitter.Parent = null; // Move controls from second split panel to the split panel parent @@ -507,17 +508,63 @@ namespace FlaxEditor.GUI.Docking splitter.Dispose(); } } + else if (IsMaster && _childPanels.Count != 0) + { + if (TryCollapseSplitter(_tabsProxy?.Parent as Panel)) + return; + } else if (!IsMaster) { throw new InvalidOperationException(); } } + else if (_childPanels.Count != 0) + { + if (TryCollapseSplitter(_tabsProxy?.Parent as Panel)) + return; + } else if (!IsMaster) { throw new InvalidOperationException(); } } + internal bool CollapseEmptyTabsProxy() + { + if (TabsCount == 0 && ChildPanelsCount > 0) + { + return TryCollapseSplitter(_tabsProxy?.Parent as Panel); + } + return false; + } + + private bool TryCollapseSplitter(Panel removedPanelParent) + { + if (removedPanelParent == null) + return false; + if (!(removedPanelParent.Parent is SplitPanel tabsSplitter)) + return false; + + var splitterParent = tabsSplitter.Parent; + if (splitterParent == null) + return false; + tabsSplitter.Parent = null; + + var scrPanel = removedPanelParent == tabsSplitter.Panel2 ? tabsSplitter.Panel1 : tabsSplitter.Panel2; + var srcPanelChildrenCount = scrPanel.ChildrenCount; + for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + { + scrPanel.GetChild(i).Parent = splitterParent; + } + Assert.IsTrue(scrPanel.ChildrenCount == 0); + Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + + tabsSplitter.Dispose(); + if (_tabsProxy != null && _tabsProxy.Parent == removedPanelParent) + _tabsProxy = null; + return true; + } + internal virtual void DockWindowInternal(DockState state, DockWindow window, bool autoSelect = true, float? splitterValue = null) { DockWindow(state, window, autoSelect, splitterValue); diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 218394a3b..162fa3a40 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -491,10 +491,15 @@ namespace FlaxEditor.Modules Editor.LogWarning("Empty panel inside layout."); p.RemoveIt(); } + else + { + p.CollapseEmptyTabsProxy(); + } } } panel.SelectTab(selectedTab); + panel.CollapseEmptyTabsProxy(); } private static void SaveBounds(XmlWriter writer, Window win) From d9f9401c5a0fd16287240baf38bab90ac152fbea Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Wed, 28 Jan 2026 20:04:39 -0600 Subject: [PATCH 027/100] Fix tree node not expanding on drag over arrow. --- Source/Editor/GUI/Tree/TreeNode.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs index ed1257819..b0ce8c251 100644 --- a/Source/Editor/GUI/Tree/TreeNode.cs +++ b/Source/Editor/GUI/Tree/TreeNode.cs @@ -1140,8 +1140,11 @@ namespace FlaxEditor.GUI.Tree ParentTree.DraggedOverNode = this; // Expand node if mouse goes over arrow - if (ArrowRect.Contains(location) && HasAnyVisibleChild) + if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed) + { Expand(true); + ParentTree?.FlushPendingPerformLayout(); + } result = OnDragEnterHeader(data); } @@ -1172,8 +1175,11 @@ namespace FlaxEditor.GUI.Tree ParentTree.DraggedOverNode = this; // Expand node if mouse goes over arrow - if (ArrowRect.Contains(location) && HasAnyVisibleChild) + if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed) + { Expand(true); + ParentTree?.FlushPendingPerformLayout(); + } if (!_isDragOverHeader) result = OnDragEnterHeader(data); From c9d16e16cc3a21d5dd6e90ac417900d83fed862f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 29 Jan 2026 10:55:10 +0100 Subject: [PATCH 028/100] Fix Rich Text Box vertical alignment of the inlined images and whole contents #3502 ae4ae7a638e26a33985836b33362942c6e65a692 7ee2e66881e732f1df98a2aa205ca99c8f36635d --- .../UI/GUI/Common/RichTextBox.Parsing.cs | 66 ++++++++++++------- .../Engine/UI/GUI/Common/RichTextBox.Tags.cs | 21 +++--- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs index 20ef1c401..bb6ee22a5 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs @@ -143,6 +143,40 @@ namespace FlaxEngine.GUI context.Caret.X = 0; OnLineAdded(ref context, _text.Length - 1); } + + // Organize lines vertically + if (_textBlocks.Count != 0) + { + var lastBlock = _textBlocks[_textBlocks.Count - 1]; + + // Get style (global or leftover from style stack or the last lime) + var verticalAlignments = _textStyle.Alignment; + if (context.StyleStack.Count > 1) + verticalAlignments = context.StyleStack.Peek().Alignment; + else if ((lastBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask) != TextBlockStyle.Alignments.Baseline) + verticalAlignments = lastBlock.Style.Alignment; + + var totalSize = lastBlock.Bounds.BottomRight; + var sizeOffset = Size - totalSize; + var textBlocks = CollectionsMarshal.AsSpan(_textBlocks); + if ((verticalAlignments & TextBlockStyle.Alignments.Middle) == TextBlockStyle.Alignments.Middle) + { + sizeOffset.Y *= 0.5f; + for (int i = 0; i < _textBlocks.Count; i++) + { + ref TextBlock textBlock = ref textBlocks[i]; + textBlock.Bounds.Location.Y += sizeOffset.Y; + } + } + else if ((verticalAlignments & TextBlockStyle.Alignments.Bottom) == TextBlockStyle.Alignments.Bottom) + { + for (int i = 0; i < _textBlocks.Count; i++) + { + ref TextBlock textBlock = ref textBlocks[i]; + textBlock.Bounds.Location.Y += sizeOffset.Y; + } + } + } } /// @@ -239,14 +273,15 @@ namespace FlaxEngine.GUI } // Organize text blocks within line - var horizontalAlignments = TextBlockStyle.Alignments.Baseline; - var verticalAlignments = TextBlockStyle.Alignments.Baseline; + var lineAlignments = TextBlockStyle.Alignments.Baseline; for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) { ref TextBlock textBlock = ref textBlocks[i]; var vOffset = lineSize.Y - textBlock.Bounds.Height; - horizontalAlignments |= textBlock.Style.Alignment & TextBlockStyle.Alignments.HorizontalMask; - verticalAlignments |= textBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask; + if (i == context.LineStartTextBlockIndex) + lineAlignments = textBlock.Style.Alignment; + else + lineAlignments &= textBlock.Style.Alignment; switch (textBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask) { case TextBlockStyle.Alignments.Baseline: @@ -275,9 +310,9 @@ namespace FlaxEngine.GUI } } - // Organize blocks within whole container + // Organize whole line horizontally var sizeOffset = Size - lineSize; - if ((horizontalAlignments & TextBlockStyle.Alignments.Center) == TextBlockStyle.Alignments.Center) + if ((lineAlignments & TextBlockStyle.Alignments.Center) == TextBlockStyle.Alignments.Center) { sizeOffset.X *= 0.5f; for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) @@ -286,7 +321,7 @@ namespace FlaxEngine.GUI textBlock.Bounds.Location.X += sizeOffset.X; } } - else if ((horizontalAlignments & TextBlockStyle.Alignments.Right) == TextBlockStyle.Alignments.Right) + else if ((lineAlignments & TextBlockStyle.Alignments.Right) == TextBlockStyle.Alignments.Right) { for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) { @@ -294,23 +329,6 @@ namespace FlaxEngine.GUI textBlock.Bounds.Location.X += sizeOffset.X; } } - if ((verticalAlignments & TextBlockStyle.Alignments.Middle) == TextBlockStyle.Alignments.Middle) - { - sizeOffset.Y *= 0.5f; - for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) - { - ref TextBlock textBlock = ref textBlocks[i]; - textBlock.Bounds.Location.Y += sizeOffset.Y; - } - } - else if ((verticalAlignments & TextBlockStyle.Alignments.Bottom) == TextBlockStyle.Alignments.Bottom) - { - for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) - { - ref TextBlock textBlock = ref textBlocks[i]; - textBlock.Bounds.Location.Y += sizeOffset.Y; - } - } // Move to the next line context.LineStartCharacterIndex = lineEnd + 1; diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs index b57fac47d..3bb99762f 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs @@ -175,7 +175,7 @@ namespace FlaxEngine.GUI // Setup size var font = imageBlock.Style.Font.GetFont(); if (font) - imageBlock.Bounds.Size = new Float2(font.Height); + imageBlock.Bounds.Size = new Float2(font.Ascender); var imageSize = image.Size; imageBlock.Bounds.Size.X *= imageSize.X / imageSize.Y; // Keep original aspect ratio bool hasWidth = TryParseNumberTag(ref tag, "width", imageBlock.Bounds.Width, out var width); @@ -215,16 +215,16 @@ namespace FlaxEngine.GUI switch (valign) { case "top": - style.Alignment = TextBlockStyle.Alignments.Top; + style.Alignment |= TextBlockStyle.Alignments.Top; break; case "bottom": - style.Alignment = TextBlockStyle.Alignments.Bottom; + style.Alignment |= TextBlockStyle.Alignments.Bottom; break; case "middle": - style.Alignment = TextBlockStyle.Alignments.Middle; + style.Alignment |= TextBlockStyle.Alignments.Middle; break; case "baseline": - style.Alignment = TextBlockStyle.Alignments.Baseline; + style.Alignment |= TextBlockStyle.Alignments.Baseline; break; } } @@ -243,17 +243,17 @@ namespace FlaxEngine.GUI var style = context.StyleStack.Peek(); if (tag.Attributes.TryGetValue(string.Empty, out var valign)) { - style.Alignment &= ~TextBlockStyle.Alignments.VerticalMask; + style.Alignment &= ~TextBlockStyle.Alignments.HorizontalMask; switch (valign) { case "left": - style.Alignment = TextBlockStyle.Alignments.Left; + style.Alignment |= TextBlockStyle.Alignments.Left; break; case "right": - style.Alignment = TextBlockStyle.Alignments.Right; + style.Alignment |= TextBlockStyle.Alignments.Right; break; case "center": - style.Alignment = TextBlockStyle.Alignments.Center; + style.Alignment |= TextBlockStyle.Alignments.Center; break; } } @@ -270,7 +270,8 @@ namespace FlaxEngine.GUI else { var style = context.StyleStack.Peek(); - style.Alignment = TextBlockStyle.Alignments.Center; + style.Alignment &= ~TextBlockStyle.Alignments.HorizontalMask; + style.Alignment |= TextBlockStyle.Alignments.Center; context.StyleStack.Push(style); } } From e834fc5a428b01bfa1e7a484405bcc879d1593b6 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 29 Jan 2026 12:35:36 +0100 Subject: [PATCH 029/100] Add more profiler categories for navigation --- Source/Engine/Navigation/NavMesh.cpp | 3 ++- Source/Engine/Navigation/NavMeshBuilder.cpp | 9 +++++---- Source/Engine/Navigation/NavMeshRuntime.cpp | 6 +++--- Source/Engine/Navigation/Navigation.cpp | 2 +- Source/Engine/Profiler/ProfilerMemory.cpp | 2 ++ Source/Engine/Profiler/ProfilerMemory.h | 4 ++++ 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Navigation/NavMesh.cpp b/Source/Engine/Navigation/NavMesh.cpp index 5593d732a..5932607a0 100644 --- a/Source/Engine/Navigation/NavMesh.cpp +++ b/Source/Engine/Navigation/NavMesh.cpp @@ -25,6 +25,7 @@ NavMesh::NavMesh(const SpawnParams& params) void NavMesh::SaveNavMesh() { #if COMPILE_WITH_ASSETS_IMPORTER + PROFILE_MEM(NavigationMesh); // Skip if scene is missing const auto scene = GetScene(); @@ -111,7 +112,7 @@ void NavMesh::OnAssetLoaded(Asset* asset, void* caller) if (Data.Tiles.HasItems()) return; ScopeLock lock(DataAsset->Locker); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); // Remove added tiles if (_navMeshActive) diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index e92173846..1f43b7fda 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -733,7 +733,7 @@ public: bool Run() override { PROFILE_CPU_NAMED("BuildNavMeshTile"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationBuilding); const auto navMesh = NavMesh.Get(); if (!navMesh) return false; @@ -1095,6 +1095,7 @@ void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds, bool rebuild else if (settings->AutoAddMissingNavMeshes) { // Spawn missing navmesh + PROFILE_MEM(Navigation); navMesh = New(); navMesh->SetStaticFlags(StaticFlags::FullyStatic); navMesh->SetName(TEXT("NavMesh.") + navMeshProperties.Name); @@ -1156,7 +1157,7 @@ void ClearNavigation(Scene* scene) void NavMeshBuilder::Update() { - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationBuilding); ScopeLock lock(NavBuildQueueLocker); // Process nav mesh building requests and kick the tasks @@ -1207,7 +1208,7 @@ void NavMeshBuilder::Build(Scene* scene, float timeoutMs) } PROFILE_CPU_NAMED("NavMeshBuilder"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationBuilding); ScopeLock lock(NavBuildQueueLocker); BuildRequest req; @@ -1244,7 +1245,7 @@ void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float t } PROFILE_CPU_NAMED("NavMeshBuilder"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationBuilding); ScopeLock lock(NavBuildQueueLocker); BuildRequest req; diff --git a/Source/Engine/Navigation/NavMeshRuntime.cpp b/Source/Engine/Navigation/NavMeshRuntime.cpp index 2758077c6..911700e19 100644 --- a/Source/Engine/Navigation/NavMeshRuntime.cpp +++ b/Source/Engine/Navigation/NavMeshRuntime.cpp @@ -326,7 +326,7 @@ void NavMeshRuntime::EnsureCapacity(int32 tilesToAddCount) if (newTilesCount <= capacity) return; PROFILE_CPU_NAMED("NavMeshRuntime.EnsureCapacity"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); // Navmesh tiles capacity growing rule int32 newCapacity = capacity ? capacity : 32; @@ -387,7 +387,7 @@ void NavMeshRuntime::AddTiles(NavMesh* navMesh) return; auto& data = navMesh->Data; PROFILE_CPU_NAMED("NavMeshRuntime.AddTiles"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); ScopeLock lock(Locker); // Validate data (must match navmesh) or init navmesh to match the tiles options @@ -419,7 +419,7 @@ void NavMeshRuntime::AddTile(NavMesh* navMesh, NavMeshTileData& tileData) ASSERT(navMesh); auto& data = navMesh->Data; PROFILE_CPU_NAMED("NavMeshRuntime.AddTile"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); ScopeLock lock(Locker); // Validate data (must match navmesh) or init navmesh to match the tiles options diff --git a/Source/Engine/Navigation/Navigation.cpp b/Source/Engine/Navigation/Navigation.cpp index 34983652f..06413ea7f 100644 --- a/Source/Engine/Navigation/Navigation.cpp +++ b/Source/Engine/Navigation/Navigation.cpp @@ -180,7 +180,7 @@ NavigationService NavigationServiceInstance; void* dtAllocDefault(size_t size, dtAllocHint) { - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); return Allocator::Allocate(size); } diff --git a/Source/Engine/Profiler/ProfilerMemory.cpp b/Source/Engine/Profiler/ProfilerMemory.cpp index c936ff5b2..794a208b0 100644 --- a/Source/Engine/Profiler/ProfilerMemory.cpp +++ b/Source/Engine/Profiler/ProfilerMemory.cpp @@ -260,6 +260,8 @@ void InitProfilerMemory(const Char* cmdLine, int32 stage) INIT_PARENT(Content, ContentFiles); INIT_PARENT(Level, LevelFoliage); INIT_PARENT(Level, LevelTerrain); + INIT_PARENT(Navigation, NavigationMesh); + INIT_PARENT(Navigation, NavigationBuilding); INIT_PARENT(Scripting, ScriptingVisual); INIT_PARENT(Scripting, ScriptingCSharp); INIT_PARENT(ScriptingCSharp, ScriptingCSharpGCCommitted); diff --git a/Source/Engine/Profiler/ProfilerMemory.h b/Source/Engine/Profiler/ProfilerMemory.h index 5dddb912b..1b9139b9a 100644 --- a/Source/Engine/Profiler/ProfilerMemory.h +++ b/Source/Engine/Profiler/ProfilerMemory.h @@ -105,6 +105,10 @@ public: // Total navigation system memory. Navigation, + // Navigation mesh memory. + NavigationMesh, + // Navigation mesh builder memory. + NavigationBuilding, // Total networking system memory. Networking, From 9a5bc444ba7d9b9675ca4f2fc5e20835233ed355 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 29 Jan 2026 12:38:23 +0100 Subject: [PATCH 030/100] Fix build issues on MSVC on Windows --- .../DirectX/DX11/GPUShaderProgramDX11.h | 1 + .../GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp | 11 ++--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h index 319e1a939..567cbb618 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h +++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h @@ -4,6 +4,7 @@ #include "Engine/Graphics/Shaders/GPUShaderProgram.h" #include "Engine/Core/Types/DataContainer.h" +#include "Engine/Core/Collections/Dictionary.h" #include "../IncludeDirectXHeaders.h" #if GRAPHICS_API_DIRECTX11 diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp index 98143c7c3..4dc923234 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp @@ -3,16 +3,9 @@ #if GRAPHICS_API_DIRECTX12 #include "Engine/Graphics/Config.h" +#include "Engine/Platform/Platform.h" +#include "../IncludeDirectXHeaders.h" #if USE_PIX && GPU_ALLOW_PROFILE_EVENTS -// Include these header files before pix3 -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#define NOGDI -#define NODRAWTEXT -//#define NOCTLMGR -#define NOFLATSBAPIS -#include -#include #include #endif #include "GPUContextDX12.h" From 277dabc8b449e24ae5b36ff483df5aa70fc2fad1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 29 Jan 2026 13:07:59 +0100 Subject: [PATCH 031/100] Fix documentation typos --- Source/Engine/Navigation/NavMeshRuntime.h | 4 +- Source/Engine/Navigation/Navigation.h | 4 +- Source/Engine/Physics/Physics.h | 54 +++++++++++------------ Source/Engine/Physics/PhysicsScene.h | 54 +++++++++++------------ 4 files changed, 58 insertions(+), 58 deletions(-) diff --git a/Source/Engine/Navigation/NavMeshRuntime.h b/Source/Engine/Navigation/NavMeshRuntime.h index 1ca6607b9..9e1165196 100644 --- a/Source/Engine/Navigation/NavMeshRuntime.h +++ b/Source/Engine/Navigation/NavMeshRuntime.h @@ -111,7 +111,7 @@ public: /// The start position. /// The result hit information. Valid only when query succeed. /// The maximum distance to search for wall (search radius). - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool FindDistanceToWall(const Vector3& startPosition, NavMeshHit& hitInfo, float maxDistance = MAX_float) const; /// @@ -187,7 +187,7 @@ public: /// The start position. /// The end position. /// The result hit information. Valid only when query succeed. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo) const; public: diff --git a/Source/Engine/Navigation/Navigation.h b/Source/Engine/Navigation/Navigation.h index 4d8b181e7..434817ca1 100644 --- a/Source/Engine/Navigation/Navigation.h +++ b/Source/Engine/Navigation/Navigation.h @@ -19,7 +19,7 @@ public: /// The start position. /// The result hit information. Valid only when query succeed. /// The maximum distance to search for wall (search radius). - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool FindDistanceToWall(const Vector3& startPosition, API_PARAM(Out) NavMeshHit& hitInfo, float maxDistance = MAX_float); /// @@ -81,7 +81,7 @@ public: /// The start position. /// The end position. /// The result hit information. Valid only when query succeed. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo); public: diff --git a/Source/Engine/Physics/Physics.h b/Source/Engine/Physics/Physics.h index 2fc116020..85cd5e77b 100644 --- a/Source/Engine/Physics/Physics.h +++ b/Source/Engine/Physics/Physics.h @@ -102,7 +102,7 @@ public: /// The end position of the line. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool LineCast(const Vector3& start, const Vector3& end, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -113,18 +113,18 @@ public: /// The result hit information. Valid only when method returns true. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool LineCast(const Vector3& start, const Vector3& end, API_PARAM(Out) RayCastHit& hitInfo, uint32 layerMask = MAX_uint32, bool hitTriggers = true); // - /// Performs a line between two points in the scene, returns all hitpoints infos. + /// Performs a line between two points in the scene, returns all hit points info. /// /// The origin of the ray. /// The end position of the line. /// The result hits. Valid only when method returns true. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool LineCastAll(const Vector3& start, const Vector3& end, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -135,7 +135,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool RayCast(const Vector3& origin, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -147,7 +147,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool RayCast(const Vector3& origin, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -159,7 +159,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool RayCastAll(const Vector3& origin, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -172,7 +172,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() static bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -186,7 +186,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() static bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -200,7 +200,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() static bool BoxCastAll(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -212,7 +212,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() static bool SphereCast(const Vector3& center, float radius, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -225,7 +225,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() static bool SphereCast(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -238,7 +238,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() static bool SphereCastAll(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -252,7 +252,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() static bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -267,7 +267,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() static bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -282,7 +282,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() static bool CapsuleCastAll(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -296,7 +296,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() static bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -311,7 +311,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() static bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -326,7 +326,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() static bool ConvexCastAll(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -375,7 +375,7 @@ public: API_FUNCTION() static bool CheckConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given box. + /// Finds all colliders touching or inside the given box. /// /// The box center. /// The half size of the box in each direction. @@ -387,7 +387,7 @@ public: API_FUNCTION() static bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given sphere. + /// Finds all colliders touching or inside the given sphere. /// /// The sphere center. /// The radius of the sphere. @@ -398,7 +398,7 @@ public: API_FUNCTION() static bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given capsule. + /// Finds all colliders touching or inside the given capsule. /// /// The capsule center. /// The radius of the capsule. @@ -411,7 +411,7 @@ public: API_FUNCTION() static bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given convex mesh. + /// Finds all colliders touching or inside the given convex mesh. /// /// The convex mesh center. /// Collision data of the convex mesh. @@ -424,7 +424,7 @@ public: API_FUNCTION() static bool OverlapConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given box. + /// Finds all colliders touching or inside the given box. /// /// The box center. /// The half size of the box in each direction. @@ -436,7 +436,7 @@ public: API_FUNCTION() static bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given sphere. + /// Finds all colliders touching or inside the given sphere. /// /// The sphere center. /// The radius of the sphere. @@ -447,7 +447,7 @@ public: API_FUNCTION() static bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given capsule. + /// Finds all colliders touching or inside the given capsule. /// /// The capsule center. /// The radius of the capsule. @@ -460,7 +460,7 @@ public: API_FUNCTION() static bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given convex mesh. + /// Finds all colliders touching or inside the given convex mesh. /// /// The convex mesh center. /// Collision data of the convex mesh. diff --git a/Source/Engine/Physics/PhysicsScene.h b/Source/Engine/Physics/PhysicsScene.h index 602e6f713..a7cb91cbe 100644 --- a/Source/Engine/Physics/PhysicsScene.h +++ b/Source/Engine/Physics/PhysicsScene.h @@ -140,7 +140,7 @@ public: /// The end position of the line. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool LineCast(const Vector3& start, const Vector3& end, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -151,18 +151,18 @@ public: /// The result hit information. Valid only when method returns true. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool LineCast(const Vector3& start, const Vector3& end, API_PARAM(Out) RayCastHit& hitInfo, uint32 layerMask = MAX_uint32, bool hitTriggers = true); // - /// Performs a line between two points in the scene, returns all hitpoints infos. + /// Performs a line between two points in the scene, returns all hit points info. /// /// The origin of the ray. /// The normalized direction of the ray. /// The result hits. Valid only when method returns true. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool LineCastAll(const Vector3& start, const Vector3& end, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -173,7 +173,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool RayCast(const Vector3& origin, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -185,7 +185,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool RayCast(const Vector3& origin, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -197,7 +197,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool RayCastAll(const Vector3& origin, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -210,7 +210,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -224,7 +224,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -238,7 +238,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() bool BoxCastAll(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -250,7 +250,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() bool SphereCast(const Vector3& center, float radius, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -263,7 +263,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() bool SphereCast(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -276,7 +276,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() bool SphereCastAll(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -290,7 +290,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -305,7 +305,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -320,7 +320,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() bool CapsuleCastAll(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -334,7 +334,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -349,7 +349,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -364,7 +364,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() bool ConvexCastAll(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -413,7 +413,7 @@ public: API_FUNCTION() bool CheckConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given box. + /// Finds all colliders touching or inside the given box. /// /// The box center. /// The half size of the box in each direction. @@ -425,7 +425,7 @@ public: API_FUNCTION() bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given sphere. + /// Finds all colliders touching or inside the given sphere. /// /// The sphere center. /// The radius of the sphere. @@ -436,7 +436,7 @@ public: API_FUNCTION() bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given capsule. + /// Finds all colliders touching or inside the given capsule. /// /// The capsule center. /// The radius of the capsule. @@ -449,7 +449,7 @@ public: API_FUNCTION() bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given convex mesh. + /// Finds all colliders touching or inside the given convex mesh. /// /// The convex mesh center. /// Collision data of the convex mesh. @@ -462,7 +462,7 @@ public: API_FUNCTION() bool OverlapConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given box. + /// Finds all colliders touching or inside the given box. /// /// The box center. /// The half size of the box in each direction. @@ -474,7 +474,7 @@ public: API_FUNCTION() bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given sphere. + /// Finds all colliders touching or inside the given sphere. /// /// The sphere center. /// The radius of the sphere. @@ -485,7 +485,7 @@ public: API_FUNCTION() bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given capsule. + /// Finds all colliders touching or inside the given capsule. /// /// The capsule center. /// The radius of the capsule. @@ -498,7 +498,7 @@ public: API_FUNCTION() bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given convex mesh. + /// Finds all colliders touching or inside the given convex mesh. /// /// The convex mesh center. /// Collision data of the convex mesh. From 528b4c89ce8f1410d612e611f6c7549ae12b3cce Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 29 Jan 2026 17:15:04 +0100 Subject: [PATCH 032/100] Add memory profiler category for engine debug data --- Source/Engine/Debug/DebugCommands.cpp | 4 +++ Source/Engine/Debug/DebugDraw.cpp | 30 +++++++++++++++++++++-- Source/Engine/Profiler/ProfilerMemory.cpp | 1 + Source/Engine/Profiler/ProfilerMemory.h | 2 ++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Debug/DebugCommands.cpp b/Source/Engine/Debug/DebugCommands.cpp index fa171d5dd..58cf2894b 100644 --- a/Source/Engine/Debug/DebugCommands.cpp +++ b/Source/Engine/Debug/DebugCommands.cpp @@ -8,6 +8,7 @@ #include "Engine/Threading/Threading.h" #include "Engine/Threading/Task.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Scripting/BinaryModule.h" #include "Engine/Scripting/Scripting.h" #include "Engine/Scripting/ManagedCLR/MAssembly.h" @@ -219,6 +220,7 @@ namespace if (module == GetBinaryModuleCorlib()) return; PROFILE_CPU(); + PROFILE_MEM(EngineDebug); #if USE_CSHARP if (auto* managedModule = dynamic_cast(module)) @@ -381,6 +383,7 @@ DebugCommandsService DebugCommandsServiceInstance; void DebugCommands::Execute(StringView command) { + PROFILE_MEM(EngineDebug); // TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush) String commandCopy = command; command = commandCopy; @@ -423,6 +426,7 @@ void DebugCommands::Search(StringView searchText, Array& matches, bo { if (searchText.IsEmpty()) return; + PROFILE_MEM(EngineDebug); // TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush) String searchTextCopy = searchText; searchText = searchTextCopy; diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp index bea9e76f4..18552dcec 100644 --- a/Source/Engine/Debug/DebugDraw.cpp +++ b/Source/Engine/Debug/DebugDraw.cpp @@ -480,6 +480,7 @@ DebugDrawCall WriteLists(int32& vertexCounter, const Array& listA, const Arra FORCE_INLINE DebugTriangle* AppendTriangles(int32 count, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); Array* list; if (depthTest) list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles; @@ -492,6 +493,7 @@ FORCE_INLINE DebugTriangle* AppendTriangles(int32 count, float duration, bool de FORCE_INLINE DebugTriangle* AppendWireTriangles(int32 count, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); Array* list; if (depthTest) list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultWireTriangles : &Context->DebugDrawDepthTest.OneFrameWireTriangles; @@ -539,7 +541,7 @@ DebugDrawService DebugDrawServiceInstance; bool DebugDrawService::Init() { - PROFILE_MEM(Graphics); + PROFILE_MEM(EngineDebug); Context = &GlobalContext; // Init wireframe sphere cache @@ -658,7 +660,7 @@ void DebugDrawService::Update() } PROFILE_CPU(); - PROFILE_MEM(Graphics); + PROFILE_MEM(EngineDebug); // Update lists float deltaTime = Time::Update.DeltaTime.GetTotalSeconds(); @@ -1114,6 +1116,7 @@ void DebugDraw::DrawRay(const Ray& ray, const Color& color, float length, float void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color& color, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); const Float3 startF = start - Context->Origin, endF = end - Context->Origin; auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) @@ -1132,6 +1135,7 @@ void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color& void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color& startColor, const Color& endColor, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); const Float3 startF = start - Context->Origin, endF = end - Context->Origin; auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) @@ -1161,6 +1165,7 @@ void DebugDraw::DrawLines(const Span& lines, const Matrix& transform, co } // Draw lines + PROFILE_MEM(EngineDebug); const Float3* p = lines.Get(); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; const Matrix transformF = transform * Matrix::Translation(-Context->Origin); @@ -1200,6 +1205,7 @@ void DebugDraw::DrawLines(GPUBuffer* lines, const Matrix& transform, float durat } // Draw lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; auto& geometry = debugDrawData.GeometryBuffers.AddOne(); geometry.Buffer = lines; @@ -1224,6 +1230,7 @@ void DebugDraw::DrawLines(const Span& lines, const Matrix& transform, c } // Draw lines + PROFILE_MEM(EngineDebug); const Double3* p = lines.Get(); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; const Matrix transformF = transform * Matrix::Translation(-Context->Origin); @@ -1270,6 +1277,7 @@ void DebugDraw::DrawBezier(const Vector3& p1, const Vector3& p2, const Vector3& const float segmentCountInv = 1.0f / (float)segmentCount; // Draw segmented curve from lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1310,6 +1318,7 @@ void DebugDraw::DrawWireBox(const BoundingBox& box, const Color& color, float du c -= Context->Origin; // Draw lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1344,6 +1353,7 @@ void DebugDraw::DrawWireFrustum(const BoundingFrustum& frustum, const Color& col c -= Context->Origin; // Draw lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1378,6 +1388,7 @@ void DebugDraw::DrawWireBox(const OrientedBoundingBox& box, const Color& color, c -= Context->Origin; // Draw lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1419,6 +1430,7 @@ void DebugDraw::DrawWireSphere(const BoundingSphere& sphere, const Color& color, auto& cache = SphereCache[index]; // Draw lines of the unit sphere after linear transform + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1454,6 +1466,7 @@ void DebugDraw::DrawSphere(const BoundingSphere& sphere, const Color& color, flo list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles; else list = duration > 0 ? &Context->DebugDrawDefault.DefaultTriangles : &Context->DebugDrawDefault.OneFrameTriangles; + PROFILE_MEM(EngineDebug); list->EnsureCapacity(list->Count() + SphereTriangleCache.Count()); const Float3 centerF = sphere.Center - Context->Origin; @@ -1485,6 +1498,7 @@ void DebugDraw::DrawCircle(const Vector3& position, const Float3& normal, float Matrix::Multiply(scale, world, matrix); // Draw lines of the unit circle after linear transform + PROFILE_MEM(EngineDebug); Float3 prev = Float3::Transform(CircleCache[0], matrix); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; for (int32 i = 1; i < DEBUG_DRAW_CIRCLE_VERTICES;) @@ -1515,6 +1529,7 @@ void DebugDraw::DrawWireTriangle(const Vector3& v0, const Vector3& v1, const Vec void DebugDraw::DrawTriangle(const Vector3& v0, const Vector3& v1, const Vector3& v2, const Color& color, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); DebugTriangle t; t.Color = Color32(color); t.TimeLeft = duration; @@ -1570,6 +1585,7 @@ void DebugDraw::DrawTriangles(GPUBuffer* triangles, const Matrix& transform, flo DebugLog::ThrowException("Cannot draw debug lines with incorrect amount of items in array"); return; } + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; auto& geometry = debugDrawData.GeometryBuffers.AddOne(); geometry.Buffer = triangles; @@ -1859,6 +1875,7 @@ void DebugDraw::DrawWireCapsule(const Vector3& position, const Quaternion& orien Matrix::Multiply(rotation, translation, world); // Write vertices + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; Color32 color32(color); if (duration > 0) @@ -1953,6 +1970,7 @@ namespace void DrawCylinder(Array* list, const Vector3& position, const Quaternion& orientation, float radius, float height, const Color& color, float duration) { // Setup cache + PROFILE_MEM(EngineDebug); Float3 CylinderCache[DEBUG_DRAW_CYLINDER_VERTICES]; const float angleBetweenFacets = TWO_PI / DEBUG_DRAW_CYLINDER_RESOLUTION; const float verticalOffset = height * 0.5f; @@ -2024,6 +2042,7 @@ namespace void DrawCone(Array* list, const Vector3& position, const Quaternion& orientation, float radius, float angleXY, float angleXZ, const Color& color, float duration) { + PROFILE_MEM(EngineDebug); const float tolerance = 0.001f; const float angle1 = Math::Clamp(angleXY, tolerance, PI - tolerance); const float angle2 = Math::Clamp(angleXZ, tolerance, PI - tolerance); @@ -2113,6 +2132,7 @@ void DebugDraw::DrawArc(const Vector3& position, const Quaternion& orientation, { if (angle <= 0) return; + PROFILE_MEM(EngineDebug); if (angle > TWO_PI) angle = TWO_PI; Array* list; @@ -2145,6 +2165,7 @@ void DebugDraw::DrawWireArc(const Vector3& position, const Quaternion& orientati { if (angle <= 0) return; + PROFILE_MEM(EngineDebug); if (angle > TWO_PI) angle = TWO_PI; const int32 resolution = Math::CeilToInt((float)DEBUG_DRAW_CONE_RESOLUTION / TWO_PI * angle); @@ -2211,6 +2232,7 @@ void DebugDraw::DrawBox(const BoundingBox& box, const Color& color, float durati list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles; else list = duration > 0 ? &Context->DebugDrawDefault.DefaultTriangles : &Context->DebugDrawDefault.OneFrameTriangles; + PROFILE_MEM(EngineDebug); list->EnsureCapacity(list->Count() + 36); for (int i0 = 0; i0 < 36;) { @@ -2239,6 +2261,7 @@ void DebugDraw::DrawBox(const OrientedBoundingBox& box, const Color& color, floa list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles; else list = duration > 0 ? &Context->DebugDrawDefault.DefaultTriangles : &Context->DebugDrawDefault.OneFrameTriangles; + PROFILE_MEM(EngineDebug); list->EnsureCapacity(list->Count() + 36); for (int i0 = 0; i0 < 36;) { @@ -2254,6 +2277,7 @@ void DebugDraw::DrawText(const StringView& text, const Float2& position, const C { if (text.Length() == 0 || size < 4) return; + PROFILE_MEM(EngineDebug); Array* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText2D : &Context->DebugDrawDefault.OneFrameText2D; auto& t = list->AddOne(); t.Text.Resize(text.Length() + 1); @@ -2269,6 +2293,7 @@ void DebugDraw::DrawText(const StringView& text, const Vector3& position, const { if (text.Length() == 0 || size < 4) return; + PROFILE_MEM(EngineDebug); Array* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText3D : &Context->DebugDrawDefault.OneFrameText3D; auto& t = list->AddOne(); t.Text.Resize(text.Length() + 1); @@ -2286,6 +2311,7 @@ void DebugDraw::DrawText(const StringView& text, const Transform& transform, con { if (text.Length() == 0 || size < 4) return; + PROFILE_MEM(EngineDebug); Array* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText3D : &Context->DebugDrawDefault.OneFrameText3D; auto& t = list->AddOne(); t.Text.Resize(text.Length() + 1); diff --git a/Source/Engine/Profiler/ProfilerMemory.cpp b/Source/Engine/Profiler/ProfilerMemory.cpp index 794a208b0..6b8f18ce3 100644 --- a/Source/Engine/Profiler/ProfilerMemory.cpp +++ b/Source/Engine/Profiler/ProfilerMemory.cpp @@ -243,6 +243,7 @@ void InitProfilerMemory(const Char* cmdLine, int32 stage) #define INIT_PARENT(parent, child) GroupParents[(int32)ProfilerMemory::Groups::child] = (uint8)ProfilerMemory::Groups::parent INIT_PARENT(Engine, EngineThreading); INIT_PARENT(Engine, EngineDelegate); + INIT_PARENT(Engine, EngineDebug); INIT_PARENT(Malloc, MallocArena); INIT_PARENT(Graphics, GraphicsTextures); INIT_PARENT(Graphics, GraphicsRenderTargets); diff --git a/Source/Engine/Profiler/ProfilerMemory.h b/Source/Engine/Profiler/ProfilerMemory.h index 1b9139b9a..9177ae6e7 100644 --- a/Source/Engine/Profiler/ProfilerMemory.h +++ b/Source/Engine/Profiler/ProfilerMemory.h @@ -44,6 +44,8 @@ public: EngineThreading, // Memory used by Delegate (engine events system to store all references). EngineDelegate, + // Memory used by debug tools (eg. DebugDraw, DebugCommands or DebugLog). + EngineDebug, // Total graphics memory usage. Graphics, From 2bf9efaf3084e7efbb8134f47e75198886f5e5e7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 29 Jan 2026 17:28:34 +0100 Subject: [PATCH 033/100] Fix rebuilding navmesh data for multiple scenes at once that share the same navmesh runtime #3744 --- Source/Engine/Navigation/NavMeshBuilder.cpp | 46 ++++++++------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index 1f43b7fda..91a4b1c20 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -907,15 +907,13 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo // Align dirty bounds to tile size BoundingBox dirtyBoundsNavMesh; BoundingBox::Transform(dirtyBounds, worldToNavMesh, dirtyBoundsNavMesh); - BoundingBox dirtyBoundsAligned; - dirtyBoundsAligned.Minimum = Float3::Floor(dirtyBoundsNavMesh.Minimum / tileSize) * tileSize; - dirtyBoundsAligned.Maximum = Float3::Ceil(dirtyBoundsNavMesh.Maximum / tileSize) * tileSize; + dirtyBoundsNavMesh.Minimum = Float3::Floor(dirtyBoundsNavMesh.Minimum / tileSize) * tileSize; + dirtyBoundsNavMesh.Maximum = Float3::Ceil(dirtyBoundsNavMesh.Maximum / tileSize) * tileSize; // Calculate tiles range for the given navigation dirty bounds (aligned to tiles size) - const Int3 tilesMin(dirtyBoundsAligned.Minimum / tileSize); - const Int3 tilesMax(dirtyBoundsAligned.Maximum / tileSize); - const int32 tilesX = tilesMax.X - tilesMin.X; - const int32 tilesY = tilesMax.Z - tilesMin.Z; + const Int3 tilesMin(dirtyBoundsNavMesh.Minimum / tileSize); + const Int3 tilesMax(dirtyBoundsNavMesh.Maximum / tileSize); + const int32 tilesXZ = (tilesMax.X - tilesMin.X) * (tilesMax.Z - tilesMin.Z); { PROFILE_CPU_NAMED("Prepare"); @@ -932,18 +930,18 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo // Remove all tiles from navmesh runtime runtime->RemoveTiles(navMesh); runtime->SetTileSize(tileSize); - runtime->EnsureCapacity(tilesX * tilesY); + runtime->EnsureCapacity(tilesXZ); // Remove all tiles from navmesh data navMesh->Data.TileSize = tileSize; navMesh->Data.Tiles.Clear(); - navMesh->Data.Tiles.EnsureCapacity(tilesX * tilesX); + navMesh->Data.Tiles.EnsureCapacity(tilesXZ); navMesh->IsDataDirty = true; } else { // Ensure to have enough memory for tiles - runtime->EnsureCapacity(tilesX * tilesY); + runtime->EnsureCapacity(tilesXZ); } runtime->Locker.Unlock(); @@ -959,11 +957,10 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo // Cache navmesh volumes Array> volumes; - for (int32 i = 0; i < scene->Navigation.Volumes.Count(); i++) + for (const NavMeshBoundsVolume* volume : scene->Navigation.Volumes) { - const auto volume = scene->Navigation.Volumes.Get()[i]; if (!volume->AgentsMask.IsNavMeshSupported(navMesh->Properties) || - !volume->GetBox().Intersects(dirtyBoundsAligned)) + !volume->GetBox().Intersects(dirtyBoundsNavMesh)) continue; auto& bounds = volumes.AddOne(); BoundingBox::Transform(volume->GetBox(), worldToNavMesh, bounds); @@ -1136,14 +1133,6 @@ void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds, bool rebuild } } -void BuildWholeScene(Scene* scene) -{ - // Compute total navigation area bounds - const BoundingBox worldBounds = scene->Navigation.GetNavigationBounds(); - - BuildDirtyBounds(scene, worldBounds, true); -} - void ClearNavigation(Scene* scene) { const bool autoRemoveMissingNavMeshes = NavigationSettings::Get()->AutoRemoveMissingNavMeshes; @@ -1162,6 +1151,7 @@ void NavMeshBuilder::Update() // Process nav mesh building requests and kick the tasks const auto now = DateTime::NowUTC(); + bool didRebuild = false; for (int32 i = 0; NavBuildQueue.HasItems() && i < NavBuildQueue.Count(); i++) { auto req = NavBuildQueue.Get()[i]; @@ -1180,14 +1170,14 @@ void NavMeshBuilder::Update() } // Check if build a custom dirty bounds or whole scene - if (req.DirtyBounds == BoundingBox::Empty) - { - BuildWholeScene(scene); - } + bool rebuild = req.DirtyBounds == BoundingBox::Empty; + if (rebuild) + req.DirtyBounds = scene->Navigation.GetNavigationBounds(); // Compute total navigation area bounds + if (didRebuild) + rebuild = false; // When rebuilding navmesh for multiple scenes, rebuild only the first one (other scenes will use additive update) else - { - BuildDirtyBounds(scene, req.DirtyBounds, false); - } + didRebuild = true; + BuildDirtyBounds(scene, req.DirtyBounds, rebuild); } } } From 9fc9382e58f86f3b344d74e7ee6ad992df521ff2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 30 Jan 2026 13:11:56 +0100 Subject: [PATCH 034/100] Refactor navmesh building to support updating all scenes automatically without specifying one #3744 --- Source/Editor/Modules/SceneEditingModule.cs | 2 +- Source/Editor/SceneGraph/Actors/SplineNode.cs | 2 +- Source/Editor/Tools/Terrain/EditTab.cs | 2 +- .../Editor/Tools/Terrain/EditTerrainGizmo.cs | 2 +- .../Terrain/Undo/EditTerrainMapAction.cs | 5 +- .../Editor/Undo/Actions/DeleteActorsAction.cs | 2 +- .../Undo/Actions/TransformObjectsAction.cs | 6 +- .../Engine/Navigation/NavMeshBoundsVolume.cpp | 29 +++- .../Engine/Navigation/NavMeshBoundsVolume.h | 1 + Source/Engine/Navigation/NavMeshBuilder.cpp | 154 +++++++++--------- Source/Engine/Navigation/NavMeshBuilder.h | 4 - .../Engine/Navigation/NavModifierVolume.cpp | 4 +- Source/Engine/Navigation/Navigation.cpp | 24 --- Source/Engine/Navigation/Navigation.h | 43 +++-- 14 files changed, 145 insertions(+), 135 deletions(-) diff --git a/Source/Editor/Modules/SceneEditingModule.cs b/Source/Editor/Modules/SceneEditingModule.cs index c36866bc3..b1a5be6f1 100644 --- a/Source/Editor/Modules/SceneEditingModule.cs +++ b/Source/Editor/Modules/SceneEditingModule.cs @@ -229,7 +229,7 @@ namespace FlaxEditor.Modules if (!isPlayMode && options.General.AutoRebuildNavMesh && actor.Scene && node.AffectsNavigationWithChildren) { var bounds = actor.BoxWithChildren; - Navigation.BuildNavMesh(actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs); } } diff --git a/Source/Editor/SceneGraph/Actors/SplineNode.cs b/Source/Editor/SceneGraph/Actors/SplineNode.cs index de319ca1d..515939526 100644 --- a/Source/Editor/SceneGraph/Actors/SplineNode.cs +++ b/Source/Editor/SceneGraph/Actors/SplineNode.cs @@ -555,7 +555,7 @@ namespace FlaxEditor.SceneGraph.Actors var options = Editor.Instance.Options.Options.General; if (options.AutoRebuildNavMesh) { - Navigation.BuildNavMesh(collider.Scene, collider.Box, options.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(collider.Box, options.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Editor/Tools/Terrain/EditTab.cs b/Source/Editor/Tools/Terrain/EditTab.cs index 551a47974..6a6191122 100644 --- a/Source/Editor/Tools/Terrain/EditTab.cs +++ b/Source/Editor/Tools/Terrain/EditTab.cs @@ -192,7 +192,7 @@ namespace FlaxEditor.Tools.Terrain { if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation)) { - Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs b/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs index 54a6d7fa4..5fc0e894f 100644 --- a/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs +++ b/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs @@ -209,7 +209,7 @@ namespace FlaxEditor.Tools.Terrain { if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation)) { - Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs b/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs index 87b0c3cc9..afac0948e 100644 --- a/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs +++ b/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs @@ -172,7 +172,7 @@ namespace FlaxEditor.Tools.Terrain.Undo if (_navmeshBoundsModifications != null) { foreach (var bounds in _navmeshBoundsModifications) - Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs); + Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs); } Editor.Instance.Scene.MarkSceneEdited(scene); @@ -217,11 +217,10 @@ namespace FlaxEditor.Tools.Terrain.Undo } // Update navmesh - var scene = Terrain.Scene; if (_navmeshBoundsModifications != null) { foreach (var bounds in _navmeshBoundsModifications) - Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs); + Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs); } Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene); diff --git a/Source/Editor/Undo/Actions/DeleteActorsAction.cs b/Source/Editor/Undo/Actions/DeleteActorsAction.cs index 75594ecb9..19ffb1e3f 100644 --- a/Source/Editor/Undo/Actions/DeleteActorsAction.cs +++ b/Source/Editor/Undo/Actions/DeleteActorsAction.cs @@ -303,7 +303,7 @@ namespace FlaxEditor.Actions if (_nodeParents[i] is ActorNode node && node.Actor && node.Actor.Scene && node.AffectsNavigationWithChildren) { var bounds = node.Actor.BoxWithChildren; - Navigation.BuildNavMesh(node.Actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Editor/Undo/Actions/TransformObjectsAction.cs b/Source/Editor/Undo/Actions/TransformObjectsAction.cs index ebed61174..df013e20e 100644 --- a/Source/Editor/Undo/Actions/TransformObjectsAction.cs +++ b/Source/Editor/Undo/Actions/TransformObjectsAction.cs @@ -121,12 +121,12 @@ namespace FlaxEditor // Handle simple case where objects were moved just a little and use one navmesh build request to improve performance if (data.BeforeBounds.Intersects(ref data.AfterBounds)) { - Navigation.BuildNavMesh(data.Scene, BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs); } else { - Navigation.BuildNavMesh(data.Scene, data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs); - Navigation.BuildNavMesh(data.Scene, data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp index 56351fded..c54f2f072 100644 --- a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp +++ b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp @@ -6,7 +6,7 @@ #if USE_EDITOR #include "Editor/Editor.h" #include "Editor/Managed/ManagedEditor.h" -#include "NavMeshBuilder.h" +#include "Navigation.h" #endif NavMeshBoundsVolume::NavMeshBoundsVolume(const SpawnParams& params) @@ -55,9 +55,30 @@ void NavMeshBoundsVolume::OnBoundsChanged(const BoundingBox& prevBounds) // Auto-rebuild modified navmesh area if (IsDuringPlay() && IsActiveInHierarchy() && !Editor::IsPlayMode && Editor::Managed->CanAutoBuildNavMesh()) { - BoundingBox dirtyBounds; - BoundingBox::Merge(prevBounds, _box, dirtyBounds); - NavMeshBuilder::Build(GetScene(), dirtyBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); + if (_box.Intersects(prevBounds)) + { + // Bounds were moved a bit so merge into a single request (for performance reasons) + BoundingBox dirtyBounds; + BoundingBox::Merge(prevBounds, _box, dirtyBounds); + Navigation::BuildNavMesh(dirtyBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); + } + else + { + // Dirty each bounds in separate + Navigation::BuildNavMesh(prevBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); + Navigation::BuildNavMesh(_box, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); + } + } +} + +void NavMeshBoundsVolume::OnActiveInTreeChanged() +{ + BoxVolume::OnActiveInTreeChanged(); + + // Auto-rebuild + if (IsDuringPlay() && !Editor::IsPlayMode && Editor::Managed->CanAutoBuildNavMesh()) + { + Navigation::BuildNavMesh(_box, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); } } diff --git a/Source/Engine/Navigation/NavMeshBoundsVolume.h b/Source/Engine/Navigation/NavMeshBoundsVolume.h index c04bc0483..80df5035a 100644 --- a/Source/Engine/Navigation/NavMeshBoundsVolume.h +++ b/Source/Engine/Navigation/NavMeshBoundsVolume.h @@ -30,6 +30,7 @@ protected: void OnDisable() override; #if USE_EDITOR void OnBoundsChanged(const BoundingBox& prevBounds) override; + void OnActiveInTreeChanged() override; Color GetWiresColor() override; #endif }; diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index 91a4b1c20..896cf4217 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -3,6 +3,7 @@ #if COMPILE_WITH_NAV_MESH_BUILDER #include "NavMeshBuilder.h" +#include "Navigation.h" #include "NavMesh.h" #include "NavigationSettings.h" #include "NavMeshBoundsVolume.h" @@ -706,6 +707,7 @@ struct BuildRequest ScriptingObjectReference Scene; DateTime Time; BoundingBox DirtyBounds; + bool SpecificScene; }; CriticalSection NavBuildQueueLocker; @@ -713,6 +715,7 @@ Array NavBuildQueue; CriticalSection NavBuildTasksLocker; int32 NavBuildTasksMaxCount = 0; +bool NavBuildCheckMissingNavMeshes = false; Array NavBuildTasks; class NavMeshTileBuildTask : public ThreadPoolTask @@ -776,13 +779,13 @@ void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime) NavBuildTasksLocker.Unlock(); } -void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y) +void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y, NavMesh* navMesh) { NavBuildTasksLocker.Lock(); for (int32 i = 0; i < NavBuildTasks.Count(); i++) { auto task = NavBuildTasks[i]; - if (task->Runtime == runtime && task->X == x && task->Y == y) + if (task->Runtime == runtime && task->X == x && task->Y == y && task->NavMesh == navMesh) { NavBuildTasksLocker.Unlock(); @@ -838,7 +841,7 @@ void NavMeshBuilder::Init() Level::SceneUnloading.Bind(); } -bool NavMeshBuilder::IsBuildingNavMesh() +bool Navigation::IsBuildingNavMesh() { NavBuildTasksLocker.Lock(); const bool hasAnyTask = NavBuildTasks.HasItems(); @@ -847,7 +850,7 @@ bool NavMeshBuilder::IsBuildingNavMesh() return hasAnyTask; } -float NavMeshBuilder::GetNavMeshBuildingProgress() +float Navigation::GetNavMeshBuildingProgress() { NavBuildTasksLocker.Lock(); float result = 1.0f; @@ -1023,7 +1026,7 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo for (const auto& tile : unusedTiles) { // Wait for any async tasks that are producing this tile - CancelNavMeshTileBuildTasks(runtime, tile.X, tile.Y); + CancelNavMeshTileBuildTasks(runtime, tile.X, tile.Y, navMesh); } runtime->Locker.Lock(); for (const auto& tile : unusedTiles) @@ -1106,31 +1109,6 @@ void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds, bool rebuild { BuildDirtyBounds(scene, navMesh, dirtyBounds, rebuild); } - - // Remove unused navmeshes - if (settings->AutoRemoveMissingNavMeshes) - { - for (NavMesh* navMesh : scene->Navigation.Meshes) - { - // Skip used navmeshes - if (navMesh->Data.Tiles.HasItems()) - continue; - - // Skip navmeshes during async building - int32 usageCount = 0; - NavBuildTasksLocker.Lock(); - for (int32 i = 0; i < NavBuildTasks.Count(); i++) - { - if (NavBuildTasks.Get()[i]->NavMesh == navMesh) - usageCount++; - } - NavBuildTasksLocker.Unlock(); - if (usageCount != 0) - continue; - - navMesh->DeleteObject(); - } - } } void ClearNavigation(Scene* scene) @@ -1144,6 +1122,40 @@ void ClearNavigation(Scene* scene) } } +void BuildNavigation(BuildRequest& request) +{ + // If scene is not specified then build all loaded scenes + if (!request.Scene) + { + for (Scene* scene : Level::Scenes) + { + request.Scene = scene; + BuildNavigation(request); + } + return; + } + + // Early out if scene is not using navigation + if (request.Scene->Navigation.Volumes.IsEmpty()) + { + ClearNavigation(request.Scene); + return; + } + + // Check if similar request is already in a queue + for (auto& e : NavBuildQueue) + { + if (e.Scene == request.Scene && (e.DirtyBounds == request.DirtyBounds || request.DirtyBounds == BoundingBox::Empty)) + { + e = request; + return; + } + } + + // Enqueue request + NavBuildQueue.Add(request); +} + void NavMeshBuilder::Update() { PROFILE_MEM(NavigationBuilding); @@ -1158,9 +1170,10 @@ void NavMeshBuilder::Update() if (now - req.Time >= 0) { NavBuildQueue.RemoveAt(i--); - const auto scene = req.Scene.Get(); + Scene* scene = req.Scene.Get(); if (!scene) continue; + bool rebuild = req.DirtyBounds == BoundingBox::Empty; // Early out if scene has no bounds volumes to define nav mesh area if (scene->Navigation.Volumes.IsEmpty()) @@ -1170,7 +1183,6 @@ void NavMeshBuilder::Update() } // Check if build a custom dirty bounds or whole scene - bool rebuild = req.DirtyBounds == BoundingBox::Empty; if (rebuild) req.DirtyBounds = scene->Navigation.GetNavigationBounds(); // Compute total navigation area bounds if (didRebuild) @@ -1178,26 +1190,37 @@ void NavMeshBuilder::Update() else didRebuild = true; BuildDirtyBounds(scene, req.DirtyBounds, rebuild); + NavBuildCheckMissingNavMeshes = true; + } + } + + // Remove unused navmeshes (when all active tasks are done) + // TODO: ignore AutoRemoveMissingNavMeshes in game and make it editor-only? + if (NavBuildCheckMissingNavMeshes && NavBuildTasksMaxCount == 0 && NavigationSettings::Get()->AutoRemoveMissingNavMeshes) + { + NavBuildCheckMissingNavMeshes = false; + NavBuildTasksLocker.Lock(); + int32 taskCount = NavBuildTasks.Count(); + NavBuildTasksLocker.Unlock(); + if (taskCount == 0) + { + for (Scene* scene : Level::Scenes) + { + for (NavMesh* navMesh : scene->Navigation.Meshes) + { + if (!navMesh->Data.Tiles.HasItems()) + { + navMesh->DeleteObject(); + } + } + } } } } -void NavMeshBuilder::Build(Scene* scene, float timeoutMs) +void Navigation::BuildNavMesh(Scene* scene, float timeoutMs) { - if (!scene) - { - LOG(Warning, "Could not generate navmesh without scene."); - return; - } - - // Early out if scene is not using navigation - if (scene->Navigation.Volumes.IsEmpty()) - { - ClearNavigation(scene); - return; - } - - PROFILE_CPU_NAMED("NavMeshBuilder"); + PROFILE_CPU(); PROFILE_MEM(NavigationBuilding); ScopeLock lock(NavBuildQueueLocker); @@ -1205,36 +1228,15 @@ void NavMeshBuilder::Build(Scene* scene, float timeoutMs) req.Scene = scene; req.Time = DateTime::NowUTC() + TimeSpan::FromMilliseconds(timeoutMs); req.DirtyBounds = BoundingBox::Empty; - - for (int32 i = 0; i < NavBuildQueue.Count(); i++) - { - auto& e = NavBuildQueue.Get()[i]; - if (e.Scene == scene && e.DirtyBounds == req.DirtyBounds) - { - e = req; - return; - } - } - - NavBuildQueue.Add(req); + req.SpecificScene = scene != nullptr; + BuildNavigation(req); } -void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs) +void Navigation::BuildNavMesh(const BoundingBox& dirtyBounds, Scene* scene, float timeoutMs) { - if (!scene) - { - LOG(Warning, "Could not generate navmesh without scene."); - return; - } - - // Early out if scene is not using navigation - if (scene->Navigation.Volumes.IsEmpty()) - { - ClearNavigation(scene); - return; - } - - PROFILE_CPU_NAMED("NavMeshBuilder"); + if (dirtyBounds.GetVolume() <= ZeroTolerance) + return; // Skip updating empty bounds + PROFILE_CPU(); PROFILE_MEM(NavigationBuilding); ScopeLock lock(NavBuildQueueLocker); @@ -1242,8 +1244,8 @@ void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float t req.Scene = scene; req.Time = DateTime::NowUTC() + TimeSpan::FromMilliseconds(timeoutMs); req.DirtyBounds = dirtyBounds; - - NavBuildQueue.Add(req); + req.SpecificScene = scene != nullptr; + BuildNavigation(req); } #endif diff --git a/Source/Engine/Navigation/NavMeshBuilder.h b/Source/Engine/Navigation/NavMeshBuilder.h index a3477db27..355bac7de 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.h +++ b/Source/Engine/Navigation/NavMeshBuilder.h @@ -15,11 +15,7 @@ class FLAXENGINE_API NavMeshBuilder { public: static void Init(); - static bool IsBuildingNavMesh(); - static float GetNavMeshBuildingProgress(); static void Update(); - static void Build(Scene* scene, float timeoutMs); - static void Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs); }; #endif diff --git a/Source/Engine/Navigation/NavModifierVolume.cpp b/Source/Engine/Navigation/NavModifierVolume.cpp index 9e1295f70..aa71e7aa1 100644 --- a/Source/Engine/Navigation/NavModifierVolume.cpp +++ b/Source/Engine/Navigation/NavModifierVolume.cpp @@ -2,7 +2,7 @@ #include "NavModifierVolume.h" #include "NavigationSettings.h" -#include "NavMeshBuilder.h" +#include "Navigation.h" #include "Engine/Level/Scene/Scene.h" #include "Engine/Serialization/Serialization.h" #if USE_EDITOR @@ -83,7 +83,7 @@ void NavModifierVolume::OnBoundsChanged(const BoundingBox& prevBounds) #else const float timeoutMs = 0.0f; #endif - NavMeshBuilder::Build(GetScene(), dirtyBounds, timeoutMs); + Navigation::BuildNavMesh(dirtyBounds, GetScene(), timeoutMs); } #endif } diff --git a/Source/Engine/Navigation/Navigation.cpp b/Source/Engine/Navigation/Navigation.cpp index 06413ea7f..908819765 100644 --- a/Source/Engine/Navigation/Navigation.cpp +++ b/Source/Engine/Navigation/Navigation.cpp @@ -382,30 +382,6 @@ bool Navigation::RayCast(const Vector3& startPosition, const Vector3& endPositio return NavMeshes.First()->RayCast(startPosition, endPosition, hitInfo); } -#if COMPILE_WITH_NAV_MESH_BUILDER - -bool Navigation::IsBuildingNavMesh() -{ - return NavMeshBuilder::IsBuildingNavMesh(); -} - -float Navigation::GetNavMeshBuildingProgress() -{ - return NavMeshBuilder::GetNavMeshBuildingProgress(); -} - -void Navigation::BuildNavMesh(Scene* scene, float timeoutMs) -{ - NavMeshBuilder::Build(scene, timeoutMs); -} - -void Navigation::BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs) -{ - NavMeshBuilder::Build(scene, dirtyBounds, timeoutMs); -} - -#endif - #if COMPILE_WITH_DEBUG_DRAW void Navigation::DrawNavMesh() diff --git a/Source/Engine/Navigation/Navigation.h b/Source/Engine/Navigation/Navigation.h index 434817ca1..80c8eb84a 100644 --- a/Source/Engine/Navigation/Navigation.h +++ b/Source/Engine/Navigation/Navigation.h @@ -84,9 +84,7 @@ public: /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo); -public: #if COMPILE_WITH_NAV_MESH_BUILDER - /// /// Returns true if navigation system is during navmesh building (any request is valid or async task active). /// @@ -100,32 +98,49 @@ public: /// /// Builds the Nav Mesh for the given scene (discards all its tiles). /// - /// - /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. - /// - /// The scene. + /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. + /// The scene. Pass null to build navmesh for all loaded scenes. /// The timeout to wait before building Nav Mesh (in milliseconds). - API_FUNCTION() static void BuildNavMesh(Scene* scene, float timeoutMs = 50); + API_FUNCTION() static void BuildNavMesh(Scene* scene = nullptr, float timeoutMs = 50); /// /// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box). /// - /// - /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. - /// + /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. + /// The bounds in world-space to build overlapping tiles. + /// The scene. Pass null to build navmesh for all loaded scenes that intersect with a given bounds. + /// The timeout to wait before building Nav Mesh (in milliseconds). + API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, Scene* scene = nullptr, float timeoutMs = 50); + + /// + /// Builds the Nav Mesh for all the loaded scenes (builds only the tiles overlapping the given bounding box). + /// + /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. + /// The bounds in world-space to build overlapping tiles. + /// The timeout to wait before building Nav Mesh (in milliseconds). + API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, float timeoutMs = 50) + { + BuildNavMesh(dirtyBounds, nullptr, timeoutMs); + } + + /// + /// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box). + /// [Deprecated in v1.12] + /// + /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. /// The scene. /// The bounds in world-space to build overlapping tiles. /// The timeout to wait before building Nav Mesh (in milliseconds). - API_FUNCTION() static void BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs = 50); - + API_FUNCTION() DEPRECATED("Use BuildNavMesh with reordered arguments instead") static void BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs = 50) + { + BuildNavMesh(dirtyBounds, scene, timeoutMs); + } #endif #if COMPILE_WITH_DEBUG_DRAW - /// /// Draws the navigation for all the scenes (uses DebugDraw interface). /// static void DrawNavMesh(); - #endif }; From e4eb064562764ed0871d9c144a198d2c2798aab2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 30 Jan 2026 13:12:45 +0100 Subject: [PATCH 035/100] Add bake button to nav mesh bounds and minor improvements --- .../Dedicated/NavMeshBoundsVolumeEditor.cs | 38 +++++++++++++++++++ Source/Editor/Editor.cs | 5 ++- Source/Engine/Navigation/NavMeshRuntime.cpp | 19 +++++++++- 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs diff --git a/Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs b/Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs new file mode 100644 index 000000000..2cbf01e41 --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs @@ -0,0 +1,38 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEngine; + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + /// + [CustomEditor(typeof(NavMeshBoundsVolume)), DefaultEditor] + internal class NavMeshBoundsVolumeEditor : ActorEditor + { + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + + if (Values.HasDifferentTypes == false) + { + var button = layout.Button("Build"); + button.Button.Clicked += OnBuildClicked; + } + } + + private void OnBuildClicked() + { + foreach (var value in Values) + { + if (value is NavMeshBoundsVolume volume) + { + Navigation.BuildNavMesh(volume.Box, volume.Scene); + Editor.Instance.Scene.MarkSceneEdited(volume.Scene); + } + } + } + } +} diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index 58466e35d..c881d9dcd 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -23,6 +23,7 @@ using FlaxEngine.Assertions; using FlaxEngine.GUI; using FlaxEngine.Interop; using FlaxEngine.Json; +using FlaxEngine.Utilities; #pragma warning disable CS1591 @@ -1370,7 +1371,7 @@ namespace FlaxEditor public void BuildCSG() { var scenes = Level.Scenes; - scenes.ToList().ForEach(x => x.BuildCSG(0)); + scenes.ForEach(x => x.BuildCSG(0)); Scene.MarkSceneEdited(scenes); } @@ -1380,7 +1381,7 @@ namespace FlaxEditor public void BuildNavMesh() { var scenes = Level.Scenes; - scenes.ToList().ForEach(x => Navigation.BuildNavMesh(x, 0)); + Navigation.BuildNavMesh(); Scene.MarkSceneEdited(scenes); } diff --git a/Source/Engine/Navigation/NavMeshRuntime.cpp b/Source/Engine/Navigation/NavMeshRuntime.cpp index 911700e19..0ace29415 100644 --- a/Source/Engine/Navigation/NavMeshRuntime.cpp +++ b/Source/Engine/Navigation/NavMeshRuntime.cpp @@ -5,6 +5,9 @@ #include "NavMesh.h" #include "Engine/Core/Log.h" #include "Engine/Core/Random.h" +#if COMPILE_WITH_DEBUG_DRAW +#include "Engine/Level/Scene/Scene.h" +#endif #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Threading/Threading.h" @@ -603,7 +606,21 @@ void NavMeshRuntime::DebugDraw() if (!tile->header) continue; - //DebugDraw::DrawWireBox(*(BoundingBox*)&tile->header->bmin[0], Color::CadetBlue); +#if 0 + // Debug draw tile bounds and owner scene name + BoundingBox tileBounds = *(BoundingBox*)&tile->header->bmin[0]; + DebugDraw::DrawWireBox(tileBounds, Color::CadetBlue); + // TODO: build map from tile coords to tile data to avoid this loop + for (const auto& e : _tiles) + { + if (e.X == tile->header->x && e.Y == tile->header->y && e.Layer == tile->header->layer) + { + if (e.NavMesh && e.NavMesh->GetScene()) + DebugDraw::DrawText(e.NavMesh->GetScene()->GetName(), tileBounds.Minimum + tileBounds.GetSize() * Float3(0.5f, 0.8f, 0.5f), Color::CadetBlue); + break; + } + } +#endif for (int i = 0; i < tile->header->polyCount; i++) { From d18c245730770f2e8fbdbbeba6d9a503894e6116 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 30 Jan 2026 13:14:36 +0100 Subject: [PATCH 036/100] Fix rare async task crash when it's canceled while dequeuing --- Source/Engine/Threading/Task.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/Engine/Threading/Task.cpp b/Source/Engine/Threading/Task.cpp index a640019d1..cef08b0bc 100644 --- a/Source/Engine/Threading/Task.cpp +++ b/Source/Engine/Threading/Task.cpp @@ -148,9 +148,8 @@ Task* Task::StartNew(Function::Signature& action, Object* target) void Task::Execute() { - if (IsCanceled()) + if (!IsQueued()) return; - ASSERT(IsQueued()); SetState(TaskState::Running); // Perform an operation From 205a8b2ebe30e01d83c3cb0cc8be45a2307c0e10 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 30 Jan 2026 15:10:31 +0100 Subject: [PATCH 037/100] Fix GPU Vertex Layout usage with explicit offsets to properly hash and calculates stride #3625 --- Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp index 23382673f..05c6d605a 100644 --- a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp +++ b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp @@ -59,7 +59,7 @@ namespace elements.Get()[j].Slot = (byte)slot; } } - GPUVertexLayout* result = anyValid ? GPUVertexLayout::Get(elements) : nullptr; + GPUVertexLayout* result = anyValid ? GPUVertexLayout::Get(elements, true) : nullptr; VertexBufferCache.Add(key, result); return result; } @@ -97,6 +97,7 @@ GPUVertexLayout::GPUVertexLayout() void GPUVertexLayout::SetElements(const Elements& elements, bool explicitOffsets) { uint32 offsets[GPU_MAX_VB_BINDED + 1] = {}; + uint32 maxOffset[GPU_MAX_VB_BINDED + 1] = {}; _elements = elements; for (int32 i = 0; i < _elements.Count(); i++) { @@ -108,9 +109,10 @@ void GPUVertexLayout::SetElements(const Elements& elements, bool explicitOffsets else e.Offset = (byte)offset; offset += PixelFormatExtensions::SizeInBytes(e.Format); + maxOffset[e.Slot] = Math::Max(maxOffset[e.Slot], offset); } _stride = 0; - for (uint32 offset : offsets) + for (uint32 offset : maxOffset) _stride += offset; } @@ -139,7 +141,7 @@ VertexElement GPUVertexLayout::FindElement(VertexElement::Types type) const GPUVertexLayout* GPUVertexLayout::Get(const Elements& elements, bool explicitOffsets) { // Hash input layout - uint32 hash = 0; + uint32 hash = explicitOffsets ? 131 : 0; for (const VertexElement& element : elements) { CombineHash(hash, GetHash(element)); From afc04dc41cc828def426ac784557be423a7420b0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 30 Jan 2026 16:43:48 +0100 Subject: [PATCH 038/100] Minor cleanup and fix #3733 --- .../Physics/PhysX/PhysicsBackendPhysX.cpp | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index ad955eab3..8c3d3610e 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -2785,21 +2785,21 @@ float PhysicsBackend::ComputeShapeSqrDistanceToPoint(void* shape, const Vector3& auto shapePhysX = (PxShape*)shape; const PxTransform trans(C2P(position), C2P(orientation)); - // Special case for heightfield collider + // Special case for heightfield collider (not implemented in PhysX) if (shapePhysX->getGeometryType() == PxGeometryType::eHEIGHTFIELD) { - // Do a bunch of raycasts... because for some reason pointDistance does not support height fields... + // Do a bunch of raycasts in all directions to find the closest point on the heightfield PxVec3 origin = C2P(point); - - // Get all unit directions based on resolution value Array unitDirections; - int32 resolution = 32; - for (int32 i = 0; i <= resolution; i++) { + constexpr int32 resolution = 32; + unitDirections.EnsureCapacity((resolution + 1) * (resolution + 1)); + for (int32 i = 0; i <= resolution; i++) + { float phi = PI * (float)i / resolution; float sinPhi = Math::Sin(phi); float cosPhi = Math::Cos(phi); - - for (int32 j = 0; j <= resolution; j++) { + for (int32 j = 0; j <= resolution; j++) + { float theta = 2.0f * PI * (float)j / resolution; float cosTheta = Math::Cos(theta); float sinTheta = Math::Sin(theta); @@ -2815,10 +2815,9 @@ float PhysicsBackend::ComputeShapeSqrDistanceToPoint(void* shape, const Vector3& } PxReal maxDistance = PX_MAX_REAL; // Search indefinitely - PxQueryFilterData filterData; filterData.data.word0 = (PxU32)shapePhysX->getSimulationFilterData().word0; - PxHitFlags hitFlags = PxHitFlag::ePOSITION | PxHitFlag::eNORMAL | PxHitFlag::eMESH_BOTH_SIDES; // Both sides added for if it is underneath the height field + PxHitFlags hitFlags = PxHitFlag::ePOSITION | PxHitFlag::eMESH_BOTH_SIDES; // Both sides added for if it is underneath the height field PxRaycastBuffer buffer; auto scene = shapePhysX->getActor()->getScene(); @@ -2841,7 +2840,7 @@ float PhysicsBackend::ComputeShapeSqrDistanceToPoint(void* shape, const Vector3& if (closestDistance < maxDistance) { *closestPoint = P2C(tempClosestPoint); - return closestDistance; + return closestDistance * closestDistance; // Result is squared distance } return -1.0f; From 4a1490f0b14e8575716f8c0a8c87271ab9c54130 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 30 Jan 2026 17:31:59 +0100 Subject: [PATCH 039/100] Follow up on #3642 and move node resizing code to shared code --- Source/Editor/Surface/Archetypes/Material.cs | 122 +----------- Source/Editor/Surface/Archetypes/Tools.cs | 134 +------------ Source/Editor/Surface/ResizableSurfaceNode.cs | 182 ++++++++++++++++++ Source/Editor/Surface/SurfaceComment.cs | 121 +----------- 4 files changed, 203 insertions(+), 356 deletions(-) create mode 100644 Source/Editor/Surface/ResizableSurfaceNode.cs diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index e46038639..9ac43082e 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -304,25 +304,14 @@ namespace FlaxEditor.Surface.Archetypes } } - internal sealed class CustomCodeNode : SurfaceNode + internal sealed class CustomCodeNode : ResizableSurfaceNode { - private Rectangle _resizeButtonRect; - private Float2 _startResizingSize; - private Float2 _startResizingCornerOffset; - private bool _isResizing; private CustomCodeTextBox _textBox; - private int SizeValueIndex => Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array - - private Float2 SizeValue - { - get => (Float2)Values[SizeValueIndex]; - set => SetValue(SizeValueIndex, value, false); - } - public CustomCodeNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { + _sizeValueIndex = Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize), size; if (nodeArch.TypeID == 8) { @@ -345,126 +334,19 @@ namespace FlaxEditor.Surface.Archetypes _textBox.EditEnd += () => SetValue(0, _textBox.Text); } - public override bool CanSelect(ref Float2 location) - { - return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); - } - public override void OnSurfaceLoaded(SurfaceNodeActions action) { base.OnSurfaceLoaded(action); _textBox.Text = (string)Values[0]; - - var size = SizeValue; - if (Surface != null && Surface.GridSnappingEnabled) - size = Surface.SnapToGrid(size, true); - Resize(size.X, size.Y); } public override void OnValuesChanged() { base.OnValuesChanged(); - var size = SizeValue; - Resize(size.X, size.Y); _textBox.Text = (string)Values[0]; } - - protected override void UpdateRectangles() - { - base.UpdateRectangles(); - - const float buttonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; - const float buttonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize; - _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize); - } - - public override void Draw() - { - base.Draw(); - - var style = Style.Current; - if (_isResizing) - { - Render2D.FillRectangle(_resizeButtonRect, style.Selection); - Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); - } - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - } - - public override void OnLostFocus() - { - if (_isResizing) - EndResizing(); - - base.OnLostFocus(); - } - - public override void OnEndMouseCapture() - { - if (_isResizing) - EndResizing(); - - base.OnEndMouseCapture(); - } - - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (base.OnMouseDown(location, button)) - return true; - - if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) - { - // Start sliding - _isResizing = true; - _startResizingSize = Size; - _startResizingCornerOffset = Size - location; - StartMouseCapture(); - Cursor = CursorType.SizeNWSE; - return true; - } - - return false; - } - - public override void OnMouseMove(Float2 location) - { - if (_isResizing) - { - var emptySize = CalculateNodeSize(0, 0); - var size = Float2.Max(location - emptySize + _startResizingCornerOffset, new Float2(240, 160)); - Resize(size.X, size.Y); - } - else - { - base.OnMouseMove(location); - } - } - - public override bool OnMouseUp(Float2 location, MouseButton button) - { - if (button == MouseButton.Left && _isResizing) - { - EndResizing(); - return true; - } - - return base.OnMouseUp(location, button); - } - - private void EndResizing() - { - Cursor = CursorType.Default; - EndMouseCapture(); - _isResizing = false; - if (_startResizingSize != Size) - { - var emptySize = CalculateNodeSize(0, 0); - SizeValue = Size - emptySize; - Surface.MarkAsEdited(false); - } - } } internal enum MaterialTemplateInputsMapping diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 3486014d5..68a733197 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -453,24 +453,11 @@ namespace FlaxEditor.Surface.Archetypes } } - private class CurveNode : SurfaceNode where T : struct + private class CurveNode : ResizableSurfaceNode where T : struct { - private Rectangle _resizeButtonRect; - private Float2 _startResizingSize; - private Float2 _startResizingCornerOffset; - private bool _isResizing; - private BezierCurveEditor _curve; private bool _isSavingCurve; - private int SizeValueIndex => 29; // Index of the Size stored in Values array - - private Float2 SizeValue - { - get => (Float2)Values[SizeValueIndex]; - set => SetValue(SizeValueIndex, value, false); - } - public static NodeArchetype GetArchetype(ushort typeId, string title, Type valueType, T zero, T one) { return new NodeArchetype @@ -519,22 +506,18 @@ namespace FlaxEditor.Surface.Archetypes public CurveNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { + _sizeValueIndex = 29; // Index of the Size stored in Values array } - - public override bool CanSelect(ref Float2 location) - { - return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); - } - + /// public override void OnLoaded(SurfaceNodeActions action) { base.OnLoaded(action); + // Create curve editor var upperLeft = GetBox(0).BottomLeft; var upperRight = GetBox(1).BottomRight; float curveMargin = 20.0f; - _curve = new BezierCurveEditor { MaxKeyframes = 7, @@ -546,13 +529,14 @@ namespace FlaxEditor.Surface.Archetypes _curve.UnlockChildrenRecursive(); _curve.PerformLayout(); + // Sync keyframes UpdateCurveKeyframes(); + } - // Reapply the curve node size - var size = SizeValue; - if (Surface != null && Surface.GridSnappingEnabled) - size = Surface.SnapToGrid(size, true); - Resize(size.X, size.Y); + /// + public override void OnSurfaceLoaded(SurfaceNodeActions action) + { + base.OnSurfaceLoaded(action); // Ensure the whole curve is shown _curve.ShowWholeCurve(); @@ -562,110 +546,12 @@ namespace FlaxEditor.Surface.Archetypes { base.OnValuesChanged(); - var size = SizeValue; - Resize(size.X, size.Y); - if (!_isSavingCurve) { UpdateCurveKeyframes(); } } - protected override void UpdateRectangles() - { - base.UpdateRectangles(); - - const float buttonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; - const float buttonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize; - _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize); - } - - public override void Draw() - { - base.Draw(); - - var style = Style.Current; - if (_isResizing) - { - Render2D.FillRectangle(_resizeButtonRect, style.Selection); - Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); - } - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - } - - public override void OnLostFocus() - { - if (_isResizing) - EndResizing(); - - base.OnLostFocus(); - } - - public override void OnEndMouseCapture() - { - if (_isResizing) - EndResizing(); - - base.OnEndMouseCapture(); - } - - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (base.OnMouseDown(location, button)) - return true; - - if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) - { - // Start sliding - _isResizing = true; - _startResizingSize = Size; - _startResizingCornerOffset = Size - location; - StartMouseCapture(); - Cursor = CursorType.SizeNWSE; - return true; - } - - return false; - } - - public override void OnMouseMove(Float2 location) - { - if (_isResizing) - { - var emptySize = CalculateNodeSize(0, 0); - var size = Float2.Max(location - emptySize + _startResizingCornerOffset, new Float2(240, 160)); - Resize(size.X, size.Y); - } - else - { - base.OnMouseMove(location); - } - } - - public override bool OnMouseUp(Float2 location, MouseButton button) - { - if (button == MouseButton.Left && _isResizing) - { - EndResizing(); - return true; - } - - return base.OnMouseUp(location, button); - } - - private void EndResizing() - { - Cursor = CursorType.Default; - EndMouseCapture(); - _isResizing = false; - if (_startResizingSize != Size) - { - var emptySize = CalculateNodeSize(0, 0); - SizeValue = Size - emptySize; - Surface.MarkAsEdited(false); - } - } - private void OnCurveEdited() { if (_isSavingCurve) diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs new file mode 100644 index 000000000..259c29836 --- /dev/null +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -0,0 +1,182 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Surface +{ + /// + /// Visject Surface node control that cna be resized. + /// + /// + [HideInEditor] + public class ResizableSurfaceNode : SurfaceNode + { + private Float2 _startResizingSize; + private Float2 _startResizingCornerOffset; + + /// + /// Indicates whether the node is currently being resized. + /// + protected bool _isResizing; + + /// + /// Index of the Float2 value in the node values list to store node size. + /// + protected int _sizeValueIndex = -1; + + /// + /// Minimum node size. + /// + protected Float2 _sizeMin = new Float2(240, 160); + + /// + /// Node resizing rectangle bounds. + /// + protected Rectangle _resizeButtonRect; + + private Float2 SizeValue + { + get => (Float2)Values[_sizeValueIndex]; + set => SetValue(_sizeValueIndex, value, false); + } + + /// + public ResizableSurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + } + + /// + public override bool CanSelect(ref Float2 location) + { + return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); + } + + /// + public override void OnSurfaceLoaded(SurfaceNodeActions action) + { + // Reapply the curve node size + var size = SizeValue; + if (Surface != null && Surface.GridSnappingEnabled) + size = Surface.SnapToGrid(size, true); + Resize(size.X, size.Y); + + base.OnSurfaceLoaded(action); + } + + /// + public override void OnValuesChanged() + { + base.OnValuesChanged(); + + var size = SizeValue; + Resize(size.X, size.Y); + } + + /// + public override void Draw() + { + base.Draw(); + + if (Surface.CanEdit) + { + var style = Style.Current; + if (_isResizing) + { + Render2D.FillRectangle(_resizeButtonRect, style.Selection); + Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); + } + Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); + } + } + + /// + public override void OnLostFocus() + { + if (_isResizing) + EndResizing(); + + base.OnLostFocus(); + } + + /// + public override void OnEndMouseCapture() + { + if (_isResizing) + EndResizing(); + + base.OnEndMouseCapture(); + } + + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (base.OnMouseDown(location, button)) + return true; + + if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) + { + // Start resizing + _isResizing = true; + _startResizingSize = Size; + _startResizingCornerOffset = Size - location; + StartMouseCapture(); + Cursor = CursorType.SizeNWSE; + return true; + } + + return false; + } + + /// + public override void OnMouseMove(Float2 location) + { + if (_isResizing) + { + var emptySize = CalculateNodeSize(0, 0); + var size = Float2.Max(location - emptySize + _startResizingCornerOffset, _sizeMin); + Resize(size.X, size.Y); + } + else + { + base.OnMouseMove(location); + } + } + + /// + public override bool OnMouseUp(Float2 location, MouseButton button) + { + if (button == MouseButton.Left && _isResizing) + { + EndResizing(); + return true; + } + + return base.OnMouseUp(location, button); + } + + /// + protected override void UpdateRectangles() + { + base.UpdateRectangles(); + + const float buttonMargin = Constants.NodeCloseButtonMargin; + const float buttonSize = Constants.NodeCloseButtonSize; + _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize); + } + + private void EndResizing() + { + Cursor = CursorType.Default; + EndMouseCapture(); + _isResizing = false; + if (_startResizingSize != Size) + { + var emptySize = CalculateNodeSize(0, 0); + SizeValue = Size - emptySize; + Surface.MarkAsEdited(false); + } + } + } +} diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 10e9fc776..a76fa245d 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -14,18 +14,11 @@ namespace FlaxEditor.Surface /// /// [HideInEditor] - public class SurfaceComment : SurfaceNode + public class SurfaceComment : ResizableSurfaceNode { private Rectangle _colorButtonRect; - private Rectangle _resizeButtonRect; - private Float2 _startResizingSize; private readonly TextBox _renameTextBox; - /// - /// True if sizing tool is in use. - /// - protected bool _isResizing; - /// /// True if rename textbox is active in order to rename comment /// @@ -52,12 +45,6 @@ namespace FlaxEditor.Surface set => SetValue(1, value, false); } - private Float2 SizeValue - { - get => (Float2)Values[2]; - set => SetValue(2, value, false); - } - private int OrderValue { get => (int)Values[3]; @@ -68,6 +55,8 @@ namespace FlaxEditor.Surface public SurfaceComment(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { + _sizeValueIndex = 2; // Index of the Size stored in Values array + _sizeMin = new Float2(140.0f, Constants.NodeHeaderSize); _renameTextBox = new TextBox(false, 0, 0, Width) { Height = Constants.NodeHeaderSize, @@ -86,10 +75,6 @@ namespace FlaxEditor.Surface // Read node data Title = TitleValue; Color = ColorValue; - var size = SizeValue; - if (Surface != null && Surface.GridSnappingEnabled) - size = Surface.SnapToGrid(size, true); - Size = size; // Order // Backwards compatibility - When opening with an older version send the old comments to the back @@ -126,27 +111,6 @@ namespace FlaxEditor.Surface // Read node data Title = TitleValue; Color = ColorValue; - Size = SizeValue; - } - - private void EndResizing() - { - // Clear state - _isResizing = false; - - if (_startResizingSize != Size) - { - SizeValue = Size; - Surface.MarkAsEdited(false); - } - - EndMouseCapture(); - } - - /// - public override bool CanSelect(ref Float2 location) - { - return _headerRect.MakeOffsetted(Location).Contains(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); } /// @@ -158,6 +122,8 @@ namespace FlaxEditor.Surface /// protected override void UpdateRectangles() { + base.UpdateRectangles(); + const float headerSize = Constants.NodeHeaderSize; const float buttonMargin = Constants.NodeCloseButtonMargin; const float buttonSize = Constants.NodeCloseButtonSize; @@ -222,16 +188,13 @@ namespace FlaxEditor.Surface // Color button Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - // Check if is resizing + // Resize button if (_isResizing) { - // Draw overlay Render2D.FillRectangle(_resizeButtonRect, style.Selection); Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); } - - // Resize button - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); + Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); } // Selection outline @@ -247,88 +210,28 @@ namespace FlaxEditor.Surface /// protected override Float2 CalculateNodeSize(float width, float height) { - return Size; + // No margins or headers + return new Float2(width, height); } /// public override void OnLostFocus() { - // Check if was resizing - if (_isResizing) - { - EndResizing(); - } - - // Check if was renaming if (_isRenaming) { Rename(_renameTextBox.Text); StopRenaming(); } - // Base base.OnLostFocus(); } - /// - public override void OnEndMouseCapture() - { - // Check if was resizing - if (_isResizing) - { - EndResizing(); - } - else - { - base.OnEndMouseCapture(); - } - } - /// public override bool ContainsPoint(ref Float2 location, bool precise) { return _headerRect.Contains(ref location) || _resizeButtonRect.Contains(ref location); } - /// - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (base.OnMouseDown(location, button)) - return true; - - // Check if can start resizing - if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) - { - // Start sliding - _isResizing = true; - _startResizingSize = Size; - StartMouseCapture(); - - return true; - } - - return false; - } - - /// - public override void OnMouseMove(Float2 location) - { - // Check if is resizing - if (_isResizing) - { - // Update size - var size = Float2.Max(location, new Float2(140.0f, _headerRect.Bottom)); - if (Surface.GridSnappingEnabled) - size = Surface.SnapToGrid(size, true); - Size = size; - } - else - { - // Base - base.OnMouseMove(location); - } - } - /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { @@ -394,12 +297,6 @@ namespace FlaxEditor.Surface /// public override bool OnMouseUp(Float2 location, MouseButton button) { - if (button == MouseButton.Left && _isResizing) - { - EndResizing(); - return true; - } - if (base.OnMouseUp(location, button)) return true; From e8c2f18a4dbb66ea41eba8959aad0963b740fc34 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 30 Jan 2026 17:50:24 +0100 Subject: [PATCH 040/100] Add tab navigation to Editor context menus and popups #3676 --- .../Editor/GUI/ContextMenu/ContextMenuBase.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index 0261792e7..1e6fd9861 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -108,6 +108,11 @@ namespace FlaxEditor.GUI.ContextMenu /// public bool UseInput = true; + /// + /// Optional flag that can disable UI navigation (tab/enter). + /// + public bool UseNavigation = true; + /// /// Initializes a new instance of the class. /// @@ -594,6 +599,21 @@ namespace FlaxEditor.GUI.ContextMenu case KeyboardKeys.Escape: Hide(); return true; + case KeyboardKeys.Return: + if (UseNavigation && Root?.FocusedControl != null) + { + Root.SubmitFocused(); + return true; + } + break; + case KeyboardKeys.Tab: + if (UseNavigation && Root != null) + { + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + Root.Navigate(shiftDown ? NavDirection.Previous : NavDirection.Next); + return true; + } + break; } return false; } From 87ccaa9dd808597bd953b866d615b165dc81f802 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 30 Jan 2026 18:00:16 +0100 Subject: [PATCH 041/100] Add keyboard navgation to context menu child menus #3676 --- Source/Editor/GUI/ContextMenu/ContextMenu.cs | 15 +++++++++++++++ Source/Editor/GUI/ContextMenu/ContextMenuBase.cs | 7 +++++-- .../GUI/ContextMenu/ContextMenuChildMenu.cs | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Source/Editor/GUI/ContextMenu/ContextMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenu.cs index 896bd5bc2..f5705c6f5 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenu.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenu.cs @@ -502,6 +502,7 @@ namespace FlaxEditor.GUI.ContextMenu if (base.OnKeyDown(key)) return true; + // Keyboard navigation around the menu switch (key) { case KeyboardKeys.ArrowDown: @@ -526,6 +527,20 @@ namespace FlaxEditor.GUI.ContextMenu } } break; + case KeyboardKeys.ArrowRight: + for (int i = 0; i < _panel.Children.Count; i++) + { + if (_panel.Children[i] is ContextMenuChildMenu item && item.Visible && item.IsFocused && !item.ContextMenu.IsOpened) + { + item.ShowChild(this); + item.ContextMenu._panel.Children.FirstOrDefault(x => x is ContextMenuButton && x.Visible)?.Focus(); + break; + } + } + break; + case KeyboardKeys.ArrowLeft: + ParentCM?.RootWindow.Focus(); + break; } return false; diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index 1e6fd9861..2f517d903 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -72,6 +72,11 @@ namespace FlaxEditor.GUI.ContextMenu /// public bool HasChildCMOpened => _childCM != null; + /// + /// Gets the parent context menu (if exists). + /// + public ContextMenuBase ParentCM => _parentCM; + /// /// Gets the topmost context menu. /// @@ -81,9 +86,7 @@ namespace FlaxEditor.GUI.ContextMenu { var cm = this; while (cm._parentCM != null && cm._isSubMenu) - { cm = cm._parentCM; - } return cm; } } diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs index 74ab560fb..78337d011 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs @@ -29,7 +29,7 @@ namespace FlaxEditor.GUI.ContextMenu CloseMenuOnClick = false; } - private void ShowChild(ContextMenu parentContextMenu) + internal void ShowChild(ContextMenu parentContextMenu) { // Hide parent CM popups and set itself as child var vAlign = parentContextMenu.ItemsAreaMargin.Top; From 7c44767d4d6b2f28d9cb68ac0ae13b45176b822e Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 30 Jan 2026 12:07:36 -0600 Subject: [PATCH 042/100] Fix viewport rubberban selector. --- .../Editor/Gizmo/ViewportRubberBandSelector.cs | 16 ++++++++++------ .../Editor/Viewport/MainEditorGizmoViewport.cs | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs index 542e8b388..66e835fac 100644 --- a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs +++ b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs @@ -36,11 +36,12 @@ public sealed class ViewportRubberBandSelector /// Triggers the start of a rubber band selection. /// /// True if selection started, otherwise false. - public bool TryStartingRubberBandSelection() + public bool TryStartingRubberBandSelection(Float2 mousePosition) { if (!_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown) { _tryStartRubberBand = true; + _cachedStartingMousePosition = mousePosition; return true; } return false; @@ -82,12 +83,15 @@ public sealed class ViewportRubberBandSelector return; } - if (_tryStartRubberBand && (Mathf.Abs(_owner.MouseDelta.X) > 0.1f || Mathf.Abs(_owner.MouseDelta.Y) > 0.1f) && canStart) + if (_tryStartRubberBand && canStart) { - _isRubberBandSpanning = true; - _cachedStartingMousePosition = mousePosition; - _rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero); - _tryStartRubberBand = false; + var delta = mousePosition - _cachedStartingMousePosition; + if (Mathf.Abs(delta.X) > 0.1f || Mathf.Abs(delta.Y) > 0.1f) + { + _isRubberBandSpanning = true; + _rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero); + _tryStartRubberBand = false; + } } else if (_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown) { diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 5343a1fe5..d25a8ab80 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -620,7 +620,7 @@ namespace FlaxEditor.Viewport { base.OnLeftMouseButtonDown(); - _rubberBandSelector.TryStartingRubberBandSelection(); + _rubberBandSelector.TryStartingRubberBandSelection(_viewMousePos); } /// From e1ffdee57a0bf8ca6186bd247da0fbb140c0cd4a Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 30 Jan 2026 15:51:53 -0600 Subject: [PATCH 043/100] If UI is culling children, dont return children outside of client area for raycast. --- Source/Engine/UI/GUI/ContainerControl.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/Engine/UI/GUI/ContainerControl.cs b/Source/Engine/UI/GUI/ContainerControl.cs index ada93ff1e..55fcecf80 100644 --- a/Source/Engine/UI/GUI/ContainerControl.cs +++ b/Source/Engine/UI/GUI/ContainerControl.cs @@ -901,6 +901,15 @@ namespace FlaxEngine.GUI internal bool RayCastChildren(ref Float2 location, out Control hit) { + if (_clipChildren) + { + GetDesireClientArea(out var clientArea); + if (!clientArea.Contains(ref location)) + { + hit = null; + return false; + } + } for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; From 2ab8b9dd557b0e6458ba53610206857332e3156d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 1 Feb 2026 23:11:44 +0100 Subject: [PATCH 044/100] Add Texture Group option for triplanar and procedural samplers #3904 --- Source/Editor/Surface/Archetypes/Textures.cs | 75 ++++++++++++++----- .../MaterialGenerator.Textures.cpp | 6 +- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 56f8154d7..f09ee5015 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -23,11 +23,14 @@ namespace FlaxEditor.Surface.Archetypes TextureGroup = 4, } - internal class SampleTextureNode : SurfaceNode + internal class TextureSamplerNode : SurfaceNode { private ComboBox _textureGroupPicker; + protected int _samplerTypeValueIndex = -1; + protected int _textureGroupValueIndex = -1; + protected int _level = 5; - public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + protected TextureSamplerNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { } @@ -48,13 +51,13 @@ namespace FlaxEditor.Surface.Archetypes private void UpdateUI() { - if ((int)Values[0] == (int)CommonSamplerType.TextureGroup) + if ((int)Values[_samplerTypeValueIndex] == (int)CommonSamplerType.TextureGroup) { if (_textureGroupPicker == null) { _textureGroupPicker = new ComboBox { - Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * 5), + Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * _level), Width = 100, Parent = this, }; @@ -71,7 +74,7 @@ namespace FlaxEditor.Surface.Archetypes _textureGroupPicker.Visible = true; } _textureGroupPicker.SelectedIndexChanged -= OnSelectedTextureGroupChanged; - _textureGroupPicker.SelectedIndex = (int)Values[2]; + _textureGroupPicker.SelectedIndex = (int)Values[_textureGroupValueIndex]; _textureGroupPicker.SelectedIndexChanged += OnSelectedTextureGroupChanged; } else if (_textureGroupPicker != null) @@ -83,7 +86,39 @@ namespace FlaxEditor.Surface.Archetypes private void OnSelectedTextureGroupChanged(ComboBox comboBox) { - SetValue(2, _textureGroupPicker.SelectedIndex); + SetValue(_textureGroupValueIndex, _textureGroupPicker.SelectedIndex); + } + } + + internal class SampleTextureNode : TextureSamplerNode + { + public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + _samplerTypeValueIndex = 0; + _textureGroupValueIndex = 2; + } + } + + internal class TriplanarSampleTextureNode : TextureSamplerNode + { + public TriplanarSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + _samplerTypeValueIndex = 3; + _textureGroupValueIndex = 5; + _level = 5; + } + } + + internal class ProceduralSampleTextureNode : TextureSamplerNode + { + public ProceduralSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + _samplerTypeValueIndex = 0; + _textureGroupValueIndex = 2; + _level = 4; } } @@ -280,9 +315,9 @@ namespace FlaxEditor.Surface.Archetypes ConnectionsHints = ConnectionsHint.Vector, DefaultValues = new object[] { - 0, - -1.0f, - 0, + (int)CommonSamplerType.LinearClamp, // Sampler + -1.0f, // Level + 0, // Texture Group }, Elements = new[] { @@ -402,6 +437,7 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 16, + Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch), Title = "Triplanar Texture", Description = "Projects a texture using world-space coordinates with triplanar mapping.", Flags = NodeFlags.MaterialGraph, @@ -411,8 +447,9 @@ namespace FlaxEditor.Surface.Archetypes Float3.One, // Scale 1.0f, // Blend Float2.Zero, // Offset - 2, // Sampler + (int)CommonSamplerType.LinearWrap, // Sampler false, // Local + 0, // Texture Group }, Elements = new[] { @@ -430,17 +467,17 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 17, - Create = (id, context, arch, groupArch) => new SampleTextureNode(id, context, arch, groupArch), + Create = (id, context, arch, groupArch) => new ProceduralSampleTextureNode(id, context, arch, groupArch), Title = "Procedural Sample Texture", Description = "Samples a texture to create a more natural look with less obvious tiling.", Flags = NodeFlags.MaterialGraph, - Size = new Float2(240, 110), + Size = new Float2(240, 130), ConnectionsHints = ConnectionsHint.Vector, DefaultValues = new object[] { - 2, - -1.0f, - 0, + (int)CommonSamplerType.LinearWrap, // Sampler + -1.0f, // Level + 0, // Texture Group }, Elements = new[] { @@ -448,8 +485,8 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Input(1, "UVs", true, null, 1), NodeElementArchetype.Factory.Input(2, "Offset", true, typeof(Float2), 3), NodeElementArchetype.Factory.Output(0, "Color", typeof(Float4), 4), - NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 4, "Sampler"), - NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 4, 100, 0, typeof(CommonSamplerType)) + NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 3, "Sampler"), + NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 3, 100, 0, typeof(CommonSamplerType)) } }, new NodeArchetype @@ -469,6 +506,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 23, Title = "Triplanar Normal Map", + Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch), Description = "Projects a normal map texture using world-space coordinates with triplanar mapping.", Flags = NodeFlags.MaterialGraph, Size = new Float2(280, 100), @@ -477,8 +515,9 @@ namespace FlaxEditor.Surface.Archetypes Float3.One, // Scale 1.0f, // Blend Float2.Zero, // Offset - 2, // Sampler + (int)CommonSamplerType.LinearWrap, // Sampler false, // Local + 0, // Texture Group }, Elements = new[] { diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp index c48a4c569..7978b4f9e 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp @@ -534,7 +534,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) } // Sample Texture case 9: - // Procedural Texture Sample + // Procedural Sample Texture case 17: { // Get input boxes @@ -739,7 +739,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) const int32 samplerIndex = node->Values.Count() >= 4 ? node->Values[3].AsInt : LinearWrap; if (samplerIndex == TextureGroup) { - auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[3].AsInt); + auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[5].AsInt); samplerName = *textureGroupSampler.ShaderName; } else if (samplerIndex >= 0 && samplerIndex < ARRAY_COUNT(SamplerNames)) @@ -828,7 +828,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) const int32 samplerIndex = node->Values[3].AsInt; if (samplerIndex == TextureGroup) { - auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[3].AsInt); + auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[5].AsInt); samplerName = *textureGroupSampler.ShaderName; } else if (samplerIndex >= 0 && samplerIndex < ARRAY_COUNT(SamplerNames)) From bf0c7fe0dc4a8e71d6ffafe8ed622ee834f14c6d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 1 Feb 2026 23:32:00 +0100 Subject: [PATCH 045/100] Fix timescale in `FixedTimestep` particles update and when using Editor preview #3811 --- Source/Engine/Particles/ParticleEffect.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index 9592147a7..14467664c 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -11,6 +11,10 @@ #include "Engine/Level/Scene/Scene.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/Engine.h" +#if USE_EDITOR +#include "Editor/Editor.h" +#include "Editor/Managed/ManagedEditor.h" +#endif ParticleEffect::ParticleEffect(const SpawnParams& params) : Actor(params) @@ -465,7 +469,12 @@ void ParticleEffect::Update() if (UpdateMode == SimulationUpdateMode::FixedTimestep) { // Check if last simulation update was past enough to kick a new on - const float time = Time::Update.Time.GetTotalSeconds(); + bool useTimeScale = UseTimeScale; +#if USE_EDITOR + if (!Editor::IsPlayMode && IsDuringPlay()) + useTimeScale = false; +#endif + const float time = (useTimeScale ? Time::Update.Time : Time::Update.UnscaledTime).GetTotalSeconds(); if (time - Instance.LastUpdateTime < FixedTimestep) return; } @@ -475,9 +484,6 @@ void ParticleEffect::Update() #if USE_EDITOR -#include "Editor/Editor.h" -#include "Editor/Managed/ManagedEditor.h" - void ParticleEffect::UpdateExecuteInEditor() { // Auto-play in Editor From 449fc597b5bfdc6e273c2225bed12cb9b796fe31 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 2 Feb 2026 19:21:45 +0100 Subject: [PATCH 046/100] Fix crash when using overlapping instances #3899 --- Source/Engine/Foliage/Foliage.cpp | 42 ++++++++++++++++++++---- Source/Engine/Foliage/FoliageCluster.cpp | 1 + Source/Engine/Foliage/FoliageCluster.h | 5 +++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index ddc3468f7..f8b9c7b0f 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -44,20 +44,39 @@ void Foliage::AddToCluster(ChunkedArray ZeroTolerance); ASSERT(cluster->Bounds.Intersects(instance.Bounds)); - // Find target cluster - while (cluster->Children[0]) + // Minor clusters don't use bounds intersection but try to find the first free cluster instead + if (cluster->IsMinor) { + // Insert into the first non-full child cluster or subdivide 1st child +#define CHECK_CHILD(idx) \ + if (cluster->Children[idx]->Instances.Count() < FOLIAGE_CLUSTER_CAPACITY) \ + { \ + cluster->Children[idx]->Instances.Add(&instance); \ + return; \ + } + CHECK_CHILD(3); + CHECK_CHILD(2); + CHECK_CHILD(1); + cluster = cluster->Children[0]; +#undef CHECK_CHILD + } + else + { + // Find target cluster + while (cluster->Children[0]) + { #define CHECK_CHILD(idx) \ if (cluster->Children[idx]->Bounds.Intersects(instance.Bounds)) \ { \ cluster = cluster->Children[idx]; \ continue; \ } - CHECK_CHILD(0); - CHECK_CHILD(1); - CHECK_CHILD(2); - CHECK_CHILD(3); + CHECK_CHILD(0); + CHECK_CHILD(1); + CHECK_CHILD(2); + CHECK_CHILD(3); #undef CHECK_CHILD + } } // Check if it's not full @@ -79,11 +98,20 @@ void Foliage::AddToCluster(ChunkedArrayBounds.Minimum; const Vector3 max = cluster->Bounds.Maximum; - const Vector3 size = cluster->Bounds.GetSize(); + const Vector3 size = max - min; cluster->Children[0]->Init(BoundingBox(min, min + size * Vector3(0.5f, 1.0f, 0.5f))); cluster->Children[1]->Init(BoundingBox(min + size * Vector3(0.5f, 0.0f, 0.5f), max)); cluster->Children[2]->Init(BoundingBox(min + size * Vector3(0.5f, 0.0f, 0.0f), min + size * Vector3(1.0f, 1.0f, 0.5f))); cluster->Children[3]->Init(BoundingBox(min + size * Vector3(0.0f, 0.0f, 0.5f), min + size * Vector3(0.5f, 1.0f, 1.0f))); + if (cluster->IsMinor || size.MinValue() < 1.0f) + { + // Mark children as minor to avoid infinite subdivision + cluster->IsMinor = true; + cluster->Children[0]->IsMinor = true; + cluster->Children[1]->IsMinor = true; + cluster->Children[2]->IsMinor = true; + cluster->Children[3]->IsMinor = true; + } // Move instances to a proper cells for (int32 i = 0; i < cluster->Instances.Count(); i++) diff --git a/Source/Engine/Foliage/FoliageCluster.cpp b/Source/Engine/Foliage/FoliageCluster.cpp index 1f76e5086..fd4c0f753 100644 --- a/Source/Engine/Foliage/FoliageCluster.cpp +++ b/Source/Engine/Foliage/FoliageCluster.cpp @@ -9,6 +9,7 @@ void FoliageCluster::Init(const BoundingBox& bounds) Bounds = bounds; TotalBounds = bounds; MaxCullDistance = 0.0f; + IsMinor = false; Children[0] = nullptr; Children[1] = nullptr; diff --git a/Source/Engine/Foliage/FoliageCluster.h b/Source/Engine/Foliage/FoliageCluster.h index 55cbeb027..c55305c5d 100644 --- a/Source/Engine/Foliage/FoliageCluster.h +++ b/Source/Engine/Foliage/FoliageCluster.h @@ -33,6 +33,11 @@ public: /// float MaxCullDistance; + /// + /// Flag used by clusters that are not typical quad-tree nodes but have no volume (eg. lots of instances placed on top of each other). + /// + int32 IsMinor : 1; + /// /// The child clusters. If any element is valid then all are created. /// From 4e3e9386cc2d243232a17f87dfde67e5beeed6ea Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 2 Feb 2026 22:25:08 +0100 Subject: [PATCH 047/100] Fix restoring `Min Screen Size` of the model on reimport #3807 --- Source/Engine/ContentImporters/ImportModel.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/Engine/ContentImporters/ImportModel.cpp b/Source/Engine/ContentImporters/ImportModel.cpp index 91547dc8d..f3548dc5c 100644 --- a/Source/Engine/ContentImporters/ImportModel.cpp +++ b/Source/Engine/ContentImporters/ImportModel.cpp @@ -478,16 +478,23 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context) } // Check if restore local changes on asset reimport + constexpr bool RestoreModelOptionsOnReimport = true; constexpr bool RestoreAnimEventsOnReimport = true; + const bool restoreModelOptions = RestoreModelOptionsOnReimport && (options.Type == ModelTool::ModelType::Model || options.Type == ModelTool::ModelType::SkinnedModel); const bool restoreMaterials = options.RestoreMaterialsOnReimport && data->Materials.HasItems(); const bool restoreAnimEvents = RestoreAnimEventsOnReimport && options.Type == ModelTool::ModelType::Animation && data->Animations.HasItems(); - if ((restoreMaterials || restoreAnimEvents) && FileSystem::FileExists(context.TargetAssetPath)) + if ((restoreModelOptions || restoreMaterials || restoreAnimEvents) && FileSystem::FileExists(context.TargetAssetPath)) { AssetReference asset = Content::LoadAsync(context.TargetAssetPath); if (asset && !asset->WaitForLoaded()) { auto* model = ScriptingObject::Cast(asset); auto* animation = ScriptingObject::Cast(asset); + if (restoreModelOptions && model) + { + // Copy general properties + data->MinScreenSize = model->MinScreenSize; + } if (restoreMaterials && model) { // Copy material settings From 3a0af54f4841c4074b25036fcedb34cb2dd62ba8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 09:23:19 +0100 Subject: [PATCH 048/100] Add `Collision Meshes Postfix` to filter collision meshes inside imported model via ending #3792 --- Source/Engine/Tools/ModelTool/ModelTool.cpp | 7 +++++-- Source/Engine/Tools/ModelTool/ModelTool.h | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 843822b98..57afeb7a5 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -567,6 +567,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(CalculateBoneOffsetMatrices); SERIALIZE(LightmapUVsSource); SERIALIZE(CollisionMeshesPrefix); + SERIALIZE(CollisionMeshesPostfix); SERIALIZE(CollisionType); SERIALIZE(PositionFormat); SERIALIZE(TexCoordFormat); @@ -621,6 +622,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(CalculateBoneOffsetMatrices); DESERIALIZE(LightmapUVsSource); DESERIALIZE(CollisionMeshesPrefix); + DESERIALIZE(CollisionMeshesPostfix); DESERIALIZE(CollisionType); DESERIALIZE(PositionFormat); DESERIALIZE(TexCoordFormat); @@ -1830,7 +1832,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } // Collision mesh output - if (options.CollisionMeshesPrefix.HasChars()) + if (options.CollisionMeshesPrefix.HasChars() || options.CollisionMeshesPostfix.HasChars()) { // Extract collision meshes from the model ModelData collisionModel; @@ -1839,7 +1841,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option for (int32 i = lod.Meshes.Count() - 1; i >= 0; i--) { auto mesh = lod.Meshes[i]; - if (mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase)) + if ((options.CollisionMeshesPrefix.HasChars() && mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase)) || + (options.CollisionMeshesPostfix.HasChars() && mesh->Name.EndsWith(options.CollisionMeshesPostfix, StringSearchCase::IgnoreCase))) { // Remove material slot used by this mesh (if no other mesh else uses it) int32 materialSlotUsageCount = 0; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index d7545b92e..bc96e8308 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -221,6 +221,9 @@ public: // If specified, all meshes that name starts with this prefix in the name will be imported as a separate collision data asset (excluded used for rendering). API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") String CollisionMeshesPrefix = TEXT(""); + // If specified, all meshes that name ends with this postfix in the name will be imported as a separate collision data asset (excluded used for rendering). + API_FIELD(Attributes="EditorOrder(101), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") + String CollisionMeshesPostfix = TEXT(""); // The type of collision that should be generated if the mesh has a collision prefix specified. API_FIELD(Attributes="EditorOrder(105), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") CollisionDataType CollisionType = CollisionDataType::ConvexMesh; From b1f85b746204b59508193b396279ea14fc8c77ee Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 09:23:50 +0100 Subject: [PATCH 049/100] Add `Auto` collision option to handle imported or created model collider asset #3792 --- .../SceneGraph/Actors/StaticModelNode.cs | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs index e95364c2d..4cd63f05d 100644 --- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs @@ -47,6 +47,11 @@ namespace FlaxEditor.SceneGraph.Actors } } + /// + /// Gets the model used by this actor. + /// + public Model Model => ((StaticModel)Actor).Model; + /// public StaticModelNode(Actor actor) : base(actor) @@ -120,12 +125,12 @@ namespace FlaxEditor.SceneGraph.Actors { base.OnContextMenu(contextMenu, window); - // Check if every selected node is a primitive + // Check if every selected node is a primitive or has collision asset var selection = GetSelection(window); bool autoOptionEnabled = true; foreach (var node in selection) { - if (node is StaticModelNode staticModelNode && !staticModelNode.IsPrimitive) + if (node is StaticModelNode staticModelNode && (!staticModelNode.IsPrimitive && GetCollisionData(staticModelNode.Model) == null)) { autoOptionEnabled = false; break; @@ -201,6 +206,54 @@ namespace FlaxEditor.SceneGraph.Actors return Array.Empty(); } + private static bool TryCollisionData(Model model, BinaryAssetItem assetItem, out CollisionData collisionData) + { + collisionData = FlaxEngine.Content.Load(assetItem.ID); + if (collisionData) + { + var options = collisionData.Options; + if (options.Model == model.ID || options.Model == Guid.Empty) + return true; + } + return false; + } + + private CollisionData GetCollisionData(Model model) + { + if (model == null) + return null; + + // Check if there already is collision data for that model to reuse + var modelItem = (AssetItem)Editor.Instance.ContentDatabase.Find(model.ID); + if (modelItem?.ParentFolder != null) + { + foreach (var child in modelItem.ParentFolder.Children) + { + // Check if there is collision that was made with this model + if (child is BinaryAssetItem b && b.IsOfType()) + { + if (TryCollisionData(model, b, out var collisionData)) + return collisionData; + } + + // Check if there is an auto-imported collision + if (child is ContentFolder childFolder && childFolder.ShortName == modelItem.ShortName) + { + foreach (var childFolderChild in childFolder.Children) + { + if (childFolderChild is BinaryAssetItem c && c.IsOfType()) + { + if (TryCollisionData(model, c, out var collisionData)) + return collisionData; + } + } + } + } + } + + return null; + } + private void CreateAuto(StaticModel actor, Spawner spawner, bool singleNode) { // Special case for in-built Editor models that can use analytical collision @@ -243,6 +296,15 @@ namespace FlaxEditor.SceneGraph.Actors collider.LocalPosition = new Vector3(0, 50.0f, 0); collider.LocalOrientation = Quaternion.Euler(0, 0, 90.0f); } + else + { + var collider = new MeshCollider + { + Transform = actor.Transform, + CollisionData = GetCollisionData(model), + }; + spawner(collider); + } } private void CreateBox(StaticModel actor, Spawner spawner, bool singleNode) From 1535f95cf11b92185ce2918b236ee8a25e81cd61 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 13:18:02 +0100 Subject: [PATCH 050/100] Fix mesh collision proxy setup for meshes using packed positions format #3791 --- Source/Engine/Core/Math/Half.h | 13 ++++++++ Source/Engine/Core/Math/Packed.cpp | 10 ------ Source/Engine/Core/Math/Packed.h | 11 +++++-- .../Engine/Graphics/Models/CollisionProxy.h | 33 ++++++++++++++----- Source/Engine/Graphics/Models/MeshBase.cpp | 5 +-- 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/Source/Engine/Core/Math/Half.h b/Source/Engine/Core/Math/Half.h index 2617649d0..ebb2cc951 100644 --- a/Source/Engine/Core/Math/Half.h +++ b/Source/Engine/Core/Math/Half.h @@ -248,6 +248,19 @@ public: explicit Half4(const Color& c); explicit Half4(const Rectangle& rect); + operator Float2() const + { + return ToFloat2(); + } + operator Float3() const + { + return ToFloat3(); + } + operator Float4() const + { + return ToFloat4(); + } + public: Float2 ToFloat2() const; Float3 ToFloat3() const; diff --git a/Source/Engine/Core/Math/Packed.cpp b/Source/Engine/Core/Math/Packed.cpp index 55ebd9ccf..b22e65d3e 100644 --- a/Source/Engine/Core/Math/Packed.cpp +++ b/Source/Engine/Core/Math/Packed.cpp @@ -41,16 +41,6 @@ FloatR10G10B10A2::FloatR10G10B10A2(const float* values) { } -FloatR10G10B10A2::operator Float3() const -{ - return ToFloat3(); -} - -FloatR10G10B10A2::operator Float4() const -{ - return ToFloat4(); -} - Float3 FloatR10G10B10A2::ToFloat3() const { Float3 vectorOut; diff --git a/Source/Engine/Core/Math/Packed.h b/Source/Engine/Core/Math/Packed.h index 8e3aad41f..c5b5e827f 100644 --- a/Source/Engine/Core/Math/Packed.h +++ b/Source/Engine/Core/Math/Packed.h @@ -40,9 +40,14 @@ struct FLAXENGINE_API FloatR10G10B10A2 { return Value; } - - operator Float3() const; - operator Float4() const; + operator Float3() const + { + return ToFloat3(); + } + operator Float4() const + { + return ToFloat4(); + } FloatR10G10B10A2& operator=(const FloatR10G10B10A2& other) { diff --git a/Source/Engine/Graphics/Models/CollisionProxy.h b/Source/Engine/Graphics/Models/CollisionProxy.h index 5dc021867..eba17cf4e 100644 --- a/Source/Engine/Graphics/Models/CollisionProxy.h +++ b/Source/Engine/Graphics/Models/CollisionProxy.h @@ -6,7 +6,9 @@ #include "Engine/Core/Math/Transform.h" #include "Engine/Core/Math/Ray.h" #include "Engine/Core/Math/CollisionsHelper.h" +#include "Engine/Core/Math/Packed.h" #include "Engine/Core/Collections/Array.h" +#include "Engine/Graphics/PixelFormat.h" /// /// Helper container used for detailed triangle mesh intersections tests. @@ -31,23 +33,38 @@ public: } template - void Init(uint32 vertices, uint32 triangles, const Float3* positions, const IndexType* indices, uint32 positionsStride = sizeof(Float3)) + void Init(uint32 vertices, uint32 triangles, const Float3* positions, const IndexType* indices, uint32 positionsStride = sizeof(Float3), PixelFormat positionsFormat = PixelFormat::R32G32B32_Float) { Triangles.Clear(); Triangles.EnsureCapacity(triangles, false); const IndexType* it = indices; - for (uint32 i = 0; i < triangles; i++) +#define LOOP_BEGIN() \ + for (uint32 i = 0; i < triangles; i++) \ + { \ + const IndexType i0 = *(it++); \ + const IndexType i1 = *(it++); \ + const IndexType i2 = *(it++); \ + if (i0 < vertices && i1 < vertices && i2 < vertices) \ { - const IndexType i0 = *(it++); - const IndexType i1 = *(it++); - const IndexType i2 = *(it++); - if (i0 < vertices && i1 < vertices && i2 < vertices) - { +#define LOOP_END() } } + if (positionsFormat == PixelFormat::R32G32B32_Float) + { + LOOP_BEGIN() #define GET_POS(idx) *(const Float3*)((const byte*)positions + positionsStride * idx) Triangles.Add({ GET_POS(i0), GET_POS(i1), GET_POS(i2) }); #undef GET_POS - } + LOOP_END() } + else if (positionsFormat == PixelFormat::R16G16B16A16_Float) + { + LOOP_BEGIN() +#define GET_POS(idx) (Float3)*(const Half4*)((const byte*)positions + positionsStride * idx) + Triangles.Add({ GET_POS(i0), GET_POS(i1), GET_POS(i2) }); +#undef GET_POS + LOOP_END() + } +#undef LOOP_BEGIN +#undef LOOP_END } void Clear() diff --git a/Source/Engine/Graphics/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp index 3434cd91a..91ed892ea 100644 --- a/Source/Engine/Graphics/Models/MeshBase.cpp +++ b/Source/Engine/Graphics/Models/MeshBase.cpp @@ -470,10 +470,11 @@ bool MeshBase::Init(uint32 vertices, uint32 triangles, const ArrayFindElement(VertexElement::Types::Position); if (use16BitIndexBuffer) - _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint16*)ibData); + _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint16*)ibData, vertexBuffer0->GetStride(), positionsElement.Format); else - _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint32*)ibData); + _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint32*)ibData, vertexBuffer0->GetStride(), positionsElement.Format); #endif // Free old buffers From f725f4c0b982764a8b3387290f6fb390cf4884ae Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 13:24:13 +0100 Subject: [PATCH 051/100] Add saving viewport icons scale (global) within Editor window layout #3820 --- Source/Editor/Windows/EditGameWindow.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Windows/EditGameWindow.cs b/Source/Editor/Windows/EditGameWindow.cs index 888dd4250..dbeab28b1 100644 --- a/Source/Editor/Windows/EditGameWindow.cs +++ b/Source/Editor/Windows/EditGameWindow.cs @@ -429,6 +429,7 @@ namespace FlaxEditor.Windows writer.WriteAttributeString("FarPlane", Viewport.FarPlane.ToString()); writer.WriteAttributeString("FieldOfView", Viewport.FieldOfView.ToString()); writer.WriteAttributeString("MovementSpeed", Viewport.MovementSpeed.ToString()); + writer.WriteAttributeString("ViewportIconsScale", ViewportIconsRenderer.Scale.ToString()); writer.WriteAttributeString("OrthographicScale", Viewport.OrthographicScale.ToString()); writer.WriteAttributeString("UseOrthographicProjection", Viewport.UseOrthographicProjection.ToString()); writer.WriteAttributeString("ViewFlags", ((ulong)Viewport.Task.View.Flags).ToString()); @@ -439,31 +440,24 @@ namespace FlaxEditor.Windows { if (bool.TryParse(node.GetAttribute("GridEnabled"), out bool value1)) Viewport.Grid.Enabled = value1; - if (bool.TryParse(node.GetAttribute("ShowFpsCounter"), out value1)) Viewport.ShowFpsCounter = value1; - if (bool.TryParse(node.GetAttribute("ShowNavigation"), out value1)) Viewport.ShowNavigation = value1; - if (float.TryParse(node.GetAttribute("NearPlane"), out float value2)) Viewport.NearPlane = value2; - if (float.TryParse(node.GetAttribute("FarPlane"), out value2)) Viewport.FarPlane = value2; - if (float.TryParse(node.GetAttribute("FieldOfView"), out value2)) Viewport.FieldOfView = value2; - if (float.TryParse(node.GetAttribute("MovementSpeed"), out value2)) Viewport.MovementSpeed = value2; - + if (float.TryParse(node.GetAttribute("ViewportIconsScale"), out value2)) + ViewportIconsRenderer.Scale = value2; if (float.TryParse(node.GetAttribute("OrthographicScale"), out value2)) Viewport.OrthographicScale = value2; - if (bool.TryParse(node.GetAttribute("UseOrthographicProjection"), out value1)) Viewport.UseOrthographicProjection = value1; - if (ulong.TryParse(node.GetAttribute("ViewFlags"), out ulong value3)) Viewport.Task.ViewFlags = (ViewFlags)value3; From c828c901616408442156c2ff8911fab845b2fde0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 15:42:26 +0100 Subject: [PATCH 052/100] Add `Set Parameter` node to Anim Graph #2162 --- .../Editor/Surface/Archetypes/Parameters.cs | 21 +++++++++++++++++++ .../Animations/Graph/AnimGroup.Animation.cpp | 15 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/Parameters.cs b/Source/Editor/Surface/Archetypes/Parameters.cs index 3d74d2e10..3383e7662 100644 --- a/Source/Editor/Surface/Archetypes/Parameters.cs +++ b/Source/Editor/Surface/Archetypes/Parameters.cs @@ -1100,6 +1100,27 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116) } }, + new NodeArchetype + { + TypeID = 5, + Create = (id, context, arch, groupArch) => new SurfaceNodeParamsSet(id, context, arch, groupArch), + Title = "Set Parameter", + Description = "Parameter value setter invoked when the animation pose is evaluated (output pose comes from input)", + Flags = NodeFlags.AnimGraph, + Size = new Float2(140, 40), + DefaultValues = new object[] + { + Guid.Empty, + null + }, + Elements = new[] + { + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 0), + NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 2), + NodeElementArchetype.Factory.Input(1, string.Empty, true, ScriptType.Null, 1, 1), + NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116) + } + }, }; } } diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 1e4444af0..08767728a 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -958,6 +958,21 @@ void AnimGraphExecutor::ProcessGroupParameters(Box* box, Node* node, Value& valu } break; } + // Set Parameter + case 5: + { + // Set parameter value + int32 paramIndex; + const auto param = _graph.GetParameter((Guid)node->Values[0], paramIndex); + if (param) + { + context.Data->Parameters[paramIndex].Value = tryGetValue(node->GetBox(1), 1, Value::Null); + } + + // Pass over the pose + value = tryGetValue(node->GetBox(2), Value::Null); + break; + } default: break; } From 21e2c830e5e85f3e8cb852658fdc7930c74d3208 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 16:32:22 +0100 Subject: [PATCH 053/100] Refactor `Animation` editor to use cloned asset for live preview of nested animations editing #3773 --- .../Editor/Windows/Assets/AnimationWindow.cs | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs index 41fdfde65..268e18503 100644 --- a/Source/Editor/Windows/Assets/AnimationWindow.cs +++ b/Source/Editor/Windows/Assets/AnimationWindow.cs @@ -2,7 +2,6 @@ using System; using System.Globalization; -using System.IO; using System.Reflection; using System.Xml; using FlaxEditor.Content; @@ -25,7 +24,7 @@ namespace FlaxEditor.Windows.Assets /// /// /// - public sealed class AnimationWindow : AssetEditorWindowBase + public sealed class AnimationWindow : ClonedAssetEditorWindowBase { private sealed class Preview : AnimationPreview { @@ -255,6 +254,7 @@ namespace FlaxEditor.Windows.Assets private bool _isWaitingForTimelineLoad; private SkinnedModel _initialPreviewModel, _initialBaseModel; private float _initialPanel2Splitter = 0.6f; + private bool _timelineIsDirty; /// /// Gets the animation timeline editor. @@ -295,7 +295,7 @@ namespace FlaxEditor.Windows.Assets Parent = _panel1.Panel1, Enabled = false }; - _timeline.Modified += MarkAsEdited; + _timeline.Modified += OnTimelineModified; _timeline.SetNoTracksText("Loading..."); // Asset properties @@ -321,11 +321,31 @@ namespace FlaxEditor.Windows.Assets { MarkAsEdited(); UpdateToolstrip(); + _propertiesPresenter.BuildLayout(); + } + + private void OnTimelineModified() + { + _timelineIsDirty = true; + MarkAsEdited(); + } + + private bool RefreshTempAsset() + { + if (_asset == null || _isWaitingForTimelineLoad) + return true; + if (_timeline.IsModified) + { + _timeline.Save(_asset); + } + _propertiesPresenter.BuildLayoutOnUpdate(); + + return false; } private string GetPreviewModelCacheName() { - return _asset.ID + ".PreviewModel"; + return _item.ID + ".PreviewModel"; } /// @@ -361,7 +381,11 @@ namespace FlaxEditor.Windows.Assets if (!IsEdited) return; - _timeline.Save(_asset); + if (RefreshTempAsset()) + return; + if (SaveToOriginal()) + return; + ClearEditedFlag(); _item.RefreshThumbnail(); } @@ -414,10 +438,18 @@ namespace FlaxEditor.Windows.Assets { base.Update(deltaTime); + // Check if temporary asset need to be updated + if (_timelineIsDirty) + { + _timelineIsDirty = false; + RefreshTempAsset(); + } + + // Check if need to load timeline if (_isWaitingForTimelineLoad && _asset.IsLoaded) { _isWaitingForTimelineLoad = false; - _timeline._id = _asset.ID; + _timeline._id = _item.ID; _timeline.Load(_asset); _undo.Clear(); _timeline.Enabled = true; From bbb5354e9ce964eeaa9f9cb974551ade82c5c9f8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 16:32:34 +0100 Subject: [PATCH 054/100] Fix potential crash on out of bounds access --- .../Tools/MaterialGenerator/MaterialGenerator.Textures.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp index 7978b4f9e..13b835185 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp @@ -585,7 +585,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) const auto offset = useOffset ? eatBox(offsetBox->GetParent(), offsetBox->FirstConnection()) : Value::Zero; const Char* samplerName; const int32 samplerIndex = node->Values[0].AsInt; - if (samplerIndex == TextureGroup) + if (samplerIndex == TextureGroup && node->Values.Count() > 2) { auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[2].AsInt); samplerName = *textureGroupSampler.ShaderName; From 0f383d2fc694c6ba751ce40cb0f2f8618a95b842 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 16:46:46 +0100 Subject: [PATCH 055/100] Move transform gizmo visibility to the base class #3692 --- Source/Editor/Gizmo/GizmoBase.cs | 6 ++++++ Source/Editor/Gizmo/TransformGizmo.cs | 13 ------------- Source/Editor/Viewport/MainEditorGizmoViewport.cs | 7 +++++-- Source/Editor/Viewport/PrefabWindowViewport.cs | 7 +++++-- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/Source/Editor/Gizmo/GizmoBase.cs b/Source/Editor/Gizmo/GizmoBase.cs index 57cd62d79..80c3e5012 100644 --- a/Source/Editor/Gizmo/GizmoBase.cs +++ b/Source/Editor/Gizmo/GizmoBase.cs @@ -13,6 +13,7 @@ namespace FlaxEditor.Gizmo public abstract class GizmoBase { private IGizmoOwner _owner; + private bool _visible = true; /// /// Gets the gizmo owner. @@ -34,6 +35,11 @@ namespace FlaxEditor.Gizmo /// public virtual BoundingSphere FocusBounds => BoundingSphere.Empty; + /// + /// Gets or sets a value indicating whether this gizmo is visible. + /// + public bool Visible { get { return _visible; } set { _visible = value; } } + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/Gizmo/TransformGizmo.cs b/Source/Editor/Gizmo/TransformGizmo.cs index 3e4a193a4..91e37ca25 100644 --- a/Source/Editor/Gizmo/TransformGizmo.cs +++ b/Source/Editor/Gizmo/TransformGizmo.cs @@ -31,7 +31,6 @@ namespace FlaxEditor.Gizmo private readonly List _selection = new List(); private readonly List _selectionParents = new List(); - private bool _visible = true; /// /// The event to apply objects transformation. @@ -53,11 +52,6 @@ namespace FlaxEditor.Gizmo /// public List SelectedParents => _selectionParents; - /// - /// Gets or sets a value indicating whether this is visible. - /// - public bool Visible { get { return _visible; } set { _visible = value; } } - /// /// Initializes a new instance of the class. /// @@ -281,13 +275,6 @@ namespace FlaxEditor.Gizmo base.OnSelectionChanged(newSelection); } - /// - public override void Draw(ref RenderContext renderContext) - { - if (Visible) - base.Draw(ref renderContext); - } - /// protected override int SelectionCount => _selectionParents.Count; diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 396c38c07..3d366809c 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -391,9 +391,12 @@ namespace FlaxEditor.Viewport public void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth) { // Draw gizmos - for (int i = 0; i < Gizmos.Count; i++) + foreach (var gizmo in Gizmos) { - Gizmos[i].Draw(ref renderContext); + if (gizmo.Visible) + { + gizmo.Draw(ref renderContext); + } } // Draw selected objects debug shapes and visuals diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index a22b4042f..7f2b1a471 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -720,9 +720,12 @@ namespace FlaxEditor.Viewport public override void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth) { // Draw gizmos - for (int i = 0; i < Gizmos.Count; i++) + foreach (var gizmo in Gizmos) { - Gizmos[i].Draw(ref renderContext); + if (gizmo.Visible) + { + gizmo.Draw(ref renderContext); + } } base.DrawEditorPrimitives(context, ref renderContext, target, targetDepth); From 217701ae05876407b7e7cebb2d47d7192ae1af34 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 17:48:30 +0100 Subject: [PATCH 056/100] Adjust error in case we support more listeners #3686 --- Source/Engine/Audio/AudioListener.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Audio/AudioListener.cpp b/Source/Engine/Audio/AudioListener.cpp index 59975e33c..3151abafd 100644 --- a/Source/Engine/Audio/AudioListener.cpp +++ b/Source/Engine/Audio/AudioListener.cpp @@ -41,7 +41,10 @@ void AudioListener::OnEnable() ASSERT(!Audio::Listeners.Contains(this)); if (Audio::Listeners.Count() >= AUDIO_MAX_LISTENERS) { - LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time."); + if IF_CONSTEXPR (AUDIO_MAX_LISTENERS == 1) + LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time."); + else + LOG(Warning, "Too many Audio Listener active."); } else { From 834c4553b22d2d8ec771293f5edf8cea9ba0fbed Mon Sep 17 00:00:00 2001 From: Phantom Date: Tue, 3 Feb 2026 17:57:31 +0100 Subject: [PATCH 057/100] Add Missing functions on `Vector4` #3715 --- Source/Engine/Core/Math/Vector4.cs | 13 +++++ Source/Engine/Core/Math/Vector4.h | 80 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/Source/Engine/Core/Math/Vector4.cs b/Source/Engine/Core/Math/Vector4.cs index e50d03ca0..909e35157 100644 --- a/Source/Engine/Core/Math/Vector4.cs +++ b/Source/Engine/Core/Math/Vector4.cs @@ -260,6 +260,19 @@ namespace FlaxEngine /// public bool IsNormalized => Mathr.Abs((X * X + Y * Y + Z * Z + W * W) - 1.0f) < 1e-4f; + /// + /// Gets the normalized vector. Returned vector has length equal 1. + /// + public Vector4 Normalized + { + get + { + Vector4 vector4 = this; + vector4.Normalize(); + return vector4; + } + } + /// /// Gets a value indicting whether this vector is zero /// diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index cc7b59730..91743ad73 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -129,6 +129,12 @@ public: FLAXENGINE_API String ToString() const; public: + // Gets a value indicting whether this instance is normalized. + bool IsNormalized() const + { + return Math::Abs((X * X + Y * Y + Z * Z + W * W) - 1.0f) < 1e-4f; + } + // Gets a value indicting whether this vector is zero. bool IsZero() const { @@ -219,6 +225,45 @@ public: return Vector4Base(-X, -Y, -Z, -W); } + /// + /// Calculates a normalized vector that has length equal to 1. + /// + Vector4Base GetNormalized() const + { + Vector4Base result(X, Y, Z, W); + result.Normalize(); + return result; + } + +public: + /// + /// Performs vector normalization (scales vector up to unit length). + /// + void Normalize() + { + const T length = Math::Sqrt(X * X + Y * Y + Z * Z + W * W); + if (length >= ZeroTolerance) + { + const T inv = (T)1.0f / length; + X *= inv; + Y *= inv; + Z *= inv; + W *= inv; + } + } + + /// + /// Performs fast vector normalization (scales vector up to unit length). + /// + void NormalizeFast() + { + const T inv = 1.0f / Math::Sqrt(X * X + Y * Y + Z * Z + W * W); + X *= inv; + Y *= inv; + Z *= inv; + W *= inv; + } + public: Vector4Base operator+(const Vector4Base& b) const { @@ -469,6 +514,41 @@ public: result = Vector4Base(Math::Clamp(v.X, min.X, max.X), Math::Clamp(v.Y, min.Y, max.Y), Math::Clamp(v.Z, min.Z, max.Z), Math::Clamp(v.W, min.W, max.W)); } + // Performs vector normalization (scales vector up to unit length). + static Vector4Base Normalize(const Vector4Base& v) + { + Vector4Base r = v; + const T length = Math::Sqrt(r.X * r.X + r.Y * r.Y + r.Z * r.Z + r.W * r.W); + if (length >= ZeroTolerance) + { + const T inv = (T)1.0f / length; + r.X *= inv; + r.Y *= inv; + r.Z *= inv; + r.W *= inv; + } + return r; + } + + // Performs vector normalization (scales vector up to unit length). This is a faster version that does not perform check for length equal 0 (it assumes that input vector is not empty). + static Vector4Base NormalizeFast(const Vector4Base& v) + { + const T inv = 1.0f / v.Length(); + return Vector4Base(v.X * inv, v.Y * inv, v.Z * inv, v.W * inv); + } + + // Performs vector normalization (scales vector up to unit length). + static FORCE_INLINE void Normalize(const Vector4Base& input, Vector4Base& result) + { + result = Normalize(input); + } + + // Calculates the dot product of two vectors. + FORCE_INLINE static T Dot(const Vector4Base& a, const Vector4Base& b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z + a.W * b.W; + } + // Performs a linear interpolation between two vectors. static void Lerp(const Vector4Base& start, const Vector4Base& end, T amount, Vector4Base& result) { From 36ab08e60df25798a3b0545eb8b6aa97b9a83477 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 18:28:25 +0100 Subject: [PATCH 058/100] Fix build regression on Clang --- Source/Engine/Core/Math/Half.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Engine/Core/Math/Half.h b/Source/Engine/Core/Math/Half.h index ebb2cc951..9ec68a6a0 100644 --- a/Source/Engine/Core/Math/Half.h +++ b/Source/Engine/Core/Math/Half.h @@ -5,6 +5,7 @@ #include "Math.h" #include "Vector2.h" #include "Vector3.h" +#include "Vector4.h" /// /// Half-precision 16 bit floating point number consisting of a sign bit, a 5 bit biased exponent, and a 10 bit mantissa From 3c6838ee3524f1aa02c8140f8e1d9c0c361318a8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 19:13:31 +0100 Subject: [PATCH 059/100] Fix error in reparent action when scene is missing (eg. in prefab editor) --- Source/Editor/Undo/Actions/ParentActorsAction.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Undo/Actions/ParentActorsAction.cs b/Source/Editor/Undo/Actions/ParentActorsAction.cs index 312ce578d..aed85cb3c 100644 --- a/Source/Editor/Undo/Actions/ParentActorsAction.cs +++ b/Source/Editor/Undo/Actions/ParentActorsAction.cs @@ -134,7 +134,8 @@ namespace FlaxEditor.Actions var obj = Object.Find(ref item.ID); if (obj != null) { - scenes.Add(obj.Parent.Scene); + if (obj.Parent != null) + scenes.Add(obj.Parent.Scene); if (obj is Actor actor) actor.SetParent(newParent, _worldPositionsStays, true); else From f3111e855df3b016f36edff63dc3a19dda3167aa Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 19:48:57 +0100 Subject: [PATCH 060/100] Fix showing C++ structures inlined in collection editor when they have a single property/field --- Source/Editor/CustomEditors/Editors/CollectionEditor.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 28593a7f5..02742b512 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -593,11 +593,12 @@ namespace FlaxEditor.CustomEditors.Editors panel.Panel.Offsets = new Margin(7, 7, 0, 0); panel.Panel.BackgroundColor = _background; var elementType = ElementType; + var bindingAttr = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public; bool single = elementType.IsPrimitive || elementType.Equals(new ScriptType(typeof(string))) || elementType.IsEnum || - (elementType.GetFields().Length == 1 && elementType.GetProperties().Length == 0) || - (elementType.GetProperties().Length == 1 && elementType.GetFields().Length == 0) || + (elementType.GetFields(bindingAttr).Length == 1 && elementType.GetProperties(bindingAttr).Length == 0) || + (elementType.GetProperties(bindingAttr).Length == 1 && elementType.GetFields(bindingAttr).Length == 0) || elementType.Equals(new ScriptType(typeof(JsonAsset))) || elementType.Equals(new ScriptType(typeof(SettingsBase))); if (_cachedDropPanels == null) From 15771355cb0f6e64a8ab81d14a8e24f63d7ef81d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 22:09:41 +0100 Subject: [PATCH 061/100] Add `ShouldSerialize` to `ISerializable` to properly handle serialization of custom C++ types in prefabs #3499 --- Source/Engine/Core/Config.h | 2 +- Source/Engine/Core/ISerializable.h | 7 +++ Source/Engine/Serialization/Serialization.h | 4 +- .../Bindings/BindingsGenerator.Cpp.cs | 43 ++++++++++++++++--- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Source/Engine/Core/Config.h b/Source/Engine/Core/Config.h index 810217050..95319b885 100644 --- a/Source/Engine/Core/Config.h +++ b/Source/Engine/Core/Config.h @@ -57,5 +57,5 @@ #define API_PARAM(...) #define API_TYPEDEF(...) #define API_INJECT_CODE(...) -#define API_AUTO_SERIALIZATION(...) public: void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; +#define API_AUTO_SERIALIZATION(...) public: bool ShouldSerialize(const void* otherObj) const override; void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; #define DECLARE_SCRIPTING_TYPE_MINIMAL(type) public: friend class type##Internal; static struct ScriptingTypeInitializer TypeInitializer; diff --git a/Source/Engine/Core/ISerializable.h b/Source/Engine/Core/ISerializable.h index c99051fc0..7500eff8b 100644 --- a/Source/Engine/Core/ISerializable.h +++ b/Source/Engine/Core/ISerializable.h @@ -36,6 +36,13 @@ public: /// virtual ~ISerializable() = default; + /// + /// Compares with other instance to decide whether serialize this instance (eg. any field orp property is modified). Used to skip object serialization if not needed. + /// + /// The instance of the object (always valid) to compare with to decide whether serialize this instance. + /// True if any field or property is modified compared to the other object instance, otherwise false. + virtual bool ShouldSerialize(const void* otherObj) const { return true; } + /// /// Serializes object to the output stream compared to the values of the other object instance (eg. default class object). If other object is null then serialize all properties. /// diff --git a/Source/Engine/Serialization/Serialization.h b/Source/Engine/Serialization/Serialization.h index d3205ad83..9af6d7be1 100644 --- a/Source/Engine/Serialization/Serialization.h +++ b/Source/Engine/Serialization/Serialization.h @@ -415,7 +415,7 @@ namespace Serialization inline bool ShouldSerialize(const ISerializable& v, const void* otherObj) { - return true; + return !otherObj || v.ShouldSerialize(otherObj); } inline void Serialize(ISerializable::SerializeStream& stream, const ISerializable& v, const void* otherObj) { @@ -431,7 +431,7 @@ namespace Serialization template inline typename TEnableIf::Value, bool>::Type ShouldSerialize(const ISerializable& v, const void* otherObj) { - return true; + return !otherObj || v.ShouldSerialize(otherObj); } template inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, const ISerializable& v, const void* otherObj) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 46f0ec245..3bdb16815 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -1892,13 +1892,13 @@ namespace Flax.Build.Bindings CppAutoSerializeProperties.Clear(); CppIncludeFiles.Add("Engine/Serialization/Serialization.h"); + // Serialize contents.AppendLine(); contents.Append($"void {typeNameNative}::Serialize(SerializeStream& stream, const void* otherObj)").AppendLine(); contents.Append('{').AppendLine(); if (baseType != null) contents.Append($" {baseType.FullNameNative}::Serialize(stream, otherObj);").AppendLine(); contents.Append($" SERIALIZE_GET_OTHER_OBJ({typeNameNative});").AppendLine(); - if (classInfo != null) { foreach (var fieldInfo in classInfo.Fields) @@ -1910,7 +1910,6 @@ namespace Flax.Build.Bindings contents.Append($" SERIALIZE{typeHint}({fieldInfo.Name});").AppendLine(); CppAutoSerializeFields.Add(fieldInfo); } - foreach (var propertyInfo in classInfo.Properties) { if (propertyInfo.Getter == null || propertyInfo.Setter == null) @@ -1952,21 +1951,19 @@ namespace Flax.Build.Bindings CppAutoSerializeFields.Add(fieldInfo); } } - contents.Append('}').AppendLine(); + // Deserialize contents.AppendLine(); contents.Append($"void {typeNameNative}::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)").AppendLine(); contents.Append('{').AppendLine(); if (baseType != null) contents.Append($" {baseType.FullNameNative}::Deserialize(stream, modifier);").AppendLine(); - foreach (var fieldInfo in CppAutoSerializeFields) { var typeHint = GenerateCppAutoSerializationDefineType(buildData, contents, moduleInfo, typeInfo, fieldInfo.Type, fieldInfo); contents.Append($" DESERIALIZE{typeHint}({fieldInfo.Name});").AppendLine(); } - foreach (var propertyInfo in CppAutoSerializeProperties) { contents.AppendLine(" {"); @@ -1978,7 +1975,43 @@ namespace Flax.Build.Bindings contents.AppendLine(" }"); contents.AppendLine(" }"); } + contents.Append('}').AppendLine(); + // ShouldSerialize + contents.AppendLine(); + contents.Append($"bool {typeNameNative}::ShouldSerialize(const void* otherObj) const").AppendLine(); + contents.Append('{').AppendLine(); + if (!typeInfo.IsScriptingObject) + { + contents.Append($" SERIALIZE_GET_OTHER_OBJ({typeNameNative});").AppendLine(); + contents.AppendLine(" bool result = false;"); + if (baseType != null) + contents.Append($" result |= {baseType.FullNameNative}::ShouldSerialize(otherObj);").AppendLine(); + foreach (var fieldInfo in CppAutoSerializeFields) + { + contents.Append($" result |= Serialization::ShouldSerialize({fieldInfo.Name}, &other->{fieldInfo.Name});").AppendLine(); + } + foreach (var propertyInfo in CppAutoSerializeProperties) + { + contents.Append(" {"); + contents.Append(" const auto"); + if (propertyInfo.Getter.ReturnType.IsConstRef) + contents.Append('&'); + contents.Append($" value = {propertyInfo.Getter.Name}();"); + contents.Append(" const auto"); + if (propertyInfo.Getter.ReturnType.IsConstRef) + contents.Append('&'); + contents.Append($" otherValue = other->{propertyInfo.Getter.Name}();"); + contents.Append(" result |= Serialization::ShouldSerialize(value, &otherValue);").AppendLine(); + contents.Append('}').AppendLine(); + } + contents.AppendLine(" return result;"); + } + else + { + // Not needed to generate + contents.AppendLine(" return true;"); + } contents.Append('}').AppendLine(); } From 39a2bc2535b6944c2e163e721355fb589ed9a2bc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 22:11:15 +0100 Subject: [PATCH 062/100] Fix build regression on Clang --- Source/Engine/Graphics/Models/MeshBase.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Graphics/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp index 91ed892ea..eadbfcba9 100644 --- a/Source/Engine/Graphics/Models/MeshBase.cpp +++ b/Source/Engine/Graphics/Models/MeshBase.cpp @@ -441,6 +441,9 @@ bool MeshBase::Init(uint32 vertices, uint32 triangles, const ArrayFindElement(VertexElement::Types::Position); + positionsElement = vbLayout[0]->FindElement(VertexElement::Types::Position); if (use16BitIndexBuffer) _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint16*)ibData, vertexBuffer0->GetStride(), positionsElement.Format); else From 9ac19cbd2f4bf4d15368ae4043c57e4ae8a09abc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 22:58:09 +0100 Subject: [PATCH 063/100] Fix build regression on Clang --- Source/Engine/Graphics/Models/CollisionProxy.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Graphics/Models/CollisionProxy.h b/Source/Engine/Graphics/Models/CollisionProxy.h index eba17cf4e..2ecdce756 100644 --- a/Source/Engine/Graphics/Models/CollisionProxy.h +++ b/Source/Engine/Graphics/Models/CollisionProxy.h @@ -6,7 +6,7 @@ #include "Engine/Core/Math/Transform.h" #include "Engine/Core/Math/Ray.h" #include "Engine/Core/Math/CollisionsHelper.h" -#include "Engine/Core/Math/Packed.h" +#include "Engine/Core/Math/Half.h" #include "Engine/Core/Collections/Array.h" #include "Engine/Graphics/PixelFormat.h" @@ -58,7 +58,7 @@ public: else if (positionsFormat == PixelFormat::R16G16B16A16_Float) { LOOP_BEGIN() -#define GET_POS(idx) (Float3)*(const Half4*)((const byte*)positions + positionsStride * idx) +#define GET_POS(idx) ((const Half4*)((const byte*)positions + positionsStride * idx))->ToFloat3() Triangles.Add({ GET_POS(i0), GET_POS(i1), GET_POS(i2) }); #undef GET_POS LOOP_END() From e84b5410ec95f0630782ac3626d2511fbea26185 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 22:59:35 +0100 Subject: [PATCH 064/100] Fix C# Json serialization to use proper value comparison for structures with Scene Object references #3499 --- Source/Engine/Level/MeshReference.cs | 2 +- Source/Engine/Level/Prefabs/Prefab.Apply.cpp | 4 +-- Source/Engine/Serialization/JsonSerializer.cs | 34 ++++++++++++++++--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Level/MeshReference.cs b/Source/Engine/Level/MeshReference.cs index 14ef0c72b..cb87e9769 100644 --- a/Source/Engine/Level/MeshReference.cs +++ b/Source/Engine/Level/MeshReference.cs @@ -13,7 +13,7 @@ namespace FlaxEngine public bool ValueEquals(object other) { var o = (MeshReference)other; - return JsonSerializer.ValueEquals(Actor, o.Actor) && + return JsonSerializer.SceneObjectEquals(Actor, o.Actor) && LODIndex == o.LODIndex && MeshIndex == o.MeshIndex; } diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index 0bed02f0b..378b706ed 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -227,9 +227,9 @@ public: void PrefabInstanceData::CollectPrefabInstances(PrefabInstancesData& prefabInstancesData, const Guid& prefabId, Actor* defaultInstance, Actor* targetActor) { ScopeLock lock(PrefabManager::PrefabsReferencesLocker); - if (PrefabManager::PrefabsReferences.ContainsKey(prefabId)) + if (auto instancesPtr = PrefabManager::PrefabsReferences.TryGet(prefabId)) { - auto& instances = PrefabManager::PrefabsReferences[prefabId]; + auto& instances = *instancesPtr; int32 usedCount = 0; for (int32 instanceIndex = 0; instanceIndex < instances.Count(); instanceIndex++) { diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index 4321ffa36..86e4ead8b 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -270,8 +270,8 @@ namespace FlaxEngine.Json // Special case when saving reference to prefab object and the objects are different but the point to the same prefab object // In that case, skip saving reference as it's defined in prefab (will be populated via IdsMapping during deserialization) - if (objA is SceneObject sceneA && objB is SceneObject sceneB && sceneA && sceneB && sceneA.HasPrefabLink && sceneB.HasPrefabLink) - return sceneA.PrefabObjectID == sceneB.PrefabObjectID; + if (objA is SceneObject sceneObjA && objB is SceneObject sceneObjB && sceneObjA && sceneObjB && sceneObjA.HasPrefabLink && sceneObjB.HasPrefabLink) + return sceneObjA.PrefabObjectID == sceneObjB.PrefabObjectID; // Comparing an Int32 and Int64 both of the same value returns false, make types the same then compare if (objA.GetType() != objB.GetType()) @@ -286,7 +286,6 @@ namespace FlaxEngine.Json type == typeof(Int32) || type == typeof(UInt32) || type == typeof(Int64) || - type == typeof(SByte) || type == typeof(UInt64); } if (IsInteger(objA) && IsInteger(objB)) @@ -301,6 +300,12 @@ namespace FlaxEngine.Json { if (aList.Count != bList.Count) return false; + for (int i = 0; i < aList.Count; i++) + { + if (!ValueEquals(aList[i], bList[i])) + return false; + } + return true; } if (objA is IEnumerable aEnumerable && objB is IEnumerable bEnumerable) { @@ -316,8 +321,29 @@ namespace FlaxEngine.Json return !bEnumerator.MoveNext(); } - if (objA is ICustomValueEquals customValueEquals && objA.GetType() == objB.GetType()) + // Custom comparer + if (objA is ICustomValueEquals customValueEquals) return customValueEquals.ValueEquals(objB); + + // If type contains SceneObject references then it needs to use custom comparision that handles prefab links (see SceneObjectEquals) + if (objA.GetType().IsStructure()) + { + var contract = Settings.ContractResolver.ResolveContract(objA.GetType()); + if (contract is JsonObjectContract objContract) + { + foreach (var property in objContract.Properties) + { + var valueProvider = property.ValueProvider; + var propA = valueProvider.GetValue(objA); + var propB = valueProvider.GetValue(objB); + if (!ValueEquals(propA, propB)) + return false; + } + return true; + } + } + + // Generic fallback return objA.Equals(objB); #endif } From 9a44902949e845ebd5de362aa3cfe8aa7ae23361 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 23:00:36 +0100 Subject: [PATCH 065/100] Optimize e84b5410ec95f0630782ac3626d2511fbea26185 to use faster code path on common math types #3499 --- Source/Engine/Core/Math/Color.cs | 9 ++++++++- Source/Engine/Core/Math/Double2.cs | 9 ++++++++- Source/Engine/Core/Math/Double3.cs | 9 ++++++++- Source/Engine/Core/Math/Double4.cs | 9 ++++++++- Source/Engine/Core/Math/Float2.cs | 9 ++++++++- Source/Engine/Core/Math/Float3.cs | 9 ++++++++- Source/Engine/Core/Math/Float4.cs | 9 ++++++++- Source/Engine/Core/Math/Int2.cs | 9 ++++++++- Source/Engine/Core/Math/Int3.cs | 9 ++++++++- Source/Engine/Core/Math/Int4.cs | 9 ++++++++- Source/Engine/Core/Math/Quaternion.cs | 9 ++++++++- Source/Engine/Core/Math/Rectangle.cs | 9 ++++++++- Source/Engine/Core/Math/Transform.cs | 9 ++++++++- Source/Engine/Core/Math/Vector2.cs | 9 ++++++++- Source/Engine/Core/Math/Vector3.cs | 9 ++++++++- Source/Engine/Core/Math/Vector4.cs | 9 ++++++++- 16 files changed, 128 insertions(+), 16 deletions(-) diff --git a/Source/Engine/Core/Math/Color.cs b/Source/Engine/Core/Math/Color.cs index 2d779dcfa..be4a12789 100644 --- a/Source/Engine/Core/Math/Color.cs +++ b/Source/Engine/Core/Math/Color.cs @@ -11,7 +11,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.ColorConverter))] #endif - partial struct Color + partial struct Color : Json.ICustomValueEquals { /// /// The size of the type, in bytes. @@ -196,6 +196,13 @@ namespace FlaxEngine A = values[3]; } + /// + public bool ValueEquals(object other) + { + var o = (Color)other; + return Equals(ref o); + } + /// public override bool Equals(object value) { diff --git a/Source/Engine/Core/Math/Double2.cs b/Source/Engine/Core/Math/Double2.cs index 9594b22cb..51fcf32d5 100644 --- a/Source/Engine/Core/Math/Double2.cs +++ b/Source/Engine/Core/Math/Double2.cs @@ -65,7 +65,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Double2Converter))] #endif - partial struct Double2 : IEquatable, IFormattable + partial struct Double2 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2}"; @@ -1574,6 +1574,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Double2)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Double3.cs b/Source/Engine/Core/Math/Double3.cs index cb26cf071..4dccc1fb3 100644 --- a/Source/Engine/Core/Math/Double3.cs +++ b/Source/Engine/Core/Math/Double3.cs @@ -66,7 +66,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Double3Converter))] #endif - partial struct Double3 : IEquatable, IFormattable + partial struct Double3 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2}"; @@ -1872,6 +1872,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Double3)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Double4.cs b/Source/Engine/Core/Math/Double4.cs index 70d27cb28..bf176e6ff 100644 --- a/Source/Engine/Core/Math/Double4.cs +++ b/Source/Engine/Core/Math/Double4.cs @@ -66,7 +66,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Double4Converter))] #endif - partial struct Double4 : IEquatable, IFormattable + partial struct Double4 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}"; @@ -1372,6 +1372,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Double4)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Float2.cs b/Source/Engine/Core/Math/Float2.cs index 1b70dd0a6..5bb81ec3a 100644 --- a/Source/Engine/Core/Math/Float2.cs +++ b/Source/Engine/Core/Math/Float2.cs @@ -60,7 +60,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Float2Converter))] #endif - partial struct Float2 : IEquatable, IFormattable + partial struct Float2 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2}"; @@ -1650,6 +1650,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Float2)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Float3.cs b/Source/Engine/Core/Math/Float3.cs index 5e8dceed6..50554345b 100644 --- a/Source/Engine/Core/Math/Float3.cs +++ b/Source/Engine/Core/Math/Float3.cs @@ -60,7 +60,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Float3Converter))] #endif - partial struct Float3 : IEquatable, IFormattable + partial struct Float3 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2}"; @@ -1904,6 +1904,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Float3)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Float4.cs b/Source/Engine/Core/Math/Float4.cs index b6eb6dd9e..26abf4b2e 100644 --- a/Source/Engine/Core/Math/Float4.cs +++ b/Source/Engine/Core/Math/Float4.cs @@ -60,7 +60,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Float4Converter))] #endif - partial struct Float4 : IEquatable, IFormattable + partial struct Float4 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}"; @@ -1412,6 +1412,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Float4)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Int2.cs b/Source/Engine/Core/Math/Int2.cs index 4a4107252..32a273307 100644 --- a/Source/Engine/Core/Math/Int2.cs +++ b/Source/Engine/Core/Math/Int2.cs @@ -14,7 +14,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Int2Converter))] #endif - partial struct Int2 : IEquatable, IFormattable + partial struct Int2 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0} Y:{1}"; @@ -940,6 +940,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Int2)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Int3.cs b/Source/Engine/Core/Math/Int3.cs index 78e4600c3..81bb8026e 100644 --- a/Source/Engine/Core/Math/Int3.cs +++ b/Source/Engine/Core/Math/Int3.cs @@ -14,7 +14,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Int3Converter))] #endif - partial struct Int3 : IEquatable, IFormattable + partial struct Int3 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0} Y:{1} Z:{2}"; @@ -1023,6 +1023,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Int3)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Int4.cs b/Source/Engine/Core/Math/Int4.cs index e180ccb31..bbccadab4 100644 --- a/Source/Engine/Core/Math/Int4.cs +++ b/Source/Engine/Core/Math/Int4.cs @@ -14,7 +14,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Int4Converter))] #endif - partial struct Int4 : IEquatable, IFormattable + partial struct Int4 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0} Y:{1} Z:{2} W:{3}"; @@ -881,6 +881,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Int4)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Quaternion.cs b/Source/Engine/Core/Math/Quaternion.cs index d89b71488..cf51fac50 100644 --- a/Source/Engine/Core/Math/Quaternion.cs +++ b/Source/Engine/Core/Math/Quaternion.cs @@ -60,7 +60,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.QuaternionConverter))] #endif - partial struct Quaternion : IEquatable, IFormattable + partial struct Quaternion : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}"; @@ -1681,6 +1681,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Quaternion)other; + return Equals(ref o); + } + /// /// Tests whether one quaternion is near another quaternion. /// diff --git a/Source/Engine/Core/Math/Rectangle.cs b/Source/Engine/Core/Math/Rectangle.cs index 81c689d48..8e3c2b6c4 100644 --- a/Source/Engine/Core/Math/Rectangle.cs +++ b/Source/Engine/Core/Math/Rectangle.cs @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; namespace FlaxEngine { - partial struct Rectangle : IEquatable + partial struct Rectangle : IEquatable, Json.ICustomValueEquals { /// /// A which represents an empty space. @@ -523,6 +523,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Rectangle)other; + return Equals(ref o); + } + /// public override string ToString() { diff --git a/Source/Engine/Core/Math/Transform.cs b/Source/Engine/Core/Math/Transform.cs index fc16a501b..90c0c36c9 100644 --- a/Source/Engine/Core/Math/Transform.cs +++ b/Source/Engine/Core/Math/Transform.cs @@ -16,7 +16,7 @@ using System.Runtime.InteropServices; namespace FlaxEngine { [Serializable] - partial struct Transform : IEquatable, IFormattable + partial struct Transform : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "Translation:{0} Orientation:{1} Scale:{2}"; @@ -673,6 +673,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Transform)other; + return Equals(ref o); + } + /// /// Tests whether one transform is near another transform. /// diff --git a/Source/Engine/Core/Math/Vector2.cs b/Source/Engine/Core/Math/Vector2.cs index 8e1599513..3f63c4e9c 100644 --- a/Source/Engine/Core/Math/Vector2.cs +++ b/Source/Engine/Core/Math/Vector2.cs @@ -73,7 +73,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Vector2Converter))] #endif - public unsafe partial struct Vector2 : IEquatable, IFormattable + public unsafe partial struct Vector2 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2}"; @@ -1774,6 +1774,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Vector2)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Vector3.cs b/Source/Engine/Core/Math/Vector3.cs index 5e01a7a6c..388317810 100644 --- a/Source/Engine/Core/Math/Vector3.cs +++ b/Source/Engine/Core/Math/Vector3.cs @@ -73,7 +73,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Vector3Converter))] #endif - public unsafe partial struct Vector3 : IEquatable, IFormattable + public unsafe partial struct Vector3 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2}"; @@ -2133,6 +2133,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Vector3)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Vector4.cs b/Source/Engine/Core/Math/Vector4.cs index 909e35157..776bbd3dc 100644 --- a/Source/Engine/Core/Math/Vector4.cs +++ b/Source/Engine/Core/Math/Vector4.cs @@ -72,7 +72,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Vector4Converter))] #endif - public partial struct Vector4 : IEquatable, IFormattable + public partial struct Vector4 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}"; @@ -1499,6 +1499,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Vector4)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// From 4d447b7544f343dd435149b999432abfaec8923d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 23:33:09 +0100 Subject: [PATCH 066/100] Fix very rare race condition when waiting for an asset to load --- Source/Engine/Content/Asset.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 9fd4cee9c..7a35d3b28 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -487,6 +487,8 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const const auto loadingTask = (ContentLoadTask*)Platform::AtomicRead(&_loadingTask); if (loadingTask == nullptr) { + if (IsLoaded()) + return false; LOG(Warning, "WaitForLoaded asset \'{0}\' failed. No loading task attached and asset is not loaded.", ToString()); return true; } From 780e78f056f6abb0ddec3a52b7758a19af487e2a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 3 Feb 2026 23:33:24 +0100 Subject: [PATCH 067/100] Another compilation fix attempt --- Source/Engine/Serialization/JsonSerializer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index 86e4ead8b..c8d00567a 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -326,9 +326,10 @@ namespace FlaxEngine.Json return customValueEquals.ValueEquals(objB); // If type contains SceneObject references then it needs to use custom comparision that handles prefab links (see SceneObjectEquals) - if (objA.GetType().IsStructure()) + var typeA = objA.GetType(); + if (typeA.IsValueType && !typeA.IsEnum && !typeA.IsPrimitive) { - var contract = Settings.ContractResolver.ResolveContract(objA.GetType()); + var contract = Settings.ContractResolver.ResolveContract(typeA); if (contract is JsonObjectContract objContract) { foreach (var property in objContract.Properties) From f733611213ef77b1e3180ff6b982f5f0e20cd14e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 4 Feb 2026 21:48:02 +0100 Subject: [PATCH 068/100] Fix various issues with audio and video playback #3716 --- .../Engine/Audio/OpenAL/AudioBackendOAL.cpp | 2 +- .../Audio/XAudio2/AudioBackendXAudio2.cpp | 4 ++-- Source/Engine/Video/MF/VideoBackendMF.cpp | 22 +++++++++---------- Source/Engine/Video/VideoPlayer.cpp | 4 +++- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index 86e828028..2891cec02 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -400,7 +400,7 @@ void AudioBackendOAL::Source_IsLoopingChanged(uint32 sourceID, bool loop) void AudioBackendOAL::Source_SpatialSetupChanged(uint32 sourceID, bool spatial, float attenuation, float minDistance, float doppler) { ALC::Locker.Lock(); - const bool pan = ALC::SourcesData[sourceID].Spatial; + const float pan = ALC::SourcesData[sourceID].Pan; ALC::Locker.Unlock(); if (spatial) { diff --git a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp index 9600a93fb..6cba1302b 100644 --- a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp +++ b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp @@ -672,7 +672,7 @@ bool AudioBackendXAudio2::Base_Init() HRESULT hr = XAudio2Create(&XAudio2::Instance, 0, XAUDIO2_DEFAULT_PROCESSOR); if (FAILED(hr)) { - LOG(Error, "Failed to initalize XAudio2. Error: 0x{0:x}", hr); + LOG(Error, "Failed to initialize XAudio2. Error: 0x{0:x}", hr); return true; } XAudio2::Instance->RegisterForCallbacks(&XAudio2::Callback); @@ -681,7 +681,7 @@ bool AudioBackendXAudio2::Base_Init() hr = XAudio2::Instance->CreateMasteringVoice(&XAudio2::MasteringVoice); if (FAILED(hr)) { - LOG(Error, "Failed to initalize XAudio2 mastering voice. Error: 0x{0:x}", hr); + LOG(Error, "Failed to initialize XAudio2 mastering voice. Error: 0x{0:x}", hr); return true; } XAUDIO2_VOICE_DETAILS details; diff --git a/Source/Engine/Video/MF/VideoBackendMF.cpp b/Source/Engine/Video/MF/VideoBackendMF.cpp index df24f5eed..3e738d09f 100644 --- a/Source/Engine/Video/MF/VideoBackendMF.cpp +++ b/Source/Engine/Video/MF/VideoBackendMF.cpp @@ -47,6 +47,8 @@ struct VideoPlayerMF namespace MF { Array Players; + TimeSpan UpdateDeltaTime; + double UpdateTime; bool Configure(VideoBackendPlayer& player, VideoPlayerMF& playerMF, DWORD streamIndex) { @@ -395,13 +397,6 @@ namespace MF if (!playerMF.Playing && !playerMF.Seek) return; - bool useTimeScale = true; -#if USE_EDITOR - if (!Editor::IsPlayMode) - useTimeScale = false; -#endif - TimeSpan dt = useTimeScale ? Time::Update.DeltaTime : Time::Update.UnscaledDeltaTime; - // Update playback time if (playerMF.FirstFrame) { @@ -410,9 +405,9 @@ namespace MF } else if (playerMF.Playing) { - playerMF.Time += dt; + playerMF.Time += UpdateDeltaTime; } - if (playerMF.Time > player.Duration) + if (playerMF.Time > player.Duration && player.Duration.Ticks != 0) { if (playerMF.Loop) { @@ -452,7 +447,7 @@ namespace MF } // Update streams - if (ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_VIDEO_STREAM, dt)) + if (ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_VIDEO_STREAM, UpdateDeltaTime)) { // Failed to pick a valid sample so try again with seeking playerMF.Seek = 1; @@ -464,7 +459,7 @@ namespace MF } } if (player.AudioInfo.BitDepth != 0) - ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_AUDIO_STREAM, dt); + ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_AUDIO_STREAM, UpdateDeltaTime); player.Tick(); } @@ -610,12 +605,17 @@ bool VideoBackendMF::Base_Init() VIDEO_API_MF_ERROR(MFStartup, hr); return true; } + MF::UpdateTime = Platform::GetTimeSeconds(); return false; } void VideoBackendMF::Base_Update(TaskGraph* graph) { + double time = Platform::GetTimeSeconds(); + MF::UpdateDeltaTime = TimeSpan::FromSeconds(time - MF::UpdateTime); + MF::UpdateTime = time; + // Schedule work to update all videos in async Function job; job.Bind(MF::UpdatePlayer); diff --git a/Source/Engine/Video/VideoPlayer.cpp b/Source/Engine/Video/VideoPlayer.cpp index 59f8d2438..363779038 100644 --- a/Source/Engine/Video/VideoPlayer.cpp +++ b/Source/Engine/Video/VideoPlayer.cpp @@ -133,7 +133,9 @@ void VideoPlayer::SetTime(float time) if (_state == States::Stopped || _player.Backend == nullptr) return; TimeSpan timeSpan = TimeSpan::FromSeconds(time); - timeSpan.Ticks = Math::Clamp(timeSpan.Ticks, 0, _player.Duration.Ticks); + timeSpan.Ticks = Math::Max(timeSpan.Ticks, 0); + if (_player.Duration.Ticks > 0) + timeSpan.Ticks = Math::Min(timeSpan.Ticks, _player.Duration.Ticks); _player.Backend->Player_Seek(_player, timeSpan); } From 7e9ee0610a7f3886889c216b3d02abd03cc62dc4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 4 Feb 2026 23:43:06 +0100 Subject: [PATCH 069/100] Fix missing audio on OpenAL when changing active device #3621 --- Source/Engine/Audio/AudioClip.cpp | 4 +- Source/Engine/Audio/AudioClip.h | 1 + .../Engine/Audio/OpenAL/AudioBackendOAL.cpp | 78 +++++++++++++++++-- Source/Engine/Level/Level.h | 13 ++++ Source/Engine/Video/VideoPlayer.h | 1 + 5 files changed, 86 insertions(+), 11 deletions(-) diff --git a/Source/Engine/Audio/AudioClip.cpp b/Source/Engine/Audio/AudioClip.cpp index 336ddb7bd..4e258e81a 100644 --- a/Source/Engine/Audio/AudioClip.cpp +++ b/Source/Engine/Audio/AudioClip.cpp @@ -132,9 +132,7 @@ int32 AudioClip::GetFirstBufferIndex(float time, float& offset) const if (_buffersStartTimes[i + 1] > time) { offset = time - _buffersStartTimes[i]; -#if BUILD_DEBUG - ASSERT(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f); -#endif + ASSERT_LOW_LAYER(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f); return i; } } diff --git a/Source/Engine/Audio/AudioClip.h b/Source/Engine/Audio/AudioClip.h index d35bd18fc..ac30b4c85 100644 --- a/Source/Engine/Audio/AudioClip.h +++ b/Source/Engine/Audio/AudioClip.h @@ -18,6 +18,7 @@ class AudioSource; API_CLASS(NoSpawn) class FLAXENGINE_API AudioClip : public BinaryAsset, public StreamableResource { DECLARE_BINARY_ASSET_HEADER(AudioClip, 2); + friend class AudioBackendOAL; public: /// diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index 2891cec02..fb883f952 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -14,6 +14,9 @@ #include "Engine/Audio/AudioListener.h" #include "Engine/Audio/AudioSource.h" #include "Engine/Audio/AudioSettings.h" +#include "Engine/Content/Content.h" +#include "Engine/Level/Level.h" +#include "Engine/Video/VideoPlayer.h" // Include OpenAL library // Source: https://github.com/kcat/openal-soft @@ -73,6 +76,7 @@ namespace ALC ALCdevice* Device = nullptr; ALCcontext* Context = nullptr; AudioBackend::FeatureFlags Features = AudioBackend::FeatureFlags::None; + bool Inited = false; CriticalSection Locker; Dictionary SourcesData; @@ -164,12 +168,9 @@ namespace ALC float Time; }; - void RebuildContext(const Array& states) + void RebuildContext() { - LOG(Info, "Rebuilding audio contexts"); - ClearContext(); - if (Device == nullptr) return; @@ -182,10 +183,16 @@ namespace ALC Context = alcCreateContext(Device, attrList); alcMakeContextCurrent(Context); - + } + + void RebuildListeners() + { for (AudioListener* listener : Audio::Listeners) Listener::Rebuild(listener); - + } + + void RebuildSources(const Array& states) + { for (int32 i = 0; i < states.Count(); i++) { AudioSource* source = Audio::Sources[i]; @@ -205,6 +212,13 @@ namespace ALC } } + void RebuildContext(const Array& states) + { + RebuildContext(); + RebuildListeners(); + RebuildSources(states); + } + void RebuildContext(bool isChangingDevice) { Array states; @@ -629,6 +643,7 @@ AudioBackend::FeatureFlags AudioBackendOAL::Base_Features() void AudioBackendOAL::Base_OnActiveDeviceChanged() { + PROFILE_CPU(); PROFILE_MEM(Audio); // Cleanup @@ -659,9 +674,55 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() LOG(Fatal, "Failed to open OpenAL device ({0}).", String(name)); return; } + if (ALC::Inited) + LOG(Info, "Changed audio device to: {}", String(Audio::GetActiveDevice()->Name)); - // Setup - ALC::RebuildContext(states); + // Rebuild context + ALC::RebuildContext(); + if (ALC::Inited) + { + // Reload all audio clips to recreate their buffers + for (Asset* asset : Content::GetAssets()) + { + if (auto* audioClip = ScriptingObject::Cast(asset)) + { + ScopeLock lock(audioClip->Locker); + + // Clear old buffer IDs + for (uint32& bufferID : audioClip->Buffers) + bufferID = 0; + + if (audioClip->IsStreamable()) + { + // Let the streaming recreate missing buffers + audioClip->RequestStreamingUpdate(); + } + else + { + // Reload audio clip + auto assetLock = audioClip->Storage->Lock(); + audioClip->LoadChunk(0); + audioClip->Buffers[0] = AudioBackend::Buffer::Create(); + audioClip->WriteBuffer(0); + + } + } + } + + // Reload all videos to recreate their buffers + for (VideoPlayer* videoPlayer : Level::GetActors(true)) + { + VideoBackendPlayer& player = videoPlayer->_player; + + // Clear audio state + for (uint32& bufferID : player.AudioBuffers) + bufferID = 0; + player.NextAudioBuffer = 0; + player.AudioSource = 0; + } + } + ALC::RebuildListeners(); + ALC::RebuildSources(states); } void AudioBackendOAL::Base_SetDopplerFactor(float value) @@ -782,6 +843,7 @@ bool AudioBackendOAL::Base_Init() if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize")) ALC::Features = EnumAddFlags(ALC::Features, FeatureFlags::SpatialMultiChannel); #endif + ALC::Inited = true; // Log service info LOG(Info, "{0} ({1})", String(alGetString(AL_RENDERER)), String(alGetString(AL_VERSION))); diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h index 110b9ac61..e3d43b7c5 100644 --- a/Source/Engine/Level/Level.h +++ b/Source/Engine/Level/Level.h @@ -473,6 +473,19 @@ public: /// Found actors list. API_FUNCTION() static Array GetActors(API_PARAM(Attributes="TypeReference(typeof(Actor))") const MClass* type, bool activeOnly = false); + /// + /// Finds all the actors of the given type in all the loaded scenes. + /// + /// Type of the object. + /// Finds only active actors. + /// Found actors list. + template + static Array GetActors(bool activeOnly = false) + { + Array actors = GetActors(T::GetStaticClass(), activeOnly); + return *(Array*)&actors; + } + /// /// Finds all the scripts of the given type in an actor or all the loaded scenes. /// diff --git a/Source/Engine/Video/VideoPlayer.h b/Source/Engine/Video/VideoPlayer.h index e1b7e877d..4447337ea 100644 --- a/Source/Engine/Video/VideoPlayer.h +++ b/Source/Engine/Video/VideoPlayer.h @@ -15,6 +15,7 @@ class FLAXENGINE_API VideoPlayer : public Actor { DECLARE_SCENE_OBJECT(VideoPlayer); API_AUTO_SERIALIZATION(); + friend class AudioBackendOAL; public: /// From 66894b71fa837f11c739246aaac53b7a896c77a4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 5 Feb 2026 12:37:03 +0100 Subject: [PATCH 070/100] Add utility to get loaded assets --- .../Engine/Audio/OpenAL/AudioBackendOAL.cpp | 40 +++++++++---------- Source/Engine/Content/Content.cpp | 13 ++++++ Source/Engine/Content/Content.h | 21 +++++++++- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index fb883f952..715ede74f 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -682,30 +682,28 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() if (ALC::Inited) { // Reload all audio clips to recreate their buffers - for (Asset* asset : Content::GetAssets()) + for (AudioClip* audioClip : Content::GetAssets()) { - if (auto* audioClip = ScriptingObject::Cast(asset)) + audioClip->WaitForLoaded(); + ScopeLock lock(audioClip->Locker); + + // Clear old buffer IDs + for (uint32& bufferID : audioClip->Buffers) + bufferID = 0; + + if (audioClip->IsStreamable()) { - ScopeLock lock(audioClip->Locker); + // Let the streaming recreate missing buffers + audioClip->RequestStreamingUpdate(); + } + else + { + // Reload audio clip + auto assetLock = audioClip->Storage->Lock(); + audioClip->LoadChunk(0); + audioClip->Buffers[0] = AudioBackend::Buffer::Create(); + audioClip->WriteBuffer(0); - // Clear old buffer IDs - for (uint32& bufferID : audioClip->Buffers) - bufferID = 0; - - if (audioClip->IsStreamable()) - { - // Let the streaming recreate missing buffers - audioClip->RequestStreamingUpdate(); - } - else - { - // Reload audio clip - auto assetLock = audioClip->Storage->Lock(); - audioClip->LoadChunk(0); - audioClip->Buffers[0] = AudioBackend::Buffer::Create(); - audioClip->WriteBuffer(0); - - } } } diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index 4a9ea500b..59d2dfffa 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -684,6 +684,19 @@ Array Content::GetAssets() return assets; } +Array Content::GetAssets(const MClass* type) +{ + Array assets; + AssetsLocker.Lock(); + for (auto& e : Assets) + { + if (e.Value->Is(type)) + assets.Add(e.Value); + } + AssetsLocker.Unlock(); + return assets; +} + const Dictionary& Content::GetAssetsRaw() { AssetsLocker.Lock(); diff --git a/Source/Engine/Content/Content.h b/Source/Engine/Content/Content.h index f6dcf59f0..15da20a9c 100644 --- a/Source/Engine/Content/Content.h +++ b/Source/Engine/Content/Content.h @@ -122,7 +122,26 @@ public: /// Gets the assets (loaded or during load). /// /// The collection of assets. - static Array GetAssets(); + API_FUNCTION() static Array GetAssets(); + + /// + /// Gets the assets (loaded or during load). + /// + /// Type of the assets to search for. Includes any assets derived from the type. + /// Found actors list. + API_FUNCTION() static Array GetAssets(API_PARAM(Attributes="TypeReference(typeof(Actor))") const MClass* type); + + /// + /// Gets the assets (loaded or during load). + /// + /// Type of the object. + /// Found actors list. + template + static Array GetAssets() + { + Array assets = GetAssets(T::GetStaticClass()); + return *(Array*) & assets; + } /// /// Gets the raw dictionary of assets (loaded or during load). From f57df83d26f2ece2359962bdf58616fa6d229a2b Mon Sep 17 00:00:00 2001 From: Phantom Date: Thu, 5 Feb 2026 12:43:29 +0100 Subject: [PATCH 071/100] Add Slerp methods on Vectors #3682 --- Source/Engine/Core/Math/Vector2.cs | 27 +++++++++++++++++++++++++++ Source/Engine/Core/Math/Vector2.h | 18 ++++++++++++++++++ Source/Engine/Core/Math/Vector3.cs | 27 +++++++++++++++++++++++++++ Source/Engine/Core/Math/Vector3.h | 18 ++++++++++++++++++ Source/Engine/Core/Math/Vector4.cs | 27 +++++++++++++++++++++++++++ Source/Engine/Core/Math/Vector4.h | 18 ++++++++++++++++++ 6 files changed, 135 insertions(+) diff --git a/Source/Engine/Core/Math/Vector2.cs b/Source/Engine/Core/Math/Vector2.cs index 3f63c4e9c..77c52035d 100644 --- a/Source/Engine/Core/Math/Vector2.cs +++ b/Source/Engine/Core/Math/Vector2.cs @@ -954,6 +954,33 @@ namespace FlaxEngine return result; } + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + /// >When the method completes, contains the linear interpolation of the two vectors. + public static void Slerp(ref Vector2 start, ref Vector2 end, float amount, out Vector2 result) + { + var dot = Mathr.Clamp(Dot(start, end), -1.0f, 1.0f); + var theta = Mathr.Acos(dot) * amount; + Vector2 relativeVector = (end - start * dot).Normalized; + result = ((start * Mathr.Cos(theta)) + (relativeVector * Mathr.Sin(theta))); + } + + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + public static Vector2 Slerp(Vector2 start, Vector2 end, float amount) + { + Slerp(ref start, ref end, amount, out Vector2 result); + return result; + } + /// /// Performs a gradual change of a vector towards a specified target over time /// diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h index 34bd1a59c..cee013840 100644 --- a/Source/Engine/Core/Math/Vector2.h +++ b/Source/Engine/Core/Math/Vector2.h @@ -558,6 +558,24 @@ public: return result; } + // Performs a spherical linear interpolation between two vectors. + static void Slerp(const Vector2Base& start, const Vector2Base& end, T amount, Vector2Base& result) + { + T dot = Math::Clamp(Dot(start, end), -1.0f, 1.0f); + T theta = Math::Acos(dot) * amount; + Vector2Base relativeVector = end - start * dot; + relativeVector.Normalize(); + result = ((start * Math::Cos(theta)) + (relativeVector * Math::Sin(theta))); + } + + // Performs a spherical linear interpolation between two vectors. + static Vector2Base Slerp(const Vector2Base& start, const Vector2Base& end, T amount) + { + Vector2Base result; + Slerp(start, end, amount, result); + return result; + } + public: /// /// Calculates the area of the triangle. diff --git a/Source/Engine/Core/Math/Vector3.cs b/Source/Engine/Core/Math/Vector3.cs index 388317810..ce7492e55 100644 --- a/Source/Engine/Core/Math/Vector3.cs +++ b/Source/Engine/Core/Math/Vector3.cs @@ -1043,6 +1043,33 @@ namespace FlaxEngine return result; } + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + /// When the method completes, contains the linear interpolation of the two vectors. + public static void Slerp(ref Vector3 start, ref Vector3 end, float amount, out Vector3 result) + { + var dot = Mathr.Clamp(Dot(start, end), -1.0f, 1.0f); + var theta = Mathr.Acos(dot) * amount; + Vector3 relativeVector = (end - start * dot).Normalized; + result = ((start * Mathr.Cos(theta)) + (relativeVector * Mathr.Sin(theta))); + } + + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + public static Vector3 Slerp(Vector3 start, Vector3 end, float amount) + { + Slerp(ref start, ref end, amount, out var result); + return result; + } + /// /// Performs a gradual change of a vector towards a specified target over time /// diff --git a/Source/Engine/Core/Math/Vector3.h b/Source/Engine/Core/Math/Vector3.h index a19253b00..bf737877b 100644 --- a/Source/Engine/Core/Math/Vector3.h +++ b/Source/Engine/Core/Math/Vector3.h @@ -686,6 +686,24 @@ public: return result; } + // Performs a spherical linear interpolation between two vectors. + static void Slerp(const Vector3Base& start, const Vector3Base& end, T amount, Vector3Base& result) + { + T dot = Math::Clamp(Dot(start, end), -1.0f, 1.0f); + T theta = Math::Acos(dot) * amount; + Vector3Base relativeVector = end - start * dot; + relativeVector.Normalize(); + result = ((start * Math::Cos(theta)) + (relativeVector * Math::Sin(theta))); + } + + // Performs a spherical linear interpolation between two vectors. + static Vector3Base Slerp(const Vector3Base& start, const Vector3Base& end, T amount) + { + Vector3Base result; + Slerp(start, end, amount, result); + return result; + } + // Performs a cubic interpolation between two vectors. static void SmoothStep(const Vector3Base& start, const Vector3Base& end, T amount, Vector3Base& result) { diff --git a/Source/Engine/Core/Math/Vector4.cs b/Source/Engine/Core/Math/Vector4.cs index 776bbd3dc..b08a08f50 100644 --- a/Source/Engine/Core/Math/Vector4.cs +++ b/Source/Engine/Core/Math/Vector4.cs @@ -891,6 +891,33 @@ namespace FlaxEngine return result; } + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + /// When the method completes, contains the linear interpolation of the two vectors. + public static void Slerp(ref Vector4 start, ref Vector4 end, Real amount, out Vector4 result) + { + var dot = Mathr.Clamp(Dot(start, end), -1.0f, 1.0f); + var theta = Mathr.Acos(dot) * amount; + Vector4 relativeVector = (end - start * dot).Normalized; + result = ((start * Mathr.Cos(theta)) + (relativeVector * Mathr.Sin(theta))); + } + + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + public static Vector4 Slerp(Vector4 start, Vector4 end, Real amount) + { + Slerp(ref start, ref end, amount, out var result); + return result; + } + /// /// Performs a cubic interpolation between two vectors. /// diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index 91743ad73..5f5e436ad 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -566,6 +566,24 @@ public: return result; } + // Performs a spherical linear interpolation between two vectors. + static void Slerp(const Vector4Base& start, const Vector4Base& end, T amount, Vector4Base& result) + { + T dot = Math::Clamp(Dot(start, end), -1.0f, 1.0f); + T theta = Math::Acos(dot) * amount; + Vector4Base relativeVector = end - start * dot; + relativeVector.Normalize(); + result = ((start * Math::Cos(theta)) + (relativeVector * Math::Sin(theta))); + } + + // Performs a spherical linear interpolation between two vectors. + static Vector4Base Slerp(const Vector4Base& start, const Vector4Base& end, T amount) + { + Vector4Base result; + Slerp(start, end, amount, result); + return result; + } + FLAXENGINE_API static Vector4Base Transform(const Vector4Base& v, const Matrix& m); }; From 65fd22f5b6bea616ea6e3a6d9e366496b6836197 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 5 Feb 2026 13:00:49 +0100 Subject: [PATCH 072/100] Add `Triangles` to `MeshAccessor` for easy index buffer access #3918 --- Source/Engine/Graphics/Models/MeshAccessor.cs | 98 +++++++++++++++++-- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index 228e43a8c..29aa86c18 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -265,6 +265,39 @@ namespace FlaxEngine } } + /// + /// Copies the contents of the input into the elements of this stream. + /// + /// The source . + public void Set(Span src) + { + if (IsLinear(PixelFormat.R32_UInt)) + { + src.CopyTo(MemoryMarshal.Cast(_data)); + } + else if (IsLinear(PixelFormat.R16_UInt)) + { + var count = Count; + fixed (byte* data = _data) + { + for (int i = 0; i < count; i++) + ((ushort*)data)[i] = (ushort)src[i]; + } + } + else + { + var count = Count; + fixed (byte* data = _data) + { + for (int i = 0; i < count; i++) + { + var v = new Float4(src[i]); + _sampler.Write(data + i * _stride, ref v); + } + } + } + } + /// /// Copies the contents of this stream into a destination . /// @@ -281,9 +314,7 @@ namespace FlaxEngine fixed (byte* data = _data) { for (int i = 0; i < count; i++) - { dst[i] = new Float2(_sampler.Read(data + i * _stride)); - } } } } @@ -304,9 +335,7 @@ namespace FlaxEngine fixed (byte* data = _data) { for (int i = 0; i < count; i++) - { dst[i] = new Float3(_sampler.Read(data + i * _stride)); - } } } } @@ -327,9 +356,37 @@ namespace FlaxEngine fixed (byte* data = _data) { for (int i = 0; i < count; i++) - { dst[i] = (Color)_sampler.Read(data + i * _stride); - } + } + } + } + + /// + /// Copies the contents of this stream into a destination . + /// + /// The destination . + public void CopyTo(Span dst) + { + if (IsLinear(PixelFormat.R32_UInt)) + { + _data.CopyTo(MemoryMarshal.Cast(dst)); + } + else if (IsLinear(PixelFormat.R16_UInt)) + { + var count = Count; + fixed (byte* data = _data) + { + for (int i = 0; i < count; i++) + dst[i] = ((ushort*)data)[i]; + } + } + else + { + var count = Count; + fixed (byte* data = _data) + { + for (int i = 0; i < count; i++) + dst[i] = (uint)_sampler.Read(data + i * _stride).X; } } } @@ -619,6 +676,16 @@ namespace FlaxEngine return Attribute((VertexElement.Types)((byte)VertexElement.Types.TexCoord0 + channel)); } + /// + /// Gets or sets the index buffer with triangle indices. + /// + /// Uses stream to read or write data to the index buffer. + public uint[] Triangles + { + get => GetStreamUInt(Index()); + set => SetStreamUInt(Index(), value); + } + /// /// Gets or sets the vertex positions. Null if does not exist in vertex buffers of the mesh. /// @@ -659,6 +726,25 @@ namespace FlaxEngine set => SetStreamFloat2(VertexElement.Types.TexCoord, value); } + private uint[] GetStreamUInt(Stream stream) + { + uint[] result = null; + if (stream.IsValid) + { + result = new uint[stream.Count]; + stream.CopyTo(result); + } + return result; + } + + private void SetStreamUInt(Stream stream, uint[] value) + { + if (stream.IsValid) + { + stream.Set(value); + } + } + private delegate void TransformDelegate3(ref Float3 value); private Float3[] GetStreamFloat3(VertexElement.Types attribute, TransformDelegate3 transform = null) From 4833c19366de0474c81becad678423f98546ca6f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 5 Feb 2026 15:45:02 +0100 Subject: [PATCH 073/100] Fix build regression --- Source/Engine/Content/Content.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Engine/Content/Content.h b/Source/Engine/Content/Content.h index 15da20a9c..e286e7c7d 100644 --- a/Source/Engine/Content/Content.h +++ b/Source/Engine/Content/Content.h @@ -3,6 +3,9 @@ #pragma once #include "Engine/Scripting/ScriptingType.h" +#ifndef _MSC_VER +#include "Engine/Core/Collections/Array.h" +#endif #include "AssetInfo.h" #include "Asset.h" #include "Config.h" From 78e5baf6a533f22463ed9a8eda27912a778c006b Mon Sep 17 00:00:00 2001 From: Menotdan <32620310+Menotdan@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:12:21 -0500 Subject: [PATCH 074/100] Fix replicating changes from one client to another --- Source/Engine/Networking/NetworkReplicator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index e5a7d232e..ced964436 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -1029,7 +1029,7 @@ void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, b } // Speed up replication of client-owned objects to other clients from server to reduce lag (data has to go from client to server and then to other clients) - if (NetworkManager::IsServer()) + if (NetworkManager::IsServer() || NetworkManager::IsHost()) DirtyObjectImpl(item, obj); } @@ -2147,7 +2147,7 @@ void NetworkInternal::NetworkReplicatorUpdate() Objects.Remove(it); continue; } - if (item.Role != NetworkObjectRole::OwnedAuthoritative) + if (item.Role != NetworkObjectRole::OwnedAuthoritative && NetworkManager::IsClient()) continue; // Send replication messages of only owned objects or from other client objects CachedReplicationResult->AddObject(obj); } From c18b9163ca92ee82246d1098402c25cd121b5ca6 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 6 Feb 2026 09:11:12 +0100 Subject: [PATCH 075/100] Refactor Job System to reduce mutex usage with more atomic operations #3917 --- Source/Engine/Threading/JobSystem.cpp | 305 ++++++++++++++------------ 1 file changed, 167 insertions(+), 138 deletions(-) diff --git a/Source/Engine/Threading/JobSystem.cpp b/Source/Engine/Threading/JobSystem.cpp index 692a088b7..cbdf53136 100644 --- a/Source/Engine/Threading/JobSystem.cpp +++ b/Source/Engine/Threading/JobSystem.cpp @@ -8,7 +8,6 @@ #include "Engine/Core/Types/Span.h" #include "Engine/Core/Types/Pair.h" #include "Engine/Core/Memory/SimpleHeapAllocation.h" -#include "Engine/Core/Collections/Dictionary.h" #include "Engine/Core/Collections/RingBuffer.h" #include "Engine/Engine/EngineService.h" #include "Engine/Profiler/ProfilerCPU.h" @@ -22,14 +21,6 @@ #if JOB_SYSTEM_ENABLED -// Local allocator for job system memory that uses internal pooling and assumes that JobsLocker is taken (write access owned by the calling thread). -class JobSystemAllocation : public SimpleHeapAllocation -{ -public: - static void* Allocate(uintptr size); - static void Free(void* ptr, uintptr size); -}; - class JobSystemService : public EngineService { public: @@ -43,24 +34,25 @@ public: void Dispose() override; }; -struct JobData -{ - int32 Index; - int64 JobKey; -}; - -template<> -struct TIsPODType -{ - enum { Value = true }; -}; - -struct JobContext +// Holds a single job dispatch data +struct alignas(int64) JobContext { + // The next index of the job to process updated when picking a job by the thread. + volatile int64 JobIndex; + // The number of jobs left to process updated after job completion by the thread. volatile int64 JobsLeft; - int32 DependenciesLeft; + // The unique label of this job used to identify it. Set to -1 when job is done. + volatile int64 JobLabel; + // Utility atomic counter used to indicate that any job is waiting for this one to finish. Then Dependants can be accessed within thread-safe JobsLocker. + volatile int64 DependantsCount; + // The number of dependency jobs left to be finished before starting this job. + volatile int64 DependenciesLeft; + // The total number of jobs to process (in this context). + int32 JobsCount; + // The job function to execute. Function Job; - Array Dependants; + // List of dependant jobs to signal when this job is done. + Array Dependants; }; template<> @@ -92,50 +84,36 @@ public: namespace { JobSystemService JobSystemInstance; - Array> MemPool; Thread* Threads[PLATFORM_THREADS_LIMIT / 2] = {}; int32 ThreadsCount = 0; bool JobStartingOnDispatch = true; volatile int64 ExitFlag = 0; volatile int64 JobLabel = 0; - Dictionary JobContexts; + volatile int64 JobEndLabel = 0; + volatile int64 JobStartLabel = 0; + volatile int64 JobContextsCount = 0; + uint32 JobContextsSize = 0; + uint32 JobContextsMask = 0; + JobContext* JobContexts = nullptr; ConditionVariable JobsSignal; CriticalSection JobsMutex; ConditionVariable WaitSignal; CriticalSection WaitMutex; CriticalSection JobsLocker; - RingBuffer Jobs; -} - -void* JobSystemAllocation::Allocate(uintptr size) -{ - void* result = nullptr; - for (int32 i = 0; i < MemPool.Count(); i++) - { - if (MemPool.Get()[i].Second == size) - { - result = MemPool.Get()[i].First; - MemPool.RemoveAt(i); - break; - } - } - if (!result) - { - PROFILE_MEM(EngineThreading); - result = Platform::Allocate(size, 16); - } - return result; -} - -void JobSystemAllocation::Free(void* ptr, uintptr size) -{ - PROFILE_MEM(EngineThreading); - MemPool.Add({ ptr, size }); +#define GET_CONTEXT_INDEX(label) (uint32)((label) & (int64)JobContextsMask) } bool JobSystemService::Init() { PROFILE_MEM(EngineThreading); + + // Initialize job context storage (fixed-size ring buffer for active jobs tracking) + JobContextsSize = 256; + JobContextsMask = JobContextsSize - 1; + JobContexts = (JobContext*)Platform::Allocate(JobContextsSize * sizeof(JobContext), alignof(JobContext)); + Platform::MemoryClear(JobContexts, sizeof(JobContextsSize * sizeof(JobContext))); + + // Spawn threads ThreadsCount = Math::Min(Platform::GetCPUInfo().LogicalProcessorCount, ARRAY_COUNT(Threads)); for (int32 i = 0; i < ThreadsCount; i++) { @@ -146,6 +124,7 @@ bool JobSystemService::Init() return true; Threads[i] = thread; } + return false; } @@ -171,35 +150,66 @@ void JobSystemService::Dispose() } } - JobContexts.SetCapacity(0); - Jobs.Release(); - for (auto& e : MemPool) - Platform::Free(e.First); - MemPool.Clear(); + Platform::Free(JobContexts); + JobContexts = nullptr; } int32 JobSystemThread::Run() { + // Pin thread to the physical core Platform::SetThreadAffinityMask(1ull << Index); - JobData data; - Function job; bool attachCSharpThread = true; MONO_THREAD_INFO_TYPE* monoThreadInfo = nullptr; while (Platform::AtomicRead(&ExitFlag) == 0) { // Try to get a job - JobsLocker.Lock(); - if (Jobs.Count() != 0) + int32 jobIndex; + JobContext* jobContext = nullptr; { - data = Jobs.PeekFront(); - Jobs.PopFront(); - const JobContext& context = ((const Dictionary&)JobContexts).At(data.JobKey); - job = context.Job; - } - JobsLocker.Unlock(); + int64 jobOffset = 0; + RETRY: + int64 jobStartLabel = Platform::AtomicRead(&JobStartLabel) + jobOffset; + int64 jobEndLabel = Platform::AtomicRead(&JobEndLabel); + if (jobStartLabel <= jobEndLabel && jobEndLabel > 0) + { + jobContext = &JobContexts[GET_CONTEXT_INDEX(jobStartLabel)]; + if (Platform::AtomicRead(&jobContext->DependenciesLeft) > 0) + { + // This job still waits for dependency so skip it for now and try the next one + jobOffset++; + jobContext = nullptr; + goto RETRY; + } - if (job.IsBinded()) + // Move forward with index for a job + jobIndex = (int32)(Platform::InterlockedIncrement(&jobContext->JobIndex) - 1); + if (jobIndex < jobContext->JobsCount) + { + // Index is valid + } + else if (jobStartLabel < jobEndLabel && jobOffset == 0) + { + // No more jobs inside this context, move to the next one + Platform::InterlockedCompareExchange(&JobStartLabel, jobStartLabel + 1, jobStartLabel); + jobContext = nullptr; + goto RETRY; + } + else + { + // No more jobs + jobContext = nullptr; + if (jobStartLabel < jobEndLabel) + { + // Try with a different one before going to sleep + jobOffset++; + goto RETRY; + } + } + } + } + + if (jobContext) { #if USE_CSHARP // Ensure to have C# thread attached to this thead (late init due to MCore being initialized after Job System) @@ -212,37 +222,39 @@ int32 JobSystemThread::Run() #endif // Run job - job(data.Index); + jobContext->Job(jobIndex); // Move forward with the job queue - bool notifyWaiting = false; - JobsLocker.Lock(); - JobContext& context = JobContexts.At(data.JobKey); - if (Platform::InterlockedDecrement(&context.JobsLeft) <= 0) + if (Platform::InterlockedDecrement(&jobContext->JobsLeft) <= 0) { - // Update any dependant jobs - for (int64 dependant : context.Dependants) + // Mark job as done before processing dependants + Platform::AtomicStore(&jobContext->JobLabel, -1); + + // Check if any other job waits on this one + if (Platform::AtomicRead(&jobContext->DependantsCount) != 0) { - JobContext& dependantContext = JobContexts.At(dependant); - if (--dependantContext.DependenciesLeft <= 0) + // Update dependant jobs + JobsLocker.Lock(); + for (int64 dependant : jobContext->Dependants) { - // Dispatch dependency when it's ready - JobData dependantData; - dependantData.JobKey = dependant; - for (dependantData.Index = 0; dependantData.Index < dependantContext.JobsLeft; dependantData.Index++) - Jobs.PushBack(dependantData); + JobContext& dependantContext = JobContexts[GET_CONTEXT_INDEX(dependant)]; + if (dependantContext.JobLabel == dependant) + Platform::InterlockedDecrement(&dependantContext.DependenciesLeft); } + JobsLocker.Unlock(); } - // Remove completed context - JobContexts.Remove(data.JobKey); - notifyWaiting = true; - } - JobsLocker.Unlock(); - if (notifyWaiting) - WaitSignal.NotifyAll(); + // Cleanup completed context + jobContext->Job.Unbind(); + jobContext->Dependants.Clear(); + Platform::AtomicStore(&jobContext->DependantsCount, 0); + Platform::AtomicStore(&jobContext->DependenciesLeft, -999); // Mark to indicate deleted context + Platform::AtomicStore(&jobContext->JobLabel, -1); + Platform::InterlockedDecrement(&JobContextsCount); - job.Unbind(); + // Wakeup any thread waiting for the jobs to complete + WaitSignal.NotifyAll(); + } } else { @@ -266,8 +278,8 @@ void JobSystem::Execute(const Function& job, int32 jobCount) if (jobCount > 1) { // Async - const int64 jobWaitHandle = Dispatch(job, jobCount); - Wait(jobWaitHandle); + const int64 label = Dispatch(job, jobCount); + Wait(label); } else #endif @@ -284,21 +296,32 @@ int64 JobSystem::Dispatch(const Function& job, int32 jobCount) return 0; PROFILE_CPU(); #if JOB_SYSTEM_ENABLED - const auto label = Platform::InterlockedAdd(&JobLabel, (int64)jobCount) + jobCount; + while (Platform::InterlockedIncrement(&JobContextsCount) >= JobContextsSize) + { + // Too many jobs in flight, wait for some to complete to free up contexts + PROFILE_CPU_NAMED("JOB SYSTEM OVERFLOW"); + ZoneColor(TracyWaitZoneColor); + Platform::InterlockedDecrement(&JobContextsCount); + Platform::Sleep(1); + } - JobData data; - data.JobKey = label; + // Get a new label + const int64 label = Platform::InterlockedIncrement(&JobLabel); - JobContext context; + // Build job + JobContext& context = JobContexts[GET_CONTEXT_INDEX(label)]; context.Job = job; + context.JobIndex = 0; context.JobsLeft = jobCount; + context.JobLabel = label; + context.DependantsCount = 0; context.DependenciesLeft = 0; + context.JobsCount = jobCount; + ASSERT(context.Dependants.IsEmpty()); + context.Dependants.Clear(); - JobsLocker.Lock(); - JobContexts.Add(label, MoveTemp(context)); - for (data.Index = 0; data.Index < jobCount; data.Index++) - Jobs.PushBack(data); - JobsLocker.Unlock(); + // Move the job queue forward + Platform::InterlockedIncrement(&JobEndLabel); if (JobStartingOnDispatch) { @@ -321,34 +344,48 @@ int64 JobSystem::Dispatch(const Function& job, Span dependen if (jobCount <= 0) return 0; PROFILE_CPU(); + PROFILE_MEM(EngineThreading); #if JOB_SYSTEM_ENABLED - const auto label = Platform::InterlockedAdd(&JobLabel, (int64)jobCount) + jobCount; + while (Platform::InterlockedIncrement(&JobContextsCount) >= JobContextsSize) + { + // Too many jobs in flight, wait for some to complete to free up contexts + PROFILE_CPU_NAMED("JOB SYSTEM OVERFLOW"); + ZoneColor(TracyWaitZoneColor); + Platform::InterlockedDecrement(&JobContextsCount); + Platform::Sleep(1); + } - JobData data; - data.JobKey = label; + // Get a new label + const int64 label = Platform::InterlockedIncrement(&JobLabel); - JobContext context; + // Build job + JobContext& context = JobContexts[GET_CONTEXT_INDEX(label)]; context.Job = job; + context.JobIndex = 0; context.JobsLeft = jobCount; + context.JobLabel = label; + context.DependantsCount = 0; context.DependenciesLeft = 0; - - JobsLocker.Lock(); - for (int64 dependency : dependencies) + context.JobsCount = jobCount; + ASSERT(context.Dependants.IsEmpty()); + context.Dependants.Clear(); { - if (JobContext* dependencyContext = JobContexts.TryGet(dependency)) + JobsLocker.Lock(); + for (int64 dependency : dependencies) { - context.DependenciesLeft++; - dependencyContext->Dependants.Add(label); + JobContext& dependencyContext = JobContexts[GET_CONTEXT_INDEX(dependency)]; + if (Platform::AtomicRead(&dependencyContext.JobLabel) == dependency) + { + Platform::InterlockedIncrement(&dependencyContext.DependantsCount); + dependencyContext.Dependants.Add(label); + context.DependenciesLeft++; + } } + JobsLocker.Unlock(); } - JobContexts.Add(label, MoveTemp(context)); - if (context.DependenciesLeft == 0) - { - // No dependencies left to complete so dispatch now - for (data.Index = 0; data.Index < jobCount; data.Index++) - Jobs.PushBack(data); - } - JobsLocker.Unlock(); + + // Move the job queue forward + Platform::InterlockedIncrement(&JobEndLabel); if (context.DependenciesLeft == 0 && JobStartingOnDispatch) { @@ -369,19 +406,17 @@ int64 JobSystem::Dispatch(const Function& job, Span dependen void JobSystem::Wait() { #if JOB_SYSTEM_ENABLED - JobsLocker.Lock(); - int32 numJobs = JobContexts.Count(); - JobsLocker.Unlock(); + PROFILE_CPU(); + ZoneColor(TracyWaitZoneColor); + int64 numJobs = Platform::AtomicRead(&JobContextsCount); while (numJobs > 0) { WaitMutex.Lock(); WaitSignal.Wait(WaitMutex, 1); WaitMutex.Unlock(); - JobsLocker.Lock(); - numJobs = JobContexts.Count(); - JobsLocker.Unlock(); + numJobs = Platform::AtomicRead(&JobContextsCount); } #endif } @@ -394,12 +429,11 @@ void JobSystem::Wait(int64 label) while (Platform::AtomicRead(&ExitFlag) == 0) { - JobsLocker.Lock(); - const JobContext* context = JobContexts.TryGet(label); - JobsLocker.Unlock(); + const JobContext& context = JobContexts[GET_CONTEXT_INDEX(label)]; + const bool finished = Platform::AtomicRead(&context.JobLabel) != label || Platform::AtomicRead(&context.JobsLeft) <= 0; // Skip if context has been already executed (last job removes it) - if (!context) + if (finished) break; // Wait on signal until input label is not yet done @@ -417,15 +451,10 @@ void JobSystem::SetJobStartingOnDispatch(bool value) { #if JOB_SYSTEM_ENABLED JobStartingOnDispatch = value; - if (value) + if (value && (Platform::AtomicRead(&JobEndLabel) - Platform::AtomicRead(&JobStartLabel)) > 0) { - JobsLocker.Lock(); - const int32 count = Jobs.Count(); - JobsLocker.Unlock(); - if (count == 1) - JobsSignal.NotifyOne(); - else if (count != 0) - JobsSignal.NotifyAll(); + // Wake up threads to start processing jobs that may be already in the queue + JobsSignal.NotifyAll(); } #endif } From 20516bb8bc86170cd8ec9d06263b0de34d263e28 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 6 Feb 2026 09:11:45 +0100 Subject: [PATCH 076/100] Fix foliage dithered LOD transitions when using shadows --- Source/Engine/Foliage/Foliage.cpp | 81 ++++++++++++++++++------------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index f8b9c7b0f..6d3d8b700 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -7,17 +7,17 @@ #include "Engine/Core/Random.h" #include "Engine/Engine/Engine.h" #include "Engine/Graphics/RenderTask.h" +#include "Engine/Graphics/GPUDevice.h" #include "Engine/Content/Deprecated.h" #if !FOLIAGE_USE_SINGLE_QUAD_TREE #include "Engine/Threading/JobSystem.h" #if FOLIAGE_USE_DRAW_CALLS_BATCHING #include "Engine/Graphics/RenderTools.h" -#include "Engine/Graphics/GPUDevice.h" -#include "Engine/Renderer/RenderList.h" #endif #endif #include "Engine/Level/SceneQuery.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Renderer/RenderList.h" #include "Engine/Renderer/GlobalSignDistanceFieldPass.h" #include "Engine/Renderer/GI/GlobalSurfaceAtlasPass.h" #include "Engine/Serialization/Serialization.h" @@ -193,6 +193,8 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, // Draw visible instances const auto frame = Engine::FrameCount; const auto model = type.Model.Get(); + const auto transitionLOD = renderContext.View.Pass != DrawPass::Depth; // Let the main view pass update LOD transitions + // TODO: move DrawState to be stored per-view (so shadows can fade objects on their own) for (int32 i = 0; i < cluster->Instances.Count(); i++) { auto& instance = *cluster->Instances.Get()[i]; @@ -210,20 +212,29 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, // Handling model fade-out transition if (modelFrame == frame && instance.DrawState.PrevLOD != -1) { - // Check if start transition - if (instance.DrawState.LODTransition == 255) + if (transitionLOD) { - instance.DrawState.LODTransition = 0; - } + // Check if start transition + if (instance.DrawState.LODTransition == 255) + { + instance.DrawState.LODTransition = 0; + } - RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition); + RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition); - // Check if end transition - if (instance.DrawState.LODTransition == 255) - { - instance.DrawState.PrevLOD = lodIndex; + // Check if end transition + if (instance.DrawState.LODTransition == 255) + { + instance.DrawState.PrevLOD = lodIndex; + } + else + { + const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD); + const float normalizedProgress = static_cast(instance.DrawState.LODTransition) * (1.0f / 255.0f); + DrawInstance(renderContext, instance, type, model, prevLOD, normalizedProgress, drawCallsLists, result); + } } - else + else if (instance.DrawState.LODTransition < 255) { const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD); const float normalizedProgress = static_cast(instance.DrawState.LODTransition) * (1.0f / 255.0f); @@ -236,29 +247,32 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, lodIndex += renderContext.View.ModelLODBias; lodIndex = model->ClampLODIndex(lodIndex); - // Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports) - if (modelFrame == frame) + if (transitionLOD) { - // Check if start transition - if (instance.DrawState.PrevLOD != lodIndex && instance.DrawState.LODTransition == 255) + // Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports) + if (modelFrame == frame) { + // Check if start transition + if (instance.DrawState.PrevLOD != lodIndex && instance.DrawState.LODTransition == 255) + { + instance.DrawState.LODTransition = 0; + } + + RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition); + + // Check if end transition + if (instance.DrawState.LODTransition == 255) + { + instance.DrawState.PrevLOD = lodIndex; + } + } + // Check if there was a gap between frames in drawing this model instance + else if (modelFrame < frame || instance.DrawState.PrevLOD == -1) + { + // Reset state + instance.DrawState.PrevLOD = lodIndex; instance.DrawState.LODTransition = 0; } - - RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition); - - // Check if end transition - if (instance.DrawState.LODTransition == 255) - { - instance.DrawState.PrevLOD = lodIndex; - } - } - // Check if there was a gap between frames in drawing this model instance - else if (modelFrame < frame || instance.DrawState.PrevLOD == -1) - { - // Reset state - instance.DrawState.PrevLOD = lodIndex; - instance.DrawState.LODTransition = 255; } // Draw @@ -281,7 +295,8 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, //DebugDraw::DrawSphere(instance.Bounds, Color::YellowGreen); - instance.DrawState.PrevFrame = frame; + if (transitionLOD) + instance.DrawState.PrevFrame = frame; } } } @@ -350,7 +365,7 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, draw.DrawState = &instance.DrawState; draw.Bounds = sphere; draw.PerInstanceRandom = instance.Random; - draw.DrawModes = type._drawModes; + draw.DrawModes = type.DrawModes; draw.SetStencilValue(_layer); type.Model->Draw(renderContext, draw); From 70b324cdecaba0a7b7a61112d9c9783dc20f2c9b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 6 Feb 2026 09:11:55 +0100 Subject: [PATCH 077/100] Optimize included header usage --- Source/Engine/Renderer/RenderList.cpp | 1 + Source/Engine/Renderer/RenderList.h | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index 544438bb5..453fd71a9 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -15,6 +15,7 @@ #include "Engine/Profiler/Profiler.h" #include "Engine/Content/Assets/CubeTexture.h" #include "Engine/Core/Log.h" +#include "Engine/Core/Math/Half.h" #include "Engine/Graphics/Shaders/GPUVertexLayout.h" #include "Engine/Level/Scene/Lightmap.h" #include "Engine/Level/Actors/PostFxVolume.h" diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h index 8eb3540e0..a37d2a941 100644 --- a/Source/Engine/Renderer/RenderList.h +++ b/Source/Engine/Renderer/RenderList.h @@ -4,7 +4,6 @@ #include "Engine/Core/Collections/Array.h" #include "Engine/Core/Memory/ArenaAllocation.h" -#include "Engine/Core/Math/Half.h" #include "Engine/Graphics/PostProcessSettings.h" #include "Engine/Graphics/DynamicBuffer.h" #include "Engine/Scripting/ScriptingObject.h" From 5d0fdc83138e42ddcea44c4f99c090a96ae335ce Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 6 Feb 2026 09:28:06 +0100 Subject: [PATCH 078/100] Add info about skinned model skeleton size --- Source/Editor/Windows/Assets/SkinnedModelWindow.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs index 392cc896a..75fa87dfe 100644 --- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs +++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs @@ -70,6 +70,13 @@ namespace FlaxEditor.Windows.Assets return; var nodes = proxy.Asset.Nodes; var bones = proxy.Asset.Bones; + var blendShapes = proxy.Asset.BlendShapes; + + // Info + { + var group = layout.Group("Info"); + group.Label($"Nodes: {nodes.Length}\nBones: {bones.Length}\nBlend Shapes: {blendShapes.Length}").AddCopyContextMenu().Label.Height *= 2.5f; + } // Skeleton Bones { @@ -109,7 +116,6 @@ namespace FlaxEditor.Windows.Assets } // Blend Shapes - var blendShapes = proxy.Asset.BlendShapes; if (blendShapes.Length != 0) { var group = layout.Group("Blend Shapes"); From 4a7f1a5fde285daba16e2038a3207b33a6827eb7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 6 Feb 2026 10:37:52 +0100 Subject: [PATCH 079/100] Fix various issues --- .../Windows/Assets/AnimationGraphWindow.cs | 9 +++- Source/Engine/Foliage/Foliage.cpp | 4 +- Source/Engine/Foliage/FoliageCluster.cpp | 45 ++++++++++--------- Source/Engine/Level/Actor.cpp | 4 +- Source/Engine/Threading/JobSystem.cpp | 2 - 5 files changed, 37 insertions(+), 27 deletions(-) diff --git a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs index 7e809d968..12142219c 100644 --- a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs +++ b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs @@ -99,7 +99,14 @@ namespace FlaxEditor.Windows.Assets Window = window; var surfaceParam = window.Surface.GetParameter(BaseModelId); if (surfaceParam != null) - BaseModel = FlaxEngine.Content.LoadAsync((Guid)surfaceParam.Value); + { + if (surfaceParam.Value is Guid asGuid) + BaseModel = FlaxEngine.Content.LoadAsync(asGuid); + else if (surfaceParam.Value is SkinnedModel asModel) + BaseModel = asModel; + else + BaseModel = null; + } else BaseModel = window.PreviewActor.GetParameterValue(BaseModelId) as SkinnedModel; } diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index 6d3d8b700..116866848 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -41,8 +41,7 @@ Foliage::Foliage(const SpawnParams& params) void Foliage::AddToCluster(ChunkedArray& clusters, FoliageCluster* cluster, FoliageInstance& instance) { - ASSERT(instance.Bounds.Radius > ZeroTolerance); - ASSERT(cluster->Bounds.Intersects(instance.Bounds)); + ASSERT_LOW_LAYER(instance.Bounds.Radius > ZeroTolerance); // Minor clusters don't use bounds intersection but try to find the first free cluster instead if (cluster->IsMinor) @@ -63,6 +62,7 @@ void Foliage::AddToCluster(ChunkedArrayBounds.Intersects(instance.Bounds)); while (cluster->Children[0]) { #define CHECK_CHILD(idx) \ diff --git a/Source/Engine/Foliage/FoliageCluster.cpp b/Source/Engine/Foliage/FoliageCluster.cpp index fd4c0f753..107bf265a 100644 --- a/Source/Engine/Foliage/FoliageCluster.cpp +++ b/Source/Engine/Foliage/FoliageCluster.cpp @@ -21,26 +21,7 @@ void FoliageCluster::Init(const BoundingBox& bounds) void FoliageCluster::UpdateTotalBoundsAndCullDistance() { - if (Children[0]) - { - ASSERT(Instances.IsEmpty()); - - Children[0]->UpdateTotalBoundsAndCullDistance(); - Children[1]->UpdateTotalBoundsAndCullDistance(); - Children[2]->UpdateTotalBoundsAndCullDistance(); - Children[3]->UpdateTotalBoundsAndCullDistance(); - - TotalBounds = Children[0]->TotalBounds; - BoundingBox::Merge(TotalBounds, Children[1]->TotalBounds, TotalBounds); - BoundingBox::Merge(TotalBounds, Children[2]->TotalBounds, TotalBounds); - BoundingBox::Merge(TotalBounds, Children[3]->TotalBounds, TotalBounds); - - MaxCullDistance = Children[0]->MaxCullDistance; - MaxCullDistance = Math::Max(MaxCullDistance, Children[1]->MaxCullDistance); - MaxCullDistance = Math::Max(MaxCullDistance, Children[2]->MaxCullDistance); - MaxCullDistance = Math::Max(MaxCullDistance, Children[3]->MaxCullDistance); - } - else if (Instances.HasItems()) + if (Instances.HasItems()) { BoundingBox box; BoundingBox::FromSphere(Instances[0]->Bounds, TotalBounds); @@ -58,6 +39,30 @@ void FoliageCluster::UpdateTotalBoundsAndCullDistance() MaxCullDistance = 0; } + if (Children[0]) + { + Children[0]->UpdateTotalBoundsAndCullDistance(); + Children[1]->UpdateTotalBoundsAndCullDistance(); + Children[2]->UpdateTotalBoundsAndCullDistance(); + Children[3]->UpdateTotalBoundsAndCullDistance(); + + if (Instances.HasItems()) + BoundingBox::Merge(TotalBounds, Children[0]->TotalBounds, TotalBounds); + else + TotalBounds = Children[0]->TotalBounds; + BoundingBox::Merge(TotalBounds, Children[1]->TotalBounds, TotalBounds); + BoundingBox::Merge(TotalBounds, Children[2]->TotalBounds, TotalBounds); + BoundingBox::Merge(TotalBounds, Children[3]->TotalBounds, TotalBounds); + + if (Instances.HasItems()) + MaxCullDistance = Math::Max(MaxCullDistance, Children[0]->MaxCullDistance); + else + MaxCullDistance = Children[0]->MaxCullDistance; + MaxCullDistance = Math::Max(MaxCullDistance, Children[1]->MaxCullDistance); + MaxCullDistance = Math::Max(MaxCullDistance, Children[2]->MaxCullDistance); + MaxCullDistance = Math::Max(MaxCullDistance, Children[3]->MaxCullDistance); + } + BoundingSphere::FromBox(TotalBounds, TotalBoundsSphere); } diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 7d07cd0e7..f52fab600 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1685,7 +1685,7 @@ Quaternion Actor::LookingAt(const Vector3& worldPos) const { const Vector3 direction = worldPos - _transform.Translation; if (direction.LengthSquared() < ZeroTolerance) - return _parent->GetOrientation(); + return _parent ? _parent->GetOrientation() : Quaternion::Identity; const Float3 newForward = Vector3::Normalize(direction); const Float3 oldForward = _transform.Orientation * Vector3::Forward; @@ -1712,7 +1712,7 @@ Quaternion Actor::LookingAt(const Vector3& worldPos, const Vector3& worldUp) con { const Vector3 direction = worldPos - _transform.Translation; if (direction.LengthSquared() < ZeroTolerance) - return _parent->GetOrientation(); + return _parent ? _parent->GetOrientation() : Quaternion::Identity; const Float3 forward = Vector3::Normalize(direction); const Float3 up = Vector3::Normalize(worldUp); if (Math::IsOne(Float3::Dot(forward, up))) diff --git a/Source/Engine/Threading/JobSystem.cpp b/Source/Engine/Threading/JobSystem.cpp index cbdf53136..4d90f1c06 100644 --- a/Source/Engine/Threading/JobSystem.cpp +++ b/Source/Engine/Threading/JobSystem.cpp @@ -317,7 +317,6 @@ int64 JobSystem::Dispatch(const Function& job, int32 jobCount) context.DependantsCount = 0; context.DependenciesLeft = 0; context.JobsCount = jobCount; - ASSERT(context.Dependants.IsEmpty()); context.Dependants.Clear(); // Move the job queue forward @@ -367,7 +366,6 @@ int64 JobSystem::Dispatch(const Function& job, Span dependen context.DependantsCount = 0; context.DependenciesLeft = 0; context.JobsCount = jobCount; - ASSERT(context.Dependants.IsEmpty()); context.Dependants.Clear(); { JobsLocker.Lock(); From b4cb1028edb41fb70f087fcbf6f353fcc36504fb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 6 Feb 2026 12:47:07 +0100 Subject: [PATCH 080/100] Fix typos --- Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp | 5 ----- Source/Engine/Graphics/Models/SkinnedMeshDrawData.h | 7 +------ Source/Engine/Level/Actors/AnimatedModel.cpp | 2 +- Source/Engine/Platform/Base/FileSystemBase.cpp | 8 ++++---- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp index 8470facac..eaa4aa5a9 100644 --- a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp +++ b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp @@ -5,11 +5,6 @@ #include "Engine/Animations/Config.h" #include "Engine/Core/Log.h" #include "Engine/Core/Math/Matrix.h" -#include "Engine/Core/Math/Matrix3x4.h" - -SkinnedMeshDrawData::SkinnedMeshDrawData() -{ -} SkinnedMeshDrawData::~SkinnedMeshDrawData() { diff --git a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.h b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.h index 24d5ca230..dc780a26d 100644 --- a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.h +++ b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.h @@ -36,11 +36,6 @@ public: Array Data; public: - /// - /// Initializes a new instance of the class. - /// - SkinnedMeshDrawData(); - /// /// Finalizes an instance of the class. /// @@ -76,7 +71,7 @@ public: void OnDataChanged(bool dropHistory); /// - /// After bones Data has been send to the GPU buffer. + /// After bones Data has been sent to the GPU buffer. /// void OnFlush() { diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index f75174f72..a2d95dbf4 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -1002,7 +1002,7 @@ void AnimatedModel::Draw(RenderContext& renderContext) if (renderContext.View.Pass == DrawPass::GlobalSDF) return; if (renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas) - return; // No supported + return; // Not supported ACTOR_GET_WORLD_MATRIX(this, view, world); GEOMETRY_DRAW_STATE_EVENT_BEGIN(_drawState, world); diff --git a/Source/Engine/Platform/Base/FileSystemBase.cpp b/Source/Engine/Platform/Base/FileSystemBase.cpp index f414bbd01..13ae3481c 100644 --- a/Source/Engine/Platform/Base/FileSystemBase.cpp +++ b/Source/Engine/Platform/Base/FileSystemBase.cpp @@ -12,25 +12,25 @@ bool FileSystemBase::ShowOpenFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array& filenames) { - // No supported + // Not supported return true; } bool FileSystemBase::ShowSaveFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array& filenames) { - // No supported + // Not supported return true; } bool FileSystemBase::ShowBrowseFolderDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& title, String& path) { - // No supported + // Not supported return true; } bool FileSystemBase::ShowFileExplorer(const StringView& path) { - // No supported + // Not supported return true; } From 73c19b278f1ffbfd7cb76de53390733267d7ea69 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 6 Feb 2026 12:47:34 +0100 Subject: [PATCH 081/100] Optimize updating Animated Model bones buffer when it's not dirty #3827 --- Source/Engine/Level/Actors/AnimatedModel.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index a2d95dbf4..1bf05a505 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -1015,6 +1015,7 @@ void AnimatedModel::Draw(RenderContext& renderContext) RenderContext::GPULocker.Lock(); GPUDevice::Instance->GetMainContext()->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count()); RenderContext::GPULocker.Unlock(); + _skinningData.OnFlush(); } SkinnedMesh::DrawInfo draw; @@ -1059,6 +1060,7 @@ void AnimatedModel::Draw(RenderContextBatch& renderContextBatch) RenderContext::GPULocker.Lock(); GPUDevice::Instance->GetMainContext()->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count()); RenderContext::GPULocker.Unlock(); + _skinningData.OnFlush(); } SkinnedMesh::DrawInfo draw; From 4afd9fd8dfe3a37f05a9d4323ab9fb421d0367f2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 6 Feb 2026 13:27:53 +0100 Subject: [PATCH 082/100] Optimize Animated Model bones buffer flushing with delayed draw action to reduce lock contention #3917 #3827 --- Source/Engine/Level/Actors/AnimatedModel.cpp | 15 +++++++++------ Source/Engine/Particles/Particles.cpp | 7 +++---- Source/Engine/Renderer/RenderList.cpp | 12 +++++------- Source/Engine/Renderer/RenderList.h | 7 ++++--- Source/Engine/Renderer/Renderer.cpp | 2 +- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 1bf05a505..f6ac51f1c 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -20,6 +20,7 @@ #include "Engine/Level/Scene/Scene.h" #include "Engine/Level/SceneObjectsFactory.h" #include "Engine/Profiler/ProfilerMemory.h" +#include "Engine/Renderer/RenderList.h" #include "Engine/Serialization/Serialization.h" AnimatedModel::AnimatedModel(const SpawnParams& params) @@ -1012,9 +1013,10 @@ void AnimatedModel::Draw(RenderContext& renderContext) // Flush skinning data with GPU if (_skinningData.IsDirty()) { - RenderContext::GPULocker.Lock(); - GPUDevice::Instance->GetMainContext()->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count()); - RenderContext::GPULocker.Unlock(); + renderContext.List->AddDelayedDraw([this](GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex) + { + context->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count()); + }); _skinningData.OnFlush(); } @@ -1057,9 +1059,10 @@ void AnimatedModel::Draw(RenderContextBatch& renderContextBatch) // Flush skinning data with GPU if (_skinningData.IsDirty()) { - RenderContext::GPULocker.Lock(); - GPUDevice::Instance->GetMainContext()->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count()); - RenderContext::GPULocker.Unlock(); + renderContext.List->AddDelayedDraw([this](GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex) + { + context->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count()); + }); _skinningData.OnFlush(); } diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp index 951b657b2..7d3703ee0 100644 --- a/Source/Engine/Particles/Particles.cpp +++ b/Source/Engine/Particles/Particles.cpp @@ -677,11 +677,10 @@ void CleanupGPUParticlesSorting() SAFE_DELETE_GPU_RESOURCE(GPUIndirectArgsBuffer); } -void DrawEmittersGPU(RenderContextBatch& renderContextBatch) +void DrawEmittersGPU(GPUContext* context, RenderContextBatch& renderContextBatch) { PROFILE_GPU_CPU_NAMED("DrawEmittersGPU"); ScopeReadLock systemScope(Particles::SystemLocker); - GPUContext* context = GPUDevice::Instance->GetMainContext(); // Count draws and sorting passes needed for resources allocation uint32 indirectArgsSize = 0; @@ -1124,9 +1123,9 @@ void DrawEmitterGPU(RenderContextBatch& renderContextBatch, ParticleBuffer* buff if (GPUEmitterDraws.Count() == 0) { // The first emitter schedules the drawing of all batched draws - renderContextBatch.GetMainContext().List->AddDelayedDraw([](RenderContextBatch& renderContextBatch, int32 contextIndex) + renderContextBatch.GetMainContext().List->AddDelayedDraw([](GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex) { - DrawEmittersGPU(renderContextBatch); + DrawEmittersGPU(context, renderContextBatch); }); } GPUEmitterDraws.Add({ buffer, drawCall, drawModes, staticFlags, bounds, renderModulesIndices, indirectArgsSize, sortOrder, sorting }); diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index 453fd71a9..ba1f7a0f9 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -258,18 +258,17 @@ void RenderList::AddSettingsBlend(IPostFxSettingsProvider* provider, float weigh void RenderList::AddDelayedDraw(DelayedDraw&& func) { - MemPoolLocker.Lock(); // TODO: convert _delayedDraws into RenderListBuffer with usage of arena Memory for fast alloc _delayedDraws.Add(MoveTemp(func)); - MemPoolLocker.Unlock(); } -void RenderList::DrainDelayedDraws(RenderContextBatch& renderContextBatch, int32 contextIndex) +void RenderList::DrainDelayedDraws(GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex) { - if (_delayedDraws.IsEmpty()) + if (_delayedDraws.Count() == 0) return; + PROFILE_CPU(); for (DelayedDraw& e : _delayedDraws) - e(renderContextBatch, contextIndex); - _delayedDraws.SetCapacity(0); + e(context, renderContextBatch, renderContextIndex); + _delayedDraws.Clear(); } void RenderList::BlendSettings() @@ -495,7 +494,6 @@ RenderList::RenderList(const SpawnParams& params) , ObjectBuffer(0, PixelFormat::R32G32B32A32_Float, false, TEXT("Object Buffer")) , TempObjectBuffer(0, PixelFormat::R32G32B32A32_Float, false, TEXT("Object Buffer")) , _instanceBuffer(0, sizeof(ShaderObjectDrawInstanceData), TEXT("Instance Buffer"), GPUVertexLayout::Get({ { VertexElement::Types::Attribute0, 3, 0, 1, PixelFormat::R32_UInt } })) - , _delayedDraws(&Memory) { } diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h index a37d2a941..9b0a91ed6 100644 --- a/Source/Engine/Renderer/RenderList.h +++ b/Source/Engine/Renderer/RenderList.h @@ -459,13 +459,14 @@ public: /// DynamicTypedBuffer TempObjectBuffer; - typedef Function DelayedDraw; + typedef Function DelayedDraw; void AddDelayedDraw(DelayedDraw&& func); - void DrainDelayedDraws(RenderContextBatch& renderContextBatch, int32 contextIndex); + void DrainDelayedDraws(GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex); /// /// Adds custom callback (eg. lambda) to invoke after scene draw calls are collected on a main thread (some async draw tasks might be active). Allows for safe usage of GPUContext for draw preparations or to perform GPU-driven drawing. /// + /// Can be called in async during scene rendering (thread-safe internally). Lambda is allocated by concurrent arena allocator owned by the RenderList. template FORCE_INLINE void AddDelayedDraw(const T& lambda) { @@ -476,7 +477,7 @@ public: private: DynamicVertexBuffer _instanceBuffer; - Array _delayedDraws; + RenderListBuffer _delayedDraws; public: /// diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp index fd7d43c8b..2f17e5294 100644 --- a/Source/Engine/Renderer/Renderer.cpp +++ b/Source/Engine/Renderer/Renderer.cpp @@ -461,7 +461,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont // Perform custom post-scene drawing (eg. GPU dispatches used by VFX) for (int32 i = 0; i < renderContextBatch.Contexts.Count(); i++) - renderContextBatch.Contexts[i].List->DrainDelayedDraws(renderContextBatch, i); + renderContextBatch.Contexts[i].List->DrainDelayedDraws(context, renderContextBatch, i); #if USE_EDITOR GBufferPass::Instance()->OverrideDrawCalls(renderContext); From 27dd1bda253cd8b849802ed44165ea3e3575b74d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 6 Feb 2026 21:57:16 +0100 Subject: [PATCH 083/100] Optimize `GPUVertexLayout::Get` to not use mutex on lookup read #3917 --- .../Graphics/Shaders/GPUVertexLayout.cpp | 33 ++++++++++--------- Source/Engine/Renderer/RenderList.cpp | 19 +++++++---- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp index 05c6d605a..6c34b2008 100644 --- a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp +++ b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp @@ -44,23 +44,30 @@ namespace Dictionary LayoutCache; Dictionary VertexBufferCache; + // TODO: it's not safe to use map and then use again with a lock (find a better way, eg. using two maps, one first read-only and thread safe, second with mutex-guarded new values from this frame) GPUVertexLayout* AddCache(const VertexBufferLayouts& key, int32 count) { - GPUVertexLayout::Elements elements; - bool anyValid = false; - for (int32 slot = 0; slot < count; slot++) + GPUVertexLayout* result; + CacheLocker.Lock(); + if (!VertexBufferCache.TryGet(key, result)) { - if (key.Layouts[slot]) + GPUVertexLayout::Elements elements; + bool anyValid = false; + for (int32 slot = 0; slot < count; slot++) { - anyValid = true; - int32 start = elements.Count(); - elements.Add(key.Layouts[slot]->GetElements()); - for (int32 j = start; j < elements.Count(); j++) - elements.Get()[j].Slot = (byte)slot; + if (key.Layouts[slot]) + { + anyValid = true; + int32 start = elements.Count(); + elements.Add(key.Layouts[slot]->GetElements()); + for (int32 j = start; j < elements.Count(); j++) + elements.Get()[j].Slot = (byte)slot; + } } + result = anyValid ? GPUVertexLayout::Get(elements, true) : nullptr; + VertexBufferCache.Add(key, result); } - GPUVertexLayout* result = anyValid ? GPUVertexLayout::Get(elements, true) : nullptr; - VertexBufferCache.Add(key, result); + CacheLocker.Unlock(); return result; } } @@ -185,11 +192,9 @@ GPUVertexLayout* GPUVertexLayout::Get(const Span& vertexBuffers) key.Layouts[i] = nullptr; // Lookup existing cache - CacheLocker.Lock(); GPUVertexLayout* result; if (!VertexBufferCache.TryGet(key, result)) result = AddCache(key, vertexBuffers.Length()); - CacheLocker.Unlock(); return result; } @@ -209,11 +214,9 @@ GPUVertexLayout* GPUVertexLayout::Get(const Span& layouts) key.Layouts[i] = nullptr; // Lookup existing cache - CacheLocker.Lock(); GPUVertexLayout* result; if (!VertexBufferCache.TryGet(key, result)) result = AddCache(key, layouts.Length()); - CacheLocker.Unlock(); return result; } diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index ba1f7a0f9..23fcd52dc 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -825,6 +825,13 @@ FORCE_INLINE bool DrawsEqual(const DrawCall* a, const DrawCall* b) Platform::MemoryCompare(a->Geometry.VertexBuffers, b->Geometry.VertexBuffers, sizeof(a->Geometry.VertexBuffers) + sizeof(a->Geometry.VertexBuffersOffsets)) == 0; } +FORCE_INLINE Span GetVB(GPUBuffer* const* ptr, int32 maxSize) +{ + while (ptr[maxSize - 1] == nullptr && maxSize > 1) + maxSize--; + return ToSpan(ptr, maxSize); +} + void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsList& list, RenderList* drawCallsList, GPUTextureView* input) { if (list.IsEmpty()) @@ -953,7 +960,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL Platform::MemoryCopy(vb, activeDraw->Geometry.VertexBuffers, sizeof(DrawCall::Geometry.VertexBuffers)); Platform::MemoryCopy(vbOffsets, activeDraw->Geometry.VertexBuffersOffsets, sizeof(DrawCall::Geometry.VertexBuffersOffsets)); context->BindIB(activeDraw->Geometry.IndexBuffer); - context->BindVB(ToSpan(vb, ARRAY_COUNT(vb)), vbOffsets); + context->BindVB(GetVB(vb, ARRAY_COUNT(vb)), vbOffsets); context->DrawIndexedInstanced(activeDraw->Draw.IndicesCount, activeCount, instanceBufferOffset, 0, activeDraw->Draw.StartIndex); instanceBufferOffset += activeCount; @@ -970,7 +977,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL // Single-draw call batch context->BindIB(drawCall.Geometry.IndexBuffer); - context->BindVB(ToSpan(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets); + context->BindVB(GetVB(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets); if (drawCall.InstanceCount == 0) { context->DrawIndexedInstancedIndirect(drawCall.Draw.IndirectArgsBuffer, drawCall.Draw.IndirectArgsOffset); @@ -993,7 +1000,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL Platform::MemoryCopy(vb, drawCall.Geometry.VertexBuffers, sizeof(DrawCall::Geometry.VertexBuffers)); Platform::MemoryCopy(vbOffsets, drawCall.Geometry.VertexBuffersOffsets, sizeof(DrawCall::Geometry.VertexBuffersOffsets)); context->BindIB(drawCall.Geometry.IndexBuffer); - context->BindVB(ToSpan(vb, vbMax + 1), vbOffsets); + context->BindVB(GetVB(vb, vbMax + 1), vbOffsets); if (drawCall.InstanceCount == 0) { @@ -1023,7 +1030,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL const DrawCall& drawCall = drawCallsData[perDraw.DrawObjectIndex]; context->BindIB(drawCall.Geometry.IndexBuffer); - context->BindVB(ToSpan(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets); + context->BindVB(GetVB(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets); if (drawCall.InstanceCount == 0) { @@ -1044,7 +1051,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL bindParams.DrawCall->Material->Bind(bindParams); context->BindIB(drawCall.Geometry.IndexBuffer); - context->BindVB(ToSpan(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets); + context->BindVB(GetVB(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets); for (int32 j = 0; j < batch.Instances.Count(); j++) { @@ -1068,7 +1075,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL drawCall.Material->Bind(bindParams); context->BindIB(drawCall.Geometry.IndexBuffer); - context->BindVB(ToSpan(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets); + context->BindVB(GetVB(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets); if (drawCall.InstanceCount == 0) { From a855b17cc0f02bce6b72350e0e146837a49eb992 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 7 Feb 2026 00:44:51 +0100 Subject: [PATCH 084/100] Add new collection type `ConcurrentDictionary` #3917 #3827 --- Source/Engine/Core/Collections/Dictionary.h | 4 + .../Engine/Threading/ConcurrentDictionary.h | 316 ++++++++++++++++++ 2 files changed, 320 insertions(+) create mode 100644 Source/Engine/Threading/ConcurrentDictionary.h diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h index e2f5f0ed6..e18a5b999 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -4,6 +4,9 @@ #include "HashSetBase.h" +template +class ConcurrentDictionary; + /// /// Describes single portion of space for the key and value pair in a hash map. /// @@ -13,6 +16,7 @@ struct DictionaryBucket friend Memory; friend HashSetBase; friend Dictionary; + friend ConcurrentDictionary; /// The key. KeyType Key; diff --git a/Source/Engine/Threading/ConcurrentDictionary.h b/Source/Engine/Threading/ConcurrentDictionary.h new file mode 100644 index 000000000..22395d798 --- /dev/null +++ b/Source/Engine/Threading/ConcurrentDictionary.h @@ -0,0 +1,316 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Collections/Dictionary.h" +#include "Engine/Platform/CriticalSection.h" + +/// +/// Template for unordered dictionary with mapped key with value pairs that supports asynchronous data reading and writing. +/// +/// The type of the keys in the dictionary. +/// The type of the values in the dictionary. +/// The type of memory allocator. +template +class ConcurrentDictionary : Dictionary +{ + friend ConcurrentDictionary; +public: + typedef Dictionary Base; + typedef DictionaryBucket Bucket; + using AllocationData = typename AllocationType::template Data; + using AllocationTag = typename AllocationType::Tag; + +private: + mutable volatile int64 _threadsReading = 0; + volatile int64 _threadsWriting = 0; + CriticalSection _locker; + +public: + /// + /// Initializes an empty without reserving any space. + /// + ConcurrentDictionary() + { + } + + /// + /// Initializes an empty without reserving any space. + /// + /// The custom allocation tag. + ConcurrentDictionary(AllocationTag tag) + : Base(tag) + { + } + + /// + /// Finalizes an instance of the class. + /// + ~ConcurrentDictionary() + { + Clear(); + } + +public: + /// + /// Gets the amount of the elements in the collection. + /// + int32 Count() const + { + Reader reader(this); + return Base::_elementsCount; + } + + /// + /// Gets the amount of the elements that can be contained by the collection. + /// + int32 Capacity() const + { + Reader reader(this); + return Base::_size; + } + + /// + /// Tries to get element with given key. + /// + /// The key of the element. + /// The result value. + /// True if element of given key has been found, otherwise false. + template + bool TryGet(const KeyComparableType& key, ValueType& result) const + { + Reader reader(this); + typename Base::FindPositionResult pos; + Base::FindPosition(key, pos); + if (pos.ObjectIndex != -1) + result = Base::_allocation.Get()[pos.ObjectIndex].Value; + return pos.ObjectIndex != -1; + } + +public: + /// + /// Adds a pair of key and value to the collection. + /// + /// The key. + /// The value. + /// True if added element, otherwise false if it already exists (or other thread added it). + template + bool Add(const KeyComparableType& key, const ValueType& value) + { + Writer writer(this); + Bucket* bucket = Base::OnAdd(key, false, true); + if (bucket) + bucket->Occupy(key, value); + return bucket != nullptr; + } + + /// + /// Removes element with a specified key. + /// + /// The element key to remove. + /// True if item was removed from collection, otherwise false. + template + bool Remove(const KeyComparableType& key) + { + Writer writer(this); + return Base::Remove(key); + } + +public: + /// + /// Removes all elements from the collection. + /// + void Clear() + { + Writer writer(this); + Base::Clear(); + } + +public: + /// + /// The read-only dictionary collection iterator. + /// + struct ConstIterator : Base::IteratorBase + { + friend ConcurrentDictionary; + public: + ConstIterator(const ConcurrentDictionary* collection, const int32 index) + : Base::IteratorBase(collection, index) + { + if (collection) + collection->BeginRead(); + } + + ConstIterator(const ConstIterator& i) + : Base::IteratorBase(i._collection, i._index) + { + if (i.collection) + i.collection->BeginRead(); + } + + ConstIterator(ConstIterator&& i) noexcept + : Base::IteratorBase(i._collection, i._index) + { + i._collection = nullptr; + } + + ~ConstIterator() + { + if (this->_collection) + ((ConcurrentDictionary*)this->_collection)->EndRead(); + } + + public: + FORCE_INLINE bool operator!() const + { + return !(bool)*this; + } + + FORCE_INLINE bool operator==(const ConstIterator& v) const + { + return this->_index == v._index && this->_collection == v._collection; + } + + FORCE_INLINE bool operator!=(const ConstIterator& v) const + { + return this->_index != v._index || this->_collection != v._collection; + } + + ConstIterator& operator=(const ConstIterator& v) + { + this->_collection = v._collection; + this->_index = v._index; + return *this; + } + + ConstIterator& operator=(ConstIterator&& v) noexcept + { + this->_collection = v._collection; + this->_index = v._index; + v._collection = nullptr; + return *this; + } + + ConstIterator& operator++() + { + this->Next(); + return *this; + } + + ConstIterator operator++(int) const + { + ConstIterator i = *this; + i.Next(); + return i; + } + + ConstIterator& operator--() + { + this->Prev(); + return *this; + } + + ConstIterator operator--(int) const + { + ConstIterator i = *this; + i.Prev(); + return i; + } + }; + + ConstIterator begin() const + { + ConstIterator i(this, -1); + ++i; + return i; + } + + FORCE_INLINE ConstIterator end() const + { + return ConstIterator(this, Base::_size); + } + +private: + void BeginWrite() + { + Platform::InterlockedIncrement(&_threadsWriting); + + // Wait for all reads to end + RETRY: + while (Platform::AtomicRead(&_threadsReading)) + Platform::Yield(); + + // Thread-safe writing + _locker.Lock(); + if (Platform::AtomicRead(&_threadsReading)) + { + // Other reader entered during mutex locking so give them a chance to transition into active-waiting + _locker.Unlock(); + goto RETRY; + } + } + + void EndWrite() + { + _locker.Unlock(); + Platform::InterlockedDecrement(&_threadsWriting); + } + + void BeginRead() const + { + RETRY: + Platform::InterlockedIncrement(&_threadsReading); + + // Check if any thread is writing (or is about to write) + if (Platform::AtomicRead(&_threadsWriting) != 0) + { + // Wait for all writes to end + Platform::InterlockedDecrement(&_threadsReading); + while (Platform::AtomicRead(&_threadsWriting)) + Platform::Yield(); + + // Try again + goto RETRY; + } + } + + void EndRead() const + { + Platform::InterlockedDecrement(&_threadsReading); + } + +private: + // Utility for methods that read-write state. + struct Writer + { + ConcurrentDictionary* _collection; + + Writer(ConcurrentDictionary* collection) + : _collection(collection) + { + _collection->BeginWrite(); + } + + ~Writer() + { + _collection->EndWrite(); + } + }; + + // Utility for methods that read-only state. + struct Reader + { + const ConcurrentDictionary* _collection; + + Reader(const ConcurrentDictionary* collection) + : _collection(collection) + { + _collection->BeginRead(); + } + + ~Reader() + { + _collection->EndRead(); + } + }; +}; From ecddb8aae5b211ce3a89da42e41e415887ae5ffc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 7 Feb 2026 00:45:14 +0100 Subject: [PATCH 085/100] Optimize `GPUVertexLayout` caches with `ConcurrentDictionary` --- .../Graphics/Shaders/GPUVertexLayout.cpp | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp index 6c34b2008..e458ff1c1 100644 --- a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp +++ b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp @@ -8,6 +8,7 @@ #include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/GPUBuffer.h" #include "Engine/Graphics/PixelFormatExtensions.h" +#include "Engine/Threading/ConcurrentDictionary.h" #if GPU_ENABLE_RESOURCE_NAMING #include "Engine/Scripting/Enums.h" #endif @@ -40,15 +41,12 @@ uint32 GetHash(const VertexBufferLayouts& key) namespace { - CriticalSection CacheLocker; - Dictionary LayoutCache; - Dictionary VertexBufferCache; + ConcurrentDictionary LayoutCache; + ConcurrentDictionary VertexBufferCache; - // TODO: it's not safe to use map and then use again with a lock (find a better way, eg. using two maps, one first read-only and thread safe, second with mutex-guarded new values from this frame) - GPUVertexLayout* AddCache(const VertexBufferLayouts& key, int32 count) + GPUVertexLayout* GetCache(const VertexBufferLayouts& key, int32 count) { GPUVertexLayout* result; - CacheLocker.Lock(); if (!VertexBufferCache.TryGet(key, result)) { GPUVertexLayout::Elements elements; @@ -65,9 +63,15 @@ namespace } } result = anyValid ? GPUVertexLayout::Get(elements, true) : nullptr; - VertexBufferCache.Add(key, result); + if (!VertexBufferCache.Add(key, result)) + { + // Other thread added the value + Delete(result); + bool found = VertexBufferCache.TryGet(key, result); + ASSERT(found); + } + } - CacheLocker.Unlock(); return result; } } @@ -155,7 +159,6 @@ GPUVertexLayout* GPUVertexLayout::Get(const Elements& elements, bool explicitOff } // Lookup existing cache - CacheLocker.Lock(); GPUVertexLayout* result; if (!LayoutCache.TryGet(hash, result)) { @@ -167,12 +170,16 @@ GPUVertexLayout* GPUVertexLayout::Get(const Elements& elements, bool explicitOff LOG(Error, " {}", e.ToString()); #endif LOG(Error, "Failed to create vertex layout"); - CacheLocker.Unlock(); return nullptr; } - LayoutCache.Add(hash, result); + if (!LayoutCache.Add(hash, result)) + { + // Other thread added the value + Delete(result); + bool found = LayoutCache.TryGet(hash, result); + ASSERT(found); + } } - CacheLocker.Unlock(); return result; } @@ -192,11 +199,7 @@ GPUVertexLayout* GPUVertexLayout::Get(const Span& vertexBuffers) key.Layouts[i] = nullptr; // Lookup existing cache - GPUVertexLayout* result; - if (!VertexBufferCache.TryGet(key, result)) - result = AddCache(key, vertexBuffers.Length()); - - return result; + return GetCache(key, vertexBuffers.Length()); } GPUVertexLayout* GPUVertexLayout::Get(const Span& layouts) @@ -214,11 +217,7 @@ GPUVertexLayout* GPUVertexLayout::Get(const Span& layouts) key.Layouts[i] = nullptr; // Lookup existing cache - GPUVertexLayout* result; - if (!VertexBufferCache.TryGet(key, result)) - result = AddCache(key, layouts.Length()); - - return result; + return GetCache(key, layouts.Length()); } GPUVertexLayout* GPUVertexLayout::Merge(GPUVertexLayout* base, GPUVertexLayout* reference, bool removeUnused, bool addMissing, int32 missingSlotOverride, bool referenceOrder) From ed5ad91a32cc6b4d01410a86e12f20ba6ee42243 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 7 Feb 2026 00:46:32 +0100 Subject: [PATCH 086/100] Optimize `SkinnedModel::GetSkeletonMapping` to not use locking for better perf when multi-threading #3827 --- Source/Engine/Content/Assets/SkinnedModel.cpp | 27 ++++++++++++------- Source/Engine/Content/Assets/SkinnedModel.h | 8 +++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index ed41c4aab..ce6300b17 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -61,16 +61,24 @@ Array SkinnedModel::GetBlendShapes() SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source, bool autoRetarget) { + // Fast-path to use cached mapping SkeletonMapping mapping; mapping.TargetSkeleton = this; + SkeletonMappingData mappingData; + if (_skeletonMappingCache.TryGet(source, mappingData)) + { + mapping.SourceSkeleton = mappingData.SourceSkeleton; + mapping.NodesMapping = mappingData.NodesMapping; + return mapping; + } + mapping.SourceSkeleton = nullptr; + if (WaitForLoaded() || !source || source->WaitForLoaded()) return mapping; + PROFILE_CPU(); ScopeLock lock(Locker); - SkeletonMappingData mappingData; if (!_skeletonMappingCache.TryGet(source, mappingData)) { - PROFILE_CPU(); - // Initialize the mapping SkeletonRetarget* retarget = nullptr; const Guid sourceId = source->GetID(); @@ -823,13 +831,13 @@ bool SkinnedModel::SaveMesh(WriteStream& stream, const ModelData& modelData, int void SkinnedModel::ClearSkeletonMapping() { - for (auto& e : _skeletonMappingCache) + for (const auto& e : _skeletonMappingCache) { e.Key->OnUnloaded.Unbind(this); #if USE_EDITOR e.Key->OnReloading.Unbind(this); #endif - Allocator::Free(e.Value.NodesMapping.Get()); + Allocator::Free((void*)e.Value.NodesMapping.Get()); } _skeletonMappingCache.Clear(); } @@ -837,8 +845,9 @@ void SkinnedModel::ClearSkeletonMapping() void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj) { ScopeLock lock(Locker); - auto i = _skeletonMappingCache.Find(obj); - ASSERT(i != _skeletonMappingCache.End()); + SkeletonMappingData mappingData; + bool found = _skeletonMappingCache.TryGet(obj, mappingData); + ASSERT(found); // Unlink event obj->OnUnloaded.Unbind(this); @@ -847,8 +856,8 @@ void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj) #endif // Clear cache - Allocator::Free(i->Value.NodesMapping.Get()); - _skeletonMappingCache.Remove(i); + Allocator::Free(mappingData.NodesMapping.Get()); + _skeletonMappingCache.Remove(obj); } uint64 SkinnedModel::GetMemoryUsage() const diff --git a/Source/Engine/Content/Assets/SkinnedModel.h b/Source/Engine/Content/Assets/SkinnedModel.h index 894a080c4..111d4d6cb 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.h +++ b/Source/Engine/Content/Assets/SkinnedModel.h @@ -3,7 +3,7 @@ #pragma once #include "ModelBase.h" -#include "Engine/Core/Collections/Dictionary.h" +#include "Engine/Threading/ConcurrentDictionary.h" #include "Engine/Graphics/Models/SkinnedMesh.h" #include "Engine/Graphics/Models/SkeletonData.h" @@ -101,9 +101,9 @@ public: struct FLAXENGINE_API SkeletonMapping { // Target skeleton. - AssetReference TargetSkeleton; + SkinnedModel* TargetSkeleton; // Source skeleton. - AssetReference SourceSkeleton; + SkinnedModel* SourceSkeleton; // The node-to-node mapping for the fast animation sampling for the skinned model skeleton nodes. Each item is index of the source skeleton node into target skeleton node. Span NodesMapping; }; @@ -115,7 +115,7 @@ private: Span NodesMapping; }; - Dictionary _skeletonMappingCache; + ConcurrentDictionary _skeletonMappingCache; public: /// From 9c32f978fb7f3ab2ce0ef3b7e550487ec052c1e1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 8 Feb 2026 00:22:37 +0100 Subject: [PATCH 087/100] Fix regression from 73c19b278f1ffbfd7cb76de53390733267d7ea69 to fix missing skeleton bones on start in Editor --- Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp index eaa4aa5a9..eb2ea0145 100644 --- a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp +++ b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp @@ -28,7 +28,7 @@ void SkinnedMeshDrawData::Setup(int32 bonesCount) BonesCount = bonesCount; _hasValidData = false; - _isDirty = false; + _isDirty = true; Data.Resize(BoneMatrices->GetSize()); SAFE_DELETE_GPU_RESOURCE(PrevBoneMatrices); } From 3d66316716307e8a8d9acb069b71e6a7c65297c1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 9 Feb 2026 15:03:42 +0100 Subject: [PATCH 088/100] Optimize animations retargeting between skeletons Cuts down `RetargetSkeletonPose` time down by over 80%. #3827 --- .../Animations/Graph/AnimGroup.Animation.cpp | 135 +++++++++--------- Source/Engine/Content/Assets/SkinnedModel.cpp | 2 + Source/Engine/Graphics/Models/SkeletonData.h | 9 ++ Source/Engine/Graphics/Models/SkinnedMesh.cpp | 36 +++++ 4 files changed, 116 insertions(+), 66 deletions(-) diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 08767728a..7eb8d32d6 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -109,86 +109,84 @@ namespace nodes->RootMotion.Orientation.Normalize(); } } - - Matrix ComputeWorldMatrixRecursive(const SkeletonData& skeleton, int32 index, Matrix localMatrix) - { - const auto& node = skeleton.Nodes[index]; - index = node.ParentIndex; - while (index != -1) - { - const auto& parent = skeleton.Nodes[index]; - localMatrix *= parent.LocalTransform.GetWorld(); - index = parent.ParentIndex; - } - return localMatrix; - } - - Matrix ComputeInverseParentMatrixRecursive(const SkeletonData& skeleton, int32 index) - { - Matrix inverseParentMatrix = Matrix::Identity; - const auto& node = skeleton.Nodes[index]; - if (node.ParentIndex != -1) - { - inverseParentMatrix = ComputeWorldMatrixRecursive(skeleton, index, inverseParentMatrix); - inverseParentMatrix = Matrix::Invert(inverseParentMatrix); - } - return inverseParentMatrix; - } } -void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping, Transform& node, int32 targetIndex) +// Utility for retargeting animation poses between skeletons. +struct Retargeting { - // sourceSkeleton - skeleton of Anim Graph (Base Locomotion pack) - // targetSkeleton - visual mesh skeleton (City Characters pack) - // target - anim graph input/output transformation of that node - const auto& targetNode = targetSkeleton.Nodes[targetIndex]; - const int32 sourceIndex = sourceMapping.NodesMapping[targetIndex]; - if (sourceIndex == -1) +private: + const Matrix* _sourcePosePtr, * _targetPosePtr; + const SkeletonData* _sourceSkeleton, *_targetSkeleton; + const SkinnedModel::SkeletonMapping* _sourceMapping; + +public: + void Init(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping) { - // Use T-pose - node = targetNode.LocalTransform; - return; + ASSERT_LOW_LAYER(targetSkeleton.Nodes.Count() == sourceMapping.NodesMapping.Length()); + + // Cache world-space poses for source and target skeletons to avoid redundant calculations during retargeting + _sourcePosePtr = sourceSkeleton.GetNodesPose().Get(); + _targetPosePtr = targetSkeleton.GetNodesPose().Get(); + + _sourceSkeleton = &sourceSkeleton; + _targetSkeleton = &targetSkeleton; + _sourceMapping = &sourceMapping; } - const auto& sourceNode = sourceSkeleton.Nodes[sourceIndex]; - // [Reference: https://wickedengine.net/2022/09/animation-retargeting/comment-page-1/] - - // Calculate T-Pose of source node, target node and target parent node - Matrix bindMatrix = ComputeWorldMatrixRecursive(sourceSkeleton, sourceIndex, sourceNode.LocalTransform.GetWorld()); - Matrix inverseBindMatrix = Matrix::Invert(bindMatrix); - Matrix targetMatrix = ComputeWorldMatrixRecursive(targetSkeleton, targetIndex, targetNode.LocalTransform.GetWorld()); - Matrix inverseParentMatrix = ComputeInverseParentMatrixRecursive(targetSkeleton, targetIndex); - - // Target node animation is world-space difference of the animated source node inside the target's parent node world-space - Matrix localMatrix = inverseBindMatrix * ComputeWorldMatrixRecursive(sourceSkeleton, sourceIndex, node.GetWorld()); - localMatrix = targetMatrix * localMatrix * inverseParentMatrix; - - // Extract local node transformation - localMatrix.Decompose(node); -} - -void RetargetSkeletonPose(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, const Transform* sourceNodes, Transform* targetNodes) -{ - // TODO: cache source and target skeletons world-space poses for faster retargeting (use some pooled memory) - ASSERT_LOW_LAYER(targetSkeleton.Nodes.Count() == mapping.NodesMapping.Length()); - for (int32 targetIndex = 0; targetIndex < targetSkeleton.Nodes.Count(); targetIndex++) + void RetargetNode(const Transform& source, Transform& target, int32 sourceIndex, int32 targetIndex) { - auto& targetNode = targetSkeleton.Nodes.Get()[targetIndex]; - const int32 sourceIndex = mapping.NodesMapping.Get()[targetIndex]; - Transform node; + // sourceSkeleton - skeleton of Anim Graph + // targetSkeleton - visual mesh skeleton + // target - anim graph input/output transformation of that node + const SkeletonNode& targetNode = _targetSkeleton->Nodes.Get()[targetIndex]; if (sourceIndex == -1) { // Use T-pose - node = targetNode.LocalTransform; + target = targetNode.LocalTransform; } else { - // Retarget - node = sourceNodes[sourceIndex]; - RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, targetIndex); + // [Reference: https://wickedengine.net/2022/09/animation-retargeting/comment-page-1/] + + // Calculate T-Pose of source node, target node and target parent node + const Matrix* sourcePosePtr = _sourcePosePtr; + const Matrix* targetPosePtr = _targetPosePtr; + const Matrix& bindMatrix = sourcePosePtr[sourceIndex]; + const Matrix& targetMatrix = targetPosePtr[targetIndex]; + Matrix inverseParentMatrix; + if (targetNode.ParentIndex != -1) + Matrix::Invert(targetPosePtr[targetNode.ParentIndex], inverseParentMatrix); + else + inverseParentMatrix = Matrix::Identity; + + // Target node animation is world-space difference of the animated source node inside the target's parent node world-space + const SkeletonNode& sourceNode = _sourceSkeleton->Nodes.Get()[sourceIndex]; + Matrix localMatrix = source.GetWorld(); + if (sourceNode.ParentIndex != -1) + localMatrix = localMatrix * sourcePosePtr[sourceNode.ParentIndex]; + localMatrix = Matrix::Invert(bindMatrix) * localMatrix; + localMatrix = targetMatrix * localMatrix * inverseParentMatrix; + + // Extract local node transformation + localMatrix.Decompose(target); } - targetNodes[targetIndex] = node; } + + FORCE_INLINE void RetargetPose(const Transform* sourceNodes, Transform* targetNodes) + { + for (int32 targetIndex = 0; targetIndex < _targetSkeleton->Nodes.Count(); targetIndex++) + { + const int32 sourceIndex = _sourceMapping->NodesMapping.Get()[targetIndex]; + RetargetNode(sourceNodes[sourceIndex], targetNodes[targetIndex], sourceIndex, targetIndex); + } + } +}; + +void RetargetSkeletonPose(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, const Transform* sourceNodes, Transform* targetNodes) +{ + Retargeting retargeting; + retargeting.Init(sourceSkeleton, targetSkeleton, mapping); + retargeting.RetargetPose(sourceNodes, targetNodes); } AnimGraphTraceEvent& AnimGraphContext::AddTraceEvent(const AnimGraphNode* node) @@ -431,9 +429,13 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* const bool weighted = weight < 1.0f; const bool retarget = mapping.SourceSkeleton && mapping.SourceSkeleton != mapping.TargetSkeleton; const auto emptyNodes = GetEmptyNodes(); + Retargeting retargeting; SkinnedModel::SkeletonMapping sourceMapping; if (retarget) + { sourceMapping = _graph.BaseModel->GetSkeletonMapping(mapping.SourceSkeleton); + retargeting.Init(mapping.SourceSkeleton->Skeleton, mapping.TargetSkeleton->Skeleton, mapping); + } for (int32 nodeIndex = 0; nodeIndex < nodes->Nodes.Count(); nodeIndex++) { const int32 nodeToChannel = mapping.NodesMapping[nodeIndex]; @@ -447,7 +449,8 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode* // Optionally retarget animation into the skeleton used by the Anim Graph if (retarget) { - RetargetSkeletonNode(mapping.SourceSkeleton->Skeleton, mapping.TargetSkeleton->Skeleton, sourceMapping, srcNode, nodeIndex); + const int32 sourceIndex = sourceMapping.NodesMapping[nodeIndex]; + retargeting.RetargetNode(srcNode, srcNode, sourceIndex, nodeIndex); } // Mark node as used diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index ce6300b17..c0355ea0e 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -378,6 +378,7 @@ bool SkinnedModel::SetupSkeleton(const Array& nodes) model->Skeleton.Bones[i].LocalTransform = node.LocalTransform; model->Skeleton.Bones[i].NodeIndex = i; } + model->Skeleton.Dirty(); ClearSkeletonMapping(); // Calculate offset matrix (inverse bind pose transform) for every bone manually @@ -435,6 +436,7 @@ bool SkinnedModel::SetupSkeleton(const Array& nodes, const ArraySkeleton.Nodes = nodes; model->Skeleton.Bones = bones; + model->Skeleton.Dirty(); ClearSkeletonMapping(); // Calculate offset matrix (inverse bind pose transform) for every bone manually diff --git a/Source/Engine/Graphics/Models/SkeletonData.h b/Source/Engine/Graphics/Models/SkeletonData.h index 0b6c7d4d7..79e0be512 100644 --- a/Source/Engine/Graphics/Models/SkeletonData.h +++ b/Source/Engine/Graphics/Models/SkeletonData.h @@ -73,6 +73,10 @@ struct TIsPODType /// class FLAXENGINE_API SkeletonData { +private: + mutable volatile int64 _dirty = 1; + mutable Array _cachedPose; + public: /// /// The nodes in this hierarchy. The root node is always at the index 0. @@ -114,6 +118,11 @@ public: int32 FindNode(const StringView& name) const; int32 FindBone(int32 nodeIndex) const; + // Gets the skeleton nodes transforms in mesh space (pose). Calculated from the local node transforms and hierarchy. Cached internally and updated when data is dirty. + const Array& GetNodesPose() const; + + // Marks data as dirty (modified) to update internal state and recalculate cached data if needed (eg. skeleton pose). + void Dirty(); uint64 GetMemoryUsage() const; /// diff --git a/Source/Engine/Graphics/Models/SkinnedMesh.cpp b/Source/Engine/Graphics/Models/SkinnedMesh.cpp index 66b3e5701..369e5d825 100644 --- a/Source/Engine/Graphics/Models/SkinnedMesh.cpp +++ b/Source/Engine/Graphics/Models/SkinnedMesh.cpp @@ -154,6 +154,8 @@ void SkeletonData::Swap(SkeletonData& other) { Nodes.Swap(other.Nodes); Bones.Swap(other.Bones); + Dirty(); + other.Dirty(); } Transform SkeletonData::GetNodeTransform(int32 nodeIndex) const @@ -171,6 +173,7 @@ Transform SkeletonData::GetNodeTransform(int32 nodeIndex) const void SkeletonData::SetNodeTransform(int32 nodeIndex, const Transform& value) { CHECK(Nodes.IsValidIndex(nodeIndex)); + Dirty(); const int32 parentIndex = Nodes[nodeIndex].ParentIndex; if (parentIndex == -1) { @@ -201,6 +204,39 @@ int32 SkeletonData::FindBone(int32 nodeIndex) const return -1; } +const Array& SkeletonData::GetNodesPose() const +{ + // Guard with a simple atomic flag to avoid locking if the pose is up to date + if (Platform::AtomicRead(&_dirty)) + { + ScopeLock lock(RenderContext::GPULocker); + if (Platform::AtomicRead(&_dirty)) + { + Platform::AtomicStore(&_dirty, 0); + const SkeletonNode* nodes = Nodes.Get(); + const int32 nodesCount = Nodes.Count(); + _cachedPose.Resize(nodesCount); + Matrix* posePtr = _cachedPose.Get(); + for (int32 nodeIndex = 0; nodeIndex < nodesCount; nodeIndex++) + { + const SkeletonNode& node = nodes[nodeIndex]; + Matrix local; + Matrix::Transformation(node.LocalTransform.Scale, node.LocalTransform.Orientation, node.LocalTransform.Translation, local); + if (node.ParentIndex != -1) + Matrix::Multiply(local, posePtr[node.ParentIndex], posePtr[nodeIndex]); + else + posePtr[nodeIndex] = local; + } + } + } + return _cachedPose; +} + +void SkeletonData::Dirty() +{ + Platform::AtomicStore(&_dirty, 1); +} + uint64 SkeletonData::GetMemoryUsage() const { uint64 result = Nodes.Capacity() * sizeof(SkeletonNode) + Bones.Capacity() * sizeof(SkeletonBone); From a2b0d0714e89cfb07647211134b404d1b6a217d0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 9 Feb 2026 15:03:54 +0100 Subject: [PATCH 089/100] Add more docs about new `ConcurrentDictionary` --- Source/Engine/Threading/ConcurrentDictionary.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Threading/ConcurrentDictionary.h b/Source/Engine/Threading/ConcurrentDictionary.h index 22395d798..1b78a735e 100644 --- a/Source/Engine/Threading/ConcurrentDictionary.h +++ b/Source/Engine/Threading/ConcurrentDictionary.h @@ -7,6 +7,8 @@ /// /// Template for unordered dictionary with mapped key with value pairs that supports asynchronous data reading and writing. +/// Implemented via reader-writer lock pattern, so multiple threads can read data at the same time, but only one thread can write data and it blocks all other threads (including readers) until the write operation is finished. +/// Optimized for frequent reads (no lock operation). /// /// The type of the keys in the dictionary. /// The type of the values in the dictionary. From bd300651ecc61b4f94500658b0fbdce223a53dfe Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 9 Feb 2026 15:04:19 +0100 Subject: [PATCH 090/100] Fix Job System regression bug with incorrect initialization of job context --- Source/Engine/Threading/JobSystem.cpp | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Source/Engine/Threading/JobSystem.cpp b/Source/Engine/Threading/JobSystem.cpp index 4d90f1c06..8d62aa8e3 100644 --- a/Source/Engine/Threading/JobSystem.cpp +++ b/Source/Engine/Threading/JobSystem.cpp @@ -38,29 +38,23 @@ public: struct alignas(int64) JobContext { // The next index of the job to process updated when picking a job by the thread. - volatile int64 JobIndex; + volatile int64 JobIndex = 0; // The number of jobs left to process updated after job completion by the thread. - volatile int64 JobsLeft; + volatile int64 JobsLeft = 0; // The unique label of this job used to identify it. Set to -1 when job is done. - volatile int64 JobLabel; + volatile int64 JobLabel = 0; // Utility atomic counter used to indicate that any job is waiting for this one to finish. Then Dependants can be accessed within thread-safe JobsLocker. - volatile int64 DependantsCount; + volatile int64 DependantsCount = 0; // The number of dependency jobs left to be finished before starting this job. - volatile int64 DependenciesLeft; + volatile int64 DependenciesLeft = 0; // The total number of jobs to process (in this context). - int32 JobsCount; + int32 JobsCount = 0; // The job function to execute. Function Job; // List of dependant jobs to signal when this job is done. Array Dependants; }; -template<> -struct TIsPODType -{ - enum { Value = false }; -}; - class JobSystemThread : public IRunnable { public: @@ -111,7 +105,7 @@ bool JobSystemService::Init() JobContextsSize = 256; JobContextsMask = JobContextsSize - 1; JobContexts = (JobContext*)Platform::Allocate(JobContextsSize * sizeof(JobContext), alignof(JobContext)); - Platform::MemoryClear(JobContexts, sizeof(JobContextsSize * sizeof(JobContext))); + Memory::ConstructItems(JobContexts, (int32)JobContextsSize); // Spawn threads ThreadsCount = Math::Min(Platform::GetCPUInfo().LogicalProcessorCount, ARRAY_COUNT(Threads)); @@ -150,6 +144,7 @@ void JobSystemService::Dispose() } } + Memory::DestructItems(JobContexts, (int32)JobContextsSize); Platform::Free(JobContexts); JobContexts = nullptr; } From 7b7a92758ff1a6f040c535b8e5a8858dd36d7380 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 9 Feb 2026 18:01:47 +0100 Subject: [PATCH 091/100] Optimize `VariantType` to use static type name in game or from non-reloadable assemblies This avoids many dynamic memory allocations in Visual Scripts and Anim Graph. # --- Source/Engine/Core/Types/Variant.cpp | 160 +++++++++++++----- Source/Engine/Core/Types/Variant.h | 36 +++- Source/Engine/Scripting/BinaryModule.cpp | 2 + Source/Engine/Scripting/BinaryModule.h | 5 + .../Engine/Scripting/ManagedCLR/MAssembly.h | 9 + Source/Engine/Scripting/ManagedCLR/MCore.cpp | 2 + Source/Engine/Scripting/Runtime/DotNet.cpp | 1 + Source/Engine/Scripting/Scripting.cpp | 4 + Source/Engine/Serialization/Serialization.cpp | 3 + Source/Engine/Serialization/Stream.cpp | 1 + 10 files changed, 170 insertions(+), 53 deletions(-) diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index 4ab8552d3..bd2d594df 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -18,8 +18,10 @@ #include "Engine/Core/Math/Ray.h" #include "Engine/Core/Math/Rectangle.h" #include "Engine/Core/Math/Transform.h" +#include "Engine/Scripting/BinaryModule.h" #include "Engine/Scripting/Scripting.h" #include "Engine/Scripting/ScriptingObject.h" +#include "Engine/Scripting/ManagedCLR/MAssembly.h" #include "Engine/Scripting/ManagedCLR/MClass.h" #include "Engine/Scripting/ManagedCLR/MCore.h" #include "Engine/Scripting/ManagedCLR/MUtils.h" @@ -33,6 +35,13 @@ #endif #define AsEnum AsUint64 +// Editor can hot-reload assemblies thus cached type names may become invalid, otherwise use modules that are never unloaded and their type names are always valid +#if USE_EDITOR +#define IS_VARIANT_TYPE_NAME_STATIC(canReload) !canReload +#else +#define IS_VARIANT_TYPE_NAME_STATIC(canReload) true +#endif + namespace { const char* InBuiltTypesTypeNames[40] = @@ -88,6 +97,7 @@ static_assert((int32)VariantType::Types::MAX == ARRAY_COUNT(InBuiltTypesTypeName VariantType::VariantType(Types type, const StringView& typeName) { Type = type; + StaticName = 0; TypeName = nullptr; const int32 length = typeName.Length(); if (length) @@ -98,32 +108,41 @@ VariantType::VariantType(Types type, const StringView& typeName) } } -VariantType::VariantType(Types type, const StringAnsiView& typeName) +VariantType::VariantType(Types type, const StringAnsiView& typeName, bool staticName) { Type = type; - TypeName = nullptr; - int32 length = typeName.Length(); - if (length) + StaticName = staticName && (typeName.HasChars() && typeName[typeName.Length()] == 0); // Require string to be null-terminated (not fully safe check) + if (staticName) { - TypeName = static_cast(Allocator::Allocate(length + 1)); - Platform::MemoryCopy(TypeName, typeName.Get(), length); - TypeName[length] = 0; + TypeName = (char*)typeName.Get(); } + else + { + TypeName = nullptr; + int32 length = typeName.Length(); + if (length) + { + TypeName = static_cast(Allocator::Allocate(length + 1)); + Platform::MemoryCopy(TypeName, typeName.Get(), length); + TypeName[length] = 0; + } + } +} + +VariantType::VariantType(Types type, const ScriptingType& sType) + : VariantType(type) +{ + SetTypeName(sType); } VariantType::VariantType(Types type, const MClass* klass) { Type = type; + StaticName = false; TypeName = nullptr; #if USE_CSHARP if (klass) - { - const StringAnsiView typeName = klass->GetFullName(); - const int32 length = typeName.Length(); - TypeName = static_cast(Allocator::Allocate(length + 1)); - Platform::MemoryCopy(TypeName, typeName.Get(), length); - TypeName[length] = 0; - } + SetTypeName(*klass); #endif } @@ -190,9 +209,9 @@ VariantType::VariantType(const StringAnsiView& typeName) if (const auto mclass = Scripting::FindClass(typeName)) { if (mclass->IsEnum()) - new(this) VariantType(Enum, typeName); + new(this) VariantType(Enum, mclass); else - new(this) VariantType(ManagedObject, typeName); + new(this) VariantType(ManagedObject, mclass); return; } #endif @@ -204,36 +223,48 @@ VariantType::VariantType(const StringAnsiView& typeName) VariantType::VariantType(const VariantType& other) { Type = other.Type; - TypeName = nullptr; - const int32 length = StringUtils::Length(other.TypeName); - if (length) + StaticName = other.StaticName; + if (StaticName) { - TypeName = static_cast(Allocator::Allocate(length + 1)); - Platform::MemoryCopy(TypeName, other.TypeName, length); - TypeName[length] = 0; + TypeName = other.TypeName; + } + else + { + TypeName = nullptr; + const int32 length = StringUtils::Length(other.TypeName); + if (length) + { + TypeName = static_cast(Allocator::Allocate(length + 1)); + Platform::MemoryCopy(TypeName, other.TypeName, length); + TypeName[length] = 0; + } } } VariantType::VariantType(VariantType&& other) noexcept { Type = other.Type; + StaticName = other.StaticName; TypeName = other.TypeName; other.Type = Null; other.TypeName = nullptr; + other.StaticName = 0; } VariantType& VariantType::operator=(const Types& type) { Type = type; - Allocator::Free(TypeName); + if (StaticName) + Allocator::Free(TypeName); TypeName = nullptr; + StaticName = 0; return *this; } VariantType& VariantType::operator=(VariantType&& other) { ASSERT(this != &other); - Swap(Type, other.Type); + Swap(Packed, other.Packed); Swap(TypeName, other.TypeName); return *this; } @@ -242,14 +273,23 @@ VariantType& VariantType::operator=(const VariantType& other) { ASSERT(this != &other); Type = other.Type; - Allocator::Free(TypeName); - TypeName = nullptr; - const int32 length = StringUtils::Length(other.TypeName); - if (length) + if (StaticName) + Allocator::Free(TypeName); + StaticName = other.StaticName; + if (StaticName) { - TypeName = static_cast(Allocator::Allocate(length + 1)); - Platform::MemoryCopy(TypeName, other.TypeName, length); - TypeName[length] = 0; + TypeName = other.TypeName; + } + else + { + TypeName = nullptr; + const int32 length = StringUtils::Length(other.TypeName); + if (length) + { + TypeName = static_cast(Allocator::Allocate(length + 1)); + Platform::MemoryCopy(TypeName, other.TypeName, length); + TypeName[length] = 0; + } } return *this; } @@ -283,24 +323,45 @@ void VariantType::SetTypeName(const StringView& typeName) { if (StringUtils::Length(TypeName) != typeName.Length()) { - Allocator::Free(TypeName); + if (StaticName) + Allocator::Free(TypeName); + StaticName = 0; TypeName = static_cast(Allocator::Allocate(typeName.Length() + 1)); TypeName[typeName.Length()] = 0; } StringUtils::ConvertUTF162ANSI(typeName.Get(), TypeName, typeName.Length()); } -void VariantType::SetTypeName(const StringAnsiView& typeName) +void VariantType::SetTypeName(const StringAnsiView& typeName, bool staticName) { - if (StringUtils::Length(TypeName) != typeName.Length()) + if (StringUtils::Length(TypeName) != typeName.Length() || StaticName != staticName) { - Allocator::Free(TypeName); + if (StaticName) + Allocator::Free(TypeName); + StaticName = staticName; + if (staticName) + { + TypeName = (char*)typeName.Get(); + return; + } TypeName = static_cast(Allocator::Allocate(typeName.Length() + 1)); TypeName[typeName.Length()] = 0; } Platform::MemoryCopy(TypeName, typeName.Get(), typeName.Length()); } +void VariantType::SetTypeName(const ScriptingType& type) +{ + SetTypeName(type.Fullname, IS_VARIANT_TYPE_NAME_STATIC(type.Module->CanReload)); +} + +void VariantType::SetTypeName(const MClass& klass) +{ +#if USE_CSHARP + SetTypeName(klass.GetFullName(), IS_VARIANT_TYPE_NAME_STATIC(klass.GetAssembly()->CanReload())); +#endif +} + const char* VariantType::GetTypeName() const { if (TypeName) @@ -322,6 +383,17 @@ VariantType VariantType::GetElementType() const return VariantType(); } +void VariantType::Inline() +{ + const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(TypeName); + if (typeHandle) + SetTypeName(typeHandle.GetType()); +#if USE_CSHARP + else if (const auto mclass = Scripting::FindClass(TypeName)) + SetTypeName(*mclass); +#endif +} + ::String VariantType::ToString() const { ::String result; @@ -632,8 +704,7 @@ Variant::Variant(ScriptingObject* v) AsObject = v; if (v) { - // TODO: optimize VariantType to support statically linked typename of ScriptingType (via 1 bit flag within Types enum, only in game as editor might hot-reload types) - Type.SetTypeName(v->GetType().Fullname); + Type.SetTypeName(v->GetType()); v->Deleted.Bind(this); } } @@ -644,9 +715,8 @@ Variant::Variant(Asset* v) AsAsset = v; if (v) { - // TODO: optimize VariantType to support statically linked typename of ScriptingType (via 1 bit flag within Types enum, only in game as editor might hot-reload types) - Type.SetTypeName(v->GetType().Fullname); v->AddReference(); + Type.SetTypeName(v->GetType()); v->OnUnloaded.Bind(this); } } @@ -3007,16 +3077,16 @@ Variant Variant::NewValue(const StringAnsiView& typeName) switch (type.Type) { case ScriptingTypes::Script: - v.SetType(VariantType(VariantType::Object, typeName)); + v.SetType(VariantType(VariantType::Object, type)); v.AsObject = type.Script.Spawn(ScriptingObjectSpawnParams(Guid::New(), typeHandle)); if (v.AsObject) v.AsObject->Deleted.Bind(&v); break; case ScriptingTypes::Structure: - v.SetType(VariantType(VariantType::Structure, typeName)); + v.SetType(VariantType(VariantType::Structure, type)); break; case ScriptingTypes::Enum: - v.SetType(VariantType(VariantType::Enum, typeName)); + v.SetType(VariantType(VariantType::Enum, type)); v.AsEnum = 0; break; default: @@ -3030,16 +3100,16 @@ Variant Variant::NewValue(const StringAnsiView& typeName) // Fallback to C#-only types if (mclass->IsEnum()) { - v.SetType(VariantType(VariantType::Enum, typeName)); + v.SetType(VariantType(VariantType::Enum, mclass)); v.AsEnum = 0; } else if (mclass->IsValueType()) { - v.SetType(VariantType(VariantType::Structure, typeName)); + v.SetType(VariantType(VariantType::Structure, mclass)); } else { - v.SetType(VariantType(VariantType::ManagedObject, typeName)); + v.SetType(VariantType(VariantType::ManagedObject, mclass)); MObject* instance = mclass->CreateInstance(); if (instance) { diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 4fd6ab2eb..5c057bc65 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -17,7 +17,7 @@ struct ScriptingTypeHandle; /// API_STRUCT(InBuild) struct FLAXENGINE_API VariantType { - enum Types + enum Types : uint8 { Null = 0, Void, @@ -80,10 +80,22 @@ API_STRUCT(InBuild) struct FLAXENGINE_API VariantType }; public: - /// - /// The type of the variant. - /// - Types Type; + union + { + struct + { + /// + /// The type of the variant. + /// + Types Type; + + /// + /// Internal flag used to indicate that pointer to TypeName has been linked from a static/external memory that is stable (eg. ScriptingType or MClass). Allows avoiding dynamic memory allocation. + /// + uint8 StaticName : 1; + }; + uint16 Packed; + }; /// /// The optional additional full name of the scripting type. Used for Asset, Object, Enum, Structure types to describe type precisely. @@ -94,17 +106,20 @@ public: FORCE_INLINE VariantType() { Type = Null; + StaticName = 0; TypeName = nullptr; } FORCE_INLINE explicit VariantType(Types type) { Type = type; + StaticName = 0; TypeName = nullptr; } explicit VariantType(Types type, const StringView& typeName); - explicit VariantType(Types type, const StringAnsiView& typeName); + explicit VariantType(Types type, const StringAnsiView& typeName, bool staticName = false); + explicit VariantType(Types type, const ScriptingType& sType); explicit VariantType(Types type, const MClass* klass); explicit VariantType(const StringAnsiView& typeName); VariantType(const VariantType& other); @@ -112,7 +127,8 @@ public: FORCE_INLINE ~VariantType() { - Allocator::Free(TypeName); + if (!StaticName) + Allocator::Free(TypeName); } public: @@ -130,9 +146,13 @@ public: public: void SetTypeName(const StringView& typeName); - void SetTypeName(const StringAnsiView& typeName); + void SetTypeName(const StringAnsiView& typeName, bool staticName = false); + void SetTypeName(const ScriptingType& type); + void SetTypeName(const MClass& klass); const char* GetTypeName() const; VariantType GetElementType() const; + // Drops custom type name into the name allocated by the scripting module to reduce memory allocations when referencing types. + void Inline(); ::String ToString() const; }; diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index 4d26e678b..bbcd7de57 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -683,6 +683,8 @@ BinaryModule* BinaryModule::GetModule(const StringAnsiView& name) BinaryModule::BinaryModule() { + CanReload = USE_EDITOR; + // Register GetModules().Add(this); } diff --git a/Source/Engine/Scripting/BinaryModule.h b/Source/Engine/Scripting/BinaryModule.h index 70aa60fff..1da35401b 100644 --- a/Source/Engine/Scripting/BinaryModule.h +++ b/Source/Engine/Scripting/BinaryModule.h @@ -91,6 +91,11 @@ public: /// Dictionary TypeNameToTypeIndex; + /// + /// Determinates whether module can be hot-reloaded at runtime. For example, in Editor after scripts recompilation. Some modules such as engine and class library modules are static. + /// + bool CanReload; + public: /// diff --git a/Source/Engine/Scripting/ManagedCLR/MAssembly.h b/Source/Engine/Scripting/ManagedCLR/MAssembly.h index 6c0aa9579..0a785c06a 100644 --- a/Source/Engine/Scripting/ManagedCLR/MAssembly.h +++ b/Source/Engine/Scripting/ManagedCLR/MAssembly.h @@ -34,6 +34,7 @@ private: int32 _isLoaded : 1; int32 _isLoading : 1; + int32 _canReload : 1; mutable int32 _hasCachedClasses : 1; mutable ClassesDictionary _classes; @@ -125,6 +126,14 @@ public: return _isLoaded != 0; } + /// + /// Returns true if assembly can be hot-reloaded at runtime. For example, in Editor after scripts recompilation. Some assemblies such as engine and class library modules are static. + /// + FORCE_INLINE bool CanReload() const + { + return USE_EDITOR && _canReload; + } + /// /// Gets the assembly name. /// diff --git a/Source/Engine/Scripting/ManagedCLR/MCore.cpp b/Source/Engine/Scripting/ManagedCLR/MCore.cpp index 350cc39d2..6fa499002 100644 --- a/Source/Engine/Scripting/ManagedCLR/MCore.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MCore.cpp @@ -45,6 +45,7 @@ MAssembly::MAssembly(MDomain* domain, const StringAnsiView& name) : _domain(domain) , _isLoaded(false) , _isLoading(false) + , _canReload(true) , _hasCachedClasses(false) , _reloadCount(0) , _name(name) @@ -59,6 +60,7 @@ MAssembly::MAssembly(MDomain* domain, const StringAnsiView& name, const StringAn , _domain(domain) , _isLoaded(false) , _isLoading(false) + , _canReload(true) , _hasCachedClasses(false) , _reloadCount(0) , _name(name) diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 1c8c2bcdd..4be0ce1a1 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -874,6 +874,7 @@ bool MAssembly::LoadCorlib() return true; } _hasCachedClasses = false; + _canReload = false; CachedAssemblyHandles.Add(_handle, this); // End diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp index 4e17bb80a..aa7e26674 100644 --- a/Source/Engine/Scripting/Scripting.cpp +++ b/Source/Engine/Scripting/Scripting.cpp @@ -502,6 +502,7 @@ bool Scripting::LoadBinaryModules(const String& path, const String& projectFolde // C# if (managedPath.HasChars() && !((ManagedBinaryModule*)module)->Assembly->IsLoaded()) { + (((ManagedBinaryModule*)module)->Assembly)->_canReload = module->CanReload; if (((ManagedBinaryModule*)module)->Assembly->Load(managedPath, nativePath)) { LOG(Error, "Failed to load C# assembly '{0}' for binary module {1}.", managedPath, name); @@ -528,6 +529,7 @@ bool Scripting::Load() #if USE_CSHARP // Load C# core assembly ManagedBinaryModule* corlib = GetBinaryModuleCorlib(); + corlib->CanReload = false; if (corlib->Assembly->LoadCorlib()) { LOG(Error, "Failed to load corlib C# assembly."); @@ -581,6 +583,8 @@ bool Scripting::Load() LOG(Error, "Failed to load FlaxEngine C# assembly."); return true; } + flaxEngineModule->CanReload = false; + flaxEngineModule->Assembly->_canReload = false; onEngineLoaded(flaxEngineModule->Assembly); // Insert type aliases for vector types that don't exist in C++ but are just typedef (properly redirect them to actual types) diff --git a/Source/Engine/Serialization/Serialization.cpp b/Source/Engine/Serialization/Serialization.cpp index a3dfc6ffa..1eb6b0181 100644 --- a/Source/Engine/Serialization/Serialization.cpp +++ b/Source/Engine/Serialization/Serialization.cpp @@ -78,7 +78,10 @@ void Serialization::Deserialize(ISerializable::DeserializeStream& stream, Varian v.Type = VariantType::Null; const auto mTypeName = SERIALIZE_FIND_MEMBER(stream, "TypeName"); if (mTypeName != stream.MemberEnd() && mTypeName->value.IsString()) + { v.SetTypeName(StringAnsiView(mTypeName->value.GetStringAnsiView())); + v.Inline(); + } } else { diff --git a/Source/Engine/Serialization/Stream.cpp b/Source/Engine/Serialization/Stream.cpp index f95e9ef9b..4c9b94042 100644 --- a/Source/Engine/Serialization/Stream.cpp +++ b/Source/Engine/Serialization/Stream.cpp @@ -255,6 +255,7 @@ void ReadStream::Read(VariantType& data) ptr++; } *ptr = 0; + data.Inline(); } else if (typeNameLength > 0) { From a1399c51575cd98a2a90b5f4eab7320b22bf7d35 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 9 Feb 2026 18:02:58 +0100 Subject: [PATCH 092/100] Optimize Anim Graph retarget to use cached pose to avoid dynamic memory allocation #3827 --- Source/Engine/Animations/Graph/AnimGraph.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp index e99f53b8f..3c2630b51 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.cpp @@ -336,11 +336,13 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt) SkeletonData* animResultSkeleton = &skeleton; // Retarget animation when using output pose from other skeleton - AnimGraphImpulse retargetNodes; if (_graph.BaseModel != data.NodesSkeleton) { ANIM_GRAPH_PROFILE_EVENT("Retarget"); auto& targetSkeleton = data.NodesSkeleton->Skeleton; + if (context.PoseCacheSize == context.PoseCache.Count()) + context.PoseCache.AddOne(); + auto& retargetNodes = context.PoseCache[context.PoseCacheSize++]; retargetNodes = *animResult; retargetNodes.Nodes.Resize(targetSkeleton.Nodes.Count()); Transform* targetNodes = retargetNodes.Nodes.Get(); From d2ee61ef8ddd55685b03ceac7486d446cf9230c5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 9 Feb 2026 18:06:57 +0100 Subject: [PATCH 093/100] Fix `GetNodesPose` issue when 2 threads call it at once --- Source/Engine/Graphics/Models/SkinnedMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Graphics/Models/SkinnedMesh.cpp b/Source/Engine/Graphics/Models/SkinnedMesh.cpp index 369e5d825..0377003be 100644 --- a/Source/Engine/Graphics/Models/SkinnedMesh.cpp +++ b/Source/Engine/Graphics/Models/SkinnedMesh.cpp @@ -212,7 +212,6 @@ const Array& SkeletonData::GetNodesPose() const ScopeLock lock(RenderContext::GPULocker); if (Platform::AtomicRead(&_dirty)) { - Platform::AtomicStore(&_dirty, 0); const SkeletonNode* nodes = Nodes.Get(); const int32 nodesCount = Nodes.Count(); _cachedPose.Resize(nodesCount); @@ -227,6 +226,7 @@ const Array& SkeletonData::GetNodesPose() const else posePtr[nodeIndex] = local; } + Platform::AtomicStore(&_dirty, 0); } } return _cachedPose; From 0f6c1aea629cbe60468209a4b6c11e8f1a7af53d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 9 Feb 2026 18:40:39 +0100 Subject: [PATCH 094/100] Fix various material nodes to work on D3D12 --- .../MaterialGenerator.Material.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index 09ac78e6b..f3d830382 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -384,7 +384,7 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) // Apply hardness, use 0.991 as max since any value above will result in harsh aliasing auto x2 = writeLocal(ValueType::Float, String::Format(TEXT("saturate((1 - {0}) * (1 / (1 - clamp({1}, 0, 0.991f))))"), x1.Value, hardness.Value), node); - value = writeLocal(ValueType::Float, String::Format(TEXT("{0} ? (1 - {1}) : {1}"), invert.Value, x2.Value), node); + value = writeLocal(ValueType::Float, String::Format(TEXT("select({0}, (1 - {1}), {1})"), invert.Value, x2.Value), node); break; } // Tiling & Offset @@ -459,7 +459,7 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) auto x = writeLocal(ValueType::Float, String::Format(TEXT("56100000.0f * pow({0}, -1) + 148.0f"), temperature.Value), node); // Value Y - auto y = writeLocal(ValueType::Float, String::Format(TEXT("{0} > 6500.0f ? 35200000.0f * pow({0}, -1) + 184.0f : 100.04f * log({0}) - 623.6f"), temperature.Value), node); + auto y = writeLocal(ValueType::Float, String::Format(TEXT("select({0} > 6500.0f, 35200000.0f * pow({0}, -1) + 184.0f, 100.04f * log({0}) - 623.6f)"), temperature.Value), node); // Value Z auto z = writeLocal(ValueType::Float, String::Format(TEXT("194.18f * log({0}) - 1448.6f"), temperature.Value), node); @@ -467,7 +467,7 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) // Final color auto color = writeLocal(ValueType::Float3, String::Format(TEXT("float3({0}, {1}, {2})"), x.Value, y.Value, z.Value), node); color = writeLocal(ValueType::Float3, String::Format(TEXT("clamp({0}, 0.0f, 255.0f) / 255.0f"), color.Value), node); - value = writeLocal(ValueType::Float3, String::Format(TEXT("{1} < 1000.0f ? {0} * {1}/1000.0f : {0}"), color.Value, temperature.Value), node); + value = writeLocal(ValueType::Float3, String::Format(TEXT("select({1} < 1000.0f, {0} * {1}/1000.0f, {0})"), color.Value, temperature.Value), node); break; } // HSVToRGB @@ -490,8 +490,8 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) const auto rgb = tryGetValue(node->GetBox(0), node->Values[0]).AsFloat3(); const auto epsilon = writeLocal(ValueType::Float, TEXT("1e-10"), node); - auto p = writeLocal(ValueType::Float4, String::Format(TEXT("({0}.g < {0}.b) ? float4({0}.bg, -1.0f, 2.0f/3.0f) : float4({0}.gb, 0.0f, -1.0f/3.0f)"), rgb.Value), node); - auto q = writeLocal(ValueType::Float4, String::Format(TEXT("({0}.r < {1}.x) ? float4({1}.xyw, {0}.r) : float4({0}.r, {1}.yzx)"), rgb.Value, p.Value), node); + auto p = writeLocal(ValueType::Float4, String::Format(TEXT("select(({0}.g < {0}.b), float4({0}.bg, -1.0f, 2.0f/3.0f), float4({0}.gb, 0.0f, -1.0f/3.0f))"), rgb.Value), node); + auto q = writeLocal(ValueType::Float4, String::Format(TEXT("select(({0}.r < {1}.x), float4({1}.xyw, {0}.r), float4({0}.r, {1}.yzx))"), rgb.Value, p.Value), node); auto c = writeLocal(ValueType::Float, String::Format(TEXT("{0}.x - min({0}.w, {0}.y)"), q.Value), node); auto h = writeLocal(ValueType::Float, String::Format(TEXT("abs(({0}.w - {0}.y) / (6 * {1} + {2}) + {0}.z)"), q.Value, c.Value, epsilon.Value), node); @@ -721,13 +721,13 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) blendFormula = TEXT("1.0 - (1.0 - base) * (1.0 - blend)"); break; case 5: // Overlay - blendFormula = TEXT("base <= 0.5 ? 2.0 * base * blend : 1.0 - 2.0 * (1.0 - base) * (1.0 - blend)"); + blendFormula = TEXT("select(base <= 0.5, 2.0 * base * blend, 1.0 - 2.0 * (1.0 - base) * (1.0 - blend))"); break; case 6: // Linear Burn blendFormula = TEXT("base + blend - 1.0"); break; case 7: // Linear Light - blendFormula = TEXT("blend < 0.5 ? max(base + (2.0 * blend) - 1.0, 0.0) : min(base + 2.0 * (blend - 0.5), 1.0)"); + blendFormula = TEXT("select(blend < 0.5, max(base + (2.0 * blend) - 1.0, 0.0), min(base + 2.0 * (blend - 0.5), 1.0))"); break; case 8: // Darken blendFormula = TEXT("min(base, blend)"); @@ -745,10 +745,10 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) blendFormula = TEXT("base / (blend + 0.000001)"); break; case 13: // Hard Light - blendFormula = TEXT("blend <= 0.5 ? 2.0 * base * blend : 1.0 - 2.0 * (1.0 - base) * (1.0 - blend)"); + blendFormula = TEXT("select(blend <= 0.5, 2.0 * base * blend, 1.0 - 2.0 * (1.0 - base) * (1.0 - blend))"); break; case 14: // Pin Light - blendFormula = TEXT("blend <= 0.5 ? min(base, 2.0 * blend) : max(base, 2.0 * (blend - 0.5))"); + blendFormula = TEXT("select(blend <= 0.5, min(base, 2.0 * blend), max(base, 2.0 * (blend - 0.5)))"); break; case 15: // Hard Mix blendFormula = TEXT("step(1.0 - base, blend)"); From 55f73b6cf7acf55613d52ad528db70eb69f03eff Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 9 Feb 2026 23:03:25 +0100 Subject: [PATCH 095/100] Optimize Animated Models bones updating with a batches memory pass and manual resource transitions batch #3917 #3827 --- Source/Engine/Level/Actors/AnimatedModel.cpp | 83 +++++++++++++++++--- Source/Engine/Renderer/RenderList.cpp | 31 ++++++++ Source/Engine/Renderer/RenderList.h | 19 +++++ Source/Engine/Renderer/Renderer.cpp | 2 + 4 files changed, 125 insertions(+), 10 deletions(-) diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index f6ac51f1c..11497e558 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -14,15 +14,84 @@ #include "Engine/Content/Deprecated.h" #include "Engine/Graphics/GPUContext.h" #include "Engine/Graphics/GPUDevice.h" +#include "Engine/Graphics/GPUPass.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/Models/MeshAccessor.h" #include "Engine/Graphics/Models/MeshDeformation.h" +#include "Engine/Renderer/RenderList.h" #include "Engine/Level/Scene/Scene.h" #include "Engine/Level/SceneObjectsFactory.h" -#include "Engine/Profiler/ProfilerMemory.h" -#include "Engine/Renderer/RenderList.h" +#include "Engine/Profiler/Profiler.h" #include "Engine/Serialization/Serialization.h" +// Implements efficient skinning data update within a shared GPUMemoryPass with manual resource transitions batched for all animated models. +class AnimatedModelRenderListExtension : public RenderList::IExtension +{ +public: + struct Item + { + GPUBuffer* BoneMatrices; + void* Data; + int32 Size; + }; + + RenderListBuffer Items; + + void PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch) override + { + Items.Clear(); + } + + void PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch) override + { + const int32 count = Items.Count(); + if (count == 0) + return; + PROFILE_GPU_CPU_NAMED("Update Bones"); + GPUMemoryPass pass(context); + Item* items = Items.Get(); + + // Special case for D3D11 backend that doesn't need transitions + if (context->GetDevice()->GetRendererType() <= RendererType::DirectX11) + { + for (int32 i = 0; i < count; i++) + { + Item& item = items[i]; + context->UpdateBuffer(item.BoneMatrices, item.Data, item.Size); + } + } + else + { + // Batch resource barriers for buffer update + for (int32 i = 0; i < count; i++) + pass.Transition(items[i].BoneMatrices, GPUResourceAccess::CopyWrite); + + // Update all buffers within Memory Pass (no barriers between) + for (int32 i = 0; i < count; i++) + { + Item& item = items[i]; + context->UpdateBuffer(item.BoneMatrices, item.Data, item.Size); + } + + // Batch resource barriers for reading in Vertex Shader + for (int32 i = 0; i < count; i++) + pass.Transition(items[i].BoneMatrices, GPUResourceAccess::ShaderReadGraphics); + } + +#if COMPILE_WITH_PROFILER + // Insert amount of kilobytes of data updated into profiler trace + uint32 dataSize = 0; + for (int32 i = 0; i < count; i++) + dataSize += items[i].Size; + ZoneValue(dataSize / 1024); +#endif + + Items.Clear(); + } +}; + +AnimatedModelRenderListExtension RenderListExtension; + AnimatedModel::AnimatedModel(const SpawnParams& params) : ModelInstanceActor(params) , _actualMode(AnimationUpdateMode::Never) @@ -1013,10 +1082,7 @@ void AnimatedModel::Draw(RenderContext& renderContext) // Flush skinning data with GPU if (_skinningData.IsDirty()) { - renderContext.List->AddDelayedDraw([this](GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex) - { - context->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count()); - }); + RenderListExtension.Items.Add({ _skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count() }); _skinningData.OnFlush(); } @@ -1059,10 +1125,7 @@ void AnimatedModel::Draw(RenderContextBatch& renderContextBatch) // Flush skinning data with GPU if (_skinningData.IsDirty()) { - renderContext.List->AddDelayedDraw([this](GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex) - { - context->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count()); - }); + RenderListExtension.Items.Add({ _skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count() }); _skinningData.OnFlush(); } diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index 23fcd52dc..ac643b4e8 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -31,6 +31,13 @@ namespace Array FreeRenderList; Array> MemPool; CriticalSection MemPoolLocker; + + typedef Array> ExtensionsList; + ExtensionsList& GetExtensions() + { + static ExtensionsList list; + return list; + } } void ShaderObjectData::Store(const Matrix& worldMatrix, const Matrix& prevWorldMatrix, const Rectangle& lightmapUVsArea, const Float3& geometrySize, float perInstanceRandom, float worldDeterminantSign, float lodDitherFactor) @@ -236,6 +243,16 @@ void RenderList::CleanupCache() MemPoolLocker.Unlock(); } +RenderList::IExtension::IExtension() +{ + GetExtensions().Add(this); +} + +RenderList::IExtension::~IExtension() +{ + GetExtensions().Remove(this); +} + bool RenderList::BlendableSettings::operator<(const BlendableSettings& other) const { // Sort by higher priority @@ -271,6 +288,20 @@ void RenderList::DrainDelayedDraws(GPUContext* context, RenderContextBatch& rend _delayedDraws.Clear(); } +#define LOOP_EXTENSIONS() const auto& extensions = GetExtensions(); for (auto* e : extensions) + +void RenderList::PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch) +{ + LOOP_EXTENSIONS() + e->PreDraw(context, renderContextBatch); +} + +void RenderList::PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch) +{ + LOOP_EXTENSIONS() + e->PostDraw(context, renderContextBatch); +} + void RenderList::BlendSettings() { PROFILE_CPU(); diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h index 9b0a91ed6..b4b7121de 100644 --- a/Source/Engine/Renderer/RenderList.h +++ b/Source/Engine/Renderer/RenderList.h @@ -326,6 +326,21 @@ API_CLASS(Sealed) class FLAXENGINE_API RenderList : public ScriptingObject /// static void CleanupCache(); + /// + /// The rendering extension interface for custom drawing/effects linked to RenderList. Can be used during async scene drawing and further drawing/processing for more optimized rendering. + /// + class FLAXENGINE_API IExtension + { + public: + IExtension(); + virtual ~IExtension(); + + // Event called before collecting draw calls. Can be used for initialization. + virtual void PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch) {} + // Event called after collecting draw calls. Can be used for cleanup or to perform additional drawing using collected draw calls data such as batched data processing. + virtual void PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch) {} + }; + public: /// /// Memory storage with all draw-related data that lives during a single frame rendering time. Thread-safe to allocate memory during rendering jobs. @@ -475,6 +490,10 @@ public: AddDelayedDraw(MoveTemp(func)); } + // IExtension implementation + void PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch); + void PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch); + private: DynamicVertexBuffer _instanceBuffer; RenderListBuffer _delayedDraws; diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp index 2f17e5294..96253934e 100644 --- a/Source/Engine/Renderer/Renderer.cpp +++ b/Source/Engine/Renderer/Renderer.cpp @@ -423,6 +423,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont if (setup.UseMotionVectors) view.Pass |= DrawPass::MotionVectors; renderContextBatch.GetMainContext() = renderContext; // Sync render context in batch with the current value + renderContext.List->PreDraw(context, renderContextBatch); bool drawShadows = !isGBufferDebug && EnumHasAnyFlags(view.Flags, ViewFlags::Shadows) && ShadowsPass::Instance()->IsReady(); switch (renderContext.View.Mode) @@ -462,6 +463,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont // Perform custom post-scene drawing (eg. GPU dispatches used by VFX) for (int32 i = 0; i < renderContextBatch.Contexts.Count(); i++) renderContextBatch.Contexts[i].List->DrainDelayedDraws(context, renderContextBatch, i); + renderContext.List->PostDraw(context, renderContextBatch); #if USE_EDITOR GBufferPass::Instance()->OverrideDrawCalls(renderContext); From 846b64048f0ceaed946d5d13cb10e2aea5c6c9c2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 10 Feb 2026 11:42:17 +0100 Subject: [PATCH 096/100] Update build number --- Flax.flaxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Flax.flaxproj b/Flax.flaxproj index 74ab82f55..c9c27281b 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,10 +4,10 @@ "Major": 1, "Minor": 11, "Revision": 0, - "Build": 6806 + "Build": 6807 }, "Company": "Flax", - "Copyright": "Copyright (c) 2012-2025 Wojciech Figat. All rights reserved.", + "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.", "GameTarget": "FlaxGame", "EditorTarget": "FlaxEditor", "Configuration": { From 1f9f281c317b2f343c5796e67142bec30acb3e5d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 10 Feb 2026 15:02:05 +0100 Subject: [PATCH 097/100] Fix regression from 7b7a92758ff1a6f040c535b8e5a8858dd36d7380 for Visual Scripts --- Source/Engine/Content/Assets/VisualScript.cpp | 2 ++ Source/Engine/Core/Types/Variant.cpp | 31 +++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp index 329696dea..a7e132bdc 100644 --- a/Source/Engine/Content/Assets/VisualScript.cpp +++ b/Source/Engine/Content/Assets/VisualScript.cpp @@ -1700,6 +1700,8 @@ void VisualScript::CacheScriptingType() VisualScriptingBinaryModule::VisualScriptingBinaryModule() : _name("Visual Scripting") { + // Visual Scripts can be unloaded and loaded again even in game + CanReload = true; } ScriptingObject* VisualScriptingBinaryModule::VisualScriptObjectSpawn(const ScriptingObjectSpawnParams& params) diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index bd2d594df..dcabe8e48 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -35,13 +35,6 @@ #endif #define AsEnum AsUint64 -// Editor can hot-reload assemblies thus cached type names may become invalid, otherwise use modules that are never unloaded and their type names are always valid -#if USE_EDITOR -#define IS_VARIANT_TYPE_NAME_STATIC(canReload) !canReload -#else -#define IS_VARIANT_TYPE_NAME_STATIC(canReload) true -#endif - namespace { const char* InBuiltTypesTypeNames[40] = @@ -352,13 +345,13 @@ void VariantType::SetTypeName(const StringAnsiView& typeName, bool staticName) void VariantType::SetTypeName(const ScriptingType& type) { - SetTypeName(type.Fullname, IS_VARIANT_TYPE_NAME_STATIC(type.Module->CanReload)); + SetTypeName(type.Fullname, type.Module->CanReload); } void VariantType::SetTypeName(const MClass& klass) { #if USE_CSHARP - SetTypeName(klass.GetFullName(), IS_VARIANT_TYPE_NAME_STATIC(klass.GetAssembly()->CanReload())); + SetTypeName(klass.GetFullName(), klass.GetAssembly()->CanReload()); #endif } @@ -385,11 +378,23 @@ VariantType VariantType::GetElementType() const void VariantType::Inline() { - const ScriptingTypeHandle typeHandle = Scripting::FindScriptingType(TypeName); - if (typeHandle) - SetTypeName(typeHandle.GetType()); + // Check if the typename comes from static assembly which can be used to inline name instead of dynamic memory allocation + StringAnsiView typeName(TypeName); + auto& modules = BinaryModule::GetModules(); + for (auto module : modules) + { + int32 typeIndex; + if (!module->CanReload && module->FindScriptingType(typeName, typeIndex)) + { + ScriptingTypeHandle typeHandle(module, typeIndex); + SetTypeName(typeHandle.GetType().Fullname, true); + return; + } + } + #if USE_CSHARP - else if (const auto mclass = Scripting::FindClass(TypeName)) + // Try with C#-only types + if (const auto mclass = Scripting::FindClass(TypeName)) SetTypeName(*mclass); #endif } From e851efa0a84353f53ea6ef2b2f108ff0c07d0844 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 10 Feb 2026 22:50:32 +0100 Subject: [PATCH 098/100] Fix applying AO twice for lighting in Forward shading and use correct specular occlusion on reflections #3717 --- .../MaterialTemplates/Features/ForwardShading.hlsl | 6 +++--- Source/Shaders/Reflections.shader | 2 +- Source/Shaders/ReflectionsCommon.hlsl | 9 ++++++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl index 123201d1e..e1b884734 100644 --- a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl +++ b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl @@ -142,11 +142,11 @@ void PS_Forward( #endif #endif - light.rgb += reflections * GetReflectionSpecularLighting(ViewPos, gBuffer) * light.a; + light.rgb += reflections * GetReflectionSpecularLighting(ViewPos, gBuffer) * light.a; #endif - // Add lighting (apply ambient occlusion) - output.rgb += light.rgb * gBuffer.AO; + // Add lighting + output.rgb += light.rgb; #endif diff --git a/Source/Shaders/Reflections.shader b/Source/Shaders/Reflections.shader index dd0b695e5..c257ca35b 100644 --- a/Source/Shaders/Reflections.shader +++ b/Source/Shaders/Reflections.shader @@ -79,7 +79,7 @@ float4 PS_CombinePass(Quad_VS2PS input) : SV_Target0 // Calculate specular color float3 specularColor = GetSpecularColor(gBuffer); - // Calculate reflecion color + // Calculate reflection color float3 V = normalize(gBufferData.ViewPos - gBuffer.WorldPos); float NoV = saturate(dot(gBuffer.Normal, V)); reflections *= EnvBRDF(PreIntegratedGF, specularColor, gBuffer.Roughness, NoV); diff --git a/Source/Shaders/ReflectionsCommon.hlsl b/Source/Shaders/ReflectionsCommon.hlsl index 87e871edf..4abf295c6 100644 --- a/Source/Shaders/ReflectionsCommon.hlsl +++ b/Source/Shaders/ReflectionsCommon.hlsl @@ -48,10 +48,17 @@ float4 SampleReflectionProbe(float3 viewPos, TextureCube probe, ProbeData data, // Calculates the reflective environment lighting to multiply the raw reflection color for the specular light (eg. from Env Probe or SSR). float3 GetReflectionSpecularLighting(float3 viewPos, GBufferSample gBuffer) { + // Calculate reflection color float3 specularColor = GetSpecularColor(gBuffer); float3 V = normalize(viewPos - gBuffer.WorldPos); float NoV = saturate(dot(gBuffer.Normal, V)); - return EnvBRDFApprox(specularColor, gBuffer.Roughness, NoV); + float3 reflections = EnvBRDFApprox(specularColor, gBuffer.Roughness, NoV); + + // Apply specular occlusion + float roughnessSq = gBuffer.Roughness * gBuffer.Roughness; + reflections *= GetSpecularOcclusion(NoV, roughnessSq, gBuffer.AO); + + return reflections; } #endif From ef551c36ae4c9baca91334831daceda6993077c6 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 11 Feb 2026 00:06:44 +0100 Subject: [PATCH 099/100] Fix Forward shading to match Deferred in fog and reflections rendering #3717 --- .../Features/ForwardShading.hlsl | 13 +++++---- .../Graphics/Materials/MaterialShader.h | 2 +- .../Materials/MaterialShaderFeatures.cpp | 9 ++++++- .../Materials/MaterialShaderFeatures.h | 15 ++++------- Source/Shaders/Reflections.shader | 12 +-------- Source/Shaders/ReflectionsCommon.hlsl | 27 ++++++++++++++----- Source/Shaders/SSR.shader | 7 +---- 7 files changed, 45 insertions(+), 40 deletions(-) diff --git a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl index e1b884734..acfff13ad 100644 --- a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl +++ b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl @@ -28,6 +28,7 @@ TextureCube SkyLightTexture : register(t__SRV__); Buffer ShadowsBuffer : register(t__SRV__); Texture2D ShadowMap : register(t__SRV__); Texture3D VolumetricFogTexture : register(t__SRV__); +Texture2D PreIntegratedGF : register(t__SRV__); @4// Forward Shading: Utilities // Public accessors for lighting data, use them as data binding might change but those methods will remain. LightData GetDirectionalLight() { return DirectionalLight; } @@ -108,7 +109,8 @@ void PS_Forward( // Calculate reflections #if USE_REFLECTIONS - float3 reflections = SampleReflectionProbe(ViewPos, EnvProbe, EnvironmentProbe, gBuffer.WorldPos, gBuffer.Normal, gBuffer.Roughness).rgb; + float4 reflections = SampleReflectionProbe(ViewPos, EnvProbe, EnvironmentProbe, gBuffer.WorldPos, gBuffer.Normal, gBuffer.Roughness); + reflections.rgb *= reflections.a; #if MATERIAL_REFLECTIONS == MATERIAL_REFLECTIONS_SSR // Screen Space Reflections @@ -124,7 +126,7 @@ void PS_Forward( if (hit.z > 0) { float3 screenColor = sceneColorTexture.SampleLevel(SamplerPointClamp, hit.xy, 0).rgb; - reflections = lerp(reflections, screenColor, hit.z); + reflections.rgb = lerp(reflections.rgb, screenColor, hit.z); } // Fallback to software tracing if possible @@ -136,13 +138,13 @@ void PS_Forward( if (TraceSDFSoftwareReflections(gBuffer, reflectWS, surfaceAtlas)) { float3 screenColor = sceneColorTexture.SampleLevel(SamplerPointClamp, hit.xy, 0).rgb; - reflections = lerp(surfaceAtlas, float4(screenColor, 1), hit.z); + reflections.rgb = lerp(surfaceAtlas, float4(screenColor, 1), hit.z); } } #endif #endif - light.rgb += reflections * GetReflectionSpecularLighting(ViewPos, gBuffer) * light.a; + light.rgb += reflections.rgb * GetReflectionSpecularLighting(PreIntegratedGF, ViewPos, gBuffer); #endif // Add lighting @@ -158,7 +160,8 @@ void PS_Forward( #else float fogSceneDistance = gBuffer.ViewPos.z; #endif - float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, 0, fogSceneDistance); + float fogSkipDistance = max(ExponentialHeightFog.VolumetricFogMaxDistance - 100, 0); + float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, fogSkipDistance, fogSceneDistance); if (ExponentialHeightFog.VolumetricFogMaxDistance > 0) { diff --git a/Source/Engine/Graphics/Materials/MaterialShader.h b/Source/Engine/Graphics/Materials/MaterialShader.h index bb68520c0..9c01cf2d0 100644 --- a/Source/Engine/Graphics/Materials/MaterialShader.h +++ b/Source/Engine/Graphics/Materials/MaterialShader.h @@ -10,7 +10,7 @@ /// /// Current materials shader version. /// -#define MATERIAL_GRAPH_VERSION 179 +#define MATERIAL_GRAPH_VERSION 180 class Material; class GPUShader; diff --git a/Source/Engine/Graphics/Materials/MaterialShaderFeatures.cpp b/Source/Engine/Graphics/Materials/MaterialShaderFeatures.cpp index 19f2042f4..bf8e92e79 100644 --- a/Source/Engine/Graphics/Materials/MaterialShaderFeatures.cpp +++ b/Source/Engine/Graphics/Materials/MaterialShaderFeatures.cpp @@ -10,9 +10,11 @@ #if USE_EDITOR #include "Engine/Renderer/Lightmaps.h" #endif +#include "Engine/Content/Content.h" #include "Engine/Graphics/GPUContext.h" #include "Engine/Level/Scene/Lightmap.h" #include "Engine/Level/Actors/EnvironmentProbe.h" +#include "Engine/Renderer/ReflectionsPass.h" void ForwardShadingFeature::Bind(MaterialShader::BindParameters& params, Span& cb, int32& srv) { @@ -26,6 +28,7 @@ void ForwardShadingFeature::Bind(MaterialShader::BindParameters& params, SpanBindSR(volumetricFogTextureRegisterIndex, volumetricFogTexture); @@ -100,9 +104,12 @@ void ForwardShadingFeature::Bind(MaterialShader::BindParameters& params, SpanUnBindSR(envProbeShaderRegisterIndex); } + // TODO: find a better way to find this texture (eg. cache GPUTextureView* handle within ForwardShading cache for a whole frame) + static AssetReference PreIntegratedGF = Content::LoadAsyncInternal(PRE_INTEGRATED_GF_ASSET_NAME); + params.GPUContext->BindSR(preIntegratedGFRegisterIndex, PreIntegratedGF->GetTexture()); // Set local lights data.LocalLightsCount = 0; diff --git a/Source/Engine/Graphics/Materials/MaterialShaderFeatures.h b/Source/Engine/Graphics/Materials/MaterialShaderFeatures.h index 54b91af23..5eab262ce 100644 --- a/Source/Engine/Graphics/Materials/MaterialShaderFeatures.h +++ b/Source/Engine/Graphics/Materials/MaterialShaderFeatures.h @@ -25,10 +25,9 @@ struct ForwardShadingFeature : MaterialShaderFeature { enum { MaxLocalLights = 4 }; - enum { SRVs = 5 }; + enum { SRVs = 6 }; - PACK_STRUCT(struct Data - { + PACK_STRUCT(struct Data { ShaderLightData DirectionalLight; ShaderLightData SkyLight; ShaderEnvProbeData EnvironmentProbe; @@ -76,8 +75,7 @@ struct GlobalIlluminationFeature : MaterialShaderFeature { enum { SRVs = 3 }; - PACK_STRUCT(struct Data - { + PACK_STRUCT(struct Data { DynamicDiffuseGlobalIlluminationPass::ConstantsData DDGI; }); @@ -92,13 +90,10 @@ struct SDFReflectionsFeature : MaterialShaderFeature { enum { SRVs = 7 }; - PACK_STRUCT(struct Data - { + PACK_STRUCT(struct Data { GlobalSignDistanceFieldPass::ConstantsData GlobalSDF; GlobalSurfaceAtlasPass::ConstantsData GlobalSurfaceAtlas; - }); - - + }); static bool Bind(MaterialShader::BindParameters& params, Span& cb, int32& srv); #if USE_EDITOR diff --git a/Source/Shaders/Reflections.shader b/Source/Shaders/Reflections.shader index c257ca35b..a7ec44638 100644 --- a/Source/Shaders/Reflections.shader +++ b/Source/Shaders/Reflections.shader @@ -76,18 +76,8 @@ float4 PS_CombinePass(Quad_VS2PS input) : SV_Target0 // Sample reflections buffer float3 reflections = SAMPLE_RT(Reflections, input.TexCoord).rgb; - // Calculate specular color - float3 specularColor = GetSpecularColor(gBuffer); - // Calculate reflection color - float3 V = normalize(gBufferData.ViewPos - gBuffer.WorldPos); - float NoV = saturate(dot(gBuffer.Normal, V)); - reflections *= EnvBRDF(PreIntegratedGF, specularColor, gBuffer.Roughness, NoV); - - // Apply specular occlusion - float roughnessSq = gBuffer.Roughness * gBuffer.Roughness; - float specularOcclusion = GetSpecularOcclusion(NoV, roughnessSq, gBuffer.AO); - reflections *= specularOcclusion; + reflections *= GetReflectionSpecularLighting(PreIntegratedGF, gBufferData.ViewPos, gBuffer); return float4(reflections, 0); } diff --git a/Source/Shaders/ReflectionsCommon.hlsl b/Source/Shaders/ReflectionsCommon.hlsl index 4abf295c6..26ae965d6 100644 --- a/Source/Shaders/ReflectionsCommon.hlsl +++ b/Source/Shaders/ReflectionsCommon.hlsl @@ -4,6 +4,7 @@ #define __REFLECTIONS_COMMON__ #include "./Flax/GBufferCommon.hlsl" +#include "./Flax/BRDF.hlsl" // Hit depth (view space) threshold to detect if sky was hit (value above it where 1.0f is default) #define REFLECTIONS_HIT_THRESHOLD 0.9f @@ -48,15 +49,29 @@ float4 SampleReflectionProbe(float3 viewPos, TextureCube probe, ProbeData data, // Calculates the reflective environment lighting to multiply the raw reflection color for the specular light (eg. from Env Probe or SSR). float3 GetReflectionSpecularLighting(float3 viewPos, GBufferSample gBuffer) { - // Calculate reflection color - float3 specularColor = GetSpecularColor(gBuffer); + // Calculate reflection color float3 V = normalize(viewPos - gBuffer.WorldPos); float NoV = saturate(dot(gBuffer.Normal, V)); + float3 specularColor = GetSpecularColor(gBuffer); float3 reflections = EnvBRDFApprox(specularColor, gBuffer.Roughness, NoV); - - // Apply specular occlusion - float roughnessSq = gBuffer.Roughness * gBuffer.Roughness; - reflections *= GetSpecularOcclusion(NoV, roughnessSq, gBuffer.AO); + + // Apply specular occlusion + float roughnessSq = gBuffer.Roughness * gBuffer.Roughness; + reflections *= GetSpecularOcclusion(NoV, roughnessSq, gBuffer.AO); + + return reflections; +} +float3 GetReflectionSpecularLighting(Texture2D preIntegratedGF, float3 viewPos, GBufferSample gBuffer) +{ + // Calculate reflection color + float3 V = normalize(viewPos - gBuffer.WorldPos); + float NoV = saturate(dot(gBuffer.Normal, V)); + float3 specularColor = GetSpecularColor(gBuffer); + float3 reflections = EnvBRDF(preIntegratedGF, specularColor, gBuffer.Roughness, NoV); + + // Apply specular occlusion + float roughnessSq = gBuffer.Roughness * gBuffer.Roughness; + reflections *= GetSpecularOcclusion(NoV, roughnessSq, gBuffer.AO); return reflections; } diff --git a/Source/Shaders/SSR.shader b/Source/Shaders/SSR.shader index 3a7f52f7c..f2c863dfd 100644 --- a/Source/Shaders/SSR.shader +++ b/Source/Shaders/SSR.shader @@ -81,13 +81,8 @@ float4 PS_CombinePass(Quad_VS2PS input) : SV_Target0 // Sample reflections buffer float3 reflections = SAMPLE_RT(Texture1, input.TexCoord).rgb; - // Calculate specular color - float3 specularColor = GetSpecularColor(gBuffer); - // Calculate reflection color - float3 V = normalize(gBufferData.ViewPos - gBuffer.WorldPos); - float NoV = saturate(dot(gBuffer.Normal, V)); - light.rgb += reflections * EnvBRDF(Texture2, specularColor, gBuffer.Roughness, NoV) * gBuffer.AO; + light.rgb += reflections * GetReflectionSpecularLighting(Texture2, gBufferData.ViewPos, gBuffer); } return light; From b53028782f43402b674afb149e578c8be5262847 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 11 Feb 2026 00:07:44 +0100 Subject: [PATCH 100/100] Update engine assets --- Content/Editor/Camera/M_Camera.flax | 2 +- Content/Editor/CubeTexturePreviewMaterial.flax | 2 +- Content/Editor/DebugMaterials/DDGIDebugProbes.flax | 2 +- Content/Editor/DebugMaterials/SingleColor/Decal.flax | 2 +- Content/Editor/DebugMaterials/SingleColor/Particle.flax | 4 ++-- Content/Editor/DebugMaterials/SingleColor/Surface.flax | 2 +- .../Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax | 4 ++-- Content/Editor/DebugMaterials/SingleColor/Terrain.flax | 2 +- Content/Editor/DefaultFontMaterial.flax | 2 +- Content/Editor/Gizmo/FoliageBrushMaterial.flax | 4 ++-- Content/Editor/Gizmo/Material.flax | 4 ++-- Content/Editor/Gizmo/MaterialWire.flax | 4 ++-- Content/Editor/Gizmo/SelectionOutlineMaterial.flax | 2 +- Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax | 2 +- Content/Editor/Highlight Material.flax | 4 ++-- Content/Editor/Icons/IconsMaterial.flax | 4 ++-- Content/Editor/IesProfilePreviewMaterial.flax | 2 +- Content/Editor/Particles/Particle Material Color.flax | 4 ++-- Content/Editor/Particles/Smoke Material.flax | 4 ++-- Content/Editor/SpriteMaterial.flax | 2 +- Content/Editor/Terrain/Circle Brush Material.flax | 2 +- Content/Editor/Terrain/Highlight Terrain Material.flax | 2 +- Content/Editor/TexturePreviewMaterial.flax | 2 +- Content/Editor/Wires Debug Material.flax | 4 ++-- Content/Engine/DefaultDeformableMaterial.flax | 2 +- Content/Engine/DefaultMaterial.flax | 2 +- Content/Engine/DefaultRadialMenu.flax | 2 +- Content/Engine/DefaultTerrainMaterial.flax | 2 +- Content/Engine/SingleColorMaterial.flax | 2 +- Content/Engine/SkyboxMaterial.flax | 2 +- 30 files changed, 40 insertions(+), 40 deletions(-) diff --git a/Content/Editor/Camera/M_Camera.flax b/Content/Editor/Camera/M_Camera.flax index 7d4c71666..24cfa698d 100644 --- a/Content/Editor/Camera/M_Camera.flax +++ b/Content/Editor/Camera/M_Camera.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b73d774c71bd7b46c9c4198a4c957055e6447e31d8252813b272db92301475e7 +oid sha256:0bbce0a3252f993e9d7ea49fe31a75e7ccd96eb9ffc75154a5ea026232973da6 size 29533 diff --git a/Content/Editor/CubeTexturePreviewMaterial.flax b/Content/Editor/CubeTexturePreviewMaterial.flax index 2d732c086..1cc15a63b 100644 --- a/Content/Editor/CubeTexturePreviewMaterial.flax +++ b/Content/Editor/CubeTexturePreviewMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4ec07a3b7e0a2dfd4332598a982c3192c0c357c6bcd128d7a7797fb483780e7 +oid sha256:9b955c07a63629b4f7c4e0566ab7a0856868a3a0e91691c8aeee7528262a3a0b size 31445 diff --git a/Content/Editor/DebugMaterials/DDGIDebugProbes.flax b/Content/Editor/DebugMaterials/DDGIDebugProbes.flax index d082bd8e7..6f16c8a0f 100644 --- a/Content/Editor/DebugMaterials/DDGIDebugProbes.flax +++ b/Content/Editor/DebugMaterials/DDGIDebugProbes.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2830919bea988e1f8bd8299ceac34b8a3695418e2f22ca670f2fec3b3d6d1a2f +oid sha256:9e6835bbc536e31289713cd1b8fd85d7565fd25cfb4014ab6b5be9e25d207761 size 41149 diff --git a/Content/Editor/DebugMaterials/SingleColor/Decal.flax b/Content/Editor/DebugMaterials/SingleColor/Decal.flax index b94f22bc8..ab2117d05 100644 --- a/Content/Editor/DebugMaterials/SingleColor/Decal.flax +++ b/Content/Editor/DebugMaterials/SingleColor/Decal.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:588c29a4b239c32d4b125052e4054a29cf5140562e90ca6fac4d2952e03f66c7 +oid sha256:0901efdfbbd4102291969c561557a16fb617f6a15ab60d02c3905ae525991f8d size 10397 diff --git a/Content/Editor/DebugMaterials/SingleColor/Particle.flax b/Content/Editor/DebugMaterials/SingleColor/Particle.flax index de2043874..d4d466b56 100644 --- a/Content/Editor/DebugMaterials/SingleColor/Particle.flax +++ b/Content/Editor/DebugMaterials/SingleColor/Particle.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b39cd76254f341c93e83625475b6e7896ef34f1d6d650da52e649bc055d0d03e -size 33503 +oid sha256:843e7bf7332d934354137e65bcf22b123e6aaaa86c76737855e2ad4517242782 +size 33659 diff --git a/Content/Editor/DebugMaterials/SingleColor/Surface.flax b/Content/Editor/DebugMaterials/SingleColor/Surface.flax index 7ae8a69c3..4ad86cbc4 100644 --- a/Content/Editor/DebugMaterials/SingleColor/Surface.flax +++ b/Content/Editor/DebugMaterials/SingleColor/Surface.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5861e912cf822c9478f824390f6258d84821b7289e3e993a7dee38b77c5a2f80 +oid sha256:f767d7e0df4020f9dbb35a8916965b693ad01918787ed9228d75d390cea789cb size 29398 diff --git a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax index fdcb880df..7fd60a785 100644 --- a/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax +++ b/Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9ed2869a2a754423e0b8c456eed621bd06bdb50cacf7a972a7f024e40a1ea6a -size 32954 +oid sha256:a44ae61dca9073913ceaa3dc4d6806b6b2b2c45deef435e5263445fbcc94b6b4 +size 33110 diff --git a/Content/Editor/DebugMaterials/SingleColor/Terrain.flax b/Content/Editor/DebugMaterials/SingleColor/Terrain.flax index ad27a422c..bffebf9f0 100644 --- a/Content/Editor/DebugMaterials/SingleColor/Terrain.flax +++ b/Content/Editor/DebugMaterials/SingleColor/Terrain.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05c27ac416ef922ee247adc12a115fd522eb3a1d8873e1056914cd96893a3097 +oid sha256:7d97b88aa9780d2d4367670cdffcce82f70daf7cd89d928e1a92cb9a5e3fc8c5 size 21096 diff --git a/Content/Editor/DefaultFontMaterial.flax b/Content/Editor/DefaultFontMaterial.flax index d84425aab..3bb18d7f6 100644 --- a/Content/Editor/DefaultFontMaterial.flax +++ b/Content/Editor/DefaultFontMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e3d4ca149e143fee31e2d038b8efec526ca995dbe13258fbb68c89cd43ecbf7 +oid sha256:d706889c353547241c10782826ce09e081e38ee636fb8a70a3f714cd4d98c693 size 29627 diff --git a/Content/Editor/Gizmo/FoliageBrushMaterial.flax b/Content/Editor/Gizmo/FoliageBrushMaterial.flax index eb7e784c9..3b08141d0 100644 --- a/Content/Editor/Gizmo/FoliageBrushMaterial.flax +++ b/Content/Editor/Gizmo/FoliageBrushMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7af1150d6e7cb6ecce5cd039f0edc92967c986a13903a201d6dc15ed0751dc57 -size 39637 +oid sha256:cba9bb7a8bf3cdb1f10f6167324225699ef0885d2b4b9de49ad567f17dd4e71d +size 39793 diff --git a/Content/Editor/Gizmo/Material.flax b/Content/Editor/Gizmo/Material.flax index bbb114662..53212f35f 100644 --- a/Content/Editor/Gizmo/Material.flax +++ b/Content/Editor/Gizmo/Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d575ca1b202c84b8268687b391be5fc8d55497ffa23fb3cd4287fa667de654ab -size 34240 +oid sha256:07c7c09b0a888f940c221b73277a3582cfe82eed1416790ea37ca48052ec9191 +size 34396 diff --git a/Content/Editor/Gizmo/MaterialWire.flax b/Content/Editor/Gizmo/MaterialWire.flax index fb4b8acca..3fdac09b6 100644 --- a/Content/Editor/Gizmo/MaterialWire.flax +++ b/Content/Editor/Gizmo/MaterialWire.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26f2d88aab9c0cad36ae527b038a36b69755ff3a5a920e8c4563dd5e1ed8ec65 -size 32689 +oid sha256:02297bb31c7e41529e29811bfe1c63c3e34d15ff9f3dffbd2e3edd3a56407790 +size 32845 diff --git a/Content/Editor/Gizmo/SelectionOutlineMaterial.flax b/Content/Editor/Gizmo/SelectionOutlineMaterial.flax index b5d224d58..b2d2c6383 100644 --- a/Content/Editor/Gizmo/SelectionOutlineMaterial.flax +++ b/Content/Editor/Gizmo/SelectionOutlineMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bb75934622d9251a8a9e72cfe4905091770798ffed22de680a70f98434d0ed7 +oid sha256:f791cbb4556aa5aa709436d17042abf6536e325cc4bcdc99c570b331f68fee84 size 16241 diff --git a/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax b/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax index 5a5262e2b..777620244 100644 --- a/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax +++ b/Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1afa76c3f9400da065c150a6a58adc904c3596f650e04dfd87b5e1c1b34695e +oid sha256:c93ddf1c826f396f81707da7f692d65462bb897758a50f0bd90c085d42f35edd size 30655 diff --git a/Content/Editor/Highlight Material.flax b/Content/Editor/Highlight Material.flax index 9d09ea792..bc063f3a4 100644 --- a/Content/Editor/Highlight Material.flax +++ b/Content/Editor/Highlight Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1290ae85e4fe41f9d8c1919b33e165287f79377aeddc68f9117c1795ca341003 -size 31267 +oid sha256:ee3bc010414450b37578fcf9a6ccd89875c959634717b7e431c85c4bd57a6443 +size 31423 diff --git a/Content/Editor/Icons/IconsMaterial.flax b/Content/Editor/Icons/IconsMaterial.flax index 2ccbce8c9..668c33943 100644 --- a/Content/Editor/Icons/IconsMaterial.flax +++ b/Content/Editor/Icons/IconsMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:340cc500a160344b43b21ed8c4c22b6d776f406581f606ced62a3e92c5bef18a -size 31300 +oid sha256:318aa93c17e75b7349f8c001d81238a1e6b26ae017e1f116dc9b9b2555fcfa84 +size 31456 diff --git a/Content/Editor/IesProfilePreviewMaterial.flax b/Content/Editor/IesProfilePreviewMaterial.flax index b3a382132..66529c79b 100644 --- a/Content/Editor/IesProfilePreviewMaterial.flax +++ b/Content/Editor/IesProfilePreviewMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d444cd33ec8d2e1e0e6651c3979260f05c06c8bac33ce2441d6974ae4fa178e4 +oid sha256:f65ec8f63e49e7bafb2248d2ba5001590e40754c6af919e285bb45650bc266dc size 20443 diff --git a/Content/Editor/Particles/Particle Material Color.flax b/Content/Editor/Particles/Particle Material Color.flax index 91b06b2fb..b73fbbe20 100644 --- a/Content/Editor/Particles/Particle Material Color.flax +++ b/Content/Editor/Particles/Particle Material Color.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:906443c7db821361b32780c17735bc9477ea96c8979dee371a4899635246af48 -size 31708 +oid sha256:11d9f756a1ea3f8b463b410eebf1d463a56d16d0eb97b74d743ae55260a3c8c8 +size 31864 diff --git a/Content/Editor/Particles/Smoke Material.flax b/Content/Editor/Particles/Smoke Material.flax index e6396c194..259e179c8 100644 --- a/Content/Editor/Particles/Smoke Material.flax +++ b/Content/Editor/Particles/Smoke Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16db9c1a18b64aea2dcdf3e74f9a44c652bf8bd9b33a5bfda39555d8c002a358 -size 39774 +oid sha256:3f49348233e05529c7ec435660a2e78f3c2f4ca105d92e79e0caac6b22f2a605 +size 39931 diff --git a/Content/Editor/SpriteMaterial.flax b/Content/Editor/SpriteMaterial.flax index 2a05418b2..12d89bdb0 100644 --- a/Content/Editor/SpriteMaterial.flax +++ b/Content/Editor/SpriteMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56254b02ffc937d61e8e8fa6492d4805e944ca639c7fcfc0f751b4ac2442365d +oid sha256:3cef7efc0f1446707c3e31fb5ab3b372377037819e7e8b841bccde11415d5bfd size 30734 diff --git a/Content/Editor/Terrain/Circle Brush Material.flax b/Content/Editor/Terrain/Circle Brush Material.flax index f481be389..746c40ef0 100644 --- a/Content/Editor/Terrain/Circle Brush Material.flax +++ b/Content/Editor/Terrain/Circle Brush Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16eefa75a2ae99bba658c4e9b8e8741187b90e577193f76394872764fff2ca0b +oid sha256:7bfdfe90e3b11caf1143d463e2fa9f3a09f24f5bedf8b4e676d4fbfb9b2553df size 28232 diff --git a/Content/Editor/Terrain/Highlight Terrain Material.flax b/Content/Editor/Terrain/Highlight Terrain Material.flax index 579db477c..63f3bb911 100644 --- a/Content/Editor/Terrain/Highlight Terrain Material.flax +++ b/Content/Editor/Terrain/Highlight Terrain Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e25a3c9e130e51b28dfe5ce43678f52c277c0def83142a2853c4c8ca84dbf417 +oid sha256:2afea0340cc0a151b449316cca850f1dccdd16ffa349523bf84ead871075cba5 size 21179 diff --git a/Content/Editor/TexturePreviewMaterial.flax b/Content/Editor/TexturePreviewMaterial.flax index d75e19d5e..bc1bea3b6 100644 --- a/Content/Editor/TexturePreviewMaterial.flax +++ b/Content/Editor/TexturePreviewMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79de09ba0616eb6066171c2b80cdb6c4235cb52be4836d23162bb9c2585760a0 +oid sha256:6ddd3fef4c2e14f0c1e84a081aaf22b79cff9434bf15973679f2469a5bd2598a size 11058 diff --git a/Content/Editor/Wires Debug Material.flax b/Content/Editor/Wires Debug Material.flax index b1f87a7d0..009a226d6 100644 --- a/Content/Editor/Wires Debug Material.flax +++ b/Content/Editor/Wires Debug Material.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02d4c767fb59c67fef16ccc081f6f371bad329a5333047f9f79fd3d50b911f93 -size 31753 +oid sha256:8480dd3ec9d240e1429ffd638cb57561ebe0e651b0c5e52110c4572ab78cc10c +size 31909 diff --git a/Content/Engine/DefaultDeformableMaterial.flax b/Content/Engine/DefaultDeformableMaterial.flax index 1244ae3ec..e44051d1b 100644 --- a/Content/Engine/DefaultDeformableMaterial.flax +++ b/Content/Engine/DefaultDeformableMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1f556b230cea8e83d00bd4357d34a77e5e468389a5f3bb615e30f6a3ce3ace4 +oid sha256:490af9df20c7a5c81eb2664b1923ba13567abfa35ad3c989a2b2fe906c876034 size 19734 diff --git a/Content/Engine/DefaultMaterial.flax b/Content/Engine/DefaultMaterial.flax index bd57e7d44..980ebd81f 100644 --- a/Content/Engine/DefaultMaterial.flax +++ b/Content/Engine/DefaultMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4ec872b3433d58f8aed640c6efee3d911f226740b4844cb07ed0bf94c00ea18 +oid sha256:b0d004b06fded8a4d8cf2d6a9960753a3ac6030358740955bab8a6f7c0600085 size 32080 diff --git a/Content/Engine/DefaultRadialMenu.flax b/Content/Engine/DefaultRadialMenu.flax index 5fba9092e..19b45e640 100644 --- a/Content/Engine/DefaultRadialMenu.flax +++ b/Content/Engine/DefaultRadialMenu.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0da99403c069966d05daea7fc11d32f20f88bac0463fbc08724840e249ee3bd2 +oid sha256:16b758ddffb7b39d7ada6f171eca464e401c2d00ea788d0ef3c9b7f88ffed619 size 21700 diff --git a/Content/Engine/DefaultTerrainMaterial.flax b/Content/Engine/DefaultTerrainMaterial.flax index 4147fe0e4..99d58ff3b 100644 --- a/Content/Engine/DefaultTerrainMaterial.flax +++ b/Content/Engine/DefaultTerrainMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdfa3b4842a5734d2cd8110af03599b4a5280b33a72b2ba435cd19487cebcde6 +oid sha256:37fb6eb71d4e9be122229e393c057f0584fa8e4370b15c90a61b49b66fa66b4f size 24082 diff --git a/Content/Engine/SingleColorMaterial.flax b/Content/Engine/SingleColorMaterial.flax index 6d556af2b..f0e5fdc98 100644 --- a/Content/Engine/SingleColorMaterial.flax +++ b/Content/Engine/SingleColorMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ff8f127d46e68e3423339a352f623c079f2c5d93512c5e9b25841edc7cd0f05 +oid sha256:ebfd6324585b0e117287bb5e6931223624995070c024ccd64b472ff035a269fe size 29615 diff --git a/Content/Engine/SkyboxMaterial.flax b/Content/Engine/SkyboxMaterial.flax index b51c5bce7..deae33378 100644 --- a/Content/Engine/SkyboxMaterial.flax +++ b/Content/Engine/SkyboxMaterial.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14c9833ed19302ea7c6e730fff63f1b72dbac71dc2b49c1d62edb61ccaa68b6f +oid sha256:088472984c47f9ae024ae7f2573afdbc7701f7736b11737af9f2bf7de23287cf size 31974