From dd4bb2f7f1f3825b4e0c57e02d9e513e1e44743b Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Wed, 4 Jun 2025 11:16:01 -0500 Subject: [PATCH 001/122] Dont scroll to node when play is starting or ending. --- Source/Editor/Windows/SceneTreeWindow.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 705976e6e..999be7f94 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -27,6 +27,7 @@ namespace FlaxEditor.Windows private Panel _sceneTreePanel; private bool _isUpdatingSelection; private bool _isMouseDown; + private bool _isPlayStateChanging = false; private DragAssets _dragAssets; private DragActorType _dragActorType; @@ -93,6 +94,11 @@ namespace FlaxEditor.Windows _tree.Parent = _sceneTreePanel; headerPanel.Parent = this; + Editor.PlayModeBeginning += () => _isPlayStateChanging = true; + Editor.PlayModeBegin += () => _isPlayStateChanging = false; + Editor.PlayModeEnding += () => _isPlayStateChanging = true; + Editor.PlayModeEnd += () => _isPlayStateChanging = false; + // Setup input actions InputActions.Add(options => options.TranslateMode, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate); InputActions.Add(options => options.RotateMode, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate); @@ -250,7 +256,7 @@ namespace FlaxEditor.Windows _tree.Select(nodes); // For single node selected scroll view so user can see it - if (nodes.Count == 1) + if (nodes.Count == 1 && !_isPlayStateChanging) { nodes[0].ExpandAllParents(true); _sceneTreePanel.ScrollViewTo(nodes[0]); From eee4e55cf0ee0dae898d38bddc0c49fdba0f9ba8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 5 Jun 2025 22:26:45 +0200 Subject: [PATCH 002/122] Fix debug shapes change --- .../Physics/Colliders/CharacterController.cpp | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Physics/Colliders/CharacterController.cpp b/Source/Engine/Physics/Colliders/CharacterController.cpp index 74c324bad..a874a9947 100644 --- a/Source/Engine/Physics/Colliders/CharacterController.cpp +++ b/Source/Engine/Physics/Colliders/CharacterController.cpp @@ -262,16 +262,16 @@ void CharacterController::OnDebugDrawSelected() Quaternion rotation = Quaternion::Euler(90, 0, 0); const Vector3 position = GetControllerPosition(); DEBUG_DRAW_WIRE_CAPSULE(position, rotation, _radius, _height, Color::GreenYellow, 0, false); - if (_contactOffset > 0) - DEBUG_DRAW_WIRE_CAPSULE(position, rotation, _radius - _contactOffset, _height, Color::Blue.AlphaMultiplied(0.4f), 0, false); -#if 1 - // More technical visuals debugging if (_controller) { + // Physics backend capsule shape float height, radius; GetControllerSize(height, radius); - Vector3 base = PhysicsBackend::GetControllerBasePosition(_controller); Vector3 pos = PhysicsBackend::GetControllerPosition(_controller); + DEBUG_DRAW_WIRE_CAPSULE(pos, rotation, radius, height, Color::Blue.AlphaMultiplied(0.2f), 0, false); +#if 0 + // More technical visuals debugging + Vector3 base = PhysicsBackend::GetControllerBasePosition(_controller); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(base, 5.0f), Color::Red, 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(pos, 4.0f), Color::Red, 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(pos + Vector3(0, height * 0.5f, 0), 2.0f), Color::Red, 0, false); @@ -279,17 +279,8 @@ void CharacterController::OnDebugDrawSelected() DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(pos + Vector3(0, height * 0.5f, 0), radius), Color::Red.AlphaMultiplied(0.5f), 0, false); DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(pos - Vector3(0, height * 0.5f, 0), radius), Color::Red.AlphaMultiplied(0.5f), 0, false); DEBUG_DRAW_WIRE_CYLINDER(pos, Quaternion::Identity, radius, height, Color::Red.AlphaMultiplied(0.2f), 0, false); - } - DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(position, 3.0f), Color::GreenYellow, 0, false); -#else - if (_controller) - { - // Physics backend capsule shape - float height, radius; - GetControllerSize(height, radius); - DEBUG_DRAW_WIRE_CAPSULE(PhysicsBackend::GetControllerPosition(_controller), rotation, radius, height, Color::Blue.AlphaMultiplied(0.2f), 0, false); - } #endif + } // Base Collider::OnDebugDrawSelected(); From 1a77ba455273c0087d595968720549c1db8c933c Mon Sep 17 00:00:00 2001 From: Zode Date: Sat, 7 Jun 2025 13:15:39 +0300 Subject: [PATCH 003/122] Add node alignment formatting options to visject --- Source/Editor/Surface/NodeAlignmentType.cs | 43 +++++++++++++++++++ .../Surface/VisjectSurface.ContextMenu.cs | 23 +++++++++- .../Surface/VisjectSurface.Formatting.cs | 42 ++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 Source/Editor/Surface/NodeAlignmentType.cs diff --git a/Source/Editor/Surface/NodeAlignmentType.cs b/Source/Editor/Surface/NodeAlignmentType.cs new file mode 100644 index 000000000..141235783 --- /dev/null +++ b/Source/Editor/Surface/NodeAlignmentType.cs @@ -0,0 +1,43 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEngine; + +namespace FlaxEditor.Surface +{ + /// + /// Node Alignment type + /// + [HideInEditor] + public enum NodeAlignmentType + { + /// + /// Align nodes vertically to top, matching top-most node + /// + Top, + + /// + /// Align nodes vertically to middle, using average of all nodes + /// + Middle, + + /// + /// Align nodes vertically to bottom, matching bottom-most node + /// + Bottom, + + /// + /// Align nodes horizontally to left, matching left-most node + /// + Left, + + /// + /// Align nodes horizontally to center, using average of all nodes + /// + Center, + + /// + /// Align nodes horizontally to right, matching right-most node + /// + Right, + } +} \ No newline at end of file diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs index ee7dd33e5..ae836bb11 100644 --- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs +++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs @@ -191,7 +191,14 @@ namespace FlaxEditor.Surface private ContextMenuButton _cmCopyButton; private ContextMenuButton _cmDuplicateButton; + private ContextMenuChildMenu _cmFormatNodesMenu; private ContextMenuButton _cmFormatNodesConnectionButton; + private ContextMenuButton _cmAlignNodesTopButton; + private ContextMenuButton _cmAlignNodesMiddleButton; + private ContextMenuButton _cmAlignNodesBottomButton; + private ContextMenuButton _cmAlignNodesLeftButton; + private ContextMenuButton _cmAlignNodesCenterButton; + private ContextMenuButton _cmAlignNodesRightButton; private ContextMenuButton _cmRemoveNodeConnectionsButton; private ContextMenuButton _cmRemoveBoxConnectionsButton; private readonly Float2 ContextMenuOffset = new Float2(5); @@ -399,8 +406,20 @@ namespace FlaxEditor.Surface } menu.AddSeparator(); - _cmFormatNodesConnectionButton = menu.AddButton("Format node(s)", () => { FormatGraph(SelectedNodes); }); - _cmFormatNodesConnectionButton.Enabled = CanEdit && HasNodesSelection; + _cmFormatNodesMenu = menu.AddChildMenu("Format node(s)"); + _cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection; + + _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", () => { FormatGraph(SelectedNodes); }); + + _cmFormatNodesMenu.ContextMenu.AddSeparator(); + _cmAlignNodesTopButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align top", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); }); + _cmAlignNodesMiddleButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align middle", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); }); + _cmAlignNodesBottomButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align bottom", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); }); + + _cmFormatNodesMenu.ContextMenu.AddSeparator(); + _cmAlignNodesLeftButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align left", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); }); + _cmAlignNodesCenterButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align center", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }); + _cmAlignNodesRightButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align right", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }); _cmRemoveNodeConnectionsButton = menu.AddButton("Remove all connections to that node(s)", () => { diff --git a/Source/Editor/Surface/VisjectSurface.Formatting.cs b/Source/Editor/Surface/VisjectSurface.Formatting.cs index c8819a559..6b942f4a1 100644 --- a/Source/Editor/Surface/VisjectSurface.Formatting.cs +++ b/Source/Editor/Surface/VisjectSurface.Formatting.cs @@ -282,5 +282,47 @@ namespace FlaxEditor.Surface return maxOffset; } + + /// + /// Align given nodes on a graph using the given alignment type. + /// Ignores any potential overlap. + /// + /// List of nodes + /// Alignemnt type + public void AlignNodes(List nodes, NodeAlignmentType alignmentType) + { + if(nodes.Count <= 1) + return; + + var undoActions = new List(); + var boundingBox = GetNodesBounds(nodes); + for(int i = 0; i < nodes.Count; i++) + { + var centerY = boundingBox.Center.Y - (nodes[i].Height / 2); + var centerX = boundingBox.Center.X - (nodes[i].Width / 2); + + var newLocation = alignmentType switch + { + NodeAlignmentType.Top => new Float2(nodes[i].Location.X, boundingBox.Top), + NodeAlignmentType.Middle => new Float2(nodes[i].Location.X, centerY), + NodeAlignmentType.Bottom => new Float2(nodes[i].Location.X, boundingBox.Bottom - nodes[i].Height), + + NodeAlignmentType.Left => new Float2(boundingBox.Left, nodes[i].Location.Y), + NodeAlignmentType.Center => new Float2(centerX, nodes[i].Location.Y), + NodeAlignmentType.Right => new Float2(boundingBox.Right - nodes[i].Width, nodes[i].Location.Y), + + _ => throw new NotImplementedException($"Unsupported node alignment type: {alignmentType}"), + }; + + var locationDelta = newLocation - nodes[i].Location; + nodes[i].Location = newLocation; + + if(Undo != null) + undoActions.Add(new MoveNodesAction(Context, new[] { nodes[i].ID }, locationDelta)); + } + + MarkAsEdited(false); + Undo?.AddAction(new MultiUndoAction(undoActions, $"Align nodes ({alignmentType})")); + } } } From 6ee3b232619b7fd0cd5bd6a93b8546f729b15918 Mon Sep 17 00:00:00 2001 From: Zode Date: Sat, 7 Jun 2025 16:57:28 +0300 Subject: [PATCH 004/122] Add highlighting to layers matrix editor to improve UX --- .../Dedicated/LayersMatrixEditor.cs | 79 ++++++++++++++++--- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/LayersMatrixEditor.cs b/Source/Editor/CustomEditors/Dedicated/LayersMatrixEditor.cs index 4b41db297..f853ccef7 100644 --- a/Source/Editor/CustomEditors/Dedicated/LayersMatrixEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/LayersMatrixEditor.cs @@ -16,6 +16,11 @@ namespace FlaxEditor.CustomEditors.Dedicated { private int _layersCount; private List _checkBoxes; + private VerticalPanel _upperRightCell; + private VerticalPanel _bottomLeftCell; + private UniformGridPanel _grid; + private Border _horizontalHighlight; + private Border _verticalHighlight; /// public override DisplayStyle Style => DisplayStyle.InlineIntoParent; @@ -37,12 +42,29 @@ namespace FlaxEditor.CustomEditors.Dedicated Parent = panel, }; + var style = FlaxEngine.GUI.Style.Current; + _horizontalHighlight = new Border() + { + Parent = panel, + BorderColor = style.Foreground, + BorderWidth = 1.0f, + Visible = false, + }; + + _verticalHighlight = new Border() + { + Parent = panel, + BorderColor = style.Foreground, + BorderWidth = 1.0f, + Visible = false, + }; + var upperLeftCell = new Label { Parent = gridPanel, }; - var upperRightCell = new VerticalPanel + _upperRightCell = new VerticalPanel { ClipChildren = false, Pivot = new Float2(0.00001f, 0.0f), @@ -54,7 +76,7 @@ namespace FlaxEditor.CustomEditors.Dedicated Parent = gridPanel, }; - var bottomLeftCell = new VerticalPanel + _bottomLeftCell = new VerticalPanel { Pivot = Float2.Zero, Spacing = 0, @@ -63,7 +85,7 @@ namespace FlaxEditor.CustomEditors.Dedicated Parent = gridPanel, }; - var grid = new UniformGridPanel(0) + _grid = new UniformGridPanel(0) { SlotsHorizontally = layersCount, SlotsVertically = layersCount, @@ -74,13 +96,13 @@ namespace FlaxEditor.CustomEditors.Dedicated int layerIndex = 0; for (; layerIndex < layerNames.Length; layerIndex++) { - upperRightCell.AddChild(new Label + _upperRightCell.AddChild(new Label { Height = labelsHeight, Text = layerNames[layerNames.Length - layerIndex - 1], HorizontalAlignment = TextAlignment.Near, }); - bottomLeftCell.AddChild(new Label + _bottomLeftCell.AddChild(new Label { Height = labelsHeight, Text = layerNames[layerIndex], @@ -90,13 +112,13 @@ namespace FlaxEditor.CustomEditors.Dedicated for (; layerIndex < layersCount; layerIndex++) { string name = "Layer " + layerIndex; - upperRightCell.AddChild(new Label + _upperRightCell.AddChild(new Label { Height = labelsHeight, Text = name, HorizontalAlignment = TextAlignment.Near, }); - bottomLeftCell.AddChild(new Label + _bottomLeftCell.AddChild(new Label { Height = labelsHeight, Text = name, @@ -118,7 +140,7 @@ namespace FlaxEditor.CustomEditors.Dedicated var box = new CheckBox(0, 0, true) { Tag = new Float2(_layersCount - column - 1, row), - Parent = grid, + Parent = _grid, Checked = GetBit(column, row), }; box.StateChanged += OnCheckBoxChanged; @@ -126,7 +148,7 @@ namespace FlaxEditor.CustomEditors.Dedicated } for (; column < layersCount; column++) { - grid.AddChild(new Label()); + _grid.AddChild(new Label()); } } } @@ -141,6 +163,12 @@ namespace FlaxEditor.CustomEditors.Dedicated /// public override void Refresh() { + _horizontalHighlight.Visible = false; + _verticalHighlight.Visible = false; + int selectedColumn = -1; + int selectedRow = -1; + var style = FlaxEngine.GUI.Style.Current; + // Sync check boxes for (int i = 0; i < _checkBoxes.Count; i++) { @@ -148,6 +176,39 @@ namespace FlaxEditor.CustomEditors.Dedicated int column = (int)((Float2)box.Tag).X; int row = (int)((Float2)box.Tag).Y; box.Checked = GetBit(column, row); + box.ImageColor = style.BorderSelected * 1.2f; + + if(box.IsMouseOver) + { + selectedColumn = column; + selectedRow = row; + + _horizontalHighlight.X = _grid.X - _bottomLeftCell.Width; + _horizontalHighlight.Y = _grid.Y + box.Y; + _horizontalHighlight.Width = _bottomLeftCell.Width + box.Width + box.X; + _horizontalHighlight.Height = box.Height; + _horizontalHighlight.Visible = true; + + _verticalHighlight.X = _grid.X + box.X; + _verticalHighlight.Y = _grid.Y - _upperRightCell.Height; + _verticalHighlight.Width = box.Width; + _verticalHighlight.Height = _upperRightCell.Height + box.Height + box.Y; + _verticalHighlight.Visible = true; + } + } + + if(selectedColumn > -1 && selectedRow > -1) + { + for (int i = 0; i < _checkBoxes.Count; i++) + { + var box = _checkBoxes[i]; + int column = (int)((Float2)box.Tag).X; + int row = (int)((Float2)box.Tag).Y; + if(column == selectedColumn || row == selectedRow) + continue; + + box.ImageColor = style.BorderSelected * 0.75f; + } } } From 53e3cee1967ddbeb7b7fa8f94b7d60202da15a11 Mon Sep 17 00:00:00 2001 From: Zode Date: Sat, 7 Jun 2025 17:15:54 +0300 Subject: [PATCH 005/122] Scroll selected to view when emptying out search box --- Source/Editor/Windows/SceneTreeWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 705976e6e..78aa123ea 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -144,6 +144,7 @@ namespace FlaxEditor.Windows _tree.UnlockChildrenRecursive(); PerformLayout(); PerformLayout(); + ScrollToSelectedNode(); } private void Spawn(Type type) From cce042045e8ebc7d869003f7ccfe525a598b0677 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Sat, 7 Jun 2025 17:34:01 +0200 Subject: [PATCH 006/122] wrap scrolling items with arrow keys and simplify scrolling logic --- Source/Editor/GUI/ItemsListContextMenu.cs | 51 +++++++++-------------- 1 file changed, 19 insertions(+), 32 deletions(-) diff --git a/Source/Editor/GUI/ItemsListContextMenu.cs b/Source/Editor/GUI/ItemsListContextMenu.cs index 82699e55c..58f8c2eb4 100644 --- a/Source/Editor/GUI/ItemsListContextMenu.cs +++ b/Source/Editor/GUI/ItemsListContextMenu.cs @@ -563,8 +563,17 @@ namespace FlaxEditor.GUI case KeyboardKeys.Escape: Hide(); return true; + case KeyboardKeys.Backspace: + // Alow the user to quickly focus the searchbar + if (_searchBox != null && !_searchBox.IsFocused) + { + _searchBox.Focus(); + _searchBox.SelectAll(); + return true; + } + break; case KeyboardKeys.ArrowDown: - { + case KeyboardKeys.ArrowUp: if (RootWindow.FocusedControl == null) { // Focus search box if nothing is focused @@ -572,39 +581,17 @@ namespace FlaxEditor.GUI return true; } - // Focus the first visible item or then next one + // Get the next item var items = GetVisibleItems(); var focusedIndex = items.IndexOf(focusedItem); - if (focusedIndex == -1) - focusedIndex = -1; - if (focusedIndex + 1 < items.Count) - { - var item = items[focusedIndex + 1]; - item.Focus(); - _scrollPanel.ScrollViewTo(item); - return true; - } - break; - } - case KeyboardKeys.ArrowUp: - if (focusedItem != null) - { - // Focus the previous visible item or the search box - var items = GetVisibleItems(); - var focusedIndex = items.IndexOf(focusedItem); - if (focusedIndex == 0) - { - _searchBox?.Focus(); - } - else if (focusedIndex > 0) - { - var item = items[focusedIndex - 1]; - item.Focus(); - _scrollPanel.ScrollViewTo(item); - return true; - } - } - break; + int delta = key == KeyboardKeys.ArrowDown ? -1 : 1; + int nextIndex = Mathf.Wrap(focusedIndex - delta, 0, items.Count - 1); + var nextItem = items[nextIndex]; + + // Focus the next item + nextItem.Focus(); + _scrollPanel.ScrollViewTo(nextItem); + return true; case KeyboardKeys.Return: if (focusedItem != null) { From 85fd1389dbb7698ec5defb476c4273fe417f8f35 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Sat, 7 Jun 2025 18:17:40 +0200 Subject: [PATCH 007/122] clear content panel search when selecting asset picker asset --- Source/Editor/GUI/AssetPicker.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index b3690e754..bf04aa068 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -299,6 +299,7 @@ namespace FlaxEditor.GUI { // Select asset Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); + Editor.Instance.Windows.ContentWin.ClearItemsSearch(); } } else if (Button1Rect.Contains(location)) @@ -312,6 +313,7 @@ namespace FlaxEditor.GUI { // Select asset Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); + Editor.Instance.Windows.ContentWin.ClearItemsSearch(); } else if (Button3Rect.Contains(location)) { From 1d6306761476fbfc7f2ca830e9c0422a4c067877 Mon Sep 17 00:00:00 2001 From: Zode Date: Sat, 7 Jun 2025 21:04:59 +0300 Subject: [PATCH 008/122] Add ability to do additive and subtractive selections in visject surfaces using ctrl and shift during selection. --- Source/Editor/Surface/VisjectSurface.Input.cs | 68 ++++++++++++++++--- Source/Editor/Surface/VisjectSurface.cs | 12 ++++ 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index a874db681..09df195eb 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -27,6 +27,7 @@ namespace FlaxEditor.Surface private Float2 _movingNodesDelta; private Float2 _gridRoundingDelta; private HashSet _movingNodes; + private HashSet _temporarySelectedNodes; private readonly Stack _inputBrackets = new Stack(); private class InputBracket @@ -130,13 +131,34 @@ namespace FlaxEditor.Surface if (_rootControl.Children[i] is SurfaceControl control) { var select = control.IsSelectionIntersecting(ref selectionRect); - if (select != control.IsSelected) + + if (Root.GetKey(KeyboardKeys.Shift)) { - control.IsSelected = select; - selectionChanged = true; + if (select == control.IsSelected && _temporarySelectedNodes.Contains(control)) + { + control.IsSelected = !select; + selectionChanged = true; + } + } + else if (Root.GetKey(KeyboardKeys.Control)) + { + if (select != control.IsSelected && !_temporarySelectedNodes.Contains(control)) + { + control.IsSelected = select; + selectionChanged = true; + } + } + else + { + if (select != control.IsSelected) + { + control.IsSelected = select; + selectionChanged = true; + } } } } + if (selectionChanged) SelectionChanged?.Invoke(); } @@ -461,6 +483,19 @@ namespace FlaxEditor.Surface // Cache data _isMovingSelection = false; _mousePos = location; + if(_temporarySelectedNodes == null) + _temporarySelectedNodes = new HashSet(); + else + _temporarySelectedNodes.Clear(); + + for (int i = 0; i < _rootControl.Children.Count; i++) + { + if (_rootControl.Children[i] is SurfaceNode node && node.IsSelected) + { + _temporarySelectedNodes.Add(node); + } + } + if (button == MouseButton.Left) { _leftMouseDown = true; @@ -488,9 +523,11 @@ namespace FlaxEditor.Surface // Check if user is pressing control if (Root.GetKey(KeyboardKeys.Control)) { - // Add/remove from selection - controlUnderMouse.IsSelected = !controlUnderMouse.IsSelected; - SelectionChanged?.Invoke(); + AddToSelection(controlUnderMouse); + } + else if (Root.GetKey(KeyboardKeys.Shift)) + { + RemoveFromSelection(controlUnderMouse); } // Check if node isn't selected else if (!controlUnderMouse.IsSelected) @@ -500,10 +537,14 @@ namespace FlaxEditor.Surface } // Start moving selected nodes - StartMouseCapture(); - _movingSelectionViewPos = _rootControl.Location; - _movingNodesDelta = Float2.Zero; - OnGetNodesToMove(); + if (!Root.GetKey(KeyboardKeys.Shift)) + { + StartMouseCapture(); + _movingSelectionViewPos = _rootControl.Location; + _movingNodesDelta = Float2.Zero; + OnGetNodesToMove(); + } + Focus(); return true; } @@ -515,7 +556,12 @@ namespace FlaxEditor.Surface { // Start selecting or commenting StartMouseCapture(); - ClearSelection(); + + if (!Root.GetKey(KeyboardKeys.Control) && !Root.GetKey(KeyboardKeys.Shift)) + { + ClearSelection(); + } + Focus(); return true; } diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index faecebbd3..69bd276d2 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -710,6 +710,18 @@ namespace FlaxEditor.Surface SelectionChanged?.Invoke(); } + /// + /// Removes the specified control from the selection. + /// + /// The control. + public void RemoveFromSelection(SurfaceControl control) + { + if (!control.IsSelected) + return; + control.IsSelected = false; + SelectionChanged?.Invoke(); + } + /// /// Selects the specified control. /// From 74000fa76622597b4183bd50116c6b848e2d7363 Mon Sep 17 00:00:00 2001 From: Zode Date: Sat, 7 Jun 2025 22:53:07 +0300 Subject: [PATCH 009/122] Expand tree if necessary, so the selected node will be shown --- Source/Editor/Windows/SceneTreeWindow.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 78aa123ea..8f06008a5 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -142,9 +142,21 @@ namespace FlaxEditor.Windows root.TreeNode.UpdateFilter(query); _tree.UnlockChildrenRecursive(); + + var nodeSelection = _tree.Selection; + if(nodeSelection.Count != 0) + { + var node = nodeSelection[nodeSelection.Count - 1]; + node.Expand(true); + } + PerformLayout(); PerformLayout(); - ScrollToSelectedNode(); + + if(nodeSelection.Count != 0) + { + ScrollToSelectedNode(); + } } private void Spawn(Type type) From 568719b615db6e7a6998c73ac18805328b0be215 Mon Sep 17 00:00:00 2001 From: Zode Date: Sat, 7 Jun 2025 23:21:41 +0300 Subject: [PATCH 010/122] Fix scroll issue caused by tree's defered layout update --- Source/Editor/Windows/SceneTreeWindow.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 8f06008a5..20cfa3584 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -34,6 +34,7 @@ namespace FlaxEditor.Windows private DragScriptItems _dragScriptItems; private DragHandlers _dragHandlers; private bool _isDropping = false; + private bool _forceScrollNodeToView = false; /// /// Scene tree panel. @@ -148,13 +149,21 @@ namespace FlaxEditor.Windows { var node = nodeSelection[nodeSelection.Count - 1]; node.Expand(true); + _forceScrollNodeToView = true; } PerformLayout(); PerformLayout(); + } - if(nodeSelection.Count != 0) + /// + public override void Update(float deltaTime) + { + base.Update(deltaTime); + + if(_tree.Selection.Count != 0 && _forceScrollNodeToView) { + _forceScrollNodeToView = false; ScrollToSelectedNode(); } } From 1fa83639c2f1e729c918b4ccd1a181e5f26b0201 Mon Sep 17 00:00:00 2001 From: Zode Date: Sun, 8 Jun 2025 00:38:59 +0300 Subject: [PATCH 011/122] Fix update order inconsistencies between machines by exposing an action for when defered layout happens. --- Source/Editor/GUI/Tree/Tree.cs | 6 ++++++ Source/Editor/Windows/SceneTreeWindow.cs | 19 +++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Source/Editor/GUI/Tree/Tree.cs b/Source/Editor/GUI/Tree/Tree.cs index e36f0ccd5..3e1453f03 100644 --- a/Source/Editor/GUI/Tree/Tree.cs +++ b/Source/Editor/GUI/Tree/Tree.cs @@ -73,6 +73,11 @@ namespace FlaxEditor.GUI.Tree /// public bool DrawRootTreeLine = true; + /// + /// Occurs when the defered layouting happens + /// + public event Action OnDeferedLayout; + /// /// Gets or sets the margin for the child tree nodes. /// @@ -375,6 +380,7 @@ namespace FlaxEditor.GUI.Tree if (_deferLayoutUpdate) { base.PerformLayout(); + OnDeferedLayout?.Invoke(); _deferLayoutUpdate = false; } diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 20cfa3584..9175a660b 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -92,6 +92,13 @@ namespace FlaxEditor.Windows _tree.SelectedChanged += Tree_OnSelectedChanged; _tree.RightClick += OnTreeRightClick; _tree.Parent = _sceneTreePanel; + _tree.OnDeferedLayout += () => { + if(_tree.Selection.Count != 0 && _forceScrollNodeToView) + { + _forceScrollNodeToView = false; + ScrollToSelectedNode(); + } + }; headerPanel.Parent = this; // Setup input actions @@ -156,18 +163,6 @@ namespace FlaxEditor.Windows PerformLayout(); } - /// - public override void Update(float deltaTime) - { - base.Update(deltaTime); - - if(_tree.Selection.Count != 0 && _forceScrollNodeToView) - { - _forceScrollNodeToView = false; - ScrollToSelectedNode(); - } - } - private void Spawn(Type type) { // Create actor From ecd5559aada8b76b5508254232ee2f5ee36f82aa Mon Sep 17 00:00:00 2001 From: Zode Date: Sun, 8 Jun 2025 00:41:02 +0300 Subject: [PATCH 012/122] Clean up a bit code after moving it around. --- Source/Editor/Windows/SceneTreeWindow.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 9175a660b..620300485 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -93,12 +93,13 @@ namespace FlaxEditor.Windows _tree.RightClick += OnTreeRightClick; _tree.Parent = _sceneTreePanel; _tree.OnDeferedLayout += () => { - if(_tree.Selection.Count != 0 && _forceScrollNodeToView) + if(_forceScrollNodeToView) { _forceScrollNodeToView = false; ScrollToSelectedNode(); } }; + headerPanel.Parent = this; // Setup input actions From f1945552ab4bff1a0cd083ee143101f7ebd47aa9 Mon Sep 17 00:00:00 2001 From: Zode Date: Sun, 8 Jun 2025 02:42:28 +0300 Subject: [PATCH 013/122] Add horizontal and vertical distribution --- .../Surface/VisjectSurface.ContextMenu.cs | 6 ++ .../Surface/VisjectSurface.Formatting.cs | 75 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs index ae836bb11..98508046d 100644 --- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs +++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs @@ -199,6 +199,8 @@ namespace FlaxEditor.Surface private ContextMenuButton _cmAlignNodesLeftButton; private ContextMenuButton _cmAlignNodesCenterButton; private ContextMenuButton _cmAlignNodesRightButton; + private ContextMenuButton _cmDistributeNodesHorizontallyButton; + private ContextMenuButton _cmDistributeNodesVerticallyButton; private ContextMenuButton _cmRemoveNodeConnectionsButton; private ContextMenuButton _cmRemoveBoxConnectionsButton; private readonly Float2 ContextMenuOffset = new Float2(5); @@ -421,6 +423,10 @@ namespace FlaxEditor.Surface _cmAlignNodesCenterButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align center", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }); _cmAlignNodesRightButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align right", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }); + _cmFormatNodesMenu.ContextMenu.AddSeparator(); + _cmDistributeNodesHorizontallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute horizontally", () => { DistributeNodes(SelectedNodes, false); }); + _cmDistributeNodesVerticallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute vertically", () => { DistributeNodes(SelectedNodes, true); }); + _cmRemoveNodeConnectionsButton = menu.AddButton("Remove all connections to that node(s)", () => { var nodes = ((List)menu.Tag); diff --git a/Source/Editor/Surface/VisjectSurface.Formatting.cs b/Source/Editor/Surface/VisjectSurface.Formatting.cs index 6b942f4a1..2ff48b290 100644 --- a/Source/Editor/Surface/VisjectSurface.Formatting.cs +++ b/Source/Editor/Surface/VisjectSurface.Formatting.cs @@ -324,5 +324,80 @@ namespace FlaxEditor.Surface MarkAsEdited(false); Undo?.AddAction(new MultiUndoAction(undoActions, $"Align nodes ({alignmentType})")); } + + /// + /// Distribute the given nodes as equally as possible inside the bounding box, if no fit can be done it will use a default pad of 10 pixels between nodes. + /// + /// List of nodes + /// If false will be done horizontally, if true will be done vertically + public void DistributeNodes(List nodes, bool vertically) + { + if(nodes.Count <= 1) + return; + + var undoActions = new List(); + var boundingBox = GetNodesBounds(nodes); + float padding = 10; + float totalSize = 0; + for (int i = 0; i < nodes.Count; i++) + { + if (vertically) + { + totalSize += nodes[i].Height; + } + else + { + totalSize += nodes[i].Width; + } + } + + if(vertically) + { + nodes.Sort((leftValue, rightValue) => { return leftValue.Y.CompareTo(rightValue.Y); }); + + float position = boundingBox.Top; + if(totalSize < boundingBox.Height) + { + padding = (boundingBox.Height - totalSize) / nodes.Count; + } + + for(int i = 0; i < nodes.Count; i++) + { + var newLocation = new Float2(nodes[i].X, position); + var locationDelta = newLocation - nodes[i].Location; + nodes[i].Location = newLocation; + + position += nodes[i].Height + padding; + + if (Undo != null) + undoActions.Add(new MoveNodesAction(Context, new[] { nodes[i].ID }, locationDelta)); + } + } + else + { + nodes.Sort((leftValue, rightValue) => { return leftValue.X.CompareTo(rightValue.X); }); + + float position = boundingBox.Left; + if(totalSize < boundingBox.Width) + { + padding = (boundingBox.Width - totalSize) / nodes.Count; + } + + for(int i = 0; i < nodes.Count; i++) + { + var newLocation = new Float2(position, nodes[i].Y); + var locationDelta = newLocation - nodes[i].Location; + nodes[i].Location = newLocation; + + position += nodes[i].Width + padding; + + if (Undo != null) + undoActions.Add(new MoveNodesAction(Context, new[] { nodes[i].ID }, locationDelta)); + } + } + + MarkAsEdited(false); + Undo?.AddAction(new MultiUndoAction(undoActions, vertically ? "Distribute nodes vertically" : "Distribute nodes horizontally")); + } } } From 9c9d560ce568c1a02afb557def492673c56f939d Mon Sep 17 00:00:00 2001 From: Zode Date: Sun, 8 Jun 2025 03:32:51 +0300 Subject: [PATCH 014/122] Add hotkeys to visject formatting --- Source/Editor/Options/InputOptions.cs | 40 +++++++++++++++++++ .../Surface/VisjectSurface.ContextMenu.cs | 18 ++++----- Source/Editor/Surface/VisjectSurface.cs | 9 +++++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index ff7971667..4683284b8 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -647,5 +647,45 @@ namespace FlaxEditor.Options public InputBinding VisualScriptDebuggerWindow = new InputBinding(KeyboardKeys.None); #endregion + + #region Node editors + + [DefaultValue(typeof(InputBinding), "Shift+W")] + [EditorDisplay("Node editors"), EditorOrder(4500)] + public InputBinding NodesAlignTop = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift); + + [DefaultValue(typeof(InputBinding), "Shift+A")] + [EditorDisplay("Node editors"), EditorOrder(4510)] + public InputBinding NodesAlignLeft = new InputBinding(KeyboardKeys.A, KeyboardKeys.Shift); + + [DefaultValue(typeof(InputBinding), "Shift+S")] + [EditorDisplay("Node editors"), EditorOrder(4520)] + public InputBinding NodesAlignBottom = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift); + + [DefaultValue(typeof(InputBinding), "Shift+D")] + [EditorDisplay("Node editors"), EditorOrder(4530)] + public InputBinding NodesAlignRight = new InputBinding(KeyboardKeys.D, KeyboardKeys.Shift); + + [DefaultValue(typeof(InputBinding), "Alt+Shift+W")] + [EditorDisplay("Node editors"), EditorOrder(4540)] + public InputBinding NodesAlignMiddle = new InputBinding(KeyboardKeys.W, KeyboardKeys.Shift, KeyboardKeys.Alt); + + [DefaultValue(typeof(InputBinding), "Alt+Shift+S")] + [EditorDisplay("Node editors"), EditorOrder(4550)] + public InputBinding NodesAlignCenter = new InputBinding(KeyboardKeys.S, KeyboardKeys.Shift, KeyboardKeys.Alt); + + [DefaultValue(typeof(InputBinding), "Q")] + [EditorDisplay("Node editors"), EditorOrder(4560)] + public InputBinding NodesAutoFormat = new InputBinding(KeyboardKeys.Q); + + [DefaultValue(typeof(InputBinding), "None")] + [EditorDisplay("Node editors"), EditorOrder(4570)] + public InputBinding NodesDistributeHorizontal = new InputBinding(KeyboardKeys.None); + + [DefaultValue(typeof(InputBinding), "None")] + [EditorDisplay("Node editors"), EditorOrder(4580)] + public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.None); + + #endregion } } diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs index 98508046d..84055aaf0 100644 --- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs +++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs @@ -411,21 +411,21 @@ namespace FlaxEditor.Surface _cmFormatNodesMenu = menu.AddChildMenu("Format node(s)"); _cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection; - _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", () => { FormatGraph(SelectedNodes); }); + _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", Editor.Instance.Options.Options.Input.NodesAutoFormat, () => { FormatGraph(SelectedNodes); }); _cmFormatNodesMenu.ContextMenu.AddSeparator(); - _cmAlignNodesTopButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align top", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); }); - _cmAlignNodesMiddleButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align middle", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); }); - _cmAlignNodesBottomButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align bottom", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); }); + _cmAlignNodesTopButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align top", Editor.Instance.Options.Options.Input.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); }); + _cmAlignNodesMiddleButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align middle", Editor.Instance.Options.Options.Input.NodesAlignMiddle, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); }); + _cmAlignNodesBottomButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align bottom", Editor.Instance.Options.Options.Input.NodesAlignBottom, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); }); _cmFormatNodesMenu.ContextMenu.AddSeparator(); - _cmAlignNodesLeftButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align left", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); }); - _cmAlignNodesCenterButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align center", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }); - _cmAlignNodesRightButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align right", () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }); + _cmAlignNodesLeftButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align left", Editor.Instance.Options.Options.Input.NodesAlignLeft, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); }); + _cmAlignNodesCenterButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align center", Editor.Instance.Options.Options.Input.NodesAlignCenter, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }); + _cmAlignNodesRightButton = _cmFormatNodesMenu.ContextMenu.AddButton("Align right", Editor.Instance.Options.Options.Input.NodesAlignRight, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }); _cmFormatNodesMenu.ContextMenu.AddSeparator(); - _cmDistributeNodesHorizontallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute horizontally", () => { DistributeNodes(SelectedNodes, false); }); - _cmDistributeNodesVerticallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute vertically", () => { DistributeNodes(SelectedNodes, true); }); + _cmDistributeNodesHorizontallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute horizontally", Editor.Instance.Options.Options.Input.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }); + _cmDistributeNodesVerticallyButton = _cmFormatNodesMenu.ContextMenu.AddButton("Distribute vertically", Editor.Instance.Options.Options.Input.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }); _cmRemoveNodeConnectionsButton = menu.AddButton("Remove all connections to that node(s)", () => { diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index faecebbd3..cfc5eb900 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -405,6 +405,15 @@ namespace FlaxEditor.Surface new InputActionsContainer.Binding(options => options.Paste, Paste), new InputActionsContainer.Binding(options => options.Cut, Cut), new InputActionsContainer.Binding(options => options.Duplicate, Duplicate), + new InputActionsContainer.Binding(options => options.NodesAutoFormat, () => { FormatGraph(SelectedNodes); }), + new InputActionsContainer.Binding(options => options.NodesAlignTop, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Top); }), + new InputActionsContainer.Binding(options => options.NodesAlignMiddle, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Middle); }), + new InputActionsContainer.Binding(options => options.NodesAlignBottom, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Bottom); }), + new InputActionsContainer.Binding(options => options.NodesAlignLeft, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); }), + new InputActionsContainer.Binding(options => options.NodesAlignCenter, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }), + new InputActionsContainer.Binding(options => options.NodesAlignRight, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }), + new InputActionsContainer.Binding(options => options.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }), + new InputActionsContainer.Binding(options => options.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }), }); Context.ControlSpawned += OnSurfaceControlSpawned; From 112e7284655eff9d2424186809b2f6f396bf6fb5 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Mon, 9 Jun 2025 13:56:41 +0200 Subject: [PATCH 015/122] fix scroll to node on selection in prefab editor --- Source/Editor/Windows/Assets/PrefabWindow.Selection.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Selection.cs b/Source/Editor/Windows/Assets/PrefabWindow.Selection.cs index 03e2a9652..6208aa7a1 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Selection.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Selection.cs @@ -97,10 +97,7 @@ namespace FlaxEditor.Windows.Assets // For single node selected scroll view so user can see it if (nodes.Count == 1) - { - nodes[0].ExpandAllParents(true); - ScrollViewTo(nodes[0]); - } + ScrollToSelectedNode(); } // Update properties editor From f8daff273a7b7b40ffa27179783e57aee5f92e71 Mon Sep 17 00:00:00 2001 From: Zode Date: Mon, 9 Jun 2025 14:56:54 +0300 Subject: [PATCH 016/122] Add import & export warning messageboxes. --- Source/Engine/ContentImporters/ImportAudio.cpp | 3 +++ Source/Engine/Tools/TextureTool/TextureTool.stb.cpp | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/Source/Engine/ContentImporters/ImportAudio.cpp b/Source/Engine/ContentImporters/ImportAudio.cpp index 6fd0cdc74..2c5b84d49 100644 --- a/Source/Engine/ContentImporters/ImportAudio.cpp +++ b/Source/Engine/ContentImporters/ImportAudio.cpp @@ -18,6 +18,7 @@ #include "Engine/Tools/AudioTool/OggVorbisDecoder.h" #include "Engine/Tools/AudioTool/OggVorbisEncoder.h" #include "Engine/Serialization/JsonWriters.h" +#include "Engine/Platform/MessageBox.h" bool ImportAudio::TryGetImportOptions(const StringView& path, Options& options) { @@ -118,6 +119,7 @@ CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder& } #else #define HANDLE_VORBIS(chunkIndex, dataPtr, dataSize) \ + MessageBox::Show(TEXT("Vorbis format is not supported."), TEXT("Import warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "Vorbis format is not supported."); \ return CreateAssetResult::Error; #endif @@ -140,6 +142,7 @@ CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder& break; \ default: \ { \ + MessageBox::Show(TEXT("Unknown audio format."), TEXT("Import warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); \ LOG(Warning, "Unknown audio format."); \ return CreateAssetResult::Error; \ } \ diff --git a/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp b/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp index 6fae6432e..f8f012c15 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp @@ -13,6 +13,7 @@ #include "Engine/Graphics/PixelFormatExtensions.h" #include "Engine/Utilities/AnsiPathTempFile.h" #include "Engine/Platform/File.h" +#include "Engine/Platform/MessageBox.h" #define STBI_ASSERT(x) ASSERT(x) #define STBI_MALLOC(sz) Allocator::Allocate(sz) @@ -286,21 +287,27 @@ bool TextureTool::ExportTextureStb(ImageType type, const StringView& path, const break; } case ImageType::GIF: + MessageBox::Show(TEXT("GIF format is not supported."), TEXT("Export warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "GIF format is not supported."); break; case ImageType::TIFF: + MessageBox::Show(TEXT("TIFF format is not supported."), TEXT("Export warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "GIF format is not supported."); break; case ImageType::DDS: + MessageBox::Show(TEXT("DDS format is not supported."), TEXT("Export warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "DDS format is not supported."); break; case ImageType::RAW: + MessageBox::Show(TEXT("RAW format is not supported."), TEXT("Export warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "RAW format is not supported."); break; case ImageType::EXR: + MessageBox::Show(TEXT("EXR format is not supported."), TEXT("Export warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "EXR format is not supported."); break; default: + MessageBox::Show(TEXT("Unknown format."), TEXT("Export warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "Unknown format."); break; } @@ -434,17 +441,21 @@ bool TextureTool::ImportTextureStb(ImageType type, const StringView& path, Textu free(pixels); #else + MessageBox::Show(TEXT("EXR format is not supported."), TEXT("Import warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "EXR format is not supported."); #endif break; } case ImageType::DDS: + MessageBox::Show(TEXT("DDS format is not supported."), TEXT("Import warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "DDS format is not supported."); break; case ImageType::TIFF: + MessageBox::Show(TEXT("TIFF format is not supported."), TEXT("Import warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "TIFF format is not supported."); break; default: + MessageBox::Show(TEXT("Unknown format."), TEXT("Import warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "Unknown format."); return true; } From 8dfb564fb3db0ecd2fa7600cf7ebea2f16a7ec2f Mon Sep 17 00:00:00 2001 From: Zode Date: Mon, 9 Jun 2025 15:04:34 +0300 Subject: [PATCH 017/122] Add messagebox to windows import too --- Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp b/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp index 6d0421e3c..01ed4ca34 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp @@ -8,6 +8,7 @@ #include "Engine/Platform/File.h" #include "Engine/Platform/CriticalSection.h" #include "Engine/Platform/ConditionVariable.h" +#include "Engine/Platform/MessageBox.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/Async/GPUTask.h" #include "Engine/Graphics/Textures/TextureData.h" @@ -358,6 +359,7 @@ HRESULT LoadFromEXRFile(const StringView& path, DirectX::ScratchImage& image) free(pixels); return result; #else + MessageBox::Show(TEXT("EXR format is not supported."), TEXT("Import warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); LOG(Warning, "EXR format is not supported."); return E_FAIL; #endif From 7fc564a0ac61a007ef5f3a4b7be12ad9af4c0ca6 Mon Sep 17 00:00:00 2001 From: Zode Date: Mon, 9 Jun 2025 15:07:52 +0300 Subject: [PATCH 018/122] Pop error box on lightmap UV generation also --- Source/Engine/Graphics/Models/ModelData.Tool.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Graphics/Models/ModelData.Tool.cpp b/Source/Engine/Graphics/Models/ModelData.Tool.cpp index 52663c271..f1c84cfb7 100644 --- a/Source/Engine/Graphics/Models/ModelData.Tool.cpp +++ b/Source/Engine/Graphics/Models/ModelData.Tool.cpp @@ -12,6 +12,7 @@ #include "Engine/Tools/ModelTool/VertexTriangleAdjacency.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Platform/Platform.h" +#include "Engine/Platform/MessageBox.h" #define USE_MIKKTSPACE 1 #include "ThirdParty/MikkTSpace/mikktspace.h" #if USE_ASSIMP @@ -181,6 +182,7 @@ bool MeshData::GenerateLightmapUVs() for (int32 i = 0; i < (int32)vb.size(); i++) lightmapChannel.Get()[i] = *(Float2*)&vb[i].uv; #else + MessageBox::Show(TEXT("Model lightmap UVs generation is not supported on this platform."), TEXT("Import error"), MessageBoxButtons::OK, MessageBoxIcon::Error); LOG(Error, "Model lightmap UVs generation is not supported on this platform."); #endif From 9e50a39ebf5b1adf9ce98b1e8c209e6cc8977fcc Mon Sep 17 00:00:00 2001 From: Zode Date: Mon, 9 Jun 2025 16:19:39 +0300 Subject: [PATCH 019/122] Add define guards so that the messagebox only appears in editor builds --- Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp b/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp index 01ed4ca34..bf74213d4 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp @@ -8,12 +8,12 @@ #include "Engine/Platform/File.h" #include "Engine/Platform/CriticalSection.h" #include "Engine/Platform/ConditionVariable.h" -#include "Engine/Platform/MessageBox.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/Async/GPUTask.h" #include "Engine/Graphics/Textures/TextureData.h" #include "Engine/Graphics/PixelFormatExtensions.h" #if USE_EDITOR +#include "Engine/Platform/MessageBox.h" #include "Engine/Graphics/GPUDevice.h" #endif #include "Engine/Utilities/AnsiPathTempFile.h" @@ -359,7 +359,9 @@ HRESULT LoadFromEXRFile(const StringView& path, DirectX::ScratchImage& image) free(pixels); return result; #else +#if USE_EDITOR MessageBox::Show(TEXT("EXR format is not supported."), TEXT("Import warning"), MessageBoxButtons::OK, MessageBoxIcon::Warning); +#endif LOG(Warning, "EXR format is not supported."); return E_FAIL; #endif From 2109a2d26153d163e3da5d5ed0324ec7b58e8489 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Mon, 9 Jun 2025 18:40:12 +0200 Subject: [PATCH 020/122] update content finder popup to wrap on keyboard navigation - Fixes auto scroll - Make it wrap - Add pressing backspace to focus search bar and select all text --- Source/Editor/Windows/Search/ContentFinder.cs | 47 +++++++------------ 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/Source/Editor/Windows/Search/ContentFinder.cs b/Source/Editor/Windows/Search/ContentFinder.cs index e4227e5c4..12b479aea 100644 --- a/Source/Editor/Windows/Search/ContentFinder.cs +++ b/Source/Editor/Windows/Search/ContentFinder.cs @@ -180,39 +180,17 @@ namespace FlaxEditor.Windows.Search switch (key) { case KeyboardKeys.ArrowDown: - { - if (_matchedItems.Count == 0) - return true; - int currentPos; - if (_selectedItem != null) - { - currentPos = _matchedItems.IndexOf(_selectedItem) + 1; - if (currentPos >= _matchedItems.Count) - currentPos--; - } - else - { - currentPos = 0; - } - SelectedItem = _matchedItems[currentPos]; - return true; - } case KeyboardKeys.ArrowUp: { if (_matchedItems.Count == 0) return true; - int currentPos; - if (_selectedItem != null) - { - currentPos = _matchedItems.IndexOf(_selectedItem) - 1; - if (currentPos < 0) - currentPos = 0; - } - else - { - currentPos = 0; - } - SelectedItem = _matchedItems[currentPos]; + + var focusedIndex = _matchedItems.IndexOf(_selectedItem); + int delta = key == KeyboardKeys.ArrowDown ? -1 : 1; + int nextIndex = Mathf.Wrap(focusedIndex - delta, 0, _matchedItems.Count - 1); + var nextItem = _matchedItems[nextIndex]; + + SelectedItem = nextItem; return true; } case KeyboardKeys.Return: @@ -234,6 +212,17 @@ namespace FlaxEditor.Windows.Search Hide(); return true; } + case KeyboardKeys.Backspace: + { + // Alow the user to quickly focus the searchbar + if (_searchBox != null && !_searchBox.IsFocused) + { + _searchBox.Focus(); + _searchBox.SelectAll(); + return true; + } + break; + } } return base.OnKeyDown(key); From 793bc33b2fa272cf9717c336ef62449515ca1da4 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Mon, 9 Jun 2025 18:40:33 +0200 Subject: [PATCH 021/122] fix right clicking on item reset search and scroll --- Source/Editor/Windows/Search/ContentFinder.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Editor/Windows/Search/ContentFinder.cs b/Source/Editor/Windows/Search/ContentFinder.cs index 12b479aea..9118739b8 100644 --- a/Source/Editor/Windows/Search/ContentFinder.cs +++ b/Source/Editor/Windows/Search/ContentFinder.cs @@ -42,6 +42,7 @@ namespace FlaxEditor.Windows.Search if (value == _selectedItem || (value != null && !_matchedItems.Contains(value))) return; + // Restore the previous selected item to the non-selected color if (_selectedItem != null) { _selectedItem.BackgroundColor = Color.Transparent; @@ -54,6 +55,7 @@ namespace FlaxEditor.Windows.Search _selectedItem.BackgroundColor = Style.Current.BackgroundSelected; if (_matchedItems.Count > VisibleItemCount) { + _selectedItem.Focus(); _resultPanel.ScrollViewTo(_selectedItem, true); } } From e1a2a369780f215758a62c25484bd86495bbded9 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 9 Jun 2025 15:02:43 -0500 Subject: [PATCH 022/122] Add GetOrAddButton utility for MainMenu. --- Source/Editor/GUI/MainMenu.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Source/Editor/GUI/MainMenu.cs b/Source/Editor/GUI/MainMenu.cs index 0b959d2c9..bce668451 100644 --- a/Source/Editor/GUI/MainMenu.cs +++ b/Source/Editor/GUI/MainMenu.cs @@ -266,6 +266,19 @@ namespace FlaxEditor.GUI return AddChild(new MainMenuButton(text)); } + /// + /// Gets or adds a button. + /// + /// The button text + /// The existing or created button control. + public MainMenuButton GetOrAddButton(string text) + { + MainMenuButton result = GetButton(text); + if (result == null) + result = AddButton(text); + return result; + } + /// /// Gets the button. /// From c1e782bb32c042617f605b6c3de81869eddd00a9 Mon Sep 17 00:00:00 2001 From: Zode Date: Mon, 9 Jun 2025 23:34:02 +0300 Subject: [PATCH 023/122] Add hotkey to quick focus debug console input --- Source/Editor/Options/InputOptions.cs | 4 ++++ Source/Editor/Utilities/Utils.cs | 1 + Source/Editor/Windows/GameWindow.cs | 1 + Source/Editor/Windows/OutputLogWindow.cs | 9 +++++++++ 4 files changed, 15 insertions(+) diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index ff7971667..ba601052c 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -139,6 +139,10 @@ namespace FlaxEditor.Options [EditorDisplay("Common"), EditorOrder(240)] public InputBinding ToggleFullscreen = new InputBinding(KeyboardKeys.F11); + [DefaultValue(typeof(InputBinding), "Ctrl+BackQuote")] + [EditorDisplay("Common"), EditorOrder(250)] + public InputBinding FocusConsoleCommand = new InputBinding(KeyboardKeys.BackQuote, KeyboardKeys.Control); + #endregion #region File diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs index 179e50ebb..949479783 100644 --- a/Source/Editor/Utilities/Utils.cs +++ b/Source/Editor/Utilities/Utils.cs @@ -1518,6 +1518,7 @@ namespace FlaxEditor.Utilities inputActions.Add(options => options.OpenScriptsProject, () => Editor.Instance.CodeEditing.OpenSolution()); inputActions.Add(options => options.GenerateScriptsProject, () => Editor.Instance.ProgressReporting.GenerateScriptsProjectFiles.RunAsync()); inputActions.Add(options => options.RecompileScripts, ScriptsBuilder.Compile); + inputActions.Add(options => options.FocusConsoleCommand, () => Editor.Instance.Windows.OutputLogWin.FocusCommand()); } internal static string ToPathProject(string path) diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index 4db3bcf0f..e05f0d2db 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -405,6 +405,7 @@ namespace FlaxEditor.Windows return; Editor.Instance.SceneEditing.Delete(); }); + InputActions.Add(options => options.FocusConsoleCommand, () => Editor.Instance.Windows.OutputLogWin.FocusCommand()); } private void ChangeViewportRatio(ViewportScaleOptions v) diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 6fc0659d7..be6e6ff4d 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -830,6 +830,15 @@ namespace FlaxEditor.Windows OnOutputTextChanged(); } + /// + /// Focus the debug command line and ensure that the output log window is visible. + /// + public void FocusCommand() + { + FocusOrShow(); + _commandLineBox.Focus(); + } + /// public override void Update(float deltaTime) { From b418ab5275ceec1b3f3e8fb223985bba3234aeb8 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Tue, 10 Jun 2025 14:58:31 +0200 Subject: [PATCH 024/122] reduce flickering in highlights and boxes --- .../Dedicated/LayersMatrixEditor.cs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/LayersMatrixEditor.cs b/Source/Editor/CustomEditors/Dedicated/LayersMatrixEditor.cs index f853ccef7..7e7f8483e 100644 --- a/Source/Editor/CustomEditors/Dedicated/LayersMatrixEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/LayersMatrixEditor.cs @@ -1,6 +1,5 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using System; using System.Collections.Generic; using FlaxEditor.Content.Settings; using FlaxEngine; @@ -163,11 +162,17 @@ namespace FlaxEditor.CustomEditors.Dedicated /// public override void Refresh() { - _horizontalHighlight.Visible = false; - _verticalHighlight.Visible = false; int selectedColumn = -1; int selectedRow = -1; var style = FlaxEngine.GUI.Style.Current; + bool mouseOverGrid = _grid.IsMouseOver; + + // Only hide highlights if mouse is not over the grid to reduce flickering + if (!mouseOverGrid) + { + _horizontalHighlight.Visible = false; + _verticalHighlight.Visible = false; + } // Sync check boxes for (int i = 0; i < _checkBoxes.Count; i++) @@ -176,9 +181,8 @@ namespace FlaxEditor.CustomEditors.Dedicated int column = (int)((Float2)box.Tag).X; int row = (int)((Float2)box.Tag).Y; box.Checked = GetBit(column, row); - box.ImageColor = style.BorderSelected * 1.2f; - - if(box.IsMouseOver) + + if (box.IsMouseOver) { selectedColumn = column; selectedRow = row; @@ -197,17 +201,18 @@ namespace FlaxEditor.CustomEditors.Dedicated } } - if(selectedColumn > -1 && selectedRow > -1) + for (int i = 0; i < _checkBoxes.Count; i++) { - for (int i = 0; i < _checkBoxes.Count; i++) - { - var box = _checkBoxes[i]; - int column = (int)((Float2)box.Tag).X; - int row = (int)((Float2)box.Tag).Y; - if(column == selectedColumn || row == selectedRow) - continue; + var box = _checkBoxes[i]; + int column = (int)((Float2)box.Tag).X; + int row = (int)((Float2)box.Tag).Y; - box.ImageColor = style.BorderSelected * 0.75f; + if (!mouseOverGrid) + box.ImageColor = style.BorderSelected; + else if (selectedColumn > -1 && selectedRow > -1) + { + bool isRowOrColumn = column == selectedColumn || row == selectedRow; + box.ImageColor = style.BorderSelected * (isRowOrColumn ? 1.2f : 0.75f); } } } From dacc0068e0e76c2c93cfa8b9bb96f0bfec04a261 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Tue, 10 Jun 2025 16:49:30 +0200 Subject: [PATCH 025/122] dont scroll to node after scripts compilation --- Source/Editor/Windows/SceneTreeWindow.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 999be7f94..80b709eb6 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -27,7 +27,7 @@ namespace FlaxEditor.Windows private Panel _sceneTreePanel; private bool _isUpdatingSelection; private bool _isMouseDown; - private bool _isPlayStateChanging = false; + private bool _blockSceneTreeScroll = false; private DragAssets _dragAssets; private DragActorType _dragActorType; @@ -94,10 +94,10 @@ namespace FlaxEditor.Windows _tree.Parent = _sceneTreePanel; headerPanel.Parent = this; - Editor.PlayModeBeginning += () => _isPlayStateChanging = true; - Editor.PlayModeBegin += () => _isPlayStateChanging = false; - Editor.PlayModeEnding += () => _isPlayStateChanging = true; - Editor.PlayModeEnd += () => _isPlayStateChanging = false; + Editor.PlayModeBeginning += () => _blockSceneTreeScroll = true; + Editor.PlayModeBegin += () => _blockSceneTreeScroll = false; + Editor.PlayModeEnding += () => _blockSceneTreeScroll = true; + Editor.PlayModeEnd += () => _blockSceneTreeScroll = false; // Setup input actions InputActions.Add(options => options.TranslateMode, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate); @@ -256,7 +256,7 @@ namespace FlaxEditor.Windows _tree.Select(nodes); // For single node selected scroll view so user can see it - if (nodes.Count == 1 && !_isPlayStateChanging) + if (nodes.Count == 1 && !_blockSceneTreeScroll) { nodes[0].ExpandAllParents(true); _sceneTreePanel.ScrollViewTo(nodes[0]); @@ -266,6 +266,12 @@ namespace FlaxEditor.Windows _isUpdatingSelection = false; } + /// + public override void OnEditorStateChanged() + { + _blockSceneTreeScroll = Editor.StateMachine.ReloadingScriptsState.IsActive; + } + private bool ValidateDragAsset(AssetItem assetItem) { if (assetItem.IsOfType()) From aa59a6faf7f8a3c9b075ae402f57f0f23290efaf Mon Sep 17 00:00:00 2001 From: Zode Date: Wed, 11 Jun 2025 17:46:37 +0300 Subject: [PATCH 026/122] Extract function to lessen repeat code for debug group buttons, add shift functionality to quick toggle others. --- Source/Editor/Windows/DebugLogWindow.cs | 45 +++++++++++++++---------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs index fd6b333ef..d0680c3c0 100644 --- a/Source/Editor/Windows/DebugLogWindow.cs +++ b/Source/Editor/Windows/DebugLogWindow.cs @@ -352,24 +352,12 @@ namespace FlaxEditor.Windows editor.Options.Apply(editor.Options.Options); }).SetAutoCheck(true).LinkTooltip("Performs auto pause on error"); toolstrip.AddSeparator(); - _groupButtons[0] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Error32, () => - { - UpdateLogTypeVisibility(LogGroup.Error, _groupButtons[0].Checked); - editor.Options.Options.Interface.DebugLogShowErrorMessages = _groupButtons[0].Checked; - editor.Options.Apply(editor.Options.Options); - }).SetAutoCheck(true).LinkTooltip("Shows/hides error messages"); - _groupButtons[1] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Warning32, () => - { - UpdateLogTypeVisibility(LogGroup.Warning, _groupButtons[1].Checked); - editor.Options.Options.Interface.DebugLogShowWarningMessages = _groupButtons[1].Checked; - editor.Options.Apply(editor.Options.Options); - }).SetAutoCheck(true).LinkTooltip("Shows/hides warning messages"); - _groupButtons[2] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Info32, () => - { - UpdateLogTypeVisibility(LogGroup.Info, _groupButtons[2].Checked); - editor.Options.Options.Interface.DebugLogShowInfoMessages = _groupButtons[2].Checked; - editor.Options.Apply(editor.Options.Options); - }).SetAutoCheck(true).LinkTooltip("Shows/hides info messages"); + _groupButtons[0] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Error32, () => { OnGroupButtonPressed(0); }) + .SetAutoCheck(true).LinkTooltip("Shows/hides error messages"); + _groupButtons[1] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Warning32, () => { OnGroupButtonPressed(1); }) + .SetAutoCheck(true).LinkTooltip("Shows/hides warning messages"); + _groupButtons[2] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Info32, () => { OnGroupButtonPressed(2); }) + .SetAutoCheck(true).LinkTooltip("Shows/hides info messages"); UpdateCount(); // Split panel @@ -418,6 +406,27 @@ namespace FlaxEditor.Windows OnEditorOptionsChanged(Editor.Options.Options); } + private void OnGroupButtonPressed(int index) + { + UpdateLogTypeVisibility((LogGroup)index, _groupButtons[index].Checked); + if(Input.GetKey(KeyboardKeys.Shift)) + { + for(int i = 0; i < (int)LogGroup.Max; i++) + { + if(i == index) + continue; + + _groupButtons[i].Checked = !_groupButtons[index].Checked; + UpdateLogTypeVisibility((LogGroup)i, _groupButtons[i].Checked); + } + } + + Editor.Options.Options.Interface.DebugLogShowErrorMessages = _groupButtons[0].Checked; + Editor.Options.Options.Interface.DebugLogShowWarningMessages = _groupButtons[1].Checked; + Editor.Options.Options.Interface.DebugLogShowInfoMessages = _groupButtons[2].Checked; + Editor.Options.Apply(Editor.Options.Options); + } + private void OnEditorOptionsChanged(EditorOptions options) { _timestampsFormats = options.Interface.DebugLogTimestampsFormat; From 00055ef66329437d044ea290e52a833fe71d35aa Mon Sep 17 00:00:00 2001 From: Zode Date: Wed, 11 Jun 2025 17:47:27 +0300 Subject: [PATCH 027/122] Make tool strip buttons more responsive by also reaction to double left clicks --- Source/Editor/GUI/ToolStripButton.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Source/Editor/GUI/ToolStripButton.cs b/Source/Editor/GUI/ToolStripButton.cs index 9ef454ccc..56e798a2e 100644 --- a/Source/Editor/GUI/ToolStripButton.cs +++ b/Source/Editor/GUI/ToolStripButton.cs @@ -215,6 +215,22 @@ namespace FlaxEditor.GUI return base.OnMouseUp(location, button); } + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + if(button == MouseButton.Left) + { + if (AutoCheck) + Checked = !Checked; + Clicked?.Invoke(); + (Parent as ToolStrip)?.OnButtonClicked(this); + + return true; + } + + return false; + } + /// public override void OnMouseLeave() { From bed1f6e9cc665bc0d876d47dd388bfb915edff7d Mon Sep 17 00:00:00 2001 From: Zode Date: Wed, 11 Jun 2025 19:06:23 +0300 Subject: [PATCH 028/122] Make tag editor expand all if holding shift while clicking the ... button --- Source/Editor/CustomEditors/Editors/TagEditor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Editor/CustomEditors/Editors/TagEditor.cs b/Source/Editor/CustomEditors/Editors/TagEditor.cs index 79f700a2b..60639a8cb 100644 --- a/Source/Editor/CustomEditors/Editors/TagEditor.cs +++ b/Source/Editor/CustomEditors/Editors/TagEditor.cs @@ -604,6 +604,9 @@ namespace FlaxEditor.CustomEditors.Editors root.SortChildrenRecursive(); root.Expand(true); + if (Input.GetKey(KeyboardKeys.Shift)) + root.ExpandAll(true); + return menu; } } From 27ac755bbefd22b59352ee3f38e58f56e8da7dcf Mon Sep 17 00:00:00 2001 From: Zode Date: Wed, 11 Jun 2025 23:15:11 +0300 Subject: [PATCH 029/122] Make particle emitter editor window source code button disable itself is no source code is available --- .../Windows/Assets/ParticleEmitterWindow.cs | 16 +++++++++++++++- Source/Engine/Particles/ParticleEmitter.cpp | 17 +++++++++++++++++ Source/Engine/Particles/ParticleEmitter.h | 6 ++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs b/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs index 80eafd9e0..bc1f49443 100644 --- a/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs +++ b/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using FlaxEditor.Content; using FlaxEditor.CustomEditors; +using FlaxEditor.GUI; using FlaxEditor.Scripting; using FlaxEditor.Surface; using FlaxEditor.Viewport.Previews; @@ -114,6 +115,7 @@ namespace FlaxEditor.Windows.Assets private readonly PropertiesProxy _properties; private Tab _previewTab; + private ToolStripButton _showSourceCodeButton; /// public ParticleEmitterWindow(Editor editor, AssetItem item) @@ -146,7 +148,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip SurfaceUtils.PerformCommonSetup(this, _toolstrip, _surface, out _saveButton, out _undoButton, out _redoButton); - _toolstrip.AddButton(editor.Icons.Code64, ShowSourceCode).LinkTooltip("Show generated shader source code"); + _showSourceCodeButton = _toolstrip.AddButton(editor.Icons.Code64, ShowSourceCode); + _showSourceCodeButton.LinkTooltip("Show generated shader source code"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/particles/index.html")).LinkTooltip("See documentation to learn more"); } @@ -285,5 +288,16 @@ namespace FlaxEditor.Windows.Assets /// public SearchAssetTypes AssetType => SearchAssetTypes.ParticleEmitter; + + /// + public override void Update(float deltaTime) + { + base.Update(deltaTime); + + if(_asset == null) + return; + + _showSourceCodeButton.Enabled = _asset.HasShaderCode(); + } } } diff --git a/Source/Engine/Particles/ParticleEmitter.cpp b/Source/Engine/Particles/ParticleEmitter.cpp index 452d4560a..4490406bc 100644 --- a/Source/Engine/Particles/ParticleEmitter.cpp +++ b/Source/Engine/Particles/ParticleEmitter.cpp @@ -440,4 +440,21 @@ bool ParticleEmitter::Save(const StringView& path) return SaveSurface(data); } +bool ParticleEmitter::HasShaderCode() +{ + if(SimulationMode != ParticlesSimulationMode::GPU) + { + return false; + } + + #if COMPILE_WITH_PARTICLE_GPU_GRAPH && COMPILE_WITH_SHADER_COMPILER + if(_shaderHeader.ParticleEmitter.GraphVersion == PARTICLE_GPU_GRAPH_VERSION + && HasChunk(SHADER_FILE_CHUNK_SOURCE) + && !HasDependenciesModified()) + return true; + #endif + + return false; +} + #endif diff --git a/Source/Engine/Particles/ParticleEmitter.h b/Source/Engine/Particles/ParticleEmitter.h index b3343da1a..4b27036f7 100644 --- a/Source/Engine/Particles/ParticleEmitter.h +++ b/Source/Engine/Particles/ParticleEmitter.h @@ -173,6 +173,12 @@ public: #if USE_EDITOR void GetReferences(Array& assets, Array& files) const override; bool Save(const StringView& path = StringView::Empty) override; + + /// + /// Determine if the particle emitter has valid shader code present. + /// + /// True if particle emitter has shader code, otherwise false. + API_FUNCTION() bool HasShaderCode(); #endif protected: From 214ec9f2b1fe9de1e545c1399922e6a8245e9e88 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Thu, 12 Jun 2025 00:49:39 +0200 Subject: [PATCH 030/122] fix node delete button not checking if the user is performing certain actions on the surface before deleting node --- Source/Editor/Surface/SurfaceNode.cs | 12 +++++++----- Source/Editor/Surface/VisjectSurface.Draw.cs | 2 +- Source/Editor/Surface/VisjectSurface.Input.cs | 3 +++ Source/Editor/Surface/VisjectSurface.cs | 14 ++++++++++++-- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 8a42a7a92..5dedd604f 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -912,7 +912,7 @@ namespace FlaxEditor.Surface /// public override bool OnTestTooltipOverControl(ref Float2 location) { - return _headerRect.Contains(ref location) && ShowTooltip; + return _headerRect.Contains(ref location) && ShowTooltip && !Surface.IsConnecting && !Surface.IsBoxSelecting; } /// @@ -1070,7 +1070,7 @@ namespace FlaxEditor.Surface // Header var headerColor = style.BackgroundHighlighted; - if (_headerRect.Contains(ref _mousePosition)) + if (_headerRect.Contains(ref _mousePosition) && !Surface.IsConnecting && !Surface.IsBoxSelecting) headerColor *= 1.07f; Render2D.FillRectangle(_headerRect, headerColor); Render2D.DrawText(style.FontLarge, Title, _headerRect, style.Foreground, TextAlignment.Center, TextAlignment.Center); @@ -1078,7 +1078,8 @@ namespace FlaxEditor.Surface // Close button if ((Archetype.Flags & NodeFlags.NoCloseButton) == 0 && Surface.CanEdit) { - Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); + bool highlightClose = _closeButtonRect.Contains(_mousePosition) && !Surface.IsConnecting && !Surface.IsBoxSelecting; + Render2D.DrawSprite(style.Cross, _closeButtonRect, highlightClose ? style.Foreground : style.ForegroundGrey); } // Footer @@ -1123,8 +1124,9 @@ namespace FlaxEditor.Surface if (base.OnMouseUp(location, button)) return true; - // Close - if (button == MouseButton.Left && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && _closeButtonRect.Contains(ref location)) + // Close/ delete + bool canDelete = !Surface.IsConnecting && !Surface.WasBoxSelecting && !Surface.WasMovingSelection; + if (button == MouseButton.Left && canDelete && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && _closeButtonRect.Contains(ref location)) { Surface.Delete(this); return true; diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index 5a63fe4de..01277d0d2 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -225,7 +225,7 @@ namespace FlaxEditor.Surface _rootControl.DrawComments(); - if (IsSelecting) + if (IsBoxSelecting) { DrawSelection(); } diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index a874db681..ced29d819 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -544,6 +544,9 @@ namespace FlaxEditor.Surface // Cache flags and state if (_leftMouseDown && button == MouseButton.Left) { + WasBoxSelecting = IsBoxSelecting; + WasMovingSelection = _isMovingSelection; + _leftMouseDown = false; EndMouseCapture(); Cursor = CursorType.Default; diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index faecebbd3..4a1bad1da 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -232,15 +232,25 @@ namespace FlaxEditor.Surface } /// - /// Gets a value indicating whether user is selecting nodes. + /// Gets a value indicating whether user is box selecting nodes. /// - public bool IsSelecting => _leftMouseDown && !_isMovingSelection && _connectionInstigator == null; + public bool IsBoxSelecting => _leftMouseDown && !_isMovingSelection && _connectionInstigator == null; + + /// + /// Gets a value indicating whether user was previously box selecting nodes. + /// + public bool WasBoxSelecting { get; private set; } /// /// Gets a value indicating whether user is moving selected nodes. /// public bool IsMovingSelection => _leftMouseDown && _isMovingSelection && _connectionInstigator == null; + /// + /// Gets a value indicating whether user was previously moving selected nodes. + /// + public bool WasMovingSelection { get; private set; } + /// /// Gets a value indicating whether user is connecting nodes. /// From eadb4411ffd53290814657bfaa009695f4c45982 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 12 Jun 2025 17:35:02 +0200 Subject: [PATCH 031/122] Fix crash in GPU Memory profiler if resource went null --- Source/Editor/Windows/Profiler/MemoryGPU.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Editor/Windows/Profiler/MemoryGPU.cs b/Source/Editor/Windows/Profiler/MemoryGPU.cs index ce266777d..74f14b584 100644 --- a/Source/Editor/Windows/Profiler/MemoryGPU.cs +++ b/Source/Editor/Windows/Profiler/MemoryGPU.cs @@ -146,6 +146,8 @@ namespace FlaxEditor.Windows.Profiler { var gpuResource = _gpuResourcesCached[i]; ref var resource = ref resources[i]; + if (!gpuResource) + continue; // Try to reuse cached resource info var gpuResourceId = gpuResource.ID; From e2d9452994a3fc1ec9c8d1ee02fd39a770e1fa67 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 12 Jun 2025 18:05:01 +0200 Subject: [PATCH 032/122] Add unified min Clang version `13` for Linux --- Source/Tools/Flax.Build/Deps/Dependencies/Assimp.cs | 6 +++--- Source/Tools/Flax.Build/Deps/Dependencies/OpenAL.cs | 4 ++-- Source/Tools/Flax.Build/Deps/Dependencies/PhysX.cs | 5 ++--- Source/Tools/Flax.Build/Deps/Dependencies/curl.cs | 4 ++-- .../Tools/Flax.Build/Deps/Dependencies/freetype.cs | 4 ++-- Source/Tools/Flax.Build/Deps/Dependencies/mono.cs | 4 ++-- Source/Tools/Flax.Build/Deps/Dependencies/vorbis.cs | 4 ++-- .../Flax.Build/Platforms/Linux/LinuxToolchain.cs | 12 ++++++++++++ 8 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/Assimp.cs b/Source/Tools/Flax.Build/Deps/Dependencies/Assimp.cs index 50235aa10..f72928bf8 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/Assimp.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/Assimp.cs @@ -124,9 +124,9 @@ namespace Flax.Deps.Dependencies { var envVars = new Dictionary { - { "CC", "clang-13" }, - { "CC_FOR_BUILD", "clang-13" }, - { "CXX", "clang++-13" }, + { "CC", "clang-" + Configuration.LinuxClangMinVer }, + { "CC_FOR_BUILD", "clang-" + Configuration.LinuxClangMinVer }, + { "CXX", "clang++-" + Configuration.LinuxClangMinVer }, }; // Build for Linux diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/OpenAL.cs b/Source/Tools/Flax.Build/Deps/Dependencies/OpenAL.cs index 6a783d73f..7b42486e2 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/OpenAL.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/OpenAL.cs @@ -121,8 +121,8 @@ namespace Flax.Deps.Dependencies }; var envVars = new Dictionary { - { "CC", "clang-7" }, - { "CC_FOR_BUILD", "clang-7" } + { "CC", "clang-" + Configuration.LinuxClangMinVer }, + { "CC_FOR_BUILD", "clang-" + Configuration.LinuxClangMinVer } }; var config = "-DALSOFT_REQUIRE_ALSA=ON -DALSOFT_REQUIRE_OSS=ON -DALSOFT_REQUIRE_PORTAUDIO=ON -DALSOFT_REQUIRE_PULSEAUDIO=ON -DALSOFT_REQUIRE_JACK=ON -DALSOFT_EMBED_HRTF_DATA=YES"; diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/PhysX.cs b/Source/Tools/Flax.Build/Deps/Dependencies/PhysX.cs index a82b1f59b..32e2d2f38 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/PhysX.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/PhysX.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Xml; using Flax.Build; using Flax.Build.Platforms; -using Flax.Build.Projects.VisualStudio; using Flax.Deploy; namespace Flax.Deps.Dependencies @@ -237,8 +236,8 @@ namespace Flax.Deps.Dependencies break; } case TargetPlatform.Linux: - envVars.Add("CC", "clang-7"); - envVars.Add("CC_FOR_BUILD", "clang-7"); + envVars.Add("CC", "clang-" + Configuration.LinuxClangMinVer); + envVars.Add("CC_FOR_BUILD", "clang-" + Configuration.LinuxClangMinVer); break; case TargetPlatform.Mac: break; default: throw new InvalidPlatformException(BuildPlatform); diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/curl.cs b/Source/Tools/Flax.Build/Deps/Dependencies/curl.cs index 4eaf8df4b..a474b9566 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/curl.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/curl.cs @@ -105,8 +105,8 @@ namespace Flax.Deps.Dependencies }; var envVars = new Dictionary { - { "CC", "clang-7" }, - { "CC_FOR_BUILD", "clang-7" }, + { "CC", "clang-" + Configuration.LinuxClangMinVer }, + { "CC_FOR_BUILD", "clang-" + Configuration.LinuxClangMinVer }, }; var buildDir = Path.Combine(root, "build"); SetupDirectory(buildDir, true); diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/freetype.cs b/Source/Tools/Flax.Build/Deps/Dependencies/freetype.cs index 6d104d563..ac0079401 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/freetype.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/freetype.cs @@ -116,8 +116,8 @@ namespace Flax.Deps.Dependencies { var envVars = new Dictionary { - { "CC", "clang-7" }, - { "CC_FOR_BUILD", "clang-7" } + { "CC", "clang-" + Configuration.LinuxClangMinVer }, + { "CC_FOR_BUILD", "clang-" + Configuration.LinuxClangMinVer } }; // Fix scripts diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/mono.cs b/Source/Tools/Flax.Build/Deps/Dependencies/mono.cs index dd52114c2..ddf1cc15d 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/mono.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/mono.cs @@ -546,8 +546,8 @@ namespace Flax.Deps.Dependencies { var envVars = new Dictionary { - { "CC", "clang-7" }, - { "CXX", "clang++-7" } + { "CC", "clang-" + Configuration.LinuxClangMinVer }, + { "CXX", "clang++-" + Configuration.LinuxClangMinVer } }; var monoOptions = new[] { diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/vorbis.cs b/Source/Tools/Flax.Build/Deps/Dependencies/vorbis.cs index d841e281c..195c0d8cb 100644 --- a/Source/Tools/Flax.Build/Deps/Dependencies/vorbis.cs +++ b/Source/Tools/Flax.Build/Deps/Dependencies/vorbis.cs @@ -365,8 +365,8 @@ namespace Flax.Deps.Dependencies var envVars = new Dictionary { - { "CC", "clang-7" }, - { "CC_FOR_BUILD", "clang-7" } + { "CC", "clang-" + Configuration.LinuxClangMinVer }, + { "CC_FOR_BUILD", "clang-" + Configuration.LinuxClangMinVer } }; var buildDir = Path.Combine(root, "build"); diff --git a/Source/Tools/Flax.Build/Platforms/Linux/LinuxToolchain.cs b/Source/Tools/Flax.Build/Platforms/Linux/LinuxToolchain.cs index ebbdc5b46..f4e7f06cf 100644 --- a/Source/Tools/Flax.Build/Platforms/Linux/LinuxToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Linux/LinuxToolchain.cs @@ -5,6 +5,18 @@ using System.IO; using Flax.Build.Graph; using Flax.Build.NativeCpp; +namespace Flax.Build +{ + partial class Configuration + { + /// + /// Specifies the minimum Clang compiler version to use on Linux (eg. 10). + /// + [CommandLine("linuxClangMinVer", "", "Specifies the minimum Clang compiler version to use on Linux (eg. 10).")] + public static string LinuxClangMinVer = "13"; + } +} + namespace Flax.Build.Platforms { /// From 47e1547d292f210986dd5de06477244f59ec7d37 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Thu, 12 Jun 2025 18:12:06 +0200 Subject: [PATCH 033/122] improve behaviour of visject node cm menu --- Source/Editor/Surface/SurfaceNode.cs | 8 ++++++++ .../Surface/VisjectSurface.ContextMenu.cs | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index 8a42a7a92..1fe9ea44b 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -40,6 +40,11 @@ namespace FlaxEditor.Surface [HideInEditor] public class SurfaceNode : SurfaceControl { + /// + /// The box to draw a highlight around. Drawing will be skipped if null. + /// + internal Box highlightBox; + /// /// Flag used to discard node values setting during event sending for node UI flushing. /// @@ -1101,6 +1106,9 @@ namespace FlaxEditor.Surface Render2D.DrawSprite(icon, new Rectangle(-7, -7, 16, 16), new Color(0.9f, 0.9f, 0.9f)); Render2D.DrawSprite(icon, new Rectangle(-6, -6, 14, 14), new Color(0.894117647f, 0.0784313725f, 0.0f)); } + + if (highlightBox != null) + Render2D.DrawRectangle(highlightBox.Bounds, style.BorderHighlighted, 2f); } /// diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs index ee7dd33e5..23e52ca7d 100644 --- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs +++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs @@ -402,7 +402,7 @@ namespace FlaxEditor.Surface _cmFormatNodesConnectionButton = menu.AddButton("Format node(s)", () => { FormatGraph(SelectedNodes); }); _cmFormatNodesConnectionButton.Enabled = CanEdit && HasNodesSelection; - _cmRemoveNodeConnectionsButton = menu.AddButton("Remove all connections to that node(s)", () => + _cmRemoveNodeConnectionsButton = menu.AddButton("Remove all connections", () => { var nodes = ((List)menu.Tag); @@ -428,8 +428,10 @@ namespace FlaxEditor.Surface MarkAsEdited(); }); - _cmRemoveNodeConnectionsButton.Enabled = CanEdit; - _cmRemoveBoxConnectionsButton = menu.AddButton("Remove all connections to that box", () => + bool anyConnection = SelectedNodes.Any(n => n.GetBoxes().Any(b => b.HasAnyConnection)); + _cmRemoveNodeConnectionsButton.Enabled = CanEdit && anyConnection; + + _cmRemoveBoxConnectionsButton = menu.AddButton("Remove all socket connections", () => { var boxUnderMouse = (Box)_cmRemoveBoxConnectionsButton.Tag; if (Undo != null) @@ -450,6 +452,16 @@ namespace FlaxEditor.Surface var boxUnderMouse = GetChildAtRecursive(location) as Box; _cmRemoveBoxConnectionsButton.Enabled = boxUnderMouse != null && boxUnderMouse.HasAnyConnection; _cmRemoveBoxConnectionsButton.Tag = boxUnderMouse; + + if (boxUnderMouse != null) + { + boxUnderMouse.ParentNode.highlightBox = boxUnderMouse; + menu.VisibleChanged += (c) => + { + if (!c.Visible) + boxUnderMouse.ParentNode.highlightBox = null; + }; + } } controlUnderMouse?.OnShowSecondaryContextMenu(menu, controlUnderMouse.PointFromParent(location)); From 766091045b1be027321b8cc33f74d50254ffe6c1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 12 Jun 2025 18:21:12 +0200 Subject: [PATCH 034/122] Improve version parsing to share code --- .../Flax.Build/Platforms/Linux/LinuxToolchain.cs | 9 +++++++-- .../Flax.Build/Platforms/Windows/WindowsToolchain.cs | 9 ++------- Source/Tools/Flax.Build/Utilities/Utilities.cs | 12 ++++++++++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Source/Tools/Flax.Build/Platforms/Linux/LinuxToolchain.cs b/Source/Tools/Flax.Build/Platforms/Linux/LinuxToolchain.cs index f4e7f06cf..174b2bcee 100644 --- a/Source/Tools/Flax.Build/Platforms/Linux/LinuxToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Linux/LinuxToolchain.cs @@ -1,9 +1,10 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using System.Collections.Generic; -using System.IO; using Flax.Build.Graph; using Flax.Build.NativeCpp; +using System; +using System.Collections.Generic; +using System.IO; namespace Flax.Build { @@ -34,6 +35,10 @@ namespace Flax.Build.Platforms public LinuxToolchain(LinuxPlatform platform, TargetArchitecture architecture) : base(platform, architecture, platform.ToolchainRoot, platform.Compiler) { + // Check version + if (Utilities.ParseVersion(Configuration.LinuxClangMinVer, out var minClangVer) && ClangVersion < minClangVer) + Log.Error($"Old Clang version {ClangVersion}. Minimum supported is {minClangVer}."); + // Setup system paths var includePath = Path.Combine(ToolsetRoot, "usr", "include"); if (Directory.Exists(includePath)) diff --git a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchain.cs b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchain.cs index 7537cdecf..5154cdbf9 100644 --- a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchain.cs @@ -44,13 +44,8 @@ namespace Flax.Build.Platforms : base(platform, architecture, WindowsPlatformToolset.Latest, WindowsPlatformSDK.Latest) { // Select minimum Windows version - if (!Version.TryParse(Configuration.WindowsMinVer, out _minVersion)) - { - if (int.TryParse(Configuration.WindowsMinVer, out var winMinVerMajor)) - _minVersion = new Version(winMinVerMajor, 0); - else - _minVersion = new Version(7, 0); - } + if (!Utilities.ParseVersion(Configuration.WindowsMinVer, out _minVersion)) + _minVersion = new Version(7, 0); } /// diff --git a/Source/Tools/Flax.Build/Utilities/Utilities.cs b/Source/Tools/Flax.Build/Utilities/Utilities.cs index 2b971f153..85f791d09 100644 --- a/Source/Tools/Flax.Build/Utilities/Utilities.cs +++ b/Source/Tools/Flax.Build/Utilities/Utilities.cs @@ -818,5 +818,17 @@ namespace Flax.Build return 0; }); } + + internal static bool ParseVersion(string text, out Version ver) + { + if (Version.TryParse(text, out ver)) + return true; + if (int.TryParse(text, out var major)) + { + ver = new Version(major, 0); + return true; + } + return false; + } } } From e2f741cab9c512cdd1c39235af93816342908ae4 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Thu, 12 Jun 2025 18:45:02 +0200 Subject: [PATCH 035/122] adjust model window and animated model slider speeds --- Source/Editor/Windows/Assets/ModelBaseWindow.cs | 9 +++++---- Source/Editor/Windows/Assets/ModelWindow.cs | 1 + Source/Engine/Level/Actors/AnimatedModel.h | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Windows/Assets/ModelBaseWindow.cs b/Source/Editor/Windows/Assets/ModelBaseWindow.cs index 14344ef71..09b4606c4 100644 --- a/Source/Editor/Windows/Assets/ModelBaseWindow.cs +++ b/Source/Editor/Windows/Assets/ModelBaseWindow.cs @@ -236,6 +236,7 @@ namespace FlaxEditor.Windows.Assets var group = layout.Group("General"); var minScreenSize = group.FloatValue("Min Screen Size", "The minimum screen size to draw model (the bottom limit). Used to cull small models. Set to 0 to disable this feature."); + minScreenSize.ValueBox.SlideSpeed = 0.005f; minScreenSize.ValueBox.MinValue = 0.0f; minScreenSize.ValueBox.MaxValue = 1.0f; minScreenSize.ValueBox.Value = proxy.Asset.MinScreenSize; @@ -476,12 +477,12 @@ namespace FlaxEditor.Windows.Assets } } - [EditorOrder(1), EditorDisplay(null, "LOD"), Limit(0, Model.MaxLODs), VisibleIf("ShowUVs")] - [Tooltip("Level Of Detail index to preview UVs layout.")] + [EditorOrder(1), EditorDisplay(null, "LOD"), Limit(0, Model.MaxLODs, 0.01f), VisibleIf("ShowUVs")] + [Tooltip("Level Of Detail index to preview UVs layout at.")] public int LOD = 0; - [EditorOrder(2), EditorDisplay(null, "Mesh"), Limit(-1, 1000000), VisibleIf("ShowUVs")] - [Tooltip("Mesh index to preview UVs layout. Use -1 for all meshes")] + [EditorOrder(2), EditorDisplay(null, "Mesh"), Limit(-1, 1000000, 0.01f), VisibleIf("ShowUVs")] + [Tooltip("Mesh index to show UVs layout for. Use -1 to display all UVs of all meshes")] public int Mesh = -1; private bool ShowUVs => _uvChannel != UVChannel.None; diff --git a/Source/Editor/Windows/Assets/ModelWindow.cs b/Source/Editor/Windows/Assets/ModelWindow.cs index 9a0b1ad82..1dc30cae3 100644 --- a/Source/Editor/Windows/Assets/ModelWindow.cs +++ b/Source/Editor/Windows/Assets/ModelWindow.cs @@ -81,6 +81,7 @@ namespace FlaxEditor.Windows.Assets } var resolution = group.FloatValue("Resolution Scale", Window.Editor.CodeDocs.GetTooltip(typeof(ModelTool.Options), nameof(ModelImportSettings.Settings.SDFResolution))); + resolution.ValueBox.SlideSpeed = 0.001f; resolution.ValueBox.MinValue = 0.0001f; resolution.ValueBox.MaxValue = 100.0f; resolution.ValueBox.Value = sdf.Texture != null ? sdf.ResolutionScale : 1.0f; diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index 89124cb87..859d89212 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -110,7 +110,7 @@ public: /// /// The animation update delta timescale. Can be used to speed up animation playback or create slow motion effect. /// - API_FIELD(Attributes="EditorOrder(45), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(45), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Skinned Model\")") float UpdateSpeed = 1.0f; /// @@ -122,7 +122,7 @@ public: /// /// The master scale parameter for the actor bounding box. Helps to reduce mesh flickering effect on screen edges. /// - API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Skinned Model\")") float BoundsScale = 1.5f; /// From 648504ceb113ba21d46b324f3e56acfd7e7772c6 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 15 Jun 2025 20:31:04 +0200 Subject: [PATCH 036/122] Format code #3544 --- Source/Editor/GUI/ToolStripButton.cs | 24 ++++++++--------- Source/Editor/Windows/DebugLogWindow.cs | 34 ++++++++++--------------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/Source/Editor/GUI/ToolStripButton.cs b/Source/Editor/GUI/ToolStripButton.cs index 56e798a2e..c49cddcbb 100644 --- a/Source/Editor/GUI/ToolStripButton.cs +++ b/Source/Editor/GUI/ToolStripButton.cs @@ -122,6 +122,14 @@ namespace FlaxEditor.GUI return this; } + private void OnClicked() + { + if (AutoCheck) + Checked = !Checked; + Clicked?.Invoke(); + (Parent as ToolStrip)?.OnButtonClicked(this); + } + /// public override void Draw() { @@ -196,11 +204,7 @@ namespace FlaxEditor.GUI if (button == MouseButton.Left && _primaryMouseDown) { _primaryMouseDown = false; - if (AutoCheck) - Checked = !Checked; - Clicked?.Invoke(); - (Parent as ToolStrip)?.OnButtonClicked(this); - + OnClicked(); return true; } if (button == MouseButton.Right && _secondaryMouseDown) @@ -218,16 +222,12 @@ namespace FlaxEditor.GUI /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { - if(button == MouseButton.Left) + if (button == MouseButton.Left) { - if (AutoCheck) - Checked = !Checked; - Clicked?.Invoke(); - (Parent as ToolStrip)?.OnButtonClicked(this); - + OnClicked(); return true; } - + return false; } diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs index d0680c3c0..b5d71f6a0 100644 --- a/Source/Editor/Windows/DebugLogWindow.cs +++ b/Source/Editor/Windows/DebugLogWindow.cs @@ -318,7 +318,7 @@ namespace FlaxEditor.Windows private Color _colorWarning; private Color _colorError; private bool _colorDebugLogText; - + /// /// Initializes a new instance of the class. /// @@ -353,11 +353,11 @@ namespace FlaxEditor.Windows }).SetAutoCheck(true).LinkTooltip("Performs auto pause on error"); toolstrip.AddSeparator(); _groupButtons[0] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Error32, () => { OnGroupButtonPressed(0); }) - .SetAutoCheck(true).LinkTooltip("Shows/hides error messages"); + .SetAutoCheck(true).LinkTooltip("Shows/hides error messages"); _groupButtons[1] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Warning32, () => { OnGroupButtonPressed(1); }) - .SetAutoCheck(true).LinkTooltip("Shows/hides warning messages"); + .SetAutoCheck(true).LinkTooltip("Shows/hides warning messages"); _groupButtons[2] = (ToolStripButton)toolstrip.AddButton(editor.Icons.Info32, () => { OnGroupButtonPressed(2); }) - .SetAutoCheck(true).LinkTooltip("Shows/hides info messages"); + .SetAutoCheck(true).LinkTooltip("Shows/hides info messages"); UpdateCount(); // Split panel @@ -409,21 +409,21 @@ namespace FlaxEditor.Windows private void OnGroupButtonPressed(int index) { UpdateLogTypeVisibility((LogGroup)index, _groupButtons[index].Checked); - if(Input.GetKey(KeyboardKeys.Shift)) + if (Input.GetKey(KeyboardKeys.Shift)) { - for(int i = 0; i < (int)LogGroup.Max; i++) + for (int i = 0; i < (int)LogGroup.Max; i++) { - if(i == index) + if (i == index) continue; - _groupButtons[i].Checked = !_groupButtons[index].Checked; UpdateLogTypeVisibility((LogGroup)i, _groupButtons[i].Checked); } } - Editor.Options.Options.Interface.DebugLogShowErrorMessages = _groupButtons[0].Checked; - Editor.Options.Options.Interface.DebugLogShowWarningMessages = _groupButtons[1].Checked; - Editor.Options.Options.Interface.DebugLogShowInfoMessages = _groupButtons[2].Checked; + var options = Editor.Options.Options.Interface; + options.DebugLogShowErrorMessages = _groupButtons[0].Checked; + options.DebugLogShowWarningMessages = _groupButtons[1].Checked; + options.DebugLogShowInfoMessages = _groupButtons[2].Checked; Editor.Options.Apply(Editor.Options.Options); } @@ -464,15 +464,9 @@ namespace FlaxEditor.Windows // Create new entry switch (_timestampsFormats) { - case InterfaceOptions.TimestampsFormats.Utc: - desc.Title = $"[{DateTime.UtcNow}] {desc.Title}"; - break; - case InterfaceOptions.TimestampsFormats.LocalTime: - desc.Title = $"[{DateTime.Now}] {desc.Title}"; - break; - case InterfaceOptions.TimestampsFormats.TimeSinceStartup: - desc.Title = string.Format("[{0:g}] ", TimeSpan.FromSeconds(Time.TimeSinceStartup)) + desc.Title; - break; + case InterfaceOptions.TimestampsFormats.Utc: desc.Title = $"[{DateTime.UtcNow}] {desc.Title}"; break; + case InterfaceOptions.TimestampsFormats.LocalTime: desc.Title = $"[{DateTime.Now}] {desc.Title}"; break; + case InterfaceOptions.TimestampsFormats.TimeSinceStartup: desc.Title = string.Format("[{0:g}] ", TimeSpan.FromSeconds(Time.TimeSinceStartup)) + desc.Title; break; } var newEntry = new LogEntry(this, ref desc); From 2f02ec52ed3d42e4f64f1487a36c6408625d57ba Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 15 Jun 2025 20:48:19 +0200 Subject: [PATCH 037/122] Cleanup code #3546 --- .../Editor/Windows/Assets/ParticleEmitterWindow.cs | 7 +++---- Source/Engine/Particles/ParticleEmitter.cpp | 13 +++++-------- Source/Engine/Particles/ParticleEmitter.h | 5 ++--- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs b/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs index bc1f49443..6513ac9e0 100644 --- a/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs +++ b/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs @@ -288,16 +288,15 @@ namespace FlaxEditor.Windows.Assets /// public SearchAssetTypes AssetType => SearchAssetTypes.ParticleEmitter; - + /// public override void Update(float deltaTime) { base.Update(deltaTime); - if(_asset == null) + if (_asset == null) return; - - _showSourceCodeButton.Enabled = _asset.HasShaderCode(); + _showSourceCodeButton.Enabled = _asset.HasShaderCode; } } } diff --git a/Source/Engine/Particles/ParticleEmitter.cpp b/Source/Engine/Particles/ParticleEmitter.cpp index 4490406bc..6264ce412 100644 --- a/Source/Engine/Particles/ParticleEmitter.cpp +++ b/Source/Engine/Particles/ParticleEmitter.cpp @@ -440,20 +440,17 @@ bool ParticleEmitter::Save(const StringView& path) return SaveSurface(data); } -bool ParticleEmitter::HasShaderCode() +bool ParticleEmitter::HasShaderCode() const { - if(SimulationMode != ParticlesSimulationMode::GPU) - { + if (SimulationMode != ParticlesSimulationMode::GPU) return false; - } - #if COMPILE_WITH_PARTICLE_GPU_GRAPH && COMPILE_WITH_SHADER_COMPILER - if(_shaderHeader.ParticleEmitter.GraphVersion == PARTICLE_GPU_GRAPH_VERSION +#if COMPILE_WITH_PARTICLE_GPU_GRAPH && COMPILE_WITH_SHADER_COMPILER + if (_shaderHeader.ParticleEmitter.GraphVersion == PARTICLE_GPU_GRAPH_VERSION && HasChunk(SHADER_FILE_CHUNK_SOURCE) && !HasDependenciesModified()) return true; - #endif - +#endif return false; } diff --git a/Source/Engine/Particles/ParticleEmitter.h b/Source/Engine/Particles/ParticleEmitter.h index 4b27036f7..772f5569e 100644 --- a/Source/Engine/Particles/ParticleEmitter.h +++ b/Source/Engine/Particles/ParticleEmitter.h @@ -175,10 +175,9 @@ public: bool Save(const StringView& path = StringView::Empty) override; /// - /// Determine if the particle emitter has valid shader code present. + /// Checks if the particle emitter has valid shader code present. /// - /// True if particle emitter has shader code, otherwise false. - API_FUNCTION() bool HasShaderCode(); + API_PROPERTY() bool HasShaderCode() const; #endif protected: From cd7233d559dded4da4ec88e5d6f498e18de22ff8 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Mon, 16 Jun 2025 14:10:44 +0200 Subject: [PATCH 038/122] allow the user to unfold folded categories when scrolling with keyboard --- Source/Editor/GUI/ItemsListContextMenu.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Source/Editor/GUI/ItemsListContextMenu.cs b/Source/Editor/GUI/ItemsListContextMenu.cs index 58f8c2eb4..0f138fb80 100644 --- a/Source/Editor/GUI/ItemsListContextMenu.cs +++ b/Source/Editor/GUI/ItemsListContextMenu.cs @@ -227,9 +227,8 @@ namespace FlaxEditor.GUI { int order = -1 * SortScore.CompareTo(otherItem.SortScore); if (order == 0) - { order = string.Compare(Name, otherItem.Name, StringComparison.Ordinal); - } + return order; } return base.Compare(other); @@ -535,6 +534,12 @@ namespace FlaxEditor.GUI return result; } + private void ExpandToItem(Item item) + { + if (item.Parent is DropPanel dropPanel) + dropPanel.Open(false); + } + /// protected override void OnShow() { @@ -564,7 +569,7 @@ namespace FlaxEditor.GUI Hide(); return true; case KeyboardKeys.Backspace: - // Alow the user to quickly focus the searchbar + // Allow the user to quickly focus the searchbar if (_searchBox != null && !_searchBox.IsFocused) { _searchBox.Focus(); @@ -590,6 +595,11 @@ namespace FlaxEditor.GUI // Focus the next item nextItem.Focus(); + + // Allow the user to expand groups while scrolling + if (Root.GetKey(KeyboardKeys.Control)) + ExpandToItem(nextItem); + _scrollPanel.ScrollViewTo(nextItem); return true; case KeyboardKeys.Return: From ea854a0f7b6864ca4c12ffedd8b6679064433755 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Jun 2025 14:41:43 +0200 Subject: [PATCH 039/122] Fix potential Grid shader accuracy issues #3229 --- Content/Shaders/Editor/Grid.flax | 4 ++-- Source/Shaders/Editor/Grid.shader | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Content/Shaders/Editor/Grid.flax b/Content/Shaders/Editor/Grid.flax index 311867604..09ecd00e6 100644 --- a/Content/Shaders/Editor/Grid.flax +++ b/Content/Shaders/Editor/Grid.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c13729c4ec2ef534271c60fee6fff2e0489bf4445fe91aa8a2bbc3d581715602 -size 4666 +oid sha256:e5671b8b77b460a17d0a3c14174994a05cf1b3d8869d10b350de4a8053419836 +size 4647 diff --git a/Source/Shaders/Editor/Grid.shader b/Source/Shaders/Editor/Grid.shader index f109ddd1a..45bb1ed90 100644 --- a/Source/Shaders/Editor/Grid.shader +++ b/Source/Shaders/Editor/Grid.shader @@ -3,7 +3,7 @@ // Ben Golus // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#3e73 -#define USE_FORWARD true; +#define MIN_DERIV 0.00001f #include "./Flax/Common.hlsl" @@ -61,11 +61,6 @@ float remap(float origFrom, float origTo, float targetFrom, float targetTo, floa return lerp(targetFrom, targetTo, rel); } -float ddLength(float a) -{ - return length(float2(ddx(a), ddy(a))); -} - float GetLine(float pos, float scale, float thickness) { float lineWidth = thickness; @@ -73,7 +68,7 @@ float GetLine(float pos, float scale, float thickness) float2 uvDDXY = float2(ddx(coord), ddy(coord)); - float deriv = float(length(uvDDXY.xy)); + float deriv = max(float(length(uvDDXY.xy)), MIN_DERIV); float drawWidth = clamp(lineWidth, deriv, 0.5); float lineAA = deriv * 1.5; float gridUV = abs(coord); @@ -92,7 +87,7 @@ float GetGrid(float3 pos, float scale, float thickness) float4 uvDDXY = float4(ddx(coord), ddy(coord)); - float2 deriv = float2(length(uvDDXY.xz), length(uvDDXY.yw)); + float2 deriv = max(float2(length(uvDDXY.xz), length(uvDDXY.yw)), float2(MIN_DERIV, MIN_DERIV)); float2 drawWidth = clamp(lineWidth, deriv, 0.5); float2 lineAA = deriv * 1.5; float2 gridUV = 1.0 - abs(frac(coord) * 2.0 - 1.0); From a6a2fd2c66c919ac3ad609fbb59b88054083512f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Jun 2025 14:48:18 +0200 Subject: [PATCH 040/122] Format code #3526 --- Source/Editor/GUI/Tree/Tree.cs | 6 +++--- Source/Editor/Windows/SceneTreeWindow.cs | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Source/Editor/GUI/Tree/Tree.cs b/Source/Editor/GUI/Tree/Tree.cs index 3e1453f03..8df26c211 100644 --- a/Source/Editor/GUI/Tree/Tree.cs +++ b/Source/Editor/GUI/Tree/Tree.cs @@ -74,9 +74,9 @@ namespace FlaxEditor.GUI.Tree public bool DrawRootTreeLine = true; /// - /// Occurs when the defered layouting happens + /// Occurs when the deferred layout operation was performed. /// - public event Action OnDeferedLayout; + public event Action AfterDeferredLayout; /// /// Gets or sets the margin for the child tree nodes. @@ -380,7 +380,7 @@ namespace FlaxEditor.GUI.Tree if (_deferLayoutUpdate) { base.PerformLayout(); - OnDeferedLayout?.Invoke(); + AfterDeferredLayout?.Invoke(); _deferLayoutUpdate = false; } diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 620300485..3c7583b0e 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -92,14 +92,15 @@ namespace FlaxEditor.Windows _tree.SelectedChanged += Tree_OnSelectedChanged; _tree.RightClick += OnTreeRightClick; _tree.Parent = _sceneTreePanel; - _tree.OnDeferedLayout += () => { - if(_forceScrollNodeToView) + _tree.AfterDeferredLayout += () => + { + if (_forceScrollNodeToView) { _forceScrollNodeToView = false; ScrollToSelectedNode(); } }; - + headerPanel.Parent = this; // Setup input actions @@ -151,9 +152,10 @@ namespace FlaxEditor.Windows root.TreeNode.UpdateFilter(query); _tree.UnlockChildrenRecursive(); - + + // When keep the selected nodes in a view var nodeSelection = _tree.Selection; - if(nodeSelection.Count != 0) + if (nodeSelection.Count != 0) { var node = nodeSelection[nodeSelection.Count - 1]; node.Expand(true); From d110237423949a04cb8da04de342c9dac16873f9 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Mon, 16 Jun 2025 14:51:29 +0200 Subject: [PATCH 041/122] fix GetVisibleItems() to only get visible items --- Source/Editor/GUI/ItemsListContextMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/GUI/ItemsListContextMenu.cs b/Source/Editor/GUI/ItemsListContextMenu.cs index 0f138fb80..e2a06342d 100644 --- a/Source/Editor/GUI/ItemsListContextMenu.cs +++ b/Source/Editor/GUI/ItemsListContextMenu.cs @@ -522,7 +522,7 @@ namespace FlaxEditor.GUI for (int i = 0; i < _categoryPanels.Count; i++) { var category = _categoryPanels[i]; - if (!category.Visible) + if (!category.Visible || (category is DropPanel panel && panel.IsClosed)) continue; for (int j = 0; j < category.Children.Count; j++) { From 90b6293bc279ef4a816b948a3da6351bead6a234 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Jun 2025 14:56:35 +0200 Subject: [PATCH 042/122] Add `OnPlayEnding` to editor modules and windows #3514 --- Source/Editor/Editor.cs | 2 ++ Source/Editor/Modules/EditorModule.cs | 7 +++++++ Source/Editor/Modules/WindowsModule.cs | 7 +++++++ Source/Editor/Windows/EditorWindow.cs | 7 +++++++ 4 files changed, 23 insertions(+) diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index c6ead7a76..8c3256eb9 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -670,6 +670,8 @@ namespace FlaxEditor { FlaxEngine.Networking.NetworkManager.Stop(); // Shutdown any multiplayer from playmode PlayModeEnding?.Invoke(); + for (int i = 0; i < _modules.Count; i++) + _modules[i].OnPlayEnding(); } internal void OnPlayEnd() diff --git a/Source/Editor/Modules/EditorModule.cs b/Source/Editor/Modules/EditorModule.cs index 8285088fa..410453302 100644 --- a/Source/Editor/Modules/EditorModule.cs +++ b/Source/Editor/Modules/EditorModule.cs @@ -76,6 +76,13 @@ namespace FlaxEditor.Modules { } + /// + /// Called when Editor will leave the play mode. + /// + public virtual void OnPlayEnding() + { + } + /// /// Called when Editor leaves the play mode. /// diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 289c5d7e8..5c9c613d2 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -1223,6 +1223,13 @@ namespace FlaxEditor.Modules Windows[i].OnPlayBegin(); } + /// + public override void OnPlayEnding() + { + for (int i = 0; i < Windows.Count; i++) + Windows[i].OnPlayEnding(); + } + /// public override void OnPlayEnd() { diff --git a/Source/Editor/Windows/EditorWindow.cs b/Source/Editor/Windows/EditorWindow.cs index f61f5cce6..6d01432ba 100644 --- a/Source/Editor/Windows/EditorWindow.cs +++ b/Source/Editor/Windows/EditorWindow.cs @@ -219,6 +219,13 @@ namespace FlaxEditor.Windows { } + /// + /// Called when Editor will leave the play mode. + /// + public virtual void OnPlayEnding() + { + } + /// /// Called when Editor leaves the play mode. /// From 8164ce924fd5867e3f236736b0baff3f23dbf482 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Mon, 16 Jun 2025 15:30:51 +0200 Subject: [PATCH 043/122] fix unfolding to actually work --- Source/Editor/GUI/ItemsListContextMenu.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Source/Editor/GUI/ItemsListContextMenu.cs b/Source/Editor/GUI/ItemsListContextMenu.cs index e2a06342d..6cb5a84fb 100644 --- a/Source/Editor/GUI/ItemsListContextMenu.cs +++ b/Source/Editor/GUI/ItemsListContextMenu.cs @@ -508,7 +508,7 @@ namespace FlaxEditor.GUI OnSearchFilterChanged(); } - private List GetVisibleItems() + private List GetVisibleItems(bool ignoreFoldedCategories) { var result = new List(); var items = ItemsPanel.Children; @@ -522,7 +522,7 @@ namespace FlaxEditor.GUI for (int i = 0; i < _categoryPanels.Count; i++) { var category = _categoryPanels[i]; - if (!category.Visible || (category is DropPanel panel && panel.IsClosed)) + if (!category.Visible || (ignoreFoldedCategories && category is DropPanel panel && panel.IsClosed)) continue; for (int j = 0; j < category.Children.Count; j++) { @@ -587,8 +587,14 @@ namespace FlaxEditor.GUI } // Get the next item - var items = GetVisibleItems(); + bool controlDown = Root.GetKey(KeyboardKeys.Control); + var items = GetVisibleItems(!controlDown); var focusedIndex = items.IndexOf(focusedItem); + + // If the user hasn't selected anything yet and is holding control, focus first folded item + if (focusedIndex == -1 && controlDown) + focusedIndex = GetVisibleItems(true).Count - 1; + int delta = key == KeyboardKeys.ArrowDown ? -1 : 1; int nextIndex = Mathf.Wrap(focusedIndex - delta, 0, items.Count - 1); var nextItem = items[nextIndex]; @@ -597,7 +603,7 @@ namespace FlaxEditor.GUI nextItem.Focus(); // Allow the user to expand groups while scrolling - if (Root.GetKey(KeyboardKeys.Control)) + if (controlDown) ExpandToItem(nextItem); _scrollPanel.ScrollViewTo(nextItem); @@ -611,7 +617,7 @@ namespace FlaxEditor.GUI else { // Select first item if no item is focused (most likely to be the best result), saves the user from pressing arrow down first - var visibleItems = GetVisibleItems(); + var visibleItems = GetVisibleItems(true); if (visibleItems.Count > 0) { OnClickItem(visibleItems[0]); From bbe54969b0eede35e7405eeff1a91495c3d2931f Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Mon, 16 Jun 2025 16:51:30 +0200 Subject: [PATCH 044/122] fix close visject surface was_ flags to properly reset to false --- Source/Editor/Surface/VisjectSurface.Draw.cs | 4 ++++ Source/Editor/Surface/VisjectSurface.Input.cs | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index 01277d0d2..f60c19d21 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -225,6 +225,10 @@ namespace FlaxEditor.Surface _rootControl.DrawComments(); + // Reset input flags here because this is the closest to Update we have + WasBoxSelecting = IsBoxSelecting; + WasMovingSelection = IsMovingSelection; + if (IsBoxSelecting) { DrawSelection(); diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index 63dfa063d..09df195eb 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -590,9 +590,6 @@ namespace FlaxEditor.Surface // Cache flags and state if (_leftMouseDown && button == MouseButton.Left) { - WasBoxSelecting = IsBoxSelecting; - WasMovingSelection = _isMovingSelection; - _leftMouseDown = false; EndMouseCapture(); Cursor = CursorType.Default; From 97b37b3ce468f8f674ff88e1600670a65cc66f5f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Jun 2025 18:00:26 +0200 Subject: [PATCH 045/122] Add `PRAGMA_DISABLE_OPTIMIZATION`/`PRAGMA_ENABLE_OPTIMIZATION` --- Source/Engine/Core/Compiler.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Engine/Core/Compiler.h b/Source/Engine/Core/Compiler.h index fb731930e..a45a4628a 100644 --- a/Source/Engine/Core/Compiler.h +++ b/Source/Engine/Core/Compiler.h @@ -26,6 +26,8 @@ _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") #define PRAGMA_ENABLE_DEPRECATION_WARNINGS \ _Pragma("clang diagnostic pop") +#define PRAGMA_DISABLE_OPTIMIZATION +#define PRAGMA_ENABLE_OPTIMIZATION #pragma clang diagnostic ignored "-Wswitch" #pragma clang diagnostic ignored "-Wmacro-redefined" @@ -54,6 +56,8 @@ #define OFFSET_OF(X, Y) __builtin_offsetof(X, Y) #define PRAGMA_DISABLE_DEPRECATION_WARNINGS #define PRAGMA_ENABLE_DEPRECATION_WARNINGS +#define PRAGMA_DISABLE_OPTIMIZATION +#define PRAGMA_ENABLE_OPTIMIZATION #elif defined(_MSC_VER) @@ -86,6 +90,8 @@ __pragma(warning(disable: 4996)) #define PRAGMA_ENABLE_DEPRECATION_WARNINGS \ __pragma (warning(pop)) +#define PRAGMA_DISABLE_OPTIMIZATION __pragma(optimize("", off)) +#define PRAGMA_ENABLE_OPTIMIZATION __pragma(optimize("", on)) #pragma warning(disable: 4251) From 6a82eb114dcf222ad5093b97d6c5004f7eb7cc67 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Jun 2025 19:05:44 +0200 Subject: [PATCH 046/122] Fix `BitArray` bit indexing --- Source/Engine/Core/Collections/BitArray.h | 13 ++++----- Source/Engine/Tests/TestCollections.cpp | 32 ++++++++++++++++++++--- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h index 8206f2164..6589fa9a1 100644 --- a/Source/Engine/Core/Collections/BitArray.h +++ b/Source/Engine/Core/Collections/BitArray.h @@ -16,6 +16,7 @@ API_CLASS(InBuild) class BitArray public: using ItemType = uint64; using AllocationData = typename AllocationType::template Data; + static constexpr int32 ItemBitCount = 64; private: int32 _count; @@ -209,8 +210,8 @@ public: bool Get(const int32 index) const { ASSERT(index >= 0 && index < _count); - const ItemType offset = index / sizeof(ItemType); - const ItemType bitMask = (ItemType)(int32)(1 << (index & ((int32)sizeof(ItemType) - 1))); + const ItemType offset = index / ItemBitCount; + const ItemType bitMask = (ItemType)(1 << (index & (ItemBitCount - 1))); const ItemType item = ((ItemType*)_allocation.Get())[offset]; return (item & bitMask) != 0; } @@ -223,13 +224,13 @@ public: void Set(const int32 index, const bool value) { ASSERT(index >= 0 && index < _count); - const ItemType offset = index / sizeof(ItemType); - const ItemType bitMask = (ItemType)(int32)(1 << (index & ((int32)sizeof(ItemType) - 1))); + const ItemType offset = index / ItemBitCount; + const ItemType bitMask = (ItemType)(1 << (index & (ItemBitCount - 1))); ItemType& item = ((ItemType*)_allocation.Get())[offset]; if (value) - item |= bitMask; + item |= bitMask; // Set the bit else - item &= ~bitMask; // Clear the bit + item &= ~bitMask; // Unset the bit } public: diff --git a/Source/Engine/Tests/TestCollections.cpp b/Source/Engine/Tests/TestCollections.cpp index fd0f3328d..b477140cf 100644 --- a/Source/Engine/Tests/TestCollections.cpp +++ b/Source/Engine/Tests/TestCollections.cpp @@ -78,6 +78,32 @@ TEST_CASE("Array") TEST_CASE("BitArray") { + SECTION("Test Access") + { + BitArray<> a1; + CHECK(a1.Count() == 0); + for (int32 i = 0; i < 310; i++) + { + a1.Add(false); + } + CHECK(a1.Count() == 310); + a1.Resize(300); + CHECK(a1.Count() == 300); + CHECK(a1.Capacity() >= 300); + a1.SetAll(true); + a1.SetAll(false); + for (int32 i = 0; i < 300; i++) + { + a1.Set(i, true); + for (int32 j = 0; j < 300; j++) + { + bool expected = j == i; + CHECK(a1.Get(j) == expected); + } + a1.Set(i, false); + } + } + SECTION("Test Allocators") { BitArray<> a1; @@ -142,7 +168,7 @@ TEST_CASE("BitArray") // Generate some random data for testing BitArray<> testData; - testData.Resize(32); + testData.Resize(128); RandomStream rand(101); for (int32 i = 0; i < testData.Count(); i++) testData.Set(i, rand.GetBool()); @@ -151,8 +177,8 @@ TEST_CASE("BitArray") { const BitArray<> a1(testData); const BitArray> a2(testData); - const BitArray> a3(testData); - const BitArray> a4(testData); + const BitArray> a3(testData); + const BitArray> a4(testData); CHECK(a1 == testData); CHECK(a2 == testData); CHECK(a3 == testData); From b92c18cf250a7cdc26c75d6867b128975a3871f2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Jun 2025 19:10:35 +0200 Subject: [PATCH 047/122] Fix missing/incorrect toolchain exception to log only once --- Source/Tools/Flax.Build/Build/Platform.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Build/Platform.cs b/Source/Tools/Flax.Build/Build/Platform.cs index 6304cdc4a..59572d669 100644 --- a/Source/Tools/Flax.Build/Build/Platform.cs +++ b/Source/Tools/Flax.Build/Build/Platform.cs @@ -17,6 +17,7 @@ namespace Flax.Build private static Platform _buildPlatform; private static Platform[] _platforms; private Dictionary _toolchains; + private uint _failedArchitectures = 0; /// /// Gets the current target platform that build tool runs on. @@ -251,7 +252,8 @@ namespace Flax.Build public Toolchain TryGetToolchain(TargetArchitecture targetArchitecture) { Toolchain result = null; - if (HasRequiredSDKsInstalled) + uint failedMask = 1u << (int)targetArchitecture; // Skip retrying if it already failed once on this arch + if (HasRequiredSDKsInstalled && (_failedArchitectures & failedMask) != failedMask) { try { @@ -259,6 +261,7 @@ namespace Flax.Build } catch (Exception ex) { + _failedArchitectures |= failedMask; Log.Exception(ex); } } From 5a23060e05623f08a2d67391502e623ac7d40a66 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Jun 2025 22:35:50 +0200 Subject: [PATCH 048/122] Add `GPUTexture.UploadData` for changing texture contents via `TextureData` container --- Source/Engine/Graphics/Textures/GPUTexture.cpp | 17 +++++++++++++++++ Source/Engine/Graphics/Textures/GPUTexture.h | 12 ++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Graphics/Textures/GPUTexture.cpp b/Source/Engine/Graphics/Textures/GPUTexture.cpp index 127cf2f5b..59b025b70 100644 --- a/Source/Engine/Graphics/Textures/GPUTexture.cpp +++ b/Source/Engine/Graphics/Textures/GPUTexture.cpp @@ -630,6 +630,23 @@ GPUTask* GPUTexture::UploadMipMapAsync(const BytesContainer& data, int32 mipInde return task; } +bool GPUTexture::UploadData(TextureData& data, bool copyData) +{ + if (!IsAllocated()) + return true; + if (data.Width != Width() || data.Height != Height() || data.Depth != Depth() || data.GetArraySize() != ArraySize() || data.Format != Format()) + return true; + for (int32 arrayIndex = 0; arrayIndex < ArraySize(); arrayIndex++) + { + for (int32 mipLevel = 0; mipLevel < MipLevels(); mipLevel++) + { + TextureMipData* mip = data.GetData(arrayIndex, mipLevel); + UploadMipMapAsync(mip->Data, mipLevel, mip->RowPitch, mip->DepthPitch, copyData); + } + } + return false; +} + class TextureDownloadDataTask : public ThreadPoolTask { private: diff --git a/Source/Engine/Graphics/Textures/GPUTexture.h b/Source/Engine/Graphics/Textures/GPUTexture.h index 977723f26..85ac4ae0b 100644 --- a/Source/Engine/Graphics/Textures/GPUTexture.h +++ b/Source/Engine/Graphics/Textures/GPUTexture.h @@ -499,7 +499,7 @@ public: /// /// Uploads mip map data to the GPU. Creates async GPU task. /// - /// Data to upload (it must be valid for the next a few frames due to GPU latency and async works executing) + /// Data to upload, it must match texture dimensions. It must be valid for the next couple of frames due to GPU async task latency or use data copy. /// Mip level index. /// If true, the data will be copied to the async execution task instead of using the input pointer provided. /// Created async task or null if cannot. @@ -508,7 +508,7 @@ public: /// /// Uploads mip map data to the GPU. Creates async GPU task. /// - /// Data to upload (it must be valid for the next a few frames due to GPU latency and async works executing) + /// Data to upload, it must match texture dimensions. It must be valid for the next couple of frames due to GPU async task latency or use data copy. /// Mip level index. /// The data row pitch. /// The data slice pitch. @@ -516,6 +516,14 @@ public: /// Created async task or null if cannot. GPUTask* UploadMipMapAsync(const BytesContainer& data, int32 mipIndex, int32 rowPitch, int32 slicePitch, bool copyData = false); + /// + /// Uploads texture data to the GPU. Actual data copy to the GPU memory is performed via async task. + /// + /// Data to upload, it must match texture dimensions. It must be valid for the next couple of frames due to GPU async task latency or use data copy. + /// If true, the data will be copied to the async execution task instead of using the input pointer provided. + /// True if cannot upload data, otherwise false. + API_FUNCTION() bool UploadData(TextureData& data, bool copyData = false); + /// /// Downloads the texture data to be accessible from CPU. For frequent access, use staging textures, otherwise current thread will be stalled to wait for the GPU frame to copy data into staging buffer. /// From bd2add7edd4b7ecf9ea5e1cce6987ce950ce07a2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Jun 2025 23:15:58 +0200 Subject: [PATCH 049/122] Tweak memory command tip --- Source/Editor/Windows/Profiler/Memory.cs | 2 +- Source/Engine/Profiler/ProfilerMemory.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Windows/Profiler/Memory.cs b/Source/Editor/Windows/Profiler/Memory.cs index 6958b828b..a74472a8c 100644 --- a/Source/Editor/Windows/Profiler/Memory.cs +++ b/Source/Editor/Windows/Profiler/Memory.cs @@ -71,7 +71,7 @@ namespace FlaxEditor.Windows.Profiler { _warningText = new Label { - Text = "Detailed memory profiling is disabled. Run with command line: -mem", + Text = "Detailed memory profiling is disabled. Run with command line '-mem'", TextColor = Color.Red, Visible = false, Parent = layout, diff --git a/Source/Engine/Profiler/ProfilerMemory.cpp b/Source/Engine/Profiler/ProfilerMemory.cpp index d53e48b17..c936ff5b2 100644 --- a/Source/Engine/Profiler/ProfilerMemory.cpp +++ b/Source/Engine/Profiler/ProfilerMemory.cpp @@ -160,7 +160,7 @@ namespace // Warn that data might be missing due to inactive profiler if (!ProfilerMemory::Enabled) - output.AppendLine(TEXT("Detailed memory profiling is disabled. Run with command line: -mem")); + output.AppendLine(TEXT("Detailed memory profiling is disabled. Run with command line '-mem'")); } #ifdef USE_TRACY_MEMORY_PLOTS From eb6010cba74286c52f3eb255fc13f67bf82b8271 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Jun 2025 23:20:20 +0200 Subject: [PATCH 050/122] Fix `BitArray` again --- Source/Engine/Core/Collections/BitArray.h | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h index 6589fa9a1..28a71351b 100644 --- a/Source/Engine/Core/Collections/BitArray.h +++ b/Source/Engine/Core/Collections/BitArray.h @@ -16,7 +16,6 @@ API_CLASS(InBuild) class BitArray public: using ItemType = uint64; using AllocationData = typename AllocationType::template Data; - static constexpr int32 ItemBitCount = 64; private: int32 _count; @@ -210,8 +209,8 @@ public: bool Get(const int32 index) const { ASSERT(index >= 0 && index < _count); - const ItemType offset = index / ItemBitCount; - const ItemType bitMask = (ItemType)(1 << (index & (ItemBitCount - 1))); + const ItemType offset = index / 64; + const ItemType bitMask = 1ull << (index & 63ull); const ItemType item = ((ItemType*)_allocation.Get())[offset]; return (item & bitMask) != 0; } @@ -224,8 +223,8 @@ public: void Set(const int32 index, const bool value) { ASSERT(index >= 0 && index < _count); - const ItemType offset = index / ItemBitCount; - const ItemType bitMask = (ItemType)(1 << (index & (ItemBitCount - 1))); + const ItemType offset = index / 64; + const ItemType bitMask = 1ull << (index & 63ull); ItemType& item = ((ItemType*)_allocation.Get())[offset]; if (value) item |= bitMask; // Set the bit From bcd2b1f0f203ea8996b87d55b149641a784aea2b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Jun 2025 23:27:26 +0200 Subject: [PATCH 051/122] Fix `Array::RemoveAt` to move element and avoid data copies --- Source/Engine/Core/Collections/Array.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Core/Collections/Array.h b/Source/Engine/Core/Collections/Array.h index 5845d7f50..01b5d044c 100644 --- a/Source/Engine/Core/Collections/Array.h +++ b/Source/Engine/Core/Collections/Array.h @@ -697,7 +697,7 @@ public: --_count; T* data = _allocation.Get(); if (_count) - data[index] = data[_count]; + data[index] = MoveTemp(data[_count]); Memory::DestructItems(data + _count, 1); } From 9cfcbfa9d25f15b11ed0bfa6359c6ca0e5f25040 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Jun 2025 23:28:15 +0200 Subject: [PATCH 052/122] Fix memory leak on recast objects when building NavMesh tile --- Source/Engine/Navigation/NavMeshBuilder.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index d9f26522d..2f8a5b113 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -10,6 +10,7 @@ #include "NavModifierVolume.h" #include "NavMeshRuntime.h" #include "Engine/Core/Log.h" +#include "Engine/Core/ScopeExit.h" #include "Engine/Core/Math/BoundingBox.h" #include "Engine/Core/Math/Vector3.h" #include "Engine/Physics/Colliders/BoxCollider.h" @@ -422,6 +423,7 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B LOG(Warning, "Could not generate navmesh: Out of memory for heightfield."); return true; } + SCOPE_EXIT{ rcFreeHeightField(heightfield); }; if (!rcCreateHeightfield(&context, *heightfield, config.width, config.height, config.bmin, config.bmax, config.cs, config.ch)) { LOG(Warning, "Could not generate navmesh: Could not create solid heightfield."); @@ -475,6 +477,7 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B LOG(Warning, "Could not generate navmesh: Out of memory compact heightfield."); return true; } + SCOPE_EXIT{ rcFreeCompactHeightfield(compactHeightfield); }; { PROFILE_CPU_NAMED("CompactHeightfield"); if (!rcBuildCompactHeightfield(&context, config.walkableHeight, config.walkableClimb, *heightfield, *compactHeightfield)) @@ -483,7 +486,6 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B return true; } } - rcFreeHeightField(heightfield); { PROFILE_CPU_NAMED("ErodeWalkableArea"); if (!rcErodeWalkableArea(&context, config.walkableRadius, *compactHeightfield)) @@ -528,6 +530,7 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B LOG(Warning, "Could not generate navmesh: Out of memory for contour set."); return true; } + SCOPE_EXIT{ rcFreeContourSet(contourSet); }; { PROFILE_CPU_NAMED("BuildContours"); if (!rcBuildContours(&context, *compactHeightfield, config.maxSimplificationError, config.maxEdgeLen, *contourSet)) @@ -543,6 +546,7 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B LOG(Warning, "Could not generate navmesh: Out of memory for poly mesh."); return true; } + SCOPE_EXIT{ rcFreePolyMesh(polyMesh); }; { PROFILE_CPU_NAMED("BuildPolyMesh"); if (!rcBuildPolyMesh(&context, *contourSet, config.maxVertsPerPoly, *polyMesh)) @@ -558,6 +562,7 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B LOG(Warning, "Could not generate navmesh: Out of memory for detail mesh."); return true; } + SCOPE_EXIT{ rcFreePolyMeshDetail(detailMesh); }; { PROFILE_CPU_NAMED("BuildPolyMeshDetail"); if (!rcBuildPolyMeshDetail(&context, *polyMesh, *compactHeightfield, config.detailSampleDist, config.detailSampleMaxError, *detailMesh)) @@ -567,9 +572,6 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B } } - rcFreeCompactHeightfield(compactHeightfield); - rcFreeContourSet(contourSet); - for (int i = 0; i < polyMesh->npolys; i++) polyMesh->flags[i] = polyMesh->areas[i] != RC_NULL_AREA ? 1 : 0; if (polyMesh->nverts == 0) @@ -661,7 +663,6 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B { PROFILE_CPU_NAMED("CreateTiles"); - ScopeLock lock(runtime->Locker); navMesh->IsDataDirty = true; From e41c48f9e510c89f36b6f0c955cf913df872e9ef Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 18 Jun 2025 22:41:26 +0200 Subject: [PATCH 053/122] Optimize NavMesh builder when updating a large number of tiles --- Source/Engine/Navigation/NavMeshBuilder.cpp | 227 ++++++++++++++------ Source/Engine/Navigation/NavMeshRuntime.cpp | 33 ++- 2 files changed, 176 insertions(+), 84 deletions(-) diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index 2f8a5b113..7bfa40da3 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -68,6 +68,11 @@ struct Modifier NavAreaProperties* NavArea; }; +struct TileId +{ + int32 X, Y, Layer; +}; + struct NavSceneRasterizer { NavMesh* NavMesh; @@ -198,13 +203,15 @@ struct NavSceneRasterizer // Transform vertices into world space vertex buffer vb.Resize(vertexCount); + Float3* vbData = vb.Get(); for (int32 i = 0; i < vertexCount; i++) - vb[i] = sphere.Center + vertices[i] * sphere.Radius; + vbData[i] = sphere.Center + vertices[i] * sphere.Radius; // Generate index buffer const int32 stride = horizontalSegments + 1; int32 indexCount = 0; ib.Resize(verticalSegments * (horizontalSegments + 1) * 6); + int32* ibData = ib.Get(); for (int32 i = 0; i < verticalSegments; i++) { const int32 nextI = i + 1; @@ -212,13 +219,13 @@ struct NavSceneRasterizer { const int32 nextJ = (j + 1) % stride; - ib[indexCount++] = i * stride + j; - ib[indexCount++] = nextI * stride + j; - ib[indexCount++] = i * stride + nextJ; + ibData[indexCount++] = i * stride + j; + ibData[indexCount++] = nextI * stride + j; + ibData[indexCount++] = i * stride + nextJ; - ib[indexCount++] = i * stride + nextJ; - ib[indexCount++] = nextI * stride + j; - ib[indexCount++] = nextI * stride + nextJ; + ibData[indexCount++] = i * stride + nextJ; + ibData[indexCount++] = nextI * stride + j; + ibData[indexCount++] = nextI * stride + nextJ; } } } @@ -335,54 +342,8 @@ struct NavSceneRasterizer } }; -// Builds navmesh tile bounds and check if there are any valid navmesh volumes at that tile location -// Returns true if tile is intersecting with any navmesh bounds volume actor - which means tile is in use -bool GetNavMeshTileBounds(Scene* scene, NavMesh* navMesh, int32 x, int32 y, float tileSize, BoundingBox& tileBoundsNavMesh, const Matrix& worldToNavMesh) -{ - // Build initial tile bounds (with infinite extent) - tileBoundsNavMesh.Minimum.X = (float)x * tileSize; - tileBoundsNavMesh.Minimum.Y = -NAV_MESH_TILE_MAX_EXTENT; - tileBoundsNavMesh.Minimum.Z = (float)y * tileSize; - tileBoundsNavMesh.Maximum.X = tileBoundsNavMesh.Minimum.X + tileSize; - tileBoundsNavMesh.Maximum.Y = NAV_MESH_TILE_MAX_EXTENT; - tileBoundsNavMesh.Maximum.Z = tileBoundsNavMesh.Minimum.Z + tileSize; - - // Check if any navmesh volume intersects with the tile - bool foundAnyVolume = false; - Vector2 rangeY; - for (int32 i = 0; i < scene->Navigation.Volumes.Count(); i++) - { - const auto volume = scene->Navigation.Volumes[i]; - if (!volume->AgentsMask.IsNavMeshSupported(navMesh->Properties)) - continue; - const auto& volumeBounds = volume->GetBox(); - BoundingBox volumeBoundsNavMesh; - BoundingBox::Transform(volumeBounds, worldToNavMesh, volumeBoundsNavMesh); - if (volumeBoundsNavMesh.Intersects(tileBoundsNavMesh)) - { - if (foundAnyVolume) - { - rangeY.X = Math::Min(rangeY.X, volumeBoundsNavMesh.Minimum.Y); - rangeY.Y = Math::Max(rangeY.Y, volumeBoundsNavMesh.Maximum.Y); - } - else - { - rangeY.X = volumeBoundsNavMesh.Minimum.Y; - rangeY.Y = volumeBoundsNavMesh.Maximum.Y; - } - foundAnyVolume = true; - } - } - - if (foundAnyVolume) - { - // Build proper tile bounds - tileBoundsNavMesh.Minimum.Y = rangeY.X; - tileBoundsNavMesh.Maximum.Y = rangeY.Y; - } - - return foundAnyVolume; -} +void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime); +void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y); void RemoveTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, int32 layer) { @@ -404,9 +365,10 @@ void RemoveTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, int runtime->RemoveTile(x, y, layer); } -bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, BoundingBox& tileBoundsNavMesh, const Matrix& worldToNavMesh, float tileSize, rcConfig& config) +bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, BoundingBox& tileBoundsNavMesh, const Matrix& worldToNavMesh, float tileSize, rcConfig& config, Task* task) { rcContext context; + context.enableLog(false); int32 layer = 0; // Expand tile bounds by a certain margin @@ -464,6 +426,9 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B } } + if (task->IsCancelRequested()) + return false; + { PROFILE_CPU_NAMED("FilterHeightfield"); rcFilterLowHangingWalkableObstacles(&context, config.walkableClimb, *heightfield); @@ -507,6 +472,9 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B } } + if (task->IsCancelRequested()) + return false; + { PROFILE_CPU_NAMED("BuildDistanceField"); if (!rcBuildDistanceField(&context, *compactHeightfield)) @@ -648,6 +616,9 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B params.offMeshConUserID = offMeshId.Get(); } + if (task->IsCancelRequested()) + return false; + // Generate navmesh tile data unsigned char* navData = nullptr; int navDataSize = 0; @@ -661,6 +632,7 @@ bool GenerateTile(NavMesh* navMesh, NavMeshRuntime* runtime, int32 x, int32 y, B } ASSERT_LOW_LAYER(navDataSize > 4 && *(uint32*)navData == DT_NAVMESH_MAGIC); // Sanity check for Detour header + if (!task->IsCancelRequested()) { PROFILE_CPU_NAMED("CreateTiles"); ScopeLock lock(runtime->Locker); @@ -762,7 +734,7 @@ public: const auto navMesh = NavMesh.Get(); if (!navMesh) return false; - if (GenerateTile(NavMesh, Runtime, X, Y, TileBoundsNavMesh, WorldToNavMesh, TileSize, Config)) + if (GenerateTile(NavMesh, Runtime, X, Y, TileBoundsNavMesh, WorldToNavMesh, TileSize, Config, this)) { LOG(Warning, "Failed to generate navmesh tile at {0}x{1}.", X, Y); } @@ -779,6 +751,50 @@ public: } }; +void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime) +{ + NavBuildTasksLocker.Lock(); + for (int32 i = 0; i < NavBuildTasks.Count(); i++) + { + auto task = NavBuildTasks[i]; + if (task->Runtime == runtime) + { + NavBuildTasksLocker.Unlock(); + + // Cancel task but without locking queue from this thread to prevent deadlocks + task->Cancel(); + + NavBuildTasksLocker.Lock(); + i--; + if (NavBuildTasks.IsEmpty()) + break; + } + } + NavBuildTasksLocker.Unlock(); +} + +void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y) +{ + NavBuildTasksLocker.Lock(); + for (int32 i = 0; i < NavBuildTasks.Count(); i++) + { + auto task = NavBuildTasks[i]; + if (task->Runtime == runtime && task->X == x && task->Y == y) + { + NavBuildTasksLocker.Unlock(); + + // Cancel task but without locking queue from this thread to prevent deadlocks + task->Cancel(); + + NavBuildTasksLocker.Lock(); + i--; + if (NavBuildTasks.IsEmpty()) + break; + } + } + NavBuildTasksLocker.Unlock(); +} + void OnSceneUnloading(Scene* scene, const Guid& sceneId) { // Cancel pending build requests @@ -843,14 +859,15 @@ float NavMeshBuilder::GetNavMeshBuildingProgress() void BuildTileAsync(NavMesh* navMesh, const int32 x, const int32 y, const rcConfig& config, const BoundingBox& tileBoundsNavMesh, const Matrix& worldToNavMesh, float tileSize) { + PROFILE_CPU(); NavMeshRuntime* runtime = navMesh->GetRuntime(); NavBuildTasksLocker.Lock(); // Skip if this tile is already during cooking for (int32 i = 0; i < NavBuildTasks.Count(); i++) { - const auto task = NavBuildTasks[i]; - if (task->X == x && task->Y == y && task->Runtime == runtime) + const auto task = NavBuildTasks.Get()[i]; + if (task->GetState() == TaskState::Queued && task->X == x && task->Y == y && task->Runtime == runtime) { NavBuildTasksLocker.Unlock(); return; @@ -899,11 +916,16 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo { PROFILE_CPU_NAMED("Prepare"); + runtime->Locker.Lock(); // Prepare scene data and navmesh rebuild |= Math::NotNearEqual(navMesh->Data.TileSize, tileSize); if (rebuild) { + runtime->Locker.Unlock(); + CancelNavMeshTileBuildTasks(runtime); + runtime->Locker.Lock(); + // Remove all tiles from navmesh runtime runtime->RemoveTiles(navMesh); runtime->SetTileSize(tileSize); @@ -918,9 +940,10 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo else { // Ensure to have enough memory for tiles - runtime->SetTileSize(tileSize); runtime->EnsureCapacity(tilesX * tilesY); } + + runtime->Locker.Unlock(); } // Initialize nav mesh configuration @@ -931,21 +954,93 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo { PROFILE_CPU_NAMED("StartBuildingTiles"); + // Cache navmesh volumes + Array> volumes; + for (int32 i = 0; i < scene->Navigation.Volumes.Count(); i++) + { + const auto volume = scene->Navigation.Volumes.Get()[i]; + if (!volume->AgentsMask.IsNavMeshSupported(navMesh->Properties) || + !volume->GetBox().Intersects(dirtyBoundsAligned)) + continue; + auto& bounds = volumes.AddOne(); + BoundingBox::Transform(volume->GetBox(), worldToNavMesh, bounds); + } + + Array unusedTiles; + Array> usedTiles; for (int32 y = tilesMin.Z; y < tilesMax.Z; y++) { for (int32 x = tilesMin.X; x < tilesMax.X; x++) { + // Build initial tile bounds (with infinite extent) BoundingBox tileBoundsNavMesh; - if (GetNavMeshTileBounds(scene, navMesh, x, y, tileSize, tileBoundsNavMesh, worldToNavMesh)) + tileBoundsNavMesh.Minimum.X = (float)x * tileSize; + tileBoundsNavMesh.Minimum.Y = -NAV_MESH_TILE_MAX_EXTENT; + tileBoundsNavMesh.Minimum.Z = (float)y * tileSize; + tileBoundsNavMesh.Maximum.X = tileBoundsNavMesh.Minimum.X + tileSize; + tileBoundsNavMesh.Maximum.Y = NAV_MESH_TILE_MAX_EXTENT; + tileBoundsNavMesh.Maximum.Z = tileBoundsNavMesh.Minimum.Z + tileSize; + + // Check if any navmesh volume intersects with the tile + bool foundAnyVolume = false; + Vector2 rangeY; + for (const auto& bounds : volumes) { - BuildTileAsync(navMesh, x, y, config, tileBoundsNavMesh, worldToNavMesh, tileSize); + if (bounds.Intersects(tileBoundsNavMesh)) + { + if (foundAnyVolume) + { + rangeY.X = Math::Min(rangeY.X, bounds.Minimum.Y); + rangeY.Y = Math::Max(rangeY.Y, bounds.Maximum.Y); + } + else + { + rangeY.X = bounds.Minimum.Y; + rangeY.Y = bounds.Maximum.Y; + foundAnyVolume = true; + } + } + } + + // Check if tile is intersecting with any navmesh bounds volume actor - which means tile is in use + if (foundAnyVolume) + { + // Setup proper tile bounds + tileBoundsNavMesh.Minimum.Y = rangeY.X; + tileBoundsNavMesh.Maximum.Y = rangeY.Y; + usedTiles.Add({ { x, y, 0 }, tileBoundsNavMesh }); } else { - RemoveTile(navMesh, runtime, x, y, 0); + unusedTiles.Add({ x, y, 0 }); } } } + + // Remove unused tiles + { + PROFILE_CPU_NAMED("RemoveUnused"); + for (const auto& tile : unusedTiles) + { + // Wait for any async tasks that are producing this tile + CancelNavMeshTileBuildTasks(runtime, tile.X, tile.Y); + } + runtime->Locker.Lock(); + for (const auto& tile : unusedTiles) + { + RemoveTile(navMesh, runtime, tile.X, tile.Y, 0); + } + runtime->Locker.Unlock(); + } + + // Build used tiles + { + PROFILE_CPU_NAMED("AddNew"); + for (const auto& e : usedTiles) + { + BuildTileAsync(navMesh, e.First.X, e.First.Y, config, e.Second, worldToNavMesh, tileSize); + } + } } } @@ -1025,7 +1120,7 @@ void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds, bool rebuild NavBuildTasksLocker.Lock(); for (int32 i = 0; i < NavBuildTasks.Count(); i++) { - if (NavBuildTasks[i]->NavMesh == navMesh) + if (NavBuildTasks.Get()[i]->NavMesh == navMesh) usageCount++; } NavBuildTasksLocker.Unlock(); @@ -1064,7 +1159,7 @@ void NavMeshBuilder::Update() const auto now = DateTime::NowUTC(); for (int32 i = 0; NavBuildQueue.HasItems() && i < NavBuildQueue.Count(); i++) { - auto req = NavBuildQueue[i]; + auto req = NavBuildQueue.Get()[i]; if (now - req.Time >= 0) { NavBuildQueue.RemoveAt(i--); @@ -1118,7 +1213,7 @@ void NavMeshBuilder::Build(Scene* scene, float timeoutMs) for (int32 i = 0; i < NavBuildQueue.Count(); i++) { - auto& e = NavBuildQueue[i]; + auto& e = NavBuildQueue.Get()[i]; if (e.Scene == scene && e.DirtyBounds == req.DirtyBounds) { e = req; diff --git a/Source/Engine/Navigation/NavMeshRuntime.cpp b/Source/Engine/Navigation/NavMeshRuntime.cpp index 37e09294d..640b5dd9b 100644 --- a/Source/Engine/Navigation/NavMeshRuntime.cpp +++ b/Source/Engine/Navigation/NavMeshRuntime.cpp @@ -15,6 +15,19 @@ #define USE_DATA_LINK 0 #define USE_NAV_MESH_ALLOC 0 +#if USE_NAV_MESH_ALLOC +#define GET_NAV_TILE_DATA(tile) \ + const int32 dataSize = (tile).Data.Length(); \ + const auto flags = DT_TILE_FREE_DATA; \ + const auto data = (byte*)dtAlloc(dataSize, DT_ALLOC_PERM); \ + Platform::MemoryCopy(data, (tile).Data.Get(), dataSize) +#else +#define GET_NAV_TILE_DATA(tile) \ + const int32 dataSize = (tile).Data.Length(); \ + const auto flags = 0; \ + const auto data = (tile).Data.Get() +#endif + namespace { FORCE_INLINE void InitFilter(dtQueryFilter& filter) @@ -353,15 +366,7 @@ void NavMeshRuntime::EnsureCapacity(int32 tilesToAddCount) // Restore previous tiles for (auto& tile : _tiles) { - const int32 dataSize = tile.Data.Length(); -#if USE_NAV_MESH_ALLOC - const auto flags = DT_TILE_FREE_DATA; - const auto data = (byte*)dtAlloc(dataSize, DT_ALLOC_PERM); - Platform::MemoryCopy(data, tile.Data.Get(), dataSize); -#else - const auto flags = 0; - const auto data = tile.Data.Get(); -#endif + GET_NAV_TILE_DATA(tile); const auto result = _navMesh->addTile(data, dataSize, flags, 0, nullptr); if (dtStatusFailed(result)) { @@ -661,15 +666,7 @@ void NavMeshRuntime::AddTileInternal(NavMesh* navMesh, NavMeshTileData& tileData #endif // Add tile to navmesh - const int32 dataSize = tile->Data.Length(); -#if USE_NAV_MESH_ALLOC - const auto flags = DT_TILE_FREE_DATA; - const auto data = (byte*)dtAlloc(dataSize, DT_ALLOC_PERM); - Platform::MemoryCopy(data, tile->Data.Get(), dataSize); -#else - const auto flags = 0; - const auto data = tile->Data.Get(); -#endif + GET_NAV_TILE_DATA(*tile); const auto result = _navMesh->addTile(data, dataSize, flags, 0, nullptr); if (dtStatusFailed(result)) { From 6479a3d3c6c97735befc1be1185a487ab5b3a34a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 18 Jun 2025 22:41:42 +0200 Subject: [PATCH 054/122] Fix log spam in task when waiting for task to end --- Source/Engine/Threading/Task.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/Engine/Threading/Task.cpp b/Source/Engine/Threading/Task.cpp index 601079e85..57aaf7bb8 100644 --- a/Source/Engine/Threading/Task.cpp +++ b/Source/Engine/Threading/Task.cpp @@ -42,8 +42,6 @@ bool Task::Wait(double timeoutMilliseconds) const PROFILE_CPU(); const double startTime = Platform::GetTimeSeconds(); - // TODO: no active waiting! use a semaphore! - do { auto state = GetState(); @@ -209,7 +207,7 @@ void Task::OnCancel() { // Wait for it a little bit constexpr double timeout = 10000.0; // 10s - LOG(Warning, "Cannot cancel \'{0}\' because it's still running, waiting for end with timeout: {1}ms", ToString(), timeout); + //LOG(Warning, "Cannot cancel \'{0}\' because it's still running, waiting for end with timeout: {1}ms", ToString(), timeout); Wait(timeout); } From 62e329ac6e491e8cb0eea6acea81de6eef4c6d3b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 18 Jun 2025 23:00:43 +0200 Subject: [PATCH 055/122] Add more memory tags for Navigation --- Source/Engine/Navigation/NavMesh.cpp | 1 + Source/Engine/Navigation/NavMeshBuilder.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Navigation/NavMesh.cpp b/Source/Engine/Navigation/NavMesh.cpp index ee3e48f3e..b48bf26c7 100644 --- a/Source/Engine/Navigation/NavMesh.cpp +++ b/Source/Engine/Navigation/NavMesh.cpp @@ -110,6 +110,7 @@ void NavMesh::OnAssetLoaded(Asset* asset, void* caller) if (Data.Tiles.HasItems()) return; ScopeLock lock(DataAsset->Locker); + PROFILE_MEM(Navigation); // Remove added tiles if (_navMeshActive) diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index dbfecefcb..e5fdec5da 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -732,6 +732,7 @@ public: bool Run() override { PROFILE_CPU_NAMED("BuildNavMeshTile"); + PROFILE_MEM(Navigation); const auto navMesh = NavMesh.Get(); if (!navMesh) return false; @@ -1154,6 +1155,7 @@ void ClearNavigation(Scene* scene) void NavMeshBuilder::Update() { + PROFILE_MEM(Navigation); ScopeLock lock(NavBuildQueueLocker); // Process nav mesh building requests and kick the tasks @@ -1204,7 +1206,7 @@ void NavMeshBuilder::Build(Scene* scene, float timeoutMs) } PROFILE_CPU_NAMED("NavMeshBuilder"); - + PROFILE_MEM(Navigation); ScopeLock lock(NavBuildQueueLocker); BuildRequest req; @@ -1241,7 +1243,7 @@ void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float t } PROFILE_CPU_NAMED("NavMeshBuilder"); - + PROFILE_MEM(Navigation); ScopeLock lock(NavBuildQueueLocker); BuildRequest req; From edb68849426fbd527392bdc1c6f74876a306972a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Jun 2025 08:24:26 +0200 Subject: [PATCH 056/122] Optimize PhysX work dispatcher to be shared by all scenes --- .../Engine/Physics/PhysX/PhysicsBackendPhysX.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index 768788f90..b15b22a9b 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -82,7 +82,6 @@ struct ActionDataPhysX struct ScenePhysX { PxScene* Scene = nullptr; - PxCpuDispatcher* CpuDispatcher = nullptr; PxControllerManager* ControllerManager = nullptr; void* ScratchMemory = nullptr; Vector3 Origin = Vector3::Zero; @@ -542,6 +541,7 @@ namespace { PxFoundation* Foundation = nullptr; PxPhysics* PhysX = nullptr; + PxDefaultCpuDispatcher* CpuDispatcher = nullptr; #if WITH_PVD PxPvd* PVD = nullptr; #endif @@ -1734,6 +1734,7 @@ void PhysicsBackend::Shutdown() #if WITH_PVD RELEASE_PHYSX(PVD); #endif + RELEASE_PHYSX(CpuDispatcher); RELEASE_PHYSX(Foundation); SceneOrigins.Clear(); } @@ -1791,9 +1792,13 @@ void* PhysicsBackend::CreateScene(const PhysicsSettings& settings) } if (sceneDesc.cpuDispatcher == nullptr) { - scenePhysX->CpuDispatcher = PxDefaultCpuDispatcherCreate(Math::Clamp(Platform::GetCPUInfo().ProcessorCoreCount - 1, 1, 4)); - CHECK_INIT(scenePhysX->CpuDispatcher, "PxDefaultCpuDispatcherCreate failed!"); - sceneDesc.cpuDispatcher = scenePhysX->CpuDispatcher; + if (CpuDispatcher == nullptr) + { + uint32 threads = Math::Clamp(Platform::GetCPUInfo().ProcessorCoreCount - 1, 1, 4); + CpuDispatcher = PxDefaultCpuDispatcherCreate(threads); + CHECK_INIT(CpuDispatcher, "PxDefaultCpuDispatcherCreate failed!"); + } + sceneDesc.cpuDispatcher = CpuDispatcher; } switch (settings.BroadPhaseType) { @@ -1855,7 +1860,6 @@ void PhysicsBackend::DestroyScene(void* scene) } #endif RELEASE_PHYSX(scenePhysX->ControllerManager); - SAFE_DELETE(scenePhysX->CpuDispatcher); Allocator::Free(scenePhysX->ScratchMemory); scenePhysX->Scene->release(); From 6144f6c74e16961992cb8599fb62b29772fa8002 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Jun 2025 09:50:07 +0200 Subject: [PATCH 057/122] Optimize physics simulation with higher limit of `8` threads --- Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index b15b22a9b..673b1e96d 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -1794,7 +1794,7 @@ void* PhysicsBackend::CreateScene(const PhysicsSettings& settings) { if (CpuDispatcher == nullptr) { - uint32 threads = Math::Clamp(Platform::GetCPUInfo().ProcessorCoreCount - 1, 1, 4); + uint32 threads = Math::Clamp(Platform::GetCPUInfo().ProcessorCoreCount - 1, 1, 8); CpuDispatcher = PxDefaultCpuDispatcherCreate(threads); CHECK_INIT(CpuDispatcher, "PxDefaultCpuDispatcherCreate failed!"); } From 4ac870f7012964f699daa94b2f66e075ab371d8c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Jun 2025 13:57:50 +0200 Subject: [PATCH 058/122] Optimize physics transformation updates propagation in async via Job System --- Source/Engine/Physics/Actors/RigidBody.cpp | 8 +++++ .../Physics/PhysX/PhysicsBackendPhysX.cpp | 31 +++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Physics/Actors/RigidBody.cpp b/Source/Engine/Physics/Actors/RigidBody.cpp index a58911dfb..565bd9fbd 100644 --- a/Source/Engine/Physics/Actors/RigidBody.cpp +++ b/Source/Engine/Physics/Actors/RigidBody.cpp @@ -468,6 +468,14 @@ void RigidBody::OnActiveTransformChanged() void RigidBody::BeginPlay(SceneBeginData* data) { +#if USE_EDITOR || !BUILD_RELEASE + // FlushActiveTransforms runs in async for each separate actor thus we don't support two rigidbodies that transformations depend on each other + if (Cast(GetParent())) + { + LOG(Warning, "Rigid Body '{0}' is attached to other Rigid Body which is not unsupported and might cause physical simulation instability.", GetNamePath()); + } +#endif + // Create rigid body ASSERT(_actor == nullptr); void* scene = GetPhysicsScene()->GetPhysicsScene(); diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index 673b1e96d..99b54e8fc 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -1901,6 +1901,23 @@ void PhysicsBackend::StartSimulateScene(void* scene, float dt) scenePhysX->Stepper.renderDone(); } +PxActor** CachedActiveActors; +int64 CachedActiveActorsCount; +volatile int64 CachedActiveActorIndex; + +void FlushActiveTransforms(int32 i) +{ + PROFILE_CPU(); + int64 index; + while ((index = Platform::InterlockedIncrement(&CachedActiveActorIndex)) < CachedActiveActorsCount) + { + const auto pxActor = (PxRigidActor*)CachedActiveActors[index]; + auto actor = static_cast(pxActor->userData); + if (actor) + actor->OnActiveTransformChanged(); + } +} + void PhysicsBackend::EndSimulateScene(void* scene) { PROFILE_MEM(Physics); @@ -1919,10 +1936,18 @@ void PhysicsBackend::EndSimulateScene(void* scene) // Gather change info PxU32 activeActorsCount; PxActor** activeActors = scenePhysX->Scene->getActiveActors(activeActorsCount); - if (activeActorsCount > 0) + + // Update changed transformations + if (activeActorsCount > 50 && JobSystem::GetThreadsCount() > 1) + { + // Run in async via job system + CachedActiveActors = activeActors; + CachedActiveActorsCount = activeActorsCount; + CachedActiveActorIndex = -1; + JobSystem::Execute(FlushActiveTransforms, JobSystem::GetThreadsCount()); + } + else { - // Update changed transformations - // TODO: use jobs system if amount if huge for (uint32 i = 0; i < activeActorsCount; i++) { const auto pxActor = (PxRigidActor*)*activeActors++; From 2e10d776e9bc4e80323a3bb82f62337e21b36971 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Jun 2025 14:04:06 +0200 Subject: [PATCH 059/122] Optimize updating actor rendering entry with better thread locking that support async writes on actor update --- Source/Engine/Level/Scene/SceneRendering.cpp | 14 +++++++------- Source/Engine/Level/Scene/SceneRendering.h | 4 ++-- Source/Engine/Threading/ConcurrentSystemLocker.cpp | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Source/Engine/Level/Scene/SceneRendering.cpp b/Source/Engine/Level/Scene/SceneRendering.cpp index fe2bc310f..4a88703ae 100644 --- a/Source/Engine/Level/Scene/SceneRendering.cpp +++ b/Source/Engine/Level/Scene/SceneRendering.cpp @@ -43,7 +43,7 @@ FORCE_INLINE bool FrustumsListCull(const BoundingSphere& bounds, const ArrayScenes.Add(this); // Add additional lock during scene rendering (prevents any Actors cache modifications on content streaming threads - eg. when model residency changes) - Locker.Lock(); + Locker.Begin(false); } else if (category == PostRender) { // Release additional lock - Locker.Unlock(); + Locker.End(false); } auto& view = renderContextBatch.GetMainContext().View; auto& list = Actors[(int32)category]; @@ -127,7 +127,7 @@ void SceneRendering::CollectPostFxVolumes(RenderContext& renderContext) void SceneRendering::Clear() { - ScopeLock lock(Locker); + ConcurrentSystemLocker::WriteScope lock(Locker); for (auto* listener : _listeners) { listener->OnSceneRenderingClear(this); @@ -149,7 +149,7 @@ void SceneRendering::AddActor(Actor* a, int32& key) return; PROFILE_MEM(Graphics); const int32 category = a->_drawCategory; - ScopeLock lock(Locker); + ConcurrentSystemLocker::WriteScope lock(Locker); auto& list = Actors[category]; if (FreeActors[category].HasItems()) { @@ -174,7 +174,7 @@ void SceneRendering::AddActor(Actor* a, int32& key) void SceneRendering::UpdateActor(Actor* a, int32& key, ISceneRenderingListener::UpdateFlags flags) { const int32 category = a->_drawCategory; - ScopeLock lock(Locker); + ConcurrentSystemLocker::ReadScope lock(Locker); // Read-access only as list doesn't get resized (like Add/Remove do) so allow updating actors from different threads at once auto& list = Actors[category]; if (list.Count() <= key) // Ignore invalid key softly return; @@ -193,7 +193,7 @@ void SceneRendering::UpdateActor(Actor* a, int32& key, ISceneRenderingListener:: void SceneRendering::RemoveActor(Actor* a, int32& key) { const int32 category = a->_drawCategory; - ScopeLock lock(Locker); + ConcurrentSystemLocker::WriteScope lock(Locker); auto& list = Actors[category]; if (list.Count() > key) // Ignore invalid key softly (eg. list after batch clear during scene unload) { diff --git a/Source/Engine/Level/Scene/SceneRendering.h b/Source/Engine/Level/Scene/SceneRendering.h index b24dcdfa9..59f997f6b 100644 --- a/Source/Engine/Level/Scene/SceneRendering.h +++ b/Source/Engine/Level/Scene/SceneRendering.h @@ -7,7 +7,7 @@ #include "Engine/Core/Math/BoundingSphere.h" #include "Engine/Core/Math/BoundingFrustum.h" #include "Engine/Level/Actor.h" -#include "Engine/Platform/CriticalSection.h" +#include "Engine/Threading/ConcurrentSystemLocker.h" class SceneRenderTask; class SceneRendering; @@ -102,7 +102,7 @@ public: Array Actors[MAX]; Array FreeActors[MAX]; Array PostFxProviders; - CriticalSection Locker; + ConcurrentSystemLocker Locker; private: #if USE_EDITOR diff --git a/Source/Engine/Threading/ConcurrentSystemLocker.cpp b/Source/Engine/Threading/ConcurrentSystemLocker.cpp index c8569b119..f8eab96d9 100644 --- a/Source/Engine/Threading/ConcurrentSystemLocker.cpp +++ b/Source/Engine/Threading/ConcurrentSystemLocker.cpp @@ -18,7 +18,7 @@ RETRY: { // Someone else is doing opposite operation so wait for it's end // TODO: use ConditionVariable+CriticalSection to prevent active-waiting - Platform::Sleep(1); + Platform::Sleep(0); goto RETRY; } From 3cb74d48ca4ac57a27f830715952949c69394c40 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Thu, 19 Jun 2025 20:01:27 -0500 Subject: [PATCH 060/122] Convert using event to using window method overrides. --- Source/Editor/Windows/SceneTreeWindow.cs | 33 ++++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index 851cd62f3..75a3723cf 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -104,11 +104,6 @@ namespace FlaxEditor.Windows headerPanel.Parent = this; - Editor.PlayModeBeginning += () => _blockSceneTreeScroll = true; - Editor.PlayModeBegin += () => _blockSceneTreeScroll = false; - Editor.PlayModeEnding += () => _blockSceneTreeScroll = true; - Editor.PlayModeEnd += () => _blockSceneTreeScroll = false; - // Setup input actions InputActions.Add(options => options.TranslateMode, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate); InputActions.Add(options => options.RotateMode, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate); @@ -117,6 +112,34 @@ namespace FlaxEditor.Windows InputActions.Add(options => options.LockFocusSelection, () => Editor.Windows.EditWin.Viewport.LockFocusSelection()); InputActions.Add(options => options.Rename, RenameSelection); } + + /// + public override void OnPlayBeginning() + { + base.OnPlayBeginning(); + _blockSceneTreeScroll = true; + } + + /// + public override void OnPlayBegin() + { + base.OnPlayBegin(); + _blockSceneTreeScroll = false; + } + + /// + public override void OnPlayEnding() + { + base.OnPlayEnding(); + _blockSceneTreeScroll = true; + } + + /// + public override void OnPlayEnd() + { + base.OnPlayEnd(); + _blockSceneTreeScroll = true; + } /// /// Enables or disables vertical and horizontal scrolling on the scene tree panel. From d3a50cdacb9349487fde4ba9951bc214f85f5fba Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 20 Jun 2025 09:05:25 +0200 Subject: [PATCH 061/122] Optimize `Actor::DestroyChildren` --- Source/Editor/Modules/SceneEditingModule.cs | 4 ++ Source/Editor/Modules/SceneModule.cs | 45 +++++++++++++++ Source/Editor/SceneGraph/SceneGraphNode.cs | 2 +- Source/Engine/Level/Actor.cpp | 63 ++++++++++++++++++++- Source/Engine/Level/Level.cpp | 8 +++ Source/Engine/Level/Level.h | 6 ++ 6 files changed, 124 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Modules/SceneEditingModule.cs b/Source/Editor/Modules/SceneEditingModule.cs index 3c7130615..11ab2fcb3 100644 --- a/Source/Editor/Modules/SceneEditingModule.cs +++ b/Source/Editor/Modules/SceneEditingModule.cs @@ -711,7 +711,11 @@ namespace FlaxEditor.Modules private void OnActorChildNodesDispose(ActorNode node) { + if (Selection.Count == 0) + return; + // TODO: cache if selection contains any actor child node and skip this loop if no need to iterate + // TODO: or build a hash set with selected nodes for quick O(1) checks (cached until selection changes) // Deselect child nodes for (int i = 0; i < node.ChildNodes.Count; i++) diff --git a/Source/Editor/Modules/SceneModule.cs b/Source/Editor/Modules/SceneModule.cs index 56c420964..9ca92ddce 100644 --- a/Source/Editor/Modules/SceneModule.cs +++ b/Source/Editor/Modules/SceneModule.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using FlaxEditor.SceneGraph; using FlaxEditor.SceneGraph.Actors; using FlaxEngine; @@ -658,6 +659,48 @@ namespace FlaxEditor.Modules //node?.TreeNode.OnActiveChanged(); } + private void OnActorDestroyChildren(Actor actor) + { + // Instead of doing OnActorParentChanged for every child lets remove all of them at once from that actor + ActorNode node = GetActorNode(actor); + if (node != null) + { + if (Editor.SceneEditing.HasSthSelected) + { + // Clear selection if one of the removed actors is selected + var selection = new HashSet(); + foreach (var e in Editor.SceneEditing.Selection) + { + if (e is ActorNode q && q.Actor) + selection.Add(q.Actor); + } + var count = actor.ChildrenCount; + for (int i = 0; i < count; i++) + { + var child = actor.GetChild(i); + if (selection.Contains(child)) + { + Editor.SceneEditing.Deselect(); + break; + } + } + } + + // Remove all child nodes (upfront remove all nodes to run faster) + for (int i = 0; i < node.ChildNodes.Count; i++) + { + if (node.ChildNodes[i] is ActorNode child) + child.parentNode = null; + } + node.TreeNode.DisposeChildren(); + for (int i = 0; i < node.ChildNodes.Count; i++) + { + node.ChildNodes[i].Dispose(); + } + node.ChildNodes.Clear(); + } + } + /// /// Gets the actor node. /// @@ -709,6 +752,7 @@ namespace FlaxEditor.Modules Level.ActorOrderInParentChanged += OnActorOrderInParentChanged; Level.ActorNameChanged += OnActorNameChanged; Level.ActorActiveChanged += OnActorActiveChanged; + Level.ActorDestroyChildren += OnActorDestroyChildren; } /// @@ -726,6 +770,7 @@ namespace FlaxEditor.Modules Level.ActorOrderInParentChanged -= OnActorOrderInParentChanged; Level.ActorNameChanged -= OnActorNameChanged; Level.ActorActiveChanged -= OnActorActiveChanged; + Level.ActorDestroyChildren -= OnActorDestroyChildren; // Cleanup graph Root.Dispose(); diff --git a/Source/Editor/SceneGraph/SceneGraphNode.cs b/Source/Editor/SceneGraph/SceneGraphNode.cs index b6cbdb135..20ac3a6a5 100644 --- a/Source/Editor/SceneGraph/SceneGraphNode.cs +++ b/Source/Editor/SceneGraph/SceneGraphNode.cs @@ -27,7 +27,7 @@ namespace FlaxEditor.SceneGraph /// /// The parent node. /// - protected SceneGraphNode parentNode; + internal SceneGraphNode parentNode; /// /// Gets the children list. diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 02210d910..24ca6b139 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -466,12 +466,71 @@ Array Actor::GetChildren(const MClass* type) const void Actor::DestroyChildren(float timeLeft) { + if (Children.IsEmpty()) + return; PROFILE_CPU(); + + // Actors system doesn't support editing scene hierarchy from multiple threads + if (!IsInMainThread() && IsDuringPlay()) + { + LOG(Error, "Editing scene hierarchy is only allowed on a main thread."); + return; + } + + // Get all actors Array children = Children; + + // Inform Editor beforehand + Level::callActorEvent(Level::ActorEventType::OnActorDestroyChildren, this, nullptr); + + if (_scene && IsActiveInHierarchy()) + { + // Disable children + for (Actor* child : children) + { + if (child->IsActiveInHierarchy()) + { + child->OnDisableInHierarchy(); + } + } + } + + Level::ScenesLock.Lock(); + + // Remove children all at once + Children.Clear(); + _isHierarchyDirty = true; + + // Unlink children from scene hierarchy + for (Actor* child : children) + { + child->_parent = nullptr; + if (!_isActiveInHierarchy) + child->_isActive = false; // Force keep children deactivated to reduce overhead during destruction + if (_scene) + child->SetSceneInHierarchy(nullptr); + } + + Level::ScenesLock.Unlock(); + + // Inform actors about this + for (Actor* child : children) + { + child->OnParentChanged(); + } + + // Unlink children for hierarchy + for (Actor* child : children) + { + //child->EndPlay(); + + //child->SetParent(nullptr, false, false); + } + + // Delete objects const bool useGameTime = timeLeft > ZeroTolerance; for (Actor* child : children) { - child->SetParent(nullptr, false, false); child->DeleteObject(timeLeft, useGameTime); } } @@ -1280,7 +1339,6 @@ void Actor::OnActiveChanged() if (wasActiveInTree != IsActiveInHierarchy()) OnActiveInTreeChanged(); - //if (GetScene()) Level::callActorEvent(Level::ActorEventType::OnActorActiveChanged, this, nullptr); } @@ -1311,7 +1369,6 @@ void Actor::OnActiveInTreeChanged() void Actor::OnOrderInParentChanged() { - //if (GetScene()) Level::callActorEvent(Level::ActorEventType::OnActorOrderInParentChanged, this, nullptr); } diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index 5516e6d53..49bef81c3 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -263,6 +263,9 @@ Delegate Level::ActorParentChanged; Delegate Level::ActorOrderInParentChanged; Delegate Level::ActorNameChanged; Delegate Level::ActorActiveChanged; +#if USE_EDITOR +Delegate Level::ActorDestroyChildren; +#endif Delegate Level::SceneSaving; Delegate Level::SceneSaved; Delegate Level::SceneSaveError; @@ -851,6 +854,11 @@ void Level::callActorEvent(ActorEventType eventType, Actor* a, Actor* b) case ActorEventType::OnActorActiveChanged: ActorActiveChanged(a); break; +#if USE_EDITOR + case ActorEventType::OnActorDestroyChildren: + ActorDestroyChildren(a); + break; +#endif } } diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h index 597bc0a87..484ba35b8 100644 --- a/Source/Engine/Level/Level.h +++ b/Source/Engine/Level/Level.h @@ -549,7 +549,13 @@ private: OnActorOrderInParentChanged = 3, OnActorNameChanged = 4, OnActorActiveChanged = 5, +#if USE_EDITOR + OnActorDestroyChildren = 6, +#endif }; static void callActorEvent(ActorEventType eventType, Actor* a, Actor* b); +#if USE_EDITOR + API_EVENT(Internal) static Delegate ActorDestroyChildren; +#endif }; From d7df403e5e5237f125123fe79b7737c4c63e45e2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 20 Jun 2025 09:05:41 +0200 Subject: [PATCH 062/122] Optimize `ContainerControl.DisposeChildren` --- Source/Engine/UI/GUI/ContainerControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/UI/GUI/ContainerControl.cs b/Source/Engine/UI/GUI/ContainerControl.cs index 32b03af0d..017b8ee5c 100644 --- a/Source/Engine/UI/GUI/ContainerControl.cs +++ b/Source/Engine/UI/GUI/ContainerControl.cs @@ -173,7 +173,7 @@ namespace FlaxEngine.GUI // Delete children while (_children.Count > 0) { - _children[0].Dispose(); + _children[^1].Dispose(); } _isLayoutLocked = wasLayoutLocked; From 9e4c1da0328c98e90792d56265f709e974e0ff51 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 20 Jun 2025 19:42:30 -0500 Subject: [PATCH 063/122] Clear entries and pending entries before play for debug log. --- Source/Editor/Windows/DebugLogWindow.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs index b5d71f6a0..36a7ffa82 100644 --- a/Source/Editor/Windows/DebugLogWindow.cs +++ b/Source/Editor/Windows/DebugLogWindow.cs @@ -735,11 +735,15 @@ namespace FlaxEditor.Windows } /// - public override void OnPlayBegin() + public override void OnPlayBeginning() { // Clear on Play - if (_clearOnPlayButton.Checked) + if (Editor.Options.Options.Interface.DebugLogClearOnPlay) { + lock (_locker) + { + _pendingEntries?.Clear(); + } Clear(); } } From 169db79b2a9358c96114312b5e14c8d84410336d Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 20 Jun 2025 20:12:43 -0500 Subject: [PATCH 064/122] Fix issue with tooltip not updating on scene reload. --- Source/Editor/States/ChangingScenesState.cs | 36 +++++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/Source/Editor/States/ChangingScenesState.cs b/Source/Editor/States/ChangingScenesState.cs index a146b7fa9..72259e832 100644 --- a/Source/Editor/States/ChangingScenesState.cs +++ b/Source/Editor/States/ChangingScenesState.cs @@ -19,6 +19,7 @@ namespace FlaxEditor.States private readonly List _scenesToLoad = new List(); private readonly List _scenesToUnload = new List(); private Guid _lastSceneFromRequest; + private bool _sameSceneReload = false; internal ChangingScenesState(Editor editor) : base(editor) @@ -164,10 +165,22 @@ namespace FlaxEditor.States { Assert.AreEqual(Guid.Empty, _lastSceneFromRequest, "Invalid state."); - // Bind events - Level.SceneLoaded += OnSceneEvent; - Level.SceneLoadError += OnSceneEvent; - Level.SceneUnloaded += OnSceneEvent; + // Bind events, only bind loading event and error if re-loading the same scene to avoid issues. + if (_scenesToUnload.Count == 1 && _scenesToLoad.Count == 1) + { + if (_scenesToLoad[0] == _scenesToUnload[0].ID) + { + Level.SceneLoaded += OnSceneEvent; + Level.SceneLoadError += OnSceneEvent; + _sameSceneReload = true; + } + } + if (!_sameSceneReload) + { + Level.SceneLoaded += OnSceneEvent; + Level.SceneLoadError += OnSceneEvent; + Level.SceneUnloaded += OnSceneEvent; + } // Push scenes changing requests for (int i = 0; i < _scenesToUnload.Count; i++) @@ -210,9 +223,18 @@ namespace FlaxEditor.States } // Unbind events - Level.SceneLoaded -= OnSceneEvent; - Level.SceneLoadError -= OnSceneEvent; - Level.SceneUnloaded -= OnSceneEvent; + if (_sameSceneReload) + { + Level.SceneLoaded -= OnSceneEvent; + Level.SceneLoadError -= OnSceneEvent; + _sameSceneReload = false; + } + else + { + Level.SceneLoaded -= OnSceneEvent; + Level.SceneLoadError -= OnSceneEvent; + Level.SceneUnloaded -= OnSceneEvent; + } } private void OnSceneEvent(Scene scene, Guid sceneId) From ef5d45874a96452fe6ea8350cf0e41f4290da73c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 22 Jun 2025 12:12:42 +0200 Subject: [PATCH 065/122] Fix compilation regression --- Source/Engine/Level/Actor.cpp | 2 ++ Source/Engine/Navigation/NavMesh.cpp | 1 + Source/Engine/Navigation/NavMeshBuilder.cpp | 1 + 3 files changed, 4 insertions(+) diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 24ca6b139..551f6fba9 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -480,8 +480,10 @@ void Actor::DestroyChildren(float timeLeft) // Get all actors Array children = Children; +#if USE_EDITOR // Inform Editor beforehand Level::callActorEvent(Level::ActorEventType::OnActorDestroyChildren, this, nullptr); +#endif if (_scene && IsActiveInHierarchy()) { diff --git a/Source/Engine/Navigation/NavMesh.cpp b/Source/Engine/Navigation/NavMesh.cpp index b48bf26c7..5593d732a 100644 --- a/Source/Engine/Navigation/NavMesh.cpp +++ b/Source/Engine/Navigation/NavMesh.cpp @@ -5,6 +5,7 @@ #include "Engine/Level/Scene/Scene.h" #include "Engine/Serialization/Serialization.h" #include "Engine/Threading/Threading.h" +#include "Engine/Profiler/ProfilerMemory.h" #if COMPILE_WITH_ASSETS_IMPORTER #include "Engine/Core/Log.h" #include "Engine/ContentImporters/AssetsImportingManager.h" diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index e5fdec5da..e92173846 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -23,6 +23,7 @@ #include "Engine/Terrain/TerrainPatch.h" #include "Engine/Terrain/Terrain.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Level/Scene/Scene.h" #include "Engine/Level/Level.h" #include From 867ae2ceaa918121f1e0f64c1973db4f123f9ab8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 23 Jun 2025 09:31:15 +0200 Subject: [PATCH 066/122] Add `Texture Size` node to particles --- Source/Editor/Surface/Archetypes/Textures.cs | 15 ++++++++++++++- .../ParticleEmitterGraph.CPU.Particles.cpp | 7 +++++++ .../GPU/ParticleEmitterGraph.GPU.Textures.cpp | 19 +++++++++++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 1e7dfb56f..56f8154d7 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -459,7 +459,7 @@ namespace FlaxEditor.Surface.Archetypes AlternativeTitles = new string[] { "Lightmap TexCoord" }, Description = "Lightmap UVs", Flags = NodeFlags.MaterialGraph, - Size = new Float2(110, 30), + Size = new Float2(110, 20), Elements = new [] { NodeElementArchetype.Factory.Output(0, "UVs", typeof(Float2), 0) @@ -493,6 +493,19 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Bool(190, Surface.Constants.LayoutOffsetY * 4, 4), } }, + new NodeArchetype + { + TypeID = 24, + Title = "Texture Size", + Description = "Gets the size of the texture (in pixels). If texture is during streaming, then returns size of the highest resident mip.", + Flags = NodeFlags.ParticleEmitterGraph, + Size = new Float2(160, 20), + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "Texture", true, typeof(FlaxEngine.Object), 0), + NodeElementArchetype.Factory.Output(0, "Size", typeof(Float3), 1), + } + }, }; } } diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp index 58058afc1..a47e15275 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp @@ -162,6 +162,13 @@ void ParticleEmitterGraphCPUExecutor::ProcessGroupTextures(Box* box, Node* node, value = Value::Zero; break; } + // Texture Size + case 24: + { + // TODO: support sampling textures in CPU particles + value = Value::Zero; + break; + } default: break; } diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp index 355548e37..820f8da3d 100644 --- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp +++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Textures.cpp @@ -34,8 +34,8 @@ bool ParticleEmitterGPUGenerator::loadTexture(Node* caller, Box* box, const Seri Value location = tryGetValue(locationBox, Value::InitForZero(VariantType::Float2)); // Convert into a proper type - if (isCubemap || isVolume || isArray) - location = Value::Cast(location, VariantType::Float3); + if (isVolume || isArray) + location = Value::Cast(location, VariantType::Float4); else location = Value::Cast(location, VariantType::Float3); @@ -332,6 +332,21 @@ void ParticleEmitterGPUGenerator::ProcessGroupTextures(Box* box, Node* node, Val value = box == gradientBox ? gradient : distance; break; } + // Texture Size + case 24: + { + value = Value::Zero; + auto textureBox = node->GetBox(0); + if (!textureBox->HasConnection()) + break; + const auto texture = eatBox(textureBox->GetParent(), textureBox->FirstConnection()); + const auto textureParam = findParam(texture.Value); + if (!textureParam) + break; + value = writeLocal(VariantType::Float2, node); + _writer.Write(TEXT("\t{0}.GetDimensions({1}.x, {1}.y);\n"), textureParam->ShaderName, value.Value); + break; + } default: break; } From c57a1a720542eee933c29b2ab2d12f949b631303 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 23 Jun 2025 09:31:58 +0200 Subject: [PATCH 067/122] Fix particle emitters to wait for used textures to be loaded and streamed in for proper usage in a graph --- Source/Engine/Particles/ParticleEmitter.cpp | 65 +++++++++++++++++++++ Source/Engine/Particles/ParticleEmitter.h | 3 + 2 files changed, 68 insertions(+) diff --git a/Source/Engine/Particles/ParticleEmitter.cpp b/Source/Engine/Particles/ParticleEmitter.cpp index 6264ce412..558c0172f 100644 --- a/Source/Engine/Particles/ParticleEmitter.cpp +++ b/Source/Engine/Particles/ParticleEmitter.cpp @@ -9,7 +9,9 @@ #include "Engine/Core/Log.h" #include "Engine/Core/Types/DataContainer.h" #include "Engine/Graphics/Shaders/Cache/ShaderCacheManager.h" +#include "Engine/Graphics/Textures/GPUTexture.h" #include "Engine/Level/Level.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Serialization/MemoryReadStream.h" #include "Engine/Serialization/MemoryWriteStream.h" #include "Engine/Threading/Threading.h" @@ -58,6 +60,35 @@ ParticleEffect* ParticleEmitter::Spawn(Actor* parent, const Transform& transform return effect; } +void ParticleEmitter::WaitForAsset(Asset* asset) +{ + // TODO: make a tool for it? + if (auto* texture = Cast(asset)) + { + // Wait for asset to be loaded + if (!texture->WaitForLoaded() && !IsInMainThread() && texture->GetTexture()) + { + PROFILE_CPU_NAMED("WaitForTexture"); + + // Mark as used by rendering and wait for streaming to load it in + double waitTimeSeconds = 10000; + double now = Platform::GetTimeSeconds(); + double end = now + waitTimeSeconds; + const int32 mipLevels = texture->GetMipLevels(); + constexpr float requestedResidency = 0.7f; + const int32 minMipLevels = Math::Max(Math::Min(mipLevels, 7), (int32)(requestedResidency * (float)mipLevels)); + while (texture->GetResidentMipLevels() < minMipLevels && + now < end && + !texture->HasStreamingError()) + { + texture->GetTexture()->LastRenderTime = now; + Platform::Sleep(1); + now = Platform::GetTimeSeconds(); + } + } + } +} + #if COMPILE_WITH_PARTICLE_GPU_GRAPH && COMPILE_WITH_SHADER_COMPILER namespace @@ -294,6 +325,40 @@ Asset::LoadResult ParticleEmitter::load() SimulationMode = ParticlesSimulationMode::CPU; #endif + // Wait for resources used by the emitter to be loaded + // eg. texture used to place particles on spawn needs to be available + bool waitForAsset = false; + for (const auto& node : Graph.Nodes) + { + if ((node.Type == GRAPH_NODE_MAKE_TYPE(5, 1) || node.Type == GRAPH_NODE_MAKE_TYPE(5, 2)) && node.Assets.Count() > 0) + { + const auto texture = node.Assets[0].As(); + if (texture) + { + if (!waitForAsset) + { + waitForAsset = true; + Particles::SystemLocker.End(true); + } + WaitForAsset(texture); + } + } + } + for (const auto& parameter : Graph.Parameters) + { + if (parameter.Type.Type == VariantType::Asset) + { + if (!waitForAsset) + { + waitForAsset = true; + Particles::SystemLocker.End(true); + } + WaitForAsset((Asset*)parameter.Value); + } + } + if (waitForAsset) + Particles::SystemLocker.Begin(true); + return LoadResult::Ok; } diff --git a/Source/Engine/Particles/ParticleEmitter.h b/Source/Engine/Particles/ParticleEmitter.h index 772f5569e..1398de5db 100644 --- a/Source/Engine/Particles/ParticleEmitter.h +++ b/Source/Engine/Particles/ParticleEmitter.h @@ -168,6 +168,9 @@ public: /// The spawned effect. API_FUNCTION() ParticleEffect* Spawn(Actor* parent, const Transform& transform, float duration = MAX_float, bool autoDestroy = false); +private: + void WaitForAsset(Asset* asset); + public: // [BinaryAsset] #if USE_EDITOR From 6c1e380a3e005cb5fd7aec2b3d80e05434673c27 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Mon, 23 Jun 2025 17:24:53 +0200 Subject: [PATCH 068/122] fix search item hover auto focusing item Change what happens when search items get focused to prevent focus being taken away from search box. Also adds a highlight to mouse hovered search item. --- Source/Editor/Windows/Search/SearchItem.cs | 30 +++++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Windows/Search/SearchItem.cs b/Source/Editor/Windows/Search/SearchItem.cs index 11bc79991..5336c2855 100644 --- a/Source/Editor/Windows/Search/SearchItem.cs +++ b/Source/Editor/Windows/Search/SearchItem.cs @@ -74,6 +74,20 @@ namespace FlaxEditor.Windows.Search typeLabel.TextColor = Style.Current.ForegroundGrey; } + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + // Select and focus the item on right click to prevent the search from being cleared + if (button == MouseButton.Right) + { + _finder.SelectedItem = this; + _finder.Hand = true; + Focus(); + return true; + } + return base.OnMouseUp(location, button); + } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { @@ -86,6 +100,15 @@ namespace FlaxEditor.Windows.Search return base.OnMouseUp(location, button); } + /// + public override void Draw() + { + if (IsMouseOver) + Render2D.FillRectangle(new Rectangle(Float2.Zero, Size), Style.Current.BackgroundHighlighted); + + base.Draw(); + } + /// public override void OnMouseEnter(Float2 location) { @@ -93,12 +116,7 @@ namespace FlaxEditor.Windows.Search var root = RootWindow; if (root != null) - { root.Cursor = CursorType.Hand; - } - - _finder.SelectedItem = this; - _finder.Hand = true; } /// @@ -176,9 +194,7 @@ namespace FlaxEditor.Windows.Search { string importLocation = System.IO.Path.GetDirectoryName(importPath); if (!string.IsNullOrEmpty(importLocation) && System.IO.Directory.Exists(importLocation)) - { cm.AddButton("Show import location", () => FileSystem.ShowFileExplorer(importLocation)); - } } } cm.AddSeparator(); From 109d4423bb66c60ebe5e71fe04d39e26cf120e68 Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Mon, 23 Jun 2025 20:14:18 +0200 Subject: [PATCH 069/122] add accent color strip to search item icons --- Source/Editor/Windows/Search/SearchItem.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/Search/SearchItem.cs b/Source/Editor/Windows/Search/SearchItem.cs index 5336c2855..4d610159e 100644 --- a/Source/Editor/Windows/Search/SearchItem.cs +++ b/Source/Editor/Windows/Search/SearchItem.cs @@ -20,6 +20,11 @@ namespace FlaxEditor.Windows.Search /// protected Image _icon; + /// + /// The color of the accent strip. + /// + protected Color _accentColor; + /// /// The item name. /// @@ -56,7 +61,7 @@ namespace FlaxEditor.Windows.Search var icon = new Image { Size = new Float2(logoSize), - Location = new Float2(5, (height - logoSize) / 2) + Location = new Float2(7, (height - logoSize) / 2) }; _icon = icon; @@ -146,6 +151,7 @@ namespace FlaxEditor.Windows.Search { _asset = item; _asset.AddReference(this); + _accentColor = Editor.Instance.ContentDatabase.GetProxy(item).AccentColor; } /// @@ -228,6 +234,10 @@ namespace FlaxEditor.Windows.Search // Draw icon var iconRect = _icon.Bounds; _asset.DrawThumbnail(ref iconRect); + + // Draw icon color strip + var rect = iconRect with { Width = 2, Height = Height, Location = Float2.Zero }; + Render2D.FillRectangle(rect, _accentColor); } /// From 5a05038a9b6fd0a5e1227d21185809633ffda750 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Jun 2025 13:08:25 +0200 Subject: [PATCH 070/122] Add new `UIBrush` that uses `UIBrushAsset` json resource with a brush data --- .../CustomEditors/Editors/IBrushEditor.cs | 1 + Source/Engine/UI/GUI/Brushes/UIBrush.cs | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 Source/Engine/UI/GUI/Brushes/UIBrush.cs diff --git a/Source/Editor/CustomEditors/Editors/IBrushEditor.cs b/Source/Editor/CustomEditors/Editors/IBrushEditor.cs index b32f58923..ce5207414 100644 --- a/Source/Editor/CustomEditors/Editors/IBrushEditor.cs +++ b/Source/Editor/CustomEditors/Editors/IBrushEditor.cs @@ -26,6 +26,7 @@ namespace FlaxEditor.CustomEditors.Editors new OptionType("Texture 9-Slicing", typeof(Texture9SlicingBrush)), new OptionType("Sprite 9-Slicing", typeof(Sprite9SlicingBrush)), new OptionType("Video", typeof(VideoBrush)), + new OptionType("UI Brush", typeof(UIBrush)), }; } } diff --git a/Source/Engine/UI/GUI/Brushes/UIBrush.cs b/Source/Engine/UI/GUI/Brushes/UIBrush.cs new file mode 100644 index 000000000..4441899c0 --- /dev/null +++ b/Source/Engine/UI/GUI/Brushes/UIBrush.cs @@ -0,0 +1,75 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +namespace FlaxEngine.GUI +{ + /// + /// Asset with that can be reused in different UI controls. + /// + /// + /// + public class UIBrushAsset + { + /// + /// Brush object. + /// + public IBrush Brush; + } + + /// + /// Implementation of for frame displaying. + /// + /// + /// + public sealed class UIBrush : IBrush + { + /// + /// The UI Brush asset to use. + /// + public JsonAssetReference Asset; + + /// + /// Initializes a new instance of the class. + /// + public UIBrush() + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The UI Brush asset to use. + public UIBrush(JsonAssetReference asset) + { + Asset = asset; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The UI Brush asset to use. + public UIBrush(JsonAsset asset) + { + Asset = asset; + } + + /// + public Float2 Size + { + get + { + var asset = (UIBrushAsset)Asset.Asset?.Instance; + if (asset != null && asset.Brush != null) + return asset.Brush.Size; + return Float2.Zero; + } + } + + /// + public void Draw(Rectangle rect, Color color) + { + var asset = (UIBrushAsset)Asset.Asset?.Instance; + if (asset != null && asset.Brush != null) + asset.Brush.Draw(rect, color); + } + } +} From 8d62a13cbc68040dfa5b84f44f32ab64ab968dd0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Jun 2025 18:32:20 +0200 Subject: [PATCH 071/122] Add `LayoutElementsContainer.Image` with `IBrush` --- .../CustomEditors/LayoutElementsContainer.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Source/Editor/CustomEditors/LayoutElementsContainer.cs b/Source/Editor/CustomEditors/LayoutElementsContainer.cs index 68ac9f47a..322a08d64 100644 --- a/Source/Editor/CustomEditors/LayoutElementsContainer.cs +++ b/Source/Editor/CustomEditors/LayoutElementsContainer.cs @@ -287,10 +287,7 @@ namespace FlaxEditor.CustomEditors /// The created element. public ImageElement Image(SpriteHandle sprite) { - var element = new ImageElement(); - element.Image.Brush = new SpriteBrush(sprite); - OnAddElement(element); - return element; + return Image(new SpriteBrush(sprite)); } /// @@ -300,10 +297,7 @@ namespace FlaxEditor.CustomEditors /// The created element. public ImageElement Image(Texture texture) { - var element = new ImageElement(); - element.Image.Brush = new TextureBrush(texture); - OnAddElement(element); - return element; + return Image(new TextureBrush(texture)); } /// @@ -312,9 +306,19 @@ namespace FlaxEditor.CustomEditors /// The GPU texture. /// The created element. public ImageElement Image(GPUTexture texture) + { + return Image(new GPUTextureBrush(texture)); + } + + /// + /// Adds brush image to the layout. + /// + /// The brush. + /// The created element. + public ImageElement Image(IBrush brush) { var element = new ImageElement(); - element.Image.Brush = new GPUTextureBrush(texture); + element.Image.Brush = brush; OnAddElement(element); return element; } From dbd829761265dd718b951324b67bb7efead2ea0e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Jun 2025 19:25:18 +0200 Subject: [PATCH 072/122] Add `SortOrder` to Environment Probe --- Source/Engine/Level/Actors/EnvironmentProbe.cpp | 3 +++ Source/Engine/Level/Actors/EnvironmentProbe.h | 6 ++++++ Source/Engine/Renderer/ReflectionsPass.cpp | 13 +++++++++---- Source/Engine/Renderer/RenderList.h | 1 + 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Level/Actors/EnvironmentProbe.cpp b/Source/Engine/Level/Actors/EnvironmentProbe.cpp index 3c49611d4..71b403eb9 100644 --- a/Source/Engine/Level/Actors/EnvironmentProbe.cpp +++ b/Source/Engine/Level/Actors/EnvironmentProbe.cpp @@ -197,6 +197,7 @@ void EnvironmentProbe::Draw(RenderContext& renderContext) data.Position = position; data.Radius = radius; data.Brightness = Brightness; + data.SortOrder = SortOrder; data.HashID = GetHash(_id); renderContext.List->EnvironmentProbes.Add(data); } @@ -234,6 +235,7 @@ void EnvironmentProbe::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE_MEMBER(Radius, _radius); SERIALIZE(CubemapResolution); SERIALIZE(Brightness); + SERIALIZE(SortOrder); SERIALIZE(UpdateMode); SERIALIZE(CaptureNearPlane); SERIALIZE_MEMBER(IsCustomProbe, _isUsingCustomProbe); @@ -248,6 +250,7 @@ void EnvironmentProbe::Deserialize(DeserializeStream& stream, ISerializeModifier DESERIALIZE_MEMBER(Radius, _radius); DESERIALIZE(CubemapResolution); DESERIALIZE(Brightness); + DESERIALIZE(SortOrder); DESERIALIZE(UpdateMode); DESERIALIZE(CaptureNearPlane); DESERIALIZE_MEMBER(IsCustomProbe, _isUsingCustomProbe); diff --git a/Source/Engine/Level/Actors/EnvironmentProbe.h b/Source/Engine/Level/Actors/EnvironmentProbe.h index 7131dc0ba..09f771437 100644 --- a/Source/Engine/Level/Actors/EnvironmentProbe.h +++ b/Source/Engine/Level/Actors/EnvironmentProbe.h @@ -51,6 +51,12 @@ public: API_FIELD(Attributes="EditorOrder(10), Limit(0, 1000, 0.01f), EditorDisplay(\"Probe\")") float Brightness = 1.0f; + /// + /// The probe rendering order. The higher values are render later (on top). + /// + API_FIELD(Attributes = "EditorOrder(25), EditorDisplay(\"Probe\")") + int32 SortOrder = 0; + /// /// The probe update mode. /// diff --git a/Source/Engine/Renderer/ReflectionsPass.cpp b/Source/Engine/Renderer/ReflectionsPass.cpp index 3aa0ed6ee..631543010 100644 --- a/Source/Engine/Renderer/ReflectionsPass.cpp +++ b/Source/Engine/Renderer/ReflectionsPass.cpp @@ -335,12 +335,17 @@ void ReflectionsPass::Dispose() bool SortProbes(RenderEnvironmentProbeData const& p1, RenderEnvironmentProbeData const& p2) { - // Compare by radius - int32 res = static_cast(p2.Radius - p1.Radius); + // Compare by Sort Order + int32 res = p1.SortOrder - p2.SortOrder; if (res == 0) { - // Compare by ID to prevent flickering - res = p2.HashID - p1.HashID; + // Compare by radius + res = static_cast(p2.Radius - p1.Radius); + if (res == 0) + { + // Compare by ID to prevent flickering + res = p2.HashID - p1.HashID; + } } return res < 0; } diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h index 469bc2cdd..d5288e6ee 100644 --- a/Source/Engine/Renderer/RenderList.h +++ b/Source/Engine/Renderer/RenderList.h @@ -155,6 +155,7 @@ struct RenderEnvironmentProbeData Float3 Position; float Radius; float Brightness; + int32 SortOrder; uint32 HashID; void SetShaderData(ShaderEnvProbeData& data) const; From bf10d0949e4aa937eb518570bc6ed36557173f8b Mon Sep 17 00:00:00 2001 From: xxSeys1 Date: Thu, 26 Jun 2025 12:47:31 +0200 Subject: [PATCH 073/122] add a bit of spacing to the left edge of the popup to reduce colors bleeding into the background --- Source/Editor/Windows/Search/SearchItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Windows/Search/SearchItem.cs b/Source/Editor/Windows/Search/SearchItem.cs index 4d610159e..63263cc58 100644 --- a/Source/Editor/Windows/Search/SearchItem.cs +++ b/Source/Editor/Windows/Search/SearchItem.cs @@ -235,8 +235,8 @@ namespace FlaxEditor.Windows.Search var iconRect = _icon.Bounds; _asset.DrawThumbnail(ref iconRect); - // Draw icon color strip - var rect = iconRect with { Width = 2, Height = Height, Location = Float2.Zero }; + // Draw color strip + var rect = iconRect with { Width = 2, Height = Height, Location = new Float2(2, 0) }; Render2D.FillRectangle(rect, _accentColor); } From 674fda7375f211655ea81b6644fea082f85ed3c9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Jun 2025 19:50:04 +0200 Subject: [PATCH 074/122] Add resizing to Custom Code nodes in Materials --- Source/Editor/Surface/Archetypes/Material.cs | 150 ++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index e46b1c6fb..bd084e286 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -1,11 +1,13 @@ // Copyright (c) Wojciech Figat. All rights reserved. using System; +using System.Linq; using FlaxEditor.Content.Settings; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEditor.Windows.Assets; using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.Surface.Archetypes { @@ -260,6 +262,148 @@ namespace FlaxEditor.Surface.Archetypes } } + internal sealed class CustomCodeNode : SurfaceNode + { + private Rectangle _resizeButtonRect; + private Float2 _startResizingSize; + private Float2 _startResizingCornerOffset; + private bool _isResizing; + + 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) + { + } + + 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); + + var textBox = (TextBox)Children.First(x => x is TextBox); + textBox.AnchorMax = Float2.One; + + 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); + } + + 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 { /// @@ -410,13 +554,15 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 8, + Create = (id, context, arch, groupArch) => new CustomCodeNode(id, context, arch, groupArch), Title = "Custom Code", Description = "Custom HLSL shader code expression", Flags = NodeFlags.MaterialGraph, Size = new Float2(300, 200), DefaultValues = new object[] { - "// Here you can add HLSL code\nOutput0 = Input0;" + "// Here you can add HLSL code\nOutput0 = Input0;", + new Float2(300, 200), }, Elements = new[] { @@ -874,6 +1020,7 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 38, + Create = (id, context, arch, groupArch) => new CustomCodeNode(id, context, arch, groupArch), Title = "Custom Global Code", Description = "Custom global HLSL shader code expression (placed before material shader code). Can contain includes to shader utilities or declare functions to reuse later.", Flags = NodeFlags.MaterialGraph, @@ -883,6 +1030,7 @@ namespace FlaxEditor.Surface.Archetypes "// Here you can add HLSL code\nfloat4 GetCustomColor()\n{\n\treturn float4(1, 0, 0, 1);\n}", true, (int)MaterialTemplateInputsMapping.Utilities, + new Float2(300, 240), }, Elements = new[] { From 5c37584eca692cb113896c4d3cd6a7d994978226 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Jun 2025 19:50:42 +0200 Subject: [PATCH 075/122] Minor adjustment for alignment of perf-critical variables in rendering --- Source/Engine/GraphicsDevice/DirectX/DX11/GPUContextDX11.cpp | 2 +- Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUContextDX11.cpp b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUContextDX11.cpp index 77cec978d..22780e3ec 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUContextDX11.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUContextDX11.cpp @@ -267,7 +267,7 @@ void GPUContextDX11::SetRenderTarget(GPUTextureView* depthBuffer, const Span(depthBuffer); ID3D11DepthStencilView* dsv = depthBufferDX11 ? depthBufferDX11->DSV() : nullptr; - ID3D11RenderTargetView* rtvs[GPU_MAX_RT_BINDED]; + __declspec(align(16)) ID3D11RenderTargetView* rtvs[GPU_MAX_RT_BINDED]; for (int32 i = 0; i < rts.Length(); i++) { auto rtDX11 = reinterpret_cast(rts[i]); diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp index 5f278a8ae..9cb285ac1 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp @@ -977,7 +977,7 @@ void GPUContextDX12::BindVB(const Span& vertexBuffers, const uint32* { ASSERT(vertexBuffers.Length() >= 0 && vertexBuffers.Length() <= GPU_MAX_VB_BINDED); bool vbEdited = _vbCount != vertexBuffers.Length(); - D3D12_VERTEX_BUFFER_VIEW views[GPU_MAX_VB_BINDED]; + __declspec(align(16)) D3D12_VERTEX_BUFFER_VIEW views[GPU_MAX_VB_BINDED]; for (int32 i = 0; i < vertexBuffers.Length(); i++) { const auto vbDX12 = static_cast(vertexBuffers[i]); From 45e82d21f4e15234ce8b7cf39d005fa929f535c8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Jun 2025 19:51:06 +0200 Subject: [PATCH 076/122] Fix `ConcurrentSystemLocker` to guard for a single writer at once --- Source/Engine/Threading/ConcurrentSystemLocker.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Source/Engine/Threading/ConcurrentSystemLocker.cpp b/Source/Engine/Threading/ConcurrentSystemLocker.cpp index f8eab96d9..c8debb561 100644 --- a/Source/Engine/Threading/ConcurrentSystemLocker.cpp +++ b/Source/Engine/Threading/ConcurrentSystemLocker.cpp @@ -22,6 +22,14 @@ RETRY: goto RETRY; } + // Writers have to check themselves to (one write at the same time - just like a mutex) + if (write && Platform::AtomicRead(thisCounter) != 0) + { + // Someone else is doing opposite operation so wait for it's end + Platform::Sleep(0); + goto RETRY; + } + // Mark that we entered this section Platform::InterlockedIncrement(thisCounter); From 1b40775d628202e31dff559a8917e1e44b1f4332 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Jun 2025 11:56:09 +0200 Subject: [PATCH 077/122] Fix deadloop in `HtmlParser` when parsing text with incorrect tags --- Source/Engine/Utilities/HtmlParser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Utilities/HtmlParser.cs b/Source/Engine/Utilities/HtmlParser.cs index 37b614b80..175b0247a 100644 --- a/Source/Engine/Utilities/HtmlParser.cs +++ b/Source/Engine/Utilities/HtmlParser.cs @@ -177,6 +177,8 @@ namespace FlaxEngine.Utilities // Get name of this tag int start = _pos; string s = ParseTagName(); + if (s == string.Empty) + return false; // Special handling bool doctype = _scriptBegin = false; From 8cdec15fa64c365260660039373080762dbefe0d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Jun 2025 15:41:48 +0200 Subject: [PATCH 078/122] Fix `GlobalSignDistanceFieldCustomBuffer` to be thread-safe (scene rendering events are not guarded via mutex anymore) --- Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp index a46568f97..56dd196a5 100644 --- a/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp +++ b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp @@ -197,6 +197,7 @@ public: GPUTexture* Texture = nullptr; GPUTexture* TextureMip = nullptr; Vector3 Origin = Vector3::Zero; + ConcurrentSystemLocker Locker; Array> Cascades; HashSet ObjectTypes; HashSet SDFTextures; @@ -395,6 +396,7 @@ public: { if (GLOBAL_SDF_ACTOR_IS_STATIC(a) && ObjectTypes.Contains(a->GetTypeHandle())) { + ConcurrentSystemLocker::WriteScope lock(Locker); OnSceneRenderingDirty(a->GetBox()); } } @@ -403,6 +405,7 @@ public: { if (GLOBAL_SDF_ACTOR_IS_STATIC(a) && ObjectTypes.Contains(a->GetTypeHandle())) { + ConcurrentSystemLocker::WriteScope lock(Locker); OnSceneRenderingDirty(BoundingBox::FromSphere(prevBounds)); OnSceneRenderingDirty(a->GetBox()); } @@ -412,6 +415,7 @@ public: { if (GLOBAL_SDF_ACTOR_IS_STATIC(a) && ObjectTypes.Contains(a->GetTypeHandle())) { + ConcurrentSystemLocker::WriteScope lock(Locker); OnSceneRenderingDirty(a->GetBox()); } } From 185151b0250bb708c3d67e183b05ee9a542e2c82 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Jun 2025 18:52:25 +0200 Subject: [PATCH 079/122] Minor fixes --- Source/Editor/Surface/Archetypes/Material.cs | 71 ++++++++++++++++++-- Source/Engine/Core/ObjectsRemovalService.cpp | 2 +- Source/Engine/UI/GUI/Common/RichTextBox.cs | 2 +- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index bd084e286..c9066eaa5 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -1,7 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. using System; -using System.Linq; using FlaxEditor.Content.Settings; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; @@ -262,12 +261,55 @@ namespace FlaxEditor.Surface.Archetypes } } +#if false // TODO: finish code editor based on RichTextBoxBase with text block parsing for custom styling + internal sealed class CustomCodeTextBox : RichTextBoxBase + { + protected override void OnParseTextBlocks() + { + base.OnParseTextBlocks(); + + // Single block for a whole text + // TODO: implement code parsing with HLSL syntax + var font = Style.Current.FontMedium; + var style = new TextBlockStyle + { + Font = new FontReference(font), + Color = Style.Current.Foreground, + BackgroundSelectedBrush = new SolidColorBrush(Style.Current.BackgroundSelected), + }; + _textBlocks.Clear(); + _textBlocks.Add(new TextBlock + { + Range = new TextRange + { + StartIndex = 0, + EndIndex = TextLength, + }, + Style = style, + Bounds = new Rectangle(Float2.Zero, font.MeasureText(Text)), + }); + } +#else + internal sealed class CustomCodeTextBox : TextBox + { +#endif + public override void Draw() + { + base.Draw(); + + // Draw border + if (!IsFocused) + Render2D.DrawRectangle(new Rectangle(Float2.Zero, Size), Style.Current.BorderNormal); + } + } + internal sealed class CustomCodeNode : SurfaceNode { 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 @@ -280,6 +322,26 @@ namespace FlaxEditor.Surface.Archetypes public CustomCodeNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { + Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize), size; + if (nodeArch.TypeID == 8) + { + pos += new Float2(60, 0); + size = new Float2(172, 200); + } + else + { + pos += new Float2(0, 40); + size = new Float2(300, 200); + } + _textBox = new CustomCodeTextBox + { + IsMultiline = true, + Location = pos, + Size = size, + Parent = this, + AnchorMax = Float2.One, + }; + _textBox.EditEnd += () => SetValue(0, _textBox.Text); } public override bool CanSelect(ref Float2 location) @@ -291,8 +353,7 @@ namespace FlaxEditor.Surface.Archetypes { base.OnSurfaceLoaded(action); - var textBox = (TextBox)Children.First(x => x is TextBox); - textBox.AnchorMax = Float2.One; + _textBox.Text = (string)Values[0]; var size = SizeValue; if (Surface != null && Surface.GridSnappingEnabled) @@ -306,6 +367,7 @@ namespace FlaxEditor.Surface.Archetypes var size = SizeValue; Resize(size.X, size.Y); + _textBox.Text = (string)Values[0]; } protected override void UpdateRectangles() @@ -579,8 +641,6 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Output(1, "Output1", typeof(Float4), 9), NodeElementArchetype.Factory.Output(2, "Output2", typeof(Float4), 10), NodeElementArchetype.Factory.Output(3, "Output3", typeof(Float4), 11), - - NodeElementArchetype.Factory.TextBox(60, 0, 175, 200, 0), } }, new NodeArchetype @@ -1038,7 +1098,6 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Text(20, 0, "Enabled"), NodeElementArchetype.Factory.Text(0, 20, "Location"), NodeElementArchetype.Factory.Enum(50, 20, 120, 2, typeof(MaterialTemplateInputsMapping)), - NodeElementArchetype.Factory.TextBox(0, 40, 300, 200, 0), } }, new NodeArchetype diff --git a/Source/Engine/Core/ObjectsRemovalService.cpp b/Source/Engine/Core/ObjectsRemovalService.cpp index 4d9159ea9..6d40c3f78 100644 --- a/Source/Engine/Core/ObjectsRemovalService.cpp +++ b/Source/Engine/Core/ObjectsRemovalService.cpp @@ -156,7 +156,7 @@ Object::~Object() { #if BUILD_DEBUG // Prevent removing object that is still reverenced by the removal service - ASSERT(!ObjectsRemovalService::IsInPool(this)); + //ASSERT(!ObjectsRemovalService::IsInPool(this)); #endif } diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.cs b/Source/Engine/UI/GUI/Common/RichTextBox.cs index b417854d7..f7726bf56 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.cs @@ -57,7 +57,7 @@ namespace FlaxEngine.GUI { base.OnSizeChanged(); - // Refresh textblocks since thos emight depend on control size (eg. align right) + // Refresh textblocks since those might depend on control size (eg. align right) UpdateTextBlocks(); } } From 3dc7546dd4907b69a206c20c07166295c659a35e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Jun 2025 19:06:25 +0200 Subject: [PATCH 080/122] Fix crash when constant buffer is unused by shader but still exists --- Source/Engine/Renderer/AmbientOcclusionPass.cpp | 10 +--------- Source/Engine/Renderer/AntiAliasing/FXAA.cpp | 6 +----- Source/Engine/Renderer/AntiAliasing/SMAA.cpp | 8 +------- Source/Engine/Renderer/AntiAliasing/TAA.cpp | 6 +----- Source/Engine/Renderer/AtmospherePreCompute.cpp | 6 +----- Source/Engine/Renderer/ColorGradingPass.cpp | 8 +------- .../Renderer/ContrastAdaptiveSharpeningPass.cpp | 8 +------- Source/Engine/Renderer/DepthOfFieldPass.cpp | 8 +------- Source/Engine/Renderer/EyeAdaptationPass.cpp | 8 +------- Source/Engine/Renderer/HistogramPass.cpp | 8 +------- Source/Engine/Renderer/LightPass.cpp | 14 ++------------ Source/Engine/Renderer/MotionBlurPass.cpp | 10 +--------- Source/Engine/Renderer/PostProcessingPass.cpp | 14 ++------------ Source/Engine/Renderer/ProbesRenderer.cpp | 6 +----- Source/Engine/Renderer/ReflectionsPass.cpp | 8 +------- Source/Engine/Renderer/RendererPass.h | 1 + .../Engine/Renderer/ScreenSpaceReflectionsPass.cpp | 8 +------- Source/Engine/Renderer/ShadowsPass.cpp | 8 +------- Source/Engine/Renderer/Utils/BitonicSort.cpp | 8 +------- Source/Engine/Renderer/Utils/MultiScaler.cpp | 8 +------- Source/Engine/Renderer/VolumetricFogPass.cpp | 14 ++------------ 21 files changed, 24 insertions(+), 151 deletions(-) diff --git a/Source/Engine/Renderer/AmbientOcclusionPass.cpp b/Source/Engine/Renderer/AmbientOcclusionPass.cpp index 5f181ab97..08a8cefcb 100644 --- a/Source/Engine/Renderer/AmbientOcclusionPass.cpp +++ b/Source/Engine/Renderer/AmbientOcclusionPass.cpp @@ -91,17 +91,9 @@ bool AmbientOcclusionPass::setupResources() { // Check shader if (!_shader->IsLoaded()) - { return true; - } const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(ASSAOConstants)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, ASSAOConstants); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, ASSAOConstants); // Create pipeline states GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/AntiAliasing/FXAA.cpp b/Source/Engine/Renderer/AntiAliasing/FXAA.cpp index 41926e2d6..00dfb0cdd 100644 --- a/Source/Engine/Renderer/AntiAliasing/FXAA.cpp +++ b/Source/Engine/Renderer/AntiAliasing/FXAA.cpp @@ -36,11 +36,7 @@ bool FXAA::setupResources() return true; } const auto shader = _shader->GetShader(); - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); GPUPipelineState::Description psDesc; if (!_psFXAA.IsValid()) diff --git a/Source/Engine/Renderer/AntiAliasing/SMAA.cpp b/Source/Engine/Renderer/AntiAliasing/SMAA.cpp index 25007ad7d..2414118d3 100644 --- a/Source/Engine/Renderer/AntiAliasing/SMAA.cpp +++ b/Source/Engine/Renderer/AntiAliasing/SMAA.cpp @@ -45,13 +45,7 @@ bool SMAA::setupResources() return true; } const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline state GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/AntiAliasing/TAA.cpp b/Source/Engine/Renderer/AntiAliasing/TAA.cpp index 8bbb6ba81..b772eb45e 100644 --- a/Source/Engine/Renderer/AntiAliasing/TAA.cpp +++ b/Source/Engine/Renderer/AntiAliasing/TAA.cpp @@ -37,11 +37,7 @@ bool TAA::setupResources() if (!_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); if (!_psTAA) _psTAA = GPUDevice::Instance->CreatePipelineState(); GPUPipelineState::Description psDesc; diff --git a/Source/Engine/Renderer/AtmospherePreCompute.cpp b/Source/Engine/Renderer/AtmospherePreCompute.cpp index 595ebcca5..b796e828b 100644 --- a/Source/Engine/Renderer/AtmospherePreCompute.cpp +++ b/Source/Engine/Renderer/AtmospherePreCompute.cpp @@ -166,11 +166,7 @@ bool init() } auto shader = _shader->GetShader(); ASSERT(shader->GetCB(0) != nullptr); - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stages _psTransmittance = GPUDevice::Instance->CreatePipelineState(); diff --git a/Source/Engine/Renderer/ColorGradingPass.cpp b/Source/Engine/Renderer/ColorGradingPass.cpp index 43b49f091..322e7d591 100644 --- a/Source/Engine/Renderer/ColorGradingPass.cpp +++ b/Source/Engine/Renderer/ColorGradingPass.cpp @@ -89,13 +89,7 @@ bool ColorGradingPass::setupResources() if (!_shader || !_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stages GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/ContrastAdaptiveSharpeningPass.cpp b/Source/Engine/Renderer/ContrastAdaptiveSharpeningPass.cpp index 6a6fce521..3231c32f8 100644 --- a/Source/Engine/Renderer/ContrastAdaptiveSharpeningPass.cpp +++ b/Source/Engine/Renderer/ContrastAdaptiveSharpeningPass.cpp @@ -48,13 +48,7 @@ bool ContrastAdaptiveSharpeningPass::setupResources() if (!_shader || !_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stage auto psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/DepthOfFieldPass.cpp b/Source/Engine/Renderer/DepthOfFieldPass.cpp index 25d9ea94f..2c3dc36f8 100644 --- a/Source/Engine/Renderer/DepthOfFieldPass.cpp +++ b/Source/Engine/Renderer/DepthOfFieldPass.cpp @@ -117,13 +117,7 @@ bool DepthOfFieldPass::setupResources() if (!_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stages GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/EyeAdaptationPass.cpp b/Source/Engine/Renderer/EyeAdaptationPass.cpp index 84e826e36..3cf44af1b 100644 --- a/Source/Engine/Renderer/EyeAdaptationPass.cpp +++ b/Source/Engine/Renderer/EyeAdaptationPass.cpp @@ -258,13 +258,7 @@ bool EyeAdaptationPass::setupResources() if (!_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(EyeAdaptationData)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, EyeAdaptationData); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, EyeAdaptationData); // Create pipeline stages GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/HistogramPass.cpp b/Source/Engine/Renderer/HistogramPass.cpp index 96f0cca73..9b6e71a77 100644 --- a/Source/Engine/Renderer/HistogramPass.cpp +++ b/Source/Engine/Renderer/HistogramPass.cpp @@ -113,13 +113,7 @@ bool HistogramPass::setupResources() if (!_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(HistogramData)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, HistogramData); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, HistogramData); _csClearHistogram = shader->GetCS("CS_ClearHistogram"); _csGenerateHistogram = shader->GetCS("CS_GenerateHistogram"); diff --git a/Source/Engine/Renderer/LightPass.cpp b/Source/Engine/Renderer/LightPass.cpp index 1371a53ee..ee38ee2ac 100644 --- a/Source/Engine/Renderer/LightPass.cpp +++ b/Source/Engine/Renderer/LightPass.cpp @@ -65,18 +65,8 @@ bool LightPass::setupResources() if (!_sphereModel->CanBeRendered() || !_shader->IsLoaded()) return true; auto shader = _shader->GetShader(); - - // Validate shader constant buffers sizes - if (shader->GetCB(0)->GetSize() != sizeof(PerLight)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, PerLight); - return true; - } - if (shader->GetCB(1)->GetSize() != sizeof(PerFrame)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 1, PerFrame); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, PerLight); + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 1, PerFrame); // Create pipeline stages GPUPipelineState::Description psDesc; diff --git a/Source/Engine/Renderer/MotionBlurPass.cpp b/Source/Engine/Renderer/MotionBlurPass.cpp index d8ce05de1..3077e4cdb 100644 --- a/Source/Engine/Renderer/MotionBlurPass.cpp +++ b/Source/Engine/Renderer/MotionBlurPass.cpp @@ -80,17 +80,9 @@ bool MotionBlurPass::setupResources() { // Check shader if (!_shader->IsLoaded()) - { return true; - } const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline state GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/PostProcessingPass.cpp b/Source/Engine/Renderer/PostProcessingPass.cpp index 9192c8ca4..bc7a1b820 100644 --- a/Source/Engine/Renderer/PostProcessingPass.cpp +++ b/Source/Engine/Renderer/PostProcessingPass.cpp @@ -98,18 +98,8 @@ bool PostProcessingPass::setupResources() if (!_shader->IsLoaded()) return true; auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } - if (shader->GetCB(1)->GetSize() != sizeof(GaussianBlurData)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 1, GaussianBlurData); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 1, GaussianBlurData); // Create pipeline stages GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/ProbesRenderer.cpp b/Source/Engine/Renderer/ProbesRenderer.cpp index 70705b8a8..ee72afe72 100644 --- a/Source/Engine/Renderer/ProbesRenderer.cpp +++ b/Source/Engine/Renderer/ProbesRenderer.cpp @@ -231,11 +231,7 @@ bool ProbesRenderer::Init() if (!_shader->IsLoaded()) return false; const auto shader = _shader->GetShader(); - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stages _psFilterFace = GPUDevice::Instance->CreatePipelineState(); diff --git a/Source/Engine/Renderer/ReflectionsPass.cpp b/Source/Engine/Renderer/ReflectionsPass.cpp index 631543010..5aa8404ab 100644 --- a/Source/Engine/Renderer/ReflectionsPass.cpp +++ b/Source/Engine/Renderer/ReflectionsPass.cpp @@ -281,13 +281,7 @@ bool ReflectionsPass::setupResources() if (!_sphereModel->CanBeRendered() || !_preIntegratedGF->IsLoaded() || !_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stages GPUPipelineState::Description psDesc; diff --git a/Source/Engine/Renderer/RendererPass.h b/Source/Engine/Renderer/RendererPass.h index 25d887883..32d3b86b9 100644 --- a/Source/Engine/Renderer/RendererPass.h +++ b/Source/Engine/Renderer/RendererPass.h @@ -113,3 +113,4 @@ class RendererPass : public Singleton, public RendererPassBase }; #define REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, index, dataType) LOG(Fatal, "Shader {0} has incorrect constant buffer {1} size: {2} bytes. Expected: {3} bytes", shader->ToString(), index, shader->GetCB(index)->GetSize(), sizeof(dataType)); +#define CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, index, dataType) if (shader->GetCB(index)->GetSize() != sizeof(dataType) && shader->GetCB(index)->GetSize() != 0) { REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, index, dataType); return true; } diff --git a/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp b/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp index 55c6d79f2..454540eec 100644 --- a/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp +++ b/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp @@ -89,13 +89,7 @@ bool ScreenSpaceReflectionsPass::setupResources() if (!_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stages GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index 3937f8554..5cf90876a 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -507,13 +507,7 @@ bool ShadowsPass::setupResources() if (!_sphereModel->CanBeRendered() || !_shader->IsLoaded()) return true; auto shader = _shader->GetShader(); - - // Validate shader constant buffers sizes - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stages GPUPipelineState::Description psDesc; diff --git a/Source/Engine/Renderer/Utils/BitonicSort.cpp b/Source/Engine/Renderer/Utils/BitonicSort.cpp index 73a310832..babc058e2 100644 --- a/Source/Engine/Renderer/Utils/BitonicSort.cpp +++ b/Source/Engine/Renderer/Utils/BitonicSort.cpp @@ -59,14 +59,8 @@ bool BitonicSort::setupResources() if (!_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size _cb = shader->GetCB(0); - if (_cb->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Cache compute shaders _indirectArgsCS = shader->GetCS("CS_IndirectArgs"); diff --git a/Source/Engine/Renderer/Utils/MultiScaler.cpp b/Source/Engine/Renderer/Utils/MultiScaler.cpp index 3f812ea77..ae5633834 100644 --- a/Source/Engine/Renderer/Utils/MultiScaler.cpp +++ b/Source/Engine/Renderer/Utils/MultiScaler.cpp @@ -41,13 +41,7 @@ bool MultiScaler::setupResources() if (!_shader->IsLoaded()) return true; const auto shader = _shader->GetShader(); - - // Validate shader constant buffer size - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline states GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; diff --git a/Source/Engine/Renderer/VolumetricFogPass.cpp b/Source/Engine/Renderer/VolumetricFogPass.cpp index c56812c72..b7e57c2bb 100644 --- a/Source/Engine/Renderer/VolumetricFogPass.cpp +++ b/Source/Engine/Renderer/VolumetricFogPass.cpp @@ -53,19 +53,9 @@ bool VolumetricFogPass::setupResources() if (!_shader->IsLoaded()) return true; auto shader = _shader->GetShader(); - - // Validate shader constant buffers sizes - if (shader->GetCB(0)->GetSize() != sizeof(Data)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // CB1 is used for per-draw info (ObjectIndex) - if (shader->GetCB(2)->GetSize() != sizeof(PerLight)) - { - REPORT_INVALID_SHADER_PASS_CB_SIZE(shader, 2, PerLight); - return true; - } + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 2, PerLight); // Cache compute shaders _csInitialize = shader->GetCS("CS_Initialize"); From bdd7bae4591360f430758fc820166000c796fb2b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 29 Jun 2025 13:51:59 +0200 Subject: [PATCH 081/122] Add new Custom Lit shading model for custom lighting in materials (eg. Cel Shading) --- .../Features/ForwardShading.hlsl | 10 ++++++-- Source/Editor/Surface/Archetypes/Material.cs | 7 +++--- Source/Engine/Content/Assets/Material.cpp | 10 ++++---- .../Materials/DeferredMaterialShader.cpp | 2 ++ .../Engine/Graphics/Materials/MaterialInfo.h | 5 ++++ .../MaterialGenerator/MaterialGenerator.cpp | 23 ++++++++++--------- Source/Shaders/LightingCommon.hlsl | 18 +++++++++++++-- 7 files changed, 53 insertions(+), 22 deletions(-) diff --git a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl index 2db55111b..263859075 100644 --- a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl +++ b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl @@ -28,6 +28,13 @@ TextureCube SkyLightTexture : register(t__SRV__); Buffer ShadowsBuffer : register(t__SRV__); Texture2D ShadowMap : 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; } +LightData GetSkyLight() { return SkyLight; } +ProbeData GetEnvironmentProbe() { return EnvironmentProbe; } +ExponentialHeightFogData GetExponentialHeightFog() { return ExponentialHeightFog; } +uint GetLocalLightsCount() { return LocalLightsCount; } +LightData GetLocalLight(uint i) { return LocalLights[i]; } @5// Forward Shading: Shaders // Pixel Shader function for Forward Pass @@ -76,9 +83,8 @@ void PS_Forward( gBuffer.ShadingModel = MATERIAL_SHADING_MODEL; // Calculate lighting from a single directional light - float4 shadowMask = 1.0f; ShadowSample shadow = SampleDirectionalLightShadow(DirectionalLight, ShadowsBuffer, ShadowMap, gBuffer); - shadowMask = GetShadowMask(shadow); + float4 shadowMask = GetShadowMask(shadow); float4 light = GetLighting(ViewPos, DirectionalLight, gBuffer, shadowMask, false, false); // Calculate lighting from sky light diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index c9066eaa5..e46038639 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -124,7 +124,8 @@ namespace FlaxEditor.Surface.Archetypes case MaterialDomain.Particle: case MaterialDomain.Deformable: { - bool isNotUnlit = info.ShadingModel != MaterialShadingModel.Unlit; + bool isNotUnlit = info.ShadingModel != MaterialShadingModel.Unlit && info.ShadingModel != MaterialShadingModel.CustomLit; + bool isOpaque = info.BlendMode == MaterialBlendMode.Opaque; bool withTess = info.TessellationMode != TessellationMethod.None; GetBox(MaterialNodeBoxes.Color).IsActive = isNotUnlit; @@ -135,8 +136,8 @@ namespace FlaxEditor.Surface.Archetypes GetBox(MaterialNodeBoxes.Roughness).IsActive = isNotUnlit; GetBox(MaterialNodeBoxes.AmbientOcclusion).IsActive = isNotUnlit; GetBox(MaterialNodeBoxes.Normal).IsActive = isNotUnlit; - GetBox(MaterialNodeBoxes.Opacity).IsActive = info.ShadingModel == MaterialShadingModel.Subsurface || info.ShadingModel == MaterialShadingModel.Foliage || info.BlendMode != MaterialBlendMode.Opaque; - GetBox(MaterialNodeBoxes.Refraction).IsActive = info.BlendMode != MaterialBlendMode.Opaque; + GetBox(MaterialNodeBoxes.Opacity).IsActive = info.ShadingModel == MaterialShadingModel.Subsurface || info.ShadingModel == MaterialShadingModel.Foliage || !isOpaque; + GetBox(MaterialNodeBoxes.Refraction).IsActive = !isOpaque; GetBox(MaterialNodeBoxes.PositionOffset).IsActive = true; GetBox(MaterialNodeBoxes.TessellationMultiplier).IsActive = withTess; GetBox(MaterialNodeBoxes.WorldDisplacement).IsActive = withTess; diff --git a/Source/Engine/Content/Assets/Material.cpp b/Source/Engine/Content/Assets/Material.cpp index ce457c862..1e36b36ae 100644 --- a/Source/Engine/Content/Assets/Material.cpp +++ b/Source/Engine/Content/Assets/Material.cpp @@ -414,16 +414,18 @@ void Material::InitCompilationOptions(ShaderCompilationOptions& options) // Prepare auto& info = _shaderHeader.Material.Info; const bool isSurfaceOrTerrainOrDeformable = info.Domain == MaterialDomain::Surface || info.Domain == MaterialDomain::Terrain || info.Domain == MaterialDomain::Deformable; + const bool isOpaque = info.BlendMode == MaterialBlendMode::Opaque; const bool useCustomData = info.ShadingModel == MaterialShadingModel::Subsurface || info.ShadingModel == MaterialShadingModel::Foliage; - const bool useForward = ((info.Domain == MaterialDomain::Surface || info.Domain == MaterialDomain::Deformable) && info.BlendMode != MaterialBlendMode::Opaque) || info.Domain == MaterialDomain::Particle; + const bool useForward = ((info.Domain == MaterialDomain::Surface || info.Domain == MaterialDomain::Deformable) && !isOpaque) || info.Domain == MaterialDomain::Particle; const bool useTess = info.TessellationMode != TessellationMethod::None && RenderTools::CanSupportTessellation(options.Profile) && isSurfaceOrTerrainOrDeformable; const bool useDistortion = (info.Domain == MaterialDomain::Surface || info.Domain == MaterialDomain::Deformable || info.Domain == MaterialDomain::Particle) && - info.BlendMode != MaterialBlendMode::Opaque && + !isOpaque && EnumHasAnyFlags(info.UsageFlags, MaterialUsageFlags::UseRefraction) && (info.FeaturesFlags & MaterialFeaturesFlags::DisableDistortion) == MaterialFeaturesFlags::None; + const MaterialShadingModel shadingModel = info.ShadingModel == MaterialShadingModel::CustomLit ? MaterialShadingModel::Unlit : info.ShadingModel; // @formatter:off static const char* Numbers[] = @@ -435,7 +437,7 @@ void Material::InitCompilationOptions(ShaderCompilationOptions& options) // Setup shader macros options.Macros.Add({ "MATERIAL_DOMAIN", Numbers[(int32)info.Domain] }); options.Macros.Add({ "MATERIAL_BLEND", Numbers[(int32)info.BlendMode] }); - options.Macros.Add({ "MATERIAL_SHADING_MODEL", Numbers[(int32)info.ShadingModel] }); + options.Macros.Add({ "MATERIAL_SHADING_MODEL", Numbers[(int32)shadingModel] }); options.Macros.Add({ "MATERIAL_MASKED", Numbers[EnumHasAnyFlags(info.UsageFlags, MaterialUsageFlags::UseMask) ? 1 : 0] }); options.Macros.Add({ "DECAL_BLEND_MODE", Numbers[(int32)info.DecalBlendingMode] }); options.Macros.Add({ "USE_EMISSIVE", Numbers[EnumHasAnyFlags(info.UsageFlags, MaterialUsageFlags::UseEmissive) ? 1 : 0] }); @@ -492,7 +494,7 @@ void Material::InitCompilationOptions(ShaderCompilationOptions& options) options.Macros.Add({ "IS_PARTICLE", Numbers[info.Domain == MaterialDomain::Particle ? 1 : 0] }); options.Macros.Add({ "IS_DEFORMABLE", Numbers[info.Domain == MaterialDomain::Deformable ? 1 : 0] }); options.Macros.Add({ "USE_FORWARD", Numbers[useForward ? 1 : 0] }); - options.Macros.Add({ "USE_DEFERRED", Numbers[isSurfaceOrTerrainOrDeformable && info.BlendMode == MaterialBlendMode::Opaque ? 1 : 0] }); + options.Macros.Add({ "USE_DEFERRED", Numbers[isSurfaceOrTerrainOrDeformable && isOpaque ? 1 : 0] }); options.Macros.Add({ "USE_DISTORTION", Numbers[useDistortion ? 1 : 0] }); #endif } diff --git a/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp b/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp index c15ff5ef2..4e9622a01 100644 --- a/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp @@ -42,6 +42,8 @@ void DeferredMaterialShader::Bind(BindParameters& params) // Setup features const bool useLightmap = _info.BlendMode == MaterialBlendMode::Opaque && LightmapFeature::Bind(params, cb, srv); + if (_info.ShadingModel == MaterialShadingModel::CustomLit) + ForwardShadingFeature::Bind(params, cb, srv); // Setup parameters MaterialParameter::BindMeta bindMeta; diff --git a/Source/Engine/Graphics/Materials/MaterialInfo.h b/Source/Engine/Graphics/Materials/MaterialInfo.h index 2a57b5f3d..69a9bd0a6 100644 --- a/Source/Engine/Graphics/Materials/MaterialInfo.h +++ b/Source/Engine/Graphics/Materials/MaterialInfo.h @@ -103,6 +103,11 @@ API_ENUM() enum class MaterialShadingModel : byte /// The foliage material. Intended for foliage materials like leaves and grass that need light scattering to transport simulation through the thin object. /// Foliage = 3, + + /// + /// The custom lit shader that calculates own lighting such as Cel Shading. It has access to the scene lights data during both GBuffer and Forward pass rendering. + /// + CustomLit = 5, }; /// diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp index 5950676a3..2aea40c94 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp @@ -184,28 +184,29 @@ bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo return true; \ } \ } + const bool isOpaque = materialInfo.BlendMode == MaterialBlendMode::Opaque; switch (baseLayer->Domain) { case MaterialDomain::Surface: if (materialInfo.TessellationMode != TessellationMethod::None) ADD_FEATURE(TessellationFeature); - if (materialInfo.BlendMode == MaterialBlendMode::Opaque) + if (isOpaque) ADD_FEATURE(MotionVectorsFeature); - if (materialInfo.BlendMode == MaterialBlendMode::Opaque) + if (isOpaque) ADD_FEATURE(LightmapFeature); - if (materialInfo.BlendMode == MaterialBlendMode::Opaque) + if (isOpaque) ADD_FEATURE(DeferredShadingFeature); - if (materialInfo.BlendMode != MaterialBlendMode::Opaque && (materialInfo.FeaturesFlags & MaterialFeaturesFlags::DisableDistortion) == MaterialFeaturesFlags::None) + if (!isOpaque && (materialInfo.FeaturesFlags & MaterialFeaturesFlags::DisableDistortion) == MaterialFeaturesFlags::None) ADD_FEATURE(DistortionFeature); - if (materialInfo.BlendMode != MaterialBlendMode::Opaque && EnumHasAnyFlags(materialInfo.FeaturesFlags, MaterialFeaturesFlags::GlobalIllumination)) + if (!isOpaque && EnumHasAnyFlags(materialInfo.FeaturesFlags, MaterialFeaturesFlags::GlobalIllumination)) { ADD_FEATURE(GlobalIlluminationFeature); - // SDF Reflections is only valid when both GI and SSR is enabled - if (materialInfo.BlendMode != MaterialBlendMode::Opaque && EnumHasAnyFlags(materialInfo.FeaturesFlags, MaterialFeaturesFlags::ScreenSpaceReflections)) + // SDF Reflections is only valid when both GI and SSR are enabled + if (EnumHasAnyFlags(materialInfo.FeaturesFlags, MaterialFeaturesFlags::ScreenSpaceReflections)) ADD_FEATURE(SDFReflectionsFeature); } - if (materialInfo.BlendMode != MaterialBlendMode::Opaque) + if (materialInfo.BlendMode != MaterialBlendMode::Opaque || materialInfo.ShadingModel == MaterialShadingModel::CustomLit) ADD_FEATURE(ForwardShadingFeature); break; case MaterialDomain::Terrain: @@ -215,16 +216,16 @@ bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo ADD_FEATURE(DeferredShadingFeature); break; case MaterialDomain::Particle: - if (materialInfo.BlendMode != MaterialBlendMode::Opaque && (materialInfo.FeaturesFlags & MaterialFeaturesFlags::DisableDistortion) == MaterialFeaturesFlags::None) + if (!isOpaque && (materialInfo.FeaturesFlags & MaterialFeaturesFlags::DisableDistortion) == MaterialFeaturesFlags::None) ADD_FEATURE(DistortionFeature); - if (materialInfo.BlendMode != MaterialBlendMode::Opaque && EnumHasAnyFlags(materialInfo.FeaturesFlags, MaterialFeaturesFlags::GlobalIllumination)) + if (!isOpaque && EnumHasAnyFlags(materialInfo.FeaturesFlags, MaterialFeaturesFlags::GlobalIllumination)) ADD_FEATURE(GlobalIlluminationFeature); ADD_FEATURE(ForwardShadingFeature); break; case MaterialDomain::Deformable: if (materialInfo.TessellationMode != TessellationMethod::None) ADD_FEATURE(TessellationFeature); - if (materialInfo.BlendMode == MaterialBlendMode::Opaque) + if (isOpaque) ADD_FEATURE(DeferredShadingFeature); if (materialInfo.BlendMode != MaterialBlendMode::Opaque) ADD_FEATURE(ForwardShadingFeature); diff --git a/Source/Shaders/LightingCommon.hlsl b/Source/Shaders/LightingCommon.hlsl index 807d6a71d..f09572310 100644 --- a/Source/Shaders/LightingCommon.hlsl +++ b/Source/Shaders/LightingCommon.hlsl @@ -62,8 +62,8 @@ void GetRadialLightAttenuation( float distanceBiasSqr, float3 toLight, float3 L, - inout float NoL, - inout float attenuation) + out float NoL, + out float attenuation) { // Distance attenuation if (lightData.InverseSquared) @@ -104,6 +104,20 @@ void GetRadialLightAttenuation( } } +// Calculates radial light (point or spot) attenuation factors (distance, spot and radius mask) +void GetRadialLightAttenuation( + LightData lightData, + bool isSpotLight, + float3 toLight, + float3 N, + out float NoL, + out float attenuation) +{ + float distanceSqr = dot(toLight, toLight); + float3 L = toLight * rsqrt(distanceSqr); + GetRadialLightAttenuation(lightData, isSpotLight, N, distanceSqr, 1, toLight, L, NoL, attenuation); +} + // Find representative incoming light direction and energy modification float AreaLightSpecular(LightData lightData, float roughness, inout float3 toLight, inout float3 L, float3 V, half3 N) { From f126a83b797c1fd3dda98f3438079c81d33dcde3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 29 Jun 2025 13:52:29 +0200 Subject: [PATCH 082/122] Fix graphical issues when batching materials that use Forward Shading for instancing --- Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp | 5 ++++- Source/Engine/Graphics/Materials/DeferredMaterialShader.h | 1 + Source/Engine/Graphics/Materials/ForwardMaterialShader.cpp | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp b/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp index 4e9622a01..5f0abad33 100644 --- a/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp @@ -29,7 +29,7 @@ bool DeferredMaterialShader::CanUseLightmap() const bool DeferredMaterialShader::CanUseInstancing(InstancingHandler& handler) const { handler = { SurfaceDrawCallHandler::GetHash, SurfaceDrawCallHandler::CanBatch, }; - return true; + return _instanced; } void DeferredMaterialShader::Bind(BindParameters& params) @@ -114,6 +114,9 @@ void DeferredMaterialShader::Unload() bool DeferredMaterialShader::Load() { + // TODO: support instancing when using ForwardShadingFeature + _instanced = _info.BlendMode == MaterialBlendMode::Opaque && _info.ShadingModel != MaterialShadingModel::CustomLit; + bool failed = false; auto psDesc = GPUPipelineState::Description::Default; psDesc.DepthWriteEnable = (_info.FeaturesFlags & MaterialFeaturesFlags::DisableDepthWrite) == MaterialFeaturesFlags::None; diff --git a/Source/Engine/Graphics/Materials/DeferredMaterialShader.h b/Source/Engine/Graphics/Materials/DeferredMaterialShader.h index 4d01c4d4a..ebfd54ecb 100644 --- a/Source/Engine/Graphics/Materials/DeferredMaterialShader.h +++ b/Source/Engine/Graphics/Materials/DeferredMaterialShader.h @@ -65,6 +65,7 @@ private: private: Cache _cache; Cache _cacheInstanced; + bool _instanced; public: DeferredMaterialShader(const StringView& name) diff --git a/Source/Engine/Graphics/Materials/ForwardMaterialShader.cpp b/Source/Engine/Graphics/Materials/ForwardMaterialShader.cpp index 4ed8e6b86..a966507d8 100644 --- a/Source/Engine/Graphics/Materials/ForwardMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/ForwardMaterialShader.cpp @@ -25,7 +25,7 @@ DrawPass ForwardMaterialShader::GetDrawModes() const bool ForwardMaterialShader::CanUseInstancing(InstancingHandler& handler) const { handler = { SurfaceDrawCallHandler::GetHash, SurfaceDrawCallHandler::CanBatch, }; - return true; + return false; // TODO: support instancing when using ForwardShadingFeature } void ForwardMaterialShader::Bind(BindParameters& params) From 43d11264f82ac2ffb3b4f94d77da427f32a05ac2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 29 Jun 2025 19:16:23 +0200 Subject: [PATCH 083/122] Fix asset references to use separate lightweight locking instead of full asset mutex --- Source/Engine/Content/Asset.cpp | 8 ++++---- Source/Engine/Content/Asset.h | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 5a9e62993..559a673da 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -242,9 +242,8 @@ void Asset::AddReference(IAssetReference* ref, bool week) if (ref) { //PROFILE_MEM(EngineDelegate); // Include references tracking memory within Delegate memory - Locker.Lock(); + ScopeLock lock(_referencesLocker); _references.Add(ref); - Locker.Unlock(); } } @@ -257,9 +256,8 @@ void Asset::RemoveReference(IAssetReference* ref, bool week) { if (ref) { - Locker.Lock(); + ScopeLock lock(_referencesLocker); _references.Remove(ref); - Locker.Unlock(); } if (!week) Platform::InterlockedDecrement(&_refCount); @@ -681,6 +679,7 @@ void Asset::onLoaded_MainThread() ASSERT(IsInMainThread()); // Send event + ScopeLock lock(_referencesLocker); for (const auto& e : _references) e.Item->OnAssetLoaded(this, this); OnLoaded(this); @@ -696,6 +695,7 @@ void Asset::onUnload_MainThread() CancelStreaming(); // Send event + ScopeLock lock(_referencesLocker); for (const auto& e : _references) e.Item->OnAssetUnloaded(this, this); OnUnloaded(this); diff --git a/Source/Engine/Content/Asset.h b/Source/Engine/Content/Asset.h index bb0fbe490..c838eddf8 100644 --- a/Source/Engine/Content/Asset.h +++ b/Source/Engine/Content/Asset.h @@ -7,6 +7,7 @@ #include "Engine/Core/Types/String.h" #include "Engine/Platform/CriticalSection.h" #include "Engine/Scripting/ScriptingObject.h" +#include "Engine/Threading/ConcurrentSystemLocker.h" #include "Config.h" #include "Types.h" @@ -63,6 +64,7 @@ protected: int8 _isVirtual : 1; // Indicates that asset is pure virtual (generated or temporary, has no storage so won't be saved) HashSet _references; + CriticalSection _referencesLocker; // TODO: convert into a single interlocked exchange for the current thread owning lock public: /// From 78d519cb9a71ab42a506c0f5018b6d417f2bf426 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 29 Jun 2025 19:16:41 +0200 Subject: [PATCH 084/122] Fix `ConcurrentSystemLocker` to have exclusive lock as an option --- Source/Engine/Level/Scene/SceneRendering.cpp | 6 +++--- Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp | 8 +++++--- Source/Engine/Threading/ConcurrentSystemLocker.cpp | 6 +++--- Source/Engine/Threading/ConcurrentSystemLocker.h | 6 +++--- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Source/Engine/Level/Scene/SceneRendering.cpp b/Source/Engine/Level/Scene/SceneRendering.cpp index 4a88703ae..e55dbd43f 100644 --- a/Source/Engine/Level/Scene/SceneRendering.cpp +++ b/Source/Engine/Level/Scene/SceneRendering.cpp @@ -127,7 +127,7 @@ void SceneRendering::CollectPostFxVolumes(RenderContext& renderContext) void SceneRendering::Clear() { - ConcurrentSystemLocker::WriteScope lock(Locker); + ConcurrentSystemLocker::WriteScope lock(Locker, true); for (auto* listener : _listeners) { listener->OnSceneRenderingClear(this); @@ -149,7 +149,7 @@ void SceneRendering::AddActor(Actor* a, int32& key) return; PROFILE_MEM(Graphics); const int32 category = a->_drawCategory; - ConcurrentSystemLocker::WriteScope lock(Locker); + ConcurrentSystemLocker::WriteScope lock(Locker, true); auto& list = Actors[category]; if (FreeActors[category].HasItems()) { @@ -193,7 +193,7 @@ void SceneRendering::UpdateActor(Actor* a, int32& key, ISceneRenderingListener:: void SceneRendering::RemoveActor(Actor* a, int32& key) { const int32 category = a->_drawCategory; - ConcurrentSystemLocker::WriteScope lock(Locker); + ConcurrentSystemLocker::WriteScope lock(Locker, true); auto& list = Actors[category]; if (list.Count() > key) // Ignore invalid key softly (eg. list after batch clear during scene unload) { diff --git a/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp index 56dd196a5..e0d227b6b 100644 --- a/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp +++ b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp @@ -396,7 +396,7 @@ public: { if (GLOBAL_SDF_ACTOR_IS_STATIC(a) && ObjectTypes.Contains(a->GetTypeHandle())) { - ConcurrentSystemLocker::WriteScope lock(Locker); + ConcurrentSystemLocker::WriteScope lock(Locker, true); OnSceneRenderingDirty(a->GetBox()); } } @@ -405,7 +405,7 @@ public: { if (GLOBAL_SDF_ACTOR_IS_STATIC(a) && ObjectTypes.Contains(a->GetTypeHandle())) { - ConcurrentSystemLocker::WriteScope lock(Locker); + ConcurrentSystemLocker::WriteScope lock(Locker, true); OnSceneRenderingDirty(BoundingBox::FromSphere(prevBounds)); OnSceneRenderingDirty(a->GetBox()); } @@ -415,13 +415,14 @@ public: { if (GLOBAL_SDF_ACTOR_IS_STATIC(a) && ObjectTypes.Contains(a->GetTypeHandle())) { - ConcurrentSystemLocker::WriteScope lock(Locker); + ConcurrentSystemLocker::WriteScope lock(Locker, true); OnSceneRenderingDirty(a->GetBox()); } } void OnSceneRenderingClear(SceneRendering* scene) override { + ConcurrentSystemLocker::WriteScope lock(Locker, true); for (auto& cascade : Cascades) cascade.StaticChunks.Clear(); } @@ -719,6 +720,7 @@ bool GlobalSignDistanceFieldPass::Render(RenderContext& renderContext, GPUContex } sdfData.LastFrameUsed = currentFrame; PROFILE_GPU_CPU("Global SDF"); + ConcurrentSystemLocker::WriteScope lock(sdfData.Locker); // Setup options int32 resolution, cascadesCount, resolutionMip; diff --git a/Source/Engine/Threading/ConcurrentSystemLocker.cpp b/Source/Engine/Threading/ConcurrentSystemLocker.cpp index c8debb561..d936f8307 100644 --- a/Source/Engine/Threading/ConcurrentSystemLocker.cpp +++ b/Source/Engine/Threading/ConcurrentSystemLocker.cpp @@ -8,7 +8,7 @@ ConcurrentSystemLocker::ConcurrentSystemLocker() _counters[0] = _counters[1] = 0; } -void ConcurrentSystemLocker::Begin(bool write) +void ConcurrentSystemLocker::Begin(bool write, bool exclusively) { volatile int64* thisCounter = &_counters[write]; volatile int64* otherCounter = &_counters[!write]; @@ -22,8 +22,8 @@ RETRY: goto RETRY; } - // Writers have to check themselves to (one write at the same time - just like a mutex) - if (write && Platform::AtomicRead(thisCounter) != 0) + // Writers might want to check themselves for a single writer at the same time - just like a mutex + if (exclusively && Platform::AtomicRead(thisCounter) != 0) { // Someone else is doing opposite operation so wait for it's end Platform::Sleep(0); diff --git a/Source/Engine/Threading/ConcurrentSystemLocker.h b/Source/Engine/Threading/ConcurrentSystemLocker.h index dd214a308..031b7e685 100644 --- a/Source/Engine/Threading/ConcurrentSystemLocker.h +++ b/Source/Engine/Threading/ConcurrentSystemLocker.h @@ -17,7 +17,7 @@ public: NON_COPYABLE(ConcurrentSystemLocker); ConcurrentSystemLocker(); - void Begin(bool write); + void Begin(bool write, bool exclusively = false); void End(bool write); public: @@ -26,10 +26,10 @@ public: { NON_COPYABLE(Scope); - Scope(ConcurrentSystemLocker& locker) + Scope(ConcurrentSystemLocker& locker, bool exclusively = false) : _locker(locker) { - _locker.Begin(Write); + _locker.Begin(Write, exclusively); } ~Scope() From 448eb48c230dd1c7c2591798d5ce4a993257ddfe Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 29 Jun 2025 20:02:24 +0200 Subject: [PATCH 085/122] Fix fog to draw Fog Cutoff Distance via a plane, not sphere test Add support for negative Fog Cutoff Distance on fog to draw it in front of the camera Far Plane, no matter the setup. Fix hot-reloading Fog shader in Editor. --- .../Features/ForwardShading.hlsl | 2 +- Content/Editor/Particles/Smoke Material.flax | 4 +- Content/Shaders/Fog.flax | 4 +- .../Level/Actors/ExponentialHeightFog.cpp | 8 +-- .../Level/Actors/ExponentialHeightFog.h | 4 +- Source/Shaders/ExponentialHeightFog.hlsl | 9 ++- Source/Shaders/Fog.shader | 55 +++++++------------ 7 files changed, 39 insertions(+), 47 deletions(-) diff --git a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl index 263859075..625a78e36 100644 --- a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl +++ b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl @@ -151,7 +151,7 @@ void PS_Forward( #if USE_FOG // Calculate exponential height fog - float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, 0); + float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, 0, gBuffer.ViewPos.z); // Apply fog to the output color #if MATERIAL_BLEND == MATERIAL_BLEND_OPAQUE diff --git a/Content/Editor/Particles/Smoke Material.flax b/Content/Editor/Particles/Smoke Material.flax index bd8c1c0e9..d5b8cb872 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:6097a8ca31dbe7a985b5c512d2049d2d22c73175551965c75d6b360323505491 -size 38427 +oid sha256:a16a3fa5bed3bc8030c40fbe0e946f2bdec28745542bf08db1d7b4a43180f785 +size 38900 diff --git a/Content/Shaders/Fog.flax b/Content/Shaders/Fog.flax index 75590f84d..3f934412c 100644 --- a/Content/Shaders/Fog.flax +++ b/Content/Shaders/Fog.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7735a770a87483d4df5e4e653373067c26469de8088f071ca092ed3e797bf461 -size 2785 +oid sha256:e83f9dbbcf84550de09e7c63bbdd3acc6591cf6ba1bcce2a2699772122ae07f4 +size 2633 diff --git a/Source/Engine/Level/Actors/ExponentialHeightFog.cpp b/Source/Engine/Level/Actors/ExponentialHeightFog.cpp index d62aecac4..efb5351e7 100644 --- a/Source/Engine/Level/Actors/ExponentialHeightFog.cpp +++ b/Source/Engine/Level/Actors/ExponentialHeightFog.cpp @@ -41,11 +41,10 @@ void ExponentialHeightFog::Draw(RenderContext& renderContext) && _shader->IsLoaded() && renderContext.View.IsPerspectiveProjection()) { - // Prepare if (_psFog.States[0] == nullptr) - { - // Create pipeline states _psFog.CreatePipelineStates(); + if (!_psFog.States[0]->IsValid()) + { GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; psDesc.DepthWriteEnable = false; psDesc.BlendMode.BlendEnable = true; @@ -59,6 +58,7 @@ void ExponentialHeightFog::Draw(RenderContext& renderContext) if (_psFog.Create(psDesc, _shader->GetShader(), "PS_Fog")) { LOG(Warning, "Cannot create graphics pipeline state object for '{0}'.", ToString()); + return; } } @@ -160,7 +160,7 @@ void ExponentialHeightFog::GetExponentialHeightFogData(const RenderView& view, S result.FogAtViewPosition = density * Math::Pow(2.0f, Math::Clamp(-heightFalloff * (viewHeight - height), -125.f, 126.f)); result.StartDistance = StartDistance; result.FogMinOpacity = 1.0f - FogMaxOpacity; - result.FogCutoffDistance = FogCutoffDistance; + result.FogCutoffDistance = FogCutoffDistance >= 0 ? FogCutoffDistance : view.Far + FogCutoffDistance; if (useDirectionalLightInscattering) { result.InscatteringLightDirection = -DirectionalInscatteringLight->GetDirection(); diff --git a/Source/Engine/Level/Actors/ExponentialHeightFog.h b/Source/Engine/Level/Actors/ExponentialHeightFog.h index 0b442ba9f..c0e5407d2 100644 --- a/Source/Engine/Level/Actors/ExponentialHeightFog.h +++ b/Source/Engine/Level/Actors/ExponentialHeightFog.h @@ -55,9 +55,9 @@ public: float StartDistance = 0.0f; /// - /// Scene elements past this distance will not have fog applied. This is useful for excluding skyboxes which already have fog baked in. Setting this value to 0 disables it. + /// Scene elements past this distance will not have fog applied. This is useful for excluding skyboxes which already have fog baked in. Setting this value to 0 disables it. Negative value sets the cutoff distance relative to the far plane of the camera. /// - API_FIELD(Attributes="EditorOrder(60), DefaultValue(0.0f), Limit(0), EditorDisplay(\"Exponential Height Fog\")") + API_FIELD(Attributes="EditorOrder(60), DefaultValue(0.0f), EditorDisplay(\"Exponential Height Fog\")") float FogCutoffDistance = 0.0f; public: diff --git a/Source/Shaders/ExponentialHeightFog.hlsl b/Source/Shaders/ExponentialHeightFog.hlsl index 2e34936eb..f6fb918f5 100644 --- a/Source/Shaders/ExponentialHeightFog.hlsl +++ b/Source/Shaders/ExponentialHeightFog.hlsl @@ -29,7 +29,7 @@ struct ExponentialHeightFogData float StartDistance; }; -float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, float3 posWS, float3 camWS, float skipDistance) +float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, float3 posWS, float3 camWS, float skipDistance, float sceneDistance) { float3 cameraToPos = posWS - camWS; float cameraToPosSqr = dot(cameraToPos, cameraToPos); @@ -78,7 +78,7 @@ float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, fl // Disable fog after a certain distance FLATTEN - if (exponentialHeightFog.FogCutoffDistance > 0 && cameraToPosLen > exponentialHeightFog.FogCutoffDistance) + if (exponentialHeightFog.FogCutoffDistance > 0 && sceneDistance > exponentialHeightFog.FogCutoffDistance) { expFogFactor = 1; directionalInscattering = 0; @@ -87,4 +87,9 @@ float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, fl return float4(inscatteringColor * (1.0f - expFogFactor) + directionalInscattering, expFogFactor); } +float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, float3 posWS, float3 camWS, float skipDistance) +{ + return GetExponentialHeightFog(exponentialHeightFog, posWS, camWS, skipDistance, distance(posWS, camWS)); +} + #endif diff --git a/Source/Shaders/Fog.shader b/Source/Shaders/Fog.shader index 7dcc679ec..dfea921cc 100644 --- a/Source/Shaders/Fog.shader +++ b/Source/Shaders/Fog.shader @@ -24,41 +24,17 @@ Texture2D Depth : register(t0); Texture3D IntegratedLightScattering : register(t1); #endif -// Get world space position at given pixel coordinate -float3 GetWorldPos(float2 uv) -{ - float depth = SAMPLE_RT(Depth, uv).r; - GBufferData gBufferData = GetGBufferData(); - float3 viewPos = GetViewPos(gBufferData, uv, depth); - return mul(float4(viewPos, 1), gBufferData.InvViewMatrix).xyz; -} - -float4 CalculateCombinedFog(float3 posWS, float sceneDepth, float3 volumeUV) -{ - float skipDistance = 0; - -#if VOLUMETRIC_FOG - skipDistance = max(ExponentialHeightFog.VolumetricFogMaxDistance - 100, 0); -#endif - - float4 fog = GetExponentialHeightFog(ExponentialHeightFog, posWS, GBuffer.ViewPos, skipDistance); - -#if VOLUMETRIC_FOG - float4 volumetricFog = IntegratedLightScattering.SampleLevel(SamplerLinearClamp, volumeUV, 0); - fog = float4(volumetricFog.rgb + fog.rgb * volumetricFog.a, volumetricFog.a * fog.a); -#endif - - return fog; -} - META_PS(true, FEATURE_LEVEL_ES2) META_PERMUTATION_1(VOLUMETRIC_FOG=0) META_PERMUTATION_1(VOLUMETRIC_FOG=1) float4 PS_Fog(Quad_VS2PS input) : SV_Target0 { - // Calculate pixel world space position - float3 posWS = GetWorldPos(input.TexCoord); - float3 viewVector = posWS - GBuffer.ViewPos; + // Get world space position at given pixel coordinate + float rawDepth = SAMPLE_RT(Depth, input.TexCoord).r; + GBufferData gBufferData = GetGBufferData(); + float3 viewPos = GetViewPos(gBufferData, input.TexCoord, rawDepth); + float3 worldPos = mul(float4(viewPos, 1), gBufferData.InvViewMatrix).xyz; + float3 viewVector = worldPos - GBuffer.ViewPos; float sceneDepth = length(viewVector); // Calculate volumetric fog coordinates @@ -67,17 +43,28 @@ float4 PS_Fog(Quad_VS2PS input) : SV_Target0 // Debug code #if VOLUMETRIC_FOG && 0 - volumeUV = posWS / 1000; + volumeUV = worldPos / 1000; if (!all(volumeUV >= 0 && volumeUV <= 1)) return 0; return float4(IntegratedLightScattering.SampleLevel(SamplerLinearClamp, volumeUV, 0).rgb, 1); //return float4(volumeUV, 1); - //return float4(posWS / 100, 1); + //return float4(worldPos / 100, 1); #endif - // Calculate fog color - float4 fog = CalculateCombinedFog(posWS, sceneDepth, volumeUV); + float skipDistance = 0; +#if VOLUMETRIC_FOG + skipDistance = max(ExponentialHeightFog.VolumetricFogMaxDistance - 100, 0); +#endif + + // Calculate exponential fog color + float4 fog = GetExponentialHeightFog(ExponentialHeightFog, worldPos, GBuffer.ViewPos, skipDistance, viewPos.z); + +#if VOLUMETRIC_FOG + // Sample volumetric fog and mix it in + float4 volumetricFog = IntegratedLightScattering.SampleLevel(SamplerLinearClamp, volumeUV, 0); + fog = float4(volumetricFog.rgb + fog.rgb * volumetricFog.a, volumetricFog.a * fog.a); +#endif return fog; } From 4e4d380267de895fb1798d03afd251f71e8dfb7c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 29 Jun 2025 20:02:24 +0200 Subject: [PATCH 086/122] Fix fog to draw Fog Cutoff Distance via a plane, not sphere test Add support for negative Fog Cutoff Distance on fog to draw it in front of the camera Far Plane, no matter the setup. Fix hot-reloading Fog shader in Editor. --- .../Features/ForwardShading.hlsl | 2 +- Content/Editor/Particles/Smoke Material.flax | 4 +- Content/Shaders/Fog.flax | 4 +- .../Level/Actors/ExponentialHeightFog.cpp | 8 +-- .../Level/Actors/ExponentialHeightFog.h | 4 +- Source/Shaders/ExponentialHeightFog.hlsl | 9 ++- Source/Shaders/Fog.shader | 55 +++++++------------ 7 files changed, 39 insertions(+), 47 deletions(-) diff --git a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl index 2db55111b..08bb82698 100644 --- a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl +++ b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl @@ -145,7 +145,7 @@ void PS_Forward( #if USE_FOG // Calculate exponential height fog - float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, 0); + float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, 0, gBuffer.ViewPos.z); // Apply fog to the output color #if MATERIAL_BLEND == MATERIAL_BLEND_OPAQUE diff --git a/Content/Editor/Particles/Smoke Material.flax b/Content/Editor/Particles/Smoke Material.flax index bd8c1c0e9..d5b8cb872 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:6097a8ca31dbe7a985b5c512d2049d2d22c73175551965c75d6b360323505491 -size 38427 +oid sha256:a16a3fa5bed3bc8030c40fbe0e946f2bdec28745542bf08db1d7b4a43180f785 +size 38900 diff --git a/Content/Shaders/Fog.flax b/Content/Shaders/Fog.flax index 75590f84d..3f934412c 100644 --- a/Content/Shaders/Fog.flax +++ b/Content/Shaders/Fog.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7735a770a87483d4df5e4e653373067c26469de8088f071ca092ed3e797bf461 -size 2785 +oid sha256:e83f9dbbcf84550de09e7c63bbdd3acc6591cf6ba1bcce2a2699772122ae07f4 +size 2633 diff --git a/Source/Engine/Level/Actors/ExponentialHeightFog.cpp b/Source/Engine/Level/Actors/ExponentialHeightFog.cpp index d62aecac4..efb5351e7 100644 --- a/Source/Engine/Level/Actors/ExponentialHeightFog.cpp +++ b/Source/Engine/Level/Actors/ExponentialHeightFog.cpp @@ -41,11 +41,10 @@ void ExponentialHeightFog::Draw(RenderContext& renderContext) && _shader->IsLoaded() && renderContext.View.IsPerspectiveProjection()) { - // Prepare if (_psFog.States[0] == nullptr) - { - // Create pipeline states _psFog.CreatePipelineStates(); + if (!_psFog.States[0]->IsValid()) + { GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; psDesc.DepthWriteEnable = false; psDesc.BlendMode.BlendEnable = true; @@ -59,6 +58,7 @@ void ExponentialHeightFog::Draw(RenderContext& renderContext) if (_psFog.Create(psDesc, _shader->GetShader(), "PS_Fog")) { LOG(Warning, "Cannot create graphics pipeline state object for '{0}'.", ToString()); + return; } } @@ -160,7 +160,7 @@ void ExponentialHeightFog::GetExponentialHeightFogData(const RenderView& view, S result.FogAtViewPosition = density * Math::Pow(2.0f, Math::Clamp(-heightFalloff * (viewHeight - height), -125.f, 126.f)); result.StartDistance = StartDistance; result.FogMinOpacity = 1.0f - FogMaxOpacity; - result.FogCutoffDistance = FogCutoffDistance; + result.FogCutoffDistance = FogCutoffDistance >= 0 ? FogCutoffDistance : view.Far + FogCutoffDistance; if (useDirectionalLightInscattering) { result.InscatteringLightDirection = -DirectionalInscatteringLight->GetDirection(); diff --git a/Source/Engine/Level/Actors/ExponentialHeightFog.h b/Source/Engine/Level/Actors/ExponentialHeightFog.h index 0b442ba9f..c0e5407d2 100644 --- a/Source/Engine/Level/Actors/ExponentialHeightFog.h +++ b/Source/Engine/Level/Actors/ExponentialHeightFog.h @@ -55,9 +55,9 @@ public: float StartDistance = 0.0f; /// - /// Scene elements past this distance will not have fog applied. This is useful for excluding skyboxes which already have fog baked in. Setting this value to 0 disables it. + /// Scene elements past this distance will not have fog applied. This is useful for excluding skyboxes which already have fog baked in. Setting this value to 0 disables it. Negative value sets the cutoff distance relative to the far plane of the camera. /// - API_FIELD(Attributes="EditorOrder(60), DefaultValue(0.0f), Limit(0), EditorDisplay(\"Exponential Height Fog\")") + API_FIELD(Attributes="EditorOrder(60), DefaultValue(0.0f), EditorDisplay(\"Exponential Height Fog\")") float FogCutoffDistance = 0.0f; public: diff --git a/Source/Shaders/ExponentialHeightFog.hlsl b/Source/Shaders/ExponentialHeightFog.hlsl index 2e34936eb..f6fb918f5 100644 --- a/Source/Shaders/ExponentialHeightFog.hlsl +++ b/Source/Shaders/ExponentialHeightFog.hlsl @@ -29,7 +29,7 @@ struct ExponentialHeightFogData float StartDistance; }; -float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, float3 posWS, float3 camWS, float skipDistance) +float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, float3 posWS, float3 camWS, float skipDistance, float sceneDistance) { float3 cameraToPos = posWS - camWS; float cameraToPosSqr = dot(cameraToPos, cameraToPos); @@ -78,7 +78,7 @@ float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, fl // Disable fog after a certain distance FLATTEN - if (exponentialHeightFog.FogCutoffDistance > 0 && cameraToPosLen > exponentialHeightFog.FogCutoffDistance) + if (exponentialHeightFog.FogCutoffDistance > 0 && sceneDistance > exponentialHeightFog.FogCutoffDistance) { expFogFactor = 1; directionalInscattering = 0; @@ -87,4 +87,9 @@ float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, fl return float4(inscatteringColor * (1.0f - expFogFactor) + directionalInscattering, expFogFactor); } +float4 GetExponentialHeightFog(ExponentialHeightFogData exponentialHeightFog, float3 posWS, float3 camWS, float skipDistance) +{ + return GetExponentialHeightFog(exponentialHeightFog, posWS, camWS, skipDistance, distance(posWS, camWS)); +} + #endif diff --git a/Source/Shaders/Fog.shader b/Source/Shaders/Fog.shader index 7dcc679ec..dfea921cc 100644 --- a/Source/Shaders/Fog.shader +++ b/Source/Shaders/Fog.shader @@ -24,41 +24,17 @@ Texture2D Depth : register(t0); Texture3D IntegratedLightScattering : register(t1); #endif -// Get world space position at given pixel coordinate -float3 GetWorldPos(float2 uv) -{ - float depth = SAMPLE_RT(Depth, uv).r; - GBufferData gBufferData = GetGBufferData(); - float3 viewPos = GetViewPos(gBufferData, uv, depth); - return mul(float4(viewPos, 1), gBufferData.InvViewMatrix).xyz; -} - -float4 CalculateCombinedFog(float3 posWS, float sceneDepth, float3 volumeUV) -{ - float skipDistance = 0; - -#if VOLUMETRIC_FOG - skipDistance = max(ExponentialHeightFog.VolumetricFogMaxDistance - 100, 0); -#endif - - float4 fog = GetExponentialHeightFog(ExponentialHeightFog, posWS, GBuffer.ViewPos, skipDistance); - -#if VOLUMETRIC_FOG - float4 volumetricFog = IntegratedLightScattering.SampleLevel(SamplerLinearClamp, volumeUV, 0); - fog = float4(volumetricFog.rgb + fog.rgb * volumetricFog.a, volumetricFog.a * fog.a); -#endif - - return fog; -} - META_PS(true, FEATURE_LEVEL_ES2) META_PERMUTATION_1(VOLUMETRIC_FOG=0) META_PERMUTATION_1(VOLUMETRIC_FOG=1) float4 PS_Fog(Quad_VS2PS input) : SV_Target0 { - // Calculate pixel world space position - float3 posWS = GetWorldPos(input.TexCoord); - float3 viewVector = posWS - GBuffer.ViewPos; + // Get world space position at given pixel coordinate + float rawDepth = SAMPLE_RT(Depth, input.TexCoord).r; + GBufferData gBufferData = GetGBufferData(); + float3 viewPos = GetViewPos(gBufferData, input.TexCoord, rawDepth); + float3 worldPos = mul(float4(viewPos, 1), gBufferData.InvViewMatrix).xyz; + float3 viewVector = worldPos - GBuffer.ViewPos; float sceneDepth = length(viewVector); // Calculate volumetric fog coordinates @@ -67,17 +43,28 @@ float4 PS_Fog(Quad_VS2PS input) : SV_Target0 // Debug code #if VOLUMETRIC_FOG && 0 - volumeUV = posWS / 1000; + volumeUV = worldPos / 1000; if (!all(volumeUV >= 0 && volumeUV <= 1)) return 0; return float4(IntegratedLightScattering.SampleLevel(SamplerLinearClamp, volumeUV, 0).rgb, 1); //return float4(volumeUV, 1); - //return float4(posWS / 100, 1); + //return float4(worldPos / 100, 1); #endif - // Calculate fog color - float4 fog = CalculateCombinedFog(posWS, sceneDepth, volumeUV); + float skipDistance = 0; +#if VOLUMETRIC_FOG + skipDistance = max(ExponentialHeightFog.VolumetricFogMaxDistance - 100, 0); +#endif + + // Calculate exponential fog color + float4 fog = GetExponentialHeightFog(ExponentialHeightFog, worldPos, GBuffer.ViewPos, skipDistance, viewPos.z); + +#if VOLUMETRIC_FOG + // Sample volumetric fog and mix it in + float4 volumetricFog = IntegratedLightScattering.SampleLevel(SamplerLinearClamp, volumeUV, 0); + fog = float4(volumetricFog.rgb + fog.rgb * volumetricFog.a, volumetricFog.a * fog.a); +#endif return fog; } From 12c8bb4f31f79d2a94ce9187785b09cdffe21e5d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 1 Jul 2025 08:27:29 +0200 Subject: [PATCH 087/122] Fix scene lightmaps removal clearing actors linkage to lightmaps --- Source/Engine/Foliage/Foliage.cpp | 6 ++++++ Source/Engine/Foliage/Foliage.h | 5 +++++ Source/Engine/Level/Scene/Scene.cpp | 15 +++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index a1cd046ae..c0e0b0b0e 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -993,6 +993,12 @@ void Foliage::RemoveAllInstances() RebuildClusters(); } +void Foliage::RemoveLightmap() +{ + for (auto& e : Instances) + e.RemoveLightmap(); +} + static float GlobalDensityScale = 1.0f; float Foliage::GetGlobalDensityScale() diff --git a/Source/Engine/Foliage/Foliage.h b/Source/Engine/Foliage/Foliage.h index f9055f377..1855e9914 100644 --- a/Source/Engine/Foliage/Foliage.h +++ b/Source/Engine/Foliage/Foliage.h @@ -139,6 +139,11 @@ public: /// API_FUNCTION() void RemoveAllInstances(); + /// + /// Removes the lightmap data from the foliage instances. + /// + API_FUNCTION() void RemoveLightmap(); + public: /// /// Gets the global density scale for all foliage instances. The default value is 1. Use values from range 0-1. Lower values decrease amount of foliage instances in-game. Use it to tweak game performance for slower devices. diff --git a/Source/Engine/Level/Scene/Scene.cpp b/Source/Engine/Level/Scene/Scene.cpp index 215153122..f60aa8d2f 100644 --- a/Source/Engine/Level/Scene/Scene.cpp +++ b/Source/Engine/Level/Scene/Scene.cpp @@ -7,6 +7,7 @@ #include "Engine/Content/Content.h" #include "Engine/Content/Deprecated.h" #include "Engine/Content/Factories/JsonAssetFactory.h" +#include "Engine/Foliage/Foliage.h" #include "Engine/Physics/Colliders/MeshCollider.h" #include "Engine/Level/Actors/StaticModel.h" #include "Engine/Level/ActorsCache.h" @@ -15,6 +16,7 @@ #include "Engine/Navigation/NavMesh.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Serialization/Serialization.h" +#include "Engine/Terrain/Terrain.h" #if USE_EDITOR #include "Engine/Engine/Globals.h" #endif @@ -98,6 +100,19 @@ void Scene::SetLightmapSettings(const LightmapSettings& value) void Scene::ClearLightmaps() { LightmapsData.ClearLightmaps(); + + // Unlink any actors from lightmap + Function function = [this](Actor* actor) + { + if (auto* staticModel = Cast(actor)) + staticModel->RemoveLightmap(); + else if (auto* terrain = Cast(actor)) + terrain->RemoveLightmap(); + else if (auto* foliage = Cast(actor)) + foliage->RemoveLightmap(); + return true; + }; + TreeExecute(function); } void Scene::BuildCSG(float timeoutMs) From dd256ad51828b895192d9291b4f5108690d370c7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 1 Jul 2025 10:37:39 +0200 Subject: [PATCH 088/122] Fix typo --- Flax.sln.DotSettings | 1 + Source/Engine/Core/Config/GraphicsSettings.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings index 86647d380..3611934dd 100644 --- a/Flax.sln.DotSettings +++ b/Flax.sln.DotSettings @@ -267,6 +267,7 @@ True True True + True True True True diff --git a/Source/Engine/Core/Config/GraphicsSettings.h b/Source/Engine/Core/Config/GraphicsSettings.h index 831605596..d8b35533a 100644 --- a/Source/Engine/Core/Config/GraphicsSettings.h +++ b/Source/Engine/Core/Config/GraphicsSettings.h @@ -84,7 +84,7 @@ public: bool EnableGlobalSDF = false; /// - /// Draw distance of the Global SDF. Actual value can be large when using DDGI. + /// Draw distance of the Global SDF. Actual value can be larger when using DDGI. /// API_FIELD(Attributes="EditorOrder(2001), EditorDisplay(\"Global SDF\"), Limit(1000), ValueCategory(Utils.ValueCategory.Distance)") float GlobalSDFDistance = 15000.0f; From 78f0ff75f29348b8df21f071b286314f977d8394 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 1 Jul 2025 10:52:04 +0200 Subject: [PATCH 089/122] Move debug log clearing to handle pending entries too in publci api --- Source/Editor/Windows/DebugLogWindow.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Windows/DebugLogWindow.cs b/Source/Editor/Windows/DebugLogWindow.cs index 36a7ffa82..ed133d4e8 100644 --- a/Source/Editor/Windows/DebugLogWindow.cs +++ b/Source/Editor/Windows/DebugLogWindow.cs @@ -447,6 +447,10 @@ namespace FlaxEditor.Windows /// public void Clear() { + lock (_locker) + { + _pendingEntries.Clear(); + } if (_entriesPanel == null) return; RemoveEntries(); @@ -740,10 +744,6 @@ namespace FlaxEditor.Windows // Clear on Play if (Editor.Options.Options.Interface.DebugLogClearOnPlay) { - lock (_locker) - { - _pendingEntries?.Clear(); - } Clear(); } } From 7f783bb7dae1e6f1be7c014cad0486c9e11a3114 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 2 Jul 2025 17:23:04 +0200 Subject: [PATCH 090/122] Fix crash when importing mesh with incorrect triangulation --- Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index 2cc2823af..81ce46d6c 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -692,8 +692,12 @@ int Triangulate(OpenFbxImporterData& data, const ofbx::GeometryData& geom, const triangulatedIndices[i] = polygon.from_vertex + (earIndices[i] % polygon.vertex_count); // Ensure that we've written enough ears - ASSERT(earIndices.Count() == 3 * (polygon.vertex_count - 2)); - return earIndices.Count(); + const int32 indexCount = 3 * (polygon.vertex_count - 2); + if (indexCount != earIndices.Count()) + { + LOG(Error, "Invalid amount of vertices after triangulation. Expected {} but got {}. Use proper mesh triangulation when exporting source asset to the engine.", earIndices.Count(), indexCount); + } + return indexCount; } bool ProcessMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& mesh, String& errorMsg, int partitionIndex) @@ -729,7 +733,7 @@ bool ProcessMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* mesh.Positions.Get()[j] = ToFloat3(positions.get(triangulatedIndices[j])); numIndicesTotal += numIndices; } - ASSERT(numIndicesTotal == vertexCount); + ASSERT_LOW_LAYER(numIndicesTotal == vertexCount); } // Indices (dummy index buffer) From 094a6562b87ce661fc549a5b53efe5abc5b40545 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 3 Jul 2025 10:18:51 +0200 Subject: [PATCH 091/122] Refactor `ProbesRenderer` --- Source/Editor/Managed/ManagedEditor.cpp | 9 +- Source/Engine/Renderer/ProbesRenderer.cpp | 338 ++++++++++------------ Source/Engine/Renderer/ProbesRenderer.h | 70 +---- 3 files changed, 173 insertions(+), 244 deletions(-) diff --git a/Source/Editor/Managed/ManagedEditor.cpp b/Source/Editor/Managed/ManagedEditor.cpp index e270d08f8..c020fe7ce 100644 --- a/Source/Editor/Managed/ManagedEditor.cpp +++ b/Source/Editor/Managed/ManagedEditor.cpp @@ -13,6 +13,7 @@ #include "Engine/Scripting/Internal/MainThreadManagedInvokeAction.h" #include "Engine/Content/Assets/VisualScript.h" #include "Engine/Content/Content.h" +#include "Engine/Level/Actor.h" #include "Engine/CSG/CSGBuilder.h" #include "Engine/Engine/CommandLine.h" #include "Engine/Renderer/ProbesRenderer.h" @@ -74,7 +75,7 @@ void OnLightmapsBuildFinished(bool failed) OnLightmapsBake(ShadowsOfMordor::BuildProgressStep::GenerateLightmapCharts, 0, 0, false); } -void OnBakeEvent(bool started, const ProbesRenderer::Entry& e) +void OnBakeEvent(bool started, Actor* e) { if (Internal_EnvProbeBake == nullptr) { @@ -82,7 +83,7 @@ void OnBakeEvent(bool started, const ProbesRenderer::Entry& e) ASSERT(Internal_EnvProbeBake); } - MObject* probeObj = e.Actor ? e.Actor->GetManagedInstance() : nullptr; + MObject* probeObj = e ? e->GetManagedInstance() : nullptr; MainThreadManagedInvokeAction::ParamsBuilder params; params.AddParam(started); @@ -90,12 +91,12 @@ void OnBakeEvent(bool started, const ProbesRenderer::Entry& e) MainThreadManagedInvokeAction::Invoke(Internal_EnvProbeBake, params); } -void OnRegisterBake(const ProbesRenderer::Entry& e) +void OnRegisterBake(Actor* e) { OnBakeEvent(true, e); } -void OnFinishBake(const ProbesRenderer::Entry& e) +void OnFinishBake(Actor* e) { OnBakeEvent(false, e); } diff --git a/Source/Engine/Renderer/ProbesRenderer.cpp b/Source/Engine/Renderer/ProbesRenderer.cpp index ee72afe72..ac19cd309 100644 --- a/Source/Engine/Renderer/ProbesRenderer.cpp +++ b/Source/Engine/Renderer/ProbesRenderer.cpp @@ -4,8 +4,8 @@ #include "Renderer.h" #include "ReflectionsPass.h" #include "Engine/Core/Config/GraphicsSettings.h" -#include "Engine/Threading/ThreadPoolTask.h" -#include "Engine/Content/Content.h" +#include "Engine/Engine/Time.h" +#include "Engine/Engine/Engine.h" #include "Engine/Engine/EngineService.h" #include "Engine/Level/Actors/PointLight.h" #include "Engine/Level/Actors/EnvironmentProbe.h" @@ -14,28 +14,49 @@ #include "Engine/Level/LargeWorlds.h" #include "Engine/ContentExporters/AssetExporters.h" #include "Engine/Serialization/FileWriteStream.h" -#include "Engine/Engine/Time.h" +#include "Engine/Content/Content.h" #include "Engine/Content/Assets/Shader.h" #include "Engine/Content/AssetReference.h" #include "Engine/Graphics/Graphics.h" +#include "Engine/Graphics/PixelFormat.h" #include "Engine/Graphics/GPUContext.h" #include "Engine/Graphics/Textures/GPUTexture.h" #include "Engine/Graphics/Textures/TextureData.h" #include "Engine/Graphics/RenderTask.h" -#include "Engine/Engine/Engine.h" +#include "Engine/Scripting/ScriptingObjectReference.h" +#include "Engine/Threading/ThreadPoolTask.h" -/// -/// Custom task called after downloading probe texture data to save it. -/// +// Amount of frames to wait for data from probe update job +#define PROBES_RENDERER_LATENCY_FRAMES 1 + +struct ProbeEntry +{ + enum class Types + { + Invalid = 0, + EnvProbe = 1, + SkyLight = 2, + }; + + Types Type = Types::Invalid; + float Timeout = 0.0f; + ScriptingObjectReference Actor; + + bool UseTextureData() const; + int32 GetResolution() const; + PixelFormat GetFormat() const; +}; + +// Custom task called after downloading probe texture data to save it. class DownloadProbeTask : public ThreadPoolTask { private: GPUTexture* _texture; TextureData _data; - ProbesRenderer::Entry _entry; + ProbeEntry _entry; public: - DownloadProbeTask(GPUTexture* target, const ProbesRenderer::Entry& entry) + DownloadProbeTask(GPUTexture* target, const ProbeEntry& entry) : _texture(target) , _entry(entry) { @@ -48,23 +69,23 @@ public: bool Run() override { - if (_entry.Type == ProbesRenderer::EntryType::EnvProbe) + Actor* actor = _entry.Actor.Get(); + if (_entry.Type == ProbeEntry::Types::EnvProbe) { - if (_entry.Actor) - ((EnvironmentProbe*)_entry.Actor.Get())->SetProbeData(_data); + if (actor) + ((EnvironmentProbe*)actor)->SetProbeData(_data); } - else if (_entry.Type == ProbesRenderer::EntryType::SkyLight) + else if (_entry.Type == ProbeEntry::Types::SkyLight) { - if (_entry.Actor) - ((SkyLight*)_entry.Actor.Get())->SetProbeData(_data); + if (actor) + ((SkyLight*)actor)->SetProbeData(_data); } else { return true; } - ProbesRenderer::OnFinishBake(_entry); - + ProbesRenderer::OnFinishBake(actor); return false; } }; @@ -75,14 +96,17 @@ GPU_CB_STRUCT(Data { float SourceMipIndex; }); -namespace ProbesRendererImpl +class ProbesRendererService : public EngineService { - TimeSpan _lastProbeUpdate(0); - Array _probesToBake; +private: + bool _initDone = false; + bool _initFailed = false; - ProbesRenderer::Entry _current; + TimeSpan _lastProbeUpdate = TimeSpan(0); + Array _probesToBake; + + ProbeEntry _current; - bool _isReady = false; AssetReference _shader; GPUPipelineState* _psFilterFace = nullptr; SceneRenderTask* _task = nullptr; @@ -92,91 +116,52 @@ namespace ProbesRendererImpl GPUTexture* _skySHIrradianceMap = nullptr; uint64 _updateFrameNumber = 0; - FORCE_INLINE bool isUpdateSynced() - { - return _updateFrameNumber > 0 && _updateFrameNumber + PROBES_RENDERER_LATENCY_FRAMES <= Engine::FrameCount; - } -} - -using namespace ProbesRendererImpl; - -class ProbesRendererService : public EngineService -{ public: ProbesRendererService() : EngineService(TEXT("Probes Renderer"), 500) { } + bool LazyInit(); void Update() override; void Dispose() override; + + void Bake(const ProbeEntry& e); + void OnRender(RenderTask* task, GPUContext* context); }; ProbesRendererService ProbesRendererServiceInstance; -TimeSpan ProbesRenderer::ProbesUpdatedBreak(0, 0, 0, 0, 500); -TimeSpan ProbesRenderer::ProbesReleaseDataTime(0, 0, 0, 60); -Delegate ProbesRenderer::OnRegisterBake; -Delegate ProbesRenderer::OnFinishBake; +TimeSpan ProbesRenderer::UpdateDelay(0, 0, 0, 0, 100); +TimeSpan ProbesRenderer::ReleaseTimeout(0, 0, 0, 30); +Delegate ProbesRenderer::OnRegisterBake; +Delegate ProbesRenderer::OnFinishBake; void ProbesRenderer::Bake(EnvironmentProbe* probe, float timeout) { if (!probe || probe->IsUsingCustomProbe()) return; - - // Check if already registered for bake - for (int32 i = 0; i < _probesToBake.Count(); i++) - { - auto& p = _probesToBake[i]; - if (p.Type == EntryType::EnvProbe && p.Actor == probe) - { - p.Timeout = timeout; - return; - } - } - - // Register probe - Entry e; - e.Type = EntryType::EnvProbe; + ProbeEntry e; + e.Type = ProbeEntry::Types::EnvProbe; e.Actor = probe; e.Timeout = timeout; - _probesToBake.Add(e); - - // Fire event - if (e.UseTextureData()) - OnRegisterBake(e); + ProbesRendererServiceInstance.Bake(e); } void ProbesRenderer::Bake(SkyLight* probe, float timeout) { - ASSERT(probe && dynamic_cast(probe)); - - // Check if already registered for bake - for (int32 i = 0; i < _probesToBake.Count(); i++) - { - auto& p = _probesToBake[i]; - if (p.Type == EntryType::SkyLight && p.Actor == probe) - { - p.Timeout = timeout; - return; - } - } - - // Register probe - Entry e; - e.Type = EntryType::SkyLight; + if (!probe) + return; + ProbeEntry e; + e.Type = ProbeEntry::Types::SkyLight; e.Actor = probe; e.Timeout = timeout; - _probesToBake.Add(e); - - // Fire event - if (e.UseTextureData()) - OnRegisterBake(e); + ProbesRendererServiceInstance.Bake(e); } -bool ProbesRenderer::Entry::UseTextureData() const +bool ProbeEntry::UseTextureData() const { - if (Type == EntryType::EnvProbe && Actor) + if (Type == Types::EnvProbe && Actor) { switch (Actor.As()->UpdateMode) { @@ -187,12 +172,12 @@ bool ProbesRenderer::Entry::UseTextureData() const return true; } -int32 ProbesRenderer::Entry::GetResolution() const +int32 ProbeEntry::GetResolution() const { auto resolution = ProbeCubemapResolution::UseGraphicsSettings; - if (Type == EntryType::EnvProbe && Actor) + if (Type == Types::EnvProbe && Actor) resolution = ((EnvironmentProbe*)Actor.Get())->CubemapResolution; - else if (Type == EntryType::SkyLight) + else if (Type == Types::SkyLight) resolution = ProbeCubemapResolution::_128; if (resolution == ProbeCubemapResolution::UseGraphicsSettings) resolution = GraphicsSettings::Get()->DefaultProbeResolution; @@ -201,116 +186,83 @@ int32 ProbesRenderer::Entry::GetResolution() const return (int32)resolution; } -PixelFormat ProbesRenderer::Entry::GetFormat() const +PixelFormat ProbeEntry::GetFormat() const { return GraphicsSettings::Get()->UseHDRProbes ? PixelFormat::R11G11B10_Float : PixelFormat::R8G8B8A8_UNorm; } -int32 ProbesRenderer::GetBakeQueueSize() +bool ProbesRendererService::LazyInit() { - return _probesToBake.Count(); -} - -bool ProbesRenderer::HasReadyResources() -{ - return _isReady && _shader->IsLoaded(); -} - -bool ProbesRenderer::Init() -{ - if (_isReady) + if (_initDone || _initFailed) return false; // Load shader if (_shader == nullptr) { _shader = Content::LoadAsyncInternal(TEXT("Shaders/ProbesFilter")); - if (_shader == nullptr) - return true; + _initFailed = _shader == nullptr; + if (_initFailed) + return false; } if (!_shader->IsLoaded()) - return false; + return true; const auto shader = _shader->GetShader(); CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); // Create pipeline stages _psFilterFace = GPUDevice::Instance->CreatePipelineState(); - GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; + auto psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; { psDesc.PS = shader->GetPS("PS_FilterFace"); - if (_psFilterFace->Init(psDesc)) - return true; + _initFailed |= _psFilterFace->Init(psDesc); } // Init rendering pipeline - _output = GPUDevice::Instance->CreateTexture(TEXT("Output")); + _output = GPUDevice::Instance->CreateTexture(TEXT("ProbesRenderer.Output")); const int32 probeResolution = _current.GetResolution(); const PixelFormat probeFormat = _current.GetFormat(); - if (_output->Init(GPUTextureDescription::New2D(probeResolution, probeResolution, probeFormat))) - return true; + _initFailed |= _output->Init(GPUTextureDescription::New2D(probeResolution, probeResolution, probeFormat)); _task = New(); auto task = _task; + task->Order = -100; // Run before main view rendering (realtime probes will get smaller latency) task->Enabled = false; task->IsCustomRendering = true; task->Output = _output; auto& view = task->View; view.Flags = - ViewFlags::AO | - ViewFlags::GI | - ViewFlags::DirectionalLights | - ViewFlags::PointLights | - ViewFlags::SpotLights | - ViewFlags::SkyLights | - ViewFlags::Decals | - ViewFlags::Shadows | - ViewFlags::Sky | - ViewFlags::Fog; + ViewFlags::AO | + ViewFlags::GI | + ViewFlags::DirectionalLights | + ViewFlags::PointLights | + ViewFlags::SpotLights | + ViewFlags::SkyLights | + ViewFlags::Decals | + ViewFlags::Shadows | + ViewFlags::Sky | + ViewFlags::Fog; view.Mode = ViewMode::NoPostFx; view.IsOfflinePass = true; view.IsSingleFrame = true; view.StaticFlagsMask = view.StaticFlagsCompare = StaticFlags::ReflectionProbe; - view.MaxShadowsQuality = Quality::Low; task->IsCameraCut = true; task->Resize(probeResolution, probeResolution); - task->Render.Bind(OnRender); + task->Render.Bind(this); // Init render targets - _probe = GPUDevice::Instance->CreateTexture(TEXT("ProbesUpdate.Probe")); - if (_probe->Init(GPUTextureDescription::NewCube(probeResolution, probeFormat, GPUTextureFlags::ShaderResource | GPUTextureFlags::RenderTarget | GPUTextureFlags::PerMipViews, 0))) - return true; - _tmpFace = GPUDevice::Instance->CreateTexture(TEXT("ProbesUpdate.TmpFace")); - if (_tmpFace->Init(GPUTextureDescription::New2D(probeResolution, probeResolution, 0, probeFormat, GPUTextureFlags::ShaderResource | GPUTextureFlags::RenderTarget | GPUTextureFlags::PerMipViews))) - return true; + _probe = GPUDevice::Instance->CreateTexture(TEXT("ProbesRenderer.Probe")); + _initFailed |= _probe->Init(GPUTextureDescription::NewCube(probeResolution, probeFormat, GPUTextureFlags::ShaderResource | GPUTextureFlags::RenderTarget | GPUTextureFlags::PerMipViews, 0)); + _tmpFace = GPUDevice::Instance->CreateTexture(TEXT("ProbesRenderer.TmpFace")); + _initFailed |= _tmpFace->Init(GPUTextureDescription::New2D(probeResolution, probeResolution, 0, probeFormat, GPUTextureFlags::ShaderResource | GPUTextureFlags::RenderTarget | GPUTextureFlags::PerMipViews)); // Mark as ready - _isReady = true; + _initDone = true; return false; } -void ProbesRenderer::Release() -{ - if (!_isReady) - return; - ASSERT(_updateFrameNumber == 0); - - // Release GPU data - if (_output) - _output->ReleaseGPU(); - - // Release data - SAFE_DELETE_GPU_RESOURCE(_psFilterFace); - _shader = nullptr; - SAFE_DELETE_GPU_RESOURCE(_output); - SAFE_DELETE(_task); - SAFE_DELETE_GPU_RESOURCE(_probe); - SAFE_DELETE_GPU_RESOURCE(_tmpFace); - SAFE_DELETE_GPU_RESOURCE(_skySHIrradianceMap); - - _isReady = false; -} - void ProbesRendererService::Update() { + PROFILE_MEM(Graphics); + // Calculate time delta since last update auto timeNow = Time::Update.UnscaledTime; auto timeSinceUpdate = timeNow - _lastProbeUpdate; @@ -321,35 +273,32 @@ void ProbesRendererService::Update() } // Check if render job is done - if (isUpdateSynced()) + if (_updateFrameNumber > 0 && _updateFrameNumber + PROBES_RENDERER_LATENCY_FRAMES <= Engine::FrameCount) { // Create async job to gather probe data from the GPU GPUTexture* texture = nullptr; switch (_current.Type) { - case ProbesRenderer::EntryType::SkyLight: - case ProbesRenderer::EntryType::EnvProbe: + case ProbeEntry::Types::SkyLight: + case ProbeEntry::Types::EnvProbe: texture = _probe; break; } ASSERT(texture && _current.UseTextureData()); auto taskB = New(texture, _current); auto taskA = texture->DownloadDataAsync(taskB->GetData()); - if (taskA == nullptr) - { - LOG(Fatal, "Failed to create async tsk to download env probe texture data fro mthe GPU."); - } + ASSERT(taskA); taskA->ContinueWith(taskB); taskA->Start(); // Clear flag _updateFrameNumber = 0; - _current.Type = ProbesRenderer::EntryType::Invalid; + _current.Type = ProbeEntry::Types::Invalid; } - else if (_current.Type == ProbesRenderer::EntryType::Invalid) + else if (_current.Type == ProbeEntry::Types::Invalid && timeSinceUpdate > ProbesRenderer::UpdateDelay) { int32 firstValidEntryIndex = -1; - auto dt = (float)Time::Update.UnscaledDeltaTime.GetTotalSeconds(); + auto dt = Time::Update.UnscaledDeltaTime.GetTotalSeconds(); for (int32 i = 0; i < _probesToBake.Count(); i++) { auto& e = _probesToBake[i]; @@ -362,40 +311,65 @@ void ProbesRendererService::Update() } // Check if need to update probe - if (firstValidEntryIndex >= 0 && timeSinceUpdate > ProbesRenderer::ProbesUpdatedBreak) + if (firstValidEntryIndex >= 0 && timeSinceUpdate > ProbesRenderer::UpdateDelay) { - // Init service - if (ProbesRenderer::Init()) - { - LOG(Fatal, "Cannot setup Probes Renderer!"); - } - if (ProbesRenderer::HasReadyResources() == false) - return; + if (LazyInit()) + return; // Shader is not yet loaded so try the next frame // Mark probe to update _current = _probesToBake[firstValidEntryIndex]; _probesToBake.RemoveAtKeepOrder(firstValidEntryIndex); _task->Enabled = true; _updateFrameNumber = 0; - - // Store time of the last probe update _lastProbeUpdate = timeNow; } // Check if need to release data - else if (_isReady && timeSinceUpdate > ProbesRenderer::ProbesReleaseDataTime) + else if (_initDone && timeSinceUpdate > ProbesRenderer::ReleaseTimeout) { - // Release service - ProbesRenderer::Release(); + // Release resources + Dispose(); } } } void ProbesRendererService::Dispose() { - ProbesRenderer::Release(); + if (!_initDone && !_initFailed) + return; + ASSERT(_updateFrameNumber == 0); + if (_output) + _output->ReleaseGPU(); + SAFE_DELETE_GPU_RESOURCE(_psFilterFace); + SAFE_DELETE_GPU_RESOURCE(_output); + SAFE_DELETE_GPU_RESOURCE(_probe); + SAFE_DELETE_GPU_RESOURCE(_tmpFace); + SAFE_DELETE_GPU_RESOURCE(_skySHIrradianceMap); + SAFE_DELETE(_task); + _shader = nullptr; + _initDone = false; + _initFailed = false; } -bool fixFarPlaneTreeExecute(Actor* actor, const Vector3& position, float& farPlane) +void ProbesRendererService::Bake(const ProbeEntry& e) +{ + // Check if already registered for bake + for (ProbeEntry& p : _probesToBake) + { + if (p.Type == e.Type && p.Actor == e.Actor) + { + p.Timeout = e.Timeout; + return; + } + } + + _probesToBake.Add(e); + + // Fire event + if (e.UseTextureData()) + ProbesRenderer::OnRegisterBake(e.Actor); +} + +static bool FixFarPlane(Actor* actor, const Vector3& position, float& farPlane) { if (auto* pointLight = dynamic_cast(actor)) { @@ -408,20 +382,19 @@ bool fixFarPlaneTreeExecute(Actor* actor, const Vector3& position, float& farPla return true; } -void ProbesRenderer::OnRender(RenderTask* task, GPUContext* context) +void ProbesRendererService::OnRender(RenderTask* task, GPUContext* context) { - ASSERT(_current.Type != EntryType::Invalid && _updateFrameNumber == 0); switch (_current.Type) { - case EntryType::EnvProbe: - case EntryType::SkyLight: + case ProbeEntry::Types::EnvProbe: + case ProbeEntry::Types::SkyLight: { if (_current.Actor == nullptr) { // Probe has been unlinked (or deleted) _task->Enabled = false; _updateFrameNumber = 0; - _current.Type = EntryType::Invalid; + _current.Type = ProbeEntry::Types::Invalid; return; } break; @@ -430,7 +403,7 @@ void ProbesRenderer::OnRender(RenderTask* task, GPUContext* context) // Canceled return; } - + ASSERT(_updateFrameNumber == 0); auto shader = _shader->GetShader(); PROFILE_GPU("Render Probe"); @@ -438,7 +411,7 @@ void ProbesRenderer::OnRender(RenderTask* task, GPUContext* context) float customCullingNear = -1; const int32 probeResolution = _current.GetResolution(); const PixelFormat probeFormat = _current.GetFormat(); - if (_current.Type == EntryType::EnvProbe) + if (_current.Type == ProbeEntry::Types::EnvProbe) { auto envProbe = (EnvironmentProbe*)_current.Actor.Get(); Vector3 position = envProbe->GetPosition(); @@ -448,14 +421,14 @@ void ProbesRenderer::OnRender(RenderTask* task, GPUContext* context) // Adjust far plane distance float farPlane = Math::Max(radius, nearPlane + 100.0f); farPlane *= farPlane < 10000 ? 10 : 4; - Function f(&fixFarPlaneTreeExecute); + Function f(&FixFarPlane); SceneQuery::TreeExecute(f, position, farPlane); // Setup view LargeWorlds::UpdateOrigin(_task->View.Origin, position); _task->View.SetUpCube(nearPlane, farPlane, position - _task->View.Origin); } - else if (_current.Type == EntryType::SkyLight) + else if (_current.Type == ProbeEntry::Types::SkyLight) { auto skyLight = (SkyLight*)_current.Actor.Get(); Vector3 position = skyLight->GetPosition(); @@ -481,6 +454,9 @@ void ProbesRenderer::OnRender(RenderTask* task, GPUContext* context) const bool isActorActive = _current.Actor->GetIsActive(); _current.Actor->SetIsActive(false); + // Lower quality when rendering probes in-game to gain performance + _task->View.MaxShadowsQuality = Engine::IsPlayMode() ? Quality::Low : Quality::Ultra; + // Render scene for all faces for (int32 faceIndex = 0; faceIndex < 6; faceIndex++) { @@ -556,13 +532,13 @@ void ProbesRenderer::OnRender(RenderTask* task, GPUContext* context) // Real-time probes don't use TextureData (for streaming) but copy generated probe directly to GPU memory if (!_current.UseTextureData()) { - if (_current.Type == EntryType::EnvProbe && _current.Actor) + if (_current.Type == ProbeEntry::Types::EnvProbe && _current.Actor) { _current.Actor.As()->SetProbeData(context, _probe); } // Clear flag _updateFrameNumber = 0; - _current.Type = EntryType::Invalid; + _current.Type = ProbeEntry::Types::Invalid; } } diff --git a/Source/Engine/Renderer/ProbesRenderer.h b/Source/Engine/Renderer/ProbesRenderer.h index 5c4e011e4..0e2007a37 100644 --- a/Source/Engine/Renderer/ProbesRenderer.h +++ b/Source/Engine/Renderer/ProbesRenderer.h @@ -2,75 +2,30 @@ #pragma once -#include "Engine/Graphics/PixelFormat.h" -#include "Engine/Scripting/ScriptingObjectReference.h" -#include "Engine/Level/Actor.h" +#include "Engine/Core/Delegate.h" +#include "Engine/Core/Types/TimeSpan.h" -// Amount of frames to wait for data from probe update job -#define PROBES_RENDERER_LATENCY_FRAMES 1 - -class EnvironmentProbe; -class SkyLight; -class RenderTask; +class Actor; /// /// Probes rendering service /// class ProbesRenderer { -public: - enum class EntryType - { - Invalid = 0, - EnvProbe = 1, - SkyLight = 2, - }; - - struct Entry - { - EntryType Type = EntryType::Invalid; - ScriptingObjectReference Actor; - float Timeout = 0.0f; - - bool UseTextureData() const; - int32 GetResolution() const; - PixelFormat GetFormat() const; - }; - public: /// - /// Minimum amount of time between two updated of probes + /// Time delay between probe updates. Can be used to improve performance by rendering probes less often. /// - static TimeSpan ProbesUpdatedBreak; + static TimeSpan UpdateDelay; /// - /// Time after last probe update when probes updating content will be released + /// Timeout after the last probe rendered when resources used to render it should be released. /// - static TimeSpan ProbesReleaseDataTime; + static TimeSpan ReleaseTimeout; - int32 GetBakeQueueSize(); + static Delegate OnRegisterBake; - static Delegate OnRegisterBake; - - static Delegate OnFinishBake; - -public: - /// - /// Checks if resources are ready to render probes (shaders or textures may be during loading). - /// - /// True if is ready, otherwise false. - static bool HasReadyResources(); - - /// - /// Init probes content - /// - /// True if cannot init service - static bool Init(); - - /// - /// Release probes content - /// - static void Release(); + static Delegate OnFinishBake; public: /// @@ -78,15 +33,12 @@ public: /// /// Probe to bake /// Timeout in seconds left to bake it. - static void Bake(EnvironmentProbe* probe, float timeout = 0); + static void Bake(class EnvironmentProbe* probe, float timeout = 0); /// /// Register probe to baking service. /// /// Probe to bake /// Timeout in seconds left to bake it. - static void Bake(SkyLight* probe, float timeout = 0); - -private: - static void OnRender(RenderTask* task, GPUContext* context); + static void Bake(class SkyLight* probe, float timeout = 0); }; From 33e58c12cbea2acd04e4de45fcb74fed89cd2241 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 3 Jul 2025 11:43:56 +0200 Subject: [PATCH 092/122] Optimize `ProbesRenderer` to use time-slicing for cubemap faces rendering and filtering --- Source/Engine/Renderer/ProbesRenderer.cpp | 101 +++++++++++++--------- Source/Engine/Renderer/ProbesRenderer.h | 5 ++ 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/Source/Engine/Renderer/ProbesRenderer.cpp b/Source/Engine/Renderer/ProbesRenderer.cpp index ac19cd309..ae94385e2 100644 --- a/Source/Engine/Renderer/ProbesRenderer.cpp +++ b/Source/Engine/Renderer/ProbesRenderer.cpp @@ -106,6 +106,8 @@ private: Array _probesToBake; ProbeEntry _current; + int32 _workStep; + float _customCullingNear; AssetReference _shader; GPUPipelineState* _psFilterFace = nullptr; @@ -134,6 +136,7 @@ ProbesRendererService ProbesRendererServiceInstance; TimeSpan ProbesRenderer::UpdateDelay(0, 0, 0, 0, 100); TimeSpan ProbesRenderer::ReleaseTimeout(0, 0, 0, 30); +int32 ProbesRenderer::MaxWorkPerFrame = 1; Delegate ProbesRenderer::OnRegisterBake; Delegate ProbesRenderer::OnFinishBake; @@ -293,6 +296,7 @@ void ProbesRendererService::Update() // Clear flag _updateFrameNumber = 0; + _workStep = 0; _current.Type = ProbeEntry::Types::Invalid; } else if (_current.Type == ProbeEntry::Types::Invalid && timeSinceUpdate > ProbesRenderer::UpdateDelay) @@ -321,6 +325,7 @@ void ProbesRendererService::Update() _probesToBake.RemoveAtKeepOrder(firstValidEntryIndex); _task->Enabled = true; _updateFrameNumber = 0; + _workStep = 0; _lastProbeUpdate = timeNow; } // Check if need to release data @@ -408,72 +413,76 @@ void ProbesRendererService::OnRender(RenderTask* task, GPUContext* context) PROFILE_GPU("Render Probe"); // Init - float customCullingNear = -1; const int32 probeResolution = _current.GetResolution(); const PixelFormat probeFormat = _current.GetFormat(); - if (_current.Type == ProbeEntry::Types::EnvProbe) + if (_workStep == 0) { - auto envProbe = (EnvironmentProbe*)_current.Actor.Get(); - Vector3 position = envProbe->GetPosition(); - float radius = envProbe->GetScaledRadius(); - float nearPlane = Math::Max(0.1f, envProbe->CaptureNearPlane); + _customCullingNear = -1; + if (_current.Type == ProbeEntry::Types::EnvProbe) + { + auto envProbe = (EnvironmentProbe*)_current.Actor.Get(); + Vector3 position = envProbe->GetPosition(); + float radius = envProbe->GetScaledRadius(); + float nearPlane = Math::Max(0.1f, envProbe->CaptureNearPlane); - // Adjust far plane distance - float farPlane = Math::Max(radius, nearPlane + 100.0f); - farPlane *= farPlane < 10000 ? 10 : 4; - Function f(&FixFarPlane); - SceneQuery::TreeExecute(f, position, farPlane); + // Adjust far plane distance + float farPlane = Math::Max(radius, nearPlane + 100.0f); + farPlane *= farPlane < 10000 ? 10 : 4; + Function f(&FixFarPlane); + SceneQuery::TreeExecute(f, position, farPlane); - // Setup view - LargeWorlds::UpdateOrigin(_task->View.Origin, position); - _task->View.SetUpCube(nearPlane, farPlane, position - _task->View.Origin); + // Setup view + LargeWorlds::UpdateOrigin(_task->View.Origin, position); + _task->View.SetUpCube(nearPlane, farPlane, position - _task->View.Origin); + } + else if (_current.Type == ProbeEntry::Types::SkyLight) + { + auto skyLight = (SkyLight*)_current.Actor.Get(); + Vector3 position = skyLight->GetPosition(); + float nearPlane = 10.0f; + float farPlane = Math::Max(nearPlane + 1000.0f, skyLight->SkyDistanceThreshold * 2.0f); + _customCullingNear = skyLight->SkyDistanceThreshold; + + // Setup view + LargeWorlds::UpdateOrigin(_task->View.Origin, position); + _task->View.SetUpCube(nearPlane, farPlane, position - _task->View.Origin); + } + + // Resize buffers + bool resizeFailed = _output->Resize(probeResolution, probeResolution, probeFormat); + resizeFailed |= _probe->Resize(probeResolution, probeResolution, probeFormat); + resizeFailed |= _tmpFace->Resize(probeResolution, probeResolution, probeFormat); + resizeFailed |= _task->Resize(probeResolution, probeResolution); + if (resizeFailed) + LOG(Error, "Failed to resize probe"); } - else if (_current.Type == ProbeEntry::Types::SkyLight) - { - auto skyLight = (SkyLight*)_current.Actor.Get(); - Vector3 position = skyLight->GetPosition(); - float nearPlane = 10.0f; - float farPlane = Math::Max(nearPlane + 1000.0f, skyLight->SkyDistanceThreshold * 2.0f); - customCullingNear = skyLight->SkyDistanceThreshold; - - // Setup view - LargeWorlds::UpdateOrigin(_task->View.Origin, position); - _task->View.SetUpCube(nearPlane, farPlane, position - _task->View.Origin); - } - _task->CameraCut(); - - // Resize buffers - bool resizeFailed = _output->Resize(probeResolution, probeResolution, probeFormat); - resizeFailed |= _probe->Resize(probeResolution, probeResolution, probeFormat); - resizeFailed |= _tmpFace->Resize(probeResolution, probeResolution, probeFormat); - resizeFailed |= _task->Resize(probeResolution, probeResolution); - if (resizeFailed) - LOG(Error, "Failed to resize probe"); // Disable actor during baking (it cannot influence own results) const bool isActorActive = _current.Actor->GetIsActive(); _current.Actor->SetIsActive(false); // Lower quality when rendering probes in-game to gain performance - _task->View.MaxShadowsQuality = Engine::IsPlayMode() ? Quality::Low : Quality::Ultra; + _task->View.MaxShadowsQuality = Engine::IsPlayMode() || probeResolution <= 128 ? Quality::Low : Quality::Ultra; // Render scene for all faces - for (int32 faceIndex = 0; faceIndex < 6; faceIndex++) + int32 workLeft = ProbesRenderer::MaxWorkPerFrame; + const int32 lastFace = Math::Min(_workStep + workLeft, 6); + for (int32 faceIndex = _workStep; faceIndex < lastFace; faceIndex++) { + _task->CameraCut(); _task->View.SetFace(faceIndex); // Handle custom frustum for the culling (used to skip objects near the camera) - if (customCullingNear > 0) + if (_customCullingNear > 0) { Matrix p; - Matrix::PerspectiveFov(PI_OVER_2, 1.0f, customCullingNear, _task->View.Far, p); + Matrix::PerspectiveFov(PI_OVER_2, 1.0f, _customCullingNear, _task->View.Far, p); _task->View.CullingFrustum.SetMatrix(_task->View.View, p); } // Render frame Renderer::Render(_task); context->ClearState(); - _task->CameraCut(); // Copy frame to cube face { @@ -483,12 +492,17 @@ void ProbesRendererService::OnRender(RenderTask* task, GPUContext* context) context->Draw(_output->View()); context->ResetRenderTarget(); } + + // Move to the next face + _workStep++; + workLeft--; } // Enable actor back _current.Actor->SetIsActive(isActorActive); // Filter all lower mip levels + if (workLeft > 0) { PROFILE_GPU("Filtering"); Data data; @@ -520,11 +534,18 @@ void ProbesRendererService::OnRender(RenderTask* task, GPUContext* context) context->Draw(_tmpFace->View(0, mipIndex)); } } + + // End + workLeft--; + _workStep++; } // Cleanup context->ClearState(); + if (_workStep < 7) + return; // Continue rendering next frame + // Mark as rendered _updateFrameNumber = Engine::FrameCount; _task->Enabled = false; diff --git a/Source/Engine/Renderer/ProbesRenderer.h b/Source/Engine/Renderer/ProbesRenderer.h index 0e2007a37..73d8b4132 100644 --- a/Source/Engine/Renderer/ProbesRenderer.h +++ b/Source/Engine/Renderer/ProbesRenderer.h @@ -23,6 +23,11 @@ public: /// static TimeSpan ReleaseTimeout; + /// + /// Maximum amount of cubemap faces or filtering passes that can be performed per-frame (in total). Set it to 7 to perform whole cubemap capture within a single frame, lower values spread the work across multiple frames. + /// + static int32 MaxWorkPerFrame; + static Delegate OnRegisterBake; static Delegate OnFinishBake; From a138c6b062bf1a0fbf3610add18a6f7f581c5ac7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 3 Jul 2025 11:45:12 +0200 Subject: [PATCH 093/122] Optimize environment probes filtering shader --- Content/Shaders/ProbesFilter.flax | 4 +- Source/Engine/Renderer/ProbesRenderer.cpp | 48 +++++++++++++++++------ Source/Shaders/ProbesFilter.shader | 8 ++-- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/Content/Shaders/ProbesFilter.flax b/Content/Shaders/ProbesFilter.flax index 0f853c5b4..679eac27b 100644 --- a/Content/Shaders/ProbesFilter.flax +++ b/Content/Shaders/ProbesFilter.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0249696b525cd59825ab3c0ce38bd612f93cf4be1f88fb49bfcecaac6e9ab34 -size 2022 +oid sha256:bbe90799accc93fabdc900df37bf762132037eeaff17f5731f379b6b3d017d2b +size 2033 diff --git a/Source/Engine/Renderer/ProbesRenderer.cpp b/Source/Engine/Renderer/ProbesRenderer.cpp index ae94385e2..9eb5f3937 100644 --- a/Source/Engine/Renderer/ProbesRenderer.cpp +++ b/Source/Engine/Renderer/ProbesRenderer.cpp @@ -125,11 +125,21 @@ public: } bool LazyInit(); + bool InitShader(); void Update() override; void Dispose() override; - void Bake(const ProbeEntry& e); + +private: void OnRender(RenderTask* task, GPUContext* context); +#if COMPILE_WITH_DEV_ENV + bool _initShader = false; + void OnShaderReloading(Asset* obj) + { + _initShader = true; + SAFE_DELETE_GPU_RESOURCE(_psFilterFace); + } +#endif }; ProbesRendererService ProbesRendererServiceInstance; @@ -206,19 +216,13 @@ bool ProbesRendererService::LazyInit() _initFailed = _shader == nullptr; if (_initFailed) return false; +#if COMPILE_WITH_DEV_ENV + _shader->OnReloading.Bind(this); +#endif } if (!_shader->IsLoaded()) return true; - const auto shader = _shader->GetShader(); - CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); - - // Create pipeline stages - _psFilterFace = GPUDevice::Instance->CreatePipelineState(); - auto psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; - { - psDesc.PS = shader->GetPS("PS_FilterFace"); - _initFailed |= _psFilterFace->Init(psDesc); - } + _initFailed |= InitShader(); // Init rendering pipeline _output = GPUDevice::Instance->CreateTexture(TEXT("ProbesRenderer.Output")); @@ -262,6 +266,16 @@ bool ProbesRendererService::LazyInit() return false; } +bool ProbesRendererService::InitShader() +{ + const auto shader = _shader->GetShader(); + CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data); + _psFilterFace = GPUDevice::Instance->CreatePipelineState(); + auto psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle; + psDesc.PS = shader->GetPS("PS_FilterFace"); + return _psFilterFace->Init(psDesc); +} + void ProbesRendererService::Update() { PROFILE_MEM(Graphics); @@ -412,6 +426,18 @@ void ProbesRendererService::OnRender(RenderTask* task, GPUContext* context) auto shader = _shader->GetShader(); PROFILE_GPU("Render Probe"); +#if COMPILE_WITH_DEV_ENV + // handle shader hot-reload + if (_initShader) + { + if (_shader->WaitForLoaded()) + return; + _initShader = false; + if (InitShader()) + return; + } +#endif + // Init const int32 probeResolution = _current.GetResolution(); const PixelFormat probeFormat = _current.GetFormat(); diff --git a/Source/Shaders/ProbesFilter.shader b/Source/Shaders/ProbesFilter.shader index 437d484f5..c64a281f2 100644 --- a/Source/Shaders/ProbesFilter.shader +++ b/Source/Shaders/ProbesFilter.shader @@ -50,18 +50,16 @@ float4 PS_FilterFace(Quad_VS2PS input) : SV_Target float2 uv = input.TexCoord * 2 - 1; float3 cubeCoordinates = UvToCubeMapUv(uv); -#define NUM_FILTER_SAMPLES 512 - float3 N = normalize(cubeCoordinates); float roughness = ProbeRoughnessFromMip(SourceMipIndex); + const uint samplesCount = roughness > 0.1 ? 64 : 32; float4 filteredColor = 0; float weight = 0; - LOOP - for (int i = 0; i < NUM_FILTER_SAMPLES; i++) + for (int i = 0; i < samplesCount; i++) { - float2 E = Hammersley(i, NUM_FILTER_SAMPLES, 0); + float2 E = Hammersley(i, samplesCount, 0); float3 H = TangentToWorld(ImportanceSampleGGX(E, roughness).xyz, N); float3 L = 2 * dot(N, H) * H - N; float NoL = saturate(dot(N, L)); From bf345f13ce573cc8a7aaffe30aa8fae222d1e19a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 3 Jul 2025 13:54:22 +0200 Subject: [PATCH 094/122] Fix reflection probes capture seams on cube face edges due to volumetric fog #3252 --- Source/Engine/Graphics/RenderTask.cpp | 9 ++++++++- Source/Engine/Renderer/ProbesRenderer.cpp | 13 ++++++++++--- Source/Engine/Renderer/RenderSetup.h | 1 + Source/Engine/Renderer/Renderer.cpp | 1 + Source/Engine/Renderer/VolumetricFogPass.cpp | 5 ++--- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Graphics/RenderTask.cpp b/Source/Engine/Graphics/RenderTask.cpp index ecdcd572c..ac969ad2d 100644 --- a/Source/Engine/Graphics/RenderTask.cpp +++ b/Source/Engine/Graphics/RenderTask.cpp @@ -200,12 +200,19 @@ void SceneRenderTask::RemoveGlobalCustomPostFx(PostProcessEffect* fx) void SceneRenderTask::CollectPostFxVolumes(RenderContext& renderContext) { + PROFILE_CPU(); + // Cache WorldPosition used for PostFx volumes blending (RenderView caches it later on) renderContext.View.WorldPosition = renderContext.View.Origin + renderContext.View.Position; if (EnumHasAllFlags(ActorsSource, ActorsSources::Scenes)) { - Level::CollectPostFxVolumes(renderContext); + //ScopeLock lock(Level::ScenesLock); + for (Scene* scene : Level::Scenes) + { + if (scene->IsActiveInHierarchy()) + scene->Rendering.CollectPostFxVolumes(renderContext); + } } if (EnumHasAllFlags(ActorsSource, ActorsSources::CustomActors)) { diff --git a/Source/Engine/Renderer/ProbesRenderer.cpp b/Source/Engine/Renderer/ProbesRenderer.cpp index 9eb5f3937..eaf7a53ca 100644 --- a/Source/Engine/Renderer/ProbesRenderer.cpp +++ b/Source/Engine/Renderer/ProbesRenderer.cpp @@ -2,6 +2,7 @@ #include "ProbesRenderer.h" #include "Renderer.h" +#include "RenderList.h" #include "ReflectionsPass.h" #include "Engine/Core/Config/GraphicsSettings.h" #include "Engine/Engine/Time.h" @@ -17,7 +18,6 @@ #include "Engine/Content/Content.h" #include "Engine/Content/Assets/Shader.h" #include "Engine/Content/AssetReference.h" -#include "Engine/Graphics/Graphics.h" #include "Engine/Graphics/PixelFormat.h" #include "Engine/Graphics/GPUContext.h" #include "Engine/Graphics/Textures/GPUTexture.h" @@ -115,7 +115,6 @@ private: GPUTexture* _output = nullptr; GPUTexture* _probe = nullptr; GPUTexture* _tmpFace = nullptr; - GPUTexture* _skySHIrradianceMap = nullptr; uint64 _updateFrameNumber = 0; public: @@ -132,6 +131,7 @@ public: private: void OnRender(RenderTask* task, GPUContext* context); + void OnSetupRender(RenderContext& renderContext); #if COMPILE_WITH_DEV_ENV bool _initShader = false; void OnShaderReloading(Asset* obj) @@ -234,6 +234,7 @@ bool ProbesRendererService::LazyInit() task->Order = -100; // Run before main view rendering (realtime probes will get smaller latency) task->Enabled = false; task->IsCustomRendering = true; + task->ActorsSource = ActorsSources::ScenesAndCustomActors; task->Output = _output; auto& view = task->View; view.Flags = @@ -254,6 +255,7 @@ bool ProbesRendererService::LazyInit() task->IsCameraCut = true; task->Resize(probeResolution, probeResolution); task->Render.Bind(this); + task->SetupRender.Bind(this); // Init render targets _probe = GPUDevice::Instance->CreateTexture(TEXT("ProbesRenderer.Probe")); @@ -362,7 +364,6 @@ void ProbesRendererService::Dispose() SAFE_DELETE_GPU_RESOURCE(_output); SAFE_DELETE_GPU_RESOURCE(_probe); SAFE_DELETE_GPU_RESOURCE(_tmpFace); - SAFE_DELETE_GPU_RESOURCE(_skySHIrradianceMap); SAFE_DELETE(_task); _shader = nullptr; _initDone = false; @@ -589,3 +590,9 @@ void ProbesRendererService::OnRender(RenderTask* task, GPUContext* context) _current.Type = ProbeEntry::Types::Invalid; } } + +void ProbesRendererService::OnSetupRender(RenderContext& renderContext) +{ + // Disable Volumetric Fog in reflection as it causes seams on cubemap face edges + renderContext.List->Setup.UseVolumetricFog = false; +} diff --git a/Source/Engine/Renderer/RenderSetup.h b/Source/Engine/Renderer/RenderSetup.h index 10377e023..3444f0838 100644 --- a/Source/Engine/Renderer/RenderSetup.h +++ b/Source/Engine/Renderer/RenderSetup.h @@ -14,4 +14,5 @@ struct FLAXENGINE_API RenderSetup bool UseTemporalAAJitter = false; bool UseGlobalSDF = false; bool UseGlobalSurfaceAtlas = false; + bool UseVolumetricFog = false; }; diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp index 87d1d94f1..fd02b133f 100644 --- a/Source/Engine/Renderer/Renderer.cpp +++ b/Source/Engine/Renderer/Renderer.cpp @@ -379,6 +379,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont setup.UseGlobalSDF = (graphicsSettings->EnableGlobalSDF && EnumHasAnyFlags(view.Flags, ViewFlags::GlobalSDF)) || renderContext.View.Mode == ViewMode::GlobalSDF || setup.UseGlobalSurfaceAtlas; + setup.UseVolumetricFog = (view.Flags & ViewFlags::Fog) != ViewFlags::None; // Disable TAA jitter in debug modes switch (renderContext.View.Mode) diff --git a/Source/Engine/Renderer/VolumetricFogPass.cpp b/Source/Engine/Renderer/VolumetricFogPass.cpp index b7e57c2bb..6029b399d 100644 --- a/Source/Engine/Renderer/VolumetricFogPass.cpp +++ b/Source/Engine/Renderer/VolumetricFogPass.cpp @@ -99,7 +99,6 @@ float ComputeZSliceFromDepth(float sceneDepth, const VolumetricFogOptions& optio bool VolumetricFogPass::Init(RenderContext& renderContext, GPUContext* context, VolumetricFogOptions& options) { - auto& view = renderContext.View; const auto fog = renderContext.List->Fog; // Check if already prepared for this frame @@ -111,7 +110,7 @@ bool VolumetricFogPass::Init(RenderContext& renderContext, GPUContext* context, } // Check if skip rendering - if (fog == nullptr || (view.Flags & ViewFlags::Fog) == ViewFlags::None || !_isSupported || checkIfSkipPass()) + if (fog == nullptr || !renderContext.List->Setup.UseVolumetricFog || !_isSupported || checkIfSkipPass()) { RenderTargetPool::Release(renderContext.Buffers->VolumetricFog); renderContext.Buffers->VolumetricFog = nullptr; @@ -184,7 +183,7 @@ bool VolumetricFogPass::Init(RenderContext& renderContext, GPUContext* context, _cache.Data.PhaseG = options.ScatteringDistribution; _cache.Data.VolumetricFogMaxDistance = options.Distance; _cache.Data.MissedHistorySamplesCount = Math::Clamp(_cache.MissedHistorySamplesCount, 1, (int32)ARRAY_COUNT(_cache.Data.FrameJitterOffsets)); - Matrix::Transpose(view.PrevViewProjection, _cache.Data.PrevWorldToClip); + Matrix::Transpose(renderContext.View.PrevViewProjection, _cache.Data.PrevWorldToClip); _cache.Data.SkyLight.VolumetricScatteringIntensity = 0; // Fill frame jitter history From 056fea9a8c0c02e8a7ef1368f38e5e1d72f9d71f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 3 Jul 2025 14:48:38 +0200 Subject: [PATCH 095/122] Fix forward shading compilation when using fog inside unlit material --- Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl index 08bb82698..22d9ca9b4 100644 --- a/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl +++ b/Content/Editor/MaterialTemplates/Features/ForwardShading.hlsl @@ -143,7 +143,7 @@ void PS_Forward( #endif -#if USE_FOG +#if USE_FOG && MATERIAL_SHADING_MODEL != SHADING_MODEL_UNLIT // Calculate exponential height fog float4 fog = GetExponentialHeightFog(ExponentialHeightFog, materialInput.WorldPosition, ViewPos, 0, gBuffer.ViewPos.z); From da08be42b4d73307eb4a25fc3fb3fecf282636aa Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 3 Jul 2025 20:32:12 +0200 Subject: [PATCH 096/122] Fix deadlock in Debug builds on object dtor --- Source/Engine/Core/ObjectsRemovalService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Core/ObjectsRemovalService.cpp b/Source/Engine/Core/ObjectsRemovalService.cpp index 4d9159ea9..052bd6040 100644 --- a/Source/Engine/Core/ObjectsRemovalService.cpp +++ b/Source/Engine/Core/ObjectsRemovalService.cpp @@ -154,7 +154,7 @@ void ObjectsRemoval::Dispose() Object::~Object() { -#if BUILD_DEBUG +#if BUILD_DEBUG && 0 // Prevent removing object that is still reverenced by the removal service ASSERT(!ObjectsRemovalService::IsInPool(this)); #endif From 85aed8c4d70f37e63b536aa88ac0252e01132525 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 3 Jul 2025 20:32:41 +0200 Subject: [PATCH 097/122] Fix using material VS to PS node directly within material input --- .../Tools/MaterialGenerator/MaterialGenerator.Material.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index 0617385bc..9105fcd2e 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -10,6 +10,10 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) { switch (node->TypeID) { + // Material + case 1: + value = tryGetValue(box, Value::Zero); + break; // World Position case 2: value = Value(VariantType::Float3, TEXT("input.WorldPosition.xyz")); From 4b10d7057aad73fdc39db71029c6d7d54878085a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 3 Jul 2025 20:33:14 +0200 Subject: [PATCH 098/122] Fix crash when using material instance that has more parameters that base due to material error --- Source/Engine/Graphics/Materials/MaterialParams.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Graphics/Materials/MaterialParams.cpp b/Source/Engine/Graphics/Materials/MaterialParams.cpp index e31697f77..d670b188a 100644 --- a/Source/Engine/Graphics/Materials/MaterialParams.cpp +++ b/Source/Engine/Graphics/Materials/MaterialParams.cpp @@ -604,10 +604,11 @@ int32 MaterialParams::GetVersionHash() const void MaterialParams::Bind(MaterialParamsLink* link, MaterialParameter::BindMeta& meta) { ASSERT(link && link->This); - for (int32 i = 0; i < link->This->Count(); i++) + const int32 count = link->This->Count(); + for (int32 i = 0; i < count; i++) { MaterialParamsLink* l = link; - while (l->Down && !l->This->At(i).IsOverride()) + while (l->Down && !l->This->At(i).IsOverride() && l->Down->This->Count() == count) { l = l->Down; } From 7abed939729a701381c508fe2970c30e676a15a3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 4 Jul 2025 11:31:27 +0200 Subject: [PATCH 099/122] Optimize terrain heightmap decoding to use shared code --- .../Editor/MaterialTemplates/Terrain.shader | 12 +++--- Source/Engine/Visject/ShaderGraph.cpp | 2 +- Source/Shaders/TerrainCommon.hlsl | 37 +++++++++---------- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/Content/Editor/MaterialTemplates/Terrain.shader b/Content/Editor/MaterialTemplates/Terrain.shader index abc444316..63313e304 100644 --- a/Content/Editor/MaterialTemplates/Terrain.shader +++ b/Content/Editor/MaterialTemplates/Terrain.shader @@ -15,6 +15,7 @@ #include "./Flax/Common.hlsl" #include "./Flax/MaterialCommon.hlsl" #include "./Flax/GBufferCommon.hlsl" +#include "./Flax/TerrainCommon.hlsl" @7 // Primary constant buffer (with additional material parameters) META_CB_BEGIN(0, Data) @@ -334,7 +335,7 @@ VertexOutput VS(TerrainVertexInput input) float lodValue = CurrentLOD; float morphAlpha = lodCalculated - CurrentLOD; - // Sample heightmap + // Sample heightmap and splatmaps float2 heightmapUVs = input.TexCoord * HeightmapUVScaleBias.xy + HeightmapUVScaleBias.zw; #if USE_SMOOTH_LOD_TRANSITION float4 heightmapValueThisLOD = Heightmap.SampleLevel(SamplerPointClamp, heightmapUVs, lodValue); @@ -342,7 +343,6 @@ VertexOutput VS(TerrainVertexInput input) float2 heightmapUVsNextLOD = nextLODPos * HeightmapUVScaleBias.xy + HeightmapUVScaleBias.zw; float4 heightmapValueNextLOD = Heightmap.SampleLevel(SamplerPointClamp, heightmapUVsNextLOD, lodValue + 1); float4 heightmapValue = lerp(heightmapValueThisLOD, heightmapValueNextLOD, morphAlpha); - bool isHole = max(heightmapValueThisLOD.b + heightmapValueThisLOD.a, heightmapValueNextLOD.b + heightmapValueNextLOD.a) >= 1.9f; #if USE_TERRAIN_LAYERS float4 splatmapValueThisLOD = Splatmap0.SampleLevel(SamplerPointClamp, heightmapUVs, lodValue); float4 splatmapValueNextLOD = Splatmap0.SampleLevel(SamplerPointClamp, heightmapUVsNextLOD, lodValue + 1); @@ -355,7 +355,6 @@ VertexOutput VS(TerrainVertexInput input) #endif #else float4 heightmapValue = Heightmap.SampleLevel(SamplerPointClamp, heightmapUVs, lodValue); - bool isHole = (heightmapValue.b + heightmapValue.a) >= 1.9f; #if USE_TERRAIN_LAYERS float4 splatmap0Value = Splatmap0.SampleLevel(SamplerPointClamp, heightmapUVs, lodValue); #if TERRAIN_LAYERS_DATA_SIZE > 1 @@ -363,12 +362,11 @@ VertexOutput VS(TerrainVertexInput input) #endif #endif #endif - float height = (float)((int)(heightmapValue.x * 255.0) + ((int)(heightmapValue.y * 255) << 8)) / 65535.0; + float height = DecodeHeightmapHeight(heightmapValue); // Extract normal and the holes mask - float2 normalTemp = float2(heightmapValue.b, heightmapValue.a) * 2.0f - 1.0f; - float3 normal = float3(normalTemp.x, sqrt(1.0 - saturate(dot(normalTemp, normalTemp))), normalTemp.y); - normal = normalize(normal); + bool isHole; + float3 normal = DecodeHeightmapNormal(heightmapValue, isHole); output.Geometry.HolesMask = isHole ? 0 : 1; if (isHole) { diff --git a/Source/Engine/Visject/ShaderGraph.cpp b/Source/Engine/Visject/ShaderGraph.cpp index ef4f53cf9..b6616d159 100644 --- a/Source/Engine/Visject/ShaderGraph.cpp +++ b/Source/Engine/Visject/ShaderGraph.cpp @@ -286,7 +286,7 @@ void ShaderGenerator::ProcessGroupMath(Box* box, Node* node, Value& value) case 29: { Value inXY = tryGetValue(node->GetBox(0), Value::Zero).AsFloat2(); - value = writeLocal(ValueType::Float3, String::Format(TEXT("float3({0}, sqrt(saturate(1.0 - dot({0}.xy, {0}.xy))))"), inXY.Value), node); + value = writeLocal(ValueType::Float3, String::Format(TEXT("float3({0}, sqrt(saturate(1.0 - dot({0}, {0}))))"), inXY.Value), node); break; } // Mad diff --git a/Source/Shaders/TerrainCommon.hlsl b/Source/Shaders/TerrainCommon.hlsl index a4db9bd4f..0c2f57168 100644 --- a/Source/Shaders/TerrainCommon.hlsl +++ b/Source/Shaders/TerrainCommon.hlsl @@ -5,28 +5,30 @@ #include "./Flax/Common.hlsl" +float DecodeHeightmapHeight(float4 value) +{ + return (float)((int)(value.x * 255.0) + ((int)(value.y * 255) << 8)) / 65535.0; +} + +float3 DecodeHeightmapNormal(float4 value, out bool isHole) +{ + isHole = (value.b + value.a) >= 1.9f; + float2 normalTemp = float2(value.b, value.a) * 2.0f - 1.0f; + float3 normal = float3(normalTemp.x, sqrt(1.0 - saturate(dot(normalTemp, normalTemp))), normalTemp.y); + return normalize(normal); +} + float SampleHeightmap(Texture2D heightmap, float2 uv, float mipOffset = 0.0f) { - // Sample heightmap float4 value = heightmap.SampleLevel(SamplerPointClamp, uv, mipOffset); - - // Decode heightmap - float height = (float)((int)(value.x * 255.0) + ((int)(value.y * 255) << 8)) / 65535.0; - return height; + return DecodeHeightmapHeight(value); } float SampleHeightmap(Texture2D heightmap, float2 uv, out float3 normal, out bool isHole, float mipOffset = 0.0f) { - // Sample heightmap float4 value = heightmap.SampleLevel(SamplerPointClamp, uv, mipOffset); - - // Decode heightmap - float height = (float)((int)(value.x * 255.0) + ((int)(value.y * 255) << 8)) / 65535.0; - float2 normalTemp = float2(value.b, value.a) * 2.0f - 1.0f; - normal = float3(normalTemp.x, sqrt(1.0 - saturate(dot(normalTemp, normalTemp))), normalTemp.y); - isHole = (value.b + value.a) >= 1.9f; - normal = normalize(normal); - return height; + normal = DecodeHeightmapNormal(value, isHole); + return DecodeHeightmapHeight(value); } float3 SampleHeightmap(Texture2D heightmap, float3 localPosition, float4 localToUV, out float3 normal, out bool isHole, float mipOffset = 0.0f) @@ -36,12 +38,9 @@ float3 SampleHeightmap(Texture2D heightmap, float3 localPosition, float4 float4 value = heightmap.SampleLevel(SamplerPointClamp, uv, mipOffset); // Decode heightmap - isHole = (value.b + value.a) >= 1.9f; - float height = (float)((int)(value.x * 255.0) + ((int)(value.y * 255) << 8)) / 65535.0; + normal = DecodeHeightmapNormal(value, isHole); + float height = DecodeHeightmapHeight(value);; float3 position = float3(localPosition.x, height, localPosition.z); - float2 normalTemp = float2(value.b, value.a) * 2.0f - 1.0f; - normal = float3(normalTemp.x, sqrt(1.0 - saturate(dot(normalTemp, normalTemp))), normalTemp.y); - normal = normalize(normal); // UVs outside the heightmap are empty isHole = isHole || any(uv < 0.0f) || any(uv > 1.0f); From 0bc595f16fb3a3dc7076aa0b4af217895ac7e57e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 4 Jul 2025 12:04:36 +0200 Subject: [PATCH 100/122] Disable terarin normals smoothening --- .../Editor/SceneGraph/Actors/TerrainNode.cs | 4 ++++ Source/Engine/Terrain/TerrainPatch.cpp | 20 ++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Source/Editor/SceneGraph/Actors/TerrainNode.cs b/Source/Editor/SceneGraph/Actors/TerrainNode.cs index f5cef604d..4e2cd3346 100644 --- a/Source/Editor/SceneGraph/Actors/TerrainNode.cs +++ b/Source/Editor/SceneGraph/Actors/TerrainNode.cs @@ -76,9 +76,13 @@ namespace FlaxEditor.SceneGraph.Actors // Skip removing this terrain file sif it's still referenced var sceneReferences = Editor.GetAssetReferences(e.SceneId); if (sceneReferences != null && sceneReferences.Contains(e.TerrainId)) + { + Debug.Log($"Skip removing files used by terrain {e.TerrainId} on scene {e.SceneId} as it's still in use"); continue; + } // Delete files + Debug.Log($"Removing files used by removed terrain {e.TerrainId} on scene {e.SceneId}"); foreach (var file in e.Files) { if (file != null && File.Exists(file)) diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp index 1c754d843..a9388224d 100644 --- a/Source/Engine/Terrain/TerrainPatch.cpp +++ b/Source/Engine/Terrain/TerrainPatch.cpp @@ -431,8 +431,6 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh GET_VERTEX(1, 1); #undef GET_VERTEX - // TODO: use SIMD for those calculations - // Calculate normals for quad two vertices Float3 n0 = Float3::Normalize((v00 - v01) ^ (v01 - v10)); Float3 n1 = Float3::Normalize((v11 - v10) ^ (v10 - v01)); @@ -446,6 +444,7 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh } } +#if 0 // Smooth normals for (int32 z = 1; z < normalsSize.Y - 1; z++) { @@ -466,8 +465,6 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh GET_NORMAL(2, 2); #undef GET_VERTEX - // TODO: use SIMD for those calculations - /* * The current vertex is (11). Calculate average for the nearby vertices. * 00 01 02 @@ -481,6 +478,7 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh normalsPerVertex[i11] = Float3::Lerp(n11, avg, 0.6f); } } +#endif // Write back to the data container const auto ptr = (Color32*)data; @@ -525,10 +523,9 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh const int32 textureIndex = tz + tx; const int32 heightmapIndex = hz + hx; const int32 normalIndex = sz + sx; -#if BUILD_DEBUG - ASSERT(normalIndex >= 0 && normalIndex < normalsLength); -#endif - Float3 normal = Float3::NormalizeFast(normalsPerVertex[normalIndex]) * 0.5f + 0.5f; + ASSERT_LOW_LAYER(normalIndex >= 0 && normalIndex < normalsLength); + Float3 normal = Float3::NormalizeFast(normalsPerVertex[normalIndex]); + normal = normal * 0.5f + 0.5f; if (holesMask && !holesMask[heightmapIndex]) normal = Float3::One; @@ -1247,6 +1244,11 @@ void TerrainPatch::ClearCache() void TerrainPatch::CacheHeightData() { + if (Heightmap == nullptr) + { + LOG(Error, "Missing heightmap."); + return; + } PROFILE_CPU_NAMED("Terrain.CacheHeightData"); const TerrainDataUpdateInfo info(this); @@ -1745,7 +1747,7 @@ bool TerrainPatch::UpdateHeightData(TerrainDataUpdateInfo& info, const Int2& mod // Prepare data for the uploading to GPU ASSERT(Heightmap); auto texture = Heightmap->GetTexture(); - ASSERT(texture->ResidentMipLevels() > 0); + ASSERT(texture->IsAllocated()); const int32 textureSize = texture->Width(); const PixelFormat pixelFormat = texture->Format(); const int32 pixelStride = PixelFormatExtensions::SizeInBytes(pixelFormat); From 48c6339ebbbcb699a457c7ba5a3b87b9c8df03ba Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 4 Jul 2025 12:21:25 +0200 Subject: [PATCH 101/122] Fix memory leak on material instances when updating layout of Text Render --- Source/Engine/UI/TextRender.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Source/Engine/UI/TextRender.cpp b/Source/Engine/UI/TextRender.cpp index 951da2316..6b2988b7a 100644 --- a/Source/Engine/UI/TextRender.cpp +++ b/Source/Engine/UI/TextRender.cpp @@ -190,6 +190,10 @@ void TextRender::UpdateLayout() _buffersDirty = true; // Init draw chunks data + Array> materials; + materials.Resize(_drawChunks.Count()); + for (int32 i = 0; i < materials.Count(); i++) + materials[i] = _drawChunks[i].Material; DrawChunk drawChunk; drawChunk.Actor = this; drawChunk.StartIndex = 0; @@ -242,10 +246,12 @@ void TextRender::UpdateLayout() } // Setup material - drawChunk.Material = Content::CreateVirtualAsset(); + if (_drawChunks.Count() < materials.Count()) + drawChunk.Material = materials[_drawChunks.Count()]; + else + drawChunk.Material = Content::CreateVirtualAsset(); drawChunk.Material->SetBaseMaterial(Material.Get()); - for (auto& param : drawChunk.Material->Params) - param.SetIsOverride(false); + drawChunk.Material->ResetParameters(); // Set the font parameter static StringView FontParamName = TEXT("Font"); From a8eb4fc14081d41e43ab8df98300186e21eaa0d4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 7 Jul 2025 23:22:32 +0200 Subject: [PATCH 102/122] Add allocator tag support for `Dictionary` and `HashSet` --- Source/Engine/Core/Collections/Dictionary.h | 9 +++++++++ Source/Engine/Core/Collections/HashSet.h | 9 +++++++++ Source/Engine/Core/Collections/HashSetBase.h | 6 ++++++ Source/Engine/Core/Memory/ArenaAllocation.h | 2 +- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h index 544a70ca0..bdab15bbe 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -163,6 +163,15 @@ public: { } + /// + /// Initializes an empty without reserving any space. + /// + /// The custom allocation tag. + Dictionary(typename Base::AllocationTag tag) + : Base(tag) + { + } + /// /// Initializes by reserving space. /// diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index ab2601525..032a407db 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -140,6 +140,15 @@ public: { } + /// + /// Initializes an empty without reserving any space. + /// + /// The custom allocation tag. + HashSet(typename Base::AllocationTag tag) + : Base(tag) + { + } + /// /// Initializes by reserving space. /// diff --git a/Source/Engine/Core/Collections/HashSetBase.h b/Source/Engine/Core/Collections/HashSetBase.h index 200e26b7b..3b487227a 100644 --- a/Source/Engine/Core/Collections/HashSetBase.h +++ b/Source/Engine/Core/Collections/HashSetBase.h @@ -59,6 +59,7 @@ class HashSetBase public: // Type of allocation data used to store hash set buckets. using AllocationData = typename AllocationType::template Data; + using AllocationTag = typename AllocationType::Tag; protected: int32 _elementsCount = 0; @@ -70,6 +71,11 @@ protected: { } + HashSetBase(AllocationTag tag) + : _allocation(tag) + { + } + void MoveToEmpty(HashSetBase&& other) { _elementsCount = other._elementsCount; diff --git a/Source/Engine/Core/Memory/ArenaAllocation.h b/Source/Engine/Core/Memory/ArenaAllocation.h index af8df2001..7de7e0994 100644 --- a/Source/Engine/Core/Memory/ArenaAllocation.h +++ b/Source/Engine/Core/Memory/ArenaAllocation.h @@ -138,7 +138,7 @@ public: FORCE_INLINE void Swap(Data& other) { ::Swap(_data, other._data); - ::Swap(_arena, other._arena); + _arena = other._arena; // TODO: find a better way to move allocation with AllocationUtils::MoveToEmpty to preserve/maintain allocation tag ownership } }; }; From 3abbf08f1f64457792bc8cf01f954d1c4c0655c5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 8 Jul 2025 22:18:00 +0200 Subject: [PATCH 103/122] Optimize foliage rendering with concurrent arena allocator --- Source/Engine/Core/Memory/Allocation.cpp | 78 ++++++++++++++++++ Source/Engine/Core/Memory/ArenaAllocation.h | 90 ++++++++++++++++++++- Source/Engine/Foliage/Foliage.cpp | 16 ++-- Source/Engine/Foliage/Foliage.h | 5 +- Source/Engine/Renderer/RenderList.cpp | 20 ++++- Source/Engine/Renderer/RenderList.h | 12 ++- 6 files changed, 205 insertions(+), 16 deletions(-) diff --git a/Source/Engine/Core/Memory/Allocation.cpp b/Source/Engine/Core/Memory/Allocation.cpp index c55ab1dab..683c31c43 100644 --- a/Source/Engine/Core/Memory/Allocation.cpp +++ b/Source/Engine/Core/Memory/Allocation.cpp @@ -52,3 +52,81 @@ void* ArenaAllocator::Allocate(uint64 size, uint64 alignment) return mem; } + +void ConcurrentArenaAllocator::Free() +{ + _locker.Lock(); + + // Free all pages + Page* page = (Page*)_first; + while (page) + { +#if COMPILE_WITH_PROFILER + ProfilerMemory::OnGroupUpdate(ProfilerMemory::Groups::MallocArena, -(int64)page->Size, -1); +#endif + if (_free1) + _free1(page->Memory); + else + _free2(page->Memory, page->Size); + Page* next = page->Next; + if (_free1) + _free1(page); + else + _free2(page, sizeof(Page)); + page = next; + } + + // Unlink + _first = 0; + _totalBytes = 0; + + _locker.Unlock(); +} + +void* ConcurrentArenaAllocator::Allocate(uint64 size, uint64 alignment) +{ +RETRY: + + // Check if the current page has some space left + Page* page = (Page*)Platform::AtomicRead(&_first); + if (page) + { + int64 offset = Platform::AtomicRead(&page->Offset); + int64 offsetAligned = Math::AlignUp(offset, (int64)alignment); + int64 end = offsetAligned + size; + if (end <= page->Size) + { + // Try to allocate within a page + if (Platform::InterlockedCompareExchange(&page->Offset, end, offset) != offset) + { + // Someone else changed allocated so retry (new offset might mismatch alignment) + goto RETRY; + } + Platform::InterlockedAdd(&_totalBytes, (int64)size); + return (byte*)page->Memory + offsetAligned; + } + } + + // Page allocation is thread-synced + _locker.Lock(); + + // Check if page was unchanged by any other thread + if ((Page*)Platform::AtomicRead(&_first) == page) + { + uint64 pageSize = Math::Max(_pageSize, size); +#if COMPILE_WITH_PROFILER + ProfilerMemory::OnGroupUpdate(ProfilerMemory::Groups::MallocArena, (int64)pageSize, 1); +#endif + page = (Page*)(_allocate1 ? _allocate1(sizeof(Page), alignof(Page)) : _allocate2(sizeof(Page))); + page->Memory = _allocate1 ? _allocate1(pageSize, 16) : _allocate2(pageSize); + page->Next = (Page*)_first; + page->Offset = 0; + page->Size = (int64)pageSize; + Platform::AtomicStore(&_first, (intptr)page); + } + + _locker.Unlock(); + + // Use a single cde for allocation + goto RETRY; +} diff --git a/Source/Engine/Core/Memory/ArenaAllocation.h b/Source/Engine/Core/Memory/ArenaAllocation.h index 7de7e0994..eaffcff95 100644 --- a/Source/Engine/Core/Memory/ArenaAllocation.h +++ b/Source/Engine/Core/Memory/ArenaAllocation.h @@ -3,6 +3,7 @@ #pragma once #include "Allocation.h" +#include "Engine/Platform/CriticalSection.h" /// /// Allocator that uses pages for stack-based allocs without freeing memory during it's lifetime. @@ -66,21 +67,94 @@ public: } }; +/// +/// Allocator that uses pages for stack-based allocs without freeing memory during it's lifetime. Thread-safe to allocate memory from multiple threads at once. +/// +class ConcurrentArenaAllocator +{ +private: + struct Page + { + void* Memory; + Page* Next; + volatile int64 Offset; + int64 Size; + }; + + int32 _pageSize; + volatile int64 _first = 0; +#if !BUILD_RELEASE + volatile int64 _totalBytes = 0; +#endif + void*(*_allocate1)(uint64 size, uint64 alignment) = nullptr; + void(*_free1)(void* ptr) = nullptr; + void*(*_allocate2)(uint64 size) = nullptr; + void(*_free2)(void* ptr, uint64 size) = nullptr; + CriticalSection _locker; + +public: + ConcurrentArenaAllocator(int32 pageSizeBytes, void* (*customAllocate)(uint64 size, uint64 alignment), void(*customFree)(void* ptr)) + : _pageSize(pageSizeBytes) + , _allocate1(customAllocate) + , _free1(customFree) + { + } + + ConcurrentArenaAllocator(int32 pageSizeBytes, void* (*customAllocate)(uint64 size), void(*customFree)(void* ptr, uint64 size)) + : _pageSize(pageSizeBytes) + , _allocate2(customAllocate) + , _free2(customFree) + { + } + + ConcurrentArenaAllocator(int32 pageSizeBytes = 1024 * 1024) // 1 MB by default + : ConcurrentArenaAllocator(pageSizeBytes, Allocator::Allocate, Allocator::Free) + { + } + + ~ConcurrentArenaAllocator() + { + Free(); + } + + // Gets the total amount of bytes allocated in arena (excluding alignment). + int64 GetTotalBytes() const + { + return Platform::AtomicRead(&_totalBytes); + } + + // Allocates a chunk of unitialized memory. + void* Allocate(uint64 size, uint64 alignment = 1); + + // Frees all memory allocations within allocator. + void Free(); + + // Creates a new object within the arena allocator. + template + inline T* New(Args&&...args) + { + T* ptr = (T*)Allocate(sizeof(T)); + new(ptr) T(Forward(args)...); + return ptr; + } +}; + /// /// The memory allocation policy that uses a part of shared page allocator. Allocations are performed in stack-manner, and free is no-op. /// -class ArenaAllocation +template +class ArenaAllocationBase { public: enum { HasSwap = true }; - typedef ArenaAllocator* Tag; + typedef ArenaType* Tag; template class Data { private: T* _data = nullptr; - ArenaAllocator* _arena = nullptr; + ArenaType* _arena = nullptr; public: FORCE_INLINE Data() @@ -142,3 +216,13 @@ public: } }; }; + +/// +/// The memory allocation policy that uses a part of shared page allocator. Allocations are performed in stack-manner, and free is no-op. +/// +typedef ArenaAllocationBase ArenaAllocation; + +/// +/// The memory allocation policy that uses a part of shared page allocator. Allocations are performed in stack-manner, and free is no-op. +/// +typedef ArenaAllocationBase ConcurrentArenaAllocation; diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index 4fc576ff8..f51f4ece0 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -103,17 +103,17 @@ void Foliage::DrawInstance(RenderContext& renderContext, FoliageInstance& instan for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++) { auto& drawCall = drawCallsLists[lod][meshIndex]; - if (!drawCall.DrawCall.Material) + if (!drawCall.Material) continue; DrawKey key; - key.Mat = drawCall.DrawCall.Material; + key.Mat = drawCall.Material; key.Geo = &meshes.Get()[meshIndex]; key.Lightmap = instance.Lightmap.TextureIndex; auto* e = result.TryGet(key); if (!e) { - e = &result[key]; + e = &result.Add(key, BatchedDrawCall(renderContext.List))->Value; ASSERT_LOW_LAYER(key.Mat); e->DrawCall.Material = key.Mat; e->DrawCall.Surface.Lightmap = EnumHasAnyFlags(_staticFlags, StaticFlags::Lightmap) && _scene ? _scene->LightmapsData.GetReadyLightmap(key.Lightmap) : nullptr; @@ -127,7 +127,7 @@ void Foliage::DrawInstance(RenderContext& renderContext, FoliageInstance& instan const Float3 translation = transform.Translation - renderContext.View.Origin; Matrix::Transformation(transform.Scale, transform.Orientation, translation, world); constexpr float worldDeterminantSign = 1.0f; - instanceData.Store(world, world, instance.Lightmap.UVsArea, drawCall.DrawCall.Surface.GeometrySize, instance.Random, worldDeterminantSign, lodDitherFactor); + instanceData.Store(world, world, instance.Lightmap.UVsArea, drawCall.Surface.GeometrySize, instance.Random, worldDeterminantSign, lodDitherFactor); } } @@ -430,7 +430,7 @@ void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, Dr { const auto& mesh = meshes.Get()[meshIndex]; auto& drawCall = drawCallsList.Get()[meshIndex]; - drawCall.DrawCall.Material = nullptr; + drawCall.Material = nullptr; // DrawInstance skips draw calls from meshes with unset material // Check entry visibility const auto& entry = type.Entries[mesh.GetMaterialSlotIndex()]; @@ -455,13 +455,13 @@ void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, Dr if (drawModes == DrawPass::None) continue; - drawCall.DrawCall.Material = material; - drawCall.DrawCall.Surface.GeometrySize = mesh.GetBox().GetSize(); + drawCall.Material = material; + drawCall.Surface.GeometrySize = mesh.GetBox().GetSize(); } } // Draw instances of the foliage type - BatchedDrawCalls result; + BatchedDrawCalls result(&renderContext.List->Memory); DrawCluster(renderContext, type.Root, type, drawCallsLists, result); // Submit draw calls with valid instances added diff --git a/Source/Engine/Foliage/Foliage.h b/Source/Engine/Foliage/Foliage.h index 1855e9914..6f8b36cf4 100644 --- a/Source/Engine/Foliage/Foliage.h +++ b/Source/Engine/Foliage/Foliage.h @@ -6,6 +6,7 @@ #include "FoliageInstance.h" #include "FoliageCluster.h" #include "FoliageType.h" +#include "Engine/Core/Memory/ArenaAllocation.h" #include "Engine/Level/Actor.h" /// @@ -178,8 +179,8 @@ private: } }; - typedef Array> DrawCallsList; - typedef Dictionary BatchedDrawCalls; + typedef Array> DrawCallsList; + typedef Dictionary BatchedDrawCalls; void DrawInstance(RenderContext& renderContext, FoliageInstance& instance, const FoliageType& type, Model* model, int32 lod, float lodDitherFactor, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const; void DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, const FoliageType& type, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const; #else diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index 2a6540da5..0dedfda38 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -169,6 +169,7 @@ void RenderEnvironmentProbeData::SetShaderData(ShaderEnvProbeData& data) const void* RendererAllocation::Allocate(uintptr size) { + PROFILE_CPU(); void* result = nullptr; MemPoolLocker.Lock(); for (int32 i = 0; i < MemPool.Count(); i++) @@ -188,6 +189,7 @@ void* RendererAllocation::Allocate(uintptr size) void RendererAllocation::Free(void* ptr, uintptr size) { + PROFILE_CPU(); MemPoolLocker.Lock(); MemPool.Add({ ptr, size }); MemPoolLocker.Unlock(); @@ -418,6 +420,18 @@ bool RenderList::HasAnyPostFx(const RenderContext& renderContext, MaterialPostFx return false; } +BatchedDrawCall::BatchedDrawCall(RenderList* list) + : Instances(&list->Memory) +{ +} + +BatchedDrawCall::BatchedDrawCall(BatchedDrawCall&& other) noexcept + : DrawCall(other.DrawCall) + , ObjectsStartIndex(other.ObjectsStartIndex) + , Instances(MoveTemp(other.Instances)) +{ +} + void DrawCallsList::Clear() { Indices.Clear(); @@ -433,6 +447,7 @@ bool DrawCallsList::IsEmpty() const RenderList::RenderList(const SpawnParams& params) : ScriptingObject(params) + , Memory(4 * 1024 * 1024, RendererAllocation::Allocate, RendererAllocation::Free) // 4MB pages, use page pooling via RendererAllocation , DirectionalLights(4) , PointLights(32) , SpotLights(32) @@ -443,8 +458,8 @@ RenderList::RenderList(const SpawnParams& params) , AtmosphericFog(nullptr) , Fog(nullptr) , Blendable(32) - , ObjectBuffer(0, PixelFormat::R32G32B32A32_Float, false, TEXT("Object Bufffer")) - , TempObjectBuffer(0, PixelFormat::R32G32B32A32_Float, false, TEXT("Object Bufffer")) + , 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 } })) { } @@ -480,6 +495,7 @@ void RenderList::Clear() _instanceBuffer.Clear(); ObjectBuffer.Clear(); TempObjectBuffer.Clear(); + Memory.Free(); } struct PackedSortKey diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h index d5288e6ee..f17e1b045 100644 --- a/Source/Engine/Renderer/RenderList.h +++ b/Source/Engine/Renderer/RenderList.h @@ -3,6 +3,7 @@ #pragma once #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" @@ -241,7 +242,11 @@ struct BatchedDrawCall { DrawCall DrawCall; uint16 ObjectsStartIndex = 0; // Index of the instances start in the ObjectsBuffer (set internally). - Array Instances; + Array Instances; + + BatchedDrawCall() { CRASH; } // Don't use it + BatchedDrawCall(RenderList* list); + BatchedDrawCall(BatchedDrawCall&& other) noexcept; }; /// @@ -298,6 +303,11 @@ API_CLASS(Sealed) class FLAXENGINE_API RenderList : public ScriptingObject static void CleanupCache(); public: + /// + /// Memory storage with all draw-related data that lives during a single frame rendering time. Thread-safe to allocate memory during rendering jobs. + /// + ConcurrentArenaAllocator Memory; + /// /// All scenes for rendering. /// From bdaf31b54f896a457d452309ff30d6870ade6ad3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 9 Jul 2025 00:22:35 +0200 Subject: [PATCH 104/122] Optimize Arena Allocator to store page metadata within itself to save on allocs --- Source/Engine/Core/Memory/Allocation.cpp | 25 ++++++++------------- Source/Engine/Core/Memory/ArenaAllocation.h | 2 -- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/Source/Engine/Core/Memory/Allocation.cpp b/Source/Engine/Core/Memory/Allocation.cpp index 683c31c43..239228b67 100644 --- a/Source/Engine/Core/Memory/Allocation.cpp +++ b/Source/Engine/Core/Memory/Allocation.cpp @@ -13,7 +13,6 @@ void ArenaAllocator::Free() #if COMPILE_WITH_PROFILER ProfilerMemory::OnGroupUpdate(ProfilerMemory::Groups::MallocArena, -(int64)page->Size, -1); #endif - Allocator::Free(page->Memory); Page* next = page->Next; Allocator::Free(page); page = next; @@ -33,21 +32,20 @@ void* ArenaAllocator::Allocate(uint64 size, uint64 alignment) // Create a new page if need to if (!page) { - uint64 pageSize = Math::Max(_pageSize, size); + uint64 pageSize = Math::Max(_pageSize, size + alignment + sizeof(Page)); #if COMPILE_WITH_PROFILER ProfilerMemory::OnGroupUpdate(ProfilerMemory::Groups::MallocArena, (int64)pageSize, 1); #endif - page = (Page*)Allocator::Allocate(sizeof(Page)); - page->Memory = Allocator::Allocate(pageSize); + page = (Page*)Allocator::Allocate(pageSize); page->Next = _first; - page->Offset = 0; + page->Offset = sizeof(Page); page->Size = (uint32)pageSize; _first = page; } // Allocate within a page page->Offset = Math::AlignUp(page->Offset, (uint32)alignment); - void* mem = (byte*)page->Memory + page->Offset; + void* mem = (byte*)page + page->Offset; page->Offset += (uint32)size; return mem; @@ -64,15 +62,11 @@ void ConcurrentArenaAllocator::Free() #if COMPILE_WITH_PROFILER ProfilerMemory::OnGroupUpdate(ProfilerMemory::Groups::MallocArena, -(int64)page->Size, -1); #endif - if (_free1) - _free1(page->Memory); - else - _free2(page->Memory, page->Size); Page* next = page->Next; if (_free1) _free1(page); else - _free2(page, sizeof(Page)); + _free2(page, page->Size); page = next; } @@ -103,7 +97,7 @@ RETRY: goto RETRY; } Platform::InterlockedAdd(&_totalBytes, (int64)size); - return (byte*)page->Memory + offsetAligned; + return (byte*)page + offsetAligned; } } @@ -113,14 +107,13 @@ RETRY: // Check if page was unchanged by any other thread if ((Page*)Platform::AtomicRead(&_first) == page) { - uint64 pageSize = Math::Max(_pageSize, size); + uint64 pageSize = Math::Max(_pageSize, size + alignment + sizeof(Page)); #if COMPILE_WITH_PROFILER ProfilerMemory::OnGroupUpdate(ProfilerMemory::Groups::MallocArena, (int64)pageSize, 1); #endif - page = (Page*)(_allocate1 ? _allocate1(sizeof(Page), alignof(Page)) : _allocate2(sizeof(Page))); - page->Memory = _allocate1 ? _allocate1(pageSize, 16) : _allocate2(pageSize); + page = (Page*)(_allocate1 ? _allocate1(pageSize, 16) : _allocate2(pageSize)); page->Next = (Page*)_first; - page->Offset = 0; + page->Offset = sizeof(Page); page->Size = (int64)pageSize; Platform::AtomicStore(&_first, (intptr)page); } diff --git a/Source/Engine/Core/Memory/ArenaAllocation.h b/Source/Engine/Core/Memory/ArenaAllocation.h index eaffcff95..2288e4560 100644 --- a/Source/Engine/Core/Memory/ArenaAllocation.h +++ b/Source/Engine/Core/Memory/ArenaAllocation.h @@ -13,7 +13,6 @@ class ArenaAllocator private: struct Page { - void* Memory; Page* Next; uint32 Offset, Size; }; @@ -75,7 +74,6 @@ class ConcurrentArenaAllocator private: struct Page { - void* Memory; Page* Next; volatile int64 Offset; int64 Size; From c27a9808c4ee91cd8db10f821b68f9c61dff2e73 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 9 Jul 2025 10:02:20 +0200 Subject: [PATCH 105/122] Fix unwanted code --- Source/Engine/Core/Memory/ArenaAllocation.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Engine/Core/Memory/ArenaAllocation.h b/Source/Engine/Core/Memory/ArenaAllocation.h index 2288e4560..bfb5dbfe6 100644 --- a/Source/Engine/Core/Memory/ArenaAllocation.h +++ b/Source/Engine/Core/Memory/ArenaAllocation.h @@ -81,9 +81,7 @@ private: int32 _pageSize; volatile int64 _first = 0; -#if !BUILD_RELEASE volatile int64 _totalBytes = 0; -#endif void*(*_allocate1)(uint64 size, uint64 alignment) = nullptr; void(*_free1)(void* ptr) = nullptr; void*(*_allocate2)(uint64 size) = nullptr; From 3e82e550f37b4b5ae777475d901243aa9b8f2bc7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 11 Jul 2025 23:00:41 +0200 Subject: [PATCH 106/122] Add option for debug shapes drawing for particle emitters #3267 --- .../Previews/ParticleSystemPreview.cs | 11 ++- .../Windows/Assets/ParticleEmitterWindow.cs | 2 + ...rticleEmitterGraph.CPU.ParticleModules.cpp | 96 ++++++++++++++++++- .../Graph/CPU/ParticleEmitterGraph.CPU.cpp | 18 ++++ .../Graph/CPU/ParticleEmitterGraph.CPU.h | 13 +++ Source/Engine/Particles/ParticleEffect.cpp | 11 ++- Source/Engine/Particles/ParticleEffect.h | 8 ++ Source/Engine/Particles/Particles.cpp | 23 +++++ Source/Engine/Particles/Particles.h | 8 ++ 9 files changed, 187 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Viewport/Previews/ParticleSystemPreview.cs b/Source/Editor/Viewport/Previews/ParticleSystemPreview.cs index 408b7161f..74c7c435b 100644 --- a/Source/Editor/Viewport/Previews/ParticleSystemPreview.cs +++ b/Source/Editor/Viewport/Previews/ParticleSystemPreview.cs @@ -246,6 +246,14 @@ namespace FlaxEditor.Viewport.Previews } } + /// + protected override void OnDebugDraw(GPUContext context, ref RenderContext renderContext) + { + base.OnDebugDraw(context, ref renderContext); + + _previewEffect.OnDebugDraw(); + } + /// public override void Draw() { @@ -295,7 +303,8 @@ namespace FlaxEditor.Viewport.Previews /// public override void OnDestroy() { - // Cleanup objects + if (IsDisposing) + return; _previewEffect.ParticleSystem = null; Object.Destroy(ref _previewEffect); Object.Destroy(ref _boundsModel); diff --git a/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs b/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs index 6513ac9e0..92cf6e61a 100644 --- a/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs +++ b/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs @@ -127,6 +127,8 @@ namespace FlaxEditor.Windows.Assets PlaySimulation = true, Parent = _split2.Panel1 }; + _preview.PreviewActor.ShowDebugDraw = true; + _preview.ShowDebugDraw = true; // Asset properties proxy _properties = new PropertiesProxy(); diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp index ad6381809..bb1e977e4 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp @@ -2,8 +2,16 @@ #include "ParticleEmitterGraph.CPU.h" #include "Engine/Core/Random.h" +#include "Engine/Core/Math/Vector2.h" +#include "Engine/Core/Math/Vector3.h" +#include "Engine/Core/Math/Vector4.h" +#include "Engine/Core/Math/Matrix.h" +#include "Engine/Core/Math/Quaternion.h" +#include "Engine/Core/Math/BoundingBox.h" +#include "Engine/Core/Math/BoundingSphere.h" +#include "Engine/Core/Math/OrientedBoundingBox.h" #include "Engine/Utilities/Noise.h" -#include "Engine/Core/Types/CommonValue.h" +#include "Engine/Debug/DebugDraw.h" // ReSharper disable CppCStyleCast // ReSharper disable CppClangTidyClangDiagnosticCastAlign @@ -1468,3 +1476,89 @@ void ParticleEmitterGraphCPUExecutor::ProcessModule(ParticleEmitterGraphCPUNode* #undef COLLISION_LOGIC } } + +#if USE_EDITOR + +void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNode* node, const Transform& transform) +{ + // Skip modules that rely on particle data + if (node->UsePerParticleDataResolve()) + return; + + const Color color = Color::White; + switch (node->TypeID) + { + case 202: // Position (sphere surface) + case 211: // Position (sphere volume) + { + const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); + const float radius = (float)GetValue(node->GetBox(1), 3); + DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, radius), color, 0.0f, true); + break; + } + case 203: // Position (plane) + { + const Float3 center = (Float3)GetValue(node->GetBox(0), 2); + const Float2 size = (Float2)GetValue(node->GetBox(1), 3); + const Float3 halfExtent = Float3(size.X * 0.5f, 0.0f, size.Y * 0.5f); + OrientedBoundingBox box(halfExtent, Transform(center)); + box.Transform(transform); + DEBUG_DRAW_WIRE_BOX(box, color, 0.0f, true); + break; + } + case 204: // Position (circle) + case 205: // Position (disc) + { + const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); + const float radius = (float)GetValue(node->GetBox(1), 3); + DEBUG_DRAW_WIRE_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, 0.0f, color, 0.0f, true); + break; + } + case 206: // Position (box surface) + case 207: // Position (box volume) + { + const Float3 center = (Float3)GetValue(node->GetBox(0), 2); + const Float3 size = (Float3)GetValue(node->GetBox(1), 3); + OrientedBoundingBox box(size * 0.5f, Transform(center)); + box.Transform(transform); + DEBUG_DRAW_WIRE_BOX(box, color, 0.0f, true); + break; + } + // Position (cylinder) + case 208: + { + const float height = (float)GetValue(node->GetBox(2), 4); + const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2) + Float3(0, 0, height * 0.5f)); + const float radius = (float)GetValue(node->GetBox(1), 3); + DEBUG_DRAW_WIRE_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, height, color, 0.0f, true); + break; + } + // Position (line) + case 209: + { + const Float3 start = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); + const Float3 end = transform.LocalToWorld((Float3)GetValue(node->GetBox(1), 3)); + DEBUG_DRAW_LINE(start, end, color, 0.0f, true); + break; + } + // Position (torus) + case 210: + { + const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); + const float radius = Math::Max((float)GetValue(node->GetBox(1), 3), ZeroTolerance); + const float thickness = (float)GetValue(node->GetBox(2), 4); + DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, radius + thickness), color, 0.0f, true); + break; + } + + // Position (spiral) + case 214: + { + const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); + DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, 5.0f), color, 0.0f, true); + break; + } + } +} + +#endif diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp index 39f87d561..bdfdf1956 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp @@ -7,6 +7,7 @@ #include "Engine/Particles/ParticleEffect.h" #include "Engine/Engine/Time.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Debug/DebugDraw.h" ThreadLocal ParticleEmitterGraphCPUExecutor::Context; @@ -423,6 +424,23 @@ void ParticleEmitterGraphCPUExecutor::Draw(ParticleEmitter* emitter, ParticleEff } } +#if USE_EDITOR + +void ParticleEmitterGraphCPUExecutor::DrawDebug(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data) +{ + // Prepare graph data + Init(emitter, effect, data); + Transform transform = emitter->SimulationSpace == ParticlesSimulationSpace::Local ? effect->GetTransform() : Transform::Identity; + + // Draw modules + for (auto module : emitter->Graph.SpawnModules) + DebugDrawModule(module, transform); + for (auto module : emitter->Graph.InitModules) + DebugDrawModule(module, transform); +} + +#endif + void ParticleEmitterGraphCPUExecutor::Update(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, float dt, bool canSpawn) { // Prepare data diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h index a31917cf8..82b7a1bef 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h @@ -162,6 +162,16 @@ public: /// The effect transform matrix. void Draw(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, RenderContext& renderContext, Matrix& transform); +#if USE_EDITOR + /// + /// Draws the particles debug shapes. + /// + /// The owning emitter. + /// The instance effect. + /// The instance data. + void DrawDebug(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data); +#endif + /// /// Updates the particles simulation (the CPU simulation). /// @@ -195,6 +205,9 @@ private: int32 ProcessSpawnModule(int32 index); void ProcessModule(ParticleEmitterGraphCPUNode* node, int32 particlesStart, int32 particlesEnd); +#if USE_EDITOR + void DebugDrawModule(ParticleEmitterGraphCPUNode* node, const Transform& transform); +#endif FORCE_INLINE Value GetValue(Box* box, int32 defaultValueBoxIndex) { diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index c1031f4ac..f29c87207 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -581,10 +581,19 @@ void ParticleEffect::OnDebugDrawSelected() { DEBUG_DRAW_WIRE_BOX(_box, Color::Violet * 0.7f, 0, true); - // Base Actor::OnDebugDrawSelected(); } +void ParticleEffect::OnDebugDraw() +{ + if (ShowDebugDraw) + { + Particles::DebugDraw(this); + } + + Actor::OnDebugDraw(); +} + #endif void ParticleEffect::OnLayerChanged() diff --git a/Source/Engine/Particles/ParticleEffect.h b/Source/Engine/Particles/ParticleEffect.h index 9be31c4c7..65600f570 100644 --- a/Source/Engine/Particles/ParticleEffect.h +++ b/Source/Engine/Particles/ParticleEffect.h @@ -244,6 +244,13 @@ public: API_FIELD(Attributes="EditorDisplay(\"Particle Effect\"), EditorOrder(80), DefaultValue(0)") int8 SortOrder = 0; +#if USE_EDITOR + /// + /// If checked, the particle emitter debug shapes will be shawn during debug drawing. This includes particle spawn location shapes display. + /// + API_FIELD(Attributes = "EditorDisplay(\"Particle Effect\"), EditorOrder(200)") bool ShowDebugDraw = false; +#endif + public: /// /// Gets the effect parameters collection. Those parameters are instanced from the that contains a linear list of emitters and every emitter has a list of own parameters. @@ -394,6 +401,7 @@ public: void Draw(RenderContext& renderContext) override; #if USE_EDITOR void OnDebugDrawSelected() override; + void OnDebugDraw() override; #endif void OnLayerChanged() override; void Serialize(SerializeStream& stream, const void* otherObj) override; diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp index e895e0b6d..216f5953c 100644 --- a/Source/Engine/Particles/Particles.cpp +++ b/Source/Engine/Particles/Particles.cpp @@ -933,6 +933,7 @@ void Particles::DrawParticles(RenderContext& renderContext, ParticleEffect* effe const DrawPass drawModes = view.Pass & effect->DrawModes; if (drawModes == DrawPass::None || SpriteRenderer.Init()) return; + ConcurrentSystemLocker::ReadScope systemScope(SystemLocker); Matrix worlds[2]; Matrix::Translation(-renderContext.View.Origin, worlds[0]); // World renderContext.View.GetWorldMatrix(effect->GetTransform(), worlds[1]); // Local @@ -1065,6 +1066,28 @@ void Particles::DrawParticles(RenderContext& renderContext, ParticleEffect* effe } } +#if USE_EDITOR + +void Particles::DebugDraw(ParticleEffect* effect) +{ + PROFILE_CPU_NAMED("Particles.DrawDebug"); + ConcurrentSystemLocker::ReadScope systemScope(SystemLocker); + + // Draw all emitters + for (auto& emitterData : effect->Instance.Emitters) + { + const auto buffer = emitterData.Buffer; + if (!buffer) + continue; + auto emitter = buffer->Emitter; + if (!emitter || !emitter->IsLoaded()) + continue; + emitter->GraphExecutorCPU.DrawDebug(emitter, effect, emitterData); + } +} + +#endif + #if COMPILE_WITH_GPU_PARTICLES void UpdateGPU(RenderTask* task, GPUContext* context) diff --git a/Source/Engine/Particles/Particles.h b/Source/Engine/Particles/Particles.h index 69d0f9dab..77f651cfa 100644 --- a/Source/Engine/Particles/Particles.h +++ b/Source/Engine/Particles/Particles.h @@ -52,6 +52,14 @@ public: /// The owning actor. static void DrawParticles(RenderContext& renderContext, ParticleEffect* effect); +#if USE_EDITOR + /// + /// Draws the particles debug shapes. + /// + /// The owning actor. + static void DebugDraw(ParticleEffect* effect); +#endif + public: /// /// Enables or disables particle buffer pooling. From 4af26a5516e345f0fb2324c206104551a155b0c7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 11 Jul 2025 23:00:53 +0200 Subject: [PATCH 107/122] SImplify editor code for actors debug shapes drawing --- Source/Editor/Viewport/MainEditorGizmoViewport.cs | 9 +-------- Source/Editor/Viewport/PrefabWindowViewport.cs | 9 +-------- Source/Editor/ViewportDebugDrawData.cs | 12 ++++++++++++ Source/Editor/Windows/GameWindow.cs | 8 +------- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 9e7983145..c14b6dfd8 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -374,14 +374,7 @@ namespace FlaxEditor.Viewport // Draw selected objects debug shapes and visuals if (DrawDebugDraw && (renderContext.View.Flags & ViewFlags.DebugDraw) == ViewFlags.DebugDraw) { - unsafe - { - fixed (IntPtr* actors = _debugDrawData.ActorsPtrs) - { - DebugDraw.DrawActors(new IntPtr(actors), _debugDrawData.ActorsCount, true); - } - } - + _debugDrawData.DrawActors(); DebugDraw.Draw(ref renderContext, target.View(), targetDepth.View(), true); } } diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 2efe7c95f..8b508eedf 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -643,14 +643,7 @@ namespace FlaxEditor.Viewport if (selectedParents[i].IsActiveInHierarchy) selectedParents[i].OnDebugDraw(_debugDrawData); } - - unsafe - { - fixed (IntPtr* actors = _debugDrawData.ActorsPtrs) - { - DebugDraw.DrawActors(new IntPtr(actors), _debugDrawData.ActorsCount, false); - } - } + _debugDrawData.DrawActors(); // Debug draw all actors in prefab and collect actors var view = Task.View; diff --git a/Source/Editor/ViewportDebugDrawData.cs b/Source/Editor/ViewportDebugDrawData.cs index 7913e0287..7b5bee95c 100644 --- a/Source/Editor/ViewportDebugDrawData.cs +++ b/Source/Editor/ViewportDebugDrawData.cs @@ -88,6 +88,18 @@ namespace FlaxEditor } } + /// + /// Draws the collected actors via . + /// + /// True if draw all loaded scenes too, otherwise will draw only provided actors. + public unsafe void DrawActors(bool drawScenes = false) + { + fixed (IntPtr* actors = ActorsPtrs) + { + DebugDraw.DrawActors(new IntPtr(actors), _actors.Count, drawScenes); + } + } + /// /// Called when task calls event. /// diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index e05f0d2db..acb2deda3 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -510,13 +510,7 @@ namespace FlaxEditor.Windows selectedParents[i].OnDebugDraw(drawDebugData); } } - unsafe - { - fixed (IntPtr* actors = drawDebugData.ActorsPtrs) - { - DebugDraw.DrawActors(new IntPtr(actors), drawDebugData.ActorsCount, true); - } - } + drawDebugData.DrawActors(true); } DebugDraw.Draw(ref renderContext, task.OutputView); From 10e9aee8cea17b9c5896abce4644b2ab8628f1b9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 14 Jul 2025 10:28:43 +0200 Subject: [PATCH 108/122] Add particle data layout debugger tab --- .../Windows/Assets/ParticleEmitterWindow.cs | 61 +++++++++++- Source/Engine/Particles/ParticleEmitter.cpp | 35 +++++++ Source/Engine/Particles/ParticleEmitter.h | 14 ++- Source/Engine/Particles/ParticlesData.cpp | 83 +++++++++++++++++ Source/Engine/Particles/ParticlesData.h | 92 ++----------------- 5 files changed, 192 insertions(+), 93 deletions(-) diff --git a/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs b/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs index 92cf6e61a..0c8653f5b 100644 --- a/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs +++ b/Source/Editor/Windows/Assets/ParticleEmitterWindow.cs @@ -113,8 +113,55 @@ namespace FlaxEditor.Windows.Assets } } + private sealed class LayoutTabProxy + { + [EditorDisplay("Layout"), CustomEditor(typeof(Editor)), NoSerialize] + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public ParticleEmitterWindow Window; + + private class Editor : CustomEditor + { + public override DisplayStyle Style => DisplayStyle.InlineIntoParent; + + public override void Initialize(LayoutElementsContainer layout) + { + var window = (ParticleEmitterWindow)Values[0]; + var emitter = window.Preview.Emitter; + if (emitter == null || !emitter.IsLoaded) + return; + var attributes = emitter.Layout; + var size = 0; + var height = 14; + foreach (var attribute in attributes) + { + layout.Label($" - {GetAttributeType(attribute.Format)} {attribute.Name}").Label.Height = height; + size += PixelFormatExtensions.SizeInBytes(attribute.Format); + } + var capacity = 0; + if (window.Surface != null && window.Surface.RootNode != null && window.Surface.RootNode.Values.Length > 0) + capacity = (int)window.Surface.RootNode.Values[0]; + layout.Space(10); + layout.Label($"Particle size: {size} bytes\nParticle buffer size: {Utilities.Utils.FormatBytesCount((ulong)(size * capacity))}").Label.Height = height * 2; + } + + private static string GetAttributeType(PixelFormat format) + { + switch (format) + { + case PixelFormat.R32_Float: return "float"; + case PixelFormat.R32G32_Float: return "Float2"; + case PixelFormat.R32G32B32_Float: return "Float3"; + case PixelFormat.R32G32B32A32_Float: return "Float4"; + case PixelFormat.R32_SInt: return "int"; + case PixelFormat.R32_UInt: return "uint"; + default: return format.ToString(); + } + } + } + } + private readonly PropertiesProxy _properties; - private Tab _previewTab; + private Tab _previewTab, _layoutTab; private ToolStripButton _showSourceCodeButton; /// @@ -135,12 +182,14 @@ namespace FlaxEditor.Windows.Assets // Preview properties editor _previewTab = new Tab("Preview"); - _previewTab.Presenter.Select(new PreviewProxy - { - Window = this, - }); + _previewTab.Presenter.Select(new PreviewProxy { Window = this }); _tabs.AddTab(_previewTab); + // Particle data layout + _layoutTab = new Tab("Layout"); + _layoutTab.Presenter.Select(new LayoutTabProxy { Window = this }); + _tabs.AddTab(_layoutTab); + // Surface _surface = new ParticleEmitterSurface(this, Save, _undo) { @@ -239,6 +288,7 @@ namespace FlaxEditor.Windows.Assets _asset.WaitForLoaded(); _preview.PreviewActor.ResetSimulation(); _previewTab.Presenter.BuildLayoutOnUpdate(); + _layoutTab.Presenter.BuildLayoutOnUpdate(); } } @@ -255,6 +305,7 @@ namespace FlaxEditor.Windows.Assets // Init asset properties and parameters proxy _properties.OnLoad(this); _previewTab.Presenter.BuildLayoutOnUpdate(); + _layoutTab.Presenter.BuildLayoutOnUpdate(); return false; } diff --git a/Source/Engine/Particles/ParticleEmitter.cpp b/Source/Engine/Particles/ParticleEmitter.cpp index 558c0172f..08344e167 100644 --- a/Source/Engine/Particles/ParticleEmitter.cpp +++ b/Source/Engine/Particles/ParticleEmitter.cpp @@ -519,4 +519,39 @@ bool ParticleEmitter::HasShaderCode() const return false; } +Array ParticleEmitter::GetLayout() const +{ + Array result; + ScopeLock lock(Locker); + result.Resize(Graph.Layout.Attributes.Count()); + for (int32 i = 0; i < result.Count(); i++) + { + auto& dst = result[i]; + const auto& src = Graph.Layout.Attributes[i]; + dst.Name = src.Name; + switch (src.ValueType) + { + case ParticleAttribute::ValueTypes::Float: + dst.Format = PixelFormat::R32_Float; + break; + case ParticleAttribute::ValueTypes::Float2: + dst.Format = PixelFormat::R32G32_Float; + break; + case ParticleAttribute::ValueTypes::Float3: + dst.Format = PixelFormat::R32G32B32_Float; + break; + case ParticleAttribute::ValueTypes::Float4: + dst.Format = PixelFormat::R32G32B32A32_Float; + break; + case ParticleAttribute::ValueTypes::Int: + dst.Format = PixelFormat::R32_SInt; + break; + case ParticleAttribute::ValueTypes::Uint: + dst.Format = PixelFormat::R32_UInt; + break; + } + } + return result; +} + #endif diff --git a/Source/Engine/Particles/ParticleEmitter.h b/Source/Engine/Particles/ParticleEmitter.h index 1398de5db..23ee83e21 100644 --- a/Source/Engine/Particles/ParticleEmitter.h +++ b/Source/Engine/Particles/ParticleEmitter.h @@ -177,10 +177,16 @@ public: void GetReferences(Array& assets, Array& files) const override; bool Save(const StringView& path = StringView::Empty) override; - /// - /// Checks if the particle emitter has valid shader code present. - /// - API_PROPERTY() bool HasShaderCode() const; + API_STRUCT(Internal) struct Attribute + { + DECLARE_SCRIPTING_TYPE_MINIMAL(Attribute); + API_FIELD() PixelFormat Format; + API_FIELD() String Name; + }; + +private: + API_PROPERTY(Internal) bool HasShaderCode() const; + API_PROPERTY(Internal) Array GetLayout() const; #endif protected: diff --git a/Source/Engine/Particles/ParticlesData.cpp b/Source/Engine/Particles/ParticlesData.cpp index dcdef46d7..958ada7cf 100644 --- a/Source/Engine/Particles/ParticlesData.cpp +++ b/Source/Engine/Particles/ParticlesData.cpp @@ -6,6 +6,89 @@ #include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/DynamicBuffer.h" +int32 ParticleAttribute::GetSize() const +{ + switch (ValueType) + { + case ValueTypes::Float2: + return 8; + case ValueTypes::Float3: + return 12; + case ValueTypes::Float4: + return 16; + case ValueTypes::Float: + case ValueTypes::Int: + case ValueTypes::Uint: + return 4; + default: + return 0; + } +} + +void ParticleLayout::Clear() +{ + Size = 0; + Attributes.Clear(); +} + +void ParticleLayout::UpdateLayout() +{ + Size = 0; + for (int32 i = 0; i < Attributes.Count(); i++) + { + Attributes[i].Offset = Size; + Size += Attributes[i].GetSize(); + } +} + +int32 ParticleLayout::FindAttribute(const StringView& name) const +{ + for (int32 i = 0; i < Attributes.Count(); i++) + { + if (name == Attributes[i].Name) + return i; + } + return -1; +} + +int32 ParticleLayout::FindAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType) const +{ + for (int32 i = 0; i < Attributes.Count(); i++) + { + if (Attributes[i].ValueType == valueType && name == Attributes[i].Name) + return i; + } + return -1; +} + +int32 ParticleLayout::FindAttributeOffset(const StringView& name, int32 fallbackValue) const +{ + for (int32 i = 0; i < Attributes.Count(); i++) + { + if (name == Attributes[i].Name) + return Attributes[i].Offset; + } + return fallbackValue; +} + +int32 ParticleLayout::FindAttributeOffset(const StringView& name, ParticleAttribute::ValueTypes valueType, int32 fallbackValue) const +{ + for (int32 i = 0; i < Attributes.Count(); i++) + { + if (Attributes[i].ValueType == valueType && name == Attributes[i].Name) + return Attributes[i].Offset; + } + return fallbackValue; +} + +int32 ParticleLayout::AddAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType) +{ + auto& a = Attributes.AddOne(); + a.Name = String(*name, name.Length()); + a.ValueType = valueType; + return Attributes.Count() - 1; +} + ParticleBuffer::ParticleBuffer() { } diff --git a/Source/Engine/Particles/ParticlesData.h b/Source/Engine/Particles/ParticlesData.h index 2d81d70f0..53f63826f 100644 --- a/Source/Engine/Particles/ParticlesData.h +++ b/Source/Engine/Particles/ParticlesData.h @@ -52,25 +52,7 @@ struct ParticleAttribute /// /// Gets the size of the attribute (in bytes). /// - /// The size (in bytes). - int32 GetSize() const - { - switch (ValueType) - { - case ValueTypes::Float2: - return 8; - case ValueTypes::Float3: - return 12; - case ValueTypes::Float4: - return 16; - case ValueTypes::Float: - case ValueTypes::Int: - case ValueTypes::Uint: - return 4; - default: - return 0; - } - } + int32 GetSize() const; }; /// @@ -93,41 +75,19 @@ public: /// /// Clears the layout data. /// - void Clear() - { - Size = 0; - Attributes.Clear(); - } + void Clear(); /// /// Updates the attributes layout (calculates offset) and updates the total size of the layout. /// - void UpdateLayout() - { - Size = 0; - for (int32 i = 0; i < Attributes.Count(); i++) - { - Attributes[i].Offset = Size; - Size += Attributes[i].GetSize(); - } - } + void UpdateLayout(); /// /// Finds the attribute by the name. /// /// The name. /// The attribute index or -1 if cannot find it. - int32 FindAttribute(const StringView& name) const - { - for (int32 i = 0; i < Attributes.Count(); i++) - { - if (name == Attributes[i].Name) - { - return i; - } - } - return -1; - } + int32 FindAttribute(const StringView& name) const; /// /// Finds the attribute by the name and type. @@ -135,17 +95,7 @@ public: /// The name. /// The type. /// The attribute index or -1 if cannot find it. - int32 FindAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType) const - { - for (int32 i = 0; i < Attributes.Count(); i++) - { - if (Attributes[i].ValueType == valueType && name == Attributes[i].Name) - { - return i; - } - } - return -1; - } + int32 FindAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType) const; /// /// Finds the attribute offset by the name. @@ -153,17 +103,7 @@ public: /// The name. /// The fallback value to return if attribute is missing. /// The attribute offset or fallback value if cannot find it. - int32 FindAttributeOffset(const StringView& name, int32 fallbackValue = 0) const - { - for (int32 i = 0; i < Attributes.Count(); i++) - { - if (name == Attributes[i].Name) - { - return Attributes[i].Offset; - } - } - return fallbackValue; - } + int32 FindAttributeOffset(const StringView& name, int32 fallbackValue = 0) const; /// /// Finds the attribute offset by the name. @@ -172,17 +112,7 @@ public: /// The type. /// The fallback value to return if attribute is missing. /// The attribute offset or fallback value if cannot find it. - int32 FindAttributeOffset(const StringView& name, ParticleAttribute::ValueTypes valueType, int32 fallbackValue = 0) const - { - for (int32 i = 0; i < Attributes.Count(); i++) - { - if (Attributes[i].ValueType == valueType && name == Attributes[i].Name) - { - return Attributes[i].Offset; - } - } - return fallbackValue; - } + int32 FindAttributeOffset(const StringView& name, ParticleAttribute::ValueTypes valueType, int32 fallbackValue = 0) const; /// /// Gets the attribute offset by the attribute index. @@ -201,13 +131,7 @@ public: /// The name. /// The value type. /// The attribute index or -1 if cannot find it. - int32 AddAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType) - { - auto& a = Attributes.AddOne(); - a.Name = String(*name, name.Length()); - a.ValueType = valueType; - return Attributes.Count() - 1; - } + int32 AddAttribute(const StringView& name, ParticleAttribute::ValueTypes valueType); }; /// From a1e4ed05c42fc986f04233b89497ab90a3885862 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 14 Jul 2025 18:14:09 +0200 Subject: [PATCH 109/122] Don't force load asset on asset clone if it's unused --- Source/Engine/Content/Content.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index 2178579cc..48e7b6c1f 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -1003,7 +1003,7 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat FileSystem::DeleteFile(tmpPath); // Reload storage - if (auto storage = ContentStorageManager::GetStorage(dstPath)) + if (auto storage = ContentStorageManager::GetStorage(dstPath, false)) { storage->Reload(); } From bb37f980ed227d28d9e02f5a541f5cc1daea2b64 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 14 Jul 2025 20:25:49 +0200 Subject: [PATCH 110/122] Fix regression in debug draw from 4af26a5516e345f0fb2324c206104551a155b0c7 --- Source/Editor/Viewport/MainEditorGizmoViewport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index c14b6dfd8..bb01bc863 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -374,7 +374,7 @@ namespace FlaxEditor.Viewport // Draw selected objects debug shapes and visuals if (DrawDebugDraw && (renderContext.View.Flags & ViewFlags.DebugDraw) == ViewFlags.DebugDraw) { - _debugDrawData.DrawActors(); + _debugDrawData.DrawActors(true); DebugDraw.Draw(ref renderContext, target.View(), targetDepth.View(), true); } } From 8ed2d6da5695b3046e2e19758912b31a1f1d72e4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 14 Jul 2025 20:26:24 +0200 Subject: [PATCH 111/122] Optimize Debug Draw performance of splines to use frustum culling --- .../Viewport/MainEditorGizmoViewport.cs | 9 +++++++- .../Editor/Viewport/PrefabWindowViewport.cs | 7 +++++- .../Editor/Viewport/Previews/AssetPreview.cs | 1 + Source/Engine/Debug/DebugDraw.cpp | 22 ++++++++++++++----- Source/Engine/Debug/DebugDraw.h | 5 +++++ Source/Engine/Level/Actors/Spline.cpp | 4 +++- 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index c14b6dfd8..5343a1fe5 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -340,6 +340,13 @@ namespace FlaxEditor.Viewport { _debugDrawData.Clear(); + if (task is SceneRenderTask sceneRenderTask) + { + // Sync debug view to avoid lag on culling/LODing + var view = sceneRenderTask.View; + DebugDraw.SetView(ref view); + } + // Collect selected objects debug shapes and visuals var selectedParents = TransformGizmo.SelectedParents; if (selectedParents.Count > 0) @@ -374,7 +381,7 @@ namespace FlaxEditor.Viewport // Draw selected objects debug shapes and visuals if (DrawDebugDraw && (renderContext.View.Flags & ViewFlags.DebugDraw) == ViewFlags.DebugDraw) { - _debugDrawData.DrawActors(); + _debugDrawData.DrawActors(true); DebugDraw.Draw(ref renderContext, target.View(), targetDepth.View(), true); } } diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 8b508eedf..3968f0379 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -243,7 +243,12 @@ namespace FlaxEditor.Viewport _tempDebugDrawContext = DebugDraw.AllocateContext(); DebugDraw.SetContext(_tempDebugDrawContext); DebugDraw.UpdateContext(_tempDebugDrawContext, 1.0f); - + if (task is SceneRenderTask sceneRenderTask) + { + // Sync debug view to avoid lag on culling/LODing + var view = sceneRenderTask.View; + DebugDraw.SetView(ref view); + } for (int i = 0; i < selectedParents.Count; i++) { if (selectedParents[i].IsActiveInHierarchy) diff --git a/Source/Editor/Viewport/Previews/AssetPreview.cs b/Source/Editor/Viewport/Previews/AssetPreview.cs index 84f60f96a..3583a9ff0 100644 --- a/Source/Editor/Viewport/Previews/AssetPreview.cs +++ b/Source/Editor/Viewport/Previews/AssetPreview.cs @@ -264,6 +264,7 @@ namespace FlaxEditor.Viewport.Previews { DebugDraw.SetContext(_debugDrawContext); DebugDraw.UpdateContext(_debugDrawContext, 1.0f / Mathf.Max(Engine.FramesPerSecond, 1)); + DebugDraw.SetView(ref renderContext.View); CustomDebugDraw?.Invoke(context, ref renderContext); OnDebugDraw(context, ref renderContext); DebugDraw.Draw(ref renderContext, target.View(), targetDepth.View(), true); diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp index 752e8bf24..3e5601532 100644 --- a/Source/Engine/Debug/DebugDraw.cpp +++ b/Source/Engine/Debug/DebugDraw.cpp @@ -357,7 +357,8 @@ struct DebugDrawContext DebugDrawData DebugDrawDefault; DebugDrawData DebugDrawDepthTest; Float3 LastViewPos = Float3::Zero; - Matrix LastViewProj = Matrix::Identity; + Matrix LastViewProjection = Matrix::Identity; + BoundingFrustum LastViewFrustum; inline int32 Count() const { @@ -779,9 +780,23 @@ Vector3 DebugDraw::GetViewPos() return Context->LastViewPos; } +BoundingFrustum DebugDraw::GetViewFrustum() +{ + return Context->LastViewFrustum; +} + +void DebugDraw::SetView(const RenderView& view) +{ + Context->LastViewPos = view.Position; + Context->LastViewProjection = view.Projection; + Context->LastViewFrustum = view.Frustum; +} + void DebugDraw::Draw(RenderContext& renderContext, GPUTextureView* target, GPUTextureView* depthBuffer, bool enableDepthTest) { PROFILE_GPU_CPU("Debug Draw"); + const RenderView& view = renderContext.View; + SetView(view); // Ensure to have shader loaded and any lines to render const int32 debugDrawDepthTestCount = Context->DebugDrawDepthTest.Count(); @@ -791,7 +806,6 @@ void DebugDraw::Draw(RenderContext& renderContext, GPUTextureView* target, GPUTe if (renderContext.Buffers == nullptr || !DebugDrawVB) return; auto context = GPUDevice::Instance->GetMainContext(); - const RenderView& view = renderContext.View; if (Context->Origin != view.Origin) { // Teleport existing debug shapes to maintain their location @@ -800,8 +814,6 @@ void DebugDraw::Draw(RenderContext& renderContext, GPUTextureView* target, GPUTe Context->DebugDrawDepthTest.Teleport(delta); Context->Origin = view.Origin; } - Context->LastViewPos = view.Position; - Context->LastViewProj = view.Projection; TaaJitterRemoveContext taaJitterRemove(view); // Fallback to task buffers @@ -1383,7 +1395,7 @@ void DebugDraw::DrawWireSphere(const BoundingSphere& sphere, const Color& color, int32 index; const Float3 centerF = sphere.Center - Context->Origin; const float radiusF = (float)sphere.Radius; - const float screenRadiusSquared = RenderTools::ComputeBoundsScreenRadiusSquared(centerF, radiusF, Context->LastViewPos, Context->LastViewProj); + const float screenRadiusSquared = RenderTools::ComputeBoundsScreenRadiusSquared(centerF, radiusF, Context->LastViewPos, Context->LastViewProjection); if (screenRadiusSquared > DEBUG_DRAW_SPHERE_LOD0_SCREEN_SIZE * DEBUG_DRAW_SPHERE_LOD0_SCREEN_SIZE * 0.25f) index = 0; else if (screenRadiusSquared > DEBUG_DRAW_SPHERE_LOD1_SCREEN_SIZE * DEBUG_DRAW_SPHERE_LOD1_SCREEN_SIZE * 0.25f) diff --git a/Source/Engine/Debug/DebugDraw.h b/Source/Engine/Debug/DebugDraw.h index 3b51c0e13..3f4ff8aff 100644 --- a/Source/Engine/Debug/DebugDraw.h +++ b/Source/Engine/Debug/DebugDraw.h @@ -76,6 +76,11 @@ API_CLASS(Static) class FLAXENGINE_API DebugDraw // Gets the last view position when rendering the current context. Can be used for custom culling or LODing when drawing more complex shapes. static Vector3 GetViewPos(); + // Gets the last view frustum when rendering the current context. Can be used for custom culling or LODing when drawing more complex shapes. + static BoundingFrustum GetViewFrustum(); + + // Sets the rendering view information beforehand. + API_FUNCTION() static void SetView(API_PARAM(ref) const RenderView& view); /// /// Draws the collected debug shapes to the output. diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp index eb3df0771..8b690100c 100644 --- a/Source/Engine/Level/Actors/Spline.cpp +++ b/Source/Engine/Level/Actors/Spline.cpp @@ -3,6 +3,7 @@ #include "Spline.h" #include "Engine/Serialization/Serialization.h" #include "Engine/Animations/CurveSerialization.h" +#include "Engine/Core/Math/BoundingFrustum.h" #include "Engine/Core/Math/Matrix.h" #include "Engine/Scripting/ManagedCLR/MCore.h" @@ -520,7 +521,8 @@ namespace void Spline::OnDebugDraw() { - DrawSpline(this, GetSplineColor().AlphaMultiplied(0.7f), _transform, true); + if (DebugDraw::GetViewFrustum().Intersects(_sphere)) + DrawSpline(this, GetSplineColor().AlphaMultiplied(0.7f), _transform, true); // Base Actor::OnDebugDraw(); From a22b33d3bbc722cb4dca38bbc9882156077da45a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 14 Jul 2025 20:26:41 +0200 Subject: [PATCH 112/122] Fix missing sphere bounds update for splines --- Source/Engine/Level/Actors/Spline.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp index 8b690100c..7be63fd99 100644 --- a/Source/Engine/Level/Actors/Spline.cpp +++ b/Source/Engine/Level/Actors/Spline.cpp @@ -465,6 +465,7 @@ void Spline::UpdateSpline() Matrix world; GetLocalToWorldMatrix(world); BoundingBox::Transform(_localBounds, world, _box); + BoundingSphere::FromBox(_box, _sphere); SplineUpdated(); } @@ -564,6 +565,7 @@ void Spline::Initialize() Matrix world; GetLocalToWorldMatrix(world); BoundingBox::Transform(_localBounds, world, _box); + BoundingSphere::FromBox(_box, _sphere); } void Spline::Serialize(SerializeStream& stream, const void* otherObj) From 20f1e67700dd884be28ecdd25ff4f750ebd81663 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 14 Jul 2025 21:09:28 +0200 Subject: [PATCH 113/122] Optimize Spline debug rendering far from camera --- Source/Engine/Level/Actors/Spline.cpp | 36 ++++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp index 7be63fd99..25bdc4ce9 100644 --- a/Source/Engine/Level/Actors/Spline.cpp +++ b/Source/Engine/Level/Actors/Spline.cpp @@ -6,6 +6,7 @@ #include "Engine/Core/Math/BoundingFrustum.h" #include "Engine/Core/Math/Matrix.h" #include "Engine/Scripting/ManagedCLR/MCore.h" +#include "Engine/Engine/Units.h" Spline::Spline(const SpawnParams& params) : Actor(params) @@ -506,16 +507,33 @@ namespace return; Spline::Keyframe* prev = spline->Curve.GetKeyframes().Get(); Vector3 prevPos = transform.LocalToWorld(prev->Value.Translation); - DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(prevPos, NodeSizeByDistance(prevPos, scaleByDistance)), color, 0.0f, depthTest); - for (int32 i = 1; i < count; i++) + float distance = Vector3::Distance(prevPos, DebugDraw::GetViewPos()); + if (distance < METERS_TO_UNITS(800)) // 800m { - Spline::Keyframe* next = prev + 1; - Vector3 nextPos = transform.LocalToWorld(next->Value.Translation); - DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(nextPos, NodeSizeByDistance(nextPos, scaleByDistance)), color, 0.0f, depthTest); - const float d = (next->Time - prev->Time) / 3.0f; - DEBUG_DRAW_BEZIER(prevPos, prevPos + prev->TangentOut.Translation * d, nextPos + next->TangentIn.Translation * d, nextPos, color, 0.0f, depthTest); - prev = next; - prevPos = nextPos; + // Bezier curve + DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(prevPos, NodeSizeByDistance(prevPos, scaleByDistance)), color, 0.0f, depthTest); + for (int32 i = 1; i < count; i++) + { + Spline::Keyframe* next = prev + 1; + Vector3 nextPos = transform.LocalToWorld(next->Value.Translation); + DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(nextPos, NodeSizeByDistance(nextPos, scaleByDistance)), color, 0.0f, depthTest); + const float d = (next->Time - prev->Time) / 3.0f; + DEBUG_DRAW_BEZIER(prevPos, prevPos + prev->TangentOut.Translation * d, nextPos + next->TangentIn.Translation * d, nextPos, color, 0.0f, depthTest); + prev = next; + prevPos = nextPos; + } + } + else + { + // Simplified + for (int32 i = 1; i < count; i++) + { + Spline::Keyframe* next = prev + 1; + Vector3 nextPos = transform.LocalToWorld(next->Value.Translation); + DEBUG_DRAW_LINE(prevPos, nextPos, color, 0.0f, depthTest); + prev = next; + prevPos = nextPos; + } } } } From ab8612a914398aa9e4017b3bbe88a455471ae10e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 14 Jul 2025 22:24:27 +0200 Subject: [PATCH 114/122] Add profiler events to editor gizmo --- Source/Editor/Gizmo/TransformGizmo.cs | 3 +++ Source/Editor/SceneGraph/RootNode.cs | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Gizmo/TransformGizmo.cs b/Source/Editor/Gizmo/TransformGizmo.cs index 8294a4302..91e37ca25 100644 --- a/Source/Editor/Gizmo/TransformGizmo.cs +++ b/Source/Editor/Gizmo/TransformGizmo.cs @@ -155,6 +155,7 @@ namespace FlaxEditor.Gizmo // Ensure player is not moving objects if (ActiveAxis != Axis.None) return; + Profiler.BeginEvent("Pick"); // Get mouse ray and try to hit any object var ray = Owner.MouseRay; @@ -243,6 +244,8 @@ namespace FlaxEditor.Gizmo { sceneEditing.Deselect(); } + + Profiler.EndEvent(); } /// diff --git a/Source/Editor/SceneGraph/RootNode.cs b/Source/Editor/SceneGraph/RootNode.cs index 1a3e47be8..30e469657 100644 --- a/Source/Editor/SceneGraph/RootNode.cs +++ b/Source/Editor/SceneGraph/RootNode.cs @@ -97,13 +97,16 @@ namespace FlaxEditor.SceneGraph /// Hit object or null if there is no intersection at all. public SceneGraphNode RayCast(ref Ray ray, ref Ray view, out Real distance, RayCastData.FlagTypes flags = RayCastData.FlagTypes.None) { + Profiler.BeginEvent("RayCastScene"); var data = new RayCastData { Ray = ray, View = view, Flags = flags }; - return RayCast(ref data, out distance, out _); + var result = RayCast(ref data, out distance, out _); + Profiler.EndEvent(); + return result; } /// @@ -117,13 +120,16 @@ namespace FlaxEditor.SceneGraph /// Hit object or null if there is no intersection at all. public SceneGraphNode RayCast(ref Ray ray, ref Ray view, out Real distance, out Vector3 normal, RayCastData.FlagTypes flags = RayCastData.FlagTypes.None) { + Profiler.BeginEvent("RayCastScene"); var data = new RayCastData { Ray = ray, View = view, Flags = flags }; - return RayCast(ref data, out distance, out normal); + var result = RayCast(ref data, out distance, out normal); + Profiler.EndEvent(); + return result; } internal static Quaternion RaycastNormalRotation(ref Vector3 normal) From c0cce748cc094f8d7a32b744f8d65ab351e2bb86 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 15 Jul 2025 00:12:31 +0200 Subject: [PATCH 115/122] Optimize `Array::RemoveAtKeepOrder` --- Source/Engine/Core/Collections/Array.h | 8 +--- Source/Engine/Core/Memory/Memory.h | 63 ++++++++------------------ 2 files changed, 19 insertions(+), 52 deletions(-) diff --git a/Source/Engine/Core/Collections/Array.h b/Source/Engine/Core/Collections/Array.h index 7cb4a3248..4f660d2a9 100644 --- a/Source/Engine/Core/Collections/Array.h +++ b/Source/Engine/Core/Collections/Array.h @@ -658,13 +658,7 @@ public: --_count; T* data = _allocation.Get(); if (index < _count) - { - T* dst = data + index; - T* src = data + (index + 1); - const int32 count = _count - index; - for (int32 i = 0; i < count; ++i) - dst[i] = MoveTemp(src[i]); - } + Memory::MoveAssignItems(data + index, data + (index + 1), _count - index); Memory::DestructItems(data + _count, 1); } diff --git a/Source/Engine/Core/Memory/Memory.h b/Source/Engine/Core/Memory/Memory.h index 329ee372d..772c9fb48 100644 --- a/Source/Engine/Core/Memory/Memory.h +++ b/Source/Engine/Core/Memory/Memory.h @@ -104,12 +104,6 @@ public: { new(dst) T(); } - - /// - /// Constructs the item in the memory. - /// - /// The optimized version is noop. - /// The address of the memory location to construct. template FORCE_INLINE static typename TEnableIf::Value>::Type ConstructItem(T* dst) { @@ -132,13 +126,6 @@ public: ++(T*&)dst; } } - - /// - /// Constructs the range of items in the memory. - /// - /// The optimized version is noop. - /// The address of the first memory location to construct. - /// The number of element to construct. Can be equal 0. template FORCE_INLINE static typename TEnableIf::Value>::Type ConstructItems(T* dst, int32 count) { @@ -163,14 +150,6 @@ public: ++src; } } - - /// - /// Constructs the range of items in the memory from the set of arguments. - /// - /// The optimized version uses low-level memory copy. - /// The address of the first memory location to construct. - /// The address of the first memory location to pass to the constructor. - /// The number of element to construct. Can be equal 0. template FORCE_INLINE static typename TEnableIf::Value>::Type ConstructItems(T* dst, const U* src, int32 count) { @@ -187,12 +166,6 @@ public: { dst->~T(); } - - /// - /// Destructs the item in the memory. - /// - /// The optimized version is noop. - /// The address of the memory location to destruct. template FORCE_INLINE static typename TEnableIf::Value>::Type DestructItem(T* dst) { @@ -213,13 +186,6 @@ public: ++dst; } } - - /// - /// Destructs the range of items in the memory. - /// - /// The optimized version is noop. - /// The address of the first memory location to destruct. - /// The number of element to destruct. Can be equal 0. template FORCE_INLINE static typename TEnableIf::Value>::Type DestructItems(T* dst, int32 count) { @@ -242,15 +208,7 @@ public: ++src; } } - - /// - /// Copies the range of items using the assignment operator. - /// - /// The optimized version is low-level memory copy. - /// The address of the first memory location to start assigning to. - /// The address of the first memory location to assign from. - /// The number of element to assign. Can be equal 0. - template + template FORCE_INLINE static typename TEnableIf::Value>::Type CopyItems(T* dst, const T* src, int32 count) { Platform::MemoryCopy(dst, src, count * sizeof(T)); @@ -273,16 +231,31 @@ public: ++src; } } + template + FORCE_INLINE static typename TEnableIf::Value>::Type MoveItems(T* dst, U* src, int32 count) + { + Platform::MemoryCopy(dst, src, count * sizeof(U)); + } /// - /// Moves the range of items in the memory from the set of arguments. + /// Moves the range of items using the assignment operator. /// /// The optimized version uses low-level memory copy. /// The address of the first memory location to move. /// The address of the first memory location to pass to the move constructor. /// The number of element to move. Can be equal 0. template - FORCE_INLINE static typename TEnableIf::Value>::Type MoveItems(T* dst, U* src, int32 count) + FORCE_INLINE static typename TEnableIf::Value>::Type MoveAssignItems(T* dst, U* src, int32 count) + { + while (count--) + { + *dst = MoveTemp(*src); + ++(T*&)dst; + ++src; + } + } + template + FORCE_INLINE static typename TEnableIf::Value>::Type MoveAssignItems(T* dst, U* src, int32 count) { Platform::MemoryCopy(dst, src, count * sizeof(U)); } From 9646dd3fc2cf95845a8c6e75a277de9df0ff84f2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 15 Jul 2025 12:33:33 +0200 Subject: [PATCH 116/122] Fix `AutoFocus` on `ContainerControl` to be `false` by default --- Source/Editor/GUI/ContextMenu/ContextMenuBase.cs | 3 ++- Source/Editor/GUI/Dialogs/ColorSelector.cs | 2 ++ Source/Editor/GUI/Docking/DockPanelProxy.cs | 2 -- Source/Editor/GUI/Input/SliderControl.cs | 2 ++ Source/Editor/GUI/Tree/TreeNode.cs | 2 ++ Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs | 2 ++ Source/Editor/Surface/Elements/BoxValue.cs | 1 + Source/Editor/Surface/SurfaceControl.cs | 1 + Source/Engine/UI/GUI/Common/Dropdown.cs | 2 ++ Source/Engine/UI/GUI/Common/Slider.cs | 2 ++ Source/Engine/UI/GUI/Common/TextBoxBase.cs | 2 ++ Source/Engine/UI/GUI/ContainerControl.cs | 4 ++++ Source/Engine/UI/GUI/Special/RadialMenu.cs | 2 ++ 13 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index d7534b15b..0261792e7 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -114,9 +114,10 @@ namespace FlaxEditor.GUI.ContextMenu public ContextMenuBase() : base(0, 0, 120, 32) { - _direction = ContextMenuDirection.RightDown; Visible = false; + AutoFocus = true; + _direction = ContextMenuDirection.RightDown; _isSubMenu = true; } diff --git a/Source/Editor/GUI/Dialogs/ColorSelector.cs b/Source/Editor/GUI/Dialogs/ColorSelector.cs index 126959d85..f556e8cd6 100644 --- a/Source/Editor/GUI/Dialogs/ColorSelector.cs +++ b/Source/Editor/GUI/Dialogs/ColorSelector.cs @@ -76,6 +76,8 @@ namespace FlaxEditor.GUI.Dialogs public ColorSelector(float wheelSize) : base(0, 0, wheelSize, wheelSize) { + AutoFocus = true; + _colorWheelSprite = Editor.Instance.Icons.ColorWheel128; _wheelRect = new Rectangle(0, 0, wheelSize, wheelSize); } diff --git a/Source/Editor/GUI/Docking/DockPanelProxy.cs b/Source/Editor/GUI/Docking/DockPanelProxy.cs index dbb4e082e..5cf64266a 100644 --- a/Source/Editor/GUI/Docking/DockPanelProxy.cs +++ b/Source/Editor/GUI/Docking/DockPanelProxy.cs @@ -65,8 +65,6 @@ namespace FlaxEditor.GUI.Docking internal DockPanelProxy(DockPanel panel) : base(0, 0, 64, 64) { - AutoFocus = false; - _panel = panel; AnchorPreset = AnchorPresets.StretchAll; Offsets = Margin.Zero; diff --git a/Source/Editor/GUI/Input/SliderControl.cs b/Source/Editor/GUI/Input/SliderControl.cs index 8e3efe956..4703da73d 100644 --- a/Source/Editor/GUI/Input/SliderControl.cs +++ b/Source/Editor/GUI/Input/SliderControl.cs @@ -368,6 +368,8 @@ namespace FlaxEditor.GUI.Input public SliderControl(float value, float x = 0, float y = 0, float width = 120, float min = float.MinValue, float max = float.MaxValue) : base(x, y, width, TextBox.DefaultHeight) { + AutoFocus = true; + _min = min; _max = max; _value = Mathf.Clamp(value, min, max); diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs index 40c276bf4..ed1257819 100644 --- a/Source/Editor/GUI/Tree/TreeNode.cs +++ b/Source/Editor/GUI/Tree/TreeNode.cs @@ -319,6 +319,8 @@ namespace FlaxEditor.GUI.Tree public TreeNode(bool canChangeOrder, SpriteHandle iconCollapsed, SpriteHandle iconOpened) : base(0, 0, 64, 16) { + AutoFocus = true; + _canChangeOrder = canChangeOrder; _animationProgress = 1.0f; _cachedHeight = _headerHeight; diff --git a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs index bd92bd266..450960af7 100644 --- a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs +++ b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs @@ -233,6 +233,8 @@ namespace FlaxEditor.Surface.Archetypes public BlendPointsEditor(Animation.MultiBlend node, bool is2D, float x, float y, float width, float height) : base(x, y, width, height) { + AutoFocus = true; + _node = node; _is2D = is2D; } diff --git a/Source/Editor/Surface/Elements/BoxValue.cs b/Source/Editor/Surface/Elements/BoxValue.cs index 597c11b9e..05d39580c 100644 --- a/Source/Editor/Surface/Elements/BoxValue.cs +++ b/Source/Editor/Surface/Elements/BoxValue.cs @@ -64,6 +64,7 @@ namespace FlaxEditor.Surface.Elements { ParentNode = parentNode; Archetype = archetype; + AutoFocus = true; var back = Style.Current.TextBoxBackground; var grayOutFactor = 0.6f; diff --git a/Source/Editor/Surface/SurfaceControl.cs b/Source/Editor/Surface/SurfaceControl.cs index 46cb94577..ce5b17d86 100644 --- a/Source/Editor/Surface/SurfaceControl.cs +++ b/Source/Editor/Surface/SurfaceControl.cs @@ -59,6 +59,7 @@ namespace FlaxEditor.Surface protected SurfaceControl(VisjectSurfaceContext context, float width, float height) : base(0, 0, width, height) { + AutoFocus = true; ClipChildren = false; Surface = context.Surface; diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index c0a81eacf..868db4b89 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -381,6 +381,8 @@ namespace FlaxEngine.GUI public Dropdown() : base(0, 0, 120, 18.0f) { + AutoFocus = true; + var style = Style.Current; Font = new FontReference(style.FontMedium); TextColor = style.Foreground; diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs index c1e7cd9f6..e02b67ab2 100644 --- a/Source/Engine/UI/GUI/Common/Slider.cs +++ b/Source/Engine/UI/GUI/Common/Slider.cs @@ -298,6 +298,8 @@ public class Slider : ContainerControl public Slider(float width, float height) : base(0, 0, width, height) { + AutoFocus = true; + var style = Style.Current; TrackLineColor = style.BackgroundHighlighted; TrackFillLineColor = style.LightBackground; diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index 237a1bb5f..243e4786e 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -511,6 +511,8 @@ namespace FlaxEngine.GUI protected TextBoxBase(bool isMultiline, float x, float y, float width = 120) : base(x, y, width, DefaultHeight) { + AutoFocus = true; + _isMultiline = isMultiline; _maxLength = 2147483646; _selectionStart = _selectionEnd = -1; diff --git a/Source/Engine/UI/GUI/ContainerControl.cs b/Source/Engine/UI/GUI/ContainerControl.cs index 017b8ee5c..ada93ff1e 100644 --- a/Source/Engine/UI/GUI/ContainerControl.cs +++ b/Source/Engine/UI/GUI/ContainerControl.cs @@ -38,6 +38,7 @@ namespace FlaxEngine.GUI public ContainerControl() { _isLayoutLocked = true; + AutoFocus = false; } /// @@ -47,6 +48,7 @@ namespace FlaxEngine.GUI : base(x, y, width, height) { _isLayoutLocked = true; + AutoFocus = false; } /// @@ -56,6 +58,7 @@ namespace FlaxEngine.GUI : base(location, size) { _isLayoutLocked = true; + AutoFocus = false; } /// @@ -63,6 +66,7 @@ namespace FlaxEngine.GUI : base(bounds) { _isLayoutLocked = true; + AutoFocus = false; } /// diff --git a/Source/Engine/UI/GUI/Special/RadialMenu.cs b/Source/Engine/UI/GUI/Special/RadialMenu.cs index 3fbaa2fe2..060c4ef90 100644 --- a/Source/Engine/UI/GUI/Special/RadialMenu.cs +++ b/Source/Engine/UI/GUI/Special/RadialMenu.cs @@ -165,6 +165,8 @@ namespace FlaxEngine.GUI public RadialMenu(float x, float y, float width = 100, float height = 100) : base(x, y, width, height) { + AutoFocus = true; + var style = Style.Current; if (style != null) { From c882b547c8709e431ad4f40f731fb6b5fb14b557 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 15 Jul 2025 12:34:02 +0200 Subject: [PATCH 117/122] Fix game UI focus loss when hiding focused control to maintain gamepad navigation in Editor --- Source/Editor/Windows/GameWindow.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index acb2deda3..5b243cfed 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -1176,6 +1176,12 @@ namespace FlaxEditor.Windows if (!_cursorVisible) Screen.CursorVisible = true; } + + if (Editor.IsPlayMode && IsDocked && IsSelected && RootWindow.FocusedControl == null) + { + // Game UI cleared focus so regain it to maintain UI navigation just like game window does + FlaxEngine.Scripting.InvokeOnUpdate(Focus); + } } /// From 3e0c085bf399a416db4738eb1b3074d18a1527ce Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 15 Jul 2025 12:34:40 +0200 Subject: [PATCH 118/122] Add error log when adding/removign actors during rendering or when `ConcurrentSystemLocker` deadlocks --- Source/Engine/Level/Scene/SceneRendering.cpp | 18 ++++++++++++- .../Threading/ConcurrentSystemLocker.cpp | 26 +++++++++++++++++++ .../Engine/Threading/ConcurrentSystemLocker.h | 1 + 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Level/Scene/SceneRendering.cpp b/Source/Engine/Level/Scene/SceneRendering.cpp index e55dbd43f..d7225036c 100644 --- a/Source/Engine/Level/Scene/SceneRendering.cpp +++ b/Source/Engine/Level/Scene/SceneRendering.cpp @@ -7,9 +7,23 @@ #include "Engine/Graphics/RenderView.h" #include "Engine/Renderer/RenderList.h" #include "Engine/Threading/JobSystem.h" -#include "Engine/Threading/Threading.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" +#if !BUILD_RELEASE +#include "Engine/Graphics/GPUDevice.h" +#include "Engine/Core/Log.h" +#endif + +#if BUILD_RELEASE +#define CHECK_SCENE_EDIT_ACCESS() +#else +#define CHECK_SCENE_EDIT_ACCESS() \ + if (Locker.HasLock(false) && IsInMainThread() && GPUDevice::Instance && GPUDevice::Instance->IsRendering()) \ + { \ + LOG(Error, "Adding/removing actors during rendering is not supported ({}, '{}').", a->ToString(), a->GetNamePath()); \ + return; \ + } +#endif ISceneRenderingListener::~ISceneRenderingListener() { @@ -148,6 +162,7 @@ void SceneRendering::AddActor(Actor* a, int32& key) if (key != -1) return; PROFILE_MEM(Graphics); + CHECK_SCENE_EDIT_ACCESS(); const int32 category = a->_drawCategory; ConcurrentSystemLocker::WriteScope lock(Locker, true); auto& list = Actors[category]; @@ -192,6 +207,7 @@ void SceneRendering::UpdateActor(Actor* a, int32& key, ISceneRenderingListener:: void SceneRendering::RemoveActor(Actor* a, int32& key) { + CHECK_SCENE_EDIT_ACCESS(); const int32 category = a->_drawCategory; ConcurrentSystemLocker::WriteScope lock(Locker, true); auto& list = Actors[category]; diff --git a/Source/Engine/Threading/ConcurrentSystemLocker.cpp b/Source/Engine/Threading/ConcurrentSystemLocker.cpp index d936f8307..cafc3dadb 100644 --- a/Source/Engine/Threading/ConcurrentSystemLocker.cpp +++ b/Source/Engine/Threading/ConcurrentSystemLocker.cpp @@ -2,6 +2,9 @@ #include "ConcurrentSystemLocker.h" #include "Engine/Platform/Platform.h" +#if !BUILD_RELEASE +#include "Engine/Core/Log.h" +#endif ConcurrentSystemLocker::ConcurrentSystemLocker() { @@ -12,7 +15,25 @@ void ConcurrentSystemLocker::Begin(bool write, bool exclusively) { volatile int64* thisCounter = &_counters[write]; volatile int64* otherCounter = &_counters[!write]; + +#if !BUILD_RELEASE + int32 retries = 0; + double startTime = Platform::GetTimeSeconds(); +#endif RETRY: +#if !BUILD_RELEASE + retries++; + if (retries > 1000) + { + double endTime = Platform::GetTimeSeconds(); + if (endTime - startTime > 0.5f) + { + LOG(Error, "Deadlock detected in ConcurrentSystemLocker! Thread 0x{0:x} waits for {1} ms...", Platform::GetCurrentThreadID(), (int32)((endTime - startTime) * 1000.0)); + retries = 0; + } + } +#endif + // Check if we can enter (cannot read while someone else is writing and vice versa) if (Platform::AtomicRead(otherCounter) != 0) { @@ -47,3 +68,8 @@ void ConcurrentSystemLocker::End(bool write) // Mark that we left this section Platform::InterlockedDecrement(&_counters[write]); } + +bool ConcurrentSystemLocker::HasLock(bool write) const +{ + return Platform::AtomicRead(&_counters[write]) != 0; +} diff --git a/Source/Engine/Threading/ConcurrentSystemLocker.h b/Source/Engine/Threading/ConcurrentSystemLocker.h index 031b7e685..0b46a64f5 100644 --- a/Source/Engine/Threading/ConcurrentSystemLocker.h +++ b/Source/Engine/Threading/ConcurrentSystemLocker.h @@ -19,6 +19,7 @@ public: void Begin(bool write, bool exclusively = false); void End(bool write); + bool HasLock(bool write) const; public: template From 2d2c5411cdbf0adf23c347478e414d194c397985 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 15 Jul 2025 12:49:05 +0200 Subject: [PATCH 119/122] Add variable DDGI probe size in debug view based on cascade --- Content/Editor/DebugMaterials/DDGIDebugProbes.flax | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Content/Editor/DebugMaterials/DDGIDebugProbes.flax b/Content/Editor/DebugMaterials/DDGIDebugProbes.flax index ee6bd8d7b..4289244c8 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:fb32df5fa9255c8d27f968819ab7f59236640661c83ae33588733542ea635b0f -size 40232 +oid sha256:740621fb235edae990ffa259a833b12001eb5027bc6036af0aa34ebca4bcec64 +size 40805 From 7fd278a68917130551ddd02e7d260f810932e4d5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 15 Jul 2025 13:34:43 +0200 Subject: [PATCH 120/122] Fix .NET version to use selection for consoles with fixed setup --- Source/Editor/Cooker/CookingData.h | 9 ++------- Source/Editor/Cooker/GameCooker.cpp | 8 ++++++++ .../Cooker/Platform/GDK/GDKPlatformTools.cpp | 5 +++++ .../Editor/Cooker/Platform/GDK/GDKPlatformTools.h | 1 + Source/Editor/Cooker/PlatformTools.h | 14 ++++++++++++++ Source/Editor/Cooker/Steps/CompileScriptsStep.cpp | 2 +- Source/Editor/Cooker/Steps/DeployDataStep.cpp | 13 ++++++++----- .../Cooker/Steps/PrecompileAssembliesStep.cpp | 2 +- 8 files changed, 40 insertions(+), 14 deletions(-) diff --git a/Source/Editor/Cooker/CookingData.h b/Source/Editor/Cooker/CookingData.h index 13db5bb3f..1ecfaf353 100644 --- a/Source/Editor/Cooker/CookingData.h +++ b/Source/Editor/Cooker/CookingData.h @@ -20,13 +20,6 @@ class PlatformTools; #define GAME_BUILD_DOTNET_RUNTIME_MAX_VER 9 #endif -#if OFFICIAL_BUILD -// Use the fixed .NET SDK version in packaged builds for compatibility (FlaxGame is precompiled with it) -#define GAME_BUILD_DOTNET_VER TEXT("-dotnet=" MACRO_TO_STR(GAME_BUILD_DOTNET_RUNTIME_MIN_VER)) -#else -#define GAME_BUILD_DOTNET_VER TEXT("") -#endif - /// /// Game building options. Used as flags. /// @@ -374,6 +367,8 @@ public: /// void GetBuildPlatformName(const Char*& platform, const Char*& architecture) const; + String GetDotnetCommandArg() const; + public: /// diff --git a/Source/Editor/Cooker/GameCooker.cpp b/Source/Editor/Cooker/GameCooker.cpp index db8ded610..c204efac5 100644 --- a/Source/Editor/Cooker/GameCooker.cpp +++ b/Source/Editor/Cooker/GameCooker.cpp @@ -312,6 +312,14 @@ void CookingData::GetBuildPlatformName(const Char*& platform, const Char*& archi } } +String CookingData::GetDotnetCommandArg() const +{ + int32 version = Tools->GetDotnetVersion(); + if (version == 0) + return String::Empty; + return String::Format(TEXT("-dotnet={}"), version); +} + void CookingData::StepProgress(const String& info, const float stepProgress) const { const float singleStepProgress = 1.0f / (StepsCount + 1); diff --git a/Source/Editor/Cooker/Platform/GDK/GDKPlatformTools.cpp b/Source/Editor/Cooker/Platform/GDK/GDKPlatformTools.cpp index eea89a794..f9a8f1b82 100644 --- a/Source/Editor/Cooker/Platform/GDK/GDKPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/GDK/GDKPlatformTools.cpp @@ -195,4 +195,9 @@ bool GDKPlatformTools::OnPostProcess(CookingData& data, GDKPlatformSettings* pla return false; } +int32 GDKPlatformTools::GetDotnetVersion() const +{ + return GAME_BUILD_DOTNET_RUNTIME_MIN_VER; +} + #endif diff --git a/Source/Editor/Cooker/Platform/GDK/GDKPlatformTools.h b/Source/Editor/Cooker/Platform/GDK/GDKPlatformTools.h index 11763fce4..d2c137664 100644 --- a/Source/Editor/Cooker/Platform/GDK/GDKPlatformTools.h +++ b/Source/Editor/Cooker/Platform/GDK/GDKPlatformTools.h @@ -26,6 +26,7 @@ public: public: // [PlatformTools] + int32 GetDotnetVersion() const override; DotNetAOTModes UseAOT() const override; bool OnDeployBinaries(CookingData& data) override; }; diff --git a/Source/Editor/Cooker/PlatformTools.h b/Source/Editor/Cooker/PlatformTools.h index 9d6a50aa9..dfe7b6ffb 100644 --- a/Source/Editor/Cooker/PlatformTools.h +++ b/Source/Editor/Cooker/PlatformTools.h @@ -70,6 +70,20 @@ public: /// virtual ArchitectureType GetArchitecture() const = 0; + /// + /// Gets the .Net version to use for the cooked game. + /// + virtual int32 GetDotnetVersion() const + { +#if OFFICIAL_BUILD + // Use the fixed .NET SDK version in packaged builds for compatibility (FlaxGame is precompiled with it) + return GAME_BUILD_DOTNET_RUNTIME_MIN_VER; +#else + // Use the highest version found on a system (Flax.Build will decide) + return 0; +#endif + } + /// /// Gets the value indicating whenever platform requires AOT (needs C# assemblies to be precompiled). /// diff --git a/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp b/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp index eaf5b2863..c0a8e452a 100644 --- a/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp +++ b/Source/Editor/Cooker/Steps/CompileScriptsStep.cpp @@ -189,7 +189,7 @@ bool CompileScriptsStep::Perform(CookingData& data) const String logFile = data.CacheDirectory / TEXT("CompileLog.txt"); auto args = String::Format( TEXT("-log -logfile=\"{4}\" -build -mutex -buildtargets={0} -platform={1} -arch={2} -configuration={3} -aotMode={5} {6}"), - target, platform, architecture, configuration, logFile, ToString(data.Tools->UseAOT()), GAME_BUILD_DOTNET_VER); + target, platform, architecture, configuration, logFile, ToString(data.Tools->UseAOT()), data.GetDotnetCommandArg()); #if PLATFORM_WINDOWS if (data.Platform == BuildPlatform::LinuxX64) #elif PLATFORM_LINUX diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index ac271ab7d..b5b24c251 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -88,7 +88,7 @@ bool DeployDataStep::Perform(CookingData& data) { // Ask Flax.Build to provide .NET SDK location for the current platform String sdks; - bool failed = ScriptsBuilder::RunBuildTool(String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs {}"), GAME_BUILD_DOTNET_VER), data.CacheDirectory); + bool failed = ScriptsBuilder::RunBuildTool(String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs {}"), data.GetDotnetCommandArg()), data.CacheDirectory); failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks); int32 idx = sdks.Find(TEXT("DotNetSdk, "), StringSearchCase::CaseSensitive); if (idx != -1) @@ -200,7 +200,7 @@ bool DeployDataStep::Perform(CookingData& data) String sdks; const Char *platformName, *archName; data.GetBuildPlatformName(platformName, archName); - String args = String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printDotNetRuntime -platform={} -arch={} {}"), platformName, archName, GAME_BUILD_DOTNET_VER); + String args = String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printDotNetRuntime -platform={} -arch={} {}"), platformName, archName, data.GetDotnetCommandArg()); bool failed = ScriptsBuilder::RunBuildTool(args, data.CacheDirectory); failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks); Array parts; @@ -244,10 +244,13 @@ bool DeployDataStep::Perform(CookingData& data) } if (version.IsEmpty()) { + int32 minVer = GAME_BUILD_DOTNET_RUNTIME_MIN_VER, maxVer = GAME_BUILD_DOTNET_RUNTIME_MAX_VER; if (srcDotnetFromEngine) { // Detect version from runtime files inside Engine Platform folder - for (int32 i = GAME_BUILD_DOTNET_RUNTIME_MAX_VER; i >= GAME_BUILD_DOTNET_RUNTIME_MIN_VER; i--) + if (data.Tools->GetDotnetVersion() != 0) + minVer = maxVer = data.Tools->GetDotnetVersion(); + for (int32 i = maxVer; i >= minVer; i--) { // Check runtime files inside Engine Platform folder String testPath1 = srcDotnet / String::Format(TEXT("lib/net{}.0"), i); @@ -262,7 +265,7 @@ bool DeployDataStep::Perform(CookingData& data) } if (version.IsEmpty()) { - data.Error(String::Format(TEXT("Failed to find supported .NET {} version for the current host platform."), GAME_BUILD_DOTNET_RUNTIME_MIN_VER)); + data.Error(String::Format(TEXT("Failed to find supported .NET {} version (min {}) for the current host platform."), maxVer, minVer)); return true; } } @@ -364,7 +367,7 @@ bool DeployDataStep::Perform(CookingData& data) const String logFile = data.CacheDirectory / TEXT("StripDotnetLibs.txt"); String args = String::Format( TEXT("-log -logfile=\"{}\" -runDotNetClassLibStripping -mutex -binaries=\"{}\" {}"), - logFile, data.DataOutputPath, GAME_BUILD_DOTNET_VER); + logFile, data.DataOutputPath, data.GetDotnetCommandArg()); for (const String& define : data.CustomDefines) { args += TEXT(" -D"); diff --git a/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp b/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp index 1c8e321aa..1a4f67c01 100644 --- a/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp +++ b/Source/Editor/Cooker/Steps/PrecompileAssembliesStep.cpp @@ -69,7 +69,7 @@ bool PrecompileAssembliesStep::Perform(CookingData& data) const String logFile = data.CacheDirectory / TEXT("AOTLog.txt"); String args = String::Format( TEXT("-log -logfile=\"{}\" -runDotNetAOT -mutex -platform={} -arch={} -configuration={} -aotMode={} -binaries=\"{}\" -intermediate=\"{}\" {}"), - logFile, platform, architecture, configuration, ToString(aotMode), data.DataOutputPath, data.ManagedCodeOutputPath, GAME_BUILD_DOTNET_VER); + logFile, platform, architecture, configuration, ToString(aotMode), data.DataOutputPath, data.ManagedCodeOutputPath, data.GetDotnetCommandArg()); if (!buildSettings.SkipUnusedDotnetLibsPackaging) args += TEXT(" -skipUnusedDotnetLibs=false"); // Run AOT on whole class library (not just used libs) for (const String& define : data.CustomDefines) From 2754d61c05b8d526b94aa300eddb66586611b294 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 15 Jul 2025 19:31:07 +0200 Subject: [PATCH 121/122] Fix building Tracy for Switch --- Source/ThirdParty/tracy/client/TracyProfiler.cpp | 6 ++++-- Source/ThirdParty/tracy/tracy.Build.cs | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Source/ThirdParty/tracy/client/TracyProfiler.cpp b/Source/ThirdParty/tracy/client/TracyProfiler.cpp index c96fc5beb..837b36cc3 100644 --- a/Source/ThirdParty/tracy/client/TracyProfiler.cpp +++ b/Source/ThirdParty/tracy/client/TracyProfiler.cpp @@ -1480,7 +1480,7 @@ Profiler::Profiler() m_safeSendBuffer = (char*)tracy_malloc( SafeSendBufferSize ); -#ifndef _WIN32 +#ifndef TRACY_NO_PIPE pipe(m_pipe); # if defined __APPLE__ || defined BSD // FreeBSD/XNU don't have F_SETPIPE_SZ, so use the default @@ -1642,7 +1642,7 @@ Profiler::~Profiler() tracy_free( m_kcore ); #endif -#ifndef _WIN32 +#ifndef TRACY_NO_PIPE close( m_pipe[0] ); close( m_pipe[1] ); #endif @@ -3139,6 +3139,8 @@ char* Profiler::SafeCopyProlog( const char* data, size_t size ) { success = false; } +#elif !defined(TRACY_NO_PIPE) + memcpy(buf, data, size); #else // Send through the pipe to ensure safe reads for( size_t offset = 0; offset != size; /*in loop*/ ) diff --git a/Source/ThirdParty/tracy/tracy.Build.cs b/Source/ThirdParty/tracy/tracy.Build.cs index 9d54ca688..beb0e9f89 100644 --- a/Source/ThirdParty/tracy/tracy.Build.cs +++ b/Source/ThirdParty/tracy/tracy.Build.cs @@ -47,11 +47,15 @@ public class tracy : ThirdPartyModule switch (options.Platform.Target) { case TargetPlatform.Windows: + case TargetPlatform.XboxOne: + case TargetPlatform.XboxScarlett: options.PrivateDefinitions.Add("TRACY_DBGHELP_LOCK=FlaxDbgHelp"); + options.PrivateDefinitions.Add("TRACY_NO_PIPE"); break; case TargetPlatform.Switch: options.PrivateDefinitions.Add("TRACY_USE_MALLOC"); options.PrivateDefinitions.Add("TRACY_ONLY_IPV4"); + options.PrivateDefinitions.Add("TRACY_NO_PIPE"); break; } } From 6763436effbe204655f3dd1fcff6166f64586122 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 15 Jul 2025 20:10:01 +0200 Subject: [PATCH 122/122] Add logging missing instance layer on Vulkan --- .../GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp index f2d0aad7d..8c77b77d3 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp @@ -1179,23 +1179,57 @@ GPUDevice* GPUDeviceVulkan::Create() Array properties; properties.Resize(propertyCount); vkEnumerateInstanceExtensionProperties(nullptr, &propertyCount, properties.Get()); + String missingExtension; for (const char* extension : InstanceExtensions) { - bool found = false; for (uint32_t propertyIndex = 0; propertyIndex < propertyCount; propertyIndex++) { if (!StringUtils::Compare(properties[propertyIndex].extensionName, extension)) { - found = true; + if (missingExtension.IsEmpty()) + missingExtension = extension; + else + missingExtension += TEXT(", ") + String(extension); break; } } - if (!found) + } + LOG(Warning, "Extensions found:"); + for (const VkExtensionProperties& property : properties) + LOG(Warning, " > {}", String(property.extensionName)); + auto error = String::Format(TEXT("Vulkan driver doesn't contain specified extensions:\n{0}\nPlease make sure your layers path is set appropriately."), missingExtension); + LOG_STR(Error, error); + Platform::Error(*error); + return nullptr; + } + if (result == VK_ERROR_LAYER_NOT_PRESENT) + { + // Layers error + uint32_t propertyCount; + vkEnumerateInstanceLayerProperties(&propertyCount, nullptr); + Array properties; + properties.Resize(propertyCount); + vkEnumerateInstanceLayerProperties(&propertyCount, properties.Get()); + String missingLayers; + for (const char* layer : InstanceLayers) + { + for (uint32_t propertyIndex = 0; propertyIndex < propertyCount; propertyIndex++) { - LOG(Warning, "Missing required Vulkan extension: {0}", String(extension)); + if (!StringUtils::Compare(properties[propertyIndex].layerName, layer)) + { + if (missingLayers.IsEmpty()) + missingLayers = layer; + else + missingLayers += TEXT(", ") + String(layer); + break; + } } } - auto error = String::Format(TEXT("Vulkan driver doesn't contain specified extensions:\n{0}\nPlease make sure your layers path is set appropriately.")); + LOG(Warning, "Layers found:"); + for (const VkLayerProperties& property : properties) + LOG(Warning, " > {}", String(property.layerName)); + auto error = String::Format(TEXT("Vulkan driver doesn't contain specified layers:\n{0}\nPlease make sure your layers path is set appropriately."), missingLayers); + LOG_STR(Error, error); Platform::Error(*error); return nullptr; }