From ad28a3fdbfb0cd3590e4cc727501a16f9e5a28b9 Mon Sep 17 00:00:00 2001 From: Luke Schneider Date: Wed, 27 Sep 2023 21:54:34 -0500 Subject: [PATCH 001/546] Better light theme (Style) support, and a Default light theme (as a secondary option) 1) Added ForegroundViewport as a new color. It is used in the main game viewport (ViewportWidgetButton), and the viewport for rendering of particles and materials. It is needed because the default foreground in a Light theme is black, but black does not work well in a viewport. A new color seemed appropriate. 2) Fixed the profiler window to use the Foreground color in multiple text elements, instead of Color.White (or no default TitleColor). This includes the Row class, Asset class, SingleChart class, Timeline Class, and more. 3) Added a second theme/Style (DefaultLight) to include with the engine. It uses RGB float values because those were easier to transfer from the saved values that I had created (and they're easier for me to edit if necessary). I tried to emulate how the Default theme is created/loaded/etc as closely as possible. --- Source/Editor/GUI/Row.cs | 2 +- Source/Editor/Options/OptionsModule.cs | 66 ++++++++++++++++++- Source/Editor/Options/ThemeOptions.cs | 5 +- .../Viewport/Widgets/ViewportWidgetButton.cs | 4 +- Source/Editor/Windows/Profiler/Assets.cs | 5 ++ Source/Editor/Windows/Profiler/CPU.cs | 7 ++ Source/Editor/Windows/Profiler/GPU.cs | 7 ++ Source/Editor/Windows/Profiler/MemoryGPU.cs | 4 ++ Source/Editor/Windows/Profiler/Network.cs | 6 ++ Source/Editor/Windows/Profiler/SingleChart.cs | 6 +- Source/Editor/Windows/Profiler/Timeline.cs | 4 +- Source/Engine/Scripting/Scripting.cs | 1 + Source/Engine/UI/GUI/Style.cs | 6 ++ 13 files changed, 111 insertions(+), 12 deletions(-) diff --git a/Source/Editor/GUI/Row.cs b/Source/Editor/GUI/Row.cs index 8dad8b20d..4526e60ac 100644 --- a/Source/Editor/GUI/Row.cs +++ b/Source/Editor/GUI/Row.cs @@ -95,7 +95,7 @@ namespace FlaxEditor.GUI rect.Width -= leftDepthMargin; Render2D.PushClip(rect); - Render2D.DrawText(style.FontMedium, text, rect, Color.White, column.CellAlignment, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, text, rect, Style.Current.Foreground, column.CellAlignment, TextAlignment.Center); Render2D.PopClip(); x += width; diff --git a/Source/Editor/Options/OptionsModule.cs b/Source/Editor/Options/OptionsModule.cs index 68aa11626..da293ec47 100644 --- a/Source/Editor/Options/OptionsModule.cs +++ b/Source/Editor/Options/OptionsModule.cs @@ -208,13 +208,20 @@ namespace FlaxEditor.Options // If a non-default style was chosen, switch to that style string styleName = themeOptions.SelectedStyle; - if (styleName != "Default" && themeOptions.Styles.TryGetValue(styleName, out var style) && style != null) + if (styleName != "Default" && styleName != "LightDefault" && themeOptions.Styles.TryGetValue(styleName, out var style) && style != null) { Style.Current = style; } else { - Style.Current = CreateDefaultStyle(); + if (styleName == "LightDefault") + { + Style.Current = CreateLightStyle(); + } + else + { + Style.Current = CreateDefaultStyle(); + } } } @@ -233,6 +240,7 @@ namespace FlaxEditor.Options Foreground = Color.FromBgra(0xFFFFFFFF), ForegroundGrey = Color.FromBgra(0xFFA9A9B3), ForegroundDisabled = Color.FromBgra(0xFF787883), + ForegroundViewport = Color.FromBgra(0xFFFFFFFF), BackgroundHighlighted = Color.FromBgra(0xFF54545C), BorderHighlighted = Color.FromBgra(0xFF6A6A75), BackgroundSelected = Color.FromBgra(0xFF007ACC), @@ -271,6 +279,60 @@ namespace FlaxEditor.Options return style; } + /// + /// Creates the light style (2nd default). + /// + /// The style object. + public Style CreateLightStyle() + { + // Metro Style colors + var options = Options; + var style = new Style + { + Background = new Color(0.92f, 0.92f, 0.92f, 1f), + LightBackground = new Color(0.84f, 0.84f, 0.88f, 1f), + DragWindow = new Color(0.0f, 0.26f, 0.43f, 0.70f), + Foreground = new Color(0.0f, 0.0f, 0.0f, 1f), + ForegroundGrey = new Color(0.30f, 0.30f, 0.31f, 1f), + ForegroundDisabled = new Color(0.45f, 0.45f, 0.49f, 1f), + ForegroundViewport = new Color(1.0f, 1.0f, 1.0f, 1f), + BackgroundHighlighted = new Color(0.59f, 0.59f, 0.64f, 1f), + BorderHighlighted = new Color(0.50f, 0.50f, 0.55f, 1f), + BackgroundSelected = new Color(0.00f, 0.46f, 0.78f, 0.78f), + BorderSelected = new Color(0.11f, 0.57f, 0.88f, 0.65f), + BackgroundNormal = new Color(0.67f, 0.67f, 0.75f, 1f), + BorderNormal = new Color(0.59f, 0.59f, 0.64f, 1f), + TextBoxBackground = new Color(0.75f, 0.75f, 0.81f, 1f), + TextBoxBackgroundSelected = new Color(0.73f, 0.73f, 0.80f, 1f), + CollectionBackgroundColor = new Color(0.25f, 0.25f, 0.25f, 1f), + ProgressNormal = new Color(0.03f, 0.65f, 0.12f, 1f), + + // Fonts + FontTitle = options.Interface.TitleFont.GetFont(), + FontLarge = options.Interface.LargeFont.GetFont(), + FontMedium = options.Interface.MediumFont.GetFont(), + FontSmall = options.Interface.SmallFont.GetFont(), + + // Icons + ArrowDown = Editor.Icons.ArrowDown12, + ArrowRight = Editor.Icons.ArrowRight12, + Search = Editor.Icons.Search12, + Settings = Editor.Icons.Settings12, + Cross = Editor.Icons.Cross12, + CheckBoxIntermediate = Editor.Icons.CheckBoxIntermediate12, + CheckBoxTick = Editor.Icons.CheckBoxTick12, + StatusBarSizeGrip = Editor.Icons.WindowDrag12, + Translate = Editor.Icons.Translate32, + Rotate = Editor.Icons.Rotate32, + Scale = Editor.Icons.Scale32, + Scalar = Editor.Icons.Scalar32, + + SharedTooltip = new Tooltip() + }; + + return style; + } + /// public override void OnInit() { diff --git a/Source/Editor/Options/ThemeOptions.cs b/Source/Editor/Options/ThemeOptions.cs index 243918939..a033b34da 100644 --- a/Source/Editor/Options/ThemeOptions.cs +++ b/Source/Editor/Options/ThemeOptions.cs @@ -63,13 +63,14 @@ namespace FlaxEditor.Options private void ReloadOptions(ComboBox obj) { var themeOptions = (ThemeOptions)ParentEditor.Values[0]; - var options = new string[themeOptions.Styles.Count + 1]; + var options = new string[themeOptions.Styles.Count + 2]; options[0] = "Default"; + options[1] = "LightDefault"; int i = 0; foreach (var styleName in themeOptions.Styles.Keys) { - options[i + 1] = styleName; + options[i + 2] = styleName; i++; } _combobox.ComboBox.SetItems(options); diff --git a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs index 481bc3f1b..544489208 100644 --- a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs +++ b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs @@ -112,7 +112,7 @@ namespace FlaxEditor.Viewport.Widgets if (Icon.IsValid) { // Draw icon - Render2D.DrawSprite(Icon, iconRect, style.Foreground); + Render2D.DrawSprite(Icon, iconRect, style.ForegroundViewport); // Update text rectangle textRect.Location.X += iconSize; @@ -120,7 +120,7 @@ namespace FlaxEditor.Viewport.Widgets } // Draw text - Render2D.DrawText(style.FontMedium, _text, textRect, style.Foreground * (IsMouseOver ? 1.0f : 0.9f), TextAlignment.Center, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, _text, textRect, style.ForegroundViewport * (IsMouseOver ? 1.0f : 0.9f), TextAlignment.Center, TextAlignment.Center); } /// diff --git a/Source/Editor/Windows/Profiler/Assets.cs b/Source/Editor/Windows/Profiler/Assets.cs index e2a65d496..39c5644c8 100644 --- a/Source/Editor/Windows/Profiler/Assets.cs +++ b/Source/Editor/Windows/Profiler/Assets.cs @@ -63,6 +63,7 @@ namespace FlaxEditor.Windows.Profiler // Table var headerColor = Style.Current.LightBackground; + var textColor = Style.Current.Foreground; _table = new Table { Columns = new[] @@ -73,22 +74,26 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = "Resource", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Type", CellAlignment = TextAlignment.Center, TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "References", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Memory Usage", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = v => Utilities.Utils.FormatBytesCount((ulong)v), }, }, diff --git a/Source/Editor/Windows/Profiler/CPU.cs b/Source/Editor/Windows/Profiler/CPU.cs index fd4061276..df4d5b59b 100644 --- a/Source/Editor/Windows/Profiler/CPU.cs +++ b/Source/Editor/Windows/Profiler/CPU.cs @@ -93,6 +93,7 @@ namespace FlaxEditor.Windows.Profiler // Table var headerColor = Style.Current.LightBackground; + var textColor = Style.Current.Foreground; _table = new Table { Columns = new[] @@ -103,36 +104,42 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = "Event", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Total", TitleBackgroundColor = headerColor, FormatValue = FormatCellPercentage, + TitleColor = textColor, }, new ColumnDefinition { Title = "Self", TitleBackgroundColor = headerColor, FormatValue = FormatCellPercentage, + TitleColor = textColor, }, new ColumnDefinition { Title = "Time ms", TitleBackgroundColor = headerColor, FormatValue = FormatCellMs, + TitleColor = textColor, }, new ColumnDefinition { Title = "Self ms", TitleBackgroundColor = headerColor, FormatValue = FormatCellMs, + TitleColor = textColor, }, new ColumnDefinition { Title = "Memory", TitleBackgroundColor = headerColor, FormatValue = FormatCellBytes, + TitleColor = textColor, }, }, Parent = layout, diff --git a/Source/Editor/Windows/Profiler/GPU.cs b/Source/Editor/Windows/Profiler/GPU.cs index 4ed18691a..e6a93df72 100644 --- a/Source/Editor/Windows/Profiler/GPU.cs +++ b/Source/Editor/Windows/Profiler/GPU.cs @@ -64,6 +64,7 @@ namespace FlaxEditor.Windows.Profiler // Table var headerColor = Style.Current.LightBackground; + var textColor = Style.Current.Foreground; _table = new Table { Columns = new[] @@ -74,35 +75,41 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = "Event", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Total", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = (x) => ((float)x).ToString("0.0") + '%', }, new ColumnDefinition { Title = "GPU ms", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = (x) => ((float)x).ToString("0.000"), }, new ColumnDefinition { Title = "Draw Calls", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCountLong, }, new ColumnDefinition { Title = "Triangles", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCountLong, }, new ColumnDefinition { Title = "Vertices", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCountLong, }, }, diff --git a/Source/Editor/Windows/Profiler/MemoryGPU.cs b/Source/Editor/Windows/Profiler/MemoryGPU.cs index 20a7898e0..e894ef0e2 100644 --- a/Source/Editor/Windows/Profiler/MemoryGPU.cs +++ b/Source/Editor/Windows/Profiler/MemoryGPU.cs @@ -64,6 +64,7 @@ namespace FlaxEditor.Windows.Profiler // Table var headerColor = Style.Current.LightBackground; + var textColor = Style.Current.Foreground; _table = new Table { Columns = new[] @@ -74,18 +75,21 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = "Resource", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Type", CellAlignment = TextAlignment.Center, TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Memory Usage", TitleBackgroundColor = headerColor, FormatValue = v => Utilities.Utils.FormatBytesCount((ulong)v), + TitleColor = textColor, }, }, Parent = layout, diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs index dbee0e8e7..b29173e76 100644 --- a/Source/Editor/Windows/Profiler/Network.cs +++ b/Source/Editor/Windows/Profiler/Network.cs @@ -205,6 +205,7 @@ namespace FlaxEditor.Windows.Profiler private static Table InitTable(ContainerControl parent, string name) { var headerColor = Style.Current.LightBackground; + var textColor = Style.Current.Foreground; var table = new Table { Columns = new[] @@ -215,28 +216,33 @@ namespace FlaxEditor.Windows.Profiler CellAlignment = TextAlignment.Near, Title = name, TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Count", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, new ColumnDefinition { Title = "Data Size", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCellBytes, }, new ColumnDefinition { Title = "Message Size", TitleBackgroundColor = headerColor, + TitleColor = textColor, FormatValue = FormatCellBytes, }, new ColumnDefinition { Title = "Receivers", TitleBackgroundColor = headerColor, + TitleColor = textColor, }, }, Splits = new[] diff --git a/Source/Editor/Windows/Profiler/SingleChart.cs b/Source/Editor/Windows/Profiler/SingleChart.cs index e14480bee..29780150a 100644 --- a/Source/Editor/Windows/Profiler/SingleChart.cs +++ b/Source/Editor/Windows/Profiler/SingleChart.cs @@ -105,7 +105,7 @@ namespace FlaxEditor.Windows.Profiler if (_selectedSampleIndex != -1) { float selectedX = Width - (_samples.Count - _selectedSampleIndex - 1) * PointsOffset; - Render2D.DrawLine(new Float2(selectedX, 0), new Float2(selectedX, chartHeight), Color.White, 1.5f); + Render2D.DrawLine(new Float2(selectedX, 0), new Float2(selectedX, chartHeight), Style.Current.Foreground, 1.5f); } int samplesInViewCount = Math.Min((int)(Width / PointsOffset), _samples.Count) - 1; @@ -138,8 +138,8 @@ namespace FlaxEditor.Windows.Profiler var headerRect = new Rectangle(0, chartHeight, Width, TitleHeight); var headerTextRect = new Rectangle(2, chartHeight, Width - 4, TitleHeight); Render2D.FillRectangle(headerRect, style.BackgroundNormal); - Render2D.DrawText(style.FontMedium, Title, headerTextRect, Color.White * 0.8f, TextAlignment.Near, TextAlignment.Center); - Render2D.DrawText(style.FontMedium, _sample, headerTextRect, Color.White, TextAlignment.Far, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, Title, headerTextRect, Style.Current.ForegroundGrey, TextAlignment.Near, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, _sample, headerTextRect, Style.Current.Foreground, TextAlignment.Far, TextAlignment.Center); } private void OnClick(ref Float2 location) diff --git a/Source/Editor/Windows/Profiler/Timeline.cs b/Source/Editor/Windows/Profiler/Timeline.cs index a61ca1d05..59a7a0e26 100644 --- a/Source/Editor/Windows/Profiler/Timeline.cs +++ b/Source/Editor/Windows/Profiler/Timeline.cs @@ -90,7 +90,7 @@ namespace FlaxEditor.Windows.Profiler if (_nameLength < bounds.Width + 4) { Render2D.PushClip(bounds); - Render2D.DrawText(style.FontMedium, _name, bounds, Color.White, TextAlignment.Center, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, _name, bounds, Style.Current.Foreground, TextAlignment.Center, TextAlignment.Center); Render2D.PopClip(); } } @@ -115,7 +115,7 @@ namespace FlaxEditor.Windows.Profiler var style = Style.Current; var rect = new Rectangle(Float2.Zero, Size); Render2D.PushClip(rect); - Render2D.DrawText(style.FontMedium, Name, rect, Color.White, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapChars); + Render2D.DrawText(style.FontMedium, Name, rect, Style.Current.Foreground, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapChars); Render2D.PopClip(); } } diff --git a/Source/Engine/Scripting/Scripting.cs b/Source/Engine/Scripting/Scripting.cs index 68da64bf7..e2e6b514a 100644 --- a/Source/Engine/Scripting/Scripting.cs +++ b/Source/Engine/Scripting/Scripting.cs @@ -266,6 +266,7 @@ namespace FlaxEngine Foreground = Color.FromBgra(0xFFFFFFFF), ForegroundGrey = Color.FromBgra(0xFFA9A9B3), ForegroundDisabled = Color.FromBgra(0xFF787883), + ForegroundViewport = Color.FromBgra(0xFFFFFFFF), BackgroundHighlighted = Color.FromBgra(0xFF54545C), BorderHighlighted = Color.FromBgra(0xFF6A6A75), BackgroundSelected = Color.FromBgra(0xFF007ACC), diff --git a/Source/Engine/UI/GUI/Style.cs b/Source/Engine/UI/GUI/Style.cs index fac65e22f..5e8b1c57f 100644 --- a/Source/Engine/UI/GUI/Style.cs +++ b/Source/Engine/UI/GUI/Style.cs @@ -104,6 +104,12 @@ namespace FlaxEngine.GUI [EditorOrder(110)] public Color ForegroundDisabled; + /// + /// The foreground color in viewports (usually have a dark background) + /// + [EditorOrder(115)] + public Color ForegroundViewport; + /// /// The background highlighted color. /// From b7b8213179ff05edbea38a3385e1af9efe2ccdd4 Mon Sep 17 00:00:00 2001 From: Luke Schneider Date: Fri, 29 Sep 2023 07:43:59 -0500 Subject: [PATCH 002/546] Some additional fixes to light theme support Fixed some issues with light theme support: 1) Icons in content tree nodes (Folder icons) now use the foreground color. I did not find a case where the content tree is used for other icons. 2) The asset picker now uses the Background Normal color (instead of a very transparent dark gray) for the background, and Orange for the text. Did not seem like it warranted adding a new color, and Orange works in both dark and light styles. 3) The platform selector icons are now hard-coded instead of based on the style. This may sound odd, but the icons are colored, so they should always use White as the fully active color. Previously they worked with a dark theme because the Foreground was set to white. 4) Fixed the CollectionBackgroundColor in the light theme being dark gray instead of light gray like it should be. This fixes certain lists of things having a dark background in the light theme. --- Source/Editor/Content/Tree/ContentTreeNode.cs | 1 + Source/Editor/GUI/AssetPicker.cs | 4 ++-- Source/Editor/GUI/PlatformSelector.cs | 7 ++++--- Source/Editor/Options/OptionsModule.cs | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs index 2f6651b14..0c0fc6a51 100644 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentTreeNode.cs @@ -86,6 +86,7 @@ namespace FlaxEditor.Content Folder.ParentFolder = parent.Folder; Parent = parent; } + IconColor = Style.Current.Foreground; } /// diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 3e5d22eb0..87346d817 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -395,8 +395,8 @@ namespace FlaxEditor.GUI else { // No element selected - Render2D.FillRectangle(iconRect, new Color(0.2f)); - Render2D.DrawText(style.FontMedium, "No asset\nselected", iconRect, Color.Wheat, TextAlignment.Center, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, Height / DefaultIconSize); + Render2D.FillRectangle(iconRect, Style.Current.BackgroundNormal); + Render2D.DrawText(style.FontMedium, "No asset\nselected", iconRect, Color.Orange, TextAlignment.Center, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, Height / DefaultIconSize); } // Check if drag is over diff --git a/Source/Editor/GUI/PlatformSelector.cs b/Source/Editor/GUI/PlatformSelector.cs index 1e03a9677..a29f62805 100644 --- a/Source/Editor/GUI/PlatformSelector.cs +++ b/Source/Editor/GUI/PlatformSelector.cs @@ -100,9 +100,10 @@ namespace FlaxEditor.GUI AutoResize = true; Offsets = new Margin(0, 0, 0, IconSize); - _mouseOverColor = style.Foreground; - _selectedColor = style.Foreground; - _defaultColor = style.ForegroundGrey; + // Ignoring style on purpose (style would make sense if the icons were white, but they are colored) + _mouseOverColor = new Color(0.8f, 0.8f, 0.8f, 1f); + _selectedColor = Color.White; + _defaultColor = new Color(0.7f, 0.7f, 0.7f, 0.5f); for (int i = 0; i < platforms.Length; i++) { diff --git a/Source/Editor/Options/OptionsModule.cs b/Source/Editor/Options/OptionsModule.cs index da293ec47..4ed3b02e1 100644 --- a/Source/Editor/Options/OptionsModule.cs +++ b/Source/Editor/Options/OptionsModule.cs @@ -304,7 +304,7 @@ namespace FlaxEditor.Options BorderNormal = new Color(0.59f, 0.59f, 0.64f, 1f), TextBoxBackground = new Color(0.75f, 0.75f, 0.81f, 1f), TextBoxBackgroundSelected = new Color(0.73f, 0.73f, 0.80f, 1f), - CollectionBackgroundColor = new Color(0.25f, 0.25f, 0.25f, 1f), + CollectionBackgroundColor = new Color(0.85f, 0.85f, 0.88f, 1f), ProgressNormal = new Color(0.03f, 0.65f, 0.12f, 1f), // Fonts From 70ca1996c5e4a9fc8204a3133baad7d3826fb3bc Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:00:03 +0200 Subject: [PATCH 003/546] added Ratangle Mask and FWidth --- Source/Editor/Surface/Archetypes/Material.cs | 38 +++++++++++++++++++ .../MaterialGenerator.Material.cpp | 19 ++++++++++ 2 files changed, 57 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 1a797c67d..064f0a771 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -882,6 +882,44 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Output(1, "Inv Size", typeof(Float2), 1), } }, + new NodeArchetype + { + TypeID = 40, + Title = "Ratangle Mask", + Description = "Creates a Ratangle mask", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(150, 100), + ConnectionsHints = ConnectionsHint.Vector, + DefaultValues = new object[] + { + new Float2(0, 0), + new Float2(0.5f, 0.5f), + }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "UV", true, typeof(Float2), 0), + NodeElementArchetype.Factory.Input(1, "Ratangle", true, typeof(Float2), 1), + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), + } + }, + new NodeArchetype + { + TypeID = 41, + Title = "FWidth", + Description = "Creates a Partial Derivatives (fwidth)", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(150, 100), + ConnectionsHints = ConnectionsHint.Vector, + DefaultValues = new object[] + { + 1 + }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "value", true, typeof(float), 0), + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), + } + }, }; } } diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index 8aa300731..b00477fdc 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -519,6 +519,25 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) } break; } + // Ratangle Mask + case 40: + { + const auto uv = tryGetValue(node->GetBox(0), getUVs).AsFloat2(); + const auto ratangle = tryGetValue(node->GetBox(1), node->Values[1]).AsFloat2(); + + auto d = writeLocal(ValueType::Float2, String::Format(TEXT("abs({0} * 2 - 1) - {1}"),uv.Value, ratangle.Value), node); + auto fwidth = writeLocal(ValueType::Float , String::Format(TEXT("abs(ddx({0})) + abs(ddy({0}))"), d.Value), node); + auto d2 = writeLocal(ValueType::Float , String::Format(TEXT("1 - {0} / {1}"), d.Value, fwidth.Value), node); + value = writeLocal(ValueType::Float , String::Format(TEXT("saturate(min({0}.x, {0}.y))"), d2.Value), node); + break; + } + // FWidth + case 41: + { + const auto d = tryGetValue(node->GetBox(0), node->Values[0]).AsFloat(); + value = writeLocal(ValueType::Float, String::Format(TEXT("abs(ddx({0})) + abs(ddy({0}))"), d.Value), node); + break; + } default: break; } From 367eaf2f8966a04628d3dedb67e366c662abb6f2 Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:28:23 +0200 Subject: [PATCH 004/546] adjustment to nodes size --- Source/Editor/Surface/Archetypes/Material.cs | 4 ++-- .../Tools/MaterialGenerator/MaterialGenerator.Material.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 064f0a771..a0aa7d29f 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -888,7 +888,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Ratangle Mask", Description = "Creates a Ratangle mask", Flags = NodeFlags.MaterialGraph, - Size = new Float2(150, 100), + Size = new Float2(150, 40), ConnectionsHints = ConnectionsHint.Vector, DefaultValues = new object[] { @@ -908,7 +908,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "FWidth", Description = "Creates a Partial Derivatives (fwidth)", Flags = NodeFlags.MaterialGraph, - Size = new Float2(150, 100), + Size = new Float2(150, 20), ConnectionsHints = ConnectionsHint.Vector, DefaultValues = new object[] { diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index b00477fdc..c6d05ef48 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -527,7 +527,7 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) auto d = writeLocal(ValueType::Float2, String::Format(TEXT("abs({0} * 2 - 1) - {1}"),uv.Value, ratangle.Value), node); auto fwidth = writeLocal(ValueType::Float , String::Format(TEXT("abs(ddx({0})) + abs(ddy({0}))"), d.Value), node); - auto d2 = writeLocal(ValueType::Float , String::Format(TEXT("1 - {0} / {1}"), d.Value, fwidth.Value), node); + auto d2 = writeLocal(ValueType::Float2 , String::Format(TEXT("1 - {0} / {1}"), d.Value, fwidth.Value), node); value = writeLocal(ValueType::Float , String::Format(TEXT("saturate(min({0}.x, {0}.y))"), d2.Value), node); break; } From d7b9056d943615767523980e47d4bffb07ab8932 Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:13:45 +0200 Subject: [PATCH 005/546] added AAStep and resolved stefnotch reviewe --- Source/Editor/Surface/Archetypes/Material.cs | 27 ++++++++++++++++--- .../MaterialGenerator.Material.cpp | 24 ++++++++++++++--- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index a0aa7d29f..8e5329d22 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -885,8 +885,8 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 40, - Title = "Ratangle Mask", - Description = "Creates a Ratangle mask", + Title = "Rectangle Mask", + Description = "Creates a Rectangle mask", Flags = NodeFlags.MaterialGraph, Size = new Float2(150, 40), ConnectionsHints = ConnectionsHint.Vector, @@ -898,7 +898,7 @@ namespace FlaxEditor.Surface.Archetypes Elements = new[] { NodeElementArchetype.Factory.Input(0, "UV", true, typeof(Float2), 0), - NodeElementArchetype.Factory.Input(1, "Ratangle", true, typeof(Float2), 1), + NodeElementArchetype.Factory.Input(1, "Rectangle", true, typeof(Float2), 1), NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), } }, @@ -906,7 +906,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 41, Title = "FWidth", - Description = "Creates a Partial Derivatives (fwidth)", + Description = "Creates a partial derivative (fwidth)", Flags = NodeFlags.MaterialGraph, Size = new Float2(150, 20), ConnectionsHints = ConnectionsHint.Vector, @@ -920,6 +920,25 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), } }, + new NodeArchetype + { + TypeID = 42, + Title = "AAStep", + Description = "Smooth version of step", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(150, 20), + ConnectionsHints = ConnectionsHint.Vector, + DefaultValues = new object[] + { + 1, + 0 + }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "value", true, typeof(float), 0), + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), + } + }, }; } } diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index c6d05ef48..0c8d0780e 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -523,11 +523,10 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) case 40: { const auto uv = tryGetValue(node->GetBox(0), getUVs).AsFloat2(); - const auto ratangle = tryGetValue(node->GetBox(1), node->Values[1]).AsFloat2(); + const auto rectangle = tryGetValue(node->GetBox(1), node->Values[1]).AsFloat2(); - auto d = writeLocal(ValueType::Float2, String::Format(TEXT("abs({0} * 2 - 1) - {1}"),uv.Value, ratangle.Value), node); - auto fwidth = writeLocal(ValueType::Float , String::Format(TEXT("abs(ddx({0})) + abs(ddy({0}))"), d.Value), node); - auto d2 = writeLocal(ValueType::Float2 , String::Format(TEXT("1 - {0} / {1}"), d.Value, fwidth.Value), node); + auto d = writeLocal(ValueType::Float2, String::Format(TEXT("abs({0} * 2 - 1) - {1}"),uv.Value, rectangle.Value), node); + auto d2 = writeLocal(ValueType::Float2 , String::Format(TEXT("1 - {0} / fwidth({0})"), d.Value), node); value = writeLocal(ValueType::Float , String::Format(TEXT("saturate(min({0}.x, {0}.y))"), d2.Value), node); break; } @@ -538,6 +537,23 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) value = writeLocal(ValueType::Float, String::Format(TEXT("abs(ddx({0})) + abs(ddy({0}))"), d.Value), node); break; } + //AAStep (smooth version of step) + case 42: + { + //source https://www.ronja-tutorials.com/post/046-fwidth/#a-better-step + + const auto compValue = tryGetValue(node->GetBox(0), getUVs).AsFloat(); + const auto gradient = tryGetValue(node->GetBox(1), node->Values[1]).AsFloat(); + + auto change = writeLocal(ValueType::Float, String::Format(TEXT("fwidth({0})"), gradient.Value), node); + + //base the range of the inverse lerp on the change over two pixels + auto lowerEdge = writeLocal(ValueType::Float, String::Format(TEXT("{0} - {1})"), compValue.Value, change.Value), node); + auto upperEdge = writeLocal(ValueType::Float, String::Format(TEXT("{0} + {1})"), compValue.Value, change.Value), node); + + //do the inverse interpolation and saturate it + value = writeLocal(ValueType::Float, String::Format(TEXT("saturate((({0} - {1}) / ({2} - {1})))"), gradient.Value, lowerEdge.Value, upperEdge.Value), node); + } default: break; } From 329910ae0d022445e34ede5fbe20401c683bd7b6 Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:18:42 +0200 Subject: [PATCH 006/546] missing input fix --- Source/Editor/Surface/Archetypes/Material.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 8e5329d22..c725b0662 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -926,7 +926,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "AAStep", Description = "Smooth version of step", Flags = NodeFlags.MaterialGraph, - Size = new Float2(150, 20), + Size = new Float2(150, 40), ConnectionsHints = ConnectionsHint.Vector, DefaultValues = new object[] { @@ -936,6 +936,7 @@ namespace FlaxEditor.Surface.Archetypes Elements = new[] { NodeElementArchetype.Factory.Input(0, "value", true, typeof(float), 0), + NodeElementArchetype.Factory.Input(1, "gradient", true, typeof(float), 0), NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), } }, From 3dcd1a5ffb9b6747870aa1fb866c04c1224ebec7 Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Wed, 4 Oct 2023 19:07:48 +0200 Subject: [PATCH 007/546] Improve viewport camera settings * remove fixed camera speed buttons from camera speed scale widget * change camera speed scale widget to general camera settings widget * move all camera-related settings from view mode widget to camera settings widget * fix some typo * add possibility to set camera speed manually * add min/max camera speed options --- Source/Editor/Viewport/EditorViewport.cs | 415 +++++++++++++++-------- 1 file changed, 279 insertions(+), 136 deletions(-) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 8c71a8b02..83fce7a3d 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -125,12 +125,19 @@ namespace FlaxEditor.Viewport public const int FpsCameraFilteringFrames = 3; /// - /// The speed widget button. + /// The camera settings widget. /// - protected ViewportWidgetButton _speedWidget; + protected ViewportWidgetsContainer _cameraWidget; + + /// + /// The camera settings widget button. + /// + protected ViewportWidgetButton _cameraButton; private float _mouseSensitivity; private float _movementSpeed; + private float _minMovementSpeed; + private float _maxMovementSpeed; private float _mouseAccelerationScale; private bool _useMouseFiltering; private bool _useMouseAcceleration; @@ -171,8 +178,8 @@ namespace FlaxEditor.Viewport private float _fieldOfView; private float _nearPlane; private float _farPlane; - private float _orthoSize = 1.0f; - private bool _isOrtho = false; + private float _orthoSize; + private bool _isOrtho; private float _wheelMovementChangeDeltaSum = 0; private bool _invertPanning; @@ -194,19 +201,31 @@ namespace FlaxEditor.Viewport get => _movementSpeed; set { - for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++) - { - if (Math.Abs(value - EditorViewportCameraSpeedValues[i]) < 0.001f) - { - _movementSpeed = EditorViewportCameraSpeedValues[i]; - if (_speedWidget != null) - _speedWidget.Text = _movementSpeed.ToString(); - break; - } - } + _movementSpeed = value; + + if (_cameraButton != null) + _cameraButton.Text = _movementSpeed.ToString(); } } + /// + /// Gets or sets the minimum camera movement speed. + /// + public float MinMovementSpeed + { + get => _minMovementSpeed; + set => _minMovementSpeed = value; + } + + /// + /// Gets or sets the maximum camera movement speed. + /// + public float MaxMovementSpeed + { + get => _maxMovementSpeed; + set => _maxMovementSpeed = value; + } + /// /// Gets the mouse movement position delta (user press and move). /// @@ -441,30 +460,183 @@ namespace FlaxEditor.Viewport if (useWidgets) { - var largestText = "Invert Panning"; + #region Camera settings widget + var largestText = "Relative Panning"; var textSize = Style.Current.FontMedium.MeasureText(largestText); var xLocationForExtras = textSize.X + 5; - // Camera speed widget - var camSpeed = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); - var camSpeedCM = new ContextMenu(); - var camSpeedButton = new ViewportWidgetButton(_movementSpeed.ToString(), Editor.Instance.Icons.CamSpeed32, camSpeedCM) + + // Camera settings widget + _cameraWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); + + // Camera settings menu + var cameraCM = new ContextMenu(); + _cameraButton = new ViewportWidgetButton(_movementSpeed.ToString(), Editor.Instance.Icons.Camera64, cameraCM) { Tag = this, - TooltipText = "Camera speed scale" + TooltipText = "Camera Settings", + Parent = _cameraWidget }; - _speedWidget = camSpeedButton; - for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++) - { - var v = EditorViewportCameraSpeedValues[i]; - var button = camSpeedCM.AddButton(v.ToString()); - button.Tag = v; - } - camSpeedCM.ButtonClicked += button => MovementSpeed = (float)button.Tag; - camSpeedCM.VisibleChanged += WidgetCamSpeedShowHide; - camSpeedButton.Parent = camSpeed; - camSpeed.Parent = this; + _cameraWidget.Parent = this; + + // Camera speed + var camSpeedButton = cameraCM.AddButton("Camera Speed"); + camSpeedButton.CloseMenuOnClick = false; + var camSpeedValue = new FloatValueBox(_movementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, _maxMovementSpeed, 0.5f) + { + Parent = camSpeedButton, + }; + + camSpeedValue.ValueChanged += () => OnMovementSpeedChanged(camSpeedValue); + cameraCM.VisibleChanged += control => camSpeedValue.Value = _movementSpeed; + + var minCamSpeedButton = cameraCM.AddButton("Min Cam Speed"); + minCamSpeedButton.CloseMenuOnClick = false; + var minCamSpeedValue = new FloatValueBox(_minMovementSpeed, xLocationForExtras, 2, 70.0f, 0.05f, _maxMovementSpeed, 0.5f) + { + Parent = minCamSpeedButton, + }; + var maxCamSpeedButton = cameraCM.AddButton("Max Cam Speed"); + maxCamSpeedButton.CloseMenuOnClick = false; + var maxCamSpeedValue = new FloatValueBox(_maxMovementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, 64.0f, 0.5f) + { + Parent = maxCamSpeedButton, + }; + + minCamSpeedValue.ValueChanged += () => + { + OnMinMovementSpeedChanged(minCamSpeedValue); + + maxCamSpeedValue.MinValue = minCamSpeedValue.Value; + + if (camSpeedValue.MinValue != minCamSpeedValue.Value) + camSpeedValue.MinValue = minCamSpeedValue.Value; + }; + cameraCM.VisibleChanged += control => minCamSpeedValue.Value = _minMovementSpeed; + maxCamSpeedValue.ValueChanged += () => + { + OnMaxMovementSpeedChanged(maxCamSpeedValue); + + minCamSpeedValue.MaxValue = maxCamSpeedValue.Value; + + if (camSpeedValue.MaxValue != maxCamSpeedValue.Value) + camSpeedValue.MaxValue = maxCamSpeedValue.Value; + }; + cameraCM.VisibleChanged += control => maxCamSpeedValue.Value = _maxMovementSpeed; + + // Invert Panning + { + var invertPanning = cameraCM.AddButton("Invert Panning"); + invertPanning.CloseMenuOnClick = false; + var invertPanningValue = new CheckBox(xLocationForExtras, 2, _invertPanning) + { + Parent = invertPanning + }; + + invertPanningValue.StateChanged += OnInvertPanningToggled; + cameraCM.VisibleChanged += control => invertPanningValue.Checked = _invertPanning; + } + + cameraCM.AddSeparator(); + + // Camera Viewpoints + { + var cameraView = cameraCM.AddChildMenu("Viewpoints").ContextMenu; + for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++) + { + var co = EditorViewportCameraViewpointValues[i]; + var button = cameraView.AddButton(co.Name); + button.Tag = co.Orientation; + } + + cameraView.ButtonClicked += OnViewpointChanged; + } + + // Orthographic + { + var ortho = cameraCM.AddButton("Orthographic"); + ortho.CloseMenuOnClick = false; + var orthoValue = new CheckBox(xLocationForExtras, 2, _isOrtho) + { + Parent = ortho + }; + + orthoValue.StateChanged += checkBox => + { + if (checkBox.Checked != _isOrtho) + { + OnOrthographicModeToggled(checkBox); + cameraCM.Hide(); + } + }; + cameraCM.VisibleChanged += control => orthoValue.Checked = _isOrtho; + } + + // Field of View + { + var fov = cameraCM.AddButton("Field Of View"); + fov.CloseMenuOnClick = false; + var fovValue = new FloatValueBox(_fieldOfView, xLocationForExtras, 2, 70.0f, 35.0f, 160.0f, 0.1f) + { + Parent = fov + }; + + fovValue.ValueChanged += () => OnFieldOfViewChanged(fovValue); + cameraCM.VisibleChanged += control => + { + fov.Visible = !_isOrtho; + fovValue.Value = _fieldOfView; + }; + } + + // Ortho Scale + { + var orthoSize = cameraCM.AddButton("Ortho Scale"); + orthoSize.CloseMenuOnClick = false; + var orthoSizeValue = new FloatValueBox(_orthoSize, xLocationForExtras, 2, 70.0f, 0.001f, 100000.0f, 0.01f) + { + Parent = orthoSize + }; + + orthoSizeValue.ValueChanged += () => OnOrthographicSizeChanged(orthoSizeValue); + cameraCM.VisibleChanged += control => + { + orthoSize.Visible = _isOrtho; + orthoSizeValue.Value = _orthoSize; + }; + } + + // Near Plane + { + var nearPlane = cameraCM.AddButton("Near Plane"); + nearPlane.CloseMenuOnClick = false; + var nearPlaneValue = new FloatValueBox(_nearPlane, xLocationForExtras, 2, 70.0f, 0.001f, 1000.0f) + { + Parent = nearPlane + }; + + nearPlaneValue.ValueChanged += () => OnNearPlaneChanged(nearPlaneValue); + cameraCM.VisibleChanged += control => nearPlaneValue.Value = _nearPlane; + } + + // Far Plane + { + var farPlane = cameraCM.AddButton("Far Plane"); + farPlane.CloseMenuOnClick = false; + var farPlaneValue = new FloatValueBox(_farPlane, xLocationForExtras, 2, 70.0f, 10.0f) + { + Parent = farPlane + }; + + farPlaneValue.ValueChanged += () => OnFarPlaneChanged(farPlaneValue); + cameraCM.VisibleChanged += control => farPlaneValue.Value = _farPlane; + } + #endregion Camera settings widget + + #region View mode widget + largestText = "Brightness"; + textSize = Style.Current.FontMedium.MeasureText(largestText); + xLocationForExtras = textSize.X + 5; - // View mode widget var viewMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft); ViewWidgetButtonMenu = new ContextMenu(); var viewModeButton = new ViewportWidgetButton("View", SpriteHandle.Invalid, ViewWidgetButtonMenu) @@ -481,8 +653,8 @@ namespace FlaxEditor.Viewport // Show FPS { InitFpsCounter(); - _showFpsButon = ViewWidgetShowMenu.AddButton("FPS Counter", () => ShowFpsCounter = !ShowFpsCounter); - _showFpsButon.CloseMenuOnClick = false; + _showFpsButton = ViewWidgetShowMenu.AddButton("FPS Counter", () => ShowFpsCounter = !ShowFpsCounter); + _showFpsButton.CloseMenuOnClick = false; } } @@ -540,104 +712,6 @@ namespace FlaxEditor.Viewport ViewWidgetButtonMenu.AddSeparator(); - // Orthographic - { - var ortho = ViewWidgetButtonMenu.AddButton("Orthographic"); - ortho.CloseMenuOnClick = false; - var orthoValue = new CheckBox(xLocationForExtras, 2, _isOrtho) - { - Parent = ortho - }; - orthoValue.StateChanged += checkBox => - { - if (checkBox.Checked != _isOrtho) - { - _isOrtho = checkBox.Checked; - ViewWidgetButtonMenu.Hide(); - if (_isOrtho) - { - var orient = ViewOrientation; - OrientViewport(ref orient); - } - } - }; - ViewWidgetButtonMenu.VisibleChanged += control => orthoValue.Checked = _isOrtho; - } - - // Camera Viewpoints - { - var cameraView = ViewWidgetButtonMenu.AddChildMenu("Viewpoints").ContextMenu; - for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++) - { - var co = EditorViewportCameraViewpointValues[i]; - var button = cameraView.AddButton(co.Name); - button.Tag = co.Orientation; - } - cameraView.ButtonClicked += button => - { - var orient = Quaternion.Euler((Float3)button.Tag); - OrientViewport(ref orient); - }; - } - - // Field of View - { - var fov = ViewWidgetButtonMenu.AddButton("Field Of View"); - fov.CloseMenuOnClick = false; - var fovValue = new FloatValueBox(1, xLocationForExtras, 2, 70.0f, 35.0f, 160.0f, 0.1f) - { - Parent = fov - }; - - fovValue.ValueChanged += () => _fieldOfView = fovValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => - { - fov.Visible = !_isOrtho; - fovValue.Value = _fieldOfView; - }; - } - - // Ortho Scale - { - var orthoSize = ViewWidgetButtonMenu.AddButton("Ortho Scale"); - orthoSize.CloseMenuOnClick = false; - var orthoSizeValue = new FloatValueBox(_orthoSize, xLocationForExtras, 2, 70.0f, 0.001f, 100000.0f, 0.01f) - { - Parent = orthoSize - }; - - orthoSizeValue.ValueChanged += () => _orthoSize = orthoSizeValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => - { - orthoSize.Visible = _isOrtho; - orthoSizeValue.Value = _orthoSize; - }; - } - - // Near Plane - { - var nearPlane = ViewWidgetButtonMenu.AddButton("Near Plane"); - nearPlane.CloseMenuOnClick = false; - var nearPlaneValue = new FloatValueBox(2.0f, xLocationForExtras, 2, 70.0f, 0.001f, 1000.0f) - { - Parent = nearPlane - }; - nearPlaneValue.ValueChanged += () => _nearPlane = nearPlaneValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => nearPlaneValue.Value = _nearPlane; - } - - // Far Plane - { - var farPlane = ViewWidgetButtonMenu.AddButton("Far Plane"); - farPlane.CloseMenuOnClick = false; - var farPlaneValue = new FloatValueBox(1000, xLocationForExtras, 2, 70.0f, 10.0f) - { - Parent = farPlane - }; - farPlaneValue.ValueChanged += () => _farPlane = farPlaneValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => farPlaneValue.Value = _farPlane; - } - // Brightness { var brightness = ViewWidgetButtonMenu.AddButton("Brightness"); @@ -661,6 +735,7 @@ namespace FlaxEditor.Viewport resolutionValue.ValueChanged += () => ResolutionScale = resolutionValue.Value; ViewWidgetButtonMenu.VisibleChanged += control => resolutionValue.Value = ResolutionScale; } + #endregion View mode widget // Invert Panning { @@ -695,6 +770,74 @@ namespace FlaxEditor.Viewport // Link for task event task.Begin += OnRenderBegin; } + + private void OnMovementSpeedChanged(FloatValueBox control) + { + var value = Mathf.Clamp(control.Value, _minMovementSpeed, _maxMovementSpeed); + MovementSpeed = value; + } + + private void OnMinMovementSpeedChanged(FloatValueBox control) + { + var value = Mathf.Clamp(control.Value, 0.05f, _maxMovementSpeed); + _minMovementSpeed = value; + + if (_movementSpeed < value) + _movementSpeed = value; + } + + private void OnMaxMovementSpeedChanged(FloatValueBox control) + { + var value = Mathf.Clamp(control.Value, _minMovementSpeed, 1000.0f); + _maxMovementSpeed = value; + + if (_movementSpeed > value) + _movementSpeed = value; + } + + + private void OnInvertPanningToggled(Control control) + { + _invertPanning = !_invertPanning; + } + + + private void OnViewpointChanged(ContextMenuButton button) + { + var orient = Quaternion.Euler((Float3)button.Tag); + OrientViewport(ref orient); + } + + private void OnFieldOfViewChanged(FloatValueBox control) + { + _fieldOfView = control.Value; + } + + private void OnOrthographicModeToggled(Control control) + { + _isOrtho = !_isOrtho; + + if (_isOrtho) + { + var orient = ViewOrientation; + OrientViewport(ref orient); + } + } + + private void OnOrthographicSizeChanged(FloatValueBox control) + { + _orthoSize = control.Value; + } + + private void OnNearPlaneChanged(FloatValueBox control) + { + _nearPlane = control.Value; + } + + private void OnFarPlaneChanged(FloatValueBox control) + { + _farPlane = control.Value; + } /// /// Gets a value indicating whether this viewport is using mouse currently (eg. user moving objects). @@ -793,7 +936,7 @@ namespace FlaxEditor.Viewport } private FpsCounter _fpsCounter; - private ContextMenuButton _showFpsButon; + private ContextMenuButton _showFpsButton; /// /// Gets or sets a value indicating whether show or hide FPS counter. @@ -805,7 +948,7 @@ namespace FlaxEditor.Viewport { _fpsCounter.Visible = value; _fpsCounter.Enabled = value; - _showFpsButon.Icon = value ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + _showFpsButton.Icon = value ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; } } @@ -1043,8 +1186,8 @@ namespace FlaxEditor.Viewport _camera.Update(deltaTime); useMovementSpeed = _camera.UseMovementSpeed; - if (_speedWidget != null) - _speedWidget.Parent.Visible = useMovementSpeed; + if (_cameraButton != null) + _cameraButton.Parent.Visible = useMovementSpeed; } // Get parent window From 67536f04e9500cc9965343bf971825e87a8d5990 Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Wed, 4 Oct 2023 21:05:21 +0200 Subject: [PATCH 008/546] Improve camera panning * add camera panning speed option to camera settings context menu * add relative panning speed based on distance to camera target to camera settings context menu * add relative panning option to camera settings context menu * fix float comparisons * remove invert panning entry from view widget * remove unused show/hide method for camera widget --- Source/Editor/Viewport/Cameras/FPSCamera.cs | 5 +- Source/Editor/Viewport/EditorViewport.cs | 111 ++++++++++++-------- 2 files changed, 74 insertions(+), 42 deletions(-) diff --git a/Source/Editor/Viewport/Cameras/FPSCamera.cs b/Source/Editor/Viewport/Cameras/FPSCamera.cs index e578933cf..bf2e840ea 100644 --- a/Source/Editor/Viewport/Cameras/FPSCamera.cs +++ b/Source/Editor/Viewport/Cameras/FPSCamera.cs @@ -259,7 +259,10 @@ namespace FlaxEditor.Viewport.Cameras // Pan if (input.IsPanning) { - var panningSpeed = 0.8f; + var panningSpeed = (Viewport.RelativePanning) + ? Mathf.Abs((position - TargetPoint).Length) * 0.005f + : Viewport.PanningSpeed; + if (Viewport.InvertPanning) { position += up * (mouseDelta.Y * panningSpeed); diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 83fce7a3d..28546d6df 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -181,6 +181,8 @@ namespace FlaxEditor.Viewport private float _orthoSize; private bool _isOrtho; private float _wheelMovementChangeDeltaSum = 0; + private float _panningSpeed; + private bool _relativePanning; private bool _invertPanning; /// @@ -412,6 +414,15 @@ namespace FlaxEditor.Viewport set => _isOrtho = value; } + /// + /// Gets or sets if the panning speed should be relative to the camera target. + /// + public bool RelativePanning + { + get => _relativePanning; + set => _relativePanning = value; + } + /// /// Gets or sets if the panning direction is inverted. /// @@ -421,6 +432,15 @@ namespace FlaxEditor.Viewport set => _invertPanning = value; } + /// + /// Gets or sets the camera panning speed. + /// + public float PanningSpeed + { + get => _panningSpeed; + set => _panningSpeed = value; + } + /// /// The input actions collection to processed during user input. /// @@ -508,7 +528,7 @@ namespace FlaxEditor.Viewport maxCamSpeedValue.MinValue = minCamSpeedValue.Value; - if (camSpeedValue.MinValue != minCamSpeedValue.Value) + if (Math.Abs(camSpeedValue.MinValue - minCamSpeedValue.Value) > Mathf.Epsilon) camSpeedValue.MinValue = minCamSpeedValue.Value; }; cameraCM.VisibleChanged += control => minCamSpeedValue.Value = _minMovementSpeed; @@ -518,11 +538,48 @@ namespace FlaxEditor.Viewport minCamSpeedValue.MaxValue = maxCamSpeedValue.Value; - if (camSpeedValue.MaxValue != maxCamSpeedValue.Value) + if (Math.Abs(camSpeedValue.MaxValue - maxCamSpeedValue.Value) > Mathf.Epsilon) camSpeedValue.MaxValue = maxCamSpeedValue.Value; }; cameraCM.VisibleChanged += control => maxCamSpeedValue.Value = _maxMovementSpeed; - + + // Panning Speed + { + var panningSpeed = cameraCM.AddButton("Panning Speed"); + panningSpeed.CloseMenuOnClick = false; + var panningSpeedValue = new FloatValueBox(_panningSpeed, xLocationForExtras, 2, 70.0f, 0.01f, 128.0f, 0.1f) + { + Parent = panningSpeed + }; + + panningSpeedValue.ValueChanged += () => OnPanningSpeedChanged(panningSpeedValue); + cameraCM.VisibleChanged += control => + { + panningSpeed.Visible = !_relativePanning; + panningSpeedValue.Value = _panningSpeed; + }; + } + + // Relative Panning + { + var relativePanning = cameraCM.AddButton("Relative Panning"); + relativePanning.CloseMenuOnClick = false; + var relativePanningValue = new CheckBox(xLocationForExtras, 2, _relativePanning) + { + Parent = relativePanning + }; + + relativePanningValue.StateChanged += checkBox => + { + if (checkBox.Checked != _relativePanning) + { + OnRelativePanningToggled(checkBox); + cameraCM.Hide(); + } + }; + cameraCM.VisibleChanged += control => relativePanningValue.Checked = _relativePanning; + } + // Invert Panning { var invertPanning = cameraCM.AddButton("Invert Panning"); @@ -736,25 +793,6 @@ namespace FlaxEditor.Viewport ViewWidgetButtonMenu.VisibleChanged += control => resolutionValue.Value = ResolutionScale; } #endregion View mode widget - - // Invert Panning - { - var invert = ViewWidgetButtonMenu.AddButton("Invert Panning"); - invert.CloseMenuOnClick = false; - var invertValue = new CheckBox(xLocationForExtras, 2, _invertPanning) - { - Parent = invert - }; - - invertValue.StateChanged += checkBox => - { - if (checkBox.Checked != _invertPanning) - { - _invertPanning = checkBox.Checked; - } - }; - ViewWidgetButtonMenu.VisibleChanged += control => invertValue.Checked = _invertPanning; - } } InputActions.Add(options => options.ViewpointTop, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Top").Orientation))); @@ -794,7 +832,16 @@ namespace FlaxEditor.Viewport if (_movementSpeed > value) _movementSpeed = value; } - + + private void OnPanningSpeedChanged(FloatValueBox control) + { + _panningSpeed = control.Value; + } + + private void OnRelativePanningToggled(Control control) + { + _relativePanning = !_relativePanning; + } private void OnInvertPanningToggled(Control control) { @@ -1636,24 +1683,6 @@ namespace FlaxEditor.Viewport new ViewModeOptions(ViewMode.GlobalIllumination, "Global Illumination"), }; - private void WidgetCamSpeedShowHide(Control cm) - { - if (cm.Visible == false) - return; - - var ccm = (ContextMenu)cm; - foreach (var e in ccm.Items) - { - if (e is ContextMenuButton b) - { - var v = (float)b.Tag; - b.Icon = Mathf.Abs(MovementSpeed - v) < 0.001f - ? Style.Current.CheckBoxTick - : SpriteHandle.Invalid; - } - } - } - private void WidgetViewModeShowHideClicked(ContextMenuButton button) { if (button.Tag is ViewMode v) From 4f93a0d9c9c326f9a7b0bcc0125e8dc4bae0999f Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Wed, 4 Oct 2023 22:07:30 +0200 Subject: [PATCH 009/546] Add default camera settings to editor options * update ViewportOptions to include new default camera settings * update existing default settings in ViewportOptions * extract viewport options setup into own method * initialize new camera settings with default settings in EditorViewport --- Source/Editor/Options/ViewportOptions.cs | 64 ++++++++++++++++++++---- Source/Editor/Viewport/EditorViewport.cs | 31 +++++++++--- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index cee63a562..c780ac06a 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -24,47 +24,89 @@ namespace FlaxEditor.Options [DefaultValue(1.0f), Limit(0.01f, 100.0f)] [EditorDisplay("General"), EditorOrder(101), Tooltip("The mouse wheel sensitivity applied to zoom in orthographic mode.")] public float MouseWheelSensitivity { get; set; } = 1.0f; - + /// - /// Gets or sets the default movement speed for the viewport camera (must match the dropdown menu values in the viewport). + /// Gets or sets the default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values). /// - [DefaultValue(1.0f), Limit(0.01f, 100.0f)] - [EditorDisplay("Defaults"), EditorOrder(110), Tooltip("The default movement speed for the viewport camera (must match the dropdown menu values in the viewport).")] + [DefaultValue(1.0f), Limit(0.05f, 64.0f)] + [EditorDisplay("Defaults"), EditorOrder(110), Tooltip("The default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values).")] public float DefaultMovementSpeed { get; set; } = 1.0f; + /// + /// Gets or sets the default minimum camera movement speed. + /// + [DefaultValue(0.05f), Limit(0.05f, 64.0f)] + [EditorDisplay("Defaults"), EditorOrder(111), Tooltip("The default minimum movement speed for the viewport camera.")] + public float DefaultMinMovementSpeed { get; set; } = 0.05f; + + /// + /// Gets or sets the default maximum camera movement speed. + /// + [DefaultValue(64.0f), Limit(32.0f, 1000.0f)] + [EditorDisplay("Defaults"), EditorOrder(112), Tooltip("The default maximum movement speed for the viewport camera.")] + public float DefaultMaxMovementSpeed { get; set; } = 64f; + /// /// Gets or sets the default near clipping plane distance for the viewport camera. /// [DefaultValue(10.0f), Limit(0.001f, 1000.0f)] - [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default near clipping plane distance for the viewport camera.")] + [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default near clipping plane distance for the viewport camera.")] public float DefaultNearPlane { get; set; } = 10.0f; /// /// Gets or sets the default far clipping plane distance for the viewport camera. /// [DefaultValue(40000.0f), Limit(10.0f)] - [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default far clipping plane distance for the viewport camera.")] + [EditorDisplay("Defaults"), EditorOrder(140), Tooltip("The default far clipping plane distance for the viewport camera.")] public float DefaultFarPlane { get; set; } = 40000.0f; /// /// Gets or sets the default field of view angle (in degrees) for the viewport camera. /// [DefaultValue(60.0f), Limit(35.0f, 160.0f, 0.1f)] - [EditorDisplay("Defaults", "Default Field Of View"), EditorOrder(140), Tooltip("The default field of view angle (in degrees) for the viewport camera.")] + [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("The default field of view angle (in degrees) for the viewport camera.")] public float DefaultFieldOfView { get; set; } = 60.0f; /// - /// Gets or sets if the panning direction is inverted for the viewport camera. + /// Gets or sets the default camera orthographic mode. /// [DefaultValue(false)] - [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("Invert the panning direction for the viewport camera.")] + [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("The default camera orthographic mode.")] + public bool DefaultOrthographicProjection { get; set; } = false; + + /// + /// Gets or sets the default camera orthographic scale (if camera uses orthographic mode). + /// + [DefaultValue(5.0f), Limit(0.001f, 100000.0f, 0.1f)] + [EditorDisplay("Defaults"), EditorOrder(170), Tooltip("The default camera orthographic scale (if camera uses orthographic mode).")] + public float DefaultOrthographicScale { get; set; } = 5.0f; + + /// + /// Gets or sets the default panning direction for the viewport camera. + /// + [DefaultValue(false)] + [EditorDisplay("Defaults"), EditorOrder(180), Tooltip("The default panning direction for the viewport camera.")] public bool DefaultInvertPanning { get; set; } = false; /// - /// Scales editor viewport grid. + /// Gets or sets the default relative panning mode. + /// + [DefaultValue(true)] + [EditorDisplay("Defaults"), EditorOrder(190), Tooltip("The default relative panning mode. Uses distance between camera and target to determine panning speed.")] + public bool DefaultRelativePanning { get; set; } = true; + + /// + /// Gets or sets the default panning speed (ignored if relative panning is speed enabled). + /// + [DefaultValue(0.8f), Limit(0.01f, 128.0f, 0.1f)] + [EditorDisplay("Defaults"), EditorOrder(200), Tooltip("The default camera panning speed (ignored if relative panning is enabled).")] + public float DefaultPanningSpeed { get; set; } = 0.8f; + + /// + /// Gets or sets the default editor viewport grid scale. /// [DefaultValue(50.0f), Limit(25.0f, 500.0f, 5.0f)] - [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("Scales editor viewport grid.")] + [EditorDisplay("Defaults"), EditorOrder(210), Tooltip("The default editor viewport grid scale.")] public float ViewportGridScale { get; set; } = 50.0f; } } diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 28546d6df..a85319298 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -467,15 +467,8 @@ namespace FlaxEditor.Viewport // Setup options { - var options = Editor.Instance.Options.Options; - _movementSpeed = options.Viewport.DefaultMovementSpeed; - _nearPlane = options.Viewport.DefaultNearPlane; - _farPlane = options.Viewport.DefaultFarPlane; - _fieldOfView = options.Viewport.DefaultFieldOfView; - _invertPanning = options.Viewport.DefaultInvertPanning; - Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged; - OnEditorOptionsChanged(options); + SetupViewportOptions(); } if (useWidgets) @@ -808,6 +801,28 @@ namespace FlaxEditor.Viewport // Link for task event task.Begin += OnRenderBegin; } + + /// + /// Sets the viewport options to the default values. + /// + private void SetupViewportOptions() + { + var options = Editor.Instance.Options.Options; + _minMovementSpeed = options.Viewport.DefaultMinMovementSpeed; + _movementSpeed = options.Viewport.DefaultMovementSpeed; + _maxMovementSpeed = options.Viewport.DefaultMaxMovementSpeed; + _panningSpeed = options.Viewport.DefaultPanningSpeed; + _invertPanning = options.Viewport.DefaultInvertPanning; + _relativePanning = options.Viewport.DefaultRelativePanning; + + _isOrtho = options.Viewport.DefaultOrthographicProjection; + _orthoSize = options.Viewport.DefaultOrthographicScale; + _fieldOfView = options.Viewport.DefaultFieldOfView; + _nearPlane = options.Viewport.DefaultNearPlane; + _farPlane = options.Viewport.DefaultFarPlane; + + OnEditorOptionsChanged(options); + } private void OnMovementSpeedChanged(FloatValueBox control) { From a280ce3dc709875041bd8a5003226fe71145370d Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Wed, 4 Oct 2023 22:12:05 +0200 Subject: [PATCH 010/546] Add reset button to camera settings widget --- Source/Editor/Viewport/EditorViewport.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index a85319298..da327a665 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -680,6 +680,22 @@ namespace FlaxEditor.Viewport farPlaneValue.ValueChanged += () => OnFarPlaneChanged(farPlaneValue); cameraCM.VisibleChanged += control => farPlaneValue.Value = _farPlane; } + + cameraCM.AddSeparator(); + + //Reset Button + { + var reset = cameraCM.AddButton("Reset to default"); + reset.ButtonClicked += button => + { + SetupViewportOptions(); + + // trigger UI update + minCamSpeedValue.Value = _minMovementSpeed; + camSpeedValue.Value = _movementSpeed; + maxCamSpeedValue.Value = _maxMovementSpeed; + }; + } #endregion Camera settings widget #region View mode widget From ccf37e9d6861086f2e129832889484d73972f9ba Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Wed, 4 Oct 2023 22:19:48 +0200 Subject: [PATCH 011/546] Add saving and loading of cached values for camera settings * remove "Default" from all settings in ViewportOptions as they are shown in the "Defaults" group anyway --- Source/Editor/Options/ViewportOptions.cs | 26 ++++----- Source/Editor/Viewport/EditorViewport.cs | 69 +++++++++++++++++++----- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index c780ac06a..94e3ee1fc 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -24,83 +24,83 @@ namespace FlaxEditor.Options [DefaultValue(1.0f), Limit(0.01f, 100.0f)] [EditorDisplay("General"), EditorOrder(101), Tooltip("The mouse wheel sensitivity applied to zoom in orthographic mode.")] public float MouseWheelSensitivity { get; set; } = 1.0f; - + /// /// Gets or sets the default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values). /// [DefaultValue(1.0f), Limit(0.05f, 64.0f)] [EditorDisplay("Defaults"), EditorOrder(110), Tooltip("The default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values).")] - public float DefaultMovementSpeed { get; set; } = 1.0f; + public float MovementSpeed { get; set; } = 1.0f; /// /// Gets or sets the default minimum camera movement speed. /// [DefaultValue(0.05f), Limit(0.05f, 64.0f)] [EditorDisplay("Defaults"), EditorOrder(111), Tooltip("The default minimum movement speed for the viewport camera.")] - public float DefaultMinMovementSpeed { get; set; } = 0.05f; + public float MinMovementSpeed { get; set; } = 0.05f; /// /// Gets or sets the default maximum camera movement speed. /// [DefaultValue(64.0f), Limit(32.0f, 1000.0f)] [EditorDisplay("Defaults"), EditorOrder(112), Tooltip("The default maximum movement speed for the viewport camera.")] - public float DefaultMaxMovementSpeed { get; set; } = 64f; - + public float MaxMovementSpeed { get; set; } = 64f; + /// /// Gets or sets the default near clipping plane distance for the viewport camera. /// [DefaultValue(10.0f), Limit(0.001f, 1000.0f)] [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default near clipping plane distance for the viewport camera.")] - public float DefaultNearPlane { get; set; } = 10.0f; + public float NearPlane { get; set; } = 10.0f; /// /// Gets or sets the default far clipping plane distance for the viewport camera. /// [DefaultValue(40000.0f), Limit(10.0f)] [EditorDisplay("Defaults"), EditorOrder(140), Tooltip("The default far clipping plane distance for the viewport camera.")] - public float DefaultFarPlane { get; set; } = 40000.0f; + public float FarPlane { get; set; } = 40000.0f; /// /// Gets or sets the default field of view angle (in degrees) for the viewport camera. /// [DefaultValue(60.0f), Limit(35.0f, 160.0f, 0.1f)] [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("The default field of view angle (in degrees) for the viewport camera.")] - public float DefaultFieldOfView { get; set; } = 60.0f; + public float FieldOfView { get; set; } = 60.0f; /// /// Gets or sets the default camera orthographic mode. /// [DefaultValue(false)] [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("The default camera orthographic mode.")] - public bool DefaultOrthographicProjection { get; set; } = false; + public bool OrthographicProjection { get; set; } = false; /// /// Gets or sets the default camera orthographic scale (if camera uses orthographic mode). /// [DefaultValue(5.0f), Limit(0.001f, 100000.0f, 0.1f)] [EditorDisplay("Defaults"), EditorOrder(170), Tooltip("The default camera orthographic scale (if camera uses orthographic mode).")] - public float DefaultOrthographicScale { get; set; } = 5.0f; + public float OrthographicScale { get; set; } = 5.0f; /// /// Gets or sets the default panning direction for the viewport camera. /// [DefaultValue(false)] [EditorDisplay("Defaults"), EditorOrder(180), Tooltip("The default panning direction for the viewport camera.")] - public bool DefaultInvertPanning { get; set; } = false; + public bool InvertPanning { get; set; } = false; /// /// Gets or sets the default relative panning mode. /// [DefaultValue(true)] [EditorDisplay("Defaults"), EditorOrder(190), Tooltip("The default relative panning mode. Uses distance between camera and target to determine panning speed.")] - public bool DefaultRelativePanning { get; set; } = true; + public bool RelativePanning { get; set; } = true; /// /// Gets or sets the default panning speed (ignored if relative panning is speed enabled). /// [DefaultValue(0.8f), Limit(0.01f, 128.0f, 0.1f)] [EditorDisplay("Defaults"), EditorOrder(200), Tooltip("The default camera panning speed (ignored if relative panning is enabled).")] - public float DefaultPanningSpeed { get; set; } = 0.8f; + public float PanningSpeed { get; set; } = 0.8f; /// /// Gets or sets the default editor viewport grid scale. diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index da327a665..a5abfd2b7 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -134,6 +134,8 @@ namespace FlaxEditor.Viewport /// protected ViewportWidgetButton _cameraButton; + private readonly Editor _editor; + private float _mouseSensitivity; private float _movementSpeed; private float _minMovementSpeed; @@ -455,6 +457,8 @@ namespace FlaxEditor.Viewport public EditorViewport(SceneRenderTask task, ViewportCamera camera, bool useWidgets) : base(task) { + _editor = Editor.Instance; + _mouseAccelerationScale = 0.1f; _useMouseFiltering = false; _useMouseAcceleration = false; @@ -471,6 +475,30 @@ namespace FlaxEditor.Viewport SetupViewportOptions(); } + // Initialize camera values from cache + if (_editor.ProjectCache.TryGetCustomData("CameraMovementSpeedValue", out var cachedState)) + _movementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraMinMovementSpeedValue", out cachedState)) + _minMovementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraMaxMovementSpeedValue", out cachedState)) + _maxMovementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraPanningSpeedValue", out cachedState)) + _panningSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraInvertPanningState", out cachedState)) + _invertPanning = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraRelativePanningState", out cachedState)) + _relativePanning = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraOrthographicState", out cachedState)) + _isOrtho = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraOrthographicSizeValue", out cachedState)) + _orthoSize = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraFieldOfViewValue", out cachedState)) + _fieldOfView = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraNearPlaneValue", out cachedState)) + _nearPlane = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraFarPlaneValue", out cachedState)) + _farPlane = float.Parse(cachedState); + if (useWidgets) { #region Camera settings widget @@ -510,7 +538,7 @@ namespace FlaxEditor.Viewport }; var maxCamSpeedButton = cameraCM.AddButton("Max Cam Speed"); maxCamSpeedButton.CloseMenuOnClick = false; - var maxCamSpeedValue = new FloatValueBox(_maxMovementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, 64.0f, 0.5f) + var maxCamSpeedValue = new FloatValueBox(_maxMovementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, 1000.0f, 0.5f) { Parent = maxCamSpeedButton, }; @@ -824,26 +852,28 @@ namespace FlaxEditor.Viewport private void SetupViewportOptions() { var options = Editor.Instance.Options.Options; - _minMovementSpeed = options.Viewport.DefaultMinMovementSpeed; - _movementSpeed = options.Viewport.DefaultMovementSpeed; - _maxMovementSpeed = options.Viewport.DefaultMaxMovementSpeed; - _panningSpeed = options.Viewport.DefaultPanningSpeed; - _invertPanning = options.Viewport.DefaultInvertPanning; - _relativePanning = options.Viewport.DefaultRelativePanning; + _minMovementSpeed = options.Viewport.MinMovementSpeed; + _movementSpeed = options.Viewport.MovementSpeed; + _maxMovementSpeed = options.Viewport.MaxMovementSpeed; + _panningSpeed = options.Viewport.PanningSpeed; + _invertPanning = options.Viewport.InvertPanning; + _relativePanning = options.Viewport.RelativePanning; - _isOrtho = options.Viewport.DefaultOrthographicProjection; - _orthoSize = options.Viewport.DefaultOrthographicScale; - _fieldOfView = options.Viewport.DefaultFieldOfView; - _nearPlane = options.Viewport.DefaultNearPlane; - _farPlane = options.Viewport.DefaultFarPlane; + _isOrtho = options.Viewport.OrthographicProjection; + _orthoSize = options.Viewport.OrthographicScale; + _fieldOfView = options.Viewport.FieldOfView; + _nearPlane = options.Viewport.NearPlane; + _farPlane = options.Viewport.FarPlane; OnEditorOptionsChanged(options); } - + private void OnMovementSpeedChanged(FloatValueBox control) { var value = Mathf.Clamp(control.Value, _minMovementSpeed, _maxMovementSpeed); MovementSpeed = value; + + _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString()); } private void OnMinMovementSpeedChanged(FloatValueBox control) @@ -853,6 +883,8 @@ namespace FlaxEditor.Viewport if (_movementSpeed < value) _movementSpeed = value; + + _editor.ProjectCache.SetCustomData("CameraMinMovementSpeedValue", _minMovementSpeed.ToString()); } private void OnMaxMovementSpeedChanged(FloatValueBox control) @@ -862,21 +894,26 @@ namespace FlaxEditor.Viewport if (_movementSpeed > value) _movementSpeed = value; + + _editor.ProjectCache.SetCustomData("CameraMaxMovementSpeedValue", _maxMovementSpeed.ToString()); } private void OnPanningSpeedChanged(FloatValueBox control) { _panningSpeed = control.Value; + _editor.ProjectCache.SetCustomData("CameraPanningSpeedValue", _panningSpeed.ToString()); } private void OnRelativePanningToggled(Control control) { _relativePanning = !_relativePanning; + _editor.ProjectCache.SetCustomData("CameraRelativePanningState", _relativePanning.ToString()); } private void OnInvertPanningToggled(Control control) { _invertPanning = !_invertPanning; + _editor.ProjectCache.SetCustomData("CameraInvertPanningState", _invertPanning.ToString()); } @@ -889,6 +926,7 @@ namespace FlaxEditor.Viewport private void OnFieldOfViewChanged(FloatValueBox control) { _fieldOfView = control.Value; + _editor.ProjectCache.SetCustomData("CameraFieldOfViewValue", _fieldOfView.ToString()); } private void OnOrthographicModeToggled(Control control) @@ -900,21 +938,26 @@ namespace FlaxEditor.Viewport var orient = ViewOrientation; OrientViewport(ref orient); } + + _editor.ProjectCache.SetCustomData("CameraOrthographicState", _isOrtho.ToString()); } private void OnOrthographicSizeChanged(FloatValueBox control) { _orthoSize = control.Value; + _editor.ProjectCache.SetCustomData("CameraOrthographicSizeValue", _orthoSize.ToString()); } private void OnNearPlaneChanged(FloatValueBox control) { _nearPlane = control.Value; + _editor.ProjectCache.SetCustomData("CameraNearPlaneValue", _nearPlane.ToString()); } private void OnFarPlaneChanged(FloatValueBox control) { _farPlane = control.Value; + _editor.ProjectCache.SetCustomData("CameraNearPlaneValue", _farPlane.ToString()); } /// From 1f2d665654f74b1791a26d8ac17c7fdc2f99d299 Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Wed, 4 Oct 2023 23:06:27 +0200 Subject: [PATCH 012/546] Add camera speed easing based on Mathf.InterpEaseInOut * change camera move speed method to use easing curve * add methods for inversion of Mathf.InterpEaseInOut (might have better alternative) * add setting for easing curve degree in ViewportOptions * update camera speed adjustments to use easing curve * remove unused camera speed values array * update some comments and namings --- Source/Editor/Options/ViewportOptions.cs | 7 ++ Source/Editor/Viewport/EditorViewport.cs | 139 +++++++++++++---------- 2 files changed, 83 insertions(+), 63 deletions(-) diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index 94e3ee1fc..26caa2694 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -46,6 +46,13 @@ namespace FlaxEditor.Options [EditorDisplay("Defaults"), EditorOrder(112), Tooltip("The default maximum movement speed for the viewport camera.")] public float MaxMovementSpeed { get; set; } = 64f; + /// + /// Gets or sets the degree to which the camera will be eased when using camera flight in the editor window. + /// + [DefaultValue(3.0f), Limit(1.0f, 8.0f)] + [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default degree to which the camera will be eased when using camera flight in the editor window.")] + public float CameraEasingDegree { get; set; } = 3.0f; + /// /// Gets or sets the default near clipping plane distance for the viewport camera. /// diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index a5abfd2b7..51b3a2d78 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -182,11 +182,14 @@ namespace FlaxEditor.Viewport private float _farPlane; private float _orthoSize; private bool _isOrtho; - private float _wheelMovementChangeDeltaSum = 0; + private float _cameraEasingDegree; private float _panningSpeed; private bool _relativePanning; private bool _invertPanning; + private float _linearMovementProgress; + private float _easedMovementProgress; + /// /// Speed of the mouse. /// @@ -499,6 +502,8 @@ namespace FlaxEditor.Viewport if (_editor.ProjectCache.TryGetCustomData("CameraFarPlaneValue", out cachedState)) _farPlane = float.Parse(cachedState); + OnCameraMovementProgressChanged(); + if (useWidgets) { #region Camera settings widget @@ -530,6 +535,7 @@ namespace FlaxEditor.Viewport camSpeedValue.ValueChanged += () => OnMovementSpeedChanged(camSpeedValue); cameraCM.VisibleChanged += control => camSpeedValue.Value = _movementSpeed; + // Minimum & maximum camera speed var minCamSpeedButton = cameraCM.AddButton("Min Cam Speed"); minCamSpeedButton.CloseMenuOnClick = false; var minCamSpeedValue = new FloatValueBox(_minMovementSpeed, xLocationForExtras, 2, 70.0f, 0.05f, _maxMovementSpeed, 0.5f) @@ -564,7 +570,7 @@ namespace FlaxEditor.Viewport }; cameraCM.VisibleChanged += control => maxCamSpeedValue.Value = _maxMovementSpeed; - // Panning Speed + // Panning speed { var panningSpeed = cameraCM.AddButton("Panning Speed"); panningSpeed.CloseMenuOnClick = false; @@ -581,7 +587,7 @@ namespace FlaxEditor.Viewport }; } - // Relative Panning + // Relative panning { var relativePanning = cameraCM.AddButton("Relative Panning"); relativePanning.CloseMenuOnClick = false; @@ -601,7 +607,7 @@ namespace FlaxEditor.Viewport cameraCM.VisibleChanged += control => relativePanningValue.Checked = _relativePanning; } - // Invert Panning + // Invert panning { var invertPanning = cameraCM.AddButton("Invert Panning"); invertPanning.CloseMenuOnClick = false; @@ -616,7 +622,7 @@ namespace FlaxEditor.Viewport cameraCM.AddSeparator(); - // Camera Viewpoints + // Camera viewpoints { var cameraView = cameraCM.AddChildMenu("Viewpoints").ContextMenu; for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++) @@ -629,7 +635,7 @@ namespace FlaxEditor.Viewport cameraView.ButtonClicked += OnViewpointChanged; } - // Orthographic + // Orthographic mode { var ortho = cameraCM.AddButton("Orthographic"); ortho.CloseMenuOnClick = false; @@ -649,7 +655,7 @@ namespace FlaxEditor.Viewport cameraCM.VisibleChanged += control => orthoValue.Checked = _isOrtho; } - // Field of View + // Field of view { var fov = cameraCM.AddButton("Field Of View"); fov.CloseMenuOnClick = false; @@ -666,7 +672,7 @@ namespace FlaxEditor.Viewport }; } - // Ortho Scale + // Orthographic scale { var orthoSize = cameraCM.AddButton("Ortho Scale"); orthoSize.CloseMenuOnClick = false; @@ -683,7 +689,7 @@ namespace FlaxEditor.Viewport }; } - // Near Plane + // Near plane { var nearPlane = cameraCM.AddButton("Near Plane"); nearPlane.CloseMenuOnClick = false; @@ -696,7 +702,7 @@ namespace FlaxEditor.Viewport cameraCM.VisibleChanged += control => nearPlaneValue.Value = _nearPlane; } - // Far Plane + // Far plane { var farPlane = cameraCM.AddButton("Far Plane"); farPlane.CloseMenuOnClick = false; @@ -711,7 +717,7 @@ namespace FlaxEditor.Viewport cameraCM.AddSeparator(); - //Reset Button + // Reset button { var reset = cameraCM.AddButton("Reset to default"); reset.ButtonClicked += button => @@ -752,7 +758,7 @@ namespace FlaxEditor.Viewport } } - // View Flags + // View flags { var viewFlags = ViewWidgetButtonMenu.AddChildMenu("View Flags").ContextMenu; viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = Editor.Instance.Icons.Rotate32; @@ -776,7 +782,7 @@ namespace FlaxEditor.Viewport viewFlags.VisibleChanged += WidgetViewFlagsShowHide; } - // Debug View + // Debug view { var debugView = ViewWidgetButtonMenu.AddChildMenu("Debug View").ContextMenu; for (int i = 0; i < EditorViewportViewModeValues.Length; i++) @@ -839,8 +845,8 @@ namespace FlaxEditor.Viewport InputActions.Add(options => options.ViewpointRight, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Right").Orientation))); InputActions.Add(options => options.ViewpointLeft, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Left").Orientation))); InputActions.Add(options => options.CameraToggleRotation, () => _isVirtualMouseRightDown = !_isVirtualMouseRightDown); - InputActions.Add(options => options.CameraIncreaseMoveSpeed, () => AdjustCameraMoveSpeed(1)); - InputActions.Add(options => options.CameraDecreaseMoveSpeed, () => AdjustCameraMoveSpeed(-1)); + InputActions.Add(options => options.CameraIncreaseMoveSpeed, () => AdjustCameraMoveSpeed(Editor.Instance.Options.Options.Viewport.MouseWheelSensitivity * 0.01f)); + InputActions.Add(options => options.CameraDecreaseMoveSpeed, () => AdjustCameraMoveSpeed(Editor.Instance.Options.Options.Viewport.MouseWheelSensitivity * -0.01f)); // Link for task event task.Begin += OnRenderBegin; @@ -873,6 +879,7 @@ namespace FlaxEditor.Viewport var value = Mathf.Clamp(control.Value, _minMovementSpeed, _maxMovementSpeed); MovementSpeed = value; + OnCameraMovementProgressChanged(); _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString()); } @@ -884,6 +891,7 @@ namespace FlaxEditor.Viewport if (_movementSpeed < value) _movementSpeed = value; + OnCameraMovementProgressChanged(); _editor.ProjectCache.SetCustomData("CameraMinMovementSpeedValue", _minMovementSpeed.ToString()); } @@ -895,6 +903,7 @@ namespace FlaxEditor.Viewport if (_movementSpeed > value) _movementSpeed = value; + OnCameraMovementProgressChanged(); _editor.ProjectCache.SetCustomData("CameraMaxMovementSpeedValue", _maxMovementSpeed.ToString()); } @@ -992,33 +1001,63 @@ namespace FlaxEditor.Viewport } } + /// + /// The inverse of .
+ /// Interpolate between A and B, applying a reverse ease in function. Exponent controls the degree of the curve. + ///
+ private float InterpInverseEaseIn(float a, float b, float alpha, float exponent) + { + return 0.5f * Mathf.Pow((2.0f * (alpha - a) / (b - a)), 1.0f / exponent); + } + + /// + /// The inverse of .
+ /// Interpolate between A and B, applying a reverse ease out function. Exponent controls the degree of the curve. + ///
+ private float InterpInverseEaseOut(float a, float b, float alpha, float exponent) + { + return 1.0f - InterpInverseEaseIn(a, b, 1.0f - alpha, exponent); + } + + /// + /// The inverse of .
+ /// Interpolate between A and B, applying a reverse ease in/out function. Exponent controls the degree of the curve. + ///
+ private float InterpInverseEaseInOut(float a, float b, float alpha, float exponent) + { + if (alpha <= 0.0f) + return a; + if (alpha >= 1.0f) + return b; + + return (alpha < 0.5f) ? InterpInverseEaseIn(a, b, alpha, exponent) : InterpInverseEaseOut(a, b, alpha, exponent); + } + + private void OnCameraMovementProgressChanged() + { + _linearMovementProgress = Math.Abs(_minMovementSpeed - _maxMovementSpeed) < Mathf.Epsilon + ? 0.0f + : Mathf.Remap(_movementSpeed, _minMovementSpeed, _maxMovementSpeed, 0.0f, 1.0f); + + _easedMovementProgress = InterpInverseEaseInOut(0.0f, 1.0f, _linearMovementProgress, _cameraEasingDegree); + } + /// /// Increases or decreases the camera movement speed. /// - /// The stepping direction for speed adjustment. - protected void AdjustCameraMoveSpeed(int step) + /// The difference in camera speed adjustment as a fraction of 1. + protected void AdjustCameraMoveSpeed(float speedDelta) { - int camValueIndex = -1; - for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++) - { - if (Mathf.NearEqual(EditorViewportCameraSpeedValues[i], _movementSpeed)) - { - camValueIndex = i; - break; - } - } - if (camValueIndex == -1) - return; - - if (step > 0) - MovementSpeed = EditorViewportCameraSpeedValues[Mathf.Min(camValueIndex + 1, EditorViewportCameraSpeedValues.Length - 1)]; - else if (step < 0) - MovementSpeed = EditorViewportCameraSpeedValues[Mathf.Max(camValueIndex - 1, 0)]; + _easedMovementProgress = Mathf.Clamp(_easedMovementProgress + speedDelta, 0.0f, 1.0f); + var easedSpeed = Mathf.InterpEaseInOut(_minMovementSpeed, _maxMovementSpeed, _easedMovementProgress, _cameraEasingDegree); + MovementSpeed = Mathf.Round(easedSpeed * 100) / 100; } private void OnEditorOptionsChanged(EditorOptions options) { _mouseSensitivity = options.Viewport.MouseSensitivity; + _cameraEasingDegree = options.Viewport.CameraEasingDegree; + OnCameraMovementProgressChanged(); } private void OnRenderBegin(RenderTask task, GPUContext context) @@ -1210,8 +1249,6 @@ namespace FlaxEditor.Viewport /// The parent window. protected virtual void OnControlMouseBegin(Window win) { - _wheelMovementChangeDeltaSum = 0; - // Hide cursor and start tracking mouse movement win.StartTrackingMouse(false); win.Cursor = CursorType.Hidden; @@ -1406,18 +1443,10 @@ namespace FlaxEditor.Viewport rmbWheel = useMovementSpeed && (_input.IsMouseRightDown || _isVirtualMouseRightDown) && wheelInUse; if (rmbWheel) { - const float step = 4.0f; - _wheelMovementChangeDeltaSum += _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity; - if (_wheelMovementChangeDeltaSum >= step) - { - _wheelMovementChangeDeltaSum -= step; - AdjustCameraMoveSpeed(1); - } - else if (_wheelMovementChangeDeltaSum <= -step) - { - _wheelMovementChangeDeltaSum += step; - AdjustCameraMoveSpeed(-1); - } + // speed delta can be adjusted through mouse wheel sensitivity in editor + // with default sensitivity (1.0), it takes about ~100 scrolls from min to max + var camSpeedDelta = _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity * 0.01f; + AdjustCameraMoveSpeed(camSpeedDelta); } } @@ -1686,22 +1715,6 @@ namespace FlaxEditor.Viewport new CameraViewpoint("Bottom", new Float3(-90, 0, 0)) }; - private readonly float[] EditorViewportCameraSpeedValues = - { - 0.05f, - 0.1f, - 0.25f, - 0.5f, - 1.0f, - 2.0f, - 4.0f, - 6.0f, - 8.0f, - 16.0f, - 32.0f, - 64.0f, - }; - private struct ViewModeOptions { public readonly string Name; From 4b78d5e39e1be9b8cba2a4c857ce9ed7b831ec84 Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Wed, 4 Oct 2023 23:29:32 +0200 Subject: [PATCH 013/546] Orthographic/perspective mode toggle Note: missing dedicated icon! --- Source/Editor/Viewport/EditorViewport.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 51b3a2d78..f83d9eb98 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -134,6 +134,11 @@ namespace FlaxEditor.Viewport ///
protected ViewportWidgetButton _cameraButton; + /// + /// The orthographic mode widget button. + /// + protected ViewportWidgetButton _orthographicModeButton; + private readonly Editor _editor; private float _mouseSensitivity; @@ -524,6 +529,15 @@ namespace FlaxEditor.Viewport }; _cameraWidget.Parent = this; + // Toggle orthographic mode widget + _orthographicModeButton = new ViewportWidgetButton(string.Empty, Editor.Instance.Icons.CamSpeed32, null, true) + { + Checked = !_isOrtho, + TooltipText = "Toggle Orthographic/Perspective Mode", + Parent = _cameraWidget, + }; + _orthographicModeButton.Toggled += OnOrthographicModeToggled; + // Camera speed var camSpeedButton = cameraCM.AddButton("Camera Speed"); camSpeedButton.CloseMenuOnClick = false; @@ -942,6 +956,9 @@ namespace FlaxEditor.Viewport { _isOrtho = !_isOrtho; + if (_orthographicModeButton != null) + _orthographicModeButton.Checked = !_isOrtho; + if (_isOrtho) { var orient = ViewOrientation; From 3a9fdd8b52dba06013b6a1fe0eee4887f898810b Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Thu, 5 Oct 2023 12:55:00 +0200 Subject: [PATCH 014/546] Add possibility to disable camera easing * add camera easing setting to camera settings widget * add camera easing default setting to ViewportOptions * update some settings in ViewportOptions --- Source/Editor/Options/ViewportOptions.cs | 13 ++++-- Source/Editor/Viewport/EditorViewport.cs | 59 +++++++++++++++++++++--- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index 26caa2694..c934fc348 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -46,11 +46,18 @@ namespace FlaxEditor.Options [EditorDisplay("Defaults"), EditorOrder(112), Tooltip("The default maximum movement speed for the viewport camera.")] public float MaxMovementSpeed { get; set; } = 64f; + /// + /// Gets or sets the default camera easing mode. + /// + [DefaultValue(true)] + [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default camera easing mode.")] + public bool UseCameraEasing { get; set; } = true; + /// /// Gets or sets the degree to which the camera will be eased when using camera flight in the editor window. /// [DefaultValue(3.0f), Limit(1.0f, 8.0f)] - [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default degree to which the camera will be eased when using camera flight in the editor window.")] + [EditorDisplay("Defaults"), EditorOrder(121), Tooltip("The default degree to which the camera will be eased when using camera flight in the editor window (ignored if camera easing degree is enabled).")] public float CameraEasingDegree { get; set; } = 3.0f; /// @@ -79,7 +86,7 @@ namespace FlaxEditor.Options /// [DefaultValue(false)] [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("The default camera orthographic mode.")] - public bool OrthographicProjection { get; set; } = false; + public bool UseOrthographicProjection { get; set; } = false; /// /// Gets or sets the default camera orthographic scale (if camera uses orthographic mode). @@ -100,7 +107,7 @@ namespace FlaxEditor.Options /// [DefaultValue(true)] [EditorDisplay("Defaults"), EditorOrder(190), Tooltip("The default relative panning mode. Uses distance between camera and target to determine panning speed.")] - public bool RelativePanning { get; set; } = true; + public bool UseRelativePanning { get; set; } = true; /// /// Gets or sets the default panning speed (ignored if relative panning is speed enabled). diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index f83d9eb98..de4311452 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -187,13 +187,14 @@ namespace FlaxEditor.Viewport private float _farPlane; private float _orthoSize; private bool _isOrtho; + private bool _useCameraEasing; private float _cameraEasingDegree; private float _panningSpeed; private bool _relativePanning; private bool _invertPanning; private float _linearMovementProgress; - private float _easedMovementProgress; + private float _easedMovementProgress = 0.0f; /// /// Speed of the mouse. @@ -238,6 +239,15 @@ namespace FlaxEditor.Viewport set => _maxMovementSpeed = value; } + /// + /// Gets or sets the camera easing mode. + /// + public bool UseCameraEasing + { + get => _useCameraEasing; + set => _useCameraEasing = value; + } + /// /// Gets the mouse movement position delta (user press and move). /// @@ -490,6 +500,8 @@ namespace FlaxEditor.Viewport _minMovementSpeed = float.Parse(cachedState); if (_editor.ProjectCache.TryGetCustomData("CameraMaxMovementSpeedValue", out cachedState)) _maxMovementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("UseCameraEasingState", out cachedState)) + _useCameraEasing = bool.Parse(cachedState); if (_editor.ProjectCache.TryGetCustomData("CameraPanningSpeedValue", out cachedState)) _panningSpeed = float.Parse(cachedState); if (_editor.ProjectCache.TryGetCustomData("CameraInvertPanningState", out cachedState)) @@ -584,6 +596,19 @@ namespace FlaxEditor.Viewport }; cameraCM.VisibleChanged += control => maxCamSpeedValue.Value = _maxMovementSpeed; + // Camera easing + { + var useCameraEasing = cameraCM.AddButton("Camera Easing"); + useCameraEasing.CloseMenuOnClick = false; + var useCameraEasingValue = new CheckBox(xLocationForExtras, 2, _useCameraEasing) + { + Parent = useCameraEasing + }; + + useCameraEasingValue.StateChanged += OnCameraEasingToggled; + cameraCM.VisibleChanged += control => useCameraEasingValue.Checked = _useCameraEasing; + } + // Panning speed { var panningSpeed = cameraCM.AddButton("Panning Speed"); @@ -875,11 +900,12 @@ namespace FlaxEditor.Viewport _minMovementSpeed = options.Viewport.MinMovementSpeed; _movementSpeed = options.Viewport.MovementSpeed; _maxMovementSpeed = options.Viewport.MaxMovementSpeed; + _useCameraEasing = options.Viewport.UseCameraEasing; _panningSpeed = options.Viewport.PanningSpeed; _invertPanning = options.Viewport.InvertPanning; - _relativePanning = options.Viewport.RelativePanning; + _relativePanning = options.Viewport.UseRelativePanning; - _isOrtho = options.Viewport.OrthographicProjection; + _isOrtho = options.Viewport.UseOrthographicProjection; _orthoSize = options.Viewport.OrthographicScale; _fieldOfView = options.Viewport.FieldOfView; _nearPlane = options.Viewport.NearPlane; @@ -920,6 +946,14 @@ namespace FlaxEditor.Viewport OnCameraMovementProgressChanged(); _editor.ProjectCache.SetCustomData("CameraMaxMovementSpeedValue", _maxMovementSpeed.ToString()); } + + private void OnCameraEasingToggled(Control control) + { + _useCameraEasing = !_useCameraEasing; + + OnCameraMovementProgressChanged(); + _editor.ProjectCache.SetCustomData("UseCameraEasingState", _useCameraEasing.ToString()); + } private void OnPanningSpeedChanged(FloatValueBox control) { @@ -1056,6 +1090,8 @@ namespace FlaxEditor.Viewport ? 0.0f : Mathf.Remap(_movementSpeed, _minMovementSpeed, _maxMovementSpeed, 0.0f, 1.0f); + if (!_useCameraEasing) + return; _easedMovementProgress = InterpInverseEaseInOut(0.0f, 1.0f, _linearMovementProgress, _cameraEasingDegree); } @@ -1065,9 +1101,20 @@ namespace FlaxEditor.Viewport /// The difference in camera speed adjustment as a fraction of 1. protected void AdjustCameraMoveSpeed(float speedDelta) { - _easedMovementProgress = Mathf.Clamp(_easedMovementProgress + speedDelta, 0.0f, 1.0f); - var easedSpeed = Mathf.InterpEaseInOut(_minMovementSpeed, _maxMovementSpeed, _easedMovementProgress, _cameraEasingDegree); - MovementSpeed = Mathf.Round(easedSpeed * 100) / 100; + float speed; + + if (_useCameraEasing) + { + _easedMovementProgress = Mathf.Clamp(_easedMovementProgress + speedDelta, 0.0f, 1.0f); + speed = Mathf.InterpEaseInOut(_minMovementSpeed, _maxMovementSpeed, _easedMovementProgress, _cameraEasingDegree); + } + else + { + _linearMovementProgress = Mathf.Clamp(_linearMovementProgress + speedDelta, 0.0f, 1.0f); + speed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, _linearMovementProgress); + } + + MovementSpeed = Mathf.Round(speed * 100) / 100; } private void OnEditorOptionsChanged(EditorOptions options) From fd94cfb4690aea20cb0f0c4c0e5db139da451c4c Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Thu, 5 Oct 2023 20:04:09 +0200 Subject: [PATCH 015/546] Update how camera settings widget is displayed * add option to force a certain text width to ViewportWidgetButton * force camera settings widget to fit longest possible speed value * changed displayed camera speed using string.Format * changed some lines to always correctly display the camera speed * switched from Mathf.Clamp to Mathf.Saturate for values between 0 and 1 --- Source/Editor/Viewport/EditorViewport.cs | 26 +++++++++---------- .../Viewport/Widgets/ViewportWidgetButton.cs | 11 +++++--- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index de4311452..399a71418 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -217,7 +217,7 @@ namespace FlaxEditor.Viewport _movementSpeed = value; if (_cameraButton != null) - _cameraButton.Text = _movementSpeed.ToString(); + _cameraButton.Text = string.Format("{0:0.##}", _movementSpeed); } } @@ -495,7 +495,7 @@ namespace FlaxEditor.Viewport // Initialize camera values from cache if (_editor.ProjectCache.TryGetCustomData("CameraMovementSpeedValue", out var cachedState)) - _movementSpeed = float.Parse(cachedState); + MovementSpeed = float.Parse(cachedState); if (_editor.ProjectCache.TryGetCustomData("CameraMinMovementSpeedValue", out cachedState)) _minMovementSpeed = float.Parse(cachedState); if (_editor.ProjectCache.TryGetCustomData("CameraMaxMovementSpeedValue", out cachedState)) @@ -533,7 +533,7 @@ namespace FlaxEditor.Viewport // Camera settings menu var cameraCM = new ContextMenu(); - _cameraButton = new ViewportWidgetButton(_movementSpeed.ToString(), Editor.Instance.Icons.Camera64, cameraCM) + _cameraButton = new ViewportWidgetButton(string.Format("{0:0.##}", _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, Style.Current.FontMedium.MeasureText("000.00").X) { Tag = this, TooltipText = "Camera Settings", @@ -546,7 +546,7 @@ namespace FlaxEditor.Viewport { Checked = !_isOrtho, TooltipText = "Toggle Orthographic/Perspective Mode", - Parent = _cameraWidget, + Parent = _cameraWidget }; _orthographicModeButton.Toggled += OnOrthographicModeToggled; @@ -555,7 +555,7 @@ namespace FlaxEditor.Viewport camSpeedButton.CloseMenuOnClick = false; var camSpeedValue = new FloatValueBox(_movementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, _maxMovementSpeed, 0.5f) { - Parent = camSpeedButton, + Parent = camSpeedButton }; camSpeedValue.ValueChanged += () => OnMovementSpeedChanged(camSpeedValue); @@ -566,13 +566,13 @@ namespace FlaxEditor.Viewport minCamSpeedButton.CloseMenuOnClick = false; var minCamSpeedValue = new FloatValueBox(_minMovementSpeed, xLocationForExtras, 2, 70.0f, 0.05f, _maxMovementSpeed, 0.5f) { - Parent = minCamSpeedButton, + Parent = minCamSpeedButton }; var maxCamSpeedButton = cameraCM.AddButton("Max Cam Speed"); maxCamSpeedButton.CloseMenuOnClick = false; var maxCamSpeedValue = new FloatValueBox(_maxMovementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, 1000.0f, 0.5f) { - Parent = maxCamSpeedButton, + Parent = maxCamSpeedButton }; minCamSpeedValue.ValueChanged += () => @@ -898,7 +898,7 @@ namespace FlaxEditor.Viewport { var options = Editor.Instance.Options.Options; _minMovementSpeed = options.Viewport.MinMovementSpeed; - _movementSpeed = options.Viewport.MovementSpeed; + MovementSpeed = options.Viewport.MovementSpeed; _maxMovementSpeed = options.Viewport.MaxMovementSpeed; _useCameraEasing = options.Viewport.UseCameraEasing; _panningSpeed = options.Viewport.PanningSpeed; @@ -929,7 +929,7 @@ namespace FlaxEditor.Viewport _minMovementSpeed = value; if (_movementSpeed < value) - _movementSpeed = value; + MovementSpeed = value; OnCameraMovementProgressChanged(); _editor.ProjectCache.SetCustomData("CameraMinMovementSpeedValue", _minMovementSpeed.ToString()); @@ -941,7 +941,7 @@ namespace FlaxEditor.Viewport _maxMovementSpeed = value; if (_movementSpeed > value) - _movementSpeed = value; + MovementSpeed = value; OnCameraMovementProgressChanged(); _editor.ProjectCache.SetCustomData("CameraMaxMovementSpeedValue", _maxMovementSpeed.ToString()); @@ -1105,16 +1105,16 @@ namespace FlaxEditor.Viewport if (_useCameraEasing) { - _easedMovementProgress = Mathf.Clamp(_easedMovementProgress + speedDelta, 0.0f, 1.0f); + _easedMovementProgress = Mathf.Saturate(_easedMovementProgress + speedDelta); speed = Mathf.InterpEaseInOut(_minMovementSpeed, _maxMovementSpeed, _easedMovementProgress, _cameraEasingDegree); } else { - _linearMovementProgress = Mathf.Clamp(_linearMovementProgress + speedDelta, 0.0f, 1.0f); + _linearMovementProgress = Mathf.Saturate(_linearMovementProgress + speedDelta); speed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, _linearMovementProgress); } - MovementSpeed = Mathf.Round(speed * 100) / 100; + MovementSpeed = speed; } private void OnEditorOptionsChanged(EditorOptions options) diff --git a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs index 481bc3f1b..5ff5cdb6d 100644 --- a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs +++ b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs @@ -19,6 +19,7 @@ namespace FlaxEditor.Viewport.Widgets private bool _checked; private bool _autoCheck; private bool _isMosueDown; + private float _forcedTextWidth; /// /// Event fired when user toggles checked state. @@ -63,14 +64,16 @@ namespace FlaxEditor.Viewport.Widgets /// The text. /// The icon. /// The context menu. - /// if set to true will be automatic checked on mouse click. - public ViewportWidgetButton(string text, SpriteHandle icon, ContextMenu contextMenu = null, bool autoCheck = false) - : base(0, 0, CalculateButtonWidth(0, icon.IsValid), ViewportWidgetsContainer.WidgetsHeight) + /// If set to true will be automatic checked on mouse click. + /// Forces the text to be drawn with the specified width. + public ViewportWidgetButton(string text, SpriteHandle icon, ContextMenu contextMenu = null, bool autoCheck = false, float textWidth = 0.0f) + : base(0, 0, CalculateButtonWidth(textWidth, icon.IsValid), ViewportWidgetsContainer.WidgetsHeight) { _text = text; Icon = icon; _cm = contextMenu; _autoCheck = autoCheck; + _forcedTextWidth = textWidth; if (_cm != null) _cm.VisibleChanged += CmOnVisibleChanged; @@ -160,7 +163,7 @@ namespace FlaxEditor.Viewport.Widgets var style = Style.Current; if (style != null && style.FontMedium) - Width = CalculateButtonWidth(style.FontMedium.MeasureText(_text).X, Icon.IsValid); + Width = CalculateButtonWidth(_forcedTextWidth > 0.0f ? _forcedTextWidth : style.FontMedium.MeasureText(_text).X, Icon.IsValid); } } } From 1736aaeb6a791005c978d6d57c1bd9f82c0fd53a Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Fri, 6 Oct 2023 22:54:26 +0200 Subject: [PATCH 016/546] Update Source/Editor/Surface/Archetypes/Material.cs Co-authored-by: stefnotch --- Source/Editor/Surface/Archetypes/Material.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index c725b0662..77bdb25c6 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -910,10 +910,6 @@ namespace FlaxEditor.Surface.Archetypes Flags = NodeFlags.MaterialGraph, Size = new Float2(150, 20), ConnectionsHints = ConnectionsHint.Vector, - DefaultValues = new object[] - { - 1 - }, Elements = new[] { NodeElementArchetype.Factory.Input(0, "value", true, typeof(float), 0), From 4e2870e90cdcc0b3c04713eefaf1f4b469ef997b Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Fri, 6 Oct 2023 22:56:16 +0200 Subject: [PATCH 017/546] Update Source/Editor/Surface/Archetypes/Material.cs Co-authored-by: stefnotch --- Source/Editor/Surface/Archetypes/Material.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 77bdb25c6..6fee3a978 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -909,7 +909,9 @@ namespace FlaxEditor.Surface.Archetypes Description = "Creates a partial derivative (fwidth)", Flags = NodeFlags.MaterialGraph, Size = new Float2(150, 20), - ConnectionsHints = ConnectionsHint.Vector, + ConnectionsHints = ConnectionsHint.Numeric, + IndependentBoxes = new[] { 0 }, + DependentBoxes = new[] { 1 }, Elements = new[] { NodeElementArchetype.Factory.Input(0, "value", true, typeof(float), 0), From d7095957d089972be84376a66bd43d981bfe5908 Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Fri, 6 Oct 2023 23:07:00 +0200 Subject: [PATCH 018/546] Update Source/Editor/Surface/Archetypes/Material.cs Co-authored-by: stefnotch --- Source/Editor/Surface/Archetypes/Material.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 6fee3a978..ee3270460 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -914,8 +914,8 @@ namespace FlaxEditor.Surface.Archetypes DependentBoxes = new[] { 1 }, Elements = new[] { - NodeElementArchetype.Factory.Input(0, "value", true, typeof(float), 0), - NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), + NodeElementArchetype.Factory.Input(0, "value", true, null, 0), + NodeElementArchetype.Factory.Output(0, string.Empty, null, 1), } }, new NodeArchetype From 004e2ab5e80f8a91a3cafffdfb94208d7da646da Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Fri, 6 Oct 2023 23:07:19 +0200 Subject: [PATCH 019/546] Update Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp Co-authored-by: stefnotch --- .../Tools/MaterialGenerator/MaterialGenerator.Material.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index 0c8d0780e..df594a02b 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -533,8 +533,8 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) // FWidth case 41: { - const auto d = tryGetValue(node->GetBox(0), node->Values[0]).AsFloat(); - value = writeLocal(ValueType::Float, String::Format(TEXT("abs(ddx({0})) + abs(ddy({0}))"), d.Value), node); + const auto inValue = tryGetValue(node->GetBox(0), 0, Value::Zero); + value = writeLocal(inValue.Type, String::Format(TEXT("fwidth({0})"), inValue.Value), node); break; } //AAStep (smooth version of step) From 00aa54cde82da867b2a861cd1b6bf307dafc0a7f Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Sat, 7 Oct 2023 02:40:52 +0200 Subject: [PATCH 020/546] Update camera speed stepping and easing * remove complex curve function * update camera easing feature to use Mathf.Pow instead * add total camera speed steps to ViewportOptions * change default value for max camera speed from 64 to 32 * update ViewportOptions ordering and grouping * update string format for movement speed --- Source/Editor/Options/ViewportOptions.cs | 77 +++++++++++--------- Source/Editor/Viewport/EditorViewport.cs | 93 ++++++++---------------- 2 files changed, 72 insertions(+), 98 deletions(-) diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index c934fc348..0fd018cbc 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -26,101 +26,108 @@ namespace FlaxEditor.Options public float MouseWheelSensitivity { get; set; } = 1.0f; /// - /// Gets or sets the default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values). + /// Gets or sets the total amount of steps the camera needs to go from minimum to maximum speed. /// - [DefaultValue(1.0f), Limit(0.05f, 64.0f)] - [EditorDisplay("Defaults"), EditorOrder(110), Tooltip("The default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values).")] - public float MovementSpeed { get; set; } = 1.0f; - - /// - /// Gets or sets the default minimum camera movement speed. - /// - [DefaultValue(0.05f), Limit(0.05f, 64.0f)] - [EditorDisplay("Defaults"), EditorOrder(111), Tooltip("The default minimum movement speed for the viewport camera.")] - public float MinMovementSpeed { get; set; } = 0.05f; - - /// - /// Gets or sets the default maximum camera movement speed. - /// - [DefaultValue(64.0f), Limit(32.0f, 1000.0f)] - [EditorDisplay("Defaults"), EditorOrder(112), Tooltip("The default maximum movement speed for the viewport camera.")] - public float MaxMovementSpeed { get; set; } = 64f; - - /// - /// Gets or sets the default camera easing mode. - /// - [DefaultValue(true)] - [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default camera easing mode.")] - public bool UseCameraEasing { get; set; } = true; + [DefaultValue(64), Limit(1, 128)] + [EditorDisplay("Camera"), EditorOrder(110), Tooltip("The total amount of steps the camera needs to go from minimum to maximum speed.")] + public int TotalCameraSpeedSteps { get; set; } = 64; /// /// Gets or sets the degree to which the camera will be eased when using camera flight in the editor window. /// [DefaultValue(3.0f), Limit(1.0f, 8.0f)] - [EditorDisplay("Defaults"), EditorOrder(121), Tooltip("The default degree to which the camera will be eased when using camera flight in the editor window (ignored if camera easing degree is enabled).")] + [EditorDisplay("Camera"), EditorOrder(111), Tooltip("The degree to which the camera will be eased when using camera flight in the editor window (ignored if camera easing degree is enabled).")] public float CameraEasingDegree { get; set; } = 3.0f; + /// + /// Gets or sets the default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values). + /// + [DefaultValue(1.0f), Limit(0.05f, 32.0f)] + [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values).")] + public float MovementSpeed { get; set; } = 1.0f; + + /// + /// Gets or sets the default minimum camera movement speed. + /// + [DefaultValue(0.05f), Limit(0.05f, 32.0f)] + [EditorDisplay("Defaults"), EditorOrder(121), Tooltip("The default minimum movement speed for the viewport camera.")] + public float MinMovementSpeed { get; set; } = 0.05f; + + /// + /// Gets or sets the default maximum camera movement speed. + /// + [DefaultValue(32.0f), Limit(16.0f, 1000.0f)] + [EditorDisplay("Defaults"), EditorOrder(122), Tooltip("The default maximum movement speed for the viewport camera.")] + public float MaxMovementSpeed { get; set; } = 32f; + + /// + /// Gets or sets the default camera easing mode. + /// + [DefaultValue(true)] + [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default camera easing mode.")] + public bool UseCameraEasing { get; set; } = true; + /// /// Gets or sets the default near clipping plane distance for the viewport camera. /// [DefaultValue(10.0f), Limit(0.001f, 1000.0f)] - [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default near clipping plane distance for the viewport camera.")] + [EditorDisplay("Defaults"), EditorOrder(140), Tooltip("The default near clipping plane distance for the viewport camera.")] public float NearPlane { get; set; } = 10.0f; /// /// Gets or sets the default far clipping plane distance for the viewport camera. /// [DefaultValue(40000.0f), Limit(10.0f)] - [EditorDisplay("Defaults"), EditorOrder(140), Tooltip("The default far clipping plane distance for the viewport camera.")] + [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("The default far clipping plane distance for the viewport camera.")] public float FarPlane { get; set; } = 40000.0f; /// /// Gets or sets the default field of view angle (in degrees) for the viewport camera. /// [DefaultValue(60.0f), Limit(35.0f, 160.0f, 0.1f)] - [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("The default field of view angle (in degrees) for the viewport camera.")] + [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("The default field of view angle (in degrees) for the viewport camera.")] public float FieldOfView { get; set; } = 60.0f; /// /// Gets or sets the default camera orthographic mode. /// [DefaultValue(false)] - [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("The default camera orthographic mode.")] + [EditorDisplay("Defaults"), EditorOrder(170), Tooltip("The default camera orthographic mode.")] public bool UseOrthographicProjection { get; set; } = false; /// /// Gets or sets the default camera orthographic scale (if camera uses orthographic mode). /// [DefaultValue(5.0f), Limit(0.001f, 100000.0f, 0.1f)] - [EditorDisplay("Defaults"), EditorOrder(170), Tooltip("The default camera orthographic scale (if camera uses orthographic mode).")] + [EditorDisplay("Defaults"), EditorOrder(180), Tooltip("The default camera orthographic scale (if camera uses orthographic mode).")] public float OrthographicScale { get; set; } = 5.0f; /// /// Gets or sets the default panning direction for the viewport camera. /// [DefaultValue(false)] - [EditorDisplay("Defaults"), EditorOrder(180), Tooltip("The default panning direction for the viewport camera.")] + [EditorDisplay("Defaults"), EditorOrder(190), Tooltip("The default panning direction for the viewport camera.")] public bool InvertPanning { get; set; } = false; /// /// Gets or sets the default relative panning mode. /// [DefaultValue(true)] - [EditorDisplay("Defaults"), EditorOrder(190), Tooltip("The default relative panning mode. Uses distance between camera and target to determine panning speed.")] + [EditorDisplay("Defaults"), EditorOrder(200), Tooltip("The default relative panning mode. Uses distance between camera and target to determine panning speed.")] public bool UseRelativePanning { get; set; } = true; /// /// Gets or sets the default panning speed (ignored if relative panning is speed enabled). /// [DefaultValue(0.8f), Limit(0.01f, 128.0f, 0.1f)] - [EditorDisplay("Defaults"), EditorOrder(200), Tooltip("The default camera panning speed (ignored if relative panning is enabled).")] + [EditorDisplay("Defaults"), EditorOrder(210), Tooltip("The default camera panning speed (ignored if relative panning is enabled).")] public float PanningSpeed { get; set; } = 0.8f; /// /// Gets or sets the default editor viewport grid scale. /// [DefaultValue(50.0f), Limit(25.0f, 500.0f, 5.0f)] - [EditorDisplay("Defaults"), EditorOrder(210), Tooltip("The default editor viewport grid scale.")] + [EditorDisplay("Defaults"), EditorOrder(220), Tooltip("The default editor viewport grid scale.")] public float ViewportGridScale { get; set; } = 50.0f; } } diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 399a71418..ba4087025 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -178,7 +178,6 @@ namespace FlaxEditor.Viewport protected Float2 _mouseDelta; // Camera - private ViewportCamera _camera; private float _yaw; private float _pitch; @@ -193,8 +192,8 @@ namespace FlaxEditor.Viewport private bool _relativePanning; private bool _invertPanning; - private float _linearMovementProgress; - private float _easedMovementProgress = 0.0f; + private int _speedStep; + private int _maxSpeedSteps; /// /// Speed of the mouse. @@ -216,8 +215,9 @@ namespace FlaxEditor.Viewport { _movementSpeed = value; + var format = (_movementSpeed < 1.0f) ? "{0:0.##}" : "{0:#}"; if (_cameraButton != null) - _cameraButton.Text = string.Format("{0:0.##}", _movementSpeed); + _cameraButton.Text = string.Format(format, _movementSpeed); } } @@ -527,13 +527,14 @@ namespace FlaxEditor.Viewport var largestText = "Relative Panning"; var textSize = Style.Current.FontMedium.MeasureText(largestText); var xLocationForExtras = textSize.X + 5; + var format = (_movementSpeed < 1.0f) ? "{0:0.##}" : "{0:#}"; // Camera settings widget _cameraWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); // Camera settings menu var cameraCM = new ContextMenu(); - _cameraButton = new ViewportWidgetButton(string.Format("{0:0.##}", _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, Style.Current.FontMedium.MeasureText("000.00").X) + _cameraButton = new ViewportWidgetButton(string.Format(format, _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, Style.Current.FontMedium.MeasureText("0.00").X) { Tag = this, TooltipText = "Camera Settings", @@ -884,8 +885,8 @@ namespace FlaxEditor.Viewport InputActions.Add(options => options.ViewpointRight, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Right").Orientation))); InputActions.Add(options => options.ViewpointLeft, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Left").Orientation))); InputActions.Add(options => options.CameraToggleRotation, () => _isVirtualMouseRightDown = !_isVirtualMouseRightDown); - InputActions.Add(options => options.CameraIncreaseMoveSpeed, () => AdjustCameraMoveSpeed(Editor.Instance.Options.Options.Viewport.MouseWheelSensitivity * 0.01f)); - InputActions.Add(options => options.CameraDecreaseMoveSpeed, () => AdjustCameraMoveSpeed(Editor.Instance.Options.Options.Viewport.MouseWheelSensitivity * -0.01f)); + InputActions.Add(options => options.CameraIncreaseMoveSpeed, () => AdjustCameraMoveSpeed(1)); + InputActions.Add(options => options.CameraDecreaseMoveSpeed, () => AdjustCameraMoveSpeed(-1)); // Link for task event task.Begin += OnRenderBegin; @@ -1052,74 +1053,42 @@ namespace FlaxEditor.Viewport } } - /// - /// The inverse of .
- /// Interpolate between A and B, applying a reverse ease in function. Exponent controls the degree of the curve. - ///
- private float InterpInverseEaseIn(float a, float b, float alpha, float exponent) - { - return 0.5f * Mathf.Pow((2.0f * (alpha - a) / (b - a)), 1.0f / exponent); - } - - /// - /// The inverse of .
- /// Interpolate between A and B, applying a reverse ease out function. Exponent controls the degree of the curve. - ///
- private float InterpInverseEaseOut(float a, float b, float alpha, float exponent) - { - return 1.0f - InterpInverseEaseIn(a, b, 1.0f - alpha, exponent); - } - - /// - /// The inverse of .
- /// Interpolate between A and B, applying a reverse ease in/out function. Exponent controls the degree of the curve. - ///
- private float InterpInverseEaseInOut(float a, float b, float alpha, float exponent) - { - if (alpha <= 0.0f) - return a; - if (alpha >= 1.0f) - return b; - - return (alpha < 0.5f) ? InterpInverseEaseIn(a, b, alpha, exponent) : InterpInverseEaseOut(a, b, alpha, exponent); - } - private void OnCameraMovementProgressChanged() { - _linearMovementProgress = Math.Abs(_minMovementSpeed - _maxMovementSpeed) < Mathf.Epsilon - ? 0.0f - : Mathf.Remap(_movementSpeed, _minMovementSpeed, _maxMovementSpeed, 0.0f, 1.0f); - - if (!_useCameraEasing) + // prevent NaN + if (Math.Abs(_minMovementSpeed - _maxMovementSpeed) < Mathf.Epsilon) { + _speedStep = 0; return; - _easedMovementProgress = InterpInverseEaseInOut(0.0f, 1.0f, _linearMovementProgress, _cameraEasingDegree); + } + + // calculate current linear/eased progress + float progress = Mathf.Remap(_movementSpeed, _minMovementSpeed, _maxMovementSpeed, 0.0f, 1.0f); + if (_useCameraEasing) + progress = Mathf.Pow(progress, 1.0f / _cameraEasingDegree); + + _speedStep = Mathf.RoundToInt(progress * _maxSpeedSteps); } /// /// Increases or decreases the camera movement speed. /// - /// The difference in camera speed adjustment as a fraction of 1. - protected void AdjustCameraMoveSpeed(float speedDelta) + /// The stepping direction for speed adjustment. + protected void AdjustCameraMoveSpeed(int step) { - float speed; + _speedStep = Mathf.Clamp(_speedStep + step, 0, _maxSpeedSteps); - if (_useCameraEasing) - { - _easedMovementProgress = Mathf.Saturate(_easedMovementProgress + speedDelta); - speed = Mathf.InterpEaseInOut(_minMovementSpeed, _maxMovementSpeed, _easedMovementProgress, _cameraEasingDegree); - } - else - { - _linearMovementProgress = Mathf.Saturate(_linearMovementProgress + speedDelta); - speed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, _linearMovementProgress); - } + // calculate new linear/eased progress + var progress = _useCameraEasing + ? Mathf.Pow((float)_speedStep / _maxSpeedSteps, _cameraEasingDegree) + : (float)_speedStep / _maxSpeedSteps; - MovementSpeed = speed; + MovementSpeed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, progress); } private void OnEditorOptionsChanged(EditorOptions options) { _mouseSensitivity = options.Viewport.MouseSensitivity; + _maxSpeedSteps = options.Viewport.TotalCameraSpeedSteps; _cameraEasingDegree = options.Viewport.CameraEasingDegree; OnCameraMovementProgressChanged(); } @@ -1507,10 +1476,8 @@ namespace FlaxEditor.Viewport rmbWheel = useMovementSpeed && (_input.IsMouseRightDown || _isVirtualMouseRightDown) && wheelInUse; if (rmbWheel) { - // speed delta can be adjusted through mouse wheel sensitivity in editor - // with default sensitivity (1.0), it takes about ~100 scrolls from min to max - var camSpeedDelta = _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity * 0.01f; - AdjustCameraMoveSpeed(camSpeedDelta); + var step = _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity; + AdjustCameraMoveSpeed(step > 0.0f ? 1 : -1); } } From 997c442e69366cc1c5e4404368fd252a4d134bf7 Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Sun, 8 Oct 2023 01:07:36 +0200 Subject: [PATCH 021/546] Update movement speed formatting * add property for movement speed text format * add additional checks to camera speed progress calculation * round movement speed to make it visually more appealing in the context menu --- Source/Editor/Viewport/EditorViewport.cs | 45 ++++++++++++++++++++---- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index ba4087025..8ce99b115 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -178,6 +178,7 @@ namespace FlaxEditor.Viewport protected Float2 _mouseDelta; // Camera + private ViewportCamera _camera; private float _yaw; private float _pitch; @@ -205,6 +206,23 @@ namespace FlaxEditor.Viewport ///
public float MouseWheelZoomSpeedFactor = 1; + /// + /// Format of the text for the camera move speed. + /// + private string MovementSpeedTextFormat { + get { + if (Mathf.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon || Mathf.Abs(_movementSpeed - _minMovementSpeed) < Mathf.Epsilon) + return "{0:0.##}"; + + if (_movementSpeed < 10.0f) + return "{0:0.00}"; + else if (_movementSpeed < 100.0f) + return "{0:0.0}"; + else + return "{0:#}"; + } + } + /// /// Gets or sets the camera movement speed. /// @@ -215,9 +233,8 @@ namespace FlaxEditor.Viewport { _movementSpeed = value; - var format = (_movementSpeed < 1.0f) ? "{0:0.##}" : "{0:#}"; if (_cameraButton != null) - _cameraButton.Text = string.Format(format, _movementSpeed); + _cameraButton.Text = string.Format(MovementSpeedTextFormat, _movementSpeed); } } @@ -527,14 +544,14 @@ namespace FlaxEditor.Viewport var largestText = "Relative Panning"; var textSize = Style.Current.FontMedium.MeasureText(largestText); var xLocationForExtras = textSize.X + 5; - var format = (_movementSpeed < 1.0f) ? "{0:0.##}" : "{0:#}"; + var cameraSpeedTextWidth = Style.Current.FontMedium.MeasureText("0.00").X; // Camera settings widget _cameraWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); // Camera settings menu var cameraCM = new ContextMenu(); - _cameraButton = new ViewportWidgetButton(string.Format(format, _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, Style.Current.FontMedium.MeasureText("0.00").X) + _cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth) { Tag = this, TooltipText = "Camera Settings", @@ -1060,9 +1077,21 @@ namespace FlaxEditor.Viewport _speedStep = 0; return; } + + if (Math.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon) + { + _speedStep = _maxSpeedSteps; + return; + } + else if (Math.Abs(_movementSpeed - _minMovementSpeed) < Mathf.Epsilon) + { + _speedStep = 0; + return; + } // calculate current linear/eased progress - float progress = Mathf.Remap(_movementSpeed, _minMovementSpeed, _maxMovementSpeed, 0.0f, 1.0f); + var progress = Mathf.Remap(_movementSpeed, _minMovementSpeed, _maxMovementSpeed, 0.0f, 1.0f); + if (_useCameraEasing) progress = Mathf.Pow(progress, 1.0f / _cameraEasingDegree); @@ -1081,8 +1110,10 @@ namespace FlaxEditor.Viewport var progress = _useCameraEasing ? Mathf.Pow((float)_speedStep / _maxSpeedSteps, _cameraEasingDegree) : (float)_speedStep / _maxSpeedSteps; - - MovementSpeed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, progress); + + var speed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, progress); + MovementSpeed = (float)Math.Round(speed, 3); + _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString()); } private void OnEditorOptionsChanged(EditorOptions options) From 966e5cdda27b209d2a775375b21379cb72b4fc3e Mon Sep 17 00:00:00 2001 From: Christopher Rothert Date: Sun, 8 Oct 2023 01:30:35 +0200 Subject: [PATCH 022/546] Update some comments --- Source/Editor/Viewport/EditorViewport.cs | 40 +++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 8ce99b115..8f733ce0c 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -546,10 +546,10 @@ namespace FlaxEditor.Viewport var xLocationForExtras = textSize.X + 5; var cameraSpeedTextWidth = Style.Current.FontMedium.MeasureText("0.00").X; - // Camera settings widget + // Camera Settings Widget _cameraWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); - // Camera settings menu + // Camera Settings Menu var cameraCM = new ContextMenu(); _cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth) { @@ -559,7 +559,7 @@ namespace FlaxEditor.Viewport }; _cameraWidget.Parent = this; - // Toggle orthographic mode widget + // Orthographic/Perspective Mode Widget _orthographicModeButton = new ViewportWidgetButton(string.Empty, Editor.Instance.Icons.CamSpeed32, null, true) { Checked = !_isOrtho, @@ -568,7 +568,7 @@ namespace FlaxEditor.Viewport }; _orthographicModeButton.Toggled += OnOrthographicModeToggled; - // Camera speed + // Camera Speed var camSpeedButton = cameraCM.AddButton("Camera Speed"); camSpeedButton.CloseMenuOnClick = false; var camSpeedValue = new FloatValueBox(_movementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, _maxMovementSpeed, 0.5f) @@ -579,7 +579,7 @@ namespace FlaxEditor.Viewport camSpeedValue.ValueChanged += () => OnMovementSpeedChanged(camSpeedValue); cameraCM.VisibleChanged += control => camSpeedValue.Value = _movementSpeed; - // Minimum & maximum camera speed + // Minimum & Maximum Camera Speed var minCamSpeedButton = cameraCM.AddButton("Min Cam Speed"); minCamSpeedButton.CloseMenuOnClick = false; var minCamSpeedValue = new FloatValueBox(_minMovementSpeed, xLocationForExtras, 2, 70.0f, 0.05f, _maxMovementSpeed, 0.5f) @@ -614,7 +614,7 @@ namespace FlaxEditor.Viewport }; cameraCM.VisibleChanged += control => maxCamSpeedValue.Value = _maxMovementSpeed; - // Camera easing + // Camera Easing { var useCameraEasing = cameraCM.AddButton("Camera Easing"); useCameraEasing.CloseMenuOnClick = false; @@ -627,7 +627,7 @@ namespace FlaxEditor.Viewport cameraCM.VisibleChanged += control => useCameraEasingValue.Checked = _useCameraEasing; } - // Panning speed + // Panning Speed { var panningSpeed = cameraCM.AddButton("Panning Speed"); panningSpeed.CloseMenuOnClick = false; @@ -644,7 +644,7 @@ namespace FlaxEditor.Viewport }; } - // Relative panning + // Relative Panning { var relativePanning = cameraCM.AddButton("Relative Panning"); relativePanning.CloseMenuOnClick = false; @@ -664,7 +664,7 @@ namespace FlaxEditor.Viewport cameraCM.VisibleChanged += control => relativePanningValue.Checked = _relativePanning; } - // Invert panning + // Invert Panning { var invertPanning = cameraCM.AddButton("Invert Panning"); invertPanning.CloseMenuOnClick = false; @@ -679,7 +679,7 @@ namespace FlaxEditor.Viewport cameraCM.AddSeparator(); - // Camera viewpoints + // Camera Viewpoints { var cameraView = cameraCM.AddChildMenu("Viewpoints").ContextMenu; for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++) @@ -692,7 +692,7 @@ namespace FlaxEditor.Viewport cameraView.ButtonClicked += OnViewpointChanged; } - // Orthographic mode + // Orthographic Mode { var ortho = cameraCM.AddButton("Orthographic"); ortho.CloseMenuOnClick = false; @@ -712,7 +712,7 @@ namespace FlaxEditor.Viewport cameraCM.VisibleChanged += control => orthoValue.Checked = _isOrtho; } - // Field of view + // Field of View { var fov = cameraCM.AddButton("Field Of View"); fov.CloseMenuOnClick = false; @@ -729,7 +729,7 @@ namespace FlaxEditor.Viewport }; } - // Orthographic scale + // Orthographic Scale { var orthoSize = cameraCM.AddButton("Ortho Scale"); orthoSize.CloseMenuOnClick = false; @@ -746,7 +746,7 @@ namespace FlaxEditor.Viewport }; } - // Near plane + // Near Plane { var nearPlane = cameraCM.AddButton("Near Plane"); nearPlane.CloseMenuOnClick = false; @@ -759,7 +759,7 @@ namespace FlaxEditor.Viewport cameraCM.VisibleChanged += control => nearPlaneValue.Value = _nearPlane; } - // Far plane + // Far Plane { var farPlane = cameraCM.AddButton("Far Plane"); farPlane.CloseMenuOnClick = false; @@ -774,14 +774,16 @@ namespace FlaxEditor.Viewport cameraCM.AddSeparator(); - // Reset button + // Reset Button { var reset = cameraCM.AddButton("Reset to default"); reset.ButtonClicked += button => { SetupViewportOptions(); - // trigger UI update + // if the context menu is opened without triggering the value changes beforehand, + // the movement speed will not be correctly reset to its default value in certain cases + // therefore, a UI update needs to be triggered here minCamSpeedValue.Value = _minMovementSpeed; camSpeedValue.Value = _movementSpeed; maxCamSpeedValue.Value = _maxMovementSpeed; @@ -815,7 +817,7 @@ namespace FlaxEditor.Viewport } } - // View flags + // View Flags { var viewFlags = ViewWidgetButtonMenu.AddChildMenu("View Flags").ContextMenu; viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = Editor.Instance.Icons.Rotate32; @@ -839,7 +841,7 @@ namespace FlaxEditor.Viewport viewFlags.VisibleChanged += WidgetViewFlagsShowHide; } - // Debug view + // Debug View { var debugView = ViewWidgetButtonMenu.AddChildMenu("Debug View").ContextMenu; for (int i = 0; i < EditorViewportViewModeValues.Length; i++) From dcec847d50d21b04344cd1caecf3096041a49250 Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Wed, 11 Oct 2023 20:01:56 +0200 Subject: [PATCH 023/546] - Basic constants can now be converted to parameters --- Source/Editor/Surface/Archetypes/Constants.cs | 82 +++++++++++++++++++ Source/Editor/Surface/SurfaceParameter.cs | 4 +- Source/Editor/Surface/VisjectSurfaceWindow.cs | 7 +- 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index cbe7822e3..e3cbd1b86 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -7,11 +7,13 @@ using Real = System.Single; #endif using System; +using System.Linq; using System.Reflection; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; +using FlaxEditor.Surface.Undo; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -347,6 +349,75 @@ namespace FlaxEditor.Surface.Archetypes } } + private class ConstantNode : SurfaceNode + { + private ScriptType _type; + + /// + public ConstantNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type) + : base(id, context, nodeArch, groupArch) + { + _type = type; + } + + /// + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) + { + base.OnShowSecondaryContextMenu(menu, location); + + menu.AddSeparator(); + menu.AddButton("Convert to Parameter", OnConvertToParameter); + } + + private void OnConvertToParameter() + { + if(Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + + Asset asset = Surface.Owner.SurfaceAsset; + if (asset == null || !asset.IsLoaded) + { + Editor.LogError("Asset is null or not loaded"); + return; + } + + var paramIndex = Surface.Parameters.Count; + var paramAction = new AddRemoveParamAction + { + Window = window, + IsAdd = true, + Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)), + Type = _type, + Index = paramIndex, + InitValue = Values[0], + }; + paramAction.Do(); + + var parameterGuid = Surface.Parameters[paramIndex].ID; + + bool undoEnabled = Surface.Undo.Enabled; + Surface.Undo.Enabled = false; + SurfaceNode node = Surface.Context.SpawnNode(6, 1, this.Location, new object[] {parameterGuid}); + Surface.Undo.Enabled = undoEnabled; + + if (node is not Parameters.SurfaceNodeParamsGet getNode) + throw new Exception("Node is not a ParamsGet node!"); + + var spawnNodeAction = new AddRemoveNodeAction(getNode, true); + var removeConstantAction = new AddRemoveNodeAction(this, false); + + Surface.AddBatchedUndoAction(new MultiUndoAction(paramAction, spawnNodeAction, removeConstantAction)); + removeConstantAction.Do(); + } + + private bool OnParameterRenameValidate(RenamePopup popup, string value) + { + if(Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); + } + } + /// /// The nodes for that group. /// @@ -356,6 +427,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Bool", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), Description = "Constant boolean value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -388,6 +460,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 2, Title = "Integer", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(int))), Description = "Constant integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -415,6 +488,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Float", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(float))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -442,6 +516,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Float2", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Float2))), Description = "Constant Float2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -472,6 +547,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 5, Title = "Float3", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), Description = "Constant Float3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -504,6 +580,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 6, Title = "Float4", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Float4))), Description = "Constant Float4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), @@ -538,6 +615,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Title = "Color", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Color))), Description = "RGBA color", Flags = NodeFlags.AllGraphs, Size = new Float2(70, 100), @@ -644,6 +722,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 12, Title = "Unsigned Integer", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(170, 20), @@ -700,6 +779,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 16, Title = "Vector2", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Vector2))), Description = "Constant Vector2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -720,6 +800,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 17, Title = "Vector3", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Vector3))), Description = "Constant Vector3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -742,6 +823,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 18, Title = "Vector4", + Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Vector4))), Description = "Constant Vector4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), diff --git a/Source/Editor/Surface/SurfaceParameter.cs b/Source/Editor/Surface/SurfaceParameter.cs index 6eafdc2aa..e74b44eb9 100644 --- a/Source/Editor/Surface/SurfaceParameter.cs +++ b/Source/Editor/Surface/SurfaceParameter.cs @@ -56,7 +56,7 @@ namespace FlaxEditor.Surface /// The type. /// The name. /// The created parameter. - public static SurfaceParameter Create(ScriptType type, string name) + public static SurfaceParameter Create(ScriptType type, string name, object initValue = null) { return new SurfaceParameter { @@ -64,7 +64,7 @@ namespace FlaxEditor.Surface IsPublic = true, Name = name, Type = type, - Value = TypeUtils.GetDefaultValue(type), + Value = initValue ?? TypeUtils.GetDefaultValue(type), }; } } diff --git a/Source/Editor/Surface/VisjectSurfaceWindow.cs b/Source/Editor/Surface/VisjectSurfaceWindow.cs index 305121e11..e1d39083c 100644 --- a/Source/Editor/Surface/VisjectSurfaceWindow.cs +++ b/Source/Editor/Surface/VisjectSurfaceWindow.cs @@ -278,6 +278,11 @@ namespace FlaxEditor.Surface ///
public ScriptType Type; + /// + /// The value to initialize the parameter with. (Can be null) + /// + public object InitValue; + /// public string ActionString => IsAdd ? "Add parameter" : "Remove parameter"; @@ -304,7 +309,7 @@ namespace FlaxEditor.Surface var type = Type; if (IsAdd && type.Type == typeof(NormalMap)) type = new ScriptType(typeof(Texture)); - var param = SurfaceParameter.Create(type, Name); + var param = SurfaceParameter.Create(type, Name, InitValue); if (IsAdd && Type.Type == typeof(NormalMap)) param.Value = FlaxEngine.Content.LoadAsyncInternal("Engine/Textures/NormalTexture"); Window.VisjectSurface.Parameters.Insert(Index, param); From 2b0db40c17ae69910c8c54faffb17c8ecaa7f971 Mon Sep 17 00:00:00 2001 From: Menotdan <32620310+Menotdan@users.noreply.github.com> Date: Thu, 12 Oct 2023 01:22:59 -0400 Subject: [PATCH 024/546] Add basic replacement functionality- replacing adds multiple copies on child actors, however. --- .../Dedicated/MissingScriptEditor.cs | 157 +++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index a6c4e6623..0af811074 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -1,6 +1,13 @@ -using FlaxEditor.CustomEditors.Editors; +using FlaxEditor.Actions; +using FlaxEditor.CustomEditors.Editors; +using FlaxEditor.CustomEditors.GUI; +using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; +using System; +using System.Collections.Generic; namespace FlaxEditor.CustomEditors.Dedicated; @@ -10,6 +17,11 @@ namespace FlaxEditor.CustomEditors.Dedicated; [CustomEditor(typeof(MissingScript)), DefaultEditor] public class MissingScriptEditor : GenericEditor { + DropPanel _dropPanel; + Button _replaceScriptButton; + CheckBox _shouldReplaceAllCheckbox; + CustomEditor _propertiesEditor; + /// public override void Initialize(LayoutElementsContainer layout) { @@ -18,9 +30,150 @@ public class MissingScriptEditor : GenericEditor base.Initialize(layout); return; } + _dropPanel = dropPanel; + _dropPanel.HeaderTextColor = Color.OrangeRed; - dropPanel.HeaderTextColor = Color.OrangeRed; + Panel replaceScriptPanel = new Panel + { + Parent = _dropPanel, + Height = 64, + }; + + _replaceScriptButton = new Button + { + Text = "Replace Script", + AnchorPreset = AnchorPresets.TopCenter, + Width = 240, + Height = 24, + X = -120, + Y = 0, + Parent = replaceScriptPanel, + }; + _replaceScriptButton.Clicked += OnReplaceScriptButtonClicked; + + Label replaceAllLabel = new Label + { + Text = "Replace All of Same Type", + AnchorPreset = AnchorPresets.BottomCenter, + Y = -34, + Parent = replaceScriptPanel, + }; + replaceAllLabel.X -= FlaxEngine.GUI.Style.Current.FontSmall.MeasureText(replaceAllLabel.Text).X; + + _shouldReplaceAllCheckbox = new CheckBox + { + TooltipText = "Wether or not to apply this script change to all scripts missing the same type.", + AnchorPreset = AnchorPresets.BottomCenter, + Y = -34, + Parent = replaceScriptPanel, + }; + + float centerDifference = (_shouldReplaceAllCheckbox.Right - replaceAllLabel.Left) / 2; + replaceAllLabel.X += centerDifference; + _shouldReplaceAllCheckbox.X += centerDifference; base.Initialize(layout); } + + private List FindActorsWithMatchingMissingScript(Actor parent = null) + { + List missingScripts = new List(); + if (parent != null) + { + for (int child = 0; child < parent.ChildrenCount; child++) + { + Actor actor = parent.Children[child]; + for (int scriptIndex = 0; scriptIndex < actor.ScriptsCount; scriptIndex++) + { + Script actorScript = actor.Scripts[scriptIndex]; + if (actorScript is not MissingScript missingActorScript) + { + continue; + } + + MissingScript currentMissing = Values[0] as MissingScript; + if (missingActorScript.MissingTypeName != currentMissing.MissingTypeName) + { + continue; + } + + Debug.Log("Found correct missing script."); + + // Matching MissingScript. + missingScripts.Add(missingActorScript); + } + + missingScripts.AddRange(FindActorsWithMatchingMissingScript(actor)); + } + } else + { + foreach (Actor actor in Level.GetActors()) + { + missingScripts.AddRange(FindActorsWithMatchingMissingScript(actor)); + } + } + + return missingScripts; + } + + private void ReplaceScript(ScriptType script, bool replaceAllInScene) + { + var actions = new List(4); + + List missingScripts = new List(); + if (!replaceAllInScene) + { + missingScripts.Add(Values[0] as MissingScript); + } else + { + missingScripts.AddRange(FindActorsWithMatchingMissingScript()); + } + + foreach (MissingScript missingScript in missingScripts) + { + actions.Add(AddRemoveScript.Add(missingScript.Actor, script)); + actions.Add(AddRemoveScript.Remove(missingScript)); + } + + if (actions.Count == 0) + { + Editor.LogWarning("Failed to replace scripts!"); + return; + } + + var multiAction = new MultiUndoAction(actions); + multiAction.Do(); + var presenter = ParentEditor.Presenter; + if (presenter != null) + { + presenter.Undo.AddAction(multiAction); + presenter.Control.Focus(); + } + } + + private void OnReplaceScriptButtonClicked() + { + var scripts = Editor.Instance.CodeEditing.Scripts.Get(); + if (scripts.Count == 0) + { + // No scripts + var cm1 = new ContextMenu(); + cm1.AddButton("No scripts in project"); + cm1.Show(_dropPanel, _replaceScriptButton.BottomLeft); + return; + } + + // Show context menu with list of scripts to add + var cm = new ItemsListContextMenu(180); + for (int i = 0; i < scripts.Count; i++) + { + cm.AddItem(new TypeSearchPopup.TypeItemView(scripts[i])); + } + // Get the parent (actor properties editor) of the parent (Scripts Editor) of our editor. + _propertiesEditor = ParentEditor.ParentEditor; + + cm.ItemClicked += item => ReplaceScript((ScriptType)item.Tag, _shouldReplaceAllCheckbox.Checked); + cm.SortItems(); + cm.Show(_dropPanel, _replaceScriptButton.BottomLeft - new Float2((cm.Width - _replaceScriptButton.Width) / 2, 0)); + } } From 7742f2d8d4b268537ce8a37698bcf23df4d7061a Mon Sep 17 00:00:00 2001 From: Menotdan <32620310+Menotdan@users.noreply.github.com> Date: Thu, 12 Oct 2023 01:26:25 -0400 Subject: [PATCH 025/546] Add debugging logs. --- Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index 0af811074..3f9c9cf76 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -97,7 +97,7 @@ public class MissingScriptEditor : GenericEditor continue; } - Debug.Log("Found correct missing script."); + Debug.Log($"Found correct missing script: {actor.Name}, {missingActorScript}, {missingScripts.Count}."); // Matching MissingScript. missingScripts.Add(missingActorScript); From 318f9e5b774618074984d1d8346c574c48bfce2c Mon Sep 17 00:00:00 2001 From: Menotdan <32620310+Menotdan@users.noreply.github.com> Date: Thu, 12 Oct 2023 01:33:22 -0400 Subject: [PATCH 026/546] Fix duplicated script replacement. --- .../Dedicated/MissingScriptEditor.cs | 43 +++++++------------ 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index 3f9c9cf76..13569a891 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -75,41 +75,28 @@ public class MissingScriptEditor : GenericEditor base.Initialize(layout); } - private List FindActorsWithMatchingMissingScript(Actor parent = null) + private List FindActorsWithMatchingMissingScript() { List missingScripts = new List(); - if (parent != null) + + foreach (Actor actor in Level.GetActors()) { - for (int child = 0; child < parent.ChildrenCount; child++) + for (int scriptIndex = 0; scriptIndex < actor.ScriptsCount; scriptIndex++) { - Actor actor = parent.Children[child]; - for (int scriptIndex = 0; scriptIndex < actor.ScriptsCount; scriptIndex++) + Script actorScript = actor.Scripts[scriptIndex]; + if (actorScript is not MissingScript missingActorScript) { - Script actorScript = actor.Scripts[scriptIndex]; - if (actorScript is not MissingScript missingActorScript) - { - continue; - } - - MissingScript currentMissing = Values[0] as MissingScript; - if (missingActorScript.MissingTypeName != currentMissing.MissingTypeName) - { - continue; - } - - Debug.Log($"Found correct missing script: {actor.Name}, {missingActorScript}, {missingScripts.Count}."); - - // Matching MissingScript. - missingScripts.Add(missingActorScript); + continue; } - missingScripts.AddRange(FindActorsWithMatchingMissingScript(actor)); - } - } else - { - foreach (Actor actor in Level.GetActors()) - { - missingScripts.AddRange(FindActorsWithMatchingMissingScript(actor)); + MissingScript currentMissing = Values[0] as MissingScript; + if (missingActorScript.MissingTypeName != currentMissing.MissingTypeName) + { + continue; + } + + // Matching MissingScript. + missingScripts.Add(missingActorScript); } } From a8260ed8b68ff5052972489b8046c4cf39ecc835 Mon Sep 17 00:00:00 2001 From: Menotdan <32620310+Menotdan@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:00:27 -0400 Subject: [PATCH 027/546] Fixed Serialization Transfer. --- .../Dedicated/MissingScriptEditor.cs | 56 +++++++++++++------ .../Undo/Actions/AddRemoveScriptAction.cs | 6 ++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index 13569a891..3f62efae3 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -103,25 +103,8 @@ public class MissingScriptEditor : GenericEditor return missingScripts; } - private void ReplaceScript(ScriptType script, bool replaceAllInScene) + private void RunReplacementMulticast(List actions) { - var actions = new List(4); - - List missingScripts = new List(); - if (!replaceAllInScene) - { - missingScripts.Add(Values[0] as MissingScript); - } else - { - missingScripts.AddRange(FindActorsWithMatchingMissingScript()); - } - - foreach (MissingScript missingScript in missingScripts) - { - actions.Add(AddRemoveScript.Add(missingScript.Actor, script)); - actions.Add(AddRemoveScript.Remove(missingScript)); - } - if (actions.Count == 0) { Editor.LogWarning("Failed to replace scripts!"); @@ -138,6 +121,43 @@ public class MissingScriptEditor : GenericEditor } } + private void ReplaceScript(ScriptType script, bool replaceAllInScene) + { + var actions = new List(4); + + List missingScripts = new List(); + if (!replaceAllInScene) + { + missingScripts.Add(Values[0] as MissingScript); + } else + { + missingScripts = FindActorsWithMatchingMissingScript(); + } + + foreach (MissingScript missingScript in missingScripts) + { + AddRemoveScript addReplacementScriptAction = AddRemoveScript.Add(missingScript.Actor, script); + actions.Add(addReplacementScriptAction); + } + RunReplacementMulticast(actions); + + for (int actionIdx = 0; actionIdx < actions.Count; actionIdx++) + { + AddRemoveScript addRemoveScriptAction = (AddRemoveScript) actions[actionIdx]; + int orderInParent = addRemoveScriptAction.GetOrderInParent(); + + Script newScript = missingScripts[actionIdx].Actor.Scripts[orderInParent]; + missingScripts[actionIdx].ReferenceScript = newScript; + } + actions.Clear(); + + foreach (MissingScript missingScript in missingScripts) + { + actions.Add(AddRemoveScript.Remove(missingScript)); + } + RunReplacementMulticast(actions); + } + private void OnReplaceScriptButtonClicked() { var scripts = Editor.Instance.CodeEditing.Scripts.Get(); diff --git a/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs b/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs index a5710494c..86afd4c0b 100644 --- a/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs +++ b/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs @@ -75,6 +75,11 @@ namespace FlaxEditor.Actions _enabled = true; } + public int GetOrderInParent() + { + return _orderInParent; + } + /// /// Creates a new added script undo action. /// @@ -184,6 +189,7 @@ namespace FlaxEditor.Actions script.Parent = parentActor; if (_orderInParent != -1) script.OrderInParent = _orderInParent; + _orderInParent = script.OrderInParent; // Ensure _orderInParent is correct for script that want to use it. if (_prefabObjectId != Guid.Empty) SceneObject.Internal_LinkPrefab(Object.GetUnmanagedPtr(script), ref _prefabId, ref _prefabObjectId); Editor.Instance.Scene.MarkSceneEdited(parentActor.Scene); From a7b2230ef4848e307f35772edef72a620301fdb4 Mon Sep 17 00:00:00 2001 From: Menotdan <32620310+Menotdan@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:23:18 -0400 Subject: [PATCH 028/546] Remove unneeded using --- Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index 3f62efae3..8c8c76561 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -1,12 +1,10 @@ using FlaxEditor.Actions; using FlaxEditor.CustomEditors.Editors; -using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; -using System; using System.Collections.Generic; namespace FlaxEditor.CustomEditors.Dedicated; From 13686ae13f2dce7280f5092476c9c6a1e1366544 Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Fri, 13 Oct 2023 00:20:34 +0200 Subject: [PATCH 029/546] Fix 436 and 1251 --- Source/Editor/Gizmo/IGizmoOwner.cs | 5 +++++ Source/Editor/Gizmo/TransformGizmoBase.cs | 21 +++++++++++++++---- Source/Editor/Viewport/EditorGizmoViewport.cs | 2 ++ .../Editor/Viewport/PrefabWindowViewport.cs | 2 ++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Gizmo/IGizmoOwner.cs b/Source/Editor/Gizmo/IGizmoOwner.cs index a28810179..ff653196e 100644 --- a/Source/Editor/Gizmo/IGizmoOwner.cs +++ b/Source/Editor/Gizmo/IGizmoOwner.cs @@ -11,6 +11,11 @@ namespace FlaxEditor.Gizmo [HideInEditor] public interface IGizmoOwner { + /// + /// Gets the gizmos collection. + /// + FlaxEditor.Viewport.EditorViewport Viewport { get; } + /// /// Gets the gizmos collection. /// diff --git a/Source/Editor/Gizmo/TransformGizmoBase.cs b/Source/Editor/Gizmo/TransformGizmoBase.cs index dd5cc6c76..6123d348a 100644 --- a/Source/Editor/Gizmo/TransformGizmoBase.cs +++ b/Source/Editor/Gizmo/TransformGizmoBase.cs @@ -162,10 +162,23 @@ namespace FlaxEditor.Gizmo // Scale gizmo to fit on-screen Vector3 position = Position; - Vector3 vLength = Owner.ViewPosition - position; - float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize; - _screenScale = (float)(vLength.Length / GizmoScaleFactor * gizmoSize); - + if (Owner.Viewport.UseOrthographicProjection) + { + //[hack] this is far form ideal the View Position is in wrong location, any think using the View Position will have problem + //the camera system needs rewrite the to be a camera on springarm, similar how the ArcBallCamera is handled + //the ortho projection cannot exist with fps camera because there is no + // - focus point to calculate correct View Position with Orthographic Scale as a reference and Orthographic Scale from View Position + // with make the camera jump + // - and deaph so w and s movment in orto mode moves the cliping plane now + float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize; + _screenScale = gizmoSize * (50 * Owner.Viewport.OrthographicScale); + } + else + { + Vector3 vLength = Owner.ViewPosition - position; + float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize; + _screenScale = (float)(vLength.Length / GizmoScaleFactor * gizmoSize); + } // Setup world Quaternion orientation = GetSelectedObject(0).Orientation; _gizmoWorld = new Transform(position, orientation, new Float3(_screenScale)); diff --git a/Source/Editor/Viewport/EditorGizmoViewport.cs b/Source/Editor/Viewport/EditorGizmoViewport.cs index 9a4fd34a2..a24dd2f38 100644 --- a/Source/Editor/Viewport/EditorGizmoViewport.cs +++ b/Source/Editor/Viewport/EditorGizmoViewport.cs @@ -41,6 +41,8 @@ namespace FlaxEditor.Viewport Gizmos[i].Update(deltaTime); } } + /// + public EditorViewport Viewport => this; /// public GizmosCollection Gizmos { get; } diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 1dc459135..eb462deb1 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -306,6 +306,8 @@ namespace FlaxEditor.Viewport var orient = ViewOrientation; ((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents, ref orient); } + /// + public EditorViewport Viewport => this; /// public GizmosCollection Gizmos { get; } From 7014007362d4d4f3435c8abdd63aca113410e9ca Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sat, 14 Oct 2023 01:45:36 +0300 Subject: [PATCH 030/546] Disable inheriting C# optimization level from dependency modules --- .../Flax.Build/Build/DotNet/Builder.DotNet.cs | 16 +++++++++------- .../Flax.Build/Build/NativeCpp/BuildOptions.cs | 10 ++-------- .../Build/NativeCpp/Builder.NativeCpp.cs | 4 ++-- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs index c64ee38b3..6e9fbea83 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs @@ -128,7 +128,7 @@ namespace Flax.Build sourceFiles.Sort(); // Build assembly - BuildDotNet(graph, buildData, targetBuildOptions, target.OutputName, sourceFiles); + BuildDotNet(graph, buildData, targetBuildOptions, target.OutputName, sourceFiles, optimizeAssembly: buildData.TargetOptions.ScriptingAPI.Optimization); } // Deploy files @@ -152,7 +152,7 @@ namespace Flax.Build } } - private static void BuildDotNet(TaskGraph graph, BuildData buildData, BuildOptions buildOptions, string name, List sourceFiles, HashSet fileReferences = null, IGrouping binaryModule = null) + private static void BuildDotNet(TaskGraph graph, BuildData buildData, BuildOptions buildOptions, string name, List sourceFiles, HashSet fileReferences = null, IGrouping binaryModule = null, bool? optimizeAssembly = null) { // Setup build options var buildPlatform = Platform.BuildTargetPlatform; @@ -257,10 +257,8 @@ namespace Flax.Build if (buildOptions.ScriptingAPI.IgnoreMissingDocumentationWarnings) args.Add("-nowarn:1591"); - // Optimizations prevent debugging, only enable in release builds - var optimize = buildData.Configuration == TargetConfiguration.Release; - if (buildData.TargetOptions.ScriptingAPI.Optimization.HasValue) - optimize = buildData.TargetOptions.ScriptingAPI.Optimization.Value; + // Optimizations prevent debugging, only enable in release builds by default + var optimize = optimizeAssembly.HasValue ? optimizeAssembly.Value : buildData.Configuration == TargetConfiguration.Release; args.Add(optimize ? "/optimize+" : "/optimize-"); #if !USE_NETCORE args.Add(string.Format("/reference:\"{0}mscorlib.dll\"", referenceAssemblies)); @@ -374,11 +372,15 @@ namespace Flax.Build // Get references fileReferences.Clear(); + bool? optimizeAssembly = null; foreach (var module in binaryModule) { if (!buildData.Modules.TryGetValue(module, out var moduleBuildOptions)) continue; + if (moduleBuildOptions.ScriptingAPI.Optimization.HasValue) + optimizeAssembly |= moduleBuildOptions.ScriptingAPI.Optimization; + // Find references based on the modules dependencies foreach (var dependencyName in moduleBuildOptions.PublicDependencies.Concat(moduleBuildOptions.PrivateDependencies)) { @@ -408,7 +410,7 @@ namespace Flax.Build } // Build assembly - BuildDotNet(graph, buildData, buildOptions, binaryModuleName + ".CSharp", sourceFiles, fileReferences, binaryModule); + BuildDotNet(graph, buildData, buildOptions, binaryModuleName + ".CSharp", sourceFiles, fileReferences, binaryModule, optimizeAssembly); } } } diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs index 72cefbfca..e161024b4 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs @@ -225,7 +225,7 @@ namespace Flax.Build.NativeCpp public CSharpNullableReferences CSharpNullableReferences = CSharpNullableReferences.Disable; /// - /// Enable code optimization. + /// Enable code optimizations for the managed module assembly. /// public bool? Optimization; @@ -237,19 +237,13 @@ namespace Flax.Build.NativeCpp /// Adds the other options into this. ///
/// The other. - public void Add(ScriptingAPIOptions other, bool addBuildOptions = true) + public void Add(ScriptingAPIOptions other) { Defines.AddRange(other.Defines); SystemReferences.AddRange(other.SystemReferences); FileReferences.AddRange(other.FileReferences); Analyzers.AddRange(other.Analyzers); IgnoreMissingDocumentationWarnings |= other.IgnoreMissingDocumentationWarnings; - - if (addBuildOptions) - { - if (other.Optimization.HasValue) - Optimization |= other.Optimization; - } } } diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs index 1e8550c4e..225e46ba7 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs @@ -403,7 +403,7 @@ namespace Flax.Build moduleOptions.PrivateIncludePaths.AddRange(dependencyOptions.PublicIncludePaths); moduleOptions.Libraries.AddRange(dependencyOptions.Libraries); moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries); - moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI, false); + moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI); moduleOptions.ExternalModules.AddRange(dependencyOptions.ExternalModules); } } @@ -418,7 +418,7 @@ namespace Flax.Build moduleOptions.PublicIncludePaths.AddRange(dependencyOptions.PublicIncludePaths); moduleOptions.Libraries.AddRange(dependencyOptions.Libraries); moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries); - moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI, false); + moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI); moduleOptions.ExternalModules.AddRange(dependencyOptions.ExternalModules); } } From 8548df4a1bcfb3096bd5e010d91242f352e4b5a9 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sat, 14 Oct 2023 02:34:15 +0300 Subject: [PATCH 031/546] Fix crash when physics actor contact is lost with another actor --- .../Physics/PhysX/SimulationEventCallbackPhysX.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp index 7c9355099..684aeca19 100644 --- a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp +++ b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp @@ -132,7 +132,8 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c const PxReal* impulses = pair.contactImpulses; //const PxU32 flippedContacts = (pair.flags & PxContactPairFlag::eINTERNAL_CONTACTS_ARE_FLIPPED); - const PxU32 hasImpulses = (pair.flags & PxContactPairFlag::eINTERNAL_HAS_IMPULSES); + const bool hasImpulses = pair.flags.isSet(PxContactPairFlag::eINTERNAL_HAS_IMPULSES); + const bool hasPostVelocities = !pair.flags.isSet(PxContactPairFlag::eACTOR_PAIR_LOST_TOUCH); PxU32 nbContacts = 0; PxVec3 totalImpulse(0.0f); @@ -166,7 +167,8 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c } // Extract velocities - if (j.nextItemSet()) + c.ThisVelocity = c.OtherVelocity = Vector3::Zero; + if (hasPostVelocities && j.nextItemSet()) { ASSERT(j.contactPairIndex == pairIndex); if (j.postSolverVelocity) @@ -177,10 +179,6 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c c.ThisVelocity = P2C(linearVelocityActor0); c.OtherVelocity = P2C(linearVelocityActor1); } - else - { - c.ThisVelocity = c.OtherVelocity = Vector3::Zero; - } } c.ContactsCount = nbContacts; @@ -195,6 +193,7 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c RemovedCollisions.Add(c); } } + ASSERT(!j.nextItemSet()); } void SimulationEventCallback::onTrigger(PxTriggerPair* pairs, PxU32 count) From dc1be86db7a5b2abc256bf3f85cb970cc8efbc00 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sat, 14 Oct 2023 03:45:13 +0300 Subject: [PATCH 032/546] Only generate one set of C++/C# launch tasks for VSCode Proper fix for b9c6dcd4c7453f2ea2bbc5cfeae613a08c5409a4 --- .../VisualStudioCodeProjectGenerator.cs | 200 +++++++++--------- 1 file changed, 96 insertions(+), 104 deletions(-) diff --git a/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs index 76ec9d22c..a044adc0a 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs @@ -166,7 +166,7 @@ namespace Flax.Build.Projects.VisualStudioCode foreach (var configuration in project.Configurations) { var target = configuration.Target; - var name = project.Name + '|' + configuration.Name; + var name = solution.Name + '|' + configuration.Name; json.BeginObject(); @@ -294,132 +294,121 @@ namespace Flax.Build.Projects.VisualStudioCode json.BeginArray("configurations"); { - foreach (var project in solution.Projects) + var cppProject = solution.Projects.FirstOrDefault(x => x.BaseName == solution.Name || x.Name == solution.Name); + var csharpProject = solution.Projects.FirstOrDefault(x => x.BaseName == solution.MainProject.Targets[0].Modules[0] || x.Name == solution.MainProject.Targets[0].Modules[0]); + + if (cppProject != null) { - if (project.Name == "BuildScripts") - continue; // C++ project - if (project.Type == TargetType.NativeCpp) + foreach (var configuration in cppProject.Configurations) { - // Skip generating launch profiles for plugins and dependencies - if (solution.MainProject.Name != "Flax" && project.Name != "Flax.Build" && solution.MainProject.WorkspaceRootPath != project.WorkspaceRootPath) - continue; + var name = solution.Name + '|' + configuration.Name + " (C++)"; + var target = configuration.Target; + var outputType = cppProject.OutputType ?? target.OutputType; + var outputTargetFilePath = target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); - foreach (var configuration in project.Configurations) + json.BeginObject(); { - var name = project.Name + '|' + configuration.Name; - var target = configuration.Target; + if (configuration.Platform == TargetPlatform.Windows) + json.AddField("type", "cppvsdbg"); + else + json.AddField("type", "cppdbg"); + json.AddField("name", name); + json.AddField("request", "launch"); + json.AddField("preLaunchTask", solution.Name + '|' + configuration.Name); + json.AddField("cwd", buildToolWorkspace); - var outputType = project.OutputType ?? target.OutputType; - var outputTargetFilePath = target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); + WriteNativePlatformLaunchSettings(json, configuration.Platform); - json.BeginObject(); + if (outputType != TargetOutputType.Executable && configuration.Name.StartsWith("Editor.")) { if (configuration.Platform == TargetPlatform.Windows) - json.AddField("type", "cppvsdbg"); - else - json.AddField("type", "cppdbg"); - json.AddField("name", name); - json.AddField("request", "launch"); - json.AddField("preLaunchTask", name); - json.AddField("cwd", buildToolWorkspace); - - WriteNativePlatformLaunchSettings(json, configuration.Platform); - - if (outputType != TargetOutputType.Executable && configuration.Name.StartsWith("Editor.")) { - if (configuration.Platform == TargetPlatform.Windows) - { - var editorFolder = configuration.Architecture == TargetArchitecture.x64 ? "Win64" : "Win32"; - json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", editorFolder, configuration.ConfigurationName, "FlaxEditor.exe")); - } - else if (configuration.Platform == TargetPlatform.Linux) - json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", "Linux", configuration.ConfigurationName, "FlaxEditor")); - else if (configuration.Platform == TargetPlatform.Mac) - json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", "Mac", configuration.ConfigurationName, "FlaxEditor")); - - json.BeginArray("args"); - { - json.AddUnnamedField("-project"); - json.AddUnnamedField(buildToolWorkspace); - json.AddUnnamedField("-skipCompile"); - if (hasMonoProjects) - { - json.AddUnnamedField("-debug"); - json.AddUnnamedField("127.0.0.1:55555"); - } - } - json.EndArray(); + var editorFolder = configuration.Architecture == TargetArchitecture.x64 ? "Win64" : "Win32"; + json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", editorFolder, configuration.ConfigurationName, "FlaxEditor.exe")); } - else + else if (configuration.Platform == TargetPlatform.Linux) + json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", "Linux", configuration.ConfigurationName, "FlaxEditor")); + else if (configuration.Platform == TargetPlatform.Mac) + json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", "Mac", configuration.ConfigurationName, "FlaxEditor")); + + json.BeginArray("args"); { - json.AddField("program", outputTargetFilePath); - json.BeginArray("args"); + json.AddUnnamedField("-project"); + json.AddUnnamedField(buildToolWorkspace); + json.AddUnnamedField("-skipCompile"); + if (hasMonoProjects) { - if (configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac) - json.AddUnnamedField("--std"); - if (hasMonoProjects) - { - json.AddUnnamedField("-debug"); - json.AddUnnamedField("127.0.0.1:55555"); - } + json.AddUnnamedField("-debug"); + json.AddUnnamedField("127.0.0.1:55555"); } - json.EndArray(); } - - json.AddField("visualizerFile", Path.Combine(Globals.EngineRoot, "Source", "flax.natvis")); + json.EndArray(); } - json.EndObject(); - } - } - // C# project - else if (project.Type == TargetType.DotNetCore) - { - // Skip generating launch profiles for plugins and dependencies - if (solution.MainProject.WorkspaceRootPath != project.WorkspaceRootPath) - continue; - - foreach (var configuration in project.Configurations) - { - var name = project.Name + '|' + configuration.Name + " (C#)"; - var outputTargetFilePath = configuration.Target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); - - json.BeginObject(); + else { - json.AddField("type", "coreclr"); - json.AddField("name", name); - json.AddField("request", "launch"); - json.AddField("preLaunchTask", project.Name + '|' + configuration.Name); - json.AddField("cwd", buildToolWorkspace); - if (configuration.Platform == Platform.BuildPlatform.Target) + json.AddField("program", outputTargetFilePath); + json.BeginArray("args"); { - var editorFolder = configuration.Platform == TargetPlatform.Windows ? (configuration.Architecture == TargetArchitecture.x64 ? "Win64" : "Win32") : configuration.Platform.ToString(); - json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", editorFolder, configuration.ConfigurationName, "FlaxEditor")); - json.BeginArray("args"); + if (configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac) + json.AddUnnamedField("--std"); + if (hasMonoProjects) { - json.AddUnnamedField("-project"); - json.AddUnnamedField(buildToolWorkspace); - json.AddUnnamedField("-skipCompile"); + json.AddUnnamedField("-debug"); + json.AddUnnamedField("127.0.0.1:55555"); } - json.EndArray(); - } - else - { - json.AddField("program", configuration.Target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable)); - } - - switch (configuration.Platform) - { - case TargetPlatform.Windows: - json.AddField("stopAtEntry", false); - json.AddField("externalConsole", true); - break; - case TargetPlatform.Linux: - break; } + json.EndArray(); } - json.EndObject(); + + json.AddField("visualizerFile", Path.Combine(Globals.EngineRoot, "Source", "flax.natvis")); } + json.EndObject(); + } + } + if (csharpProject != null) + { + // C# project + foreach (var configuration in csharpProject.Configurations) + { + var name = solution.Name + '|' + configuration.Name + " (C#)"; + var outputTargetFilePath = configuration.Target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); + + json.BeginObject(); + { + json.AddField("type", "coreclr"); + json.AddField("name", name); + json.AddField("request", "launch"); + json.AddField("preLaunchTask", solution.Name + '|' + configuration.Name); + json.AddField("cwd", buildToolWorkspace); + if (configuration.Platform == Platform.BuildPlatform.Target) + { + var editorFolder = configuration.Platform == TargetPlatform.Windows ? (configuration.Architecture == TargetArchitecture.x64 ? "Win64" : "Win32") : configuration.Platform.ToString(); + json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", editorFolder, configuration.ConfigurationName, "FlaxEditor")); + json.BeginArray("args"); + { + json.AddUnnamedField("-project"); + json.AddUnnamedField(buildToolWorkspace); + json.AddUnnamedField("-skipCompile"); + } + json.EndArray(); + } + else + { + json.AddField("program", configuration.Target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable)); + } + + switch (configuration.Platform) + { + case TargetPlatform.Windows: + json.AddField("stopAtEntry", false); + json.AddField("externalConsole", true); + break; + case TargetPlatform.Linux: + break; + } + } + json.EndObject(); } } } @@ -428,6 +417,9 @@ namespace Flax.Build.Projects.VisualStudioCode { foreach (var platform in solution.Projects.SelectMany(x => x.Configurations).Select(x => x.Platform).Distinct()) { + if (platform != TargetPlatform.Windows && platform != TargetPlatform.Linux && platform != TargetPlatform.Mac) + continue; + json.BeginObject(); { if (platform == TargetPlatform.Windows) From 7b984396cc9c51448a9f7eceabb8ec66b001dde1 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 13 Oct 2023 20:52:26 -0500 Subject: [PATCH 033/546] Add not to close Debug View CM on button clicked. --- Source/Editor/Viewport/EditorViewport.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index e20069120..9a444f2da 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -593,12 +593,14 @@ namespace FlaxEditor.Viewport { ref var vv = ref v.Options[j]; var button = childMenu.AddButton(vv.Name); + button.CloseMenuOnClick = false; button.Tag = vv.Mode; } } else { var button = debugView.AddButton(v.Name); + button.CloseMenuOnClick = false; button.Tag = v.Mode; } } @@ -1587,7 +1589,14 @@ namespace FlaxEditor.Viewport private void WidgetViewModeShowHideClicked(ContextMenuButton button) { if (button.Tag is ViewMode v) + { Task.ViewMode = v; + var cm = button.ParentContextMenu; + WidgetViewModeShowHide(cm); + var mainCM = ViewWidgetButtonMenu.GetChildMenu("Debug View").ContextMenu; + if (mainCM != null && cm != mainCM) + WidgetViewModeShowHide(mainCM); + } } private void WidgetViewModeShowHide(Control cm) @@ -1599,7 +1608,7 @@ namespace FlaxEditor.Viewport foreach (var e in ccm.Items) { if (e is ContextMenuButton b && b.Tag is ViewMode v) - b.Icon = Task.View.Mode == v ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + b.Icon = Task.ViewMode == v ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; } } From 7904690ecf5f5b8dd59014f656f48942c10460ed Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Oct 2023 14:00:58 +0200 Subject: [PATCH 034/546] Cleanuo some old code style --- Source/Engine/Content/Content.cpp | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index c4ce37dcb..236c9400b 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -697,13 +697,11 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat LOG(Warning, "Cannot copy file to destination."); return true; } - if (JsonStorageProxy::ChangeId(dstPath, dstId)) { LOG(Warning, "Cannot change asset ID."); return true; } - return false; } @@ -768,12 +766,9 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat FileSystem::DeleteFile(tmpPath); // Reload storage + if (auto storage = ContentStorageManager::GetStorage(dstPath)) { - auto storage = ContentStorageManager::GetStorage(dstPath); - if (storage) - { - storage->Reload(); - } + storage->Reload(); } } @@ -911,12 +906,8 @@ bool Content::IsAssetTypeIdInvalid(const ScriptingTypeHandle& type, const Script Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) { - // Early out if (!id.IsValid()) - { - // Back return nullptr; - } // Check if asset has been already loaded Asset* result = GetAsset(id); @@ -928,7 +919,6 @@ Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) LOG(Warning, "Different loaded asset type! Asset: \'{0}\'. Expected type: {1}", result->ToString(), type.ToString()); return nullptr; } - return result; } @@ -946,12 +936,8 @@ Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) LoadCallAssetsLocker.Lock(); const bool contains = LoadCallAssets.Contains(id); LoadCallAssetsLocker.Unlock(); - if (!contains) - { return GetAsset(id); - } - Platform::Sleep(1); } } @@ -959,7 +945,6 @@ Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) { // Mark asset as loading LoadCallAssets.Add(id); - LoadCallAssetsLocker.Unlock(); } From dca6baee49e70ce4f215023fd68a4a76b09965a3 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sat, 14 Oct 2023 16:44:21 +0300 Subject: [PATCH 035/546] Use and store restored window position after minimizing the window --- Source/Editor/Modules/WindowsModule.cs | 10 ++----- .../Engine/Platform/Windows/WindowsWindow.cpp | 28 +++++++++++++++++++ .../Engine/Platform/Windows/WindowsWindow.h | 1 + 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 826d57285..1a4f5ce9e 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -472,19 +472,13 @@ namespace FlaxEditor.Modules { writer.WriteStartElement("Bounds"); { - var isMaximized = win.IsMaximized; - var isMinimized = win.IsMinimized; - if (isMinimized) - win.Restore(); // Restore window back to desktop to get proper client bounds var bounds = win.ClientBounds; - if (isMinimized) - win.Minimize(); writer.WriteAttributeString("X", bounds.X.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("Y", bounds.Y.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("Width", bounds.Width.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString("Height", bounds.Height.ToString(CultureInfo.InvariantCulture)); - writer.WriteAttributeString("IsMaximized", isMaximized.ToString()); - writer.WriteAttributeString("IsMinimized", isMinimized.ToString()); + writer.WriteAttributeString("IsMaximized", win.IsMaximized.ToString()); + writer.WriteAttributeString("IsMinimized", win.IsMinimized.ToString()); } writer.WriteEndElement(); } diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp index fb9fbe09d..ff65e9864 100644 --- a/Source/Engine/Platform/Windows/WindowsWindow.cpp +++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp @@ -532,6 +532,12 @@ Float2 WindowsWindow::ClientToScreen(const Float2& clientPos) const { ASSERT(HasHWND()); + if (_minimized) + { + // Return cached position when window is not on screen + return _minimizedScreenPosition + clientPos; + } + POINT p; p.x = static_cast(clientPos.X); p.y = static_cast(clientPos.Y); @@ -1109,6 +1115,28 @@ LRESULT WindowsWindow::WndProc(UINT msg, WPARAM wParam, LPARAM lParam) { if (SIZE_MINIMIZED == wParam) { + // Get the minimized window position in workspace coordinates + WINDOWPLACEMENT placement; + placement.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(_handle, &placement); + + // Calculate client offsets from window borders and title bar + RECT winRect = { 0, 0, static_cast(_clientSize.X), static_cast(_clientSize.Y) }; + LONG style = GetWindowLong(_handle, GWL_STYLE); + LONG exStyle = GetWindowLong(_handle, GWL_EXSTYLE); + AdjustWindowRectEx(&winRect, style, FALSE, exStyle); + + // Calculate monitor offsets from taskbar position + const HMONITOR monitor = MonitorFromWindow(_handle, MONITOR_DEFAULTTONEAREST); + MONITORINFO monitorInfo; + monitorInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfoW(monitor, &monitorInfo); + + // Convert the workspace coordinates to screen space and store it + _minimizedScreenPosition = Float2( + static_cast(placement.rcNormalPosition.left + monitorInfo.rcWork.left - monitorInfo.rcMonitor.left - winRect.left), + static_cast(placement.rcNormalPosition.top + monitorInfo.rcWork.top - monitorInfo.rcMonitor.top - winRect.top)); + _minimized = true; _maximized = false; } diff --git a/Source/Engine/Platform/Windows/WindowsWindow.h b/Source/Engine/Platform/Windows/WindowsWindow.h index e15f86a45..186793b1e 100644 --- a/Source/Engine/Platform/Windows/WindowsWindow.h +++ b/Source/Engine/Platform/Windows/WindowsWindow.h @@ -32,6 +32,7 @@ private: Windows::HANDLE _monitor = nullptr; Windows::LONG _clipCursorRect[4]; int32 _regionWidth = 0, _regionHeight = 0; + Float2 _minimizedScreenPosition = Float2::Zero; public: From e4b89e1badbcdd4bfa69b5d72650ba10f2f425b8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Oct 2023 16:20:27 +0200 Subject: [PATCH 036/546] Fix plugins initialization order to properly unload scenes before them in cooked game --- Source/Engine/Engine/Screen.cpp | 2 +- Source/Engine/Level/Level.cpp | 2 +- Source/Engine/Level/Prefabs/PrefabManager.cpp | 2 +- Source/Engine/Online/Online.cpp | 2 +- Source/Engine/Renderer/ProbesRenderer.cpp | 2 +- Source/Engine/Scripting/Plugins/PluginManager.cpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Engine/Screen.cpp b/Source/Engine/Engine/Screen.cpp index fc7e8a022..ede49cf92 100644 --- a/Source/Engine/Engine/Screen.cpp +++ b/Source/Engine/Engine/Screen.cpp @@ -22,7 +22,7 @@ class ScreenService : public EngineService { public: ScreenService() - : EngineService(TEXT("Screen"), 120) + : EngineService(TEXT("Screen"), 500) { } diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index df65fec3e..dfd5786c6 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -132,7 +132,7 @@ class LevelService : public EngineService { public: LevelService() - : EngineService(TEXT("Scene Manager"), 30) + : EngineService(TEXT("Scene Manager"), 200) { } diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index 123013064..cd9d893ec 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -29,7 +29,7 @@ class PrefabManagerService : public EngineService { public: PrefabManagerService() - : EngineService(TEXT("Prefab Manager"), 110) + : EngineService(TEXT("Prefab Manager")) { } }; diff --git a/Source/Engine/Online/Online.cpp b/Source/Engine/Online/Online.cpp index 313497f98..bb79c6d60 100644 --- a/Source/Engine/Online/Online.cpp +++ b/Source/Engine/Online/Online.cpp @@ -10,7 +10,7 @@ class OnlineService : public EngineService { public: OnlineService() - : EngineService(TEXT("Online"), 100) + : EngineService(TEXT("Online"), 500) { } diff --git a/Source/Engine/Renderer/ProbesRenderer.cpp b/Source/Engine/Renderer/ProbesRenderer.cpp index e24022b07..a965dee09 100644 --- a/Source/Engine/Renderer/ProbesRenderer.cpp +++ b/Source/Engine/Renderer/ProbesRenderer.cpp @@ -105,7 +105,7 @@ class ProbesRendererService : public EngineService { public: ProbesRendererService() - : EngineService(TEXT("Probes Renderer"), 70) + : EngineService(TEXT("Probes Renderer"), 500) { } diff --git a/Source/Engine/Scripting/Plugins/PluginManager.cpp b/Source/Engine/Scripting/Plugins/PluginManager.cpp index 328afa69c..a8198474c 100644 --- a/Source/Engine/Scripting/Plugins/PluginManager.cpp +++ b/Source/Engine/Scripting/Plugins/PluginManager.cpp @@ -90,7 +90,7 @@ class PluginManagerService : public EngineService { public: PluginManagerService() - : EngineService(TEXT("Plugin Manager"), 130) + : EngineService(TEXT("Plugin Manager"), 100) { } From 060bff1bdb50780da099bd7fc1954a72dc7f72f4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Oct 2023 16:21:10 +0200 Subject: [PATCH 037/546] Fix leftover separator in context menu in Content Window in some locations --- Source/Editor/Windows/ContentWindow.ContextMenu.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 03873df57..56136cfbf 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -2,6 +2,7 @@ using System; using System.IO; +using System.Linq; using FlaxEditor.Content; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; @@ -249,6 +250,10 @@ namespace FlaxEditor.Windows }); } + // Remove any leftover separator + if (cm.ItemsContainer.Children.LastOrDefault() is ContextMenuSeparator) + cm.ItemsContainer.Children.Last().Dispose(); + // Show it cm.Show(this, location); } From a5f00ed624af7ab22f900d6b316549fed2f7c8bd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Oct 2023 16:21:42 +0200 Subject: [PATCH 038/546] Fix `NavCrowd` to properly wait for navmesh loaded --- Source/Engine/Navigation/NavCrowd.cpp | 51 +++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Navigation/NavCrowd.cpp b/Source/Engine/Navigation/NavCrowd.cpp index 80055c3a4..b05f0f7d3 100644 --- a/Source/Engine/Navigation/NavCrowd.cpp +++ b/Source/Engine/Navigation/NavCrowd.cpp @@ -3,6 +3,9 @@ #include "NavCrowd.h" #include "NavMesh.h" #include "NavMeshRuntime.h" +#include "Engine/Core/Log.h" +#include "Engine/Level/Level.h" +#include "Engine/Level/Scene/Scene.h" #include "Engine/Profiler/ProfilerCPU.h" #include @@ -26,6 +29,15 @@ bool NavCrowd::Init(float maxAgentRadius, int32 maxAgents, NavMesh* navMesh) bool NavCrowd::Init(const NavAgentProperties& agentProperties, int32 maxAgents) { NavMeshRuntime* navMeshRuntime = NavMeshRuntime::Get(agentProperties); +#if !BUILD_RELEASE + if (!navMeshRuntime) + { + if (NavMeshRuntime::Get()) + LOG(Error, "Cannot create crowd. Failed to find a navmesh that matches a given agent properties."); + else + LOG(Error, "Cannot create crowd. No navmesh is loaded."); + } +#endif return Init(agentProperties.Radius * 3.0f, maxAgents, navMeshRuntime); } @@ -33,6 +45,41 @@ bool NavCrowd::Init(float maxAgentRadius, int32 maxAgents, NavMeshRuntime* navMe { if (!_crowd || !navMesh) return true; + + // This can happen on game start when no navmesh is loaded yet (eg. navmesh tiles data is during streaming) so wait for navmesh + if (navMesh->GetNavMesh() == nullptr) + { + PROFILE_CPU_NAMED("WaitForNavMesh"); + if (IsInMainThread()) + { + // Wait for any navmesh data load + for (const Scene* scene : Level::Scenes) + { + for (const NavMesh* actor : scene->Navigation.Meshes) + { + if (actor->DataAsset) + { + actor->DataAsset->WaitForLoaded(); + if (navMesh->GetNavMesh()) + break; + } + } + if (navMesh->GetNavMesh()) + break; + } + } + else + { + while (navMesh->GetNavMesh() == nullptr) + Platform::Sleep(1); + } + if (navMesh->GetNavMesh() == nullptr) + { + LOG(Error, "Cannot create crowd. Navmesh is not yet laoded."); + return true; + } + } + return !_crowd->init(maxAgents, maxAgentRadius, navMesh->GetNavMesh()); } @@ -48,7 +95,7 @@ Vector3 NavCrowd::GetAgentPosition(int32 id) const { Vector3 result = Vector3::Zero; const dtCrowdAgent* agent = _crowd->getAgent(id); - if (agent && agent->state != DT_CROWDAGENT_STATE_INVALID) + if (agent) { result = Float3(agent->npos); } @@ -59,7 +106,7 @@ Vector3 NavCrowd::GetAgentVelocity(int32 id) const { Vector3 result = Vector3::Zero; const dtCrowdAgent* agent = _crowd->getAgent(id); - if (agent && agent->state != DT_CROWDAGENT_STATE_INVALID) + if (agent) { result = Float3(agent->vel); } From 0c23b764568abd4994260a34bd655c23de026d67 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sat, 14 Oct 2023 17:28:21 +0300 Subject: [PATCH 039/546] Fix error when deleting multiple files and no folders selected --- Source/Editor/Windows/ContentWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index c287900e5..d43174cf3 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -631,7 +631,7 @@ namespace FlaxEditor.Windows // Sort items to remove files first, then folders var toDelete = new List(items); - toDelete.Sort((a, b) => a.IsFolder ? 1 : -1); + toDelete.Sort((a, b) => a.IsFolder ? 1 : b.IsFolder ? -1 : a.Compare(b)); string msg = toDelete.Count == 1 ? string.Format("Are you sure to delete \'{0}\'?\nThis action cannot be undone. Files will be deleted permanently.", items[0].Path) From a3d44e0feed5db2f7e47ae5fba9b987dea388f91 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sat, 14 Oct 2023 17:18:34 +0300 Subject: [PATCH 040/546] Prevent project file regeneration running while compiling scripts --- Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs | 2 +- Source/Editor/Scripting/ScriptsBuilder.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs index e9ff4c5b5..c40658fc8 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs @@ -410,7 +410,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing base.OnUpdate(); // Automatic project files generation after workspace modifications - if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty) + if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty && !ScriptsBuilder.IsCompiling) { Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync(); } diff --git a/Source/Editor/Scripting/ScriptsBuilder.cpp b/Source/Editor/Scripting/ScriptsBuilder.cpp index 8807379cf..ef3f3738b 100644 --- a/Source/Editor/Scripting/ScriptsBuilder.cpp +++ b/Source/Editor/Scripting/ScriptsBuilder.cpp @@ -266,7 +266,7 @@ bool ScriptsBuilder::RunBuildTool(const StringView& args, const StringView& work bool ScriptsBuilder::GenerateProject(const StringView& customArgs) { - String args(TEXT("-log -genproject ")); + String args(TEXT("-log -mutex -genproject ")); args += customArgs; _wasProjectStructureChanged = false; return RunBuildTool(args); From a87eaf82f07de89676e5de19335f61d7cb045c99 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Oct 2023 16:49:09 +0200 Subject: [PATCH 041/546] Code style fix --- .../Dedicated/MissingScriptEditor.cs | 67 +++++++------------ .../Undo/Actions/AddRemoveScriptAction.cs | 2 +- 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index 8c8c76561..8fb742b5e 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -1,3 +1,5 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + using FlaxEditor.Actions; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI; @@ -15,10 +17,9 @@ namespace FlaxEditor.CustomEditors.Dedicated; [CustomEditor(typeof(MissingScript)), DefaultEditor] public class MissingScriptEditor : GenericEditor { - DropPanel _dropPanel; - Button _replaceScriptButton; - CheckBox _shouldReplaceAllCheckbox; - CustomEditor _propertiesEditor; + private DropPanel _dropPanel; + private Button _replaceScriptButton; + private CheckBox _shouldReplaceAllCheckbox; /// public override void Initialize(LayoutElementsContainer layout) @@ -31,7 +32,7 @@ public class MissingScriptEditor : GenericEditor _dropPanel = dropPanel; _dropPanel.HeaderTextColor = Color.OrangeRed; - Panel replaceScriptPanel = new Panel + var replaceScriptPanel = new Panel { Parent = _dropPanel, Height = 64, @@ -40,6 +41,7 @@ public class MissingScriptEditor : GenericEditor _replaceScriptButton = new Button { Text = "Replace Script", + TooltipText = "Replaces the missing script with a given script type", AnchorPreset = AnchorPresets.TopCenter, Width = 240, Height = 24, @@ -49,9 +51,10 @@ public class MissingScriptEditor : GenericEditor }; _replaceScriptButton.Clicked += OnReplaceScriptButtonClicked; - Label replaceAllLabel = new Label + var replaceAllLabel = new Label { - Text = "Replace All of Same Type", + Text = "Replace all matching missing scripts", + TooltipText = "Whether or not to apply this script change to all scripts missing the same type.", AnchorPreset = AnchorPresets.BottomCenter, Y = -34, Parent = replaceScriptPanel, @@ -60,7 +63,7 @@ public class MissingScriptEditor : GenericEditor _shouldReplaceAllCheckbox = new CheckBox { - TooltipText = "Wether or not to apply this script change to all scripts missing the same type.", + TooltipText = replaceAllLabel.TooltipText, AnchorPreset = AnchorPresets.BottomCenter, Y = -34, Parent = replaceScriptPanel, @@ -73,35 +76,26 @@ public class MissingScriptEditor : GenericEditor base.Initialize(layout); } - private List FindActorsWithMatchingMissingScript() + private void FindActorsWithMatchingMissingScript(List missingScripts) { - List missingScripts = new List(); - - foreach (Actor actor in Level.GetActors()) + foreach (Actor actor in Level.GetActors(typeof(Actor))) { for (int scriptIndex = 0; scriptIndex < actor.ScriptsCount; scriptIndex++) { Script actorScript = actor.Scripts[scriptIndex]; if (actorScript is not MissingScript missingActorScript) - { continue; - } MissingScript currentMissing = Values[0] as MissingScript; if (missingActorScript.MissingTypeName != currentMissing.MissingTypeName) - { continue; - } - // Matching MissingScript. missingScripts.Add(missingActorScript); } } - - return missingScripts; } - private void RunReplacementMulticast(List actions) + private void RunReplacementMultiCast(List actions) { if (actions.Count == 0) { @@ -123,25 +117,19 @@ public class MissingScriptEditor : GenericEditor { var actions = new List(4); - List missingScripts = new List(); + var missingScripts = new List(); if (!replaceAllInScene) - { - missingScripts.Add(Values[0] as MissingScript); - } else - { - missingScripts = FindActorsWithMatchingMissingScript(); - } + missingScripts.Add((MissingScript)Values[0]); + else + FindActorsWithMatchingMissingScript(missingScripts); - foreach (MissingScript missingScript in missingScripts) - { - AddRemoveScript addReplacementScriptAction = AddRemoveScript.Add(missingScript.Actor, script); - actions.Add(addReplacementScriptAction); - } - RunReplacementMulticast(actions); + foreach (var missingScript in missingScripts) + actions.Add(AddRemoveScript.Add(missingScript.Actor, script)); + RunReplacementMultiCast(actions); for (int actionIdx = 0; actionIdx < actions.Count; actionIdx++) { - AddRemoveScript addRemoveScriptAction = (AddRemoveScript) actions[actionIdx]; + AddRemoveScript addRemoveScriptAction = (AddRemoveScript)actions[actionIdx]; int orderInParent = addRemoveScriptAction.GetOrderInParent(); Script newScript = missingScripts[actionIdx].Actor.Scripts[orderInParent]; @@ -149,11 +137,9 @@ public class MissingScriptEditor : GenericEditor } actions.Clear(); - foreach (MissingScript missingScript in missingScripts) - { + foreach (var missingScript in missingScripts) actions.Add(AddRemoveScript.Remove(missingScript)); - } - RunReplacementMulticast(actions); + RunReplacementMultiCast(actions); } private void OnReplaceScriptButtonClicked() @@ -171,12 +157,7 @@ public class MissingScriptEditor : GenericEditor // Show context menu with list of scripts to add var cm = new ItemsListContextMenu(180); for (int i = 0; i < scripts.Count; i++) - { cm.AddItem(new TypeSearchPopup.TypeItemView(scripts[i])); - } - // Get the parent (actor properties editor) of the parent (Scripts Editor) of our editor. - _propertiesEditor = ParentEditor.ParentEditor; - cm.ItemClicked += item => ReplaceScript((ScriptType)item.Tag, _shouldReplaceAllCheckbox.Checked); cm.SortItems(); cm.Show(_dropPanel, _replaceScriptButton.BottomLeft - new Float2((cm.Width - _replaceScriptButton.Width) / 2, 0)); diff --git a/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs b/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs index 86afd4c0b..dac2900f4 100644 --- a/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs +++ b/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs @@ -189,7 +189,7 @@ namespace FlaxEditor.Actions script.Parent = parentActor; if (_orderInParent != -1) script.OrderInParent = _orderInParent; - _orderInParent = script.OrderInParent; // Ensure _orderInParent is correct for script that want to use it. + _orderInParent = script.OrderInParent; // Ensure order is correct for script that want to use it later if (_prefabObjectId != Guid.Empty) SceneObject.Internal_LinkPrefab(Object.GetUnmanagedPtr(script), ref _prefabId, ref _prefabObjectId); Editor.Instance.Scene.MarkSceneEdited(parentActor.Scene); From eb430b05be2c92579208f23452757aa4ea6abfe9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Oct 2023 18:00:53 +0200 Subject: [PATCH 042/546] Fx regression from #1474 that was causing incorrect checks for `IsZero`/`IsOne` in `ShaderGraphValue` --- Source/Engine/Visject/ShaderGraphValue.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Visject/ShaderGraphValue.cpp b/Source/Engine/Visject/ShaderGraphValue.cpp index 56d7c9d06..3390563f4 100644 --- a/Source/Engine/Visject/ShaderGraphValue.cpp +++ b/Source/Engine/Visject/ShaderGraphValue.cpp @@ -40,11 +40,15 @@ ShaderGraphValue::ShaderGraphValue(const Variant& v) break; case VariantType::Float: Type = VariantType::Types::Float; - Value = String::Format(TEXT("{:.8f}"), v.AsFloat); + Value = String::Format(TEXT("{}"), v.AsFloat); + if (Value.Find('.') == -1) + Value = String::Format(TEXT("{:.1f}"), v.AsFloat); break; case VariantType::Double: Type = VariantType::Types::Float; - Value = String::Format(TEXT("{:.8f}"), (float)v.AsDouble); + Value = String::Format(TEXT("{}"), (float)v.AsDouble); + if (Value.Find('.') == -1) + Value = String::Format(TEXT("{:.1f}"), (float)v.AsDouble); break; case VariantType::Float2: { From e51aab0c56dd6440c75cd8bee014f4bcce9efffd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Oct 2023 18:03:21 +0200 Subject: [PATCH 043/546] Minor improvements --- .../Engine/Graphics/Models/ModelData.Tool.cpp | 17 ++++++++++---- Source/Engine/Visject/ShaderGraphValue.cpp | 23 +++++++++++++++++++ Source/Engine/Visject/ShaderGraphValue.h | 13 ++++++----- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/Source/Engine/Graphics/Models/ModelData.Tool.cpp b/Source/Engine/Graphics/Models/ModelData.Tool.cpp index d061d0745..727fe7754 100644 --- a/Source/Engine/Graphics/Models/ModelData.Tool.cpp +++ b/Source/Engine/Graphics/Models/ModelData.Tool.cpp @@ -318,7 +318,9 @@ void MeshData::BuildIndexBuffer() } const auto endTime = Platform::GetTimeSeconds(); - LOG(Info, "Generated index buffer for mesh in {0}s ({1} vertices, {2} indices)", Utilities::RoundTo2DecimalPlaces(endTime - startTime), Positions.Count(), Indices.Count()); + const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime); + if (time > 0.5f) // Don't log if generation was fast enough + LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, Indices.Count(), TEXT("indices")); } void MeshData::FindPositions(const Float3& position, float epsilon, Array& result) @@ -449,7 +451,9 @@ bool MeshData::GenerateNormals(float smoothingAngle) } const auto endTime = Platform::GetTimeSeconds(); - LOG(Info, "Generated tangents for mesh in {0}s ({1} vertices, {2} indices)", Utilities::RoundTo2DecimalPlaces(endTime - startTime), vertexCount, indexCount); + const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime); + if (time > 0.5f) // Don't log if generation was fast enough + LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, indexCount, TEXT("normals")); return false; } @@ -685,7 +689,10 @@ bool MeshData::GenerateTangents(float smoothingAngle) #endif const auto endTime = Platform::GetTimeSeconds(); - LOG(Info, "Generated tangents for mesh in {0}s ({1} vertices, {2} indices)", Utilities::RoundTo2DecimalPlaces(endTime - startTime), vertexCount, indexCount); + const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime); + if (time > 0.5f) // Don't log if generation was fast enough + LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, indexCount, TEXT("tangents")); + return false; } @@ -872,7 +879,9 @@ void MeshData::ImproveCacheLocality() Allocator::Free(piCandidates); const auto endTime = Platform::GetTimeSeconds(); - LOG(Info, "Cache relevant optimize for {0} vertices and {1} indices. Average output ACMR is {2}. Time: {3}s", vertexCount, indexCount, (float)iCacheMisses / indexCount / 3, Utilities::RoundTo2DecimalPlaces(endTime - startTime)); + const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime); + if (time > 0.5f) // Don't log if generation was fast enough + LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, indexCount, TEXT("optimized indices")); } float MeshData::CalculateTrianglesArea() const diff --git a/Source/Engine/Visject/ShaderGraphValue.cpp b/Source/Engine/Visject/ShaderGraphValue.cpp index 3390563f4..6564cde2b 100644 --- a/Source/Engine/Visject/ShaderGraphValue.cpp +++ b/Source/Engine/Visject/ShaderGraphValue.cpp @@ -138,6 +138,29 @@ bool ShaderGraphValue::IsOne() const } } +bool ShaderGraphValue::IsLiteral() const +{ + switch (Type) + { + case VariantType::Types::Bool: + case VariantType::Types::Int: + case VariantType::Types::Uint: + case VariantType::Types::Float: + if (Value.HasChars()) + { + for (int32 i = 0; i < Value.Length(); i++) + { + const Char c = Value[i]; + if (!StringUtils::IsDigit(c) && c != '.') + return false; + } + return true; + } + default: + return false; + } +} + ShaderGraphValue ShaderGraphValue::InitForZero(VariantType::Types type) { const Char* v; diff --git a/Source/Engine/Visject/ShaderGraphValue.h b/Source/Engine/Visject/ShaderGraphValue.h index 7c7e25154..439b59077 100644 --- a/Source/Engine/Visject/ShaderGraphValue.h +++ b/Source/Engine/Visject/ShaderGraphValue.h @@ -143,8 +143,7 @@ public: /// /// Returns true if value is valid. /// - /// True if is valid, otherwise false. - bool IsValid() const + FORCE_INLINE bool IsValid() const { return Type != VariantType::Types::Null; } @@ -152,8 +151,7 @@ public: /// /// Returns true if value is invalid. /// - /// True if is invalid, otherwise false. - bool IsInvalid() const + FORCE_INLINE bool IsInvalid() const { return Type == VariantType::Types::Null; } @@ -161,15 +159,18 @@ public: /// /// Checks if value contains static part with zero. /// - /// True if contains zero number. bool IsZero() const; /// /// Checks if value contains static part with one. /// - /// True if contains one number. bool IsOne() const; + /// + /// Checks if value is a compile-time constant literal (eg. int, bool or float). + /// + bool IsLiteral() const; + /// /// Clears this instance. /// From 2f3929efe0d2952c420b65a4bc52d59134b23677 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Oct 2023 18:03:51 +0200 Subject: [PATCH 044/546] Fix `Height Layer Blend` material node codegen when alpha is constant #1705 --- Source/Editor/Surface/Archetypes/Layers.cs | 2 +- .../Tools/MaterialGenerator/MaterialGenerator.Layers.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Layers.cs b/Source/Editor/Surface/Archetypes/Layers.cs index 4a46276c3..0308cab15 100644 --- a/Source/Editor/Surface/Archetypes/Layers.cs +++ b/Source/Editor/Surface/Archetypes/Layers.cs @@ -176,7 +176,7 @@ namespace FlaxEditor.Surface.Archetypes Size = new Float2(200, 100), DefaultValues = new object[] { - 0.0f, + 0.5f, }, Elements = new[] { diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Layers.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Layers.cpp index 8db8a0d0d..fe45b8b96 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Layers.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Layers.cpp @@ -147,7 +147,7 @@ void MaterialGenerator::ProcessGroupLayers(Box* box, Node* node, Value& value) case 8: { const Value defaultValue = MaterialValue::InitForZero(VariantType::Void); - const Value alpha = tryGetValue(node->GetBox(2), 0, Value::Zero); + Value alpha = tryGetValue(node->GetBox(2), 0, Value::Zero).AsFloat(); if (alpha.IsZero()) { // Bottom-only @@ -178,6 +178,7 @@ void MaterialGenerator::ProcessGroupLayers(Box* box, Node* node, Value& value) auto topHeightScaled = writeLocal(VariantType::Float, String::Format(TEXT("{0} * {1}"), topHeight.Value, alpha.Value), node); auto heightStart = writeLocal(VariantType::Float, String::Format(TEXT("max({0}, {1}) - 0.05"), bottomHeightScaled.Value, topHeightScaled.Value), node); auto bottomLevel = writeLocal(VariantType::Float, String::Format(TEXT("max({0} - {1}, 0.0001)"), topHeightScaled.Value, heightStart.Value), node); + alpha = writeLocal(VariantType::Float, alpha.Value, node); _writer.Write(TEXT("\t{0} = {1} / (max({2} - {3}, 0) + {4});\n"), alpha.Value, bottomLevel.Value, bottomHeightScaled.Value, heightStart.Value, bottomLevel.Value); } #define EAT_BOX(type) writeBlending(MaterialGraphBoxes::type, value, bottom, top, alpha) From 660c0660e24e9e4c4864799e8b7f49ced3233669 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sat, 14 Oct 2023 20:52:56 +0300 Subject: [PATCH 045/546] Delay compiler and project generation after large file operations --- Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs | 4 +++- Source/Editor/Scripting/ScriptsBuilder.cpp | 4 ++-- Source/Editor/Scripting/ScriptsBuilder.h | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs index e9ff4c5b5..4d970d9b1 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs @@ -412,7 +412,9 @@ namespace FlaxEditor.Modules.SourceCodeEditing // Automatic project files generation after workspace modifications if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty) { - Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync(); + // Try to delay generation when a lot of files are added at once + if (ScriptsBuilder.IsSourceDirtyFor(TimeSpan.FromMilliseconds(150))) + Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync(); } } diff --git a/Source/Editor/Scripting/ScriptsBuilder.cpp b/Source/Editor/Scripting/ScriptsBuilder.cpp index 8807379cf..20cb36660 100644 --- a/Source/Editor/Scripting/ScriptsBuilder.cpp +++ b/Source/Editor/Scripting/ScriptsBuilder.cpp @@ -170,7 +170,7 @@ bool ScriptsBuilder::IsSourceWorkspaceDirty() bool ScriptsBuilder::IsSourceDirtyFor(const TimeSpan& timeout) { ScopeLock scopeLock(_locker); - return _lastSourceCodeEdited > (_lastCompileAction + timeout); + return _lastSourceCodeEdited > _lastCompileAction && DateTime::Now() > _lastSourceCodeEdited + timeout; } bool ScriptsBuilder::IsCompiling() @@ -669,7 +669,7 @@ void ScriptsBuilderService::Update() } // Check if compile code (if has been edited) - const TimeSpan timeToCallCompileIfDirty = TimeSpan::FromMilliseconds(50); + const TimeSpan timeToCallCompileIfDirty = TimeSpan::FromMilliseconds(150); auto mainWindow = Engine::MainWindow; if (ScriptsBuilder::IsSourceDirtyFor(timeToCallCompileIfDirty) && mainWindow && mainWindow->IsFocused()) { diff --git a/Source/Editor/Scripting/ScriptsBuilder.h b/Source/Editor/Scripting/ScriptsBuilder.h index f954b0fd0..0a11bd9b7 100644 --- a/Source/Editor/Scripting/ScriptsBuilder.h +++ b/Source/Editor/Scripting/ScriptsBuilder.h @@ -68,7 +68,7 @@ public: ///
/// Time to use for checking. /// True if source code is dirty, otherwise false. - static bool IsSourceDirtyFor(const TimeSpan& timeout); + API_FUNCTION() static bool IsSourceDirtyFor(const TimeSpan& timeout); /// /// Returns true if scripts are being now compiled/reloaded. From f8f1a66ab99ff1d394da5dbe4f956f2545d599bd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 15 Oct 2023 10:12:33 +0200 Subject: [PATCH 046/546] Fix multi-line logs on Windows regression from cf94cd937a7a72aa9828bf63f47d945084963d8f --- Source/Engine/Core/Log.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/Engine/Core/Log.cpp b/Source/Engine/Core/Log.cpp index e3e1cc494..c8586db76 100644 --- a/Source/Engine/Core/Log.cpp +++ b/Source/Engine/Core/Log.cpp @@ -9,7 +9,6 @@ #include "Engine/Platform/FileSystem.h" #include "Engine/Platform/CriticalSection.h" #include "Engine/Serialization/FileWriteStream.h" -#include "Engine/Serialization/MemoryWriteStream.h" #include "Engine/Debug/Exceptions/Exceptions.h" #if USE_EDITOR #include "Engine/Core/Collections/Array.h" @@ -210,17 +209,18 @@ void Log::Logger::ProcessLogMessage(LogType type, const StringView& msg, fmt_fla hasWindowsNewLine |= msg.Get()[i - 1] != '\r' && msg.Get()[i] == '\n'; if (hasWindowsNewLine) { - MemoryWriteStream msgStream(msgLength * sizeof(Char)); - msgStream.WriteChar(msg.Get()[0]); + Array msgStream; + msgStream.EnsureCapacity(msgLength); + msgStream.Add(msg.Get()[0]); for (int32 i = 1; i < msgLength; i++) { if (msg.Get()[i - 1] != '\r' && msg.Get()[i] == '\n') - msgStream.WriteChar(TEXT('\r')); - msgStream.WriteChar(msg.Get()[i]); + msgStream.Add(TEXT('\r')); + msgStream.Add(msg.Get()[i]); } - msgStream.WriteChar(TEXT('\0')); - w.append((const Char*)msgStream.GetHandle(), (const Char*)msgStream.GetHandle() + msgStream.GetPosition()); - //fmt_flax::format(w, TEXT("{}"), (const Char*)msgStream.GetHandle()); + msgStream.Add(TEXT('\0')); + w.append(msgStream.Get(), (const Char*)(msgStream.Get() + msgStream.Count())); + //fmt_flax::format(w, TEXT("{}"), (const Char*)msgStream.Get()); return; } #endif From 00dc5d44d4ba715e6ebef413c6df36618d20f729 Mon Sep 17 00:00:00 2001 From: Vizepi Date: Sun, 15 Oct 2023 12:24:25 +0200 Subject: [PATCH 047/546] #1716 Do not rebuild contexts twice in a row --- .../Engine/Audio/OpenAL/AudioBackendOAL.cpp | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index 2f65a4b6c..ace8b6591 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -33,7 +33,8 @@ int alError = alGetError(); \ if (alError != 0) \ { \ - LOG(Error, "OpenAL method {0} failed with error 0x{1:X} (at line {2})", TEXT(#method), alError, __LINE__ - 1); \ + const Char* errorStr = GetOpenALErrorString(alError); \ + LOG(Error, "OpenAL method {0} failed with error 0x{1:X}:{2} (at line {3})", TEXT(#method), alError, errorStr, __LINE__ - 1); \ } \ } #endif @@ -290,6 +291,28 @@ ALenum GetOpenALBufferFormat(uint32 numChannels, uint32 bitDepth) return 0; } +const Char* GetOpenALErrorString(int error) +{ + switch (error) + { + case AL_NO_ERROR: + return TEXT("AL_NO_ERROR"); + case AL_INVALID_NAME: + return TEXT("AL_INVALID_NAME"); + case AL_INVALID_ENUM: + return TEXT("AL_INVALID_ENUM"); + case AL_INVALID_VALUE: + return TEXT("AL_INVALID_VALUE"); + case AL_INVALID_OPERATION: + return TEXT("AL_INVALID_OPERATION"); + case AL_OUT_OF_MEMORY: + return TEXT("AL_OUT_OF_MEMORY"); + default: + break; + } + return TEXT("???"); +} + void AudioBackendOAL::Listener_OnAdd(AudioListener* listener) { #if ALC_MULTIPLE_LISTENERS @@ -838,7 +861,11 @@ bool AudioBackendOAL::Base_Init() // Init Base_SetDopplerFactor(AudioSettings::Get()->DopplerFactor); alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); // Default attenuation model - ALC::RebuildContexts(true); + int32 clampedIndex = Math::Clamp(activeDeviceIndex, -1, Audio::Devices.Count() - 1); + if (clampedIndex == Audio::GetActiveDeviceIndex()) + { + ALC::RebuildContexts(true); + } Audio::SetActiveDeviceIndex(activeDeviceIndex); #ifdef AL_SOFT_source_spatialize if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize")) From 0d7e7c30ca0269594bd0cddee5f8c5d515b33051 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Wed, 11 Oct 2023 21:40:38 +0300 Subject: [PATCH 048/546] Run Flax.Build when building main C# project --- .../VisualStudio/CSSDKProjectGenerator.cs | 17 ++++-- .../VisualStudioProjectGenerator.cs | 60 +++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs index 387a0dcff..1ff3ea4db 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs @@ -90,6 +90,11 @@ namespace Flax.Build.Projects.VisualStudio var baseConfiguration = project.Configurations.First(); var baseOutputDir = Utilities.MakePathRelativeTo(project.CSharp.OutputPath ?? baseConfiguration.TargetBuildOptions.OutputFolder, projectDirectory); var baseIntermediateOutputPath = Utilities.MakePathRelativeTo(project.CSharp.IntermediateOutputPath ?? Path.Combine(baseConfiguration.TargetBuildOptions.IntermediateFolder, "CSharp"), projectDirectory); + + bool isMainProject = Globals.Project.ProjectFolderPath == project.WorkspaceRootPath && project.Name != "BuildScripts"; + var flaxBuildTargetsFilename = isMainProject ? "Flax.Build.CSharp.targets" : "Flax.Build.CSharp.SkipBuild.targets"; + var cacheProjectsPath = Utilities.MakePathRelativeTo(Path.Combine(Globals.Root, "Cache", "Projects"), projectDirectory); + var flaxBuildTargetsPath = !string.IsNullOrEmpty(cacheProjectsPath) ? Path.Combine(cacheProjectsPath, flaxBuildTargetsFilename) : flaxBuildTargetsFilename; csProjectFileContent.AppendLine(" net7.0"); csProjectFileContent.AppendLine(" disable"); @@ -106,14 +111,14 @@ namespace Flax.Build.Projects.VisualStudio csProjectFileContent.AppendLine(" 11.0"); csProjectFileContent.AppendLine(" 512"); - // Needed for Hostfxr - csProjectFileContent.AppendLine(" true"); - csProjectFileContent.AppendLine(" true"); //csProjectFileContent.AppendLine(" false"); // TODO: use it to reduce burden of framework libs - // This needs to be set here to fix errors in VS - csProjectFileContent.AppendLine(string.Format(" {0}", baseOutputDir)); - csProjectFileContent.AppendLine(string.Format(" {0}", baseIntermediateOutputPath)); + // Custom .targets file for overriding MSBuild build tasks + csProjectFileContent.AppendLine(string.Format(" $(MSBuildThisFileDirectory){0}", flaxBuildTargetsPath)); + + // Hide annoying warnings during build + csProjectFileContent.AppendLine(" false"); + csProjectFileContent.AppendLine(" True"); csProjectFileContent.AppendLine(" "); csProjectFileContent.AppendLine(""); diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs index 1ba8e6709..dd4bf2f42 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs @@ -679,6 +679,57 @@ namespace Flax.Build.Projects.VisualStudio Utilities.WriteFileIfChanged(dotSettingsUserFilePath, dotSettingsFileContent.ToString()); } + + // Custom MSBuild .targets file to prevent building Flax C#-projects directly with MSBuild + { + var targetsFileContent = new StringBuilder(); + targetsFileContent.AppendLine(""); + targetsFileContent.AppendLine(" "); + targetsFileContent.AppendLine(" "); + targetsFileContent.AppendLine(""); + + Utilities.WriteFileIfChanged(Path.Combine(Globals.Root, "Cache", "Projects", "Flax.Build.CSharp.SkipBuild.targets"), targetsFileContent.ToString()); + } + + // Override MSBuild build tasks to run Flax.Build in C#-only projects + { + // Build command for the build tool + var buildToolPath = Path.ChangeExtension(Utilities.MakePathRelativeTo(typeof(Builder).Assembly.Location, Path.GetDirectoryName(solution.MainProject.Path)), null); + + var targetsFileContent = new StringBuilder(); + targetsFileContent.AppendLine(""); + targetsFileContent.AppendLine(" "); + targetsFileContent.AppendLine(" "); + AppendBuildToolCommands(targetsFileContent, "-build"); + targetsFileContent.AppendLine(" "); + targetsFileContent.AppendLine(" "); + AppendBuildToolCommands(targetsFileContent, "-rebuild"); + targetsFileContent.AppendLine(" "); + targetsFileContent.AppendLine(" "); + AppendBuildToolCommands(targetsFileContent, "-clean"); + targetsFileContent.AppendLine(" "); + targetsFileContent.AppendLine(""); + + Utilities.WriteFileIfChanged(Path.Combine(Globals.Root, "Cache", "Projects", "Flax.Build.CSharp.targets"), targetsFileContent.ToString()); + + void AppendBuildToolCommands(StringBuilder str, string extraArgs) + { + foreach (var configuration in solution.MainProject.Configurations) + { + var cmdLine = string.Format("{0} -log -mutex -workspace={1} -arch={2} -configuration={3} -platform={4} -buildTargets={5}", + FixPath(buildToolPath), + FixPath(solution.MainProject.WorkspaceRootPath), + configuration.Architecture, + configuration.Configuration, + configuration.Platform, + configuration.Target); + if (!string.IsNullOrEmpty(Configuration.Compiler)) + cmdLine += " -compiler=" + Configuration.Compiler; + + str.AppendLine(string.Format(" ", cmdLine, extraArgs, configuration.Name)); + } + } + } } /// @@ -716,5 +767,14 @@ namespace Flax.Build.Projects.VisualStudio projects.Add(project); } } + + private static string FixPath(string path) + { + if (path.Contains(' ')) + { + path = "\"" + path + "\""; + } + return path; + } } } From 5b3e09baec4bb8680186d4d586c71136bc8b4fd1 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Wed, 11 Oct 2023 21:42:06 +0300 Subject: [PATCH 049/546] Build C# projects in VS/Rider solution configurations Rider's solution wide analysis does not work properly when projects are not included in the active configuration for build. --- .../VisualStudio/VisualStudioProjectGenerator.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs index dd4bf2f42..48ecb6425 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs @@ -518,7 +518,15 @@ namespace Flax.Build.Projects.VisualStudio else if (firstFullMatch != -1) { projectConfiguration = configuration; - build = solution.MainProject == project || (solution.MainProject == null && project.Name == solution.Name); + + // Always build the main project + build = solution.MainProject == project; + + // Build C# projects (needed for Rider solution wide analysis) + build |= project.Type == TargetType.DotNetCore; + + // + build |= solution.MainProject == null && project.Name == solution.Name; } else if (firstPlatformMatch != -1 && !configuration.Name.StartsWith("Editor.")) { From ff7e6d82f8cff320cdbd7afb8049ad0dca89f434 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Wed, 11 Oct 2023 21:42:45 +0300 Subject: [PATCH 050/546] Hide exception when build errors occurs in referenced targets --- .../Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs | 6 ++++++ Source/Tools/Flax.Build/Globals.cs | 5 +++++ Source/Tools/Flax.Build/Program.cs | 4 +++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs index 1e8550c4e..4192b0caa 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs @@ -628,7 +628,10 @@ namespace Flax.Build } } if (failed) + { + Globals.BuildErrors = true; throw new Exception($"Failed to build target {target.Name}. See log."); + } } else { @@ -689,7 +692,10 @@ namespace Flax.Build } } if (failed) + { + Globals.BuildErrors = true; throw new Exception($"Failed to build target {target.Name}. See log."); + } } else { diff --git a/Source/Tools/Flax.Build/Globals.cs b/Source/Tools/Flax.Build/Globals.cs index b5f3088d6..994094124 100644 --- a/Source/Tools/Flax.Build/Globals.cs +++ b/Source/Tools/Flax.Build/Globals.cs @@ -22,6 +22,11 @@ namespace Flax.Build /// public static ProjectInfo Project; + /// + /// Set when any build related errors were raised. + /// + public static bool BuildErrors = false; + /// /// All platforms array. /// diff --git a/Source/Tools/Flax.Build/Program.cs b/Source/Tools/Flax.Build/Program.cs index 5e7cf926c..bac1caaa8 100644 --- a/Source/Tools/Flax.Build/Program.cs +++ b/Source/Tools/Flax.Build/Program.cs @@ -171,7 +171,9 @@ namespace Flax.Build } catch (Exception ex) { - Log.Exception(ex); + // Ignore exception logging for build errors + if (!Globals.BuildErrors) + Log.Exception(ex); failed = true; } finally From bcdd6c0551cd126a483624f66ba033f46f0b2091 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 15 Oct 2023 16:15:51 +0300 Subject: [PATCH 051/546] Fix FlaxEngine C#-project getting built twice in engine solution --- .../Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs index 1ff3ea4db..77b7abf46 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs @@ -91,7 +91,7 @@ namespace Flax.Build.Projects.VisualStudio var baseOutputDir = Utilities.MakePathRelativeTo(project.CSharp.OutputPath ?? baseConfiguration.TargetBuildOptions.OutputFolder, projectDirectory); var baseIntermediateOutputPath = Utilities.MakePathRelativeTo(project.CSharp.IntermediateOutputPath ?? Path.Combine(baseConfiguration.TargetBuildOptions.IntermediateFolder, "CSharp"), projectDirectory); - bool isMainProject = Globals.Project.ProjectFolderPath == project.WorkspaceRootPath && project.Name != "BuildScripts"; + bool isMainProject = Globals.Project.ProjectFolderPath == project.WorkspaceRootPath && project.Name != "BuildScripts" && (Globals.Project.Name != "Flax" || project.Name != "FlaxEngine"); var flaxBuildTargetsFilename = isMainProject ? "Flax.Build.CSharp.targets" : "Flax.Build.CSharp.SkipBuild.targets"; var cacheProjectsPath = Utilities.MakePathRelativeTo(Path.Combine(Globals.Root, "Cache", "Projects"), projectDirectory); var flaxBuildTargetsPath = !string.IsNullOrEmpty(cacheProjectsPath) ? Path.Combine(cacheProjectsPath, flaxBuildTargetsFilename) : flaxBuildTargetsFilename; From e4941e5b008d380ee20eb0823f3d7a4e461cc570 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 15 Oct 2023 18:46:15 +0200 Subject: [PATCH 052/546] Add asset loading task state check on fail --- .../Content/Loading/Tasks/LoadAssetTask.h | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h index 5ee384769..19d6fdd31 100644 --- a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h +++ b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h @@ -31,10 +31,13 @@ public: if (Asset) { Asset->Locker.Lock(); - Asset->_loadFailed = true; - Asset->_isLoaded = false; - LOG(Error, "Loading asset \'{0}\' result: {1}.", ToString(), ToString(Result::TaskFailed)); - Asset->_loadingTask = nullptr; + if (Asset->_loadingTask == this) + { + Asset->_loadFailed = true; + Asset->_isLoaded = false; + LOG(Error, "Loading asset \'{0}\' result: {1}.", ToString(), ToString(Result::TaskFailed)); + Asset->_loadingTask = nullptr; + } Asset->Locker.Unlock(); } } @@ -73,7 +76,10 @@ protected: { if (Asset) { - Asset->_loadingTask = nullptr; + Asset->Locker.Lock(); + if (Asset->_loadingTask == this) + Asset->_loadingTask = nullptr; + Asset->Locker.Unlock(); Asset = nullptr; } @@ -84,7 +90,10 @@ protected: { if (Asset) { - Asset->_loadingTask = nullptr; + Asset->Locker.Lock(); + if (Asset->_loadingTask == this) + Asset->_loadingTask = nullptr; + Asset->Locker.Unlock(); Asset = nullptr; } From 148ced5a55890197bfaad3ae695925d90d33bbf0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 15 Oct 2023 19:04:35 +0200 Subject: [PATCH 053/546] Fix compilation --- Source/Engine/Core/Log.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Core/Log.cpp b/Source/Engine/Core/Log.cpp index c8586db76..bb8d42aba 100644 --- a/Source/Engine/Core/Log.cpp +++ b/Source/Engine/Core/Log.cpp @@ -3,7 +3,7 @@ #include "Log.h" #include "Engine/Engine/CommandLine.h" #include "Engine/Core/Types/DateTime.h" -#include "Engine/Core/Collections/Sorting.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/Globals.h" #include "Engine/Platform/FileSystem.h" @@ -11,7 +11,7 @@ #include "Engine/Serialization/FileWriteStream.h" #include "Engine/Debug/Exceptions/Exceptions.h" #if USE_EDITOR -#include "Engine/Core/Collections/Array.h" +#include "Engine/Core/Collections/Sorting.h" #endif #include From 88f718709b6e6bd814ff51076fc4da08ed6d7040 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 15 Oct 2023 20:13:34 +0200 Subject: [PATCH 054/546] Add data sent/receive rate charts to the network profiler in Editor --- Source/Editor/Windows/Profiler/Network.cs | 71 ++++++++++++++++--- .../Editor/Windows/Profiler/SamplesBuffer.cs | 2 +- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs index 78d84ad9d..d29000d27 100644 --- a/Source/Editor/Windows/Profiler/Network.cs +++ b/Source/Editor/Windows/Profiler/Network.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using FlaxEditor.GUI; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Networking; namespace FlaxEngine { @@ -37,11 +38,14 @@ namespace FlaxEditor.Windows.Profiler { private readonly SingleChart _dataSentChart; private readonly SingleChart _dataReceivedChart; + private readonly SingleChart _dataSentRateChart; + private readonly SingleChart _dataReceivedRateChart; private readonly Table _tableRpc; private readonly Table _tableRep; - private SamplesBuffer _events; private List _tableRowsCache; - private FlaxEngine.Networking.NetworkDriverStats _prevStats; + private SamplesBuffer _events; + private NetworkDriverStats _prevStats; + private List _stats; public Network() : base("Network") @@ -76,6 +80,20 @@ namespace FlaxEditor.Windows.Profiler Parent = layout, }; _dataReceivedChart.SelectedSampleChanged += OnSelectedSampleChanged; + _dataSentRateChart = new SingleChart + { + Title = "Data Sent Rate", + FormatSample = FormatSampleBytesRate, + Parent = layout, + }; + _dataSentRateChart.SelectedSampleChanged += OnSelectedSampleChanged; + _dataReceivedRateChart = new SingleChart + { + Title = "Data Received Rate", + FormatSample = FormatSampleBytesRate, + Parent = layout, + }; + _dataReceivedRateChart.SelectedSampleChanged += OnSelectedSampleChanged; // Tables _tableRpc = InitTable(layout, "RPC Name"); @@ -87,24 +105,52 @@ namespace FlaxEditor.Windows.Profiler { _dataSentChart.Clear(); _dataReceivedChart.Clear(); + _dataSentRateChart.Clear(); + _dataReceivedRateChart.Clear(); _events?.Clear(); + _stats?.Clear(); + _prevStats = new NetworkDriverStats(); } /// public override void Update(ref SharedUpdateData sharedData) { // Gather peer stats - var peers = FlaxEngine.Networking.NetworkPeer.Peers; - var stats = new FlaxEngine.Networking.NetworkDriverStats(); + var peers = NetworkPeer.Peers; + var thisStats = new NetworkDriverStats(); + thisStats.RTT = Time.UnscaledGameTime; // Store sample time in RTT foreach (var peer in peers) { var peerStats = peer.NetworkDriver.GetStats(); - stats.TotalDataSent += peerStats.TotalDataSent; - stats.TotalDataReceived += peerStats.TotalDataReceived; + thisStats.TotalDataSent += peerStats.TotalDataSent; + thisStats.TotalDataReceived += peerStats.TotalDataReceived; } - _dataSentChart.AddSample(Mathf.Max((long)stats.TotalDataSent - (long)_prevStats.TotalDataSent, 0)); - _dataReceivedChart.AddSample(Mathf.Max((long)stats.TotalDataReceived - (long)_prevStats.TotalDataReceived, 0)); - _prevStats = stats; + var stats = thisStats; + stats.TotalDataSent = (uint)Mathf.Max((long)thisStats.TotalDataSent - (long)_prevStats.TotalDataSent, 0); + stats.TotalDataReceived = (uint)Mathf.Max((long)thisStats.TotalDataReceived - (long)_prevStats.TotalDataReceived, 0); + _dataSentChart.AddSample(stats.TotalDataSent); + _dataReceivedChart.AddSample(stats.TotalDataReceived); + _prevStats = thisStats; + if (_stats == null) + _stats = new List(); + _stats.Add(stats); + + // Remove all stats older than 1 second + while (_stats.Count > 0 && thisStats.RTT - _stats[0].RTT >= 1.0f) + _stats.RemoveAt(0); + + // Calculate average data rates (from last second) + var avgStats = new NetworkDriverStats(); + foreach (var e in _stats) + { + avgStats.TotalDataSent += e.TotalDataSent; + avgStats.TotalDataReceived += e.TotalDataReceived; + } + avgStats.TotalDataSent /= (uint)_stats.Count; + avgStats.TotalDataReceived /= (uint)_stats.Count; + _dataSentRateChart.AddSample(avgStats.TotalDataSent); + _dataReceivedRateChart.AddSample(avgStats.TotalDataReceived); + // Gather network events var events = ProfilingTools.EventsNetwork; @@ -118,6 +164,8 @@ namespace FlaxEditor.Windows.Profiler { _dataSentChart.SelectedSampleIndex = selectedFrame; _dataReceivedChart.SelectedSampleIndex = selectedFrame; + _dataSentRateChart.SelectedSampleIndex = selectedFrame; + _dataReceivedRateChart.SelectedSampleIndex = selectedFrame; // Update events tables if (_events != null) @@ -257,6 +305,11 @@ namespace FlaxEditor.Windows.Profiler return Utilities.Utils.FormatBytesCount((ulong)v); } + private static string FormatSampleBytesRate(float v) + { + return Utilities.Utils.FormatBytesCount((ulong)v) + "/s"; + } + private static string FormatCellBytes(object x) { return Utilities.Utils.FormatBytesCount((int)x); diff --git a/Source/Editor/Windows/Profiler/SamplesBuffer.cs b/Source/Editor/Windows/Profiler/SamplesBuffer.cs index 999156dca..2e0169d47 100644 --- a/Source/Editor/Windows/Profiler/SamplesBuffer.cs +++ b/Source/Editor/Windows/Profiler/SamplesBuffer.cs @@ -49,7 +49,7 @@ namespace FlaxEditor.Windows.Profiler /// The sample value public T Get(int index) { - if (index >= _data.Length || _data.Length == 0) + if (_count == 0 || index >= _data.Length || _data.Length == 0) return default; return index == -1 ? _data[_count - 1] : _data[index]; } From d1b2b64cd1a6fb0ce8efb523af112c789207671e Mon Sep 17 00:00:00 2001 From: nothingTVatYT Date: Sun, 15 Oct 2023 21:43:15 +0200 Subject: [PATCH 055/546] fix color picker (signal handler + alpha channel) --- Source/Editor/GUI/Dialogs/ColorPickerDialog.cs | 3 ++- Source/Editor/Utilities/ScreenUtilities.cpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 161b3f4ae..efb262bd2 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -218,6 +218,7 @@ namespace FlaxEditor.GUI.Dialogs private void OnColorPicked(Color32 colorPicked) { + Editor.Log("OnColorPicked " + colorPicked + ", _active eye dropper=" + _activeEyedropper); if (_activeEyedropper) { _activeEyedropper = false; @@ -319,7 +320,7 @@ namespace FlaxEditor.GUI.Dialogs protected override void OnShow() { // Auto cancel on lost focus - ((WindowRootControl)Root).Window.LostFocus += OnCancel; + //((WindowRootControl)Root).Window.LostFocus += OnCancel; base.OnShow(); } diff --git a/Source/Editor/Utilities/ScreenUtilities.cpp b/Source/Editor/Utilities/ScreenUtilities.cpp index 44f52350e..730a69aa3 100644 --- a/Source/Editor/Utilities/ScreenUtilities.cpp +++ b/Source/Editor/Utilities/ScreenUtilities.cpp @@ -73,6 +73,7 @@ Color32 ScreenUtilities::GetColorAt(const Float2& pos) outputColor.R = color.red / 256; outputColor.G = color.green / 256; outputColor.B = color.blue / 256; + outputColor.A = 255; return outputColor; } From c42a264944f94db74fccbaed4ef1a9de156b88fe Mon Sep 17 00:00:00 2001 From: nothingTVatYT Date: Mon, 16 Oct 2023 01:08:47 +0200 Subject: [PATCH 056/546] removed a left over debug statement --- Source/Editor/GUI/Dialogs/ColorPickerDialog.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index efb262bd2..4425813c0 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -218,7 +218,6 @@ namespace FlaxEditor.GUI.Dialogs private void OnColorPicked(Color32 colorPicked) { - Editor.Log("OnColorPicked " + colorPicked + ", _active eye dropper=" + _activeEyedropper); if (_activeEyedropper) { _activeEyedropper = false; From 48a5fe54e8f0d12f9237d90c4be7df2edc837f92 Mon Sep 17 00:00:00 2001 From: nothingTVatYT Date: Mon, 16 Oct 2023 01:13:06 +0200 Subject: [PATCH 057/546] keep auto-close for non-Linux platforms --- Source/Editor/GUI/Dialogs/ColorPickerDialog.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 4425813c0..27878a763 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -319,7 +319,9 @@ namespace FlaxEditor.GUI.Dialogs protected override void OnShow() { // Auto cancel on lost focus - //((WindowRootControl)Root).Window.LostFocus += OnCancel; +#if !PLATFORM_LINUX + ((WindowRootControl)Root).Window.LostFocus += OnCancel; +#endif base.OnShow(); } From db6b98ecd1396f0d8c93b90296e67784eaa49f2b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Oct 2023 10:29:44 +0200 Subject: [PATCH 058/546] Add `ProfilerGPU.Enabled` to manually toggle GPU timings --- Source/Engine/Profiler/ProfilerGPU.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Profiler/ProfilerGPU.h b/Source/Engine/Profiler/ProfilerGPU.h index 29392b7a5..fa2c72ab0 100644 --- a/Source/Engine/Profiler/ProfilerGPU.h +++ b/Source/Engine/Profiler/ProfilerGPU.h @@ -125,7 +125,7 @@ public: /// /// True if GPU profiling is enabled, otherwise false to disable events collecting and GPU timer queries usage. Can be changed during rendering. /// - static bool Enabled; + API_FIELD() static bool Enabled; /// /// The current frame buffer to collect events. From d25954725f9f61b870ca9886883d959035cc518d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Oct 2023 10:58:07 +0200 Subject: [PATCH 059/546] Add excluding GPU swapchain present time from CPU draw time in profiling tool --- Source/Engine/Graphics/GPUDevice.cpp | 7 +++++ Source/Engine/Profiler/ProfilerGPU.cpp | 17 +++++++++--- Source/Engine/Profiler/ProfilerGPU.h | 33 ++++++++++------------- Source/Engine/Profiler/ProfilingTools.cpp | 4 ++- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/Source/Engine/Graphics/GPUDevice.cpp b/Source/Engine/Graphics/GPUDevice.cpp index 019cfad5a..34d8e7661 100644 --- a/Source/Engine/Graphics/GPUDevice.cpp +++ b/Source/Engine/Graphics/GPUDevice.cpp @@ -503,6 +503,9 @@ void GPUDevice::DrawEnd() // Call present on all used tasks int32 presentCount = 0; bool anyVSync = false; +#if COMPILE_WITH_PROFILER + const double presentStart = Platform::GetTimeSeconds(); +#endif for (int32 i = 0; i < RenderTask::Tasks.Count(); i++) { const auto task = RenderTask::Tasks[i]; @@ -537,6 +540,10 @@ void GPUDevice::DrawEnd() #endif GetMainContext()->Flush(); } +#if COMPILE_WITH_PROFILER + const double presentEnd = Platform::GetTimeSeconds(); + ProfilerGPU::OnPresentTime((float)((presentEnd - presentStart) * 1000.0)); +#endif _wasVSyncUsed = anyVSync; _isRendering = false; diff --git a/Source/Engine/Profiler/ProfilerGPU.cpp b/Source/Engine/Profiler/ProfilerGPU.cpp index 3054abe67..6944441be 100644 --- a/Source/Engine/Profiler/ProfilerGPU.cpp +++ b/Source/Engine/Profiler/ProfilerGPU.cpp @@ -74,6 +74,7 @@ void ProfilerGPU::EventBuffer::Clear() _data.Clear(); _isResolved = false; FrameIndex = 0; + PresentTime = 0.0f; } GPUTimerQuery* ProfilerGPU::GetTimerQuery() @@ -133,7 +134,9 @@ void ProfilerGPU::BeginFrame() // Clear stats RenderStatsData::Counter = RenderStatsData(); _depth = 0; - Buffers[CurrentBuffer].FrameIndex = Engine::FrameCount; + auto& buffer = Buffers[CurrentBuffer]; + buffer.FrameIndex = Engine::FrameCount; + buffer.PresentTime = 0.0f; // Try to resolve previous frames for (int32 i = 0; i < PROFILER_GPU_EVENTS_FRAMES; i++) @@ -149,6 +152,12 @@ void ProfilerGPU::OnPresent() buffer.EndAll(); } +void ProfilerGPU::OnPresentTime(float time) +{ + auto& buffer = Buffers[CurrentBuffer]; + buffer.PresentTime += time; +} + void ProfilerGPU::EndFrame() { if (_depth) @@ -164,7 +173,7 @@ void ProfilerGPU::EndFrame() buffer.Clear(); } -bool ProfilerGPU::GetLastFrameData(float& drawTimeMs, RenderStatsData& statsData) +bool ProfilerGPU::GetLastFrameData(float& drawTimeMs, float& presentTimeMs, RenderStatsData& statsData) { uint64 maxFrame = 0; int32 maxFrameIndex = -1; @@ -177,17 +186,19 @@ bool ProfilerGPU::GetLastFrameData(float& drawTimeMs, RenderStatsData& statsData maxFrameIndex = i; } } - if (maxFrameIndex != -1) { auto& frame = frames[maxFrameIndex]; const auto root = frame.Get(0); drawTimeMs = root->Time; + presentTimeMs = frame.PresentTime; statsData = root->Stats; return true; } + // No data drawTimeMs = 0.0f; + presentTimeMs = 0.0f; Platform::MemoryClear(&statsData, sizeof(statsData)); return false; } diff --git a/Source/Engine/Profiler/ProfilerGPU.h b/Source/Engine/Profiler/ProfilerGPU.h index fa2c72ab0..94ed345b9 100644 --- a/Source/Engine/Profiler/ProfilerGPU.h +++ b/Source/Engine/Profiler/ProfilerGPU.h @@ -20,6 +20,8 @@ class GPUTimerQuery; API_CLASS(Static) class FLAXENGINE_API ProfilerGPU { DECLARE_SCRIPTING_TYPE_NO_SPAWN(ProfilerGPU); + friend class Engine; + friend class GPUDevice; public: /// /// Represents single CPU profiling event data. @@ -69,6 +71,11 @@ public: /// uint64 FrameIndex; + /// + /// Sum of all present events duration on CPU during this frame (in milliseconds). + /// + float PresentTime; + /// /// Determines whether this buffer has ready data (resolved and not empty). /// @@ -151,32 +158,20 @@ public: /// The event token index returned by the BeginEvent method. static void EndEvent(int32 index); - /// - /// Begins the new frame rendering. Called by the engine to sync profiling data. - /// - static void BeginFrame(); - - /// - /// Called when just before flushing current frame GPU commands (via Present or Flush). Call active timer queries should be ended now. - /// - static void OnPresent(); - - /// - /// Ends the frame rendering. Called by the engine to sync profiling data. - /// - static void EndFrame(); - /// /// Tries to get the rendering stats from the last frame drawing (that has been resolved and has valid data). /// /// The draw execution time on a GPU (in milliseconds). + /// The final frame present time on a CPU (in milliseconds). Time game waited for vsync or GPU to finish previous frame rendering. /// The rendering stats data. /// True if got the data, otherwise false. - static bool GetLastFrameData(float& drawTimeMs, RenderStatsData& statsData); + API_FUNCTION() static bool GetLastFrameData(float& drawTimeMs, float& presentTimeMs, RenderStatsData& statsData); - /// - /// Releases resources. Calls to the profiling API after Dispose are not valid - /// +private: + static void BeginFrame(); + static void OnPresent(); + static void OnPresentTime(float time); + static void EndFrame(); static void Dispose(); }; diff --git a/Source/Engine/Profiler/ProfilingTools.cpp b/Source/Engine/Profiler/ProfilingTools.cpp index 61c0e2c33..113a04ec8 100644 --- a/Source/Engine/Profiler/ProfilingTools.cpp +++ b/Source/Engine/Profiler/ProfilingTools.cpp @@ -48,7 +48,9 @@ void ProfilingToolsService::Update() stats.PhysicsTimeMs = static_cast(Time::Physics.LastLength * 1000.0); stats.DrawCPUTimeMs = static_cast(Time::Draw.LastLength * 1000.0); - ProfilerGPU::GetLastFrameData(stats.DrawGPUTimeMs, stats.DrawStats); + float presentTime; + ProfilerGPU::GetLastFrameData(stats.DrawGPUTimeMs, presentTime, stats.DrawStats); + stats.DrawCPUTimeMs = Math::Max(stats.DrawCPUTimeMs - presentTime, 0.0f); // Remove swapchain present wait time to exclude from drawing on CPU } // Extract CPU profiler events From 036ad570cd0940f1b6ed3f6b8f60db2e6189d77b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Oct 2023 11:20:49 +0200 Subject: [PATCH 060/546] Fix warning when dotnet cache file is missing --- Source/Editor/Cooker/Steps/DeployDataStep.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index 727363846..8306475d1 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -131,7 +131,8 @@ bool DeployDataStep::Perform(CookingData& data) if (FileSystem::DirectoryExists(dstDotnet)) { String cachedData; - File::ReadAllText(dotnetCacheFilePath, cachedData); + if (FileSystem::FileExists(dotnetCacheFilePath)) + File::ReadAllText(dotnetCacheFilePath, cachedData); if (cachedData != dotnetCachedValue) { FileSystem::DeleteDirectory(dstDotnet); From 50d47fe80105fcff567412d733c6ce5369c2ed04 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Oct 2023 16:10:57 +0200 Subject: [PATCH 061/546] Fix asset load to trigger loading task within mutex to prevent race conditions when loading the same prefab from many threads at once --- Source/Engine/Content/Asset.cpp | 6 +----- Source/Engine/Content/Content.cpp | 8 +++++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 309a82e04..613c7c2c2 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -538,11 +538,7 @@ ContentLoadTask* Asset::createLoadingTask() void Asset::startLoading() { - // Check if is already loaded - if (IsLoaded()) - return; - - // Start loading (using async tasks) + ASSERT(!IsLoaded()); ASSERT(_loadingTask == nullptr); _loadingTask = createLoadingTask(); ASSERT(_loadingTask != nullptr); diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index 236c9400b..bd27abc5a 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -154,7 +154,7 @@ void ContentService::LateUpdate() // Unload marked assets for (int32 i = 0; i < ToUnload.Count(); i++) { - Asset* asset = ToUnload[i]; + Asset* asset = ToUnload[i]; // Check if has no references if (asset->GetReferencesCount() <= 0) @@ -965,7 +965,7 @@ Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& // Get cached asset info (from registry) if (!GetAssetInfo(id, assetInfo)) { - LOG(Warning, "Invalid or missing asset ({0}, {1}).", id.ToString(Guid::FormatType::N), type.ToString()); + LOG(Warning, "Invalid or missing asset ({0}, {1}).", id, type.ToString()); return nullptr; } @@ -1009,11 +1009,13 @@ Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& ASSERT(!Assets.ContainsKey(id)); #endif Assets.Add(id, result); - AssetsLocker.Unlock(); // Start asset loading + // TODO: refactor this to create asset loading task-chain before AssetsLocker.Lock() to allow better parallelization result->startLoading(); + AssetsLocker.Unlock(); + return result; } From 30d510f0737c8b74875c79f0549385fc8f872e90 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Oct 2023 16:34:55 +0200 Subject: [PATCH 062/546] Fix models in toolbox --- Source/Engine/Level/Actors/AnimatedModel.h | 2 +- Source/Engine/Level/Actors/StaticModel.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index d070d99a2..0c5c4d73b 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -12,7 +12,7 @@ /// /// Performs an animation and renders a skinned model. /// -API_CLASS(Attributes="ActorContextMenu(\"New/Other/Animated Model\"), ActorToolbox(\"Other\")") +API_CLASS(Attributes="ActorContextMenu(\"New/Other/Animated Model\"), ActorToolbox(\"Visuals\")") class FLAXENGINE_API AnimatedModel : public ModelInstanceActor { DECLARE_SCENE_OBJECT(AnimatedModel); diff --git a/Source/Engine/Level/Actors/StaticModel.h b/Source/Engine/Level/Actors/StaticModel.h index 2e932096e..2f064eb8a 100644 --- a/Source/Engine/Level/Actors/StaticModel.h +++ b/Source/Engine/Level/Actors/StaticModel.h @@ -10,7 +10,8 @@ /// /// Renders model on the screen. /// -API_CLASS(Attributes="ActorContextMenu(\"New/Model\")") class FLAXENGINE_API StaticModel : public ModelInstanceActor +API_CLASS(Attributes="ActorContextMenu(\"New/Model\"), ActorToolbox(\"Visuals\")") +class FLAXENGINE_API StaticModel : public ModelInstanceActor { DECLARE_SCENE_OBJECT(StaticModel); private: From 3e7368b1cb31aedfa37d9cdae1d8612b08e99967 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Oct 2023 16:36:52 +0200 Subject: [PATCH 063/546] Fix `HandleObjectDeserializationError` to be editor or dev only and thread-safe --- Source/Engine/Level/SceneObjectsFactory.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index 7f62d943b..ffaebb15c 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -14,6 +14,9 @@ #include "Engine/Serialization/JsonWriters.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Threading/ThreadLocal.h" +#if !BUILD_RELEASE || USE_EDITOR +#include "Engine/Level/Level.h" +#endif SceneObjectsFactory::Context::Context(ISerializeModifier* modifier) : Modifier(modifier) @@ -254,6 +257,10 @@ void SceneObjectsFactory::Deserialize(Context& context, SceneObject* obj, ISeria void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable::DeserializeStream& value) { +#if !BUILD_RELEASE || USE_EDITOR + // Prevent race-conditions when logging missing objects (especially when adding dummy MissingScript) + ScopeLock lock(Level::ScenesLock); + // Print invalid object data contents rapidjson_flax::StringBuffer buffer; PrettyJsonWriter writer(buffer); @@ -280,6 +287,7 @@ void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable:: LOG(Warning, "Parent actor of the missing object: {0}", parent->GetName()); } } +#endif } Actor* SceneObjectsFactory::CreateActor(int32 typeId, const Guid& id) From 582cf941984bffcd96c09e68a24bfe7b8362b71b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 16 Oct 2023 16:37:44 +0200 Subject: [PATCH 064/546] Fix setting up prefab objects ids mapping for nested prefabs to link cross-object references correctly --- Source/Engine/Level/SceneObjectsFactory.cpp | 19 +++++++++++++++++-- Source/Engine/Level/SceneObjectsFactory.h | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index ffaebb15c..5632d8bc7 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -369,14 +369,14 @@ SceneObjectsFactory::PrefabSyncData::PrefabSyncData(Array& sceneOb { } -void SceneObjectsFactory::SetupPrefabInstances(Context& context, PrefabSyncData& data) +void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyncData& data) { PROFILE_CPU_NAMED("SetupPrefabInstances"); const int32 count = data.Data.Size(); ASSERT(count <= data.SceneObjects.Count()); for (int32 i = 0; i < count; i++) { - SceneObject* obj = data.SceneObjects[i]; + const SceneObject* obj = data.SceneObjects[i]; if (!obj) continue; const auto& stream = data.Data[i]; @@ -417,6 +417,21 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, PrefabSyncData& // Add to the prefab instance IDs mapping auto& prefabInstance = context.Instances[index]; prefabInstance.IdsMapping[prefabObjectId] = id; + + // Walk over nested prefabs to link any subobjects into this object (eg. if nested prefab uses cross-object references to link them correctly) + NESTED_PREFAB_WALK: + const ISerializable::DeserializeStream* prefabData; + if (prefab->ObjectsDataCache.TryGet(prefabObjectId, prefabData) && JsonTools::GetGuidIfValid(prefabObjectId, *prefabData, "PrefabObjectID")) + { + prefabId = JsonTools::GetGuid(stream, "PrefabID"); + prefab = Content::LoadAsync(prefabId); + if (prefab && !prefab->WaitForLoaded()) + { + // Map prefab object ID to the deserialized instance ID + prefabInstance.IdsMapping[prefabObjectId] = id; + goto NESTED_PREFAB_WALK; + } + } } } diff --git a/Source/Engine/Level/SceneObjectsFactory.h b/Source/Engine/Level/SceneObjectsFactory.h index f7f495449..97cf6b9ce 100644 --- a/Source/Engine/Level/SceneObjectsFactory.h +++ b/Source/Engine/Level/SceneObjectsFactory.h @@ -100,7 +100,7 @@ public: /// /// The serialization context. /// The sync data. - static void SetupPrefabInstances(Context& context, PrefabSyncData& data); + static void SetupPrefabInstances(Context& context, const PrefabSyncData& data); /// /// Synchronizes the new prefab instances by spawning missing objects that were added to prefab but were not saved with scene objects collection. From 447030f53ad3e9940fb6062524d6e51ad4009371 Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Mon, 16 Oct 2023 20:19:18 +0200 Subject: [PATCH 065/546] - Spawned get parameter node now reconnects to all boxes from the converted node --- Source/Editor/Surface/Archetypes/Constants.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index e3cbd1b86..0e9049a42 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -381,6 +381,7 @@ namespace FlaxEditor.Surface.Archetypes return; } + // Add parameter to editor var paramIndex = Surface.Parameters.Count; var paramAction = new AddRemoveParamAction { @@ -403,6 +404,26 @@ namespace FlaxEditor.Surface.Archetypes if (node is not Parameters.SurfaceNodeParamsGet getNode) throw new Exception("Node is not a ParamsGet node!"); + // Recreate connections of constant node + // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections + var boxes = GetBoxes(); + for (int i = 0;i < boxes.Count; i++) + { + Box box = boxes[i]; + if (!box.HasAnyConnection) + continue; + + if (!getNode.TryGetBox(i, out Box paramBox)) + continue; + + // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true + for (int k = box.Connections.Count-1; k >= 0; k--) + { + Box connectedBox = box.Connections[k]; + paramBox.CreateConnection(connectedBox); + } + } + var spawnNodeAction = new AddRemoveNodeAction(getNode, true); var removeConstantAction = new AddRemoveNodeAction(this, false); From d92c7af2cf73ffc67e066432cdc210fedf91b3ba Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Oct 2023 09:53:06 +0200 Subject: [PATCH 066/546] Fix debug file access race-conditions when importing many materials at once --- Source/Engine/Content/Assets/Material.cpp | 3 +++ Source/Engine/Particles/ParticleEmitter.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Source/Engine/Content/Assets/Material.cpp b/Source/Engine/Content/Assets/Material.cpp index 76ed34b80..276b6b1f7 100644 --- a/Source/Engine/Content/Assets/Material.cpp +++ b/Source/Engine/Content/Assets/Material.cpp @@ -20,6 +20,7 @@ #include "Engine/ShadersCompilation/Config.h" #if BUILD_DEBUG #include "Engine/Engine/Globals.h" +#include "Engine/Scripting/BinaryModule.h" #endif #endif @@ -256,7 +257,9 @@ Asset::LoadResult Material::load() #if BUILD_DEBUG && USE_EDITOR // Dump generated material source to the temporary file + BinaryModule::Locker.Lock(); source.SaveToFile(Globals::ProjectCacheFolder / TEXT("material.txt")); + BinaryModule::Locker.Unlock(); #endif // Encrypt source code diff --git a/Source/Engine/Particles/ParticleEmitter.cpp b/Source/Engine/Particles/ParticleEmitter.cpp index 20e4c0490..ea1161c6b 100644 --- a/Source/Engine/Particles/ParticleEmitter.cpp +++ b/Source/Engine/Particles/ParticleEmitter.cpp @@ -19,6 +19,7 @@ #include "Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h" #if BUILD_DEBUG #include "Engine/Engine/Globals.h" +#include "Engine/Scripting/BinaryModule.h" #endif #endif #if COMPILE_WITH_GPU_PARTICLES @@ -185,7 +186,9 @@ Asset::LoadResult ParticleEmitter::load() #if BUILD_DEBUG && USE_EDITOR // Dump generated shader source to the temporary file + BinaryModule::Locker.Lock(); source.SaveToFile(Globals::ProjectCacheFolder / TEXT("particle_emitter.txt")); + BinaryModule::Locker.Unlock(); #endif // Encrypt source code From cebae5c4e159a1963f371962cd515886b8b2e995 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Oct 2023 09:53:48 +0200 Subject: [PATCH 067/546] Add `foreach` loop support to `Span` type --- Source/Engine/Core/Types/Span.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Source/Engine/Core/Types/Span.h b/Source/Engine/Core/Types/Span.h index e0518ec77..b51c858ec 100644 --- a/Source/Engine/Core/Types/Span.h +++ b/Source/Engine/Core/Types/Span.h @@ -107,6 +107,26 @@ public: ASSERT(index >= 0 && index < _length); return _data[index]; } + + FORCE_INLINE T* begin() + { + return _data; + } + + FORCE_INLINE T* end() + { + return _data + _length; + } + + FORCE_INLINE const T* begin() const + { + return _data; + } + + FORCE_INLINE const T* end() const + { + return _data + _length; + } }; template From 90427da42f84019e8d3aaa3091fb5a4ce4177e36 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Oct 2023 09:59:21 +0200 Subject: [PATCH 068/546] Add `SubAssetFolder` to model import option to redirect auto-imported materials and textures location --- Source/Engine/ContentImporters/ImportModelFile.cpp | 3 ++- Source/Engine/Tools/ModelTool/ModelTool.cpp | 2 ++ Source/Engine/Tools/ModelTool/ModelTool.h | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModelFile.cpp index b9a4f5675..530769e49 100644 --- a/Source/Engine/ContentImporters/ImportModelFile.cpp +++ b/Source/Engine/ContentImporters/ImportModelFile.cpp @@ -124,7 +124,8 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) // Import model file ModelData modelData; String errorMsg; - String autoImportOutput = String(StringUtils::GetDirectoryName(context.TargetAssetPath)) / StringUtils::GetFileNameWithoutExtension(context.InputPath); + String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath)); + autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : StringUtils::GetFileNameWithoutExtension(context.InputPath); if (ModelTool::ImportModel(context.InputPath, modelData, options, errorMsg, autoImportOutput)) { LOG(Error, "Cannot import model file. {0}", errorMsg); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index b703b47d1..f026eaf95 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -396,6 +396,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(SDFResolution); SERIALIZE(SplitObjects); SERIALIZE(ObjectIndex); + SERIALIZE(SubAssetFolder); } void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) @@ -441,6 +442,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(SDFResolution); DESERIALIZE(SplitObjects); DESERIALIZE(ObjectIndex); + DESERIALIZE(SubAssetFolder); // [Deprecated on 23.11.2021, expires on 21.11.2023] int32 AnimationIndex = -1; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index 36bb9b12c..bad10b86e 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -370,6 +370,12 @@ public: API_FIELD(Attributes="EditorOrder(2010), EditorDisplay(\"Splitting\")") int32 ObjectIndex = -1; + public: // Other + + // If specified, will be used as sub-directory name for automatically imported sub assets such as textures and materials. Set to whitespace (single space) to import to the same directory. + API_FIELD(Attributes="EditorOrder(3030), EditorDisplay(\"Other\")") + String SubAssetFolder = TEXT(""); + // Runtime data for objects splitting during import (used internally) void* SplitContext = nullptr; Function OnSplitImport; From 8d9f4d72f563ebfc406aa81477c00b094754dcd2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Oct 2023 22:42:47 +0200 Subject: [PATCH 069/546] Various stability improvements for Editor --- Source/Editor/Content/Proxy/SceneProxy.cs | 6 ++ .../Content/Thumbnails/ThumbnailsModule.cs | 10 ++- .../Editor/Modules/ContentDatabaseModule.cs | 61 +++++++++++++------ Source/Editor/Surface/SurfaceUtils.cs | 2 + .../Windows/Assets/AssetEditorWindow.cs | 6 +- .../Windows/Assets/MaterialInstanceWindow.cs | 7 ++- 6 files changed, 63 insertions(+), 29 deletions(-) diff --git a/Source/Editor/Content/Proxy/SceneProxy.cs b/Source/Editor/Content/Proxy/SceneProxy.cs index 78d88b440..004c2aed7 100644 --- a/Source/Editor/Content/Proxy/SceneProxy.cs +++ b/Source/Editor/Content/Proxy/SceneProxy.cs @@ -30,6 +30,12 @@ namespace FlaxEditor.Content return item is SceneItem; } + /// + public override bool AcceptsAsset(string typeName, string path) + { + return (typeName == Scene.AssetTypename || typeName == Scene.EditorPickerTypename) && path.EndsWith(FileExtension, StringComparison.OrdinalIgnoreCase); + } + /// public override bool CanCreate(ContentFolder targetLocation) { diff --git a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs index 160c783ed..1282e4daa 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs @@ -406,18 +406,16 @@ namespace FlaxEditor.Content.Thumbnails for (int i = 0; i < maxChecks; i++) { var request = _requests[i]; - try { if (request.IsReady) - { return request; - } } catch (Exception ex) { - Editor.LogWarning(ex); Editor.LogWarning($"Failed to prepare thumbnail rendering for {request.Item.ShortName}."); + Editor.LogWarning(ex); + _requests.RemoveAt(i--); } } @@ -515,7 +513,6 @@ namespace FlaxEditor.Content.Thumbnails for (int i = 0; i < checks; i++) { var request = _requests[i]; - try { if (request.IsReady) @@ -529,8 +526,9 @@ namespace FlaxEditor.Content.Thumbnails } catch (Exception ex) { - Editor.LogWarning(ex); Editor.LogWarning($"Failed to prepare thumbnail rendering for {request.Item.ShortName}."); + Editor.LogWarning(ex); + _requests.RemoveAt(i--); } } diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index b1de78456..80f419a6f 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -811,10 +811,9 @@ namespace FlaxEditor.Modules { if (node == null) return; - - // Temporary data var folder = node.Folder; var path = folder.Path; + var canHaveAssets = node.CanHaveAssets; if (_isDuringFastSetup) { @@ -833,20 +832,38 @@ namespace FlaxEditor.Modules var child = folder.Children[i]; if (!child.Exists) { - // Send info + // Item doesn't exist anymore Editor.Log(string.Format($"Content item \'{child.Path}\' has been removed")); - - // Destroy it Delete(child, false); - i--; } + else if (canHaveAssets && child is AssetItem childAsset) + { + // Check if asset type doesn't match the item proxy (eg. item reimported as Material Instance instead of Material) + if (FlaxEngine.Content.GetAssetInfo(child.Path, out var assetInfo)) + { + bool changed = assetInfo.ID != childAsset.ID; + if (!changed && assetInfo.TypeName != childAsset.TypeName) + { + // Use proxy check (eg. scene asset might accept different typename than AssetInfo reports) + var proxy = GetAssetProxy(childAsset.TypeName, child.Path); + if (proxy == null) + proxy = GetAssetProxy(assetInfo.TypeName, child.Path); + changed = !proxy.AcceptsAsset(assetInfo.TypeName, child.Path); + } + if (changed) + { + OnAssetTypeInfoChanged(childAsset, ref assetInfo); + i--; + } + } + } } } // Find files var files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly); - if (node.CanHaveAssets) + if (canHaveAssets) { LoadAssets(node, files); } @@ -1157,19 +1174,8 @@ namespace FlaxEditor.Modules // For eg. change texture to sprite atlas on reimport if (binaryAssetItem.TypeName != assetInfo.TypeName) { - // Asset type has been changed! - Editor.LogWarning(string.Format("Asset \'{0}\' changed type from {1} to {2}", item.Path, binaryAssetItem.TypeName, assetInfo.TypeName)); - Editor.Windows.CloseAllEditors(item); - - // Remove this item from the database and some related data var toRefresh = binaryAssetItem.ParentFolder; - binaryAssetItem.Dispose(); - toRefresh.Children.Remove(binaryAssetItem); - if (!binaryAssetItem.HasDefaultThumbnail) - { - // Delete old thumbnail and remove it from the cache - Editor.Instance.Thumbnails.DeletePreview(binaryAssetItem); - } + OnAssetTypeInfoChanged(binaryAssetItem, ref assetInfo); // Refresh the parent folder to find the new asset (it should have different type or some other format) RefreshFolder(toRefresh, false); @@ -1186,6 +1192,23 @@ namespace FlaxEditor.Modules } } + private void OnAssetTypeInfoChanged(AssetItem assetItem, ref AssetInfo assetInfo) + { + // Asset type has been changed! + Editor.LogWarning(string.Format("Asset \'{0}\' changed type from {1} to {2}", assetItem.Path, assetItem.TypeName, assetInfo.TypeName)); + Editor.Windows.CloseAllEditors(assetItem); + + // Remove this item from the database and some related data + assetItem.Dispose(); + assetItem.ParentFolder.Children.Remove(assetItem); + + // Delete old thumbnail and remove it from the cache + if (!assetItem.HasDefaultThumbnail) + { + Editor.Instance.Thumbnails.DeletePreview(assetItem); + } + } + internal void OnDirectoryEvent(MainContentTreeNode node, FileSystemEventArgs e) { // Ensure to be ready for external events diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs index 5f4c3ef07..6fc54545c 100644 --- a/Source/Editor/Surface/SurfaceUtils.cs +++ b/Source/Editor/Surface/SurfaceUtils.cs @@ -295,6 +295,8 @@ namespace FlaxEditor.Surface continue; var tag = e.Tag; var parameter = e.Parameter; + if (parameter == null) + continue; // Editor Display var itemLayout = CustomEditors.Editors.GenericEditor.OnGroup(layout, e.Display); diff --git a/Source/Editor/Windows/Assets/AssetEditorWindow.cs b/Source/Editor/Windows/Assets/AssetEditorWindow.cs index 0d244479c..2e048b924 100644 --- a/Source/Editor/Windows/Assets/AssetEditorWindow.cs +++ b/Source/Editor/Windows/Assets/AssetEditorWindow.cs @@ -388,14 +388,16 @@ namespace FlaxEditor.Windows.Assets protected override void OnShow() { // Check if has no asset (but has item linked) - if (_asset == null && _item != null) + var item = _item; + if (_asset == null && item != null) { // Load asset _asset = LoadAsset(); if (_asset == null) { - Editor.LogError(string.Format("Cannot load asset \'{0}\' ({1})", _item.Path, typeof(T))); + Editor.LogError(string.Format("Cannot load asset \'{0}\' ({1})", item.Path, typeof(T))); Close(); + Editor.ContentDatabase.RefreshFolder(item, false); return; } diff --git a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs index 5f1273999..3e0525fb7 100644 --- a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs +++ b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs @@ -521,8 +521,11 @@ namespace FlaxEditor.Windows.Assets /// protected override void OnClose() { - // Discard unsaved changes - _properties.DiscardChanges(); + if (Asset) + { + // Discard unsaved changes + _properties.DiscardChanges(); + } // Cleanup _undo.Clear(); From 1cbf867fc79b8f29f81152ccba6b2247321b481c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Oct 2023 22:43:43 +0200 Subject: [PATCH 070/546] Hide `build scenes data` toolbar button when none action is set and fix tooltip for `cook and run` action #1731 --- Source/Editor/Modules/UIModule.cs | 11 ++++++----- Source/Editor/Options/GeneralOptions.cs | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 492d78918..1369ecd1e 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -2,7 +2,6 @@ using System; using System.IO; -using System.Linq; using System.Collections.Generic; using FlaxEditor.Gizmo; using FlaxEditor.GUI; @@ -11,7 +10,6 @@ using FlaxEditor.GUI.Dialogs; using FlaxEditor.GUI.Input; using FlaxEditor.Progress.Handlers; using FlaxEditor.SceneGraph; -using FlaxEditor.SceneGraph.Actors; using FlaxEditor.Utilities; using FlaxEditor.Viewport.Cameras; using FlaxEditor.Windows; @@ -208,6 +206,7 @@ namespace FlaxEditor.Modules _toolStripScale.Checked = gizmoMode == TransformGizmoBase.Mode.Scale; // _toolStripBuildScenes.Enabled = (canEditScene && !isPlayMode) || Editor.StateMachine.BuildingScenesState.IsActive; + _toolStripBuildScenes.Visible = Editor.Options.Options.General.BuildActions?.Length != 0; _toolStripCook.Enabled = Editor.Windows.GameCookerWin.CanBuild(Platform.PlatformType) && !GameCooker.IsRunning; // var play = _toolStripPlay; @@ -653,7 +652,7 @@ namespace FlaxEditor.Modules cm.AddButton("Information about Flax", () => new AboutDialog().Show()); } - private void OnOptionsChanged(FlaxEditor.Options.EditorOptions options) + private void OnOptionsChanged(EditorOptions options) { var inputOptions = options.Input; @@ -688,6 +687,8 @@ namespace FlaxEditor.Modules _menuToolsTakeScreenshot.ShortKeys = inputOptions.TakeScreenshot.ToString(); MainMenuShortcutKeysUpdated?.Invoke(); + + UpdateToolstrip(); } private void InitToolstrip(RootControl mainWindow) @@ -709,11 +710,11 @@ namespace FlaxEditor.Modules _toolStripScale = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Scale32, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip($"Change Gizmo tool mode to Scale ({inputOptions.ScaleMode})"); ToolStrip.AddSeparator(); - // Cook scenes + // Build scenes _toolStripBuildScenes = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Build64, Editor.BuildScenesOrCancel).LinkTooltip($"Build scenes data - CSG, navmesh, static lighting, env probes - configurable via Build Actions in editor options ({inputOptions.BuildScenesData})"); // Cook and run - _toolStripCook = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.ShipIt64, Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip($"Cook & Run - build game for the current platform and run it locally ({inputOptions.Play})"); + _toolStripCook = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.ShipIt64, Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip($"Cook & Run - build game for the current platform and run it locally ({inputOptions.CookAndRun})"); _toolStripCook.ContextMenu = new ContextMenu(); _toolStripCook.ContextMenu.AddButton("Run cooked game", Editor.Windows.GameCookerWin.RunCooked); _toolStripCook.ContextMenu.AddSeparator(); diff --git a/Source/Editor/Options/GeneralOptions.cs b/Source/Editor/Options/GeneralOptions.cs index 33124bab0..9f29f4bdb 100644 --- a/Source/Editor/Options/GeneralOptions.cs +++ b/Source/Editor/Options/GeneralOptions.cs @@ -117,7 +117,7 @@ namespace FlaxEditor.Options /// /// Gets or sets the sequence of actions to perform when using Build Scenes button. Can be used to configure this as button (eg. compile code or just update navmesh). /// - [EditorDisplay("General"), EditorOrder(200), Tooltip("The sequence of actions to perform when using Build Scenes button. Can be used to configure this as button (eg. compile code or just update navmesh).")] + [EditorDisplay("General"), EditorOrder(200), ExpandGroups, Tooltip("The sequence of actions to perform when using Build Scenes button. Can be used to configure this as button (eg. compile code or just update navmesh).")] public BuildAction[] BuildActions { get; set; } = { BuildAction.CSG, From c457087612f34e8ce3f8b804b7f72a9b3fd52d11 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Oct 2023 22:44:15 +0200 Subject: [PATCH 071/546] Optimize memory allocation when using `Sprite` size during 2D rendering --- Source/Engine/Render2D/SpriteAtlas.cpp | 8 +++++++- Source/Engine/Render2D/SpriteAtlas.cs | 11 +++++++++-- Source/Engine/Render2D/SpriteAtlas.h | 8 ++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Render2D/SpriteAtlas.cpp b/Source/Engine/Render2D/SpriteAtlas.cpp index 0e0aa071f..ac711b8b8 100644 --- a/Source/Engine/Render2D/SpriteAtlas.cpp +++ b/Source/Engine/Render2D/SpriteAtlas.cpp @@ -47,10 +47,16 @@ int32 SpriteAtlas::GetSpritesCount() const Sprite SpriteAtlas::GetSprite(int32 index) const { - CHECK_RETURN(index >= 0 && index < Sprites.Count(), Sprite()) + CHECK_RETURN(index >= 0 && index < Sprites.Count(), Sprite()); return Sprites.Get()[index]; } +void SpriteAtlas::GetSpriteArea(int32 index, Rectangle& result) const +{ + CHECK(index >= 0 && index < Sprites.Count()); + result = Sprites.Get()[index].Area; +} + void SpriteAtlas::SetSprite(int32 index, const Sprite& value) { CHECK(index >= 0 && index < Sprites.Count()); diff --git a/Source/Engine/Render2D/SpriteAtlas.cs b/Source/Engine/Render2D/SpriteAtlas.cs index b3796ffd3..93cdf68b2 100644 --- a/Source/Engine/Render2D/SpriteAtlas.cs +++ b/Source/Engine/Render2D/SpriteAtlas.cs @@ -70,7 +70,13 @@ namespace FlaxEngine [NoSerialize] public Float2 Size { - get => Area.Size * Atlas.Size; + get + { + if (Atlas == null) + throw new InvalidOperationException("Cannot use invalid sprite."); + Atlas.GetSpriteArea(Index, out var area); + return area.Size * Atlas.Size; + } set { var area = Area; @@ -89,7 +95,8 @@ namespace FlaxEngine { if (Atlas == null) throw new InvalidOperationException("Cannot use invalid sprite."); - return Atlas.GetSprite(Index).Area; + Atlas.GetSpriteArea(Index, out var area); + return area; } set { diff --git a/Source/Engine/Render2D/SpriteAtlas.h b/Source/Engine/Render2D/SpriteAtlas.h index 6fcda5162..533440c68 100644 --- a/Source/Engine/Render2D/SpriteAtlas.h +++ b/Source/Engine/Render2D/SpriteAtlas.h @@ -120,6 +120,14 @@ public: /// The sprite data. API_FUNCTION() Sprite GetSprite(int32 index) const; + /// + /// Gets the sprite area. + /// + /// The index. + /// The output sprite area. + /// The sprite data. + API_FUNCTION() void GetSpriteArea(int32 index, API_PARAM(Out) Rectangle& result) const; + /// /// Sets the sprite data. /// From 52a621de4284c0f3cd004aade56f23660318ab1e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Oct 2023 22:45:22 +0200 Subject: [PATCH 072/546] Add color, emissive and opacity parameters setup for auto-imported material instances --- Source/Engine/Tools/ModelTool/ModelTool.cpp | 38 ++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index f026eaf95..116f63407 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -22,6 +22,7 @@ #include "Engine/Core/Types/DateTime.h" #include "Engine/Core/Types/TimeSpan.h" #include "Engine/Core/Types/Pair.h" +#include "Engine/Core/Types/Variant.h" #include "Engine/Graphics/Models/SkeletonUpdater.h" #include "Engine/Graphics/Models/SkeletonMapping.h" #include "Engine/Core/Utilities.h" @@ -746,6 +747,32 @@ void MeshOptDeallocate(void* ptr) Allocator::Free(ptr); } +void TrySetupMaterialParameter(MaterialInstance* instance, Span paramNames, const Variant& value, MaterialParameterType type) +{ + for (const Char* name : paramNames) + { + for (MaterialParameter& param : instance->Params) + { + const MaterialParameterType paramType = param.GetParameterType(); + if (type != paramType) + { + if (type == MaterialParameterType::Color) + { + if (paramType != MaterialParameterType::Vector3 || + paramType != MaterialParameterType::Vector4) + continue; + } + else + continue; + } + if (StringUtils::CompareIgnoreCase(name, param.GetName().Get()) != 0) + continue; + param.SetValue(value); + return; + } + } +} + bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& options, String& errorMsg, const String& autoImportOutput) { LOG(Info, "Importing model from \'{0}\'", path); @@ -1016,9 +1043,18 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op { // Create material instance AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialInstanceTag, assetPath, material.AssetID); - if (MaterialInstance* materialInstance = Content::Load(assetPath)) + if (auto* materialInstance = Content::Load(assetPath)) { materialInstance->SetBaseMaterial(options.InstanceToImportAs); + + // Customize base material based on imported material (blind guess based on the common names used in materials) + const Char* diffuseColorNames[] = { TEXT("color"), TEXT("col"), TEXT("diffuse"), TEXT("basecolor"), TEXT("base color") }; + TrySetupMaterialParameter(materialInstance, ToSpan(diffuseColorNames, ARRAY_COUNT(diffuseColorNames)), material.Diffuse.Color, MaterialParameterType::Color); + const Char* emissiveColorNames[] = { TEXT("emissive"), TEXT("emission"), TEXT("light") }; + TrySetupMaterialParameter(materialInstance, ToSpan(emissiveColorNames, ARRAY_COUNT(emissiveColorNames)), material.Emissive.Color, MaterialParameterType::Color); + const Char* opacityValueNames[] = { TEXT("opacity"), TEXT("alpha") }; + TrySetupMaterialParameter(materialInstance, ToSpan(opacityValueNames, ARRAY_COUNT(opacityValueNames)), material.Opacity.Value, MaterialParameterType::Float); + materialInstance->Save(); } else From 387e30a1dc5814dd50ee80309f52d7a4be7decad Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Oct 2023 23:13:05 +0200 Subject: [PATCH 073/546] Fix compilation regression --- Source/Engine/ContentImporters/ImportModelFile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModelFile.cpp index 530769e49..b34bbfaaf 100644 --- a/Source/Engine/ContentImporters/ImportModelFile.cpp +++ b/Source/Engine/ContentImporters/ImportModelFile.cpp @@ -125,7 +125,7 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) ModelData modelData; String errorMsg; String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath)); - autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : StringUtils::GetFileNameWithoutExtension(context.InputPath); + autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath)); if (ModelTool::ImportModel(context.InputPath, modelData, options, errorMsg, autoImportOutput)) { LOG(Error, "Cannot import model file. {0}", errorMsg); From 2d0eabc8be5b6fbdf6411a0153ef2690d29aa926 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 17 Oct 2023 23:20:30 +0200 Subject: [PATCH 074/546] Fix deadlock in `Foliage::GetFoliageTypeInstancesCount` #1724 --- Source/Engine/Core/Collections/ChunkedArray.h | 12 ++++-------- Source/Engine/Foliage/Foliage.cpp | 5 +---- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/Source/Engine/Core/Collections/ChunkedArray.h b/Source/Engine/Core/Collections/ChunkedArray.h index 38bf92fb8..86aa9acd9 100644 --- a/Source/Engine/Core/Collections/ChunkedArray.h +++ b/Source/Engine/Core/Collections/ChunkedArray.h @@ -133,18 +133,16 @@ public: return _chunkIndex * ChunkSize + _index; } - public: - bool IsEnd() const + FORCE_INLINE bool IsEnd() const { - return Index() == _collection->Count(); + return (_chunkIndex * ChunkSize + _index) == _collection->_count; } - bool IsNotEnd() const + FORCE_INLINE bool IsNotEnd() const { - return Index() != _collection->Count(); + return (_chunkIndex * ChunkSize + _index) != _collection->_count; } - public: FORCE_INLINE T& operator*() const { return _collection->_chunks[_chunkIndex]->At(_index); @@ -155,7 +153,6 @@ public: return &_collection->_chunks[_chunkIndex]->At(_index); } - public: FORCE_INLINE bool operator==(const Iterator& v) const { return _collection == v._collection && _chunkIndex == v._chunkIndex && _index == v._index; @@ -166,7 +163,6 @@ public: return _collection != v._collection || _chunkIndex != v._chunkIndex || _index != v._index; } - public: Iterator& operator++() { // Check if it is not at end diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index 82765f151..898139ce7 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -634,15 +634,12 @@ void Foliage::RemoveFoliageType(int32 index) int32 Foliage::GetFoliageTypeInstancesCount(int32 index) const { PROFILE_CPU(); - int32 result = 0; - - for (auto i = Instances.Begin(); i.IsNotEnd(); i++) + for (auto i = Instances.Begin(); i.IsNotEnd(); ++i) { if (i->Type == index) result++; } - return result; } From 8b096a9286cc8e9585cbd751a1a85234fdc7cfcb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 18 Oct 2023 10:52:36 +0200 Subject: [PATCH 075/546] Fixes --- Source/Editor/Surface/SurfaceUtils.cs | 2 - Source/Engine/Core/Collections/ChunkedArray.h | 59 ++++++++++++++----- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs index 6fc54545c..5f4c3ef07 100644 --- a/Source/Editor/Surface/SurfaceUtils.cs +++ b/Source/Editor/Surface/SurfaceUtils.cs @@ -295,8 +295,6 @@ namespace FlaxEditor.Surface continue; var tag = e.Tag; var parameter = e.Parameter; - if (parameter == null) - continue; // Editor Display var itemLayout = CustomEditors.Editors.GenericEditor.OnGroup(layout, e.Display); diff --git a/Source/Engine/Core/Collections/ChunkedArray.h b/Source/Engine/Core/Collections/ChunkedArray.h index 86aa9acd9..b3765a9fe 100644 --- a/Source/Engine/Core/Collections/ChunkedArray.h +++ b/Source/Engine/Core/Collections/ChunkedArray.h @@ -100,7 +100,7 @@ public: int32 _chunkIndex; int32 _index; - Iterator(ChunkedArray const* collection, const int32 index) + Iterator(const ChunkedArray* collection, const int32 index) : _collection(const_cast(collection)) , _chunkIndex(index / ChunkSize) , _index(index % ChunkSize) @@ -122,12 +122,14 @@ public: { } - public: - FORCE_INLINE ChunkedArray* GetChunkedArray() const + Iterator(Iterator&& i) + : _collection(i._collection) + , _chunkIndex(i._chunkIndex) + , _index(i._index) { - return _collection; } + public: FORCE_INLINE int32 Index() const { return _chunkIndex * ChunkSize + _index; @@ -163,16 +165,22 @@ public: return _collection != v._collection || _chunkIndex != v._chunkIndex || _index != v._index; } + Iterator& operator=(const Iterator& v) + { + _collection = v._collection; + _chunkIndex = v._chunkIndex; + _index = v._index; + return *this; + } + Iterator& operator++() { // Check if it is not at end - const int32 end = _collection->Count(); - if (Index() != end) + if ((_chunkIndex * ChunkSize + _index) != _collection->_count) { // Move forward within chunk _index++; - // Check if need to change chunk if (_index == ChunkSize && _chunkIndex < _collection->_chunks.Count() - 1) { // Move to next chunk @@ -185,9 +193,9 @@ public: Iterator operator++(int) { - Iterator temp = *this; - ++temp; - return temp; + Iterator i = *this; + ++i; + return i; } Iterator& operator--() @@ -195,7 +203,6 @@ public: // Check if it's not at beginning if (_index != 0 || _chunkIndex != 0) { - // Check if need to change chunk if (_index == 0) { // Move to previous chunk @@ -213,9 +220,9 @@ public: Iterator operator--(int) { - Iterator temp = *this; - --temp; - return temp; + Iterator i = *this; + --i; + return i; } }; @@ -290,7 +297,7 @@ public: { if (IsEmpty()) return; - ASSERT(i.GetChunkedArray() == this); + ASSERT(i._collection == this); ASSERT(i._chunkIndex < _chunks.Count() && i._index < ChunkSize); ASSERT(i.Index() < Count()); @@ -428,11 +435,31 @@ public: Iterator End() const { - return Iterator(this, Count()); + return Iterator(this, _count); } Iterator IteratorAt(int32 index) const { return Iterator(this, index); } + + FORCE_INLINE Iterator begin() + { + return Iterator(this, 0); + } + + FORCE_INLINE Iterator end() + { + return Iterator(this, _count); + } + + FORCE_INLINE const Iterator begin() const + { + return Iterator(this, 0); + } + + FORCE_INLINE const Iterator end() const + { + return Iterator(this, _count); + } }; From 24790b18cee93db7e61243bf5b939588ea9e2cc6 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 18 Oct 2023 12:56:48 +0200 Subject: [PATCH 076/546] Fix typo in shader --- Content/Shaders/GlobalSignDistanceField.flax | 4 ++-- Source/Shaders/GlobalSignDistanceField.shader | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Content/Shaders/GlobalSignDistanceField.flax b/Content/Shaders/GlobalSignDistanceField.flax index 18f653f86..0e94c5c06 100644 --- a/Content/Shaders/GlobalSignDistanceField.flax +++ b/Content/Shaders/GlobalSignDistanceField.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c8aa181a814d69b15ffec6493a71a6f42ae816ce04f7803cff2d5073b4b3c4f -size 11790 +oid sha256:e075583620e62407503c73f52487c204ddcad421e80fa621aebd55af1cfb08d5 +size 11798 diff --git a/Source/Shaders/GlobalSignDistanceField.shader b/Source/Shaders/GlobalSignDistanceField.shader index 3d4f7dd46..bc4f272fc 100644 --- a/Source/Shaders/GlobalSignDistanceField.shader +++ b/Source/Shaders/GlobalSignDistanceField.shader @@ -211,7 +211,7 @@ float SampleSDF(uint3 voxelCoordMip, int3 offset) float result = GlobalSDFTex[voxelCoordMip].r; // Extend by distance to the sampled texel location - float distanceInWorldUnits = length(offset) * (MaxDistance / (float)GenerateMipTexResolution); + float distanceInWorldUnits = length((float3)offset) * (MaxDistance / (float)GenerateMipTexResolution); float distanceToVoxel = distanceInWorldUnits / MaxDistance; result = CombineDistanceToSDF(result, distanceToVoxel); From 55ad5ae367a094105b2dec06e43433cf70593a1d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 18 Oct 2023 16:17:19 +0200 Subject: [PATCH 077/546] Build Editor bindings only when generating editor project files #1734 #1569 --- GenerateProjectFiles.bat | 2 +- GenerateProjectFiles.command | 2 +- GenerateProjectFiles.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GenerateProjectFiles.bat b/GenerateProjectFiles.bat index 622939c34..28970a203 100644 --- a/GenerateProjectFiles.bat +++ b/GenerateProjectFiles.bat @@ -15,7 +15,7 @@ if errorlevel 1 goto BuildToolFailed :: Build bindings for all editor configurations echo Building C# bindings... -Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor,FlaxGame +Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor popd echo Done! diff --git a/GenerateProjectFiles.command b/GenerateProjectFiles.command index 5ee5c0783..a42121252 100755 --- a/GenerateProjectFiles.command +++ b/GenerateProjectFiles.command @@ -14,4 +14,4 @@ bash ./Development/Scripts/Mac/CallBuildTool.sh --genproject "$@" # Build bindings for all editor configurations echo Building C# bindings... # TODO: Detect the correct architecture here -Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor,FlaxGame +Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor diff --git a/GenerateProjectFiles.sh b/GenerateProjectFiles.sh index dceb8abe8..76d96c7ef 100755 --- a/GenerateProjectFiles.sh +++ b/GenerateProjectFiles.sh @@ -14,4 +14,4 @@ bash ./Development/Scripts/Linux/CallBuildTool.sh --genproject "$@" # Build bindings for all editor configurations echo Building C# bindings... # TODO: Detect the correct architecture here -Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor,FlaxGame +Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor From 5181db8a0e2f057be0051b3d107516c002e58fb7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 18 Oct 2023 21:51:36 +0200 Subject: [PATCH 078/546] Fix vscode default build task to favor current architecture (eg. arm64 for Editor on macOS) --- .../VisualStudioCode/VisualStudioCodeProjectGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs index a044adc0a..5c4b2dbe0 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs @@ -172,7 +172,7 @@ namespace Flax.Build.Projects.VisualStudioCode json.AddField("label", name); - bool isDefaultTask = defaultTask && configuration.Configuration == TargetConfiguration.Development && configuration.Platform == Platform.BuildPlatform.Target; + bool isDefaultTask = defaultTask && configuration.Configuration == TargetConfiguration.Development && configuration.Platform == Platform.BuildPlatform.Target && configuration.Architecture == Platform.BuildTargetArchitecture; json.BeginObject("group"); { From f373c867a734dfaa36d9dbd627be73966b3938f7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 18 Oct 2023 21:57:35 +0200 Subject: [PATCH 079/546] Improve Game Cooker label text when platform data is missing --- Source/Editor/Windows/GameCookerWindow.cs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Source/Editor/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs index 5ece067a0..2ef9c05cf 100644 --- a/Source/Editor/Windows/GameCookerWindow.cs +++ b/Source/Editor/Windows/GameCookerWindow.cs @@ -155,29 +155,42 @@ namespace FlaxEditor.Windows public virtual void OnNotAvailableLayout(LayoutElementsContainer layout) { - layout.Label("Missing platform data tools for the target platform.", TextAlignment.Center); + string text = "Missing platform data tools for the target platform."; if (FlaxEditor.Editor.IsOfficialBuild()) { switch (BuildPlatform) { +#if PLATFORM_WINDOWS case BuildPlatform.Windows32: case BuildPlatform.Windows64: case BuildPlatform.UWPx86: case BuildPlatform.UWPx64: case BuildPlatform.LinuxX64: case BuildPlatform.AndroidARM64: - layout.Label("Use Flax Launcher and download the required package.", TextAlignment.Center); + text += "\nUse Flax Launcher and download the required package."; break; +#endif default: - layout.Label("Engine source is required to target this platform.", TextAlignment.Center); + text += "\nEngine source is required to target this platform."; break; } } else { - var label = layout.Label("To target this platform separate engine source package is required.\nTo get access please contact via https://flaxengine.com/contact", TextAlignment.Center); - label.Label.AutoHeight = true; + text += "\nTo target this platform separate engine source package is required."; + switch (BuildPlatform) + { + case BuildPlatform.XboxOne: + case BuildPlatform.XboxScarlett: + case BuildPlatform.PS4: + case BuildPlatform.PS5: + case BuildPlatform.Switch: + text += "\nTo get access please contact via https://flaxengine.com/contact"; + break; + } } + var label = layout.Label(text, TextAlignment.Center); + label.Label.AutoHeight = true; } public virtual void Build() From 6c45141ef9d89b2ef654500d37906d05582cc8cb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 18 Oct 2023 22:10:46 +0200 Subject: [PATCH 080/546] Add dmg file building for macOS Editor deployment --- Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index 91da6d83d..cfb2446c3 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -164,6 +164,15 @@ namespace Flax.Deploy var defaultEditorConfig = "Development"; var ediotrBinariesPath = Path.Combine(appContentsPath, "Binaries/Editor/Mac", defaultEditorConfig); Utilities.DirectoryCopy(ediotrBinariesPath, appBinariesPath, true, true); + + // Build a disk image + var dmgPath = Path.Combine(Deployer.PackageOutputPath, "FlaxEditor.dmg"); + Log.Info("Building disk image..."); + if (File.Exists(dmgPath)) + File.Delete(dmgPath); + Utilities.Run("hdiutil", $"create -srcFolder \"{appPath}\" -o \"{dmgPath}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); + CodeSign(dmgPath); + Log.Info("Output disk image size: " + Utilities.GetFileSize(dmgPath)); } // Compress From b2ba40b0825e5c6c2f4be693bda27151a4ff32ef Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Wed, 18 Oct 2023 20:34:39 -0400 Subject: [PATCH 081/546] Add button to reload scenes --- Source/Editor/Modules/SceneModule.cs | 13 +++++++++++++ Source/Editor/Modules/UIModule.cs | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/Source/Editor/Modules/SceneModule.cs b/Source/Editor/Modules/SceneModule.cs index 7789eb7c4..4fb28ac76 100644 --- a/Source/Editor/Modules/SceneModule.cs +++ b/Source/Editor/Modules/SceneModule.cs @@ -266,6 +266,19 @@ namespace FlaxEditor.Modules Editor.StateMachine.ChangingScenesState.LoadScene(sceneId, additive); } + /// + /// Reload all loaded scenes. + /// + public void ReloadScenes() + { + foreach (var scene in Level.Scenes) + { + var sceneId = scene.ID; + if (!Level.UnloadScene(scene)) + Level.LoadScene(sceneId); + } + } + /// /// Closes scene (async). /// diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 1369ecd1e..55c318a97 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -39,6 +39,7 @@ namespace FlaxEditor.Modules ContextMenuSingleSelectGroup _numberOfClientsGroup = new ContextMenuSingleSelectGroup(); private ContextMenuButton _menuFileSaveScenes; + private ContextMenuButton _menuFileReloadScenes; private ContextMenuButton _menuFileCloseScenes; private ContextMenuButton _menuFileOpenScriptsProject; private ContextMenuButton _menuFileGenerateScriptsProjectFiles; @@ -525,6 +526,9 @@ namespace FlaxEditor.Modules var cm = MenuFile.ContextMenu; cm.VisibleChanged += OnMenuFileShowHide; _menuFileSaveAll = cm.AddButton("Save All", inputOptions.Save, Editor.SaveAll); + cm.AddSeparator(); + _menuFileReloadScenes = cm.AddButton("Reload Scenes", Editor.Scene.ReloadScenes); + cm.AddSeparator(); _menuFileSaveScenes = cm.AddButton("Save scenes", inputOptions.SaveScenes, Editor.Scene.SaveScenes); _menuFileCloseScenes = cm.AddButton("Close scenes", inputOptions.CloseScenes, Editor.Scene.CloseAllScenes); cm.AddSeparator(); From 3afb6cc88e1fb7a42a226b8156b399e9b54283f8 Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Thu, 19 Oct 2023 14:02:46 +0200 Subject: [PATCH 082/546] - Renamed ConstantNode to ConvertableNode - Moved ConvertableNode into it's own class - Added support for custom conversion code (currently only used by rotation) - Added support for Texture, Normal Map, Cube Texture, Uint, Double, String, Quaternion/Rotation --- Source/Editor/Surface/Archetypes/Constants.cs | 118 +++--------------- .../Surface/Archetypes/ConvertableNode.cs | 105 ++++++++++++++++ Source/Editor/Surface/Archetypes/Textures.cs | 4 + 3 files changed, 126 insertions(+), 101 deletions(-) create mode 100644 Source/Editor/Surface/Archetypes/ConvertableNode.cs diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index 0e9049a42..581b87e0e 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -348,96 +348,6 @@ namespace FlaxEditor.Surface.Archetypes SetValue(0, array); } } - - private class ConstantNode : SurfaceNode - { - private ScriptType _type; - - /// - public ConstantNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type) - : base(id, context, nodeArch, groupArch) - { - _type = type; - } - - /// - public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) - { - base.OnShowSecondaryContextMenu(menu, location); - - menu.AddSeparator(); - menu.AddButton("Convert to Parameter", OnConvertToParameter); - } - - private void OnConvertToParameter() - { - if(Surface.Owner is not IVisjectSurfaceWindow window) - throw new Exception("Surface owner is not a Visject Surface Window"); - - Asset asset = Surface.Owner.SurfaceAsset; - if (asset == null || !asset.IsLoaded) - { - Editor.LogError("Asset is null or not loaded"); - return; - } - - // Add parameter to editor - var paramIndex = Surface.Parameters.Count; - var paramAction = new AddRemoveParamAction - { - Window = window, - IsAdd = true, - Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)), - Type = _type, - Index = paramIndex, - InitValue = Values[0], - }; - paramAction.Do(); - - var parameterGuid = Surface.Parameters[paramIndex].ID; - - bool undoEnabled = Surface.Undo.Enabled; - Surface.Undo.Enabled = false; - SurfaceNode node = Surface.Context.SpawnNode(6, 1, this.Location, new object[] {parameterGuid}); - Surface.Undo.Enabled = undoEnabled; - - if (node is not Parameters.SurfaceNodeParamsGet getNode) - throw new Exception("Node is not a ParamsGet node!"); - - // Recreate connections of constant node - // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections - var boxes = GetBoxes(); - for (int i = 0;i < boxes.Count; i++) - { - Box box = boxes[i]; - if (!box.HasAnyConnection) - continue; - - if (!getNode.TryGetBox(i, out Box paramBox)) - continue; - - // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true - for (int k = box.Connections.Count-1; k >= 0; k--) - { - Box connectedBox = box.Connections[k]; - paramBox.CreateConnection(connectedBox); - } - } - - var spawnNodeAction = new AddRemoveNodeAction(getNode, true); - var removeConstantAction = new AddRemoveNodeAction(this, false); - - Surface.AddBatchedUndoAction(new MultiUndoAction(paramAction, spawnNodeAction, removeConstantAction)); - removeConstantAction.Do(); - } - - private bool OnParameterRenameValidate(RenamePopup popup, string value) - { - if(Surface.Owner is not IVisjectSurfaceWindow window) - throw new Exception("Surface owner is not a Visject Surface Window"); - return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); - } - } /// /// The nodes for that group. @@ -448,7 +358,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Bool", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), Description = "Constant boolean value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -481,7 +391,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 2, Title = "Integer", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(int))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(int))), Description = "Constant integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -509,7 +419,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Float", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(float))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(float))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -537,7 +447,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Float2", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Float2))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Float2))), Description = "Constant Float2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -568,7 +478,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 5, Title = "Float3", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), Description = "Constant Float3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -601,7 +511,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 6, Title = "Float4", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Float4))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Float4))), Description = "Constant Float4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), @@ -636,7 +546,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Title = "Color", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Color))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Color))), Description = "RGBA color", Flags = NodeFlags.AllGraphs, Size = new Float2(70, 100), @@ -669,6 +579,10 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 8, Title = "Rotation", + // TODO: Way too long and ugly - find a better way to add conversion + Create = (id, context, arch, groupArch) => + new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Quaternion)), + values => Quaternion.Euler((float)values[0], (float)values[1], (float)values[2])), Description = "Euler angle rotation", Flags = NodeFlags.AnimGraph | NodeFlags.VisualScriptGraph | NodeFlags.ParticleEmitterGraph, Size = new Float2(110, 60), @@ -693,6 +607,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 9, Title = "String", + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(string))), Description = "Text", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, Size = new Float2(200, 20), @@ -743,7 +658,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 12, Title = "Unsigned Integer", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(170, 20), @@ -783,6 +698,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 15, Title = "Double", + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(double))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -800,7 +716,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 16, Title = "Vector2", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Vector2))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Vector2))), Description = "Constant Vector2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -821,7 +737,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 17, Title = "Vector3", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Vector3))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Vector3))), Description = "Constant Vector3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -844,7 +760,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 18, Title = "Vector4", - Create = (id, context, arch, groupArch) => new ConstantNode(id, context, arch, groupArch, new ScriptType(typeof(Vector4))), + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Vector4))), Description = "Constant Vector4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), diff --git a/Source/Editor/Surface/Archetypes/ConvertableNode.cs b/Source/Editor/Surface/Archetypes/ConvertableNode.cs new file mode 100644 index 000000000..4e798009a --- /dev/null +++ b/Source/Editor/Surface/Archetypes/ConvertableNode.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using FlaxEditor.GUI; +using FlaxEditor.Scripting; +using FlaxEditor.Surface.Elements; +using FlaxEditor.Surface.Undo; +using FlaxEngine; + +namespace FlaxEditor.Surface.Archetypes; + +/// +/// +/// +internal class ConvertableNode : SurfaceNode +{ + private ScriptType _type; + private Func _convertFunction; + + /// + public ConvertableNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type, Func convertFunction = null) + : base(id, context, nodeArch, groupArch) + { + _type = type; + _convertFunction = convertFunction; + } + + /// + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) + { + base.OnShowSecondaryContextMenu(menu, location); + + menu.AddSeparator(); + menu.AddButton("Convert to Parameter", OnConvertToParameter); + } + + private void OnConvertToParameter() + { + if(Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + + Asset asset = Surface.Owner.SurfaceAsset; + if (asset == null || !asset.IsLoaded) + { + Editor.LogError("Asset is null or not loaded"); + return; + } + + // Add parameter to editor + var paramIndex = Surface.Parameters.Count; + object initValue = _convertFunction == null ? Values[0] : _convertFunction.Invoke(Values); + var paramAction = new AddRemoveParamAction + { + Window = window, + IsAdd = true, + Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)), + Type = _type, + Index = paramIndex, + InitValue = initValue, + }; + paramAction.Do(); + + var parameterGuid = Surface.Parameters[paramIndex].ID; + + bool undoEnabled = Surface.Undo.Enabled; + Surface.Undo.Enabled = false; + SurfaceNode node = Surface.Context.SpawnNode(6, 1, this.Location, new object[] {parameterGuid}); + Surface.Undo.Enabled = undoEnabled; + + if (node is not Parameters.SurfaceNodeParamsGet getNode) + throw new Exception("Node is not a ParamsGet node!"); + + // Recreate connections of constant node + // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections + var boxes = GetBoxes(); + for (int i = 0;i < boxes.Count; i++) + { + Box box = boxes[i]; + if (!box.HasAnyConnection) + continue; + + if (!getNode.TryGetBox(i, out Box paramBox)) + continue; + + // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true + for (int k = box.Connections.Count-1; k >= 0; k--) + { + Box connectedBox = box.Connections[k]; + paramBox.CreateConnection(connectedBox); + } + } + + var spawnNodeAction = new AddRemoveNodeAction(getNode, true); + var removeConstantAction = new AddRemoveNodeAction(this, false); + + Surface.AddBatchedUndoAction(new MultiUndoAction(paramAction, spawnNodeAction, removeConstantAction)); + removeConstantAction.Do(); + } + + private bool OnParameterRenameValidate(RenamePopup popup, string value) + { + if(Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); + } +} diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 18c21ecea..10d7eb18b 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -3,6 +3,7 @@ using System; using FlaxEditor.Content.Settings; using FlaxEditor.GUI; +using FlaxEditor.Scripting; using FlaxEngine; namespace FlaxEditor.Surface.Archetypes @@ -95,6 +96,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Texture", + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), Description = "Two dimensional texture object", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -131,6 +133,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Cube Texture", + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(CubeTexture))), Description = "Set of 6 textures arranged in a cube", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -154,6 +157,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Normal Map", + Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(NormalMap))), Description = "Two dimensional texture object sampled as a normal map", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), From 41a7aff6d75f914fb806bb4b937dfdfaf86f44e5 Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Thu, 19 Oct 2023 14:10:10 +0200 Subject: [PATCH 083/546] - Converting to parameter now also works in Particle and Visual Scripting editor --- Source/Editor/Surface/Archetypes/ConvertableNode.cs | 5 +++-- Source/Editor/Surface/ParticleEmitterSurface.cs | 2 +- Source/Editor/Surface/VisjectSurface.DragDrop.cs | 2 +- Source/Editor/Surface/VisualScriptSurface.cs | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/ConvertableNode.cs b/Source/Editor/Surface/Archetypes/ConvertableNode.cs index 4e798009a..e7c3c5517 100644 --- a/Source/Editor/Surface/Archetypes/ConvertableNode.cs +++ b/Source/Editor/Surface/Archetypes/ConvertableNode.cs @@ -59,11 +59,12 @@ internal class ConvertableNode : SurfaceNode }; paramAction.Do(); - var parameterGuid = Surface.Parameters[paramIndex].ID; + Guid parameterGuid = Surface.Parameters[paramIndex].ID; bool undoEnabled = Surface.Undo.Enabled; Surface.Undo.Enabled = false; - SurfaceNode node = Surface.Context.SpawnNode(6, 1, this.Location, new object[] {parameterGuid}); + NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); + SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, this.Location, new object[] {parameterGuid}); // 1 Visject, 2 particle, 3 VS Surface.Undo.Enabled = undoEnabled; if (node is not Parameters.SurfaceNodeParamsGet getNode) diff --git a/Source/Editor/Surface/ParticleEmitterSurface.cs b/Source/Editor/Surface/ParticleEmitterSurface.cs index 76f96f06c..b7cf83c62 100644 --- a/Source/Editor/Surface/ParticleEmitterSurface.cs +++ b/Source/Editor/Surface/ParticleEmitterSurface.cs @@ -93,7 +93,7 @@ namespace FlaxEditor.Surface } /// - protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) + protected internal override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[1]; diff --git a/Source/Editor/Surface/VisjectSurface.DragDrop.cs b/Source/Editor/Surface/VisjectSurface.DragDrop.cs index 1728c282f..2ff82c269 100644 --- a/Source/Editor/Surface/VisjectSurface.DragDrop.cs +++ b/Source/Editor/Surface/VisjectSurface.DragDrop.cs @@ -151,7 +151,7 @@ namespace FlaxEditor.Surface /// /// The group ID. /// The node archetype. - protected virtual NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) + protected internal virtual NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[0]; diff --git a/Source/Editor/Surface/VisualScriptSurface.cs b/Source/Editor/Surface/VisualScriptSurface.cs index 902582311..2f8d4cda8 100644 --- a/Source/Editor/Surface/VisualScriptSurface.cs +++ b/Source/Editor/Surface/VisualScriptSurface.cs @@ -144,7 +144,7 @@ namespace FlaxEditor.Surface } /// - protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) + protected internal override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[2]; From aafdc64b680130d97724a2ef2ceee7a291626a9c Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Thu, 19 Oct 2023 14:14:51 +0200 Subject: [PATCH 084/546] - Cleanup - Typo --- Source/Editor/Surface/Archetypes/Constants.cs | 32 +++++++++---------- ...{ConvertableNode.cs => ConvertibleNode.cs} | 24 +++++++------- Source/Editor/Surface/Archetypes/Textures.cs | 6 ++-- 3 files changed, 30 insertions(+), 32 deletions(-) rename Source/Editor/Surface/Archetypes/{ConvertableNode.cs => ConvertibleNode.cs} (92%) diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index 581b87e0e..8dcfc8233 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -7,13 +7,11 @@ using Real = System.Single; #endif using System; -using System.Linq; using System.Reflection; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; -using FlaxEditor.Surface.Undo; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -348,7 +346,7 @@ namespace FlaxEditor.Surface.Archetypes SetValue(0, array); } } - + /// /// The nodes for that group. /// @@ -358,7 +356,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Bool", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), Description = "Constant boolean value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -391,7 +389,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 2, Title = "Integer", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(int))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(int))), Description = "Constant integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -419,7 +417,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Float", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(float))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(float))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -447,7 +445,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Float2", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Float2))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Float2))), Description = "Constant Float2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -478,7 +476,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 5, Title = "Float3", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), Description = "Constant Float3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -511,7 +509,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 6, Title = "Float4", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Float4))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Float4))), Description = "Constant Float4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), @@ -546,7 +544,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Title = "Color", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Color))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Color))), Description = "RGBA color", Flags = NodeFlags.AllGraphs, Size = new Float2(70, 100), @@ -581,7 +579,7 @@ namespace FlaxEditor.Surface.Archetypes Title = "Rotation", // TODO: Way too long and ugly - find a better way to add conversion Create = (id, context, arch, groupArch) => - new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Quaternion)), + new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Quaternion)), values => Quaternion.Euler((float)values[0], (float)values[1], (float)values[2])), Description = "Euler angle rotation", Flags = NodeFlags.AnimGraph | NodeFlags.VisualScriptGraph | NodeFlags.ParticleEmitterGraph, @@ -607,7 +605,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 9, Title = "String", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(string))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(string))), Description = "Text", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, Size = new Float2(200, 20), @@ -658,7 +656,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 12, Title = "Unsigned Integer", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(170, 20), @@ -698,7 +696,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 15, Title = "Double", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(double))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(double))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -716,7 +714,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 16, Title = "Vector2", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Vector2))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Vector2))), Description = "Constant Vector2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -737,7 +735,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 17, Title = "Vector3", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Vector3))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Vector3))), Description = "Constant Vector3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -760,7 +758,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 18, Title = "Vector4", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Vector4))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Vector4))), Description = "Constant Vector4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), diff --git a/Source/Editor/Surface/Archetypes/ConvertableNode.cs b/Source/Editor/Surface/Archetypes/ConvertibleNode.cs similarity index 92% rename from Source/Editor/Surface/Archetypes/ConvertableNode.cs rename to Source/Editor/Surface/Archetypes/ConvertibleNode.cs index e7c3c5517..a54c9e0d4 100644 --- a/Source/Editor/Surface/Archetypes/ConvertableNode.cs +++ b/Source/Editor/Surface/Archetypes/ConvertibleNode.cs @@ -9,15 +9,15 @@ using FlaxEngine; namespace FlaxEditor.Surface.Archetypes; /// -/// +/// A special type of node that adds the functionality to convert nodes to parameters /// -internal class ConvertableNode : SurfaceNode +internal class ConvertibleNode : SurfaceNode { - private ScriptType _type; - private Func _convertFunction; + private readonly ScriptType _type; + private readonly Func _convertFunction; /// - public ConvertableNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type, Func convertFunction = null) + public ConvertibleNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type, Func convertFunction = null) : base(id, context, nodeArch, groupArch) { _type = type; @@ -37,7 +37,7 @@ internal class ConvertableNode : SurfaceNode { if(Surface.Owner is not IVisjectSurfaceWindow window) throw new Exception("Surface owner is not a Visject Surface Window"); - + Asset asset = Surface.Owner.SurfaceAsset; if (asset == null || !asset.IsLoaded) { @@ -60,7 +60,7 @@ internal class ConvertableNode : SurfaceNode paramAction.Do(); Guid parameterGuid = Surface.Parameters[paramIndex].ID; - + bool undoEnabled = Surface.Undo.Enabled; Surface.Undo.Enabled = false; NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); @@ -69,7 +69,7 @@ internal class ConvertableNode : SurfaceNode if (node is not Parameters.SurfaceNodeParamsGet getNode) throw new Exception("Node is not a ParamsGet node!"); - + // Recreate connections of constant node // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections var boxes = GetBoxes(); @@ -81,7 +81,7 @@ internal class ConvertableNode : SurfaceNode if (!getNode.TryGetBox(i, out Box paramBox)) continue; - + // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true for (int k = box.Connections.Count-1; k >= 0; k--) { @@ -89,14 +89,14 @@ internal class ConvertableNode : SurfaceNode paramBox.CreateConnection(connectedBox); } } - + var spawnNodeAction = new AddRemoveNodeAction(getNode, true); var removeConstantAction = new AddRemoveNodeAction(this, false); - + Surface.AddBatchedUndoAction(new MultiUndoAction(paramAction, spawnNodeAction, removeConstantAction)); removeConstantAction.Do(); } - + private bool OnParameterRenameValidate(RenamePopup popup, string value) { if(Surface.Owner is not IVisjectSurfaceWindow window) diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 10d7eb18b..85523961f 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -96,7 +96,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Texture", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), Description = "Two dimensional texture object", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -133,7 +133,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Cube Texture", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(CubeTexture))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(CubeTexture))), Description = "Set of 6 textures arranged in a cube", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -157,7 +157,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Normal Map", - Create = (id, context, arch, groupArch) => new ConvertableNode(id, context, arch, groupArch, new ScriptType(typeof(NormalMap))), + Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(NormalMap))), Description = "Two dimensional texture object sampled as a normal map", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), From ad6affc863ca63c52c8fff1a1f07634d70b068d0 Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Thu, 19 Oct 2023 14:17:28 +0200 Subject: [PATCH 085/546] - More cleanup --- .../Surface/Archetypes/ConvertibleNode.cs | 165 +++++++++--------- 1 file changed, 83 insertions(+), 82 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/ConvertibleNode.cs b/Source/Editor/Surface/Archetypes/ConvertibleNode.cs index a54c9e0d4..44a8147e6 100644 --- a/Source/Editor/Surface/Archetypes/ConvertibleNode.cs +++ b/Source/Editor/Surface/Archetypes/ConvertibleNode.cs @@ -6,101 +6,102 @@ using FlaxEditor.Surface.Elements; using FlaxEditor.Surface.Undo; using FlaxEngine; -namespace FlaxEditor.Surface.Archetypes; - -/// -/// A special type of node that adds the functionality to convert nodes to parameters -/// -internal class ConvertibleNode : SurfaceNode -{ - private readonly ScriptType _type; - private readonly Func _convertFunction; - - /// - public ConvertibleNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type, Func convertFunction = null) - : base(id, context, nodeArch, groupArch) +namespace FlaxEditor.Surface.Archetypes +{ + /// + /// A special type of node that adds the functionality to convert nodes to parameters + /// + internal class ConvertibleNode : SurfaceNode { - _type = type; - _convertFunction = convertFunction; - } + private readonly ScriptType _type; + private readonly Func _convertFunction; - /// - public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) - { - base.OnShowSecondaryContextMenu(menu, location); - - menu.AddSeparator(); - menu.AddButton("Convert to Parameter", OnConvertToParameter); - } - - private void OnConvertToParameter() - { - if(Surface.Owner is not IVisjectSurfaceWindow window) - throw new Exception("Surface owner is not a Visject Surface Window"); - - Asset asset = Surface.Owner.SurfaceAsset; - if (asset == null || !asset.IsLoaded) + /// + public ConvertibleNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type, Func convertFunction = null) + : base(id, context, nodeArch, groupArch) { - Editor.LogError("Asset is null or not loaded"); - return; + _type = type; + _convertFunction = convertFunction; } - // Add parameter to editor - var paramIndex = Surface.Parameters.Count; - object initValue = _convertFunction == null ? Values[0] : _convertFunction.Invoke(Values); - var paramAction = new AddRemoveParamAction + /// + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) { - Window = window, - IsAdd = true, - Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)), - Type = _type, - Index = paramIndex, - InitValue = initValue, - }; - paramAction.Do(); + base.OnShowSecondaryContextMenu(menu, location); - Guid parameterGuid = Surface.Parameters[paramIndex].ID; + menu.AddSeparator(); + menu.AddButton("Convert to Parameter", OnConvertToParameter); + } - bool undoEnabled = Surface.Undo.Enabled; - Surface.Undo.Enabled = false; - NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); - SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, this.Location, new object[] {parameterGuid}); // 1 Visject, 2 particle, 3 VS - Surface.Undo.Enabled = undoEnabled; - - if (node is not Parameters.SurfaceNodeParamsGet getNode) - throw new Exception("Node is not a ParamsGet node!"); - - // Recreate connections of constant node - // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections - var boxes = GetBoxes(); - for (int i = 0;i < boxes.Count; i++) + private void OnConvertToParameter() { - Box box = boxes[i]; - if (!box.HasAnyConnection) - continue; + if (Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); - if (!getNode.TryGetBox(i, out Box paramBox)) - continue; - - // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true - for (int k = box.Connections.Count-1; k >= 0; k--) + Asset asset = Surface.Owner.SurfaceAsset; + if (asset == null || !asset.IsLoaded) { - Box connectedBox = box.Connections[k]; - paramBox.CreateConnection(connectedBox); + Editor.LogError("Asset is null or not loaded"); + return; } + + // Add parameter to editor + var paramIndex = Surface.Parameters.Count; + object initValue = _convertFunction == null ? Values[0] : _convertFunction.Invoke(Values); + var paramAction = new AddRemoveParamAction + { + Window = window, + IsAdd = true, + Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)), + Type = _type, + Index = paramIndex, + InitValue = initValue, + }; + paramAction.Do(); + + Guid parameterGuid = Surface.Parameters[paramIndex].ID; + + bool undoEnabled = Surface.Undo.Enabled; + Surface.Undo.Enabled = false; + NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); + SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, this.Location, new object[] { parameterGuid }); // 1 Visject, 2 particle, 3 VS + Surface.Undo.Enabled = undoEnabled; + + if (node is not Parameters.SurfaceNodeParamsGet getNode) + throw new Exception("Node is not a ParamsGet node!"); + + // Recreate connections of constant node + // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections + var boxes = GetBoxes(); + for (int i = 0; i < boxes.Count; i++) + { + Box box = boxes[i]; + if (!box.HasAnyConnection) + continue; + + if (!getNode.TryGetBox(i, out Box paramBox)) + continue; + + // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true + for (int k = box.Connections.Count - 1; k >= 0; k--) + { + Box connectedBox = box.Connections[k]; + paramBox.CreateConnection(connectedBox); + } + } + + var spawnNodeAction = new AddRemoveNodeAction(getNode, true); + var removeConstantAction = new AddRemoveNodeAction(this, false); + + Surface.AddBatchedUndoAction(new MultiUndoAction(paramAction, spawnNodeAction, removeConstantAction)); + removeConstantAction.Do(); } - var spawnNodeAction = new AddRemoveNodeAction(getNode, true); - var removeConstantAction = new AddRemoveNodeAction(this, false); - - Surface.AddBatchedUndoAction(new MultiUndoAction(paramAction, spawnNodeAction, removeConstantAction)); - removeConstantAction.Do(); - } - - private bool OnParameterRenameValidate(RenamePopup popup, string value) - { - if(Surface.Owner is not IVisjectSurfaceWindow window) - throw new Exception("Surface owner is not a Visject Surface Window"); - return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); + private bool OnParameterRenameValidate(RenamePopup popup, string value) + { + if (Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); + } } } From c0fa858dd09ebe01fd06531e877612c9515edd9d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Oct 2023 14:25:37 +0200 Subject: [PATCH 086/546] Fix warning on missing windows layout file when loading default layout --- Source/Editor/Modules/WindowsModule.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 1a4f5ce9e..05d72598f 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -237,7 +237,11 @@ namespace FlaxEditor.Modules /// public void LoadDefaultLayout() { - LoadLayout(StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml")); + var path = StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml"); + if (File.Exists(path)) + { + LoadLayout(path); + } } /// From 4efbed91a4c1f20a6091aa4885f3b566dcbecca8 Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Thu, 19 Oct 2023 14:34:10 +0200 Subject: [PATCH 087/546] - Fixed a bug where the wrong boxes got connected - Cleanup --- Source/Editor/Surface/Archetypes/ConvertibleNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/ConvertibleNode.cs b/Source/Editor/Surface/Archetypes/ConvertibleNode.cs index 44a8147e6..eadbee08d 100644 --- a/Source/Editor/Surface/Archetypes/ConvertibleNode.cs +++ b/Source/Editor/Surface/Archetypes/ConvertibleNode.cs @@ -64,7 +64,7 @@ namespace FlaxEditor.Surface.Archetypes bool undoEnabled = Surface.Undo.Enabled; Surface.Undo.Enabled = false; NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); - SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, this.Location, new object[] { parameterGuid }); // 1 Visject, 2 particle, 3 VS + SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, this.Location, new object[] { parameterGuid }); Surface.Undo.Enabled = undoEnabled; if (node is not Parameters.SurfaceNodeParamsGet getNode) @@ -79,7 +79,7 @@ namespace FlaxEditor.Surface.Archetypes if (!box.HasAnyConnection) continue; - if (!getNode.TryGetBox(i, out Box paramBox)) + if (!getNode.TryGetBox(box.ID, out Box paramBox)) continue; // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true From 6f60218becca4fb7b3818f057d09ee0f718abc3f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Oct 2023 15:07:03 +0200 Subject: [PATCH 088/546] Optimize various rendering stages to skip loading shader when effect is disables --- Source/Engine/Renderer/DepthOfFieldPass.cpp | 8 +++----- Source/Engine/Renderer/EyeAdaptationPass.cpp | 7 +------ Source/Engine/Renderer/MotionBlurPass.cpp | 2 +- Source/Engine/Renderer/PostProcessingPass.cpp | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Source/Engine/Renderer/DepthOfFieldPass.cpp b/Source/Engine/Renderer/DepthOfFieldPass.cpp index 4c43ccdbf..cd7930f5c 100644 --- a/Source/Engine/Renderer/DepthOfFieldPass.cpp +++ b/Source/Engine/Renderer/DepthOfFieldPass.cpp @@ -202,16 +202,14 @@ GPUTexture* DepthOfFieldPass::getDofBokehShape(DepthOfFieldSettings& dofSettings void DepthOfFieldPass::Render(RenderContext& renderContext, GPUTexture*& frame, GPUTexture*& tmp) { - if (!_platformSupportsDoF || checkIfSkipPass()) + DepthOfFieldSettings& dofSettings = renderContext.List->Settings.DepthOfField; + const bool useDoF = EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::DepthOfField) && dofSettings.Enabled; + if (!useDoF || _platformSupportsDoF || checkIfSkipPass()) return; auto device = GPUDevice::Instance; auto context = device->GetMainContext(); const auto depthBuffer = renderContext.Buffers->DepthBuffer; const auto shader = _shader->GetShader(); - DepthOfFieldSettings& dofSettings = renderContext.List->Settings.DepthOfField; - const bool useDoF = _platformSupportsDoF && EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::DepthOfField) && dofSettings.Enabled; - if (!useDoF) - return; PROFILE_GPU_CPU("Depth Of Field"); context->ResetSR(); diff --git a/Source/Engine/Renderer/EyeAdaptationPass.cpp b/Source/Engine/Renderer/EyeAdaptationPass.cpp index 12b26ea41..3a840ff01 100644 --- a/Source/Engine/Renderer/EyeAdaptationPass.cpp +++ b/Source/Engine/Renderer/EyeAdaptationPass.cpp @@ -35,7 +35,6 @@ PACK_STRUCT(struct EyeAdaptationData { void EyeAdaptationPass::Render(RenderContext& renderContext, GPUTexture* colorBuffer) { - // Cache data auto device = GPUDevice::Instance; auto context = device->GetMainContext(); auto& view = renderContext.View; @@ -45,12 +44,8 @@ void EyeAdaptationPass::Render(RenderContext& renderContext, GPUTexture* colorBu //const float frameDelta = Time::ElapsedGameTime.GetTotalSeconds(); const float frameDelta = time - renderContext.Buffers->LastEyeAdaptationTime; renderContext.Buffers->LastEyeAdaptationTime = 0.0f; - - // Optionally skip the rendering - if (checkIfSkipPass() || (view.Flags & ViewFlags::EyeAdaptation) == ViewFlags::None || settings.Mode == EyeAdaptationMode::None) - { + if ((view.Flags & ViewFlags::EyeAdaptation) == ViewFlags::None || settings.Mode == EyeAdaptationMode::None || checkIfSkipPass()) return; - } PROFILE_GPU_CPU("Eye Adaptation"); diff --git a/Source/Engine/Renderer/MotionBlurPass.cpp b/Source/Engine/Renderer/MotionBlurPass.cpp index d425df80f..2030027ed 100644 --- a/Source/Engine/Renderer/MotionBlurPass.cpp +++ b/Source/Engine/Renderer/MotionBlurPass.cpp @@ -244,7 +244,7 @@ void MotionBlurPass::RenderDebug(RenderContext& renderContext, GPUTextureView* f { auto context = GPUDevice::Instance->GetMainContext(); const auto motionVectors = renderContext.Buffers->MotionVectors; - if (!motionVectors->IsAllocated() || checkIfSkipPass()) + if (!motionVectors || !motionVectors->IsAllocated() || checkIfSkipPass()) { context->Draw(frame); return; diff --git a/Source/Engine/Renderer/PostProcessingPass.cpp b/Source/Engine/Renderer/PostProcessingPass.cpp index 345147b61..006927639 100644 --- a/Source/Engine/Renderer/PostProcessingPass.cpp +++ b/Source/Engine/Renderer/PostProcessingPass.cpp @@ -195,7 +195,7 @@ void PostProcessingPass::Render(RenderContext& renderContext, GPUTexture* input, bool useLensFlares = EnumHasAnyFlags(view.Flags, ViewFlags::LensFlares) && settings.LensFlares.Intensity > 0.0f && useBloom; // Ensure to have valid data and if at least one effect should be applied - if (checkIfSkipPass() || !(useBloom || useToneMapping || useCameraArtifacts)) + if (!(useBloom || useToneMapping || useCameraArtifacts) || checkIfSkipPass()) { // Resources are missing. Do not perform rendering. Just copy raw frame context->SetViewportAndScissors((float)output->Width(), (float)output->Height()); From a989173e2dd0d154845373581dbc89caa16098ea Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Oct 2023 15:16:14 +0200 Subject: [PATCH 089/546] Fix `UnitsToText` to properly print string with 2 decimal places --- Source/Engine/Core/Utilities.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Core/Utilities.h b/Source/Engine/Core/Utilities.h index 36339baf5..f8807607b 100644 --- a/Source/Engine/Core/Utilities.h +++ b/Source/Engine/Core/Utilities.h @@ -54,7 +54,7 @@ namespace Utilities dblSUnits = units / static_cast(divider); if (i >= sizes.Length()) i = 0; - return String::Format(TEXT("{0} {1}"), RoundTo2DecimalPlaces(dblSUnits), sizes[i]); + return String::Format(TEXT("{0:.2f} {1}"), RoundTo2DecimalPlaces(dblSUnits), sizes[i]); } // Converts size of the file (in bytes) to the best fitting string From 0ea00a09cabe4bf4c6797d47fd92c25ea4c1d986 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Oct 2023 15:29:26 +0200 Subject: [PATCH 090/546] Fix regression from 3e7368b1cb31aedfa37d9cdae1d8612b08e99967 to remove scene lock when spawning actors in async #1743 --- Source/Engine/Level/Level.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index dfd5786c6..d7659ca1c 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -975,6 +975,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou SceneObject** objects = sceneObjects->Get(); if (context.Async) { + ScenesLock.Unlock(); // Unlock scenes from Main Thread so Job Threads can use it to safely setup actors hierarchy (see Actor::Deserialize) JobSystem::Execute([&](int32 i) { i++; // Start from 1. at index [0] was scene @@ -992,6 +993,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou else SceneObjectsFactory::HandleObjectDeserializationError(stream); }, objectsCount - 1); + ScenesLock.Lock(); } else { From 7cb4d279790b6d02c869735f178a2318cf88a1ce Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Thu, 19 Oct 2023 17:23:34 +0200 Subject: [PATCH 091/546] - Minor cleanup and comments --- Source/Editor/Surface/Archetypes/ConvertibleNode.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/ConvertibleNode.cs b/Source/Editor/Surface/Archetypes/ConvertibleNode.cs index eadbee08d..65d1984e7 100644 --- a/Source/Editor/Surface/Archetypes/ConvertibleNode.cs +++ b/Source/Editor/Surface/Archetypes/ConvertibleNode.cs @@ -59,8 +59,8 @@ namespace FlaxEditor.Surface.Archetypes }; paramAction.Do(); + // Spawn Get Parameter Node based on the added parameter Guid parameterGuid = Surface.Parameters[paramIndex].ID; - bool undoEnabled = Surface.Undo.Enabled; Surface.Undo.Enabled = false; NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); @@ -90,6 +90,7 @@ namespace FlaxEditor.Surface.Archetypes } } + // Add undo actions and remove constant node var spawnNodeAction = new AddRemoveNodeAction(getNode, true); var removeConstantAction = new AddRemoveNodeAction(this, false); From 2f3685c16122afd4985aaae8ee550b19d99f56b7 Mon Sep 17 00:00:00 2001 From: MineBill Date: Thu, 19 Oct 2023 18:43:13 +0300 Subject: [PATCH 092/546] Use PrefabObjectID when setting/getting the actor expanded value for prefabs. --- Source/Editor/SceneGraph/GUI/ActorTreeNode.cs | 12 ++++++++---- Source/Editor/Windows/Assets/PrefabWindow.cs | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index d392e8309..163397768 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -66,7 +66,8 @@ namespace FlaxEditor.SceneGraph.GUI _orderInParent = actor.OrderInParent; Visible = (actor.HideFlags & HideFlags.HideInHierarchy) == 0; - var id = actor.ID; + // Pick the correct id when inside a prefab window. + var id = actor.HasPrefabLink && actor.Scene == null ? actor.PrefabObjectID : actor.ID; if (Editor.Instance.ProjectCache.IsExpandedActor(ref id)) { Expand(true); @@ -171,7 +172,8 @@ namespace FlaxEditor.SceneGraph.GUI // Restore cached state on query filter clear if (noFilter && actor != null) { - var id = actor.ID; + // Pick the correct id when inside a prefab window. + var id = actor.HasPrefabLink && actor.Scene.Scene == null ? actor.PrefabObjectID : actor.ID; isExpanded = Editor.Instance.ProjectCache.IsExpandedActor(ref id); } @@ -301,10 +303,12 @@ namespace FlaxEditor.SceneGraph.GUI protected override void OnExpandedChanged() { base.OnExpandedChanged(); + var actor = Actor; - if (!IsLayoutLocked && Actor) + if (!IsLayoutLocked && actor) { - var id = Actor.ID; + // Pick the correct id when inside a prefab window. + var id = actor.HasPrefabLink && actor.Scene == null ? actor.PrefabObjectID : actor.ID; Editor.Instance.ProjectCache.SetExpandedActor(ref id, IsExpanded); } } diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index 4face0dc0..16f7c21c6 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -149,6 +149,7 @@ namespace FlaxEditor.Windows.Assets // Prefab structure tree Graph = new LocalSceneGraph(new CustomRootNode(this)); + Graph.Root.TreeNode.Expand(true); _tree = new PrefabTree { Margin = new Margin(0.0f, 0.0f, -16.0f, _treePanel.ScrollBarsSize), // Hide root node @@ -317,7 +318,7 @@ namespace FlaxEditor.Windows.Assets Graph.MainActor = _viewport.Instance; Selection.Clear(); Select(Graph.Main); - Graph.Root.TreeNode.ExpandAll(true); + Graph.Root.TreeNode.Expand(true); _undo.Clear(); ClearEditedFlag(); } @@ -413,7 +414,7 @@ namespace FlaxEditor.Windows.Assets _focusCamera = true; Selection.Clear(); Select(Graph.Main); - Graph.Root.TreeNode.ExpandAll(true); + Graph.Root.TreeNode.Expand(true); _undo.Clear(); ClearEditedFlag(); From c773c3e8fc59359c9f4ebed3e83b9c040922c7af Mon Sep 17 00:00:00 2001 From: MineBill Date: Thu, 19 Oct 2023 18:55:58 +0300 Subject: [PATCH 093/546] Collapse/Expand all node in the tree if the user is pressing the Alt key. --- Source/Editor/GUI/Tree/TreeNode.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs index 7e3af05bf..703079469 100644 --- a/Source/Editor/GUI/Tree/TreeNode.cs +++ b/Source/Editor/GUI/Tree/TreeNode.cs @@ -776,11 +776,20 @@ namespace FlaxEditor.GUI.Tree // Check if mouse hits arrow if (_mouseOverArrow && HasAnyVisibleChild) { - // Toggle open state - if (_opened) - Collapse(); + if (ParentTree.Root.GetKey(KeyboardKeys.Alt)) + { + if (_opened) + CollapseAll(); + else + ExpandAll(); + } else - Expand(); + { + if (_opened) + Collapse(); + else + Expand(); + } } // Check if mouse hits bar From 70ccc79d54dc37d274c78144b27d6d4bc6e0909f Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Thu, 19 Oct 2023 12:33:23 -0400 Subject: [PATCH 094/546] change button order --- Source/Editor/Modules/UIModule.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 55c318a97..5bc380c29 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -526,12 +526,11 @@ namespace FlaxEditor.Modules var cm = MenuFile.ContextMenu; cm.VisibleChanged += OnMenuFileShowHide; _menuFileSaveAll = cm.AddButton("Save All", inputOptions.Save, Editor.SaveAll); - cm.AddSeparator(); - _menuFileReloadScenes = cm.AddButton("Reload Scenes", Editor.Scene.ReloadScenes); - cm.AddSeparator(); _menuFileSaveScenes = cm.AddButton("Save scenes", inputOptions.SaveScenes, Editor.Scene.SaveScenes); _menuFileCloseScenes = cm.AddButton("Close scenes", inputOptions.CloseScenes, Editor.Scene.CloseAllScenes); cm.AddSeparator(); + _menuFileReloadScenes = cm.AddButton("Reload Scenes", Editor.Scene.ReloadScenes); + cm.AddSeparator(); _menuFileOpenScriptsProject = cm.AddButton("Open scripts project", inputOptions.OpenScriptsProject, Editor.CodeEditing.OpenSolution); _menuFileGenerateScriptsProjectFiles = cm.AddButton("Generate scripts project files", inputOptions.GenerateScriptsProject, Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync); _menuFileRecompileScripts = cm.AddButton("Recompile scripts", inputOptions.RecompileScripts, ScriptsBuilder.Compile); From 2bb8c82329a51fa37bb0f6fb8e8fc76cb0abcf25 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Oct 2023 19:09:06 +0200 Subject: [PATCH 095/546] Add support for packaging editor with bundled platform data --- Source/Tools/Flax.Build/Deploy/Deployer.cs | 16 ++++++---- .../Flax.Build/Deploy/Deployment.Editor.cs | 29 ++++++++++++++++++- .../Flax.Build/Deploy/Deployment.Platforms.cs | 5 ++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/Source/Tools/Flax.Build/Deploy/Deployer.cs b/Source/Tools/Flax.Build/Deploy/Deployer.cs index 5882bd366..6e5ccc390 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployer.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployer.cs @@ -66,12 +66,6 @@ namespace Flax.Deploy { Initialize(); - if (Configuration.DeployEditor) - { - BuildEditor(); - Deployment.Editor.Package(); - } - if (Configuration.DeployPlatforms) { if (Configuration.BuildPlatforms == null || Configuration.BuildPlatforms.Length == 0) @@ -94,6 +88,12 @@ namespace Flax.Deploy } } } + + if (Configuration.DeployEditor) + { + BuildEditor(); + Deployment.Editor.Package(); + } } catch (Exception ex) { @@ -183,6 +183,10 @@ namespace Flax.Deploy { if (Platform.IsPlatformSupported(platform, architecture)) { + Log.Info(string.Empty); + Log.Info($"Build {platform} {architecture} platform"); + Log.Info(string.Empty); + foreach (var configuration in Configurations) { FlaxBuild.Build(Globals.EngineRoot, "FlaxGame", platform, architecture, configuration); diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index cfb2446c3..87a2ae340 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -127,6 +127,33 @@ namespace Flax.Deploy // Deploy project DeployFile(RootPath, OutputPath, "Flax.flaxproj"); + // When deploying Editor with Platforms at once then bundle them inside it + if (Configuration.DeployPlatforms && Platforms.PackagedPlatforms != null) + { + foreach (var platform in Platforms.PackagedPlatforms) + { + Log.Info(string.Empty); + Log.Info($"Bunding {platform} platform with Editor"); + Log.Info(string.Empty); + + string platformName = platform.ToString(); + string platformFiles = Path.Combine(Deployer.PackageOutputPath, platformName); + string platformData = Path.Combine(OutputPath, "Source", "Platforms", platformName); + if (Directory.Exists(platformFiles)) + { + // Copy deployed files + Utilities.DirectoryCopy(platformFiles, platformData); + } + else + { + // Extract deployed files + var packageZipPath = Path.Combine(Deployer.PackageOutputPath, platformName + ".zip"); + Log.Verbose(packageZipPath + " -> " + platformData); + System.IO.Compression.ZipFile.ExtractToDirectory(packageZipPath, platformData, true); + } + } + } + // Package Editor into macOS app if (Platform.BuildTargetPlatform == TargetPlatform.Mac) { @@ -167,6 +194,7 @@ namespace Flax.Deploy // Build a disk image var dmgPath = Path.Combine(Deployer.PackageOutputPath, "FlaxEditor.dmg"); + Log.Info(string.Empty); Log.Info("Building disk image..."); if (File.Exists(dmgPath)) File.Delete(dmgPath); @@ -178,7 +206,6 @@ namespace Flax.Deploy // Compress if (Configuration.DontCompress) return; - Log.Info(string.Empty); Log.Info("Compressing editor files..."); string editorPackageZipPath; diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs index 832d6deb3..a2846aa14 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs @@ -10,8 +10,13 @@ namespace Flax.Deploy { public class Platforms { + internal static List PackagedPlatforms; + public static void Package(TargetPlatform platform) { + if (PackagedPlatforms == null) + PackagedPlatforms = new List(); + PackagedPlatforms.Add(platform); var platformsRoot = Path.Combine(Globals.EngineRoot, "Source", "Platforms"); Log.Info(string.Empty); From a8f961c43879e506aac502b9cfd40f7bacfb214a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Oct 2023 19:09:32 +0200 Subject: [PATCH 096/546] Add cook&run support for macOS and Linux --- .../Cooker/Platform/Linux/LinuxPlatformTools.cpp | 15 +++++++++++++++ .../Cooker/Platform/Linux/LinuxPlatformTools.h | 1 + .../Cooker/Platform/Mac/MacPlatformTools.cpp | 15 +++++++++++++++ .../Editor/Cooker/Platform/Mac/MacPlatformTools.h | 1 + 4 files changed, 32 insertions(+) diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp index ec7b8e7e1..42ef6fcdf 100644 --- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp @@ -104,4 +104,19 @@ bool LinuxPlatformTools::OnDeployBinaries(CookingData& data) return false; } +void LinuxPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) +{ + // Pick the first executable file + Array files; + FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); + for (auto& file : files) + { + if (FileSystem::GetExtension(file).IsEmpty()) + { + executableFile = file; + break; + } + } +} + #endif diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h index 562b38962..432240d00 100644 --- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h +++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h @@ -20,6 +20,7 @@ public: ArchitectureType GetArchitecture() const override; bool UseSystemDotnet() const override; bool OnDeployBinaries(CookingData& data) override; + void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override; }; #endif diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp index a1db61dbb..aa56a6b95 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp @@ -249,4 +249,19 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) return false; } +void MacPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) +{ + // Pick the first executable file + Array files; + FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); + for (auto& file : files) + { + if (FileSystem::GetExtension(file).IsEmpty()) + { + executableFile = file; + break; + } + } +} + #endif diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h index 21d9141e3..efdd0b733 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h @@ -27,6 +27,7 @@ public: bool IsNativeCodeFile(CookingData& data, const String& file) override; void OnBuildStarted(CookingData& data) override; bool OnPostProcess(CookingData& data) override; + void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override; }; #endif From 418e220c00297f7871d0cafed7e44277cb0d8f8c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Oct 2023 22:40:42 +0200 Subject: [PATCH 097/546] Add proper codesigning for the Editor app for macOS --- Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs | 4 ++++ Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index 87a2ae340..78875c5d8 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -192,6 +192,9 @@ namespace Flax.Deploy var ediotrBinariesPath = Path.Combine(appContentsPath, "Binaries/Editor/Mac", defaultEditorConfig); Utilities.DirectoryCopy(ediotrBinariesPath, appBinariesPath, true, true); + // Sign app resources + CodeSign(appPath); + // Build a disk image var dmgPath = Path.Combine(Deployer.PackageOutputPath, "FlaxEditor.dmg"); Log.Info(string.Empty); @@ -335,6 +338,7 @@ namespace Flax.Deploy Utilities.Run("strip", "FlaxEditor.dylib", null, dst, Utilities.RunOptions.None); Utilities.Run("strip", "libMoltenVK.dylib", null, dst, Utilities.RunOptions.None); + // Sign binaries CodeSign(Path.Combine(dst, "FlaxEditor")); CodeSign(Path.Combine(dst, "FlaxEditor.dylib")); CodeSign(Path.Combine(dst, "libMoltenVK.dylib")); diff --git a/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs b/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs index 3be0beb76..8b614b6e4 100644 --- a/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs +++ b/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs @@ -53,10 +53,15 @@ namespace Flax.Build.Platforms /// App code signing idenity name (from local Mac keychain). Use 'security find-identity -v -p codesigning' to list possible options. public static void CodeSign(string file, string signIdenity) { - if (!File.Exists(file)) + var isDirectory = Directory.Exists(file); + if (!isDirectory && !File.Exists(file)) throw new FileNotFoundException("Missing file to sign.", file); string cmdLine = string.Format("--force --timestamp -s \"{0}\" \"{1}\"", signIdenity, file); - if (string.IsNullOrEmpty(Path.GetExtension(file))) + if (isDirectory) + { + // Automatically sign contents + cmdLine += " --deep"; + } { // Add entitlements file with some settings for the app execution cmdLine += string.Format(" --entitlements \"{0}\"", Path.Combine(Globals.EngineRoot, "Source/Platforms/Mac/Default.entitlements")); From 770d21566aa846645113be4e48019623cd6488d3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 19 Oct 2023 23:13:05 +0200 Subject: [PATCH 098/546] Add macOS disk image notarization --- Source/Tools/Flax.Build/Deploy/Deployer.cs | 6 ++++++ Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs | 10 ++++++++++ Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/Source/Tools/Flax.Build/Deploy/Deployer.cs b/Source/Tools/Flax.Build/Deploy/Deployer.cs index 6e5ccc390..4f12e769f 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployer.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployer.cs @@ -44,6 +44,12 @@ namespace Flax.Build /// [CommandLine("deployCertPass", "Certificate file password for binaries signing.")] public static string DeployCertPass; + + /// + /// Apple keychain profile name to use for app notarize action (installed locally). + /// + [CommandLine("deployKeychainProfile", "Apple keychain profile name to use for app notarize action (installed locally).")] + public static string DeployKeychainProfile; } } diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index 78875c5d8..e1f2ab4e6 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -204,6 +204,16 @@ namespace Flax.Deploy Utilities.Run("hdiutil", $"create -srcFolder \"{appPath}\" -o \"{dmgPath}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); CodeSign(dmgPath); Log.Info("Output disk image size: " + Utilities.GetFileSize(dmgPath)); + + // Notarize disk image + if (!string.IsNullOrEmpty(Configuration.DeployKeychainProfile)) + { + Log.Info(string.Empty); + Log.Info("Notarizing disk image..."); + Utilities.Run("xcrun", $"notarytool submit \"{dmgPath}\" --wait --keychain-profile \"{Configuration.DeployKeychainProfile}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); + Utilities.Run("xcrun", $"stapler staple \"{dmgPath}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); + Log.Info("App notarized for macOS distribution!"); + } } // Compress diff --git a/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs b/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs index 8b614b6e4..4a8115fcd 100644 --- a/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs +++ b/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs @@ -62,6 +62,10 @@ namespace Flax.Build.Platforms // Automatically sign contents cmdLine += " --deep"; } + { + // Enable the hardened runtime + cmdLine += " --options=runtime"; + } { // Add entitlements file with some settings for the app execution cmdLine += string.Format(" --entitlements \"{0}\"", Path.Combine(Globals.EngineRoot, "Source/Platforms/Mac/Default.entitlements")); From 7906e26fe04377331db0dab7fd78141497aba1d8 Mon Sep 17 00:00:00 2001 From: MineBill Date: Fri, 20 Oct 2023 00:19:32 +0300 Subject: [PATCH 099/546] Limit what characters module names can contain. --- .../Windows/ContentWindow.ContextMenu.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 56136cfbf..1071e6b01 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -369,7 +369,7 @@ namespace FlaxEditor.Windows } var pluginPath = Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text); - if (Directory.Exists(pluginPath)) + if (!IsValidModuleName(nameTextBox.Text) || Directory.Exists(pluginPath)) { nameTextBox.BorderColor = Color.Red; nameTextBox.BorderSelectedColor = Color.Red; @@ -429,6 +429,12 @@ namespace FlaxEditor.Windows submitButton.Clicked += () => { // TODO: Check all modules in project including plugins + if (!IsValidModuleName(nameTextBox.Text)) + { + Editor.LogWarning("Invalid module name. Module names cannot contain spaces, start with a number or contain non-alphanumeric characters."); + return; + } + if (Directory.Exists(Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text))) { Editor.LogWarning("Cannot create module due to name conflict."); @@ -460,5 +466,16 @@ namespace FlaxEditor.Windows button.ParentContextMenu.Hide(); }; } + + private static bool IsValidModuleName(string text) + { + if (text.Contains(' ')) + return false; + if (char.IsDigit(text[0])) + return false; + if (text.Any(c => !char.IsLetterOrDigit(c) && c != '_')) + return false; + return true; + } } } From 4e9a739a52025da83ad2626a136b1298ce230b83 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 20 Oct 2023 00:28:13 +0200 Subject: [PATCH 100/546] Update GPU particle emitter graph version after recent changes --- Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h index 84834a6e5..967dc0e0c 100644 --- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h +++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h @@ -5,7 +5,7 @@ /// /// Current GPU particles emitter shader version. /// -#define PARTICLE_GPU_GRAPH_VERSION 9 +#define PARTICLE_GPU_GRAPH_VERSION 10 #if COMPILE_WITH_PARTICLE_GPU_GRAPH From a5aaa92d62fd3370c85c5a4be83db9217d28cf2d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 20 Oct 2023 00:28:28 +0200 Subject: [PATCH 101/546] Bump up build number --- Flax.flaxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flax.flaxproj b/Flax.flaxproj index ebcdc2830..6b3014e94 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -3,7 +3,7 @@ "Version": { "Major": 1, "Minor": 7, - "Build": 6401 + "Build": 6402 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.", From e796d9ea6fbb57b6a9b3d412b7daeaeed041dc51 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 20 Oct 2023 11:22:22 +0200 Subject: [PATCH 102/546] Further improve a989173e2dd0d154845373581dbc89caa16098ea --- Source/Engine/Core/Utilities.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Core/Utilities.h b/Source/Engine/Core/Utilities.h index f8807607b..737e423b2 100644 --- a/Source/Engine/Core/Utilities.h +++ b/Source/Engine/Core/Utilities.h @@ -51,10 +51,14 @@ namespace Utilities int32 i = 0; double dblSUnits = static_cast(units); for (; static_cast(units / static_cast(divider)) > 0; i++, units /= divider) - dblSUnits = units / static_cast(divider); + dblSUnits = (double)units / (double)divider; if (i >= sizes.Length()) i = 0; - return String::Format(TEXT("{0:.2f} {1}"), RoundTo2DecimalPlaces(dblSUnits), sizes[i]); + String text = String::Format(TEXT("{}"), RoundTo2DecimalPlaces(dblSUnits)); + const int32 dot = text.FindLast('.'); + if (dot != -1) + text = text.Left(dot + 3); + return String::Format(TEXT("{0} {1}"), text, sizes[i]); } // Converts size of the file (in bytes) to the best fitting string From c371a5b78c8f743ef7f37c90d149a670b611e4af Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Fri, 20 Oct 2023 12:55:54 +0200 Subject: [PATCH 103/546] - Removed Quaternion and Transform from material parameter types since they are not supported --- Source/Editor/Windows/Assets/MaterialWindow.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Editor/Windows/Assets/MaterialWindow.cs b/Source/Editor/Windows/Assets/MaterialWindow.cs index 6fbe32d7e..5aa77dbc3 100644 --- a/Source/Editor/Windows/Assets/MaterialWindow.cs +++ b/Source/Editor/Windows/Assets/MaterialWindow.cs @@ -41,8 +41,6 @@ namespace FlaxEditor.Windows.Assets new ScriptType(typeof(Vector3)), new ScriptType(typeof(Vector4)), new ScriptType(typeof(Color)), - new ScriptType(typeof(Quaternion)), - new ScriptType(typeof(Transform)), new ScriptType(typeof(Matrix)), }; From ab494579441ecf3c2a6149ec9abb6f5fa5477471 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 21 Oct 2023 12:44:25 +0200 Subject: [PATCH 104/546] Add log for build command invoke in deployer --- Source/Tools/Flax.Build/Deploy/FlaxBuild.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Tools/Flax.Build/Deploy/FlaxBuild.cs b/Source/Tools/Flax.Build/Deploy/FlaxBuild.cs index 7d3a31061..b4e8b8680 100644 --- a/Source/Tools/Flax.Build/Deploy/FlaxBuild.cs +++ b/Source/Tools/Flax.Build/Deploy/FlaxBuild.cs @@ -20,6 +20,7 @@ namespace Flax.Deploy if (!string.IsNullOrEmpty(Configuration.Compiler)) cmdLine += " -compiler=" + Configuration.Compiler; + Log.Info($"Building {target} for {platform} {architecture} {configuration}..."); int result = Utilities.Run(flaxBuildTool, cmdLine, null, root); if (result != 0) { From ba39938ed5457a4de5111b3d94fa0dca0ab8edca Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 21 Oct 2023 12:44:43 +0200 Subject: [PATCH 105/546] Update Flax icon for macOS to match design guidelines --- Source/Platforms/Mac/Default.icns | Bin 193857 -> 176267 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Source/Platforms/Mac/Default.icns b/Source/Platforms/Mac/Default.icns index 455acd991a7572831f097c615562180774495fd7..911276d8ee57429688ff8d8979e4b479a509c53a 100644 GIT binary patch literal 176267 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 z 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ⅆ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

ev$zU+4x6EXMSw!Pr5zC>v89P=F&O%7#T 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%0A>9LdZ9oAsE@>)QlAl$t0#bRiK)~ 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%0AIs&*bb=ae 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

nLE=PT9q$0d4OCl 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 From 08a6a2b6dc4a62b0a65df61fa05ac98558c386fb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 21 Oct 2023 13:04:33 +0200 Subject: [PATCH 106/546] Fix potential crashes on macOS due to missing window inside view event --- Source/Engine/Platform/Mac/MacWindow.cpp | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index 000accbfa..3129ef883 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -4,6 +4,7 @@ #include "../Window.h" #include "Engine/Platform/Apple/AppleUtils.h" +#include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/IGuiData.h" #include "Engine/Core/Log.h" #include "Engine/Input/Input.h" @@ -14,6 +15,14 @@ #include #include +inline bool IsWindowInvalid(Window* win) +{ + WindowsManager::WindowsLocker.Lock(); + const bool hasWindow = WindowsManager::Windows.Contains(win); + WindowsManager::WindowsLocker.Unlock(); + return !hasWindow || !win; +} + KeyboardKeys GetKey(NSEvent* event) { switch ([event keyCode]) @@ -268,17 +277,20 @@ NSDragOperation GetDragDropOperation(DragDropEffect dragDropEffect) // Handle resizing to be sure that content has valid size when window was resized [self windowDidResize:notification]; + if (IsWindowInvalid(Window)) return; Window->OnGotFocus(); } - (void)windowDidResignKey:(NSNotification*)notification { + if (IsWindowInvalid(Window)) return; Window->OnLostFocus(); } - (void)windowWillClose:(NSNotification*)notification { [self setDelegate: nil]; + if (IsWindowInvalid(Window)) return; Window->Close(ClosingReason::User); } @@ -375,6 +387,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)keyDown:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; KeyboardKeys key = GetKey(event); if (key != KeyboardKeys::None) Input::Keyboard->OnKeyDown(key, Window); @@ -405,6 +418,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)keyUp:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; KeyboardKeys key = GetKey(event); if (key != KeyboardKeys::None) Input::Keyboard->OnKeyUp(key, Window); @@ -412,6 +426,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)flagsChanged:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; int32 modMask; int32 keyCode = [event keyCode]; if (keyCode == 0x36 || keyCode == 0x37) @@ -437,6 +452,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)scrollWheel:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); double deltaX = [event scrollingDeltaX]; double deltaY = [event scrollingDeltaY]; @@ -451,6 +467,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)mouseMoved:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); if (!Window->IsMouseTracking() && !IsMouseOver) return; @@ -459,18 +476,21 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)mouseEntered:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; IsMouseOver = true; Window->SetIsMouseOver(true); } - (void)mouseExited:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; IsMouseOver = false; Window->SetIsMouseOver(false); } - (void)mouseDown:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton = MouseButton::Left; if ([event clickCount] == 2) @@ -486,6 +506,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)mouseUp:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton = MouseButton::Left; Input::Mouse->OnMouseUp(Window->ClientToScreen(mousePos), mouseButton, Window); @@ -493,6 +514,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)rightMouseDown:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton = MouseButton::Right; if ([event clickCount] == 2) @@ -508,6 +530,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)rightMouseUp:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton = MouseButton::Right; Input::Mouse->OnMouseUp(Window->ClientToScreen(mousePos), mouseButton, Window); @@ -515,6 +538,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)otherMouseDown:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton; switch ([event buttonNumber]) @@ -544,6 +568,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)otherMouseUp:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton; switch ([event buttonNumber]) @@ -565,6 +590,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (NSDragOperation)draggingEntered:(id)sender { + if (IsWindowInvalid(Window)) return NSDragOperationNone; Float2 mousePos; MacDropData dropData; GetDragDropData(Window, sender, mousePos, dropData); @@ -580,6 +606,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (NSDragOperation)draggingUpdated:(id)sender { + if (IsWindowInvalid(Window)) return NSDragOperationNone; Float2 mousePos; MacDropData dropData; GetDragDropData(Window, sender, mousePos, dropData); @@ -590,6 +617,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (BOOL)performDragOperation:(id)sender { + if (IsWindowInvalid(Window)) return NO; Float2 mousePos; MacDropData dropData; GetDragDropData(Window, sender, mousePos, dropData); @@ -600,6 +628,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)draggingExited:(id)sender { + if (IsWindowInvalid(Window)) return; Window->OnDragLeave(); } From 571f8febf48f7d6311fe7fad4a639f99d107723e Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Sat, 21 Oct 2023 13:11:42 +0200 Subject: [PATCH 107/546] - Moved convertible node file out of archetypes folder into surface folder to be more in line of the project structure (other special nodes like surfacecomment are also just in the surface folder) - Cleanup --- .../Editor/Surface/{Archetypes => }/ConvertibleNode.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) rename Source/Editor/Surface/{Archetypes => }/ConvertibleNode.cs (94%) diff --git a/Source/Editor/Surface/Archetypes/ConvertibleNode.cs b/Source/Editor/Surface/ConvertibleNode.cs similarity index 94% rename from Source/Editor/Surface/Archetypes/ConvertibleNode.cs rename to Source/Editor/Surface/ConvertibleNode.cs index 65d1984e7..9c4a65ab9 100644 --- a/Source/Editor/Surface/Archetypes/ConvertibleNode.cs +++ b/Source/Editor/Surface/ConvertibleNode.cs @@ -1,12 +1,11 @@ using System; using System.Linq; -using FlaxEditor.GUI; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEditor.Surface.Undo; using FlaxEngine; -namespace FlaxEditor.Surface.Archetypes +namespace FlaxEditor.Surface { ///

/// A special type of node that adds the functionality to convert nodes to parameters @@ -52,7 +51,7 @@ namespace FlaxEditor.Surface.Archetypes { Window = window, IsAdd = true, - Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)), + Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(x)), Type = _type, Index = paramIndex, InitValue = initValue, @@ -67,7 +66,7 @@ namespace FlaxEditor.Surface.Archetypes SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, this.Location, new object[] { parameterGuid }); Surface.Undo.Enabled = undoEnabled; - if (node is not Parameters.SurfaceNodeParamsGet getNode) + if (node is not Archetypes.Parameters.SurfaceNodeParamsGet getNode) throw new Exception("Node is not a ParamsGet node!"); // Recreate connections of constant node @@ -98,7 +97,7 @@ namespace FlaxEditor.Surface.Archetypes removeConstantAction.Do(); } - private bool OnParameterRenameValidate(RenamePopup popup, string value) + private bool OnParameterRenameValidate(string value) { if (Surface.Owner is not IVisjectSurfaceWindow window) throw new Exception("Surface owner is not a Visject Surface Window"); From fa8b92a456b30e2d1343c0a0f3d06828d727d5a3 Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Sat, 21 Oct 2023 14:18:30 +0200 Subject: [PATCH 108/546] Adding the ability to order comments in visject --- Source/Editor/Surface/SurfaceComment.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index ff38b1aa8..12b60a302 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -2,6 +2,7 @@ using System; using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; using FlaxEngine; using FlaxEngine.GUI; @@ -314,5 +315,20 @@ namespace FlaxEditor.Surface Color = ColorValue = color; Surface.MarkAsEdited(false); } + + /// + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) + { + base.OnShowSecondaryContextMenu(menu, location); + + menu.AddSeparator(); + ContextMenuChildMenu cmOrder = menu.AddChildMenu("Order"); + { + cmOrder.ContextMenu.AddButton("Bring Forward", () => { if(IndexInParent < Context.Comments.Count-1) IndexInParent++;}); + cmOrder.ContextMenu.AddButton("Bring to Front", () => { IndexInParent = Context.Comments.Count - 1;}); + cmOrder.ContextMenu.AddButton("Send Backward", () => {if(IndexInParent > 0) IndexInParent--;}); + cmOrder.ContextMenu.AddButton("Send to Back", () => { IndexInParent = 0;}); + } + } } } From dcbc917b7defb603743b97757bfcad73984b7363 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 21 Oct 2023 15:13:36 +0200 Subject: [PATCH 109/546] Add local util script to ignored on macOS --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 54907892f..b7e11e554 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ Source/*.csproj /Package_*/ !Source/Engine/Debug /Source/Platforms/Editor/Linux/Mono/etc/mono/registry +PackageEditor_Cert.command PackageEditor_Cert.bat PackagePlatforms_Cert.bat From 21f2e59d12ad12af3e974be563d4cde7ec5fe8a1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 21 Oct 2023 15:36:38 +0200 Subject: [PATCH 110/546] Add drag&drop support to macOS --- Source/Engine/Platform/Mac/MacWindow.cpp | 103 ++++++++++++++++++++++- Source/Engine/Platform/Mac/MacWindow.h | 8 +- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index 3129ef883..08a4e0d2f 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -6,6 +6,12 @@ #include "Engine/Platform/Apple/AppleUtils.h" #include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/IGuiData.h" +#if USE_EDITOR +#include "Engine/Platform/CriticalSection.h" +#include "Engine/Threading/ThreadPoolTask.h" +#include "Engine/Threading/ThreadPool.h" +#include "Engine/Engine/Engine.h" +#endif #include "Engine/Core/Log.h" #include "Engine/Input/Input.h" #include "Engine/Input/Mouse.h" @@ -15,6 +21,29 @@ #include #include +#if USE_EDITOR +// Data for drawing window while doing drag&drop on Mac (engine is paused during platform tick) +CriticalSection MacDragLocker; +NSDraggingSession* MacDragSession = nullptr; +class DoDragDropJob* MacDragJob = nullptr; + +class DoDragDropJob : public ThreadPoolTask +{ +public: + int64 ExitFlag = 0; + + bool Run() override + { + while (Platform::AtomicRead(&ExitFlag) == 0) + { + Engine::OnDraw(); + Platform::Sleep(20); + } + return false; + } +}; +#endif + inline bool IsWindowInvalid(Window* win) { WindowsManager::WindowsLocker.Lock(); @@ -323,7 +352,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) @end -@interface MacViewImpl : NSView +@interface MacViewImpl : NSView { MacWindow* Window; NSTrackingArea* TrackingArea; @@ -632,6 +661,34 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) Window->OnDragLeave(); } +- (NSDragOperation)draggingSession:(NSDraggingSession*)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context +{ + if (IsWindowInvalid(Window)) return NSDragOperationNone; + return NSDragOperationMove; +} + +- (void)draggingSession:(NSDraggingSession*)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation +{ +#if USE_EDITOR + // Stop background worker once the drag ended + MacDragLocker.Lock(); + if (MacDragSession && MacDragSession == session) + { + Platform::AtomicStore(&MacDragJob->ExitFlag, 1); + MacDragJob->Wait(); + MacDragSession = nullptr; + MacDragJob = nullptr; + } + MacDragLocker.Unlock(); +#endif +} + +- (void)pasteboard:(nullable NSPasteboard*)pasteboard item:(NSPasteboardItem*)item provideDataForType:(NSPasteboardType)type +{ + if (IsWindowInvalid(Window)) return; + [pasteboard setString:(NSString*)AppleUtils::ToString(Window->GetDragText()) forType:NSPasteboardTypeString]; +} + @end MacWindow::MacWindow(const CreateWindowSettings& settings) @@ -682,6 +739,7 @@ MacWindow::MacWindow(const CreateWindowSettings& settings) [window setAcceptsMouseMovedEvents:YES]; [window setDelegate:window]; _window = window; + _view = view; if (settings.AllowDragAndDrop) { [view registerForDraggedTypes:@[NSPasteboardTypeFileURL, NSPasteboardTypeString]]; @@ -705,6 +763,7 @@ MacWindow::~MacWindow() [window close]; [window release]; _window = nullptr; + _view = nullptr; } void MacWindow::CheckForResize(float width, float height) @@ -938,8 +997,46 @@ void MacWindow::SetTitle(const StringView& title) DragDropEffect MacWindow::DoDragDrop(const StringView& data) { - // TODO: implement using beginDraggingSession and NSDraggingSource - return DragDropEffect::None; + NSWindow* window = (NSWindow*)_window; + MacViewImpl* view = (MacViewImpl*)_view; + _dragText = data; + + // Create mouse drag event + NSEvent* event = [NSEvent + mouseEventWithType:NSEventTypeLeftMouseDragged + location:window.mouseLocationOutsideOfEventStream + modifierFlags:0 + timestamp:NSApp.currentEvent.timestamp + windowNumber:window.windowNumber + context:nil + eventNumber:0 + clickCount:1 + pressure:1.0]; + + // Create drag item + NSPasteboardItem* pasteItem = [NSPasteboardItem new]; + [pasteItem setDataProvider:view forTypes:[NSArray arrayWithObjects:NSPasteboardTypeString, nil]]; + NSDraggingItem* dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:pasteItem]; + [dragItem setDraggingFrame:NSMakeRect(event.locationInWindow.x, event.locationInWindow.y, 1, 1) contents:nil]; + + // Start dragging session + NSDraggingSession* draggingSession = [view beginDraggingSessionWithItems:[NSArray arrayWithObject:dragItem] event:event source:view]; + DragDropEffect result = DragDropEffect::None; + +#if USE_EDITOR + // Create background worker that will keep updating GUI (perform rendering) + MacDragLocker.Lock(); + ASSERT(!MacDragSession && !MacDragJob); + MacDragSession = draggingSession; + MacDragJob = New(); + Task::StartNew(MacDragJob); + MacDragLocker.Unlock(); + while (MacDragJob->GetState() == TaskState::Queued) + Platform::Sleep(1); + // TODO: maybe wait for the drag end to return result? +#endif + + return result; } void MacWindow::SetCursor(CursorType type) diff --git a/Source/Engine/Platform/Mac/MacWindow.h b/Source/Engine/Platform/Mac/MacWindow.h index 8850f80ba..7c3f091b4 100644 --- a/Source/Engine/Platform/Mac/MacWindow.h +++ b/Source/Engine/Platform/Mac/MacWindow.h @@ -14,8 +14,10 @@ class FLAXENGINE_API MacWindow : public WindowBase { private: - void* _window; + void* _window = nullptr; + void* _view = nullptr; bool _isMouseOver = false; + String _dragText; public: @@ -24,6 +26,10 @@ public: void CheckForResize(float width, float height); void SetIsMouseOver(bool value); + const String& GetDragText() const + { + return _dragText; + } public: From ec7840f36b9bbdd31ca76e27502d639e8d5ca5f6 Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Sat, 21 Oct 2023 16:06:28 +0200 Subject: [PATCH 111/546] - New comments now get spawned on top of other comments - Commenting other comments now puts the spawned comment below the child comments - Added Order Value to comments to serialize order - Added backwards compatiblity to older editor versions - Cleanup - XML Comments --- Source/Editor/Surface/Archetypes/Tools.cs | 1 + Source/Editor/Surface/SurfaceComment.cs | 49 +++++++++++++++++-- Source/Editor/Surface/VisjectSurface.cs | 13 ++++- .../VisjectSurfaceContext.Serialization.cs | 6 --- .../Editor/Surface/VisjectSurfaceContext.cs | 28 +++++++++-- 5 files changed, 83 insertions(+), 14 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 6d2d31b8c..7a37ad746 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1510,6 +1510,7 @@ namespace FlaxEditor.Surface.Archetypes "Comment", // Title new Color(1.0f, 1.0f, 1.0f, 0.2f), // Color new Float2(400.0f, 400.0f), // Size + -1, // Order }, }, CurveNode.GetArchetype(12, "Curve", typeof(float), 0.0f, 1.0f), diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 12b60a302..aad45190e 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -53,6 +53,12 @@ namespace FlaxEditor.Surface set => SetValue(2, value, false); } + private int OrderValue + { + get => (int)Values[3]; + set => SetValue(3, value, false); + } + /// public SurfaceComment(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) @@ -68,6 +74,19 @@ namespace FlaxEditor.Surface Title = TitleValue; Color = ColorValue; Size = SizeValue; + + // Order + // Backwards compatibility - When opening with an older version send the old comments to the back + if (Values.Length < 4) + { + if (IndexInParent > 0) + IndexInParent = 0; + OrderValue = IndexInParent; + } + else if(OrderValue != -1) + { + IndexInParent = OrderValue; + } } /// @@ -77,6 +96,10 @@ namespace FlaxEditor.Surface // Randomize color Color = ColorValue = Color.FromHSV(new Random().NextFloat(0, 360), 0.7f, 0.25f, 0.8f); + + if(OrderValue == -1) + OrderValue = Context.CommentCount - 1; + IndexInParent = OrderValue; } /// @@ -324,10 +347,28 @@ namespace FlaxEditor.Surface menu.AddSeparator(); ContextMenuChildMenu cmOrder = menu.AddChildMenu("Order"); { - cmOrder.ContextMenu.AddButton("Bring Forward", () => { if(IndexInParent < Context.Comments.Count-1) IndexInParent++;}); - cmOrder.ContextMenu.AddButton("Bring to Front", () => { IndexInParent = Context.Comments.Count - 1;}); - cmOrder.ContextMenu.AddButton("Send Backward", () => {if(IndexInParent > 0) IndexInParent--;}); - cmOrder.ContextMenu.AddButton("Send to Back", () => { IndexInParent = 0;}); + cmOrder.ContextMenu.AddButton("Bring Forward", () => + { + if(IndexInParent < Context.CommentCount-1) + IndexInParent++; + OrderValue = IndexInParent; + }); + cmOrder.ContextMenu.AddButton("Bring to Front", () => + { + IndexInParent = Context.CommentCount-1; + OrderValue = IndexInParent; + }); + cmOrder.ContextMenu.AddButton("Send Backward", () => + { + if(IndexInParent > 0) + IndexInParent--; + OrderValue = IndexInParent; + }); + cmOrder.ContextMenu.AddButton("Send to Back", () => + { + IndexInParent = 0; + OrderValue = IndexInParent; + }); } } } diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 3fe722091..0185e364a 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -716,7 +716,18 @@ namespace FlaxEditor.Surface return null; Rectangle surfaceArea = GetNodesBounds(selection).MakeExpanded(80.0f); - return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f)); + // Order below other selected comments + bool hasCommentsSelected = false; + int lowestCommentOrder = int.MaxValue; + for (int i = 0; i < selection.Count; i++) + { + if (selection[i] is not SurfaceComment || selection[i].IndexInParent >= lowestCommentOrder) + continue; + hasCommentsSelected = true; + lowestCommentOrder = selection[i].IndexInParent; + } + + return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f), hasCommentsSelected ? lowestCommentOrder : -1); } private static Rectangle GetNodesBounds(List nodes) diff --git a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs index 7d75e006b..12f01d4f1 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs @@ -920,12 +920,6 @@ namespace FlaxEditor.Surface // Link control control.OnLoaded(action); control.Parent = RootControl; - - if (control is SurfaceComment) - { - // Move comments to the background - control.IndexInParent = 0; - } } /// diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs index 36e811c48..54c456d04 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.cs @@ -85,6 +85,27 @@ namespace FlaxEditor.Surface } } + /// + /// Gets the amount of surface comments + /// + /// + /// This is used as an alternative to , if only the amount of comments is important. + /// Is faster and doesn't allocate as much memory + /// + public int CommentCount + { + get + { + int count = 0; + for (int i = 0; i < RootControl.Children.Count; i++) + { + if (RootControl.Children[i] is SurfaceComment) + count++; + } + return count; + } + } + /// /// Gets a value indicating whether this context is modified (needs saving and flushing with surface data context source). /// @@ -286,13 +307,14 @@ namespace FlaxEditor.Surface /// The comment title. /// The comment color. /// The comment object - public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color) + public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1) { var values = new object[] { title, // Title color, // Color surfaceArea.Size, // Size + customOrder, // Order }; return (SurfaceComment)SpawnNode(7, 11, surfaceArea.Location, values); } @@ -304,10 +326,10 @@ namespace FlaxEditor.Surface /// The comment title. /// The comment color. /// The comment object - public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color) + public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1) { // Create comment - var comment = SpawnComment(ref surfaceArea, title, color); + var comment = SpawnComment(ref surfaceArea, title, color, customOrder); if (comment == null) { Editor.LogWarning("Failed to create comment."); From ad15c5b2fcecabec14fb7b31402d4b9ca42f2f94 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 21 Oct 2023 13:36:39 -0500 Subject: [PATCH 112/546] Fix particles effect not being able to just call play if islooped is false. --- Source/Engine/Particles/ParticleEffect.h | 2 +- Source/Engine/Particles/Particles.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Particles/ParticleEffect.h b/Source/Engine/Particles/ParticleEffect.h index 9e9792a4c..07d82d703 100644 --- a/Source/Engine/Particles/ParticleEffect.h +++ b/Source/Engine/Particles/ParticleEffect.h @@ -133,7 +133,7 @@ public: /// /// The particle system instance that plays the particles simulation in the game. /// -API_CLASS(Attributes="ActorContextMenu(\"New/Visuals/Particle Effects\"), ActorToolbox(\"Visuals\")") +API_CLASS(Attributes="ActorContextMenu(\"New/Visuals/Particle Effect\"), ActorToolbox(\"Visuals\")") class FLAXENGINE_API ParticleEffect : public Actor { DECLARE_SCENE_OBJECT(ParticleEffect); diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp index 5850b6736..c039f4ba6 100644 --- a/Source/Engine/Particles/Particles.cpp +++ b/Source/Engine/Particles/Particles.cpp @@ -1318,6 +1318,7 @@ void ParticlesSystem::Job(int32 index) emitterInstance.Buffer = nullptr; } } + effect->Stop(); return; } } From 7d9991999d583a2c0ed89ff15bf1c1aa8046d7c2 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 21 Oct 2023 14:08:23 -0500 Subject: [PATCH 113/546] Better fix. --- Source/Engine/Particles/ParticleEffect.cpp | 4 ++++ Source/Engine/Particles/Particles.cpp | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index 788a4263f..656a61991 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -283,6 +283,10 @@ void ParticleEffect::UpdateSimulation(bool singleFrame) void ParticleEffect::Play() { + // Reset simulation when play is called and particle system is time is complete - ex. if IsLooping is false and simulation is complete + if (!IsLooping && Instance.Time >= (float)ParticleSystem->DurationFrames / ParticleSystem->FramesPerSecond) + ResetSimulation(); + _isPlaying = true; } diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp index c039f4ba6..15d484f8a 100644 --- a/Source/Engine/Particles/Particles.cpp +++ b/Source/Engine/Particles/Particles.cpp @@ -1318,7 +1318,8 @@ void ParticlesSystem::Job(int32 index) emitterInstance.Buffer = nullptr; } } - effect->Stop(); + // Set is playing to false because it is not playing anymore. + effect->Pause(); return; } } From 78aae0da5a18afdc8e528cfe59427b069b2cb501 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 21 Oct 2023 17:22:02 -0500 Subject: [PATCH 114/546] Better handling stopping/resetting non-looping effect. --- Source/Engine/Particles/ParticleEffect.cpp | 8 +++----- Source/Engine/Particles/ParticleEffect.h | 1 + Source/Engine/Particles/Particles.cpp | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index 656a61991..e461f332e 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -283,11 +283,8 @@ void ParticleEffect::UpdateSimulation(bool singleFrame) void ParticleEffect::Play() { - // Reset simulation when play is called and particle system is time is complete - ex. if IsLooping is false and simulation is complete - if (!IsLooping && Instance.Time >= (float)ParticleSystem->DurationFrames / ParticleSystem->FramesPerSecond) - ResetSimulation(); - _isPlaying = true; + _isStopped = false; } void ParticleEffect::Pause() @@ -297,6 +294,7 @@ void ParticleEffect::Pause() void ParticleEffect::Stop() { + _isStopped = true; _isPlaying = false; ResetSimulation(); } @@ -452,7 +450,7 @@ void ParticleEffect::Update() void ParticleEffect::UpdateExecuteInEditor() { // Auto-play in Editor - if (!Editor::IsPlayMode) + if (!Editor::IsPlayMode && !_isStopped) { _isPlaying = true; Update(); diff --git a/Source/Engine/Particles/ParticleEffect.h b/Source/Engine/Particles/ParticleEffect.h index 07d82d703..d3f0cc9d0 100644 --- a/Source/Engine/Particles/ParticleEffect.h +++ b/Source/Engine/Particles/ParticleEffect.h @@ -185,6 +185,7 @@ private: Array _parameters; // Cached for scripting API Array _parametersOverrides; // Cached parameter modifications to be applied to the parameters bool _isPlaying = false; + bool _isStopped = false; public: /// diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp index 15d484f8a..52845a0f8 100644 --- a/Source/Engine/Particles/Particles.cpp +++ b/Source/Engine/Particles/Particles.cpp @@ -1318,8 +1318,8 @@ void ParticlesSystem::Job(int32 index) emitterInstance.Buffer = nullptr; } } - // Set is playing to false because it is not playing anymore. - effect->Pause(); + // Stop playing effect. + effect->Stop(); return; } } From 7b2058d1b2c18103d6f5f9374d06d1783cddec94 Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Sun, 22 Oct 2023 12:37:51 +0200 Subject: [PATCH 115/546] - Added alternative titles to some nodes --- Source/Editor/Surface/Archetypes/Bitwise.cs | 2 +- Source/Editor/Surface/Archetypes/Boolean.cs | 2 +- Source/Editor/Surface/Archetypes/Constants.cs | 1 + Source/Editor/Surface/Archetypes/Math.cs | 14 +++++++++++--- Source/Editor/Surface/Archetypes/Tools.cs | 3 ++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Bitwise.cs b/Source/Editor/Surface/Archetypes/Bitwise.cs index 06f719adc..43dcb91b5 100644 --- a/Source/Editor/Surface/Archetypes/Bitwise.cs +++ b/Source/Editor/Surface/Archetypes/Bitwise.cs @@ -60,7 +60,7 @@ namespace FlaxEditor.Surface.Archetypes Op1(1, "Bitwise NOT", "Negates the value using bitwise operation", new[] { "!", "~" }), Op2(2, "Bitwise AND", "Performs a bitwise conjunction on two values", new[] { "&" }), Op2(3, "Bitwise OR", "", new[] { "|" }), - Op2(4, "Bitwise XOR", ""), + Op2(4, "Bitwise XOR", "", new[] { "^" }), }; } } diff --git a/Source/Editor/Surface/Archetypes/Boolean.cs b/Source/Editor/Surface/Archetypes/Boolean.cs index 153b2fead..ed97a9642 100644 --- a/Source/Editor/Surface/Archetypes/Boolean.cs +++ b/Source/Editor/Surface/Archetypes/Boolean.cs @@ -60,7 +60,7 @@ namespace FlaxEditor.Surface.Archetypes Op1(1, "Boolean NOT", "Negates the boolean value", new[] { "!", "~" }), Op2(2, "Boolean AND", "Performs a logical conjunction on two values", new[] { "&&" }), Op2(3, "Boolean OR", "Returns true if either (or both) of its operands is true", new[] { "||" }), - Op2(4, "Boolean XOR", ""), + Op2(4, "Boolean XOR", "", new [] { "^" } ), Op2(5, "Boolean NOR", ""), Op2(6, "Boolean NAND", ""), }; diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index cbe7822e3..f845e2fc1 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -644,6 +644,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 12, Title = "Unsigned Integer", + AlternativeTitles = new[] { "UInt" , "U Int" }, Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(170, 20), diff --git a/Source/Editor/Surface/Archetypes/Math.cs b/Source/Editor/Surface/Archetypes/Math.cs index fe2f1e044..bb81fe05d 100644 --- a/Source/Editor/Surface/Archetypes/Math.cs +++ b/Source/Editor/Surface/Archetypes/Math.cs @@ -13,6 +13,11 @@ namespace FlaxEditor.Surface.Archetypes public static class Math { private static NodeArchetype Op1(ushort id, string title, string desc, ConnectionsHint hints = ConnectionsHint.Numeric, Type type = null) + { + return Op1(id, title, desc, null, hints, type); + } + + private static NodeArchetype Op1(ushort id, string title, string desc, string[] altTitles, ConnectionsHint hints = ConnectionsHint.Numeric, Type type = null) { return new NodeArchetype { @@ -20,6 +25,7 @@ namespace FlaxEditor.Surface.Archetypes Title = title, Description = desc, Flags = NodeFlags.AllGraphs, + AlternativeTitles = altTitles, Size = new Float2(110, 20), DefaultType = new ScriptType(type), ConnectionsHints = hints, @@ -92,6 +98,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 11, Title = "Length", + AlternativeTitles = new[] { "Magnitude", "Mag" }, Description = "Returns the length of A vector", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -107,10 +114,10 @@ namespace FlaxEditor.Surface.Archetypes Op1(13, "Round", "Rounds A to the nearest integer"), Op1(14, "Saturate", "Clamps A to the range [0, 1]"), Op1(15, "Sine", "Returns sine of A"), - Op1(16, "Sqrt", "Returns square root of A"), + Op1(16, "Sqrt", "Returns square root of A", new [] { "Square Root", "Square", "Root" }), Op1(17, "Tangent", "Returns tangent of A"), Op2(18, "Cross", "Returns the cross product of A and B", ConnectionsHint.None, typeof(Float3)), - Op2(19, "Distance", "Returns a distance scalar between A and B", ConnectionsHint.Vector, null, typeof(float), false), + Op2(19, "Distance", "Returns a distance scalar between A and B", new [] { "Magnitude", "Mag", "Length" }, ConnectionsHint.Vector, null, typeof(float), false), Op2(20, "Dot", "Returns the dot product of A and B", ConnectionsHint.Vector, null, typeof(float), false), Op2(21, "Max", "Selects the greater of A and B"), Op2(22, "Min", "Selects the lesser of A and B"), @@ -185,7 +192,7 @@ namespace FlaxEditor.Surface.Archetypes } }, // - Op1(27, "Negate", "Returns opposite value"), + Op1(27, "Negate", "Returns opposite value", new [] { "Invert" }), Op1(28, "One Minus", "Returns 1 - value"), // new NodeArchetype @@ -225,6 +232,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 31, Title = "Mad", + AlternativeTitles = new [] { "Multiply", "Add", "*+" }, Description = "Performs value multiplication and addition at once", Flags = NodeFlags.AllGraphs, Size = new Float2(160, 60), diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 6d2d31b8c..0f9e6979f 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1483,7 +1483,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 11, Title = "Comment", - AlternativeTitles = new[] { "//" }, + AlternativeTitles = new[] { "//" , "Group" }, TryParseText = (string filterText, out object[] data) => { data = null; @@ -1638,6 +1638,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 22, Title = "As", + AlternativeTitles = new [] { "Cast" }, Create = (id, context, arch, groupArch) => new AsNode(id, context, arch, groupArch), Description = "Casts the object to a different type. Returns null if cast fails.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, From ea00a448ef6545320b6e9b0e8b1729e2d4786abf Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Sun, 22 Oct 2023 12:54:14 +0200 Subject: [PATCH 116/546] - Minor cleanup --- Source/Editor/Surface/Archetypes/Math.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Math.cs b/Source/Editor/Surface/Archetypes/Math.cs index bb81fe05d..7f4afa6a6 100644 --- a/Source/Editor/Surface/Archetypes/Math.cs +++ b/Source/Editor/Surface/Archetypes/Math.cs @@ -16,7 +16,7 @@ namespace FlaxEditor.Surface.Archetypes { return Op1(id, title, desc, null, hints, type); } - + private static NodeArchetype Op1(ushort id, string title, string desc, string[] altTitles, ConnectionsHint hints = ConnectionsHint.Numeric, Type type = null) { return new NodeArchetype From 9fa0b174f52d5129baaa674822f74450d18a0f40 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 22 Oct 2023 15:32:56 +0200 Subject: [PATCH 117/546] Fix deprecation warnings on Apple --- Source/Engine/Platform/Unix/UnixNetwork.cpp | 4 ++++ Source/ThirdParty/stb/stb_image_write.h | 2 ++ 2 files changed, 6 insertions(+) diff --git a/Source/Engine/Platform/Unix/UnixNetwork.cpp b/Source/Engine/Platform/Unix/UnixNetwork.cpp index 5ffdb2c01..ae2d8d9f2 100644 --- a/Source/Engine/Platform/Unix/UnixNetwork.cpp +++ b/Source/Engine/Platform/Unix/UnixNetwork.cpp @@ -96,7 +96,11 @@ static bool CreateEndPointFromAddr(sockaddr* addr, NetworkEndPoint& endPoint) return true; } char strPort[6]; +#if __APPLE__ + snprintf(strPort, sizeof(strPort), "%d", port); +#else sprintf(strPort, "%d", port); +#endif endPoint.IPVersion = addr->sa_family == AF_INET6 ? NetworkIPVersion::IPv6 : NetworkIPVersion::IPv4; memcpy(endPoint.Data, addr, size); return false; diff --git a/Source/ThirdParty/stb/stb_image_write.h b/Source/ThirdParty/stb/stb_image_write.h index 95943eb60..8cae247eb 100644 --- a/Source/ThirdParty/stb/stb_image_write.h +++ b/Source/ThirdParty/stb/stb_image_write.h @@ -758,6 +758,8 @@ static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, f #ifdef __STDC_WANT_SECURE_LIB__ len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#elif __APPLE__ + len = snprintf(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); #else len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); #endif From 1280e61af0184585df5c640cc7d4a9a3cee03d36 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 22 Oct 2023 15:33:21 +0200 Subject: [PATCH 118/546] Add IsDebuggerPresent for macOS and iOS platforms --- .../Engine/Platform/Apple/ApplePlatform.cpp | 29 ++++++++++++++++++- Source/Engine/Platform/Apple/ApplePlatform.h | 3 ++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Platform/Apple/ApplePlatform.cpp b/Source/Engine/Platform/Apple/ApplePlatform.cpp index bf79c95c2..523f3b561 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.cpp +++ b/Source/Engine/Platform/Apple/ApplePlatform.cpp @@ -245,13 +245,40 @@ void ApplePlatform::GetUTCTime(int32& year, int32& month, int32& dayOfWeek, int3 millisecond = time.tv_usec / 1000; } +#if !BUILD_RELEASE + void ApplePlatform::Log(const StringView& msg) { -#if !BUILD_RELEASE && !USE_EDITOR +#if !USE_EDITOR NSLog(@"%s", StringAsANSI<>(*msg, msg.Length()).Get()); #endif } +bool ApplePlatform::IsDebuggerPresent() +{ + // Reference: https://developer.apple.com/library/archive/qa/qa1361/_index.html + int mib[4]; + struct kinfo_proc info; + + // Initialize the flags so that, if sysctl fails for some bizarre reason, we get a predictable result + info.kp_proc.p_flag = 0; + + // Initialize mib, which tells sysctl the info we want, in this case we're looking for information about a specific process ID + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + + // Call sysctl + size_t size = sizeof(info); + sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + + // We're being debugged if the P_TRACED flag is set + return ((info.kp_proc.p_flag & P_TRACED) != 0); +} + +#endif + bool ApplePlatform::Init() { if (UnixPlatform::Init()) diff --git a/Source/Engine/Platform/Apple/ApplePlatform.h b/Source/Engine/Platform/Apple/ApplePlatform.h index f1148d0b6..94b6534d6 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.h +++ b/Source/Engine/Platform/Apple/ApplePlatform.h @@ -79,7 +79,10 @@ public: static uint64 GetClockFrequency(); static void GetSystemTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); static void GetUTCTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); +#if !BUILD_RELEASE static void Log(const StringView& msg); + static bool IsDebuggerPresent(); +#endif static bool Init(); static void Tick(); static void BeforeExit(); From c88e184df363ab006428b2f40789783ff4ede30b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 22 Oct 2023 15:56:25 +0200 Subject: [PATCH 119/546] Fix crash when window gets removed during windows update loop --- Source/Editor/Managed/ManagedEditor.Internal.cpp | 4 +++- Source/Engine/Platform/Base/WindowsManager.cpp | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index e01fd04bb..969608676 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -513,7 +513,9 @@ DEFINE_INTERNAL_CALL(void) EditorInternal_RunVisualScriptBreakpointLoopTick(floa WindowsManager::WindowsLocker.Unlock(); } WindowsManager::WindowsLocker.Lock(); - for (auto& win : WindowsManager::Windows) + Array> windows; + windows.Add(WindowsManager::Windows); + for (Window* win : windows) { if (win->IsVisible()) win->OnUpdate(deltaTime); diff --git a/Source/Engine/Platform/Base/WindowsManager.cpp b/Source/Engine/Platform/Base/WindowsManager.cpp index 0d58008d8..34381b3d2 100644 --- a/Source/Engine/Platform/Base/WindowsManager.cpp +++ b/Source/Engine/Platform/Base/WindowsManager.cpp @@ -59,9 +59,11 @@ void WindowsManagerService::Update() // Update windows const float deltaTime = Time::Update.UnscaledDeltaTime.GetTotalSeconds(); WindowsManager::WindowsLocker.Lock(); - for (Window* win : WindowsManager::Windows) + Array> windows; + windows.Add(WindowsManager::Windows); + for (Window* win : windows) { - if (win && win->IsVisible()) + if (win->IsVisible()) win->OnUpdate(deltaTime); } WindowsManager::WindowsLocker.Unlock(); @@ -71,7 +73,8 @@ void WindowsManagerService::Dispose() { // Close remaining windows WindowsManager::WindowsLocker.Lock(); - auto windows = WindowsManager::Windows; + Array> windows; + windows.Add(WindowsManager::Windows); for (Window* win : windows) { win->Close(ClosingReason::EngineExit); From ccf6c28b02b0d6d140000391a8d440af8a977406 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 22 Oct 2023 19:55:20 +0200 Subject: [PATCH 120/546] Add interval to Apple autoreleasepool --- .../Engine/Platform/Apple/ApplePlatform.cpp | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Platform/Apple/ApplePlatform.cpp b/Source/Engine/Platform/Apple/ApplePlatform.cpp index 523f3b561..d4348b107 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.cpp +++ b/Source/Engine/Platform/Apple/ApplePlatform.cpp @@ -48,6 +48,7 @@ CPUInfo Cpu; String UserLocale; double SecondsPerCycle; NSAutoreleasePool* AutoreleasePool = nullptr; +int32 AutoreleasePoolInterval = 0; float ApplePlatform::ScreenScale = 1.0f; @@ -359,9 +360,13 @@ bool ApplePlatform::Init() void ApplePlatform::Tick() { - // TODO: do it once every X fames - [AutoreleasePool drain]; - AutoreleasePool = [[NSAutoreleasePool alloc] init]; + AutoreleasePoolInterval++; + if (AutoreleasePoolInterval >= 60) + { + AutoreleasePoolInterval = 0; + [AutoreleasePool drain]; + AutoreleasePool = [[NSAutoreleasePool alloc] init]; + } } void ApplePlatform::BeforeExit() @@ -370,6 +375,11 @@ void ApplePlatform::BeforeExit() void ApplePlatform::Exit() { + if (AutoreleasePool) + { + [AutoreleasePool drain]; + AutoreleasePool = nullptr; + } } void ApplePlatform::SetHighDpiAwarenessEnabled(bool enable) @@ -396,9 +406,7 @@ bool ApplePlatform::GetHasFocus() if (window->IsFocused()) return true; } - - // Default to true if has no windows open - return WindowsManager::Windows.IsEmpty(); + return false; } void ApplePlatform::CreateGuid(Guid& result) From 6ff3e0f48876757532e645132b6e6c7e5050fb2a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 22 Oct 2023 20:06:54 +0200 Subject: [PATCH 121/546] Various improvements to macOS platform --- Source/Editor/GUI/Docking/DockHintWindow.cs | 28 +++++++-------- .../Engine/Platform/Linux/LinuxPlatform.cpp | 4 +-- Source/Engine/Platform/Mac/MacWindow.cpp | 35 +++++++++++++------ Source/Engine/Platform/Mac/MacWindow.h | 6 ++-- Source/Engine/Platform/iOS/iOSPlatform.cpp | 2 +- 5 files changed, 42 insertions(+), 33 deletions(-) diff --git a/Source/Editor/GUI/Docking/DockHintWindow.cs b/Source/Editor/GUI/Docking/DockHintWindow.cs index 52c5dcd3c..7d459e163 100644 --- a/Source/Editor/GUI/Docking/DockHintWindow.cs +++ b/Source/Editor/GUI/Docking/DockHintWindow.cs @@ -83,6 +83,7 @@ namespace FlaxEditor.GUI.Docking // Enable hit window presentation Proxy.Window.RenderingEnabled = true; Proxy.Window.Show(); + Proxy.Window.Focus(); } /// @@ -270,15 +271,16 @@ namespace FlaxEditor.GUI.Docking // Cache dock rectangles var size = _rectDock.Size; var offset = _rectDock.Location; - float BorderMargin = 4.0f; - float ProxyHintWindowsSize2 = Proxy.HintWindowsSize * 0.5f; - float centerX = size.X * 0.5f; - float centerY = size.Y * 0.5f; - _rUpper = new Rectangle(centerX - ProxyHintWindowsSize2, BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rBottom = new Rectangle(centerX - ProxyHintWindowsSize2, size.Y - Proxy.HintWindowsSize - BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rLeft = new Rectangle(BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rRight = new Rectangle(size.X - Proxy.HintWindowsSize - BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rCenter = new Rectangle(centerX - ProxyHintWindowsSize2, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; + var borderMargin = 4.0f; + var hintWindowsSize = Proxy.HintWindowsSize * Platform.DpiScale; + var hintWindowsSize2 = hintWindowsSize * 0.5f; + var centerX = size.X * 0.5f; + var centerY = size.Y * 0.5f; + _rUpper = new Rectangle(centerX - hintWindowsSize2, borderMargin, hintWindowsSize, hintWindowsSize) + offset; + _rBottom = new Rectangle(centerX - hintWindowsSize2, size.Y - hintWindowsSize - borderMargin, hintWindowsSize, hintWindowsSize) + offset; + _rLeft = new Rectangle(borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; + _rRight = new Rectangle(size.X - hintWindowsSize - borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; + _rCenter = new Rectangle(centerX - hintWindowsSize2, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; // Hit test DockState toSet = DockState.Float; @@ -428,7 +430,6 @@ namespace FlaxEditor.GUI.Docking { if (Window == null) { - // Create proxy window var settings = CreateWindowSettings.Default; settings.Title = "DockHint.Window"; settings.Size = initSize; @@ -440,12 +441,10 @@ namespace FlaxEditor.GUI.Docking settings.IsRegularWindow = false; settings.SupportsTransparency = true; settings.ShowInTaskbar = false; - settings.ShowAfterFirstPaint = true; + settings.ShowAfterFirstPaint = false; settings.IsTopmost = true; Window = Platform.CreateWindow(ref settings); - - // Set opacity and background color Window.Opacity = 0.6f; Window.GUI.BackgroundColor = Style.Current.DragWindow; } @@ -465,7 +464,7 @@ namespace FlaxEditor.GUI.Docking var settings = CreateWindowSettings.Default; settings.Title = name; - settings.Size = new Float2(HintWindowsSize); + settings.Size = new Float2(HintWindowsSize * Platform.DpiScale); settings.AllowInput = false; settings.AllowMaximize = false; settings.AllowMinimize = false; @@ -479,7 +478,6 @@ namespace FlaxEditor.GUI.Docking settings.ShowAfterFirstPaint = false; win = Platform.CreateWindow(ref settings); - win.Opacity = 0.6f; win.GUI.BackgroundColor = Style.Current.DragWindow; } diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp index 4787b4549..36affd8c8 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp +++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp @@ -2622,9 +2622,7 @@ bool LinuxPlatform::GetHasFocus() if (window->IsFocused()) return true; } - - // Default to true if has no windows open - return WindowsManager::Windows.IsEmpty(); + return false; } bool LinuxPlatform::CanOpenUrl(const StringView& url) diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index 08a4e0d2f..ea38a635c 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -710,6 +710,8 @@ MacWindow::MacWindow(const CreateWindowSettings& settings) { styleMask |= NSWindowStyleMaskBorderless; } + if (settings.Fullscreen) + styleMask |= NSWindowStyleMaskFullScreen; if (settings.HasBorder) { styleMask |= NSWindowStyleMaskTitled; @@ -736,7 +738,8 @@ MacWindow::MacWindow(const CreateWindowSettings& settings) [window setMaxSize:NSMakeSize(settings.MaximumSize.X, settings.MaximumSize.Y)]; [window setOpaque:!settings.SupportsTransparency]; [window setContentView:view]; - [window setAcceptsMouseMovedEvents:YES]; + if (settings.AllowInput) + [window setAcceptsMouseMovedEvents:YES]; [window setDelegate:window]; _window = window; _view = view; @@ -751,8 +754,6 @@ MacWindow::MacWindow(const CreateWindowSettings& settings) layer.contentsScale = screenScale; // TODO: impl Parent for MacWindow - // TODO: impl StartPosition for MacWindow - // TODO: impl Fullscreen for MacWindow // TODO: impl ShowInTaskbar for MacWindow // TODO: impl IsTopmost for MacWindow } @@ -816,7 +817,10 @@ void MacWindow::Show() // Show NSWindow* window = (NSWindow*)_window; - [window makeKeyAndOrderFront:window]; + if (_settings.AllowInput) + [window makeKeyAndOrderFront:window]; + else + [window orderFront:window]; if (_settings.ActivateWhenFirstShown) [NSApp activateIgnoringOtherApps:YES]; _focused = true; @@ -870,14 +874,9 @@ void MacWindow::Restore() [window zoom:nil]; } -bool MacWindow::IsClosed() const -{ - return _window != nullptr; -} - bool MacWindow::IsForegroundWindow() const { - return Platform::GetHasFocus() && IsFocused(); + return IsFocused() && Platform::GetHasFocus(); } void MacWindow::BringToFront(bool force) @@ -1039,6 +1038,22 @@ DragDropEffect MacWindow::DoDragDrop(const StringView& data) return result; } +void MacWindow::StartTrackingMouse(bool useMouseScreenOffset) +{ + if (_isTrackingMouse || !_window) + return; + _isTrackingMouse = true; + _trackingMouseOffset = Float2::Zero; + _isUsingMouseOffset = useMouseScreenOffset; +} + +void MacWindow::EndTrackingMouse() +{ + if (!_isTrackingMouse || !_window) + return; + _isTrackingMouse = false; +} + void MacWindow::SetCursor(CursorType type) { CursorType prev = _cursor; diff --git a/Source/Engine/Platform/Mac/MacWindow.h b/Source/Engine/Platform/Mac/MacWindow.h index 7c3f091b4..eb1389f29 100644 --- a/Source/Engine/Platform/Mac/MacWindow.h +++ b/Source/Engine/Platform/Mac/MacWindow.h @@ -13,14 +13,12 @@ class FLAXENGINE_API MacWindow : public WindowBase { private: - void* _window = nullptr; void* _view = nullptr; bool _isMouseOver = false; String _dragText; public: - MacWindow(const CreateWindowSettings& settings); ~MacWindow(); @@ -32,7 +30,6 @@ public: } public: - // [WindowBase] void* GetNativePtr() const override; void Show() override; @@ -40,7 +37,6 @@ public: void Minimize() override; void Maximize() override; void Restore() override; - bool IsClosed() const override; bool IsForegroundWindow() const override; void BringToFront(bool force = false) override; void SetClientBounds(const Rectangle& clientArea) override; @@ -56,6 +52,8 @@ public: void Focus() override; void SetTitle(const StringView& title) override; DragDropEffect DoDragDrop(const StringView& data) override; + void StartTrackingMouse(bool useMouseScreenOffset) override; + void EndTrackingMouse() override; void SetCursor(CursorType type) override; }; diff --git a/Source/Engine/Platform/iOS/iOSPlatform.cpp b/Source/Engine/Platform/iOS/iOSPlatform.cpp index 7ca27c481..26c3b8f28 100644 --- a/Source/Engine/Platform/iOS/iOSPlatform.cpp +++ b/Source/Engine/Platform/iOS/iOSPlatform.cpp @@ -410,7 +410,7 @@ bool iOSWindow::IsClosed() const bool iOSWindow::IsForegroundWindow() const { - return Platform::GetHasFocus() && IsFocused(); + return IsFocused() && Platform::GetHasFocus(); } void iOSWindow::BringToFront(bool force) From fbaf14b6fa963eb3513b450cce63faf44fb961e2 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sun, 22 Oct 2023 21:58:20 -0500 Subject: [PATCH 122/546] Add to be able to specify order of plugin initialization and deinitialization. --- .../Scripting/Attributes/PluginLoadOrder.cs | 21 + .../Scripting/Plugins/PluginManager.cpp | 480 +++++++++++++++++- 2 files changed, 473 insertions(+), 28 deletions(-) create mode 100644 Source/Engine/Scripting/Attributes/PluginLoadOrder.cs diff --git a/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs b/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs new file mode 100644 index 000000000..d1d02d024 --- /dev/null +++ b/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs @@ -0,0 +1,21 @@ +using System; + +namespace FlaxEngine; + +/// +/// This attribute allows for specifying initialization and deinitialization order for plugins +/// +[Serializable] +[AttributeUsage(AttributeTargets.Class)] +public class PluginLoadOrderAttribute : Attribute +{ + /// + /// The plugin type to initialize this plugin after. + /// + public Type InitializeAfter; + + /// + /// The plugin type to deinitialize this plugin before. + /// + public Type DeinitializeBefore; +} diff --git a/Source/Engine/Scripting/Plugins/PluginManager.cpp b/Source/Engine/Scripting/Plugins/PluginManager.cpp index a8198474c..124e842eb 100644 --- a/Source/Engine/Scripting/Plugins/PluginManager.cpp +++ b/Source/Engine/Scripting/Plugins/PluginManager.cpp @@ -13,6 +13,7 @@ #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Engine/EngineService.h" #include "Engine/Core/Log.h" +#include "Engine/Scripting/ManagedCLR/MField.h" Plugin::Plugin(const SpawnParams& params) : ScriptingObject(params) @@ -145,19 +146,20 @@ void PluginManagerImpl::LoadPlugin(MClass* klass, bool isEditor) auto plugin = (Plugin*)Scripting::NewObject(klass); if (!plugin) return; - +/* + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); + ASSERT(afterTypeField); +*/ if (!isEditor) { GamePlugins.Add((GamePlugin*)plugin); -#if !USE_EDITOR - PluginManagerService::InvokeInitialize(plugin); -#endif } #if USE_EDITOR else { EditorPlugins.Add(plugin); - PluginManagerService::InvokeInitialize(plugin); } #endif PluginManager::PluginsChanged(); @@ -209,23 +211,104 @@ void PluginManagerImpl::OnAssemblyLoaded(MAssembly* assembly) void PluginManagerImpl::OnAssemblyUnloading(MAssembly* assembly) { bool changed = false; - for (int32 i = EditorPlugins.Count() - 1; i >= 0 && EditorPlugins.Count() > 0; i--) + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(afterTypeField); + + // Sort editor plugins + Array editorPlugins; + for(int i = 0; i < EditorPlugins.Count(); i++) { - auto plugin = EditorPlugins[i]; + Plugin* plugin = EditorPlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < editorPlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = editorPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + editorPlugins.Add(plugin); + else + editorPlugins.Insert(insertIndex, plugin); + } + + for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) + { + auto plugin = editorPlugins[i]; if (plugin->GetType().ManagedClass->GetAssembly() == assembly) { PluginManagerService::InvokeDeinitialize(plugin); - EditorPlugins.RemoveAtKeepOrder(i); + EditorPlugins.Remove(plugin); changed = true; } } - for (int32 i = GamePlugins.Count() - 1; i >= 0 && GamePlugins.Count() > 0; i--) + + // Sort game plugins + Array gamePlugins; + for(int i = 0; i < GamePlugins.Count(); i++) { - auto plugin = GamePlugins[i]; + GamePlugin* plugin = GamePlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < gamePlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + gamePlugins.Add(plugin); + else + gamePlugins.Insert(insertIndex, plugin); + } + + for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) + { + auto plugin = gamePlugins[i]; if (plugin->GetType().ManagedClass->GetAssembly() == assembly) { PluginManagerService::InvokeDeinitialize(plugin); - GamePlugins.RemoveAtKeepOrder(i); + GamePlugins.Remove(plugin); changed = true; } } @@ -261,21 +344,102 @@ void PluginManagerImpl::OnScriptsReloading() { // When scripting is reloading (eg. for hot-reload in Editor) we have to deinitialize plugins (Scripting service destroys C# objects later on) bool changed = false; - for (int32 i = EditorPlugins.Count() - 1; i >= 0 && EditorPlugins.Count() > 0; i--) + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(afterTypeField); + + // Sort editor plugins + Array editorPlugins; + for(int i = 0; i < EditorPlugins.Count(); i++) { - auto plugin = EditorPlugins[i]; + Plugin* plugin = EditorPlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < editorPlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = editorPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + editorPlugins.Add(plugin); + else + editorPlugins.Insert(insertIndex, plugin); + } + + for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) + { + auto plugin = editorPlugins[i]; { PluginManagerService::InvokeDeinitialize(plugin); - EditorPlugins.RemoveAtKeepOrder(i); + EditorPlugins.Remove(plugin); changed = true; } } - for (int32 i = GamePlugins.Count() - 1; i >= 0 && GamePlugins.Count() > 0; i--) + + // Sort game plugins + Array gamePlugins; + for(int i = 0; i < GamePlugins.Count(); i++) { - auto plugin = GamePlugins[i]; + GamePlugin* plugin = GamePlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < gamePlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + gamePlugins.Add(plugin); + else + gamePlugins.Insert(insertIndex, plugin); + } + + for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) + { + auto plugin = gamePlugins[i]; { PluginManagerService::InvokeDeinitialize(plugin); - GamePlugins.RemoveAtKeepOrder(i); + GamePlugins.Remove(plugin); changed = true; } } @@ -291,6 +455,102 @@ bool PluginManagerService::Init() OnBinaryModuleLoaded(module); } + // Initialize plugins + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); + ASSERT(afterTypeField); + +#if !USE_EDITOR + // Sort game plugins + Array gamePlugins; + for(int i = 0; i < GamePlugins.Count(); i++) + { + GamePlugin* plugin = GamePlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < gamePlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + gamePlugins.Add(plugin); + else + gamePlugins.Insert(insertIndex, plugin); + } + + // Initalize game plugins + for (auto plugin : gamePlugins) + { + PluginManagerService::InvokeInitialize(plugin); + } +#endif + +#if USE_EDITOR + // Sort editor plugins + Array editorPlugins; + for(int i = 0; i < EditorPlugins.Count(); i++) + { + Plugin* plugin = EditorPlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < editorPlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = editorPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + editorPlugins.Add(plugin); + else + editorPlugins.Insert(insertIndex, plugin); + } + + // Initialize editor plugins + for (auto plugin : editorPlugins) + { + PluginManagerService::InvokeInitialize(plugin); + } +#endif + // Register for new binary modules load actions Scripting::BinaryModuleLoaded.Bind(&OnBinaryModuleLoaded); Scripting::ScriptsReloading.Bind(&OnScriptsReloading); @@ -309,17 +569,97 @@ void PluginManagerService::Dispose() if (pluginsCount == 0) return; LOG(Info, "Unloading {0} plugins", pluginsCount); - for (int32 i = EditorPlugins.Count() - 1; i >= 0 && EditorPlugins.Count() > 0; i--) + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(afterTypeField); + + // Sort editor plugins + Array editorPlugins; + for(int i = 0; i < EditorPlugins.Count(); i++) { - auto plugin = EditorPlugins[i]; - InvokeDeinitialize(plugin); - EditorPlugins.RemoveAtKeepOrder(i); + Plugin* plugin = EditorPlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < editorPlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = editorPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + editorPlugins.Add(plugin); + else + editorPlugins.Insert(insertIndex, plugin); } - for (int32 i = GamePlugins.Count() - 1; i >= 0 && GamePlugins.Count() > 0; i--) + + for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) { - auto plugin = GamePlugins[i]; + auto plugin = editorPlugins[i]; InvokeDeinitialize(plugin); - GamePlugins.RemoveAtKeepOrder(i); + EditorPlugins.Remove(plugin); + } + + // Sort game plugins + Array gamePlugins; + for(int i = 0; i < GamePlugins.Count(); i++) + { + GamePlugin* plugin = GamePlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < gamePlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + gamePlugins.Add(plugin); + else + gamePlugins.Insert(insertIndex, plugin); + } + for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) + { + auto plugin = gamePlugins[i]; + InvokeDeinitialize(plugin); + GamePlugins.Remove(plugin); } PluginManager::PluginsChanged(); } @@ -386,18 +726,102 @@ Plugin* PluginManager::GetPlugin(const ScriptingTypeHandle& type) void PluginManager::InitializeGamePlugins() { PROFILE_CPU(); - for (int32 i = 0; i < GamePlugins.Count(); i++) + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); + ASSERT(afterTypeField); + + // Sort game plugins + Array gamePlugins; + for(int i = 0; i < GamePlugins.Count(); i++) { - PluginManagerService::InvokeInitialize(GamePlugins[i]); + GamePlugin* plugin = GamePlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < gamePlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + gamePlugins.Add(plugin); + else + gamePlugins.Insert(insertIndex, plugin); + } + for (int32 i = 0; i < gamePlugins.Count(); i++) + { + PluginManagerService::InvokeInitialize(gamePlugins[i]); } } void PluginManager::DeinitializeGamePlugins() { PROFILE_CPU(); - for (int32 i = GamePlugins.Count() - 1; i >= 0; i--) + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(afterTypeField); + + // Sort game plugins + Array gamePlugins; + for(int i = 0; i < GamePlugins.Count(); i++) { - PluginManagerService::InvokeDeinitialize(GamePlugins[i]); + GamePlugin* plugin = GamePlugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < gamePlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + afterTypeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + gamePlugins.Add(plugin); + else + gamePlugins.Insert(insertIndex, plugin); + } + for (int32 i = gamePlugins.Count() - 1; i >= 0; i--) + { + PluginManagerService::InvokeDeinitialize(gamePlugins[i]); } } From 2e85ff0fb3c479af85120dd0e452ace02a5bd898 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sun, 22 Oct 2023 22:19:51 -0500 Subject: [PATCH 123/546] Simplify code --- .../Scripting/Plugins/PluginManager.cpp | 479 ++++-------------- 1 file changed, 104 insertions(+), 375 deletions(-) diff --git a/Source/Engine/Scripting/Plugins/PluginManager.cpp b/Source/Engine/Scripting/Plugins/PluginManager.cpp index 124e842eb..77a3467fd 100644 --- a/Source/Engine/Scripting/Plugins/PluginManager.cpp +++ b/Source/Engine/Scripting/Plugins/PluginManager.cpp @@ -83,6 +83,8 @@ namespace PluginManagerImpl void OnAssemblyUnloading(MAssembly* assembly); void OnBinaryModuleLoaded(BinaryModule* module); void OnScriptsReloading(); + Array SortGamePlugins(Array& plugins, MClass* pluginLoadOrderAttribute, MField* typeField); + Array SortPlugins(Array plugins, MClass* pluginLoadOrderAttribute, MField* typeField); } using namespace PluginManagerImpl; @@ -146,12 +148,7 @@ void PluginManagerImpl::LoadPlugin(MClass* klass, bool isEditor) auto plugin = (Plugin*)Scripting::NewObject(klass); if (!plugin) return; -/* - auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; - auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); - auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); - ASSERT(afterTypeField); -*/ + if (!isEditor) { GamePlugins.Add((GamePlugin*)plugin); @@ -214,45 +211,10 @@ void PluginManagerImpl::OnAssemblyUnloading(MAssembly* assembly) auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); - auto afterTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); - ASSERT(afterTypeField); + auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(beforeTypeField); - // Sort editor plugins - Array editorPlugins; - for(int i = 0; i < EditorPlugins.Count(); i++) - { - Plugin* plugin = EditorPlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < editorPlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = editorPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - editorPlugins.Add(plugin); - else - editorPlugins.Insert(insertIndex, plugin); - } + auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) { @@ -265,42 +227,7 @@ void PluginManagerImpl::OnAssemblyUnloading(MAssembly* assembly) } } - // Sort game plugins - Array gamePlugins; - for(int i = 0; i < GamePlugins.Count(); i++) - { - GamePlugin* plugin = GamePlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < gamePlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - gamePlugins.Add(plugin); - else - gamePlugins.Insert(insertIndex, plugin); - } + auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) { @@ -347,45 +274,10 @@ void PluginManagerImpl::OnScriptsReloading() auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); - auto afterTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); - ASSERT(afterTypeField); + auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(beforeTypeField); - // Sort editor plugins - Array editorPlugins; - for(int i = 0; i < EditorPlugins.Count(); i++) - { - Plugin* plugin = EditorPlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < editorPlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = editorPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - editorPlugins.Add(plugin); - else - editorPlugins.Insert(insertIndex, plugin); - } + auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) { @@ -397,42 +289,7 @@ void PluginManagerImpl::OnScriptsReloading() } } - // Sort game plugins - Array gamePlugins; - for(int i = 0; i < GamePlugins.Count(); i++) - { - GamePlugin* plugin = GamePlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < gamePlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - gamePlugins.Add(plugin); - else - gamePlugins.Insert(insertIndex, plugin); - } + auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) { @@ -447,6 +304,88 @@ void PluginManagerImpl::OnScriptsReloading() PluginManager::PluginsChanged(); } +Array PluginManagerImpl::SortGamePlugins(Array& plugins, MClass* pluginLoadOrderAttribute, MField* typeField) +{ + // Sort game plugins + Array newPlugins; + for(int i = 0; i < plugins.Count(); i++) + { + GamePlugin* plugin = plugins[i]; + // Sort game plugin as needed + int insertIndex = -1; + for(int j = 0; j < newPlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = newPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + typeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + newPlugins.Add(plugin); + else + newPlugins.Insert(insertIndex, plugin); + } + return newPlugins; +} + +Array PluginManagerImpl::SortPlugins(Array plugins, MClass* pluginLoadOrderAttribute, MField* typeField) +{ + // Sort plugins + Array newPlugins; + for(int i = 0; i < plugins.Count(); i++) + { + Plugin* plugin = plugins[i]; + // Sort plugin as needed + int insertIndex = -1; + for(int j = 0; j < newPlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = newPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + typeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + newPlugins.Add(plugin); + else + newPlugins.Insert(insertIndex, plugin); + } + return newPlugins; +} + bool PluginManagerService::Init() { // Process already loaded modules @@ -460,44 +399,9 @@ bool PluginManagerService::Init() auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); ASSERT(afterTypeField); - + #if !USE_EDITOR - // Sort game plugins - Array gamePlugins; - for(int i = 0; i < GamePlugins.Count(); i++) - { - GamePlugin* plugin = GamePlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < gamePlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - gamePlugins.Add(plugin); - else - gamePlugins.Insert(insertIndex, plugin); - } + auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); // Initalize game plugins for (auto plugin : gamePlugins) @@ -507,42 +411,7 @@ bool PluginManagerService::Init() #endif #if USE_EDITOR - // Sort editor plugins - Array editorPlugins; - for(int i = 0; i < EditorPlugins.Count(); i++) - { - Plugin* plugin = EditorPlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < editorPlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = editorPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - editorPlugins.Add(plugin); - else - editorPlugins.Insert(insertIndex, plugin); - } + auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, afterTypeField); // Initialize editor plugins for (auto plugin : editorPlugins) @@ -572,45 +441,10 @@ void PluginManagerService::Dispose() auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); - auto afterTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); - ASSERT(afterTypeField); + auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(beforeTypeField); - // Sort editor plugins - Array editorPlugins; - for(int i = 0; i < EditorPlugins.Count(); i++) - { - Plugin* plugin = EditorPlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < editorPlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = editorPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - editorPlugins.Add(plugin); - else - editorPlugins.Insert(insertIndex, plugin); - } + auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) { @@ -619,42 +453,7 @@ void PluginManagerService::Dispose() EditorPlugins.Remove(plugin); } - // Sort game plugins - Array gamePlugins; - for(int i = 0; i < GamePlugins.Count(); i++) - { - GamePlugin* plugin = GamePlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < gamePlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - gamePlugins.Add(plugin); - else - gamePlugins.Insert(insertIndex, plugin); - } + auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) { auto plugin = gamePlugins[i]; @@ -731,43 +530,8 @@ void PluginManager::InitializeGamePlugins() auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); ASSERT(afterTypeField); - - // Sort game plugins - Array gamePlugins; - for(int i = 0; i < GamePlugins.Count(); i++) - { - GamePlugin* plugin = GamePlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < gamePlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - gamePlugins.Add(plugin); - else - gamePlugins.Insert(insertIndex, plugin); - } + auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); for (int32 i = 0; i < gamePlugins.Count(); i++) { PluginManagerService::InvokeInitialize(gamePlugins[i]); @@ -780,45 +544,10 @@ void PluginManager::DeinitializeGamePlugins() auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); - auto afterTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); - ASSERT(afterTypeField); - - // Sort game plugins - Array gamePlugins; - for(int i = 0; i < GamePlugins.Count(); i++) - { - GamePlugin* plugin = GamePlugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < gamePlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = gamePlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - afterTypeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; + auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(beforeTypeField); - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - gamePlugins.Add(plugin); - else - gamePlugins.Insert(insertIndex, plugin); - } + auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = gamePlugins.Count() - 1; i >= 0; i--) { PluginManagerService::InvokeDeinitialize(gamePlugins[i]); From f28947f59b61f6322a180ea027d26b3bd53b0375 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 23 Oct 2023 14:50:10 +0200 Subject: [PATCH 124/546] Fix MacWindow::SetClientBounds to include screen scale --- Source/Engine/Platform/Mac/MacWindow.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index ea38a635c..0274ecb9a 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -895,14 +895,13 @@ void MacWindow::SetClientBounds(const Rectangle& clientArea) NSWindow* window = (NSWindow*)_window; if (!window) return; + const float screenScale = MacPlatform::ScreenScale; + NSRect oldRect = [window frame]; - NSRect newRect = NSMakeRect(0, 0, clientArea.Size.X, clientArea.Size.Y); + NSRect newRect = NSMakeRect(0, 0, clientArea.Size.X / screenScale, clientArea.Size.Y / screenScale); newRect = [window frameRectForContentRect:newRect]; - //newRect.origin.x = oldRect.origin.x; - //newRect.origin.y = NSMaxY(oldRect) - newRect.size.height; - - Float2 pos = AppleUtils::PosToCoca(clientArea.Location); + Float2 pos = AppleUtils::PosToCoca(clientArea.Location) / screenScale; Float2 titleSize = GetWindowTitleSize(this); newRect.origin.x = pos.X + titleSize.X; newRect.origin.y = pos.Y - newRect.size.height + titleSize.Y; From 9c4382dffb31de90c571b75a241c120171bb158c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 23 Oct 2023 15:59:09 +0200 Subject: [PATCH 125/546] Fix using dock window panels on macOS in Editor --- Source/Editor/GUI/Docking/DockHintWindow.cs | 10 ++--- Source/Engine/Platform/Mac/MacWindow.cpp | 44 ++++++++++++++++++--- Source/Engine/Platform/Mac/MacWindow.h | 3 ++ 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/Source/Editor/GUI/Docking/DockHintWindow.cs b/Source/Editor/GUI/Docking/DockHintWindow.cs index 7d459e163..6e2353441 100644 --- a/Source/Editor/GUI/Docking/DockHintWindow.cs +++ b/Source/Editor/GUI/Docking/DockHintWindow.cs @@ -44,11 +44,11 @@ namespace FlaxEditor.GUI.Docking var mousePos = window.MousePosition; var previousSize = window.Size; window.Restore(); - window.Position = FlaxEngine.Input.MouseScreenPosition - mousePos * window.Size / previousSize; + window.Position = Platform.MousePosition - mousePos * window.Size / previousSize; } // Calculate dragging offset and move window to the destination position - var mouseScreenPosition = FlaxEngine.Input.MouseScreenPosition; + var mouseScreenPosition = Platform.MousePosition; // If the _toMove window was not focused when initializing this window, the result vector only contains zeros // and to prevent a failure, we need to perform an update for the drag offset at later time which will be done in the OnMouseMove event handler. @@ -114,7 +114,7 @@ namespace FlaxEditor.GUI.Docking var window = _toMove.Window?.Window; if (window == null) return; - var mouse = FlaxEngine.Input.MouseScreenPosition; + var mouse = Platform.MousePosition; // Move base window window.Position = mouse - _dragOffset; @@ -194,7 +194,7 @@ namespace FlaxEditor.GUI.Docking // Move window to the mouse position (with some offset for caption bar) var window = (WindowRootControl)toMove.Root; - var mouse = FlaxEngine.Input.MouseScreenPosition; + var mouse = Platform.MousePosition; window.Window.Position = mouse - new Float2(8, 8); // Get floating panel @@ -245,7 +245,7 @@ namespace FlaxEditor.GUI.Docking private void UpdateRects() { // Cache mouse position - _mouse = FlaxEngine.Input.MouseScreenPosition; + _mouse = Platform.MousePosition; // Check intersection with any dock panel var uiMouse = _mouse; diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index 0274ecb9a..38e8c95df 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -498,7 +498,9 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) { if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); - if (!Window->IsMouseTracking() && !IsMouseOver) + if (Window->IsMouseTracking()) + return; // Skip mouse events when tracking mouse (handled in MacWindow::OnUpdate) + if (!IsMouseOver) return; Input::Mouse->OnMouseMove(Window->ClientToScreen(mousePos), Window); } @@ -521,11 +523,12 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) { if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); + mousePos = Window->ClientToScreen(mousePos); MouseButton mouseButton = MouseButton::Left; if ([event clickCount] == 2) - Input::Mouse->OnMouseDoubleClick(Window->ClientToScreen(mousePos), mouseButton, Window); + Input::Mouse->OnMouseDoubleClick(mousePos, mouseButton, Window); else - Input::Mouse->OnMouseDown(Window->ClientToScreen(mousePos), mouseButton, Window); + Input::Mouse->OnMouseDown(mousePos, mouseButton, Window); } - (void)mouseDragged:(NSEvent*)event @@ -537,8 +540,21 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) { if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); + mousePos = Window->ClientToScreen(mousePos); MouseButton mouseButton = MouseButton::Left; - Input::Mouse->OnMouseUp(Window->ClientToScreen(mousePos), mouseButton, Window); + Input::Mouse->OnMouseUp(mousePos, mouseButton, Window); + + // Redirect event to any window that tracks the mouse (eg. dock window in Editor) + WindowsManager::WindowsLocker.Lock(); + for (auto* win : WindowsManager::Windows) + { + if (win->IsVisible() && win->IsMouseTracking() && win != Window) + { + Input::Mouse->OnMouseUp(mousePos, mouseButton, win); + break; + } + } + WindowsManager::WindowsLocker.Unlock(); } - (void)rightMouseDown:(NSEvent*)event @@ -788,7 +804,6 @@ void MacWindow::SetIsMouseOver(bool value) // Refresh cursor typet SetCursor(CursorType::Default); SetCursor(cursor); - } else { @@ -803,6 +818,22 @@ void* MacWindow::GetNativePtr() const return _window; } +void MacWindow::OnUpdate(float dt) +{ + if (IsMouseTracking()) + { + // Keep sending mouse movement events no matter if window has focus + Float2 mousePos = Platform::GetMousePosition(); + if (_mouseTrackPos != mousePos) + { + _mouseTrackPos = mousePos; + Input::Mouse->OnMouseMove(mousePos, this); + } + } + + WindowBase::OnUpdate(dt); +} + void MacWindow::Show() { if (!_visible) @@ -838,7 +869,7 @@ void MacWindow::Hide() // Hide NSWindow* window = (NSWindow*)_window; - [window orderOut:nil]; + [window orderOut:window]; // Base WindowBase::Hide(); @@ -1044,6 +1075,7 @@ void MacWindow::StartTrackingMouse(bool useMouseScreenOffset) _isTrackingMouse = true; _trackingMouseOffset = Float2::Zero; _isUsingMouseOffset = useMouseScreenOffset; + _mouseTrackPos = Float2::Minimum; } void MacWindow::EndTrackingMouse() diff --git a/Source/Engine/Platform/Mac/MacWindow.h b/Source/Engine/Platform/Mac/MacWindow.h index eb1389f29..ccd43b354 100644 --- a/Source/Engine/Platform/Mac/MacWindow.h +++ b/Source/Engine/Platform/Mac/MacWindow.h @@ -6,6 +6,7 @@ #include "Engine/Platform/Base/WindowBase.h" #include "Engine/Platform/Platform.h" +#include "Engine/Core/Math/Vector2.h" /// /// Implementation of the window class for Mac platform. @@ -16,6 +17,7 @@ private: void* _window = nullptr; void* _view = nullptr; bool _isMouseOver = false; + Float2 _mouseTrackPos = Float2::Minimum; String _dragText; public: @@ -32,6 +34,7 @@ public: public: // [WindowBase] void* GetNativePtr() const override; + void OnUpdate(float dt) override; void Show() override; void Hide() override; void Minimize() override; From aed81b58bae0d0341d01b3bab7b259d49973cd6e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 20 Oct 2023 14:20:07 +0200 Subject: [PATCH 126/546] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fac631a6a..d6688bd03 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Flax Engine is a high quality modern 3D game engine written in C++ and C#. -From stunning graphics to powerful scripts - Flax can give everything for your games. Designed for fast workflow with many ready to use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)). +From stunning graphics to powerful scripts, it's designed for fast workflow with many ready-to-use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)). This repository contains full source code of the Flax Engine (excluding NDA-protected platforms support). Anyone is welcome to contribute or use the modified source in Flax-based games. From 9d1ba6cacfc7f2e812c544f3b67a51c17cdb6f9e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 23 Oct 2023 22:10:21 +0200 Subject: [PATCH 127/546] Fix compilation errors when building with old `Delegate` impl --- Source/Engine/Core/ObjectsRemovalService.cpp | 1 + Source/Engine/Level/SceneObjectsFactory.cpp | 1 + Source/Engine/Navigation/NavCrowd.cpp | 1 + 3 files changed, 3 insertions(+) diff --git a/Source/Engine/Core/ObjectsRemovalService.cpp b/Source/Engine/Core/ObjectsRemovalService.cpp index a020f7844..a185ce8b3 100644 --- a/Source/Engine/Core/ObjectsRemovalService.cpp +++ b/Source/Engine/Core/ObjectsRemovalService.cpp @@ -5,6 +5,7 @@ #include "Collections/Dictionary.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/EngineService.h" +#include "Engine/Platform/CriticalSection.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Scripting/ScriptingObject.h" diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index 5632d8bc7..3bf0f68a1 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -16,6 +16,7 @@ #include "Engine/Threading/ThreadLocal.h" #if !BUILD_RELEASE || USE_EDITOR #include "Engine/Level/Level.h" +#include "Engine/Threading/Threading.h" #endif SceneObjectsFactory::Context::Context(ISerializeModifier* modifier) diff --git a/Source/Engine/Navigation/NavCrowd.cpp b/Source/Engine/Navigation/NavCrowd.cpp index b05f0f7d3..fe1c2f728 100644 --- a/Source/Engine/Navigation/NavCrowd.cpp +++ b/Source/Engine/Navigation/NavCrowd.cpp @@ -7,6 +7,7 @@ #include "Engine/Level/Level.h" #include "Engine/Level/Scene/Scene.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Threading/Threading.h" #include NavCrowd::NavCrowd(const SpawnParams& params) From ba374a27db88a3fe342efb404e513313aab478cb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 23 Oct 2023 22:26:55 +0200 Subject: [PATCH 128/546] Reduce code bloat --- Source/Engine/Core/Delegate.h | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h index 4e081faef..821d7916e 100644 --- a/Source/Engine/Core/Delegate.h +++ b/Source/Engine/Core/Delegate.h @@ -583,18 +583,9 @@ public: template void Unbind() { -#if DELEGATE_USE_ATOMIC FunctionType f; f.template Bind(); Unbind(f); -#else - if (_functions == nullptr) - return; - FunctionType f; - f.template Bind(); - ScopeLock lock(*_locker); - _functions->Remove(f); -#endif } /// @@ -604,18 +595,9 @@ public: template void Unbind(T* callee) { -#if DELEGATE_USE_ATOMIC FunctionType f; f.template Bind(callee); Unbind(f); -#else - if (_functions == nullptr) - return; - FunctionType f; - f.template Bind(callee); - ScopeLock lock(*_locker); - _functions->Remove(f); -#endif } /// @@ -624,16 +606,8 @@ public: /// The method. void Unbind(Signature method) { -#if DELEGATE_USE_ATOMIC FunctionType f(method); Unbind(f); -#else - if (_functions == nullptr) - return; - FunctionType f(method); - ScopeLock lock(*_locker); - _functions->Remove(f); -#endif } /// From 898296125424820576c5fcc5e7bedeb1b8bbb5f4 Mon Sep 17 00:00:00 2001 From: Menotdan <32620310+Menotdan@users.noreply.github.com> Date: Mon, 23 Oct 2023 22:08:50 -0400 Subject: [PATCH 129/546] Add custom material option to Material Preview. --- .../Viewport/Previews/MaterialPreview.cs | 74 ++++++++++++++++--- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/Source/Editor/Viewport/Previews/MaterialPreview.cs b/Source/Editor/Viewport/Previews/MaterialPreview.cs index 5520d6c4c..f05c8413b 100644 --- a/Source/Editor/Viewport/Previews/MaterialPreview.cs +++ b/Source/Editor/Viewport/Previews/MaterialPreview.cs @@ -7,6 +7,9 @@ using FlaxEngine.GUI; using FlaxEditor.Viewport.Widgets; using FlaxEditor.GUI.ContextMenu; using Object = FlaxEngine.Object; +using FlaxEditor.CustomEditors.Editors; +using FlaxEditor.GUI; +using FlaxEditor.Scripting; namespace FlaxEditor.Viewport.Previews { @@ -49,6 +52,8 @@ namespace FlaxEditor.Viewport.Previews private Image _guiMaterialControl; private readonly MaterialBase[] _postFxMaterialsCache = new MaterialBase[1]; private ContextMenu _modelWidgetButtonMenu; + private AssetPicker _customModelPicker; + private Model _customModel; /// /// Gets or sets the material asset to preview. It can be or . @@ -74,15 +79,72 @@ namespace FlaxEditor.Viewport.Previews get => _selectedModelIndex; set { + if (value == -1) // Using Custom Model + { + return; + } + if (value < 0 || value > Models.Length) throw new ArgumentOutOfRangeException(); + if (_customModelPicker != null) + { + _customModelPicker.SelectedAsset = null; + } _selectedModelIndex = value; _previewModel.Model = FlaxEngine.Content.LoadAsyncInternal("Editor/Primitives/" + Models[value]); _previewModel.Transform = Transforms[value]; } } + // Used to automatically update which entry is checked. + // TODO: Maybe a better system with predicate bool checks could be used? + private void ResetModelContextMenu() + { + _modelWidgetButtonMenu.ItemsContainer.DisposeChildren(); + + // Fill out all models + for (int i = 0; i < Models.Length; i++) + { + var index = i; + var button = _modelWidgetButtonMenu.AddButton(Models[index]); + button.ButtonClicked += _ => SelectedModelIndex = index; + button.Checked = SelectedModelIndex == index && _customModel == null; + button.Tag = index; + } + + _modelWidgetButtonMenu.AddSeparator(); + _customModelPicker = new AssetPicker(new ScriptType(typeof(Model)), Float2.Zero); + + // Label Button + var customModelPickerLabel = _modelWidgetButtonMenu.AddButton("Custom Model:"); + customModelPickerLabel.CloseMenuOnClick = false; + customModelPickerLabel.Checked = _customModel != null; + + // Container button + var customModelPickerButton = _modelWidgetButtonMenu.AddButton(""); + + customModelPickerButton.Height = _customModelPicker.Height + 4; + customModelPickerButton.CloseMenuOnClick = false; + _customModelPicker.Parent = customModelPickerButton; + _customModelPicker.SelectedAsset = _customModel; + _customModelPicker.SelectedItemChanged += () => + { + _customModel = _customModelPicker.SelectedAsset as Model; + if (_customModelPicker.SelectedAsset == null) + { + SelectedModelIndex = 0; + ResetModelContextMenu(); + return; + } + + _previewModel.Model = _customModel; + _previewModel.Transform = Transforms[0]; + SelectedModelIndex = -1; + ResetModelContextMenu(); + }; + } + /// /// Initializes a new instance of the class. /// @@ -107,17 +169,7 @@ namespace FlaxEditor.Viewport.Previews { if (!control.Visible) return; - _modelWidgetButtonMenu.ItemsContainer.DisposeChildren(); - - // Fill out all models - for (int i = 0; i < Models.Length; i++) - { - var index = i; - var button = _modelWidgetButtonMenu.AddButton(Models[index]); - button.ButtonClicked += _ => SelectedModelIndex = index; - button.Checked = SelectedModelIndex == index; - button.Tag = index; - } + ResetModelContextMenu(); }; new ViewportWidgetButton("Model", SpriteHandle.Invalid, _modelWidgetButtonMenu) { From 806590d1c3350136ae6d0043fa22e06a3faf7002 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Oct 2023 14:08:34 +0200 Subject: [PATCH 130/546] Fix deadlock in scene loading #1761 #1773 --- Source/Engine/Level/Level.cpp | 34 ++----------------- Source/Engine/Level/Level.h | 4 +-- Source/Engine/Level/SceneObjectsFactory.cpp | 6 ++-- .../Platform/Win32/Win32CriticalSection.h | 10 +----- 4 files changed, 9 insertions(+), 45 deletions(-) diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index d7659ca1c..cfdf11a6f 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -416,7 +416,7 @@ public: } // Load scene - if (Level::loadScene(SceneAsset.Get())) + if (Level::loadScene(SceneAsset)) { LOG(Error, "Failed to deserialize scene {0}", SceneId); CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId); @@ -816,40 +816,10 @@ bool LevelImpl::unloadScenes() return false; } -bool Level::loadScene(const Guid& sceneId) -{ - const auto sceneAsset = Content::LoadAsync(sceneId); - return loadScene(sceneAsset); -} - -bool Level::loadScene(const String& scenePath) -{ - LOG(Info, "Loading scene from file. Path: \'{0}\'", scenePath); - - // Check for missing file - if (!FileSystem::FileExists(scenePath)) - { - LOG(Error, "Missing scene file."); - return true; - } - - // Load file - BytesContainer sceneData; - if (File::ReadAllBytes(scenePath, sceneData)) - { - LOG(Error, "Cannot load data from file."); - return true; - } - - return loadScene(sceneData); -} - bool Level::loadScene(JsonAsset* sceneAsset) { // Keep reference to the asset (prevent unloading during action) AssetReference ref = sceneAsset; - - // Wait for loaded if (sceneAsset == nullptr || sceneAsset->WaitForLoaded()) { LOG(Error, "Cannot load scene asset."); @@ -879,6 +849,7 @@ bool Level::loadScene(const BytesContainer& sceneData, Scene** outScene) return true; } + ScopeLock lock(ScenesLock); return loadScene(document, outScene); } @@ -1332,6 +1303,7 @@ bool Level::LoadScene(const Guid& id) } // Load scene + ScopeLock lock(ScenesLock); if (loadScene(sceneAsset)) { LOG(Error, "Failed to deserialize scene {0}", id); diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h index 1f0acda2d..e09f8e376 100644 --- a/Source/Engine/Level/Level.h +++ b/Source/Engine/Level/Level.h @@ -541,8 +541,8 @@ private: }; static void callActorEvent(ActorEventType eventType, Actor* a, Actor* b); - static bool loadScene(const Guid& sceneId); - static bool loadScene(const String& scenePath); + + // All loadScene assume that ScenesLock has been taken by the calling thread static bool loadScene(JsonAsset* sceneAsset); static bool loadScene(const BytesContainer& sceneData, Scene** outScene = nullptr); static bool loadScene(rapidjson_flax::Document& document, Scene** outScene = nullptr); diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index 3bf0f68a1..2936da3da 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -280,9 +280,9 @@ void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable:: #if USE_EDITOR // Add dummy script auto* dummyScript = parent->AddScript(); - const auto parentIdMember = value.FindMember("TypeName"); - if (parentIdMember != value.MemberEnd() && parentIdMember->value.IsString()) - dummyScript->MissingTypeName = parentIdMember->value.GetString(); + const auto typeNameMember = value.FindMember("TypeName"); + if (typeNameMember != value.MemberEnd() && typeNameMember->value.IsString()) + dummyScript->MissingTypeName = typeNameMember->value.GetString(); dummyScript->Data = MoveTemp(bufferStr); #endif LOG(Warning, "Parent actor of the missing object: {0}", parent->GetName()); diff --git a/Source/Engine/Platform/Win32/Win32CriticalSection.h b/Source/Engine/Platform/Win32/Win32CriticalSection.h index 2c103ef85..95d85efac 100644 --- a/Source/Engine/Platform/Win32/Win32CriticalSection.h +++ b/Source/Engine/Platform/Win32/Win32CriticalSection.h @@ -16,16 +16,13 @@ class FLAXENGINE_API Win32CriticalSection friend Win32ConditionVariable; private: - mutable Windows::CRITICAL_SECTION _criticalSection; private: - Win32CriticalSection(const Win32CriticalSection&); Win32CriticalSection& operator=(const Win32CriticalSection&); public: - /// /// Initializes a new instance of the class. /// @@ -43,17 +40,12 @@ public: } public: - /// /// Locks the critical section. /// void Lock() const { - // Spin first before entering critical section, causing ring-0 transition and context switch - if (Windows::TryEnterCriticalSection(&_criticalSection) == 0) - { - Windows::EnterCriticalSection(&_criticalSection); - } + Windows::EnterCriticalSection(&_criticalSection); } /// From 6f773bd558a760eb0d347042181b29b8cf16dbb1 Mon Sep 17 00:00:00 2001 From: MineBill Date: Tue, 24 Oct 2023 18:03:07 +0300 Subject: [PATCH 131/546] Make the message box a TASKMODEL to prevent interation with the editor. --- Source/Engine/Platform/Windows/WindowsPlatform.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.cpp b/Source/Engine/Platform/Windows/WindowsPlatform.cpp index c5bc44ab9..474a92072 100644 --- a/Source/Engine/Platform/Windows/WindowsPlatform.cpp +++ b/Source/Engine/Platform/Windows/WindowsPlatform.cpp @@ -451,6 +451,7 @@ DialogResult MessageBox::Show(Window* parent, const StringView& text, const Stri default: break; } + flags |= MB_TASKMODAL; // Show dialog int result = MessageBoxW(parent ? static_cast(parent->GetNativePtr()) : nullptr, String(text).GetText(), String(caption).GetText(), flags); From 081648ef063600c6dd620c46b8b118695c701371 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 24 Oct 2023 21:47:18 +0300 Subject: [PATCH 132/546] Fix common tooltips with hardcoded keybindings --- Source/Editor/Surface/VisjectSurfaceWindow.cs | 8 +++++--- Source/Editor/Windows/Assets/AnimationWindow.cs | 6 ++++-- .../Editor/Windows/Assets/BehaviorTreeWindow.cs | 8 +++++--- .../Windows/Assets/GameplayGlobalsWindow.cs | 8 +++++--- Source/Editor/Windows/Assets/JsonAssetWindow.cs | 6 ++++-- .../Windows/Assets/LocalizedStringTableWindow.cs | 6 ++++-- .../Windows/Assets/MaterialInstanceWindow.cs | 6 ++++-- .../Editor/Windows/Assets/ParticleSystemWindow.cs | 6 ++++-- Source/Editor/Windows/Assets/PrefabWindow.cs | 12 +++++++----- .../Editor/Windows/Assets/SceneAnimationWindow.cs | 6 ++++-- .../Assets/VisjectFunctionSurfaceWindow.cs | 8 +++++--- .../Editor/Windows/Assets/VisualScriptWindow.cs | 15 ++++++++------- .../Editor/Windows/VisualScriptDebuggerWindow.cs | 4 +++- 13 files changed, 62 insertions(+), 37 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurfaceWindow.cs b/Source/Editor/Surface/VisjectSurfaceWindow.cs index 305121e11..f676f6010 100644 --- a/Source/Editor/Surface/VisjectSurfaceWindow.cs +++ b/Source/Editor/Surface/VisjectSurfaceWindow.cs @@ -725,6 +725,8 @@ namespace FlaxEditor.Surface protected VisjectSurfaceWindow(Editor editor, AssetItem item, bool useTabs = false) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new FlaxEditor.Undo(); _undo.UndoDone += OnUndoRedo; @@ -775,10 +777,10 @@ namespace FlaxEditor.Surface // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); // Setup input actions diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs index a765c2faa..8f88d93f6 100644 --- a/Source/Editor/Windows/Assets/AnimationWindow.cs +++ b/Source/Editor/Windows/Assets/AnimationWindow.cs @@ -230,6 +230,8 @@ namespace FlaxEditor.Windows.Assets public AnimationWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -265,8 +267,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/animation/index.html")).LinkTooltip("See documentation to learn more"); diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 8cbf6cf75..01ee9cb8a 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -130,6 +130,8 @@ namespace FlaxEditor.Windows.Assets public BehaviorTreeWindow(Editor editor, BinaryAssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -172,10 +174,10 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); _toolstrip.AddButton(editor.Icons.CenterView64, _surface.ShowWholeGraph).LinkTooltip("Show whole graph"); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/ai/behavior-trees/index.html")).LinkTooltip("See documentation to learn more"); diff --git a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs index 623c4ef5b..a8121162a 100644 --- a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs +++ b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs @@ -395,6 +395,8 @@ namespace FlaxEditor.Windows.Assets public GameplayGlobalsWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + _undo = new Undo(); _undo.ActionDone += OnUndo; _undo.UndoDone += OnUndo; @@ -411,10 +413,10 @@ namespace FlaxEditor.Windows.Assets _proxy = new PropertiesProxy(); _propertiesEditor.Select(_proxy); - _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save asset"); + _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip($"Save asset ({inputOptions.Save})"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _resetButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Rotate32, Reset).LinkTooltip("Resets the variables values to the default values"); diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs index 097d4992a..a1178fb68 100644 --- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs +++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs @@ -34,6 +34,8 @@ namespace FlaxEditor.Windows.Assets public JsonAssetWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -43,8 +45,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); // Panel var panel = new Panel(ScrollBars.Vertical) diff --git a/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs b/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs index 0de5ce315..85f351fef 100644 --- a/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs +++ b/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs @@ -126,6 +126,8 @@ namespace FlaxEditor.Windows.Assets public LocalizedStringTableWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -135,8 +137,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(Editor.Icons.Up64, OnExport).LinkTooltip("Export localization table entries for translation to .pot file"); diff --git a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs index 3e0525fb7..775e0c0dc 100644 --- a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs +++ b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs @@ -375,6 +375,8 @@ namespace FlaxEditor.Windows.Assets public MaterialInstanceWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -384,8 +386,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(Editor.Icons.Rotate64, OnRevertAllParameters).LinkTooltip("Revert all the parameters to the default values"); _toolstrip.AddSeparator(); diff --git a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs index 17eda1358..4a5e02e6e 100644 --- a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs +++ b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs @@ -306,6 +306,8 @@ namespace FlaxEditor.Windows.Assets public ParticleSystemWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -359,8 +361,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/particles/index.html")).LinkTooltip("See documentation to learn more"); diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index 16f7c21c6..f50a832a1 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -94,6 +94,8 @@ namespace FlaxEditor.Windows.Assets public PrefabWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoEvent; @@ -176,12 +178,12 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip("Change Gizmo tool mode to Translate (1)"); - _toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip("Change Gizmo tool mode to Rotate (2)"); - _toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip("Change Gizmo tool mode to Scale (3)"); + _toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip($"Change Gizmo tool mode to Translate ({inputOptions.TranslateMode})"); + _toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip($"Change Gizmo tool mode to Rotate ({inputOptions.RotateMode})"); + _toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip($"Change Gizmo tool mode to Scale ({inputOptions.ScaleMode})"); _toolstrip.AddSeparator(); _toolStripLiveReload = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, () => LiveReload = !LiveReload).SetChecked(true).SetAutoCheck(true).LinkTooltip("Live changes preview (applies prefab changes on modification by auto)"); diff --git a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs index 162944144..05435cc77 100644 --- a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs +++ b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs @@ -627,6 +627,8 @@ namespace FlaxEditor.Windows.Assets public SceneAnimationWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -652,8 +654,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _previewButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, OnPreviewButtonClicked).SetAutoCheck(true).LinkTooltip("If checked, enables live-preview of the animation on a scene while editing"); _renderButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Build64, OnRenderButtonClicked).LinkTooltip("Open the scene animation rendering utility..."); diff --git a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs index 7ae6ae8be..e3575d0e8 100644 --- a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs +++ b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs @@ -61,6 +61,8 @@ namespace FlaxEditor.Windows.Assets protected VisjectFunctionSurfaceWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -70,10 +72,10 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); // Panel diff --git a/Source/Editor/Windows/Assets/VisualScriptWindow.cs b/Source/Editor/Windows/Assets/VisualScriptWindow.cs index 584f1c307..79157f850 100644 --- a/Source/Editor/Windows/Assets/VisualScriptWindow.cs +++ b/Source/Editor/Windows/Assets/VisualScriptWindow.cs @@ -561,6 +561,7 @@ namespace FlaxEditor.Windows.Assets : base(editor, item) { var isPlayMode = Editor.IsPlayMode; + var inputOptions = Editor.Options.Options.Input; // Undo _undo = new Undo(); @@ -598,21 +599,21 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/visual/index.html")).LinkTooltip("See documentation to learn more"); _debugToolstripControls = new[] { _toolstrip.AddSeparator(), - _toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip("Continue (F5)"), + _toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip($"Continue ({inputOptions.DebuggerContinue})"), _toolstrip.AddButton(editor.Icons.Search64, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"), - _toolstrip.AddButton(editor.Icons.Right64, OnDebuggerStepOver).LinkTooltip("Step Over (F10)"), - _toolstrip.AddButton(editor.Icons.Down64, OnDebuggerStepInto).LinkTooltip("Step Into (F11)"), - _toolstrip.AddButton(editor.Icons.Up64, OnDebuggerStepOut).LinkTooltip("Step Out (Shift+F11)"), + _toolstrip.AddButton(editor.Icons.Right64, OnDebuggerStepOver).LinkTooltip($"Step Over ({inputOptions.DebuggerStepOver})"), + _toolstrip.AddButton(editor.Icons.Down64, OnDebuggerStepInto).LinkTooltip($"Step Into ({inputOptions.DebuggerStepInto})"), + _toolstrip.AddButton(editor.Icons.Up64, OnDebuggerStepOut).LinkTooltip($"Step Out ({inputOptions.DebuggerStepOut})"), _toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"), }; foreach (var control in _debugToolstripControls) diff --git a/Source/Editor/Windows/VisualScriptDebuggerWindow.cs b/Source/Editor/Windows/VisualScriptDebuggerWindow.cs index c84a9ba33..57f10f2f4 100644 --- a/Source/Editor/Windows/VisualScriptDebuggerWindow.cs +++ b/Source/Editor/Windows/VisualScriptDebuggerWindow.cs @@ -399,6 +399,8 @@ namespace FlaxEditor.Windows { Title = "Visual Script Debugger"; + var inputOptions = editor.Options.Options.Input; + var toolstrip = new ToolStrip { Parent = this @@ -407,7 +409,7 @@ namespace FlaxEditor.Windows _debugToolstripControls = new[] { toolstrip.AddSeparator(), - toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip("Continue (F5)"), + toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip($"Continue ({inputOptions.DebuggerContinue})"), toolstrip.AddButton(editor.Icons.Search64, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"), toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"), }; From 06f37794c2f1adbc09fcad592894c48222e21a7b Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 24 Oct 2023 22:27:12 +0300 Subject: [PATCH 133/546] Add input bindings for game window mouse unlock and toggle fullscreen --- Source/Editor/Options/InputOptions.cs | 14 ++++++++++--- Source/Editor/Windows/GameWindow.cs | 29 ++++++--------------------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index 90d098bb6..e2e7d0e71 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -76,6 +76,10 @@ namespace FlaxEditor.Options [EditorDisplay("Common"), EditorOrder(230)] public InputBinding RotateSelection = new InputBinding(KeyboardKeys.R); + [DefaultValue(typeof(InputBinding), "F11")] + [EditorDisplay("Common"), EditorOrder(240)] + public InputBinding ToggleFullscreen = new InputBinding(KeyboardKeys.F11); + #endregion #region File @@ -208,16 +212,20 @@ namespace FlaxEditor.Options [EditorDisplay("Debugger", "Continue"), EditorOrder(810)] public InputBinding DebuggerContinue = new InputBinding(KeyboardKeys.F5); + [DefaultValue(typeof(InputBinding), "Shift+F11")] + [EditorDisplay("Debugger", "Unlock mouse in Play Mode"), EditorOrder(820)] + public InputBinding DebuggerUnlockMouse = new InputBinding(KeyboardKeys.F11, KeyboardKeys.Shift); + [DefaultValue(typeof(InputBinding), "F10")] - [EditorDisplay("Debugger", "Step Over"), EditorOrder(820)] + [EditorDisplay("Debugger", "Step Over"), EditorOrder(830)] public InputBinding DebuggerStepOver = new InputBinding(KeyboardKeys.F10); [DefaultValue(typeof(InputBinding), "F11")] - [EditorDisplay("Debugger", "Step Into"), EditorOrder(830)] + [EditorDisplay("Debugger", "Step Into"), EditorOrder(840)] public InputBinding DebuggerStepInto = new InputBinding(KeyboardKeys.F11); [DefaultValue(typeof(InputBinding), "Shift+F11")] - [EditorDisplay("Debugger", "Step Out"), EditorOrder(840)] + [EditorDisplay("Debugger", "Step Out"), EditorOrder(850)] public InputBinding DebuggerStepOut = new InputBinding(KeyboardKeys.F11, KeyboardKeys.Shift); #endregion diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index fdedbb3c2..45952e179 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -271,8 +271,6 @@ namespace FlaxEditor.Windows Title = "Game"; AutoFocus = true; - FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); - var task = MainRenderTask.Instance; // Setup viewport @@ -304,6 +302,12 @@ namespace FlaxEditor.Windows // Link editor options Editor.Options.OptionsChanged += OnOptionsChanged; OnOptionsChanged(Editor.Options.Options); + + InputActions.Add(options => options.TakeScreenshot, () => Screenshot.Capture(string.Empty)); + InputActions.Add(options => options.DebuggerUnlockMouse, UnlockMouseInPlay); + InputActions.Add(options => options.DebuggerToggleFullscreen, () => { if (Editor.IsPlayMode) IsMaximized = !IsMaximized; }); + + FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); } private void ChangeViewportRatio(ViewportScaleOptions v) @@ -945,27 +949,6 @@ namespace FlaxEditor.Windows /// public override bool OnKeyDown(KeyboardKeys key) { - switch (key) - { - case KeyboardKeys.F12: - Screenshot.Capture(string.Empty); - return true; - case KeyboardKeys.F11: - if (Root.GetKey(KeyboardKeys.Shift)) - { - // Unlock mouse in game mode - UnlockMouseInPlay(); - return true; - } - else if (Editor.IsPlayMode) - { - // Maximized game window toggle - IsMaximized = !IsMaximized; - return true; - } - break; - } - // Prevent closing the game window tab during a play session if (Editor.StateMachine.IsPlayMode && Editor.Options.Options.Input.CloseTab.Process(this, key)) { From b2db1330c04c8fb44c70041bd6bc5d597bb8ab57 Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Tue, 24 Oct 2023 21:45:00 -0400 Subject: [PATCH 134/546] copy old control data to new control when set UIControl type --- .../Dedicated/UIControlEditor.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs index 5215e23a4..296560507 100644 --- a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs @@ -695,7 +695,41 @@ namespace FlaxEditor.CustomEditors.Dedicated private void SetType(ref ScriptType controlType, UIControl uiControl) { string previousName = uiControl.Control?.GetType().Name ?? nameof(UIControl); - uiControl.Control = (Control)controlType.CreateInstance(); + + var oldControlType = (Control)uiControl.Control; + var newControlType = (Control)controlType.CreateInstance(); + + // copy old control data to new control + if (oldControlType != null) + { + newControlType.Visible = oldControlType.Visible; + newControlType.Enabled = oldControlType.Enabled; + newControlType.AutoFocus = oldControlType.AutoFocus; + + newControlType.AnchorMin = oldControlType.AnchorMin; + newControlType.AnchorMax = oldControlType.AnchorMax; + newControlType.Offsets = oldControlType.Offsets; + + newControlType.LocalLocation = oldControlType.LocalLocation; + newControlType.Scale = oldControlType.Scale; + newControlType.Bounds = oldControlType.Bounds; + newControlType.Width = oldControlType.Width; + newControlType.Height = oldControlType.Height; + newControlType.Center = oldControlType.Center; + newControlType.PivotRelative = oldControlType.PivotRelative; + + newControlType.Pivot = oldControlType.Pivot; + newControlType.Shear = oldControlType.Shear; + newControlType.Rotation = oldControlType.Rotation; + } + if (oldControlType is ContainerControl oldContainer && newControlType is ContainerControl newContainer) + { + newContainer.CullChildren = oldContainer.CullChildren; + newContainer.ClipChildren = oldContainer.ClipChildren; + } + + uiControl.Control = newControlType; + if (uiControl.Name.StartsWith(previousName)) { string newName = controlType.Name + uiControl.Name.Substring(previousName.Length); From 137c82a38748666cfc954e91e4acb2ed2ac3e549 Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Wed, 25 Oct 2023 06:21:19 +0200 Subject: [PATCH 135/546] Update Material.cs --- Source/Editor/Surface/Archetypes/Material.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index ee3270460..11237481d 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -934,7 +934,7 @@ namespace FlaxEditor.Surface.Archetypes Elements = new[] { NodeElementArchetype.Factory.Input(0, "value", true, typeof(float), 0), - NodeElementArchetype.Factory.Input(1, "gradient", true, typeof(float), 0), + NodeElementArchetype.Factory.Input(1, "gradient", true, typeof(float), 1), NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), } }, From 1f8da14780e20ec33cee6219c431b87ff8d6aaca Mon Sep 17 00:00:00 2001 From: Nils Hausfeld Date: Wed, 25 Oct 2023 11:53:43 +0200 Subject: [PATCH 136/546] - Alternative titles for comparisons --- Source/Editor/Surface/Archetypes/Comparisons.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Comparisons.cs b/Source/Editor/Surface/Archetypes/Comparisons.cs index a0efbd92f..b72111c46 100644 --- a/Source/Editor/Surface/Archetypes/Comparisons.cs +++ b/Source/Editor/Surface/Archetypes/Comparisons.cs @@ -14,7 +14,7 @@ namespace FlaxEditor.Surface.Archetypes [HideInEditor] public static class Comparisons { - private static NodeArchetype Op(ushort id, string title, string desc) + private static NodeArchetype Op(ushort id, string title, string desc, string[] altTitles = null) { return new NodeArchetype { @@ -22,6 +22,7 @@ namespace FlaxEditor.Surface.Archetypes Title = title, Description = desc, Flags = NodeFlags.AllGraphs, + AlternativeTitles = altTitles, ConnectionsHints = ConnectionsHint.Value, Size = new Float2(100, 40), IndependentBoxes = new[] @@ -170,12 +171,12 @@ namespace FlaxEditor.Surface.Archetypes /// public static NodeArchetype[] Nodes = { - Op(1, "==", "Determines whether two values are equal"), - Op(2, "!=", "Determines whether two values are not equal"), - Op(3, ">", "Determines whether the first value is greater than the other"), - Op(4, "<", "Determines whether the first value is less than the other"), - Op(5, "<=", "Determines whether the first value is less or equal to the other"), - Op(6, ">=", "Determines whether the first value is greater or equal to the other"), + Op(1, "==", "Determines whether two values are equal", new[] { "equals" }), + Op(2, "!=", "Determines whether two values are not equal", new[] { "not equals" }), + Op(3, ">", "Determines whether the first value is greater than the other", new[] { "greater than", "larger than", "bigger than" }), + Op(4, "<", "Determines whether the first value is less than the other", new[] { "less than", "smaller than" }), + Op(5, "<=", "Determines whether the first value is less or equal to the other", new[] { "less equals than", "smaller equals than" }), + Op(6, ">=", "Determines whether the first value is greater or equal to the other", new[] { "greater equals than", "larger equals than", "bigger equals than" }), new NodeArchetype { TypeID = 7, From daf31cfa4dc26f6c87dde163a43efab100a2ded9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 25 Oct 2023 14:57:44 +0200 Subject: [PATCH 137/546] Format code #1611 --- Source/Editor/Viewport/EditorViewport.cs | 27 +++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 8fd28ebfd..c49392d01 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -212,8 +212,10 @@ namespace FlaxEditor.Viewport /// /// Format of the text for the camera move speed. /// - private string MovementSpeedTextFormat { - get { + private string MovementSpeedTextFormat + { + get + { if (Mathf.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon || Mathf.Abs(_movementSpeed - _minMovementSpeed) < Mathf.Epsilon) return "{0:0.##}"; @@ -480,7 +482,7 @@ namespace FlaxEditor.Viewport get => _panningSpeed; set => _panningSpeed = value; } - + /// /// The input actions collection to processed during user input. /// @@ -544,6 +546,7 @@ namespace FlaxEditor.Viewport if (useWidgets) { #region Camera settings widget + var largestText = "Relative Panning"; var textSize = Style.Current.FontMedium.MeasureText(largestText); var xLocationForExtras = textSize.X + 5; @@ -609,7 +612,7 @@ namespace FlaxEditor.Viewport maxCamSpeedValue.ValueChanged += () => { OnMaxMovementSpeedChanged(maxCamSpeedValue); - + minCamSpeedValue.MaxValue = maxCamSpeedValue.Value; if (Math.Abs(camSpeedValue.MaxValue - maxCamSpeedValue.Value) > Mathf.Epsilon) @@ -792,9 +795,11 @@ namespace FlaxEditor.Viewport maxCamSpeedValue.Value = _maxMovementSpeed; }; } + #endregion Camera settings widget #region View mode widget + largestText = "Brightness"; textSize = Style.Current.FontMedium.MeasureText(largestText); xLocationForExtras = textSize.X + 5; @@ -964,6 +969,7 @@ namespace FlaxEditor.Viewport resolutionValue.ValueChanged += () => ResolutionScale = resolutionValue.Value; ViewWidgetButtonMenu.VisibleChanged += control => resolutionValue.Value = ResolutionScale; } + #endregion View mode widget } @@ -1036,7 +1042,7 @@ namespace FlaxEditor.Viewport OnCameraMovementProgressChanged(); _editor.ProjectCache.SetCustomData("CameraMaxMovementSpeedValue", _maxMovementSpeed.ToString()); } - + private void OnCameraEasingToggled(Control control) { _useCameraEasing = !_useCameraEasing; @@ -1145,11 +1151,12 @@ namespace FlaxEditor.Viewport private void OnCameraMovementProgressChanged() { // prevent NaN - if (Math.Abs(_minMovementSpeed - _maxMovementSpeed) < Mathf.Epsilon) { + if (Math.Abs(_minMovementSpeed - _maxMovementSpeed) < Mathf.Epsilon) + { _speedStep = 0; return; } - + if (Math.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon) { _speedStep = _maxSpeedSteps; @@ -1180,9 +1187,9 @@ namespace FlaxEditor.Viewport // calculate new linear/eased progress var progress = _useCameraEasing - ? Mathf.Pow((float)_speedStep / _maxSpeedSteps, _cameraEasingDegree) - : (float)_speedStep / _maxSpeedSteps; - + ? Mathf.Pow((float)_speedStep / _maxSpeedSteps, _cameraEasingDegree) + : (float)_speedStep / _maxSpeedSteps; + var speed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, progress); MovementSpeed = (float)Math.Round(speed, 3); _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString()); From 73694cba6c4abe9444ca652c996a018ab6002f09 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Wed, 25 Oct 2023 09:57:51 -0500 Subject: [PATCH 138/546] Add being able to set origin of mesh to the local mesh origin. fix centering several meshes. --- Source/Engine/Graphics/Models/ModelData.h | 15 +++++ .../Tools/ModelTool/ModelTool.Assimp.cpp | 59 +++++++++++++++++++ .../Tools/ModelTool/ModelTool.OpenFBX.cpp | 15 +++++ Source/Engine/Tools/ModelTool/ModelTool.cpp | 9 ++- Source/Engine/Tools/ModelTool/ModelTool.h | 5 +- 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h index 32caa2d01..b1080714b 100644 --- a/Source/Engine/Graphics/Models/ModelData.h +++ b/Source/Engine/Graphics/Models/ModelData.h @@ -90,6 +90,21 @@ public: /// Array BlendShapes; + /// + /// Global translation for this mesh to be at it's local origin. + /// + Vector3 OriginTranslation = Vector3::Zero; + + /// + /// Orientation for this mesh at it's local origin. + /// + Quaternion OriginOrientation = Quaternion::Identity; + + /// + /// Meshes scaling. + /// + Vector3 Scaling = Vector3(1); + public: /// /// Determines whether this instance has any mesh data. diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index 01eb8d369..ab7c6c480 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -1,5 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +#include "OpenFBX/ofbx.h" #if COMPILE_WITH_MODEL_TOOL && USE_ASSIMP #include "ModelTool.h" @@ -648,6 +649,64 @@ bool ImportMesh(int32 i, ImportedModelData& result, AssimpImporterData& data, St result.LODs.Resize(lodIndex + 1); result.LODs[lodIndex].Meshes.Add(meshData); } + + auto root = data.Scene->mRootNode; + Array points; + if (root->mNumChildren == 0) + { + //auto translation = ToMatrix(root->mTransformation).GetTranslation(); + //points.Add(Vector3(translation.X, translation.Y, translation.Z)); + aiQuaternion aiQuat; + aiVector3D aiPos; + aiVector3D aiScale; + root->mTransformation.Decompose(aiScale, aiQuat, aiPos); + auto quat = ToQuaternion(aiQuat); + auto pos = ToFloat3(aiPos); + auto scale = ToFloat3(aiScale); + Transform trans = Transform(pos, quat, scale); + points.Add(trans); + } + else + { + for (unsigned int j = 0; j < root->mNumChildren; j++) + { + //auto translation = ToMatrix(root->mChildren[j]->mTransformation).GetTranslation(); + //points.Add(Vector3(translation.X, translation.Y, -translation.Z)); + aiQuaternion aiQuat; + aiVector3D aiPos; + aiVector3D aiScale; + root->mChildren[j]->mTransformation.Decompose(aiScale, aiQuat, aiPos); + auto quat = ToQuaternion(aiQuat); + auto pos = ToFloat3(aiPos); + auto scale = ToFloat3(aiScale); + Transform trans = Transform(pos, quat, scale); + points.Add(trans); + } + } + + Float3 translation = Float3::Zero; + Float3 scale = Float3::Zero; + Quaternion orientation = Quaternion::Identity; + for (auto point : points) + { + translation += point.Translation; + scale += point.Scale; + orientation *= point.Orientation; + } + + if (points.Count() > 0) + { + meshData->OriginTranslation = translation / (float)points.Count(); + meshData->OriginOrientation = Quaternion::Invert(orientation); + meshData->Scaling = scale / (float)points.Count(); + } + else + { + meshData->OriginTranslation = translation; + meshData->OriginOrientation = Quaternion::Invert(orientation); + meshData->Scaling = Float3(1); + } + return false; } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index f1e89d6bc..3db9888ad 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -836,6 +836,21 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb mesh.TransformBuffer(geometryTransform); }*/ + // Get local transform for origin shifting translation + auto translation = ToMatrix(aMesh->getGlobalTransform()).GetTranslation(); + auto scale = data.GlobalSettings.UnitScaleFactor; + if (data.GlobalSettings.CoordAxis == ofbx::CoordSystem_RightHanded) + mesh.OriginTranslation = scale * Vector3(translation.X, translation.Y, -translation.Z); + else + mesh.OriginTranslation = scale * Vector3(translation.X, translation.Y, translation.Z); + + auto rot = aMesh->getLocalRotation(); + auto quat = Quaternion::Euler(-(float)rot.x, -(float)rot.y, -(float)rot.z); + mesh.OriginOrientation = quat; + + auto scaling = aMesh->getLocalScaling(); + auto scaleFactor = data.GlobalSettings.UnitScaleFactor; + mesh.Scaling = Vector3(scaleFactor * (float)scaling.x, scaleFactor * (float)scaling.y, scaleFactor * (float)scaling.z); return false; } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 116f63407..c9b55ec79 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -372,6 +372,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(Scale); SERIALIZE(Rotation); SERIALIZE(Translation); + SERIALIZE(UseLocalOrigin); SERIALIZE(CenterGeometry); SERIALIZE(Duration); SERIALIZE(FramesRange); @@ -418,6 +419,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(Scale); DESERIALIZE(Rotation); DESERIALIZE(Translation); + DESERIALIZE(UseLocalOrigin); DESERIALIZE(CenterGeometry); DESERIALIZE(Duration); DESERIALIZE(FramesRange); @@ -1089,11 +1091,16 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op // Prepare import transformation Transform importTransform(options.Translation, options.Rotation, Float3(options.Scale)); + if (options.UseLocalOrigin && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems()) + { + importTransform.Translation -= importTransform.Orientation * data.LODs[0].Meshes[0]->OriginTranslation * importTransform.Scale; + } if (options.CenterGeometry && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems()) { // Calculate the bounding box (use LOD0 as a reference) BoundingBox box = data.LODs[0].GetBox(); - importTransform.Translation -= box.GetCenter(); + auto center = data.LODs[0].Meshes[0]->OriginOrientation * importTransform.Orientation * box.GetCenter() * importTransform.Scale * data.LODs[0].Meshes[0]->Scaling; + importTransform.Translation -= center; } const bool applyImportTransform = !importTransform.IsIdentity(); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index bad10b86e..98462ff9d 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -279,8 +279,11 @@ public: // Custom import geometry offset. API_FIELD(Attributes="EditorOrder(520), EditorDisplay(\"Transform\")") Float3 Translation = Float3::Zero; - // If checked, the imported geometry will be shifted to the center of mass. + // If checked, the imported geometry will be shifted to its local transform origin. API_FIELD(Attributes="EditorOrder(530), EditorDisplay(\"Transform\")") + bool UseLocalOrigin = false; + // If checked, the imported geometry will be shifted to the center of mass. + API_FIELD(Attributes="EditorOrder(540), EditorDisplay(\"Transform\")") bool CenterGeometry = false; public: // Animation From b8921fd9909922314a644eeab4db576e9b43dfa4 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Wed, 25 Oct 2023 09:58:02 -0500 Subject: [PATCH 139/546] clean up --- Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index ab7c6c480..19a59d113 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -654,8 +654,6 @@ bool ImportMesh(int32 i, ImportedModelData& result, AssimpImporterData& data, St Array points; if (root->mNumChildren == 0) { - //auto translation = ToMatrix(root->mTransformation).GetTranslation(); - //points.Add(Vector3(translation.X, translation.Y, translation.Z)); aiQuaternion aiQuat; aiVector3D aiPos; aiVector3D aiScale; @@ -670,8 +668,6 @@ bool ImportMesh(int32 i, ImportedModelData& result, AssimpImporterData& data, St { for (unsigned int j = 0; j < root->mNumChildren; j++) { - //auto translation = ToMatrix(root->mChildren[j]->mTransformation).GetTranslation(); - //points.Add(Vector3(translation.X, translation.Y, -translation.Z)); aiQuaternion aiQuat; aiVector3D aiPos; aiVector3D aiScale; From bfaae46c7eaf34fc1b941fcf560d0c0a8500d81b Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Wed, 25 Oct 2023 10:35:12 -0500 Subject: [PATCH 140/546] remove not needed variable. --- Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index 3db9888ad..c7ee11b42 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -849,8 +849,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb mesh.OriginOrientation = quat; auto scaling = aMesh->getLocalScaling(); - auto scaleFactor = data.GlobalSettings.UnitScaleFactor; - mesh.Scaling = Vector3(scaleFactor * (float)scaling.x, scaleFactor * (float)scaling.y, scaleFactor * (float)scaling.z); + mesh.Scaling = Vector3(scale * (float)scaling.x, scale * (float)scaling.y, scale * (float)scaling.z); return false; } From 66a709f09e511c9b6fdb5fc8aded8479af72249f Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Wed, 25 Oct 2023 10:46:04 -0500 Subject: [PATCH 141/546] small fixes --- Source/Engine/Graphics/Models/ModelData.h | 2 +- Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h index b1080714b..d7359d905 100644 --- a/Source/Engine/Graphics/Models/ModelData.h +++ b/Source/Engine/Graphics/Models/ModelData.h @@ -103,7 +103,7 @@ public: /// /// Meshes scaling. /// - Vector3 Scaling = Vector3(1); + Vector3 Scaling = Vector3::One; public: /// diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index 19a59d113..3d9be61d2 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -1,6 +1,5 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. -#include "OpenFBX/ofbx.h" #if COMPILE_WITH_MODEL_TOOL && USE_ASSIMP #include "ModelTool.h" From 966cd973c660e76feeac8547a8314960f389e967 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 25 Oct 2023 18:35:59 +0200 Subject: [PATCH 142/546] Add `GetHash` to various math/core types #1802 --- Source/Engine/Core/Math/Vector2.h | 6 ++++++ Source/Engine/Core/Math/Vector3.h | 6 ++++++ Source/Engine/Core/Math/Vector4.h | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h index 83deb009a..cea03ec10 100644 --- a/Source/Engine/Core/Math/Vector2.h +++ b/Source/Engine/Core/Math/Vector2.h @@ -646,6 +646,12 @@ inline Vector2Base operator/(typename TOtherFloat::Type a, const Vector2Ba return Vector2Base(a) / b; } +template +inline uint32 GetHash(const Vector2Base& key) +{ + return (*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y; +} + namespace Math { template diff --git a/Source/Engine/Core/Math/Vector3.h b/Source/Engine/Core/Math/Vector3.h index c0ebdf01d..01b55e9bd 100644 --- a/Source/Engine/Core/Math/Vector3.h +++ b/Source/Engine/Core/Math/Vector3.h @@ -977,6 +977,12 @@ inline Vector3Base operator/(typename TOtherFloat::Type a, const Vector3Ba return Vector3Base(a) / b; } +template +inline uint32 GetHash(const Vector3Base& key) +{ + return (((*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y) * 397) ^ *(uint32*)&key.Z; +} + namespace Math { template diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index 5c7b24c4a..1cc6d4db8 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -552,6 +552,12 @@ inline Vector4Base operator/(typename TOtherFloat::Type a, const Vector4Ba return Vector4Base(a) / b; } +template +inline uint32 GetHash(const Vector4Base& key) +{ + return (((((*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y) * 397) ^ *(uint32*)&key.Z) * 397) ^*(uint32*)&key.W; +} + namespace Math { template From 8c6ced4bb9c1ab6ff7a157452ab9660db470d249 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Wed, 25 Oct 2023 11:38:14 -0500 Subject: [PATCH 143/546] Limit the scene tree and prefab tree rename popup to only go to panel right edge. --- Source/Editor/SceneGraph/GUI/ActorTreeNode.cs | 7 +++++-- Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs | 2 +- Source/Editor/Windows/SceneTreeWindow.cs | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index 163397768..1f2189346 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -266,7 +266,7 @@ namespace FlaxEditor.SceneGraph.GUI /// /// Starts the actor renaming action. /// - public void StartRenaming(EditorWindow window) + public void StartRenaming(EditorWindow window, Panel treePanel) { // Block renaming during scripts reload if (Editor.Instance.ProgressReporting.CompileScripts.IsActive) @@ -281,7 +281,10 @@ namespace FlaxEditor.SceneGraph.GUI (window as PrefabWindow).ScrollingOnTreeView(false); // Start renaming the actor - var dialog = RenamePopup.Show(this, TextRect, _actorNode.Name, false); + treePanel.ScrollViewTo(this, true); + var size = new Float2(treePanel.Width - TextRect.Location.X, TextRect.Height); + var rect = new Rectangle(TextRect.Location, size); + var dialog = RenamePopup.Show(this, rect, _actorNode.Name, false); dialog.Renamed += OnRenamed; dialog.Closed += popup => { diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index 70aa1dca3..a8d9ae1be 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -339,7 +339,7 @@ namespace FlaxEditor.Windows.Assets { if (selection.Count != 0) Select(actor); - actor.TreeNode.StartRenaming(this); + actor.TreeNode.StartRenaming(this, _treePanel); } } diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index cbaa27371..250bd2301 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -142,7 +142,7 @@ namespace FlaxEditor.Windows { if (selection.Count != 0) Editor.SceneEditing.Select(actor); - actor.TreeNode.StartRenaming(this); + actor.TreeNode.StartRenaming(this, _sceneTreePanel); } } From c27187bd0ab0a5c5065d1e05df0772264062588e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 25 Oct 2023 19:17:04 +0200 Subject: [PATCH 144/546] Fix deserializing vector types in Editor from `ToString` FormatException #1802 --- Source/Editor/Options/InputBinding.cs | 7 ------ .../Math/TypeConverters/ColorConverter.cs | 4 ---- .../Math/TypeConverters/Double2Converter.cs | 24 ++----------------- .../Math/TypeConverters/Double3Converter.cs | 24 ++----------------- .../Math/TypeConverters/Double4Converter.cs | 24 ++----------------- .../Math/TypeConverters/Float2Converter.cs | 24 ++----------------- .../Math/TypeConverters/Float3Converter.cs | 24 ++----------------- .../Math/TypeConverters/Float4Converter.cs | 24 ++++++++++++++----- .../Core/Math/TypeConverters/Int2Converter.cs | 24 ++----------------- .../Core/Math/TypeConverters/Int3Converter.cs | 24 ++----------------- .../Core/Math/TypeConverters/Int4Converter.cs | 24 ++----------------- .../TypeConverters/QuaternionConverter.cs | 24 ++----------------- .../Math/TypeConverters/Vector2Converter.cs | 24 ++----------------- .../Math/TypeConverters/Vector3Converter.cs | 24 ++----------------- .../Math/TypeConverters/Vector4Converter.cs | 24 ++----------------- 15 files changed, 42 insertions(+), 281 deletions(-) diff --git a/Source/Editor/Options/InputBinding.cs b/Source/Editor/Options/InputBinding.cs index eb61c0f68..95c3c1d6f 100644 --- a/Source/Editor/Options/InputBinding.cs +++ b/Source/Editor/Options/InputBinding.cs @@ -259,10 +259,7 @@ namespace FlaxEditor.Options public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) - { return true; - } - return base.CanConvertFrom(context, sourceType); } @@ -270,9 +267,7 @@ namespace FlaxEditor.Options public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) - { return false; - } return base.CanConvertTo(context, destinationType); } @@ -284,7 +279,6 @@ namespace FlaxEditor.Options InputBinding.TryParse(str, out var result); return result; } - return base.ConvertFrom(context, culture, value); } @@ -295,7 +289,6 @@ namespace FlaxEditor.Options { return ((InputBinding)value).ToString(); } - return base.ConvertTo(context, culture, value, destinationType); } } diff --git a/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs b/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs index afd34bbfd..65377acaa 100644 --- a/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs +++ b/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs @@ -13,9 +13,7 @@ namespace FlaxEngine.TypeConverters public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) - { return true; - } return base.CanConvertFrom(context, sourceType); } @@ -23,9 +21,7 @@ namespace FlaxEngine.TypeConverters public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) - { return false; - } return base.CanConvertTo(context, destinationType); } diff --git a/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs index 4eebbfce4..e0670df05 100644 --- a/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Double2Converter : TypeConverter + internal class Double2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Double2(double.Parse(v[0], culture), double.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs index 420e0016c..a66892ecb 100644 --- a/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Double3Converter : TypeConverter + internal class Double3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Double3(double.Parse(v[0], culture), double.Parse(v[1], culture), double.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs index fc1d9a7fe..d085217ef 100644 --- a/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Double4Converter : TypeConverter + internal class Double4Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Double4(double.Parse(v[0], culture), double.Parse(v[1], culture), double.Parse(v[2], culture), double.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs index a41a0f4d5..4b2ffadf5 100644 --- a/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Float2Converter : TypeConverter + internal class Float2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Float2(float.Parse(v[0], culture), float.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs index aded4117e..3739c44ef 100644 --- a/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Float3Converter : TypeConverter + internal class Float3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Float3(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs index 58c76ac65..620f2c838 100644 --- a/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs @@ -7,15 +7,13 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Float4Converter : TypeConverter + internal class VectorConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) - { return true; - } return base.CanConvertFrom(context, sourceType); } @@ -23,18 +21,32 @@ namespace FlaxEngine.TypeConverters public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) - { return false; - } return base.CanConvertTo(context, destinationType); } + internal static string[] GetParts(string str) + { + string[] v = str.Split(','); + if (v.Length == 1) + { + // When converting from ToString() + v = str.Split(' '); + for (int i = 0; i < v.Length; i++) + v[i] = v[i].Substring(v[i].IndexOf(':') + 1); + } + return v; + } + } + + internal class Float4Converter : VectorConverter + { /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Float4(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs index c4989c085..f528aa46b 100644 --- a/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Int2Converter : TypeConverter + internal class Int2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Int2(int.Parse(v[0], culture), int.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs index fe01f91fd..520f806d0 100644 --- a/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Int3Converter : TypeConverter + internal class Int3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Int3(int.Parse(v[0], culture), int.Parse(v[1], culture), int.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs index 2ce0fc202..e9a27dfda 100644 --- a/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Int4Converter : TypeConverter + internal class Int4Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Int4(int.Parse(v[0], culture), int.Parse(v[1], culture), int.Parse(v[2], culture), int.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs b/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs index 23bb901be..5d9aa206b 100644 --- a/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs +++ b/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class QuaternionConverter : TypeConverter + internal class QuaternionConverter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Quaternion(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs index 96d6beadc..acb5b5817 100644 --- a/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Vector2Converter : TypeConverter + internal class Vector2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Vector2(float.Parse(v[0], culture), float.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs index 23ee4df11..66ec831f0 100644 --- a/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Vector3Converter : TypeConverter + internal class Vector3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Vector3(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs index c3b4d074b..f4781f45b 100644 --- a/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Vector4Converter : TypeConverter + internal class Vector4Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Vector4(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); From 18c119c155a75bcea86d49b44cbbbe9c05137024 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Wed, 25 Oct 2023 16:25:26 -0500 Subject: [PATCH 145/546] Ensure layer confirmation does not popup in play mode. --- Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs index bf087feda..4829bd91a 100644 --- a/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs @@ -34,7 +34,7 @@ namespace FlaxEditor.CustomEditors.Editors value = 0; // If selected is single actor that has children, ask if apply layer to the sub objects as well - if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren) + if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren && !Editor.IsPlayMode) { var valueText = comboBox.SelectedItem; From 2158fa7358aca67c2a5c9f90c89edd4a92a2b52d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 25 Oct 2023 23:34:29 +0200 Subject: [PATCH 146/546] Remove not needed end line in script template --- Content/Editor/Scripting/ScriptTemplate.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Content/Editor/Scripting/ScriptTemplate.cs b/Content/Editor/Scripting/ScriptTemplate.cs index 663acf05f..30fbe9d86 100644 --- a/Content/Editor/Scripting/ScriptTemplate.cs +++ b/Content/Editor/Scripting/ScriptTemplate.cs @@ -33,4 +33,3 @@ public class %class% : Script // Here you can add code that needs to be called every frame } } - From f44421b7a76551415e12915599387c862f7ecbb2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Oct 2023 11:25:30 +0200 Subject: [PATCH 147/546] Optimize managed method invoke on NetCore to skip virtual call that is the same as default one --- Source/Engine/Scripting/BinaryModule.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index efd147917..679ead4e3 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -1270,7 +1270,11 @@ bool ManagedBinaryModule::InvokeMethod(void* method, const Variant& instance, Sp // Invoke the method MObject* exception = nullptr; +#if USE_NETCORE // NetCore uses the same path for both virtual and non-virtual calls + MObject* resultObject = mMethod->Invoke(mInstance, params, &exception); +#else MObject* resultObject = withInterfaces ? mMethod->InvokeVirtual((MObject*)mInstance, params, &exception) : mMethod->Invoke(mInstance, params, &exception); +#endif if (exception) { MException ex(exception); From bfaa292b04ae6c793a0d9ab4e4b161cdc52a8687 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Oct 2023 11:37:37 +0200 Subject: [PATCH 148/546] Fix invoking managed method on value types (eg. `Transform`) to properly handle instance value #1801 --- Source/Engine/Engine/NativeInterop.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 2f74e421c..fea1fadda 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -1302,7 +1302,8 @@ namespace FlaxEngine.Interop #if !USE_AOT internal bool TryGetDelegate(out Invoker.MarshalAndInvokeDelegate outDeleg, out object outDelegInvoke) { - if (invokeDelegate == null) + // Skip using in-built delegate for value types (eg. Transform) to properly handle instance value passing to method + if (invokeDelegate == null && !method.DeclaringType.IsValueType) { List methodTypes = new List(); if (!method.IsStatic) From 186e13b5e894d898db4ad8bf3fae70df5f3cbed1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Oct 2023 14:33:21 +0200 Subject: [PATCH 149/546] Add support for runtime running on GPU without Compute Shaders support --- Source/Engine/Graphics/GPUDevice.cpp | 2 ++ Source/Engine/Graphics/Shaders/GPUShader.cpp | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Graphics/GPUDevice.cpp b/Source/Engine/Graphics/GPUDevice.cpp index 34d8e7661..f0056e26c 100644 --- a/Source/Engine/Graphics/GPUDevice.cpp +++ b/Source/Engine/Graphics/GPUDevice.cpp @@ -313,6 +313,8 @@ bool GPUDevice::Init() _res->TasksManager.SetExecutor(CreateTasksExecutor()); LOG(Info, "Total graphics memory: {0}", Utilities::BytesToText(TotalGraphicsMemory)); + if (!Limits.HasCompute) + LOG(Warning, "Compute Shaders are not supported"); return false; } diff --git a/Source/Engine/Graphics/Shaders/GPUShader.cpp b/Source/Engine/Graphics/Shaders/GPUShader.cpp index e76ee9996..86d983fd8 100644 --- a/Source/Engine/Graphics/Shaders/GPUShader.cpp +++ b/Source/Engine/Graphics/Shaders/GPUShader.cpp @@ -87,7 +87,11 @@ bool GPUShader::Create(MemoryReadStream& stream) GPUShaderProgramInitializer initializer; #if !BUILD_RELEASE initializer.Owner = this; + const StringView name = GetName(); +#else + const StringView name; #endif + const bool hasCompute = GPUDevice::Instance->Limits.HasCompute; for (int32 i = 0; i < shadersCount; i++) { const ShaderStage type = static_cast(stream.ReadByte()); @@ -117,10 +121,15 @@ bool GPUShader::Create(MemoryReadStream& stream) stream.ReadBytes(&initializer.Bindings, sizeof(ShaderBindings)); // Create shader program + if (type == ShaderStage::Compute && !hasCompute) + { + LOG(Warning, "Failed to create {} Shader program '{}' ({}).", ::ToString(type), String(initializer.Name), name); + continue; + } GPUShaderProgram* shader = CreateGPUShaderProgram(type, initializer, cache, cacheSize, stream); if (shader == nullptr) { - LOG(Error, "Failed to create {} Shader program '{}'.", ::ToString(type), String(initializer.Name)); + LOG(Error, "Failed to create {} Shader program '{}' ({}).", ::ToString(type), String(initializer.Name), name); return true; } From 1d41aa01ce95d942148deb7b97132872bbc8db9d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Oct 2023 14:36:02 +0200 Subject: [PATCH 150/546] Refactor vertex shaders to use `GPUShaderProgramVS::InputElement` for input layout data --- .../Graphics/Shaders/GPUShaderProgram.h | 13 +++++++ .../DirectX/DX11/GPUShaderDX11.cpp | 35 ++++++----------- .../DirectX/DX12/GPUShaderDX12.cpp | 33 ++++++---------- .../GraphicsDevice/Vulkan/GPUShaderVulkan.cpp | 38 +++++++------------ .../ShadersCompilation/ShaderCompiler.cpp | 19 +++++----- 5 files changed, 59 insertions(+), 79 deletions(-) diff --git a/Source/Engine/Graphics/Shaders/GPUShaderProgram.h b/Source/Engine/Graphics/Shaders/GPUShaderProgram.h index 09c45506f..31f6638dc 100644 --- a/Source/Engine/Graphics/Shaders/GPUShaderProgram.h +++ b/Source/Engine/Graphics/Shaders/GPUShaderProgram.h @@ -122,6 +122,19 @@ public: /// class GPUShaderProgramVS : public GPUShaderProgram { +public: + // Input element run-time data (see VertexShaderMeta::InputElement for compile-time data) + PACK_STRUCT(struct InputElement + { + byte Type; // VertexShaderMeta::InputType + byte Index; + byte Format; // PixelFormat + byte InputSlot; + uint32 AlignedByteOffset; // Fixed value or INPUT_LAYOUT_ELEMENT_ALIGN if auto + byte InputSlotClass; // INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA or INPUT_LAYOUT_ELEMENT_PER_INSTANCE_DATA + uint32 InstanceDataStepRate; // 0 if per-vertex + }); + public: /// /// Gets input layout description handle (platform dependent). diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp index b58684a4e..52192c634 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp @@ -15,32 +15,21 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const { case ShaderStage::Vertex: { - D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; - - // Temporary variables - byte Type, Format, Index, InputSlot, InputSlotClass; - uint32 AlignedByteOffset, InstanceDataStepRate; - - // Load Input Layout (it may be empty) + // Load Input Layout byte inputLayoutSize; stream.ReadByte(&inputLayoutSize); ASSERT(inputLayoutSize <= VERTEX_SHADER_MAX_INPUT_ELEMENTS); + D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; for (int32 a = 0; a < inputLayoutSize; a++) { // Read description - // TODO: maybe use struct and load at once? - stream.ReadByte(&Type); - stream.ReadByte(&Index); - stream.ReadByte(&Format); - stream.ReadByte(&InputSlot); - stream.ReadUint32(&AlignedByteOffset); - stream.ReadByte(&InputSlotClass); - stream.ReadUint32(&InstanceDataStepRate); + GPUShaderProgramVS::InputElement inputElement; + stream.Read(inputElement); // Get semantic name const char* semanticName = nullptr; // TODO: maybe use enum+mapping ? - switch (Type) + switch (inputElement.Type) { case 1: semanticName = "POSITION"; @@ -70,7 +59,7 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const semanticName = "BLENDWEIGHT"; break; default: - LOG(Fatal, "Invalid vertex shader element semantic type: {0}", Type); + LOG(Fatal, "Invalid vertex shader element semantic type: {0}", inputElement.Type); break; } @@ -78,12 +67,12 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const inputLayoutDesc[a] = { semanticName, - static_cast(Index), - static_cast(Format), - static_cast(InputSlot), - static_cast(AlignedByteOffset), - static_cast(InputSlotClass), - static_cast(InstanceDataStepRate) + static_cast(inputElement.Index), + static_cast(inputElement.Format), + static_cast(inputElement.InputSlot), + static_cast(inputElement.AlignedByteOffset), + static_cast(inputElement.InputSlotClass), + static_cast(inputElement.InstanceDataStepRate) }; } diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp index e1e716853..07352b674 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp @@ -20,32 +20,21 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const { case ShaderStage::Vertex: { - D3D12_INPUT_ELEMENT_DESC inputLayout[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; - - // Temporary variables - byte Type, Format, Index, InputSlot, InputSlotClass; - uint32 AlignedByteOffset, InstanceDataStepRate; - // Load Input Layout (it may be empty) byte inputLayoutSize; stream.ReadByte(&inputLayoutSize); ASSERT(inputLayoutSize <= VERTEX_SHADER_MAX_INPUT_ELEMENTS); + D3D12_INPUT_ELEMENT_DESC inputLayout[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; for (int32 a = 0; a < inputLayoutSize; a++) { // Read description - // TODO: maybe use struct and load at once? - stream.ReadByte(&Type); - stream.ReadByte(&Index); - stream.ReadByte(&Format); - stream.ReadByte(&InputSlot); - stream.ReadUint32(&AlignedByteOffset); - stream.ReadByte(&InputSlotClass); - stream.ReadUint32(&InstanceDataStepRate); + GPUShaderProgramVS::InputElement inputElement; + stream.Read(inputElement); // Get semantic name const char* semanticName = nullptr; // TODO: maybe use enum+mapping ? - switch (Type) + switch (inputElement.Type) { case 1: semanticName = "POSITION"; @@ -75,7 +64,7 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const semanticName = "BLENDWEIGHT"; break; default: - LOG(Fatal, "Invalid vertex shader element semantic type: {0}", Type); + LOG(Fatal, "Invalid vertex shader element semantic type: {0}", inputElement.Type); break; } @@ -83,12 +72,12 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const inputLayout[a] = { semanticName, - static_cast(Index), - static_cast(Format), - static_cast(InputSlot), - static_cast(AlignedByteOffset), - static_cast(InputSlotClass), - static_cast(InstanceDataStepRate) + static_cast(inputElement.Index), + static_cast(inputElement.Format), + static_cast(inputElement.InputSlot), + static_cast(inputElement.AlignedByteOffset), + static_cast(inputElement.InputSlotClass), + static_cast(inputElement.InstanceDataStepRate) }; } diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp index 5e583d713..852ce5bad 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp @@ -14,9 +14,9 @@ #include "Engine/Graphics/PixelFormatExtensions.h" #if PLATFORM_DESKTOP -#define VULKAN_UNIFORM_RING_BUFFER_SIZE 24 * 1024 * 1024 +#define VULKAN_UNIFORM_RING_BUFFER_SIZE (24 * 1024 * 1024) #else -#define VULKAN_UNIFORM_RING_BUFFER_SIZE 8 * 1024 * 1024 +#define VULKAN_UNIFORM_RING_BUFFER_SIZE (8 * 1024 * 1024) #endif UniformBufferUploaderVulkan::UniformBufferUploaderVulkan(GPUDeviceVulkan* device) @@ -153,10 +153,6 @@ GPUShaderProgram* GPUShaderVulkan::CreateGPUShaderProgram(ShaderStage type, cons vertexBindingDescriptions[i].inputRate = VK_VERTEX_INPUT_RATE_VERTEX; } - // Temporary variables - byte Type, Format, Index, InputSlot, InputSlotClass; - uint32 AlignedByteOffset, InstanceDataStepRate; - // Load Input Layout (it may be empty) byte inputLayoutSize; stream.ReadByte(&inputLayoutSize); @@ -167,32 +163,26 @@ GPUShaderProgram* GPUShaderVulkan::CreateGPUShaderProgram(ShaderStage type, cons for (int32 a = 0; a < inputLayoutSize; a++) { // Read description - // TODO: maybe use struct and load at once? - stream.ReadByte(&Type); - stream.ReadByte(&Index); - stream.ReadByte(&Format); - stream.ReadByte(&InputSlot); - stream.ReadUint32(&AlignedByteOffset); - stream.ReadByte(&InputSlotClass); - stream.ReadUint32(&InstanceDataStepRate); + GPUShaderProgramVS::InputElement inputElement; + stream.Read(inputElement); - const auto size = PixelFormatExtensions::SizeInBytes((PixelFormat)Format); - if (AlignedByteOffset != INPUT_LAYOUT_ELEMENT_ALIGN) - offset = AlignedByteOffset; + const auto size = PixelFormatExtensions::SizeInBytes((PixelFormat)inputElement.Format); + if (inputElement.AlignedByteOffset != INPUT_LAYOUT_ELEMENT_ALIGN) + offset = inputElement.AlignedByteOffset; - auto& vertexBindingDescription = vertexBindingDescriptions[InputSlot]; - vertexBindingDescription.binding = InputSlot; + auto& vertexBindingDescription = vertexBindingDescriptions[inputElement.InputSlot]; + vertexBindingDescription.binding = inputElement.InputSlot; vertexBindingDescription.stride = Math::Max(vertexBindingDescription.stride, (uint32_t)(offset + size)); - vertexBindingDescription.inputRate = InputSlotClass == INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; - ASSERT(InstanceDataStepRate == 0 || InstanceDataStepRate == 1); + vertexBindingDescription.inputRate = inputElement.InputSlotClass == INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; + ASSERT(inputElement.InstanceDataStepRate == 0 || inputElement.InstanceDataStepRate == 1); auto& vertexAttributeDescription = vertexAttributeDescriptions[a]; vertexAttributeDescription.location = a; - vertexAttributeDescription.binding = InputSlot; - vertexAttributeDescription.format = RenderToolsVulkan::ToVulkanFormat((PixelFormat)Format); + vertexAttributeDescription.binding = inputElement.InputSlot; + vertexAttributeDescription.format = RenderToolsVulkan::ToVulkanFormat((PixelFormat)inputElement.Format); vertexAttributeDescription.offset = offset; - bindingsCount = Math::Max(bindingsCount, (uint32)InputSlot + 1); + bindingsCount = Math::Max(bindingsCount, (uint32)inputElement.InputSlot + 1); offset += size; } diff --git a/Source/Engine/ShadersCompilation/ShaderCompiler.cpp b/Source/Engine/ShadersCompilation/ShaderCompiler.cpp index 291910b11..c0fda4d6e 100644 --- a/Source/Engine/ShadersCompilation/ShaderCompiler.cpp +++ b/Source/Engine/ShadersCompilation/ShaderCompiler.cpp @@ -460,16 +460,15 @@ bool ShaderCompiler::WriteCustomDataVS(ShaderCompilationContext* context, Shader auto& element = layout[a]; if (!layoutVisible[a]) continue; - - // TODO: serialize whole struct? - - output->WriteByte(static_cast(element.Type)); - output->WriteByte(element.Index); - output->WriteByte(static_cast(element.Format)); - output->WriteByte(element.InputSlot); - output->WriteUint32(element.AlignedByteOffset); - output->WriteByte(element.InputSlotClass); - output->WriteUint32(element.InstanceDataStepRate); + GPUShaderProgramVS::InputElement data; + data.Type = static_cast(element.Type); + data.Index = element.Index; + data.Format = static_cast(element.Format); + data.InputSlot = element.InputSlot; + data.AlignedByteOffset = element.AlignedByteOffset; + data.InputSlotClass = element.InputSlotClass; + data.InstanceDataStepRate = element.InstanceDataStepRate; + output->Write(data); } return false; From d9b0e99b9c46e5370f79ad810beb21b709a40815 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Oct 2023 15:20:35 +0200 Subject: [PATCH 151/546] Add support for using `Revision` number in projects version --- Source/Editor/ProjectInfo.cpp | 3 +- Source/Editor/ProjectInfo.cs | 85 ++++++++--------- Source/Engine/Core/Types/Version.cpp | 6 +- .../Bindings/BindingsGenerator.Cpp.cs | 7 +- Source/Tools/Flax.Build/ProjectInfo.cs | 93 +++++++++---------- 5 files changed, 91 insertions(+), 103 deletions(-) diff --git a/Source/Editor/ProjectInfo.cpp b/Source/Editor/ProjectInfo.cpp index 4e7ee4483..30c558b5d 100644 --- a/Source/Editor/ProjectInfo.cpp +++ b/Source/Editor/ProjectInfo.cpp @@ -154,7 +154,8 @@ bool ProjectInfo::LoadProject(const String& projectPath) Version = ::Version( JsonTools::GetInt(version, "Major", 0), JsonTools::GetInt(version, "Minor", 0), - JsonTools::GetInt(version, "Build", 0)); + JsonTools::GetInt(version, "Build", -1), + JsonTools::GetInt(version, "Revision", -1)); } } if (Version.Revision() == 0) diff --git a/Source/Editor/ProjectInfo.cs b/Source/Editor/ProjectInfo.cs index b00c4e042..083665f0f 100644 --- a/Source/Editor/ProjectInfo.cs +++ b/Source/Editor/ProjectInfo.cs @@ -23,17 +23,11 @@ namespace FlaxEditor public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { if (value == null) - { writer.WriteNull(); - } else if (value is Version) - { writer.WriteValue(value.ToString()); - } else - { throw new JsonSerializationException("Expected Version object value"); - } } /// @@ -47,65 +41,60 @@ namespace FlaxEditor public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) - { return null; - } - else + + if (reader.TokenType == JsonToken.StartObject) { - if (reader.TokenType == JsonToken.StartObject) + try { - try + reader.Read(); + var values = new Dictionary(); + while (reader.TokenType == JsonToken.PropertyName) { + var key = reader.Value as string; reader.Read(); - Dictionary values = new Dictionary(); - while (reader.TokenType == JsonToken.PropertyName) - { - var key = reader.Value as string; - reader.Read(); - var val = (long)reader.Value; - reader.Read(); - values.Add(key, (int)val); - } + var val = (long)reader.Value; + reader.Read(); + values.Add(key, (int)val); + } - int major = 0, minor = 0, build = 0; - values.TryGetValue("Major", out major); - values.TryGetValue("Minor", out minor); - values.TryGetValue("Build", out build); + values.TryGetValue("Major", out var major); + values.TryGetValue("Minor", out var minor); + if (!values.TryGetValue("Build", out var build)) + build = -1; + if (!values.TryGetValue("Revision", out var revision)) + revision = -1; - Version v = new Version(major, minor, build); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); - } + if (build <= 0) + return new Version(major, minor); + if (revision <= 0) + return new Version(major, minor, build); + return new Version(major, minor, build, revision); } - else if (reader.TokenType == JsonToken.String) + catch (Exception ex) { - try - { - Version v = new Version((string)reader.Value!); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); - } - } - else - { - throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.Value)); + throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); } } + if (reader.TokenType == JsonToken.String) + { + try + { + return new Version((string)reader.Value!); + } + catch (Exception ex) + { + throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); + } + } + throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.Value)); } /// /// Determines whether this instance can convert the specified object type. /// /// Type of the object. - /// - /// true if this instance can convert the specified object type; otherwise, false. - /// + /// true if this instance can convert the specified object type; otherwise, false. public override bool CanConvert(Type objectType) { return objectType == typeof(Version); diff --git a/Source/Engine/Core/Types/Version.cpp b/Source/Engine/Core/Types/Version.cpp index 4a11a0af6..c0410d1cb 100644 --- a/Source/Engine/Core/Types/Version.cpp +++ b/Source/Engine/Core/Types/Version.cpp @@ -7,15 +7,15 @@ Version::Version(int32 major, int32 minor, int32 build, int32 revision) { _major = Math::Max(major, 0); _minor = Math::Max(minor, 0); - _build = Math::Max(build, 0); - _revision = Math::Max(revision, 0); + _build = Math::Max(build, -1); + _revision = Math::Max(revision, -1); } Version::Version(int32 major, int32 minor, int32 build) { _major = Math::Max(major, 0); _minor = Math::Max(minor, 0); - _build = Math::Max(build, 0); + _build = Math::Max(build, -1); _revision = -1; } diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 90e96d075..10e4d5846 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -3135,14 +3135,17 @@ namespace Flax.Build.Bindings contents.AppendLine("#pragma once"); contents.AppendLine(); contents.AppendLine($"#define {binaryModuleNameUpper}_NAME \"{binaryModuleName}\""); - if (version.Build == -1) + if (version.Build <= 0) contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION Version({version.Major}, {version.Minor})"); - else + else if (version.Revision <= 0) contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION Version({version.Major}, {version.Minor}, {version.Build})"); + else + contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION Version({version.Major}, {version.Minor}, {version.Build}, {version.Revision})"); contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_TEXT \"{version}\""); contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_MAJOR {version.Major}"); contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_MINOR {version.Minor}"); contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_BUILD {version.Build}"); + contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_REVISION {version.Revision}"); contents.AppendLine($"#define {binaryModuleNameUpper}_COMPANY \"{project.Company}\""); contents.AppendLine($"#define {binaryModuleNameUpper}_COPYRIGHT \"{project.Copyright}\""); contents.AppendLine(); diff --git a/Source/Tools/Flax.Build/ProjectInfo.cs b/Source/Tools/Flax.Build/ProjectInfo.cs index 7830f59c1..ec615fc49 100644 --- a/Source/Tools/Flax.Build/ProjectInfo.cs +++ b/Source/Tools/Flax.Build/ProjectInfo.cs @@ -14,9 +14,9 @@ namespace Flax.Build /// /// Writes the JSON representation of the object. /// - /// The to write to. + /// The to write to. /// The value. - /// The calling serializer. + /// The calling serializer. public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); @@ -25,73 +25,68 @@ namespace Flax.Build /// /// Reads the JSON representation of the object. /// - /// The to read from. - /// Type of the object. - /// The existing property value of the JSON that is being converted. - /// The calling serializer. + /// The to read from. + /// Type of the object. + /// The serializer options. /// The object value. public override Version? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) - { return null; - } - else + + if (reader.TokenType == JsonTokenType.StartObject) { - if (reader.TokenType == JsonTokenType.StartObject) + try { - try + reader.Read(); + var values = new Dictionary(); + while (reader.TokenType == JsonTokenType.PropertyName) { + var key = reader.GetString(); reader.Read(); - Dictionary values = new Dictionary(); - while (reader.TokenType == JsonTokenType.PropertyName) - { - var key = reader.GetString(); - reader.Read(); - var val = reader.GetInt32(); - reader.Read(); - values.Add(key, val); - } + var val = reader.GetInt32(); + reader.Read(); + values.Add(key, val); + } - int major = 0, minor = 0, build = 0; - values.TryGetValue("Major", out major); - values.TryGetValue("Minor", out minor); - values.TryGetValue("Build", out build); + values.TryGetValue("Major", out var major); + values.TryGetValue("Minor", out var minor); + if (!values.TryGetValue("Build", out var build)) + build = -1; + if (!values.TryGetValue("Revision", out var revision)) + revision = -1; - Version v = new Version(major, minor, build); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.GetString()), ex); - } + if (build <= 0) + return new Version(major, minor); + if (revision <= 0) + return new Version(major, minor, build); + return new Version(major, minor, build, revision); } - else if (reader.TokenType == JsonTokenType.String) + catch (Exception ex) { - try - { - Version v = new Version((string)reader.GetString()!); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.GetString()), ex); - } - } - else - { - throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.GetString())); + throw new Exception(string.Format("Error parsing version string: {0}", reader.GetString()), ex); } } + + if (reader.TokenType == JsonTokenType.String) + { + try + { + return new Version((string)reader.GetString()!); + } + catch (Exception ex) + { + throw new Exception(string.Format("Error parsing version string: {0}", reader.GetString()), ex); + } + } + throw new Exception(string.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.GetString())); } /// /// Determines whether this instance can convert the specified object type. /// /// Type of the object. - /// - /// true if this instance can convert the specified object type; otherwise, false. - /// + /// true if this instance can convert the specified object type; otherwise, false. public override bool CanConvert(Type objectType) { return objectType == typeof(Version); @@ -318,7 +313,7 @@ namespace Flax.Build Log.Verbose("Loading project file from \"" + path + "\"..."); var contents = File.ReadAllText(path); var project = JsonSerializer.Deserialize(contents.AsSpan(), - new JsonSerializerOptions() { Converters = { new FlaxVersionConverter() }, IncludeFields = true, TypeInfoResolver = ProjectInfoSourceGenerationContext.Default }); + new JsonSerializerOptions() { Converters = { new FlaxVersionConverter() }, IncludeFields = true, TypeInfoResolver = ProjectInfoSourceGenerationContext.Default }); project.ProjectPath = path; project.ProjectFolderPath = Path.GetDirectoryName(path); From 70a06c5db362624f51358d6e0afc8f76b0197ba5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 26 Oct 2023 15:21:01 +0200 Subject: [PATCH 152/546] Update build number and add revision field for future patches --- Flax.flaxproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flax.flaxproj b/Flax.flaxproj index 6b3014e94..78d9714f5 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -3,7 +3,8 @@ "Version": { "Major": 1, "Minor": 7, - "Build": 6402 + "Revision": 0, + "Build": 6403 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.", From 900e6338d6ada108ac8c89f0531c29fd98bb577f Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Fri, 27 Oct 2023 19:21:21 +0300 Subject: [PATCH 153/546] Fix wrong variable name Co-authored-by: NoriteSC <53096989+NoriteSC@users.noreply.github.com> --- Source/Editor/Windows/GameWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index 45952e179..8c6f5dc06 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -305,7 +305,7 @@ namespace FlaxEditor.Windows InputActions.Add(options => options.TakeScreenshot, () => Screenshot.Capture(string.Empty)); InputActions.Add(options => options.DebuggerUnlockMouse, UnlockMouseInPlay); - InputActions.Add(options => options.DebuggerToggleFullscreen, () => { if (Editor.IsPlayMode) IsMaximized = !IsMaximized; }); + InputActions.Add(options => options.ToggleFullscreen, () => { if (Editor.IsPlayMode) IsMaximized = !IsMaximized; }); FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); } From 90642b8862e2b6ab6f57669a9975ccb89c0fc70d Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 27 Oct 2023 13:25:50 -0500 Subject: [PATCH 154/546] Spawn canvas scalar on canvas creation. --- Source/Editor/SceneGraph/Actors/UICanvasNode.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs index c09e1a246..41e1c77c6 100644 --- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs +++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs @@ -7,6 +7,7 @@ using Real = System.Single; #endif using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.SceneGraph.Actors { @@ -30,6 +31,12 @@ namespace FlaxEditor.SceneGraph.Actors // Rotate to match the space (GUI uses upper left corner as a root) Actor.LocalOrientation = Quaternion.Euler(0, -180, -180); + var uiControl = new UIControl + { + Name = "Canvas Scalar", + Control = new CanvasScaler() + }; + Root.Spawn(uiControl, Actor); } /// From 615b6470e53a4643230fd51aee70454958544706 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Oct 2023 11:44:11 +0200 Subject: [PATCH 155/546] Fix iOS project path if it contains whitespace chars --- .../iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj b/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj index 1dd5d4e01..3b8889487 100644 --- a/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj +++ b/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj @@ -222,7 +222,7 @@ ${PBXResourcesGroup} GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = ${HeaderSearchPaths}; + HEADER_SEARCH_PATHS = "${HeaderSearchPaths}"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -275,7 +275,7 @@ ${PBXResourcesGroup} GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = ${HeaderSearchPaths}; + HEADER_SEARCH_PATHS = "${HeaderSearchPaths}"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; From a95c9059aa09501503ea7e6f32b8b94bbf2663d9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 27 Oct 2023 10:08:04 +0200 Subject: [PATCH 156/546] Update build number --- Flax.flaxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flax.flaxproj b/Flax.flaxproj index 78d9714f5..a3157a032 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 7, "Revision": 0, - "Build": 6403 + "Build": 6404 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.", From b028faf0ccdc35df62ce9d9f98e388eec9cf71b7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Oct 2023 13:52:23 +0200 Subject: [PATCH 157/546] Update assets --- Content/Editor/Particles/Smoke.flax | 4 ++-- Content/Editor/Particles/Sparks.flax | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Content/Editor/Particles/Smoke.flax b/Content/Editor/Particles/Smoke.flax index 1335a84f4..b42c2f325 100644 --- a/Content/Editor/Particles/Smoke.flax +++ b/Content/Editor/Particles/Smoke.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3dc51e7805056006ca6cbb481ba202583a9b2287c152fc04e28e1d07747d6ce -size 14706 +oid sha256:334ac0d00495fc88b10839061ff0c3f45323d4f75ab6176b19005199a7324a19 +size 14569 diff --git a/Content/Editor/Particles/Sparks.flax b/Content/Editor/Particles/Sparks.flax index 7977e231b..7ee0ed6e9 100644 --- a/Content/Editor/Particles/Sparks.flax +++ b/Content/Editor/Particles/Sparks.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d902ab5f79426cc66dc5f19a3b8280136a58aa3c6fd317554d1a032357c65a -size 15275 +oid sha256:87046a9bfe275cac290b4764de8a512c222ccc386d01af9026d57c3e4b7773b6 +size 13625 From 468c93949ece14505fcd643350112290198da936 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Oct 2023 14:01:44 +0200 Subject: [PATCH 158/546] Fix crash when creating empty particle emitter --- Source/Engine/Particles/Particles.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp index 52845a0f8..784f28c98 100644 --- a/Source/Engine/Particles/Particles.cpp +++ b/Source/Engine/Particles/Particles.cpp @@ -1334,7 +1334,8 @@ void ParticlesSystem::Job(int32 index) auto emitter = particleSystem->Emitters[track.AsEmitter.Index].Get(); auto& data = instance.Emitters[track.AsEmitter.Index]; ASSERT(emitter && emitter->IsLoaded()); - ASSERT(emitter->Capacity != 0 && emitter->Graph.Layout.Size != 0); + if (emitter->Capacity == 0 || emitter->Graph.Layout.Size == 0) + continue; PROFILE_CPU_ASSET(emitter); // Calculate new time position From 9f3221c533cd75716b2fbe4b0ce9ab20f6f1e959 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Oct 2023 14:02:25 +0200 Subject: [PATCH 159/546] Remove unused include --- Source/Engine/Physics/Actors/PhysicsColliderActor.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Engine/Physics/Actors/PhysicsColliderActor.cpp b/Source/Engine/Physics/Actors/PhysicsColliderActor.cpp index 02645943c..f2ab5d8a2 100644 --- a/Source/Engine/Physics/Actors/PhysicsColliderActor.cpp +++ b/Source/Engine/Physics/Actors/PhysicsColliderActor.cpp @@ -1,7 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "PhysicsColliderActor.h" -#include "Engine/Scripting/Script.h" #include "RigidBody.h" PhysicsColliderActor::PhysicsColliderActor(const SpawnParams& params) From 590f3f7493f8fff78e83256f43fef417540035fa Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 28 Oct 2023 10:29:39 -0500 Subject: [PATCH 160/546] Fix Dropdown scaling with CanvasScalar. Add limiting number of items to show in the dropdown. --- Source/Engine/UI/GUI/Common/Dropdown.cs | 57 ++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index aea4e173a..bf2dbc0ef 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -22,6 +22,11 @@ namespace FlaxEngine.GUI /// Occurs when popup lost focus. /// public Action LostFocus; + + /// + /// The selected control. Used to scroll to the control on popup creation. + /// + public ContainerControl SelectedControl = null; /// public override void OnEndContainsFocus() @@ -233,6 +238,18 @@ namespace FlaxEngine.GUI } } + /// + /// Gets or sets whether to show all of the items. + /// + [EditorOrder(3), Tooltip("Whether to show all of the items in the drop down.")] + public bool ShowAllItems { get; set; } = true; + + /// + /// Gets or sets the number of items to show. Only used if ShowAllItems is false. + /// + [EditorOrder(4), VisibleIf(nameof(ShowAllItems), true), Limit(1), Tooltip("The number of items to show in the drop down.")] + public int NumberOfItemsToShow { get; set; } = 5; + /// /// Event fired when selected index gets changed. /// @@ -411,13 +428,22 @@ namespace FlaxEngine.GUI // TODO: support item templates - var container = new VerticalPanel + var panel = new Panel { AnchorPreset = AnchorPresets.StretchAll, BackgroundColor = BackgroundColor, - AutoSize = false, + ScrollBars = ScrollBars.Vertical, Parent = popup, }; + + var container = new VerticalPanel + { + AnchorPreset = AnchorPresets.StretchAll, + BackgroundColor = Color.Transparent, + IsScrollable = true, + AutoSize = true, + Parent = panel, + }; var border = new Border { BorderColor = BorderColorHighlighted, @@ -482,10 +508,20 @@ namespace FlaxEngine.GUI //AnchorPreset = AnchorPresets.VerticalStretchLeft, Parent = item, }; + popup.SelectedControl = item; } } - popup.Size = new Float2(itemsWidth, height); + if (ShowAllItems || _items.Count < NumberOfItemsToShow) + { + popup.Size = new Float2(itemsWidth, height); + panel.Size = popup.Size; + } + else + { + popup.Size = new Float2(itemsWidth, (itemsHeight + container.Spacing) * NumberOfItemsToShow); + panel.Size = popup.Size; + } return popup; } @@ -527,7 +563,16 @@ namespace FlaxEngine.GUI /// public void ShowPopup() { - var root = Root; + // Find canvas scalar and set as root if it exists. + ContainerControl c = Parent; + while(c.Parent != Root && c.Parent != null) + { + c = c.Parent; + if (c is CanvasScaler scalar) + break; + } + var root = c is CanvasScaler ? c : Root; + if (_items.Count == 0 || root == null) return; @@ -542,7 +587,7 @@ namespace FlaxEngine.GUI // Show dropdown popup var locationRootSpace = Location + new Float2(0, Height); var parent = Parent; - while (parent != null && parent != Root) + while (parent != null && parent != root) { locationRootSpace = parent.PointToParent(ref locationRootSpace); parent = parent.Parent; @@ -551,6 +596,8 @@ namespace FlaxEngine.GUI _popup.Parent = root; _popup.Focus(); _popup.StartMouseCapture(); + if (_popup.SelectedControl != null && _popup.Children[0] is Panel panel) + panel.ScrollViewTo(_popup.SelectedControl, true); OnPopupShow(); } From 1fa03a0de285ee0a3443cc627dfeeaf880de8ef9 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 28 Oct 2023 11:25:14 -0500 Subject: [PATCH 161/546] Fix focus issues to allow panel scroll bar to be clicked and used. --- Source/Engine/UI/GUI/Common/Dropdown.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index bf2dbc0ef..a6c79ea3f 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -32,7 +32,13 @@ namespace FlaxEngine.GUI public override void OnEndContainsFocus() { base.OnEndContainsFocus(); - + + // Dont lose focus when using panel. Does prevent LostFocus even from being called if clicking inside of the panel. + if (Children[0] is Panel panel && panel.IsMouseOver && !panel.ContainsFocus) + { + panel.Focus(); + return; + } // Call event after this 'focus contains flag' propagation ends to prevent focus issues if (LostFocus != null) Scripting.RunOnUpdate(LostFocus); @@ -433,6 +439,7 @@ namespace FlaxEngine.GUI AnchorPreset = AnchorPresets.StretchAll, BackgroundColor = BackgroundColor, ScrollBars = ScrollBars.Vertical, + AutoFocus = true, Parent = popup, }; From d9c2decff50f8c5e2658a151624160505cb43d2c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Oct 2023 20:09:27 +0200 Subject: [PATCH 162/546] Fix crash when contact was not read properly --- Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp index 684aeca19..5968bdd78 100644 --- a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp +++ b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp @@ -117,9 +117,7 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c { // Skip sending events to removed actors if (pairHeader.flags & (PxContactPairHeaderFlag::eREMOVED_ACTOR_0 | PxContactPairHeaderFlag::eREMOVED_ACTOR_1)) - { return; - } Collision c; PxContactPairExtraDataIterator j(pairHeader.extraDataStream, pairHeader.extraDataStreamSize); @@ -193,7 +191,7 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c RemovedCollisions.Add(c); } } - ASSERT(!j.nextItemSet()); + //ASSERT(!j.nextItemSet()); } void SimulationEventCallback::onTrigger(PxTriggerPair* pairs, PxU32 count) From 9c1a7a20d490ee3cf561dd4f9b6dab919595e045 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 28 Oct 2023 22:10:41 +0200 Subject: [PATCH 163/546] Add `volk.h` header to distributed build files --- Source/ThirdParty/volk/volk.Build.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/ThirdParty/volk/volk.Build.cs b/Source/ThirdParty/volk/volk.Build.cs index 2f4cfa0d6..67f7593d9 100644 --- a/Source/ThirdParty/volk/volk.Build.cs +++ b/Source/ThirdParty/volk/volk.Build.cs @@ -1,5 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System.Collections.Generic; using System.IO; using Flax.Build; using Flax.Build.NativeCpp; @@ -62,4 +63,12 @@ public class volk : ThirdPartyModule Log.ErrorOnce("Missing VulkanSDK.", ref _missingSDKError); } } + + /// + public override void GetFilesToDeploy(List files) + { + base.GetFilesToDeploy(files); + + files.Add(Path.Combine(FolderPath, "volk.h")); + } } From 83a931de7e21f1dfdc79461f3fc5214b225019da Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 29 Oct 2023 01:57:48 +0200 Subject: [PATCH 164/546] Fix typo bug in DOF --- Source/Engine/Renderer/DepthOfFieldPass.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Renderer/DepthOfFieldPass.cpp b/Source/Engine/Renderer/DepthOfFieldPass.cpp index cd7930f5c..707a441e6 100644 --- a/Source/Engine/Renderer/DepthOfFieldPass.cpp +++ b/Source/Engine/Renderer/DepthOfFieldPass.cpp @@ -204,7 +204,7 @@ void DepthOfFieldPass::Render(RenderContext& renderContext, GPUTexture*& frame, { DepthOfFieldSettings& dofSettings = renderContext.List->Settings.DepthOfField; const bool useDoF = EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::DepthOfField) && dofSettings.Enabled; - if (!useDoF || _platformSupportsDoF || checkIfSkipPass()) + if (!useDoF || !_platformSupportsDoF || checkIfSkipPass()) return; auto device = GPUDevice::Instance; auto context = device->GetMainContext(); From 56c9429e253960e2dbb16b80462877d812dc1662 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 29 Oct 2023 02:57:59 +0200 Subject: [PATCH 165/546] Fix spawning prefab without transform provided #1831 --- Source/Engine/Level/Prefabs/PrefabManager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index cd9d893ec..de164343b 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -39,7 +39,7 @@ PrefabManagerService PrefabManagerServiceInstance; Actor* PrefabManager::SpawnPrefab(Prefab* prefab) { Actor* parent = Level::Scenes.Count() != 0 ? Level::Scenes.Get()[0] : nullptr; - return SpawnPrefab(prefab, Transform::Identity, parent, nullptr); + return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, nullptr); } Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Vector3& position) @@ -73,12 +73,12 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, const Transform Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent) { - return SpawnPrefab(prefab, Transform::Identity, parent, nullptr); + return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, nullptr); } Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization) { - return SpawnPrefab(prefab, Transform::Identity, parent, objectsCache, withSynchronization); + return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, objectsCache, withSynchronization); } Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization) @@ -191,7 +191,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac parent->Children.Add(root); // Move root to the right location - if (transform != Transform::Identity) + if (transform.Translation != Vector3::Minimum) root->SetTransform(transform); // Link actors hierarchy From f434ff2efe76ecaadbbbb7c075dd561febf2fd2a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 29 Oct 2023 13:37:05 +0100 Subject: [PATCH 166/546] Fix memory overcommitment by `HashSet` when adding and removing the same item in a loop #1829 --- Source/Engine/Core/Collections/HashSet.h | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index 107e42e65..fba8ca823 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -82,7 +82,6 @@ public: private: int32 _elementsCount = 0; - int32 _deletedCount = 0; int32 _size = 0; AllocationData _allocation; @@ -109,14 +108,11 @@ public: /// The other collection to move. HashSet(HashSet&& other) noexcept : _elementsCount(other._elementsCount) - , _deletedCount(other._deletedCount) , _size(other._size) { _elementsCount = other._elementsCount; - _deletedCount = other._deletedCount; _size = other._size; other._elementsCount = 0; - other._deletedCount = 0; other._size = 0; _allocation.Swap(other._allocation); } @@ -154,10 +150,8 @@ public: Clear(); _allocation.Free(); _elementsCount = other._elementsCount; - _deletedCount = other._deletedCount; _size = other._size; other._elementsCount = 0; - other._deletedCount = 0; other._size = 0; _allocation.Swap(other._allocation); } @@ -337,12 +331,12 @@ public: /// void Clear() { - if (_elementsCount + _deletedCount != 0) + if (_elementsCount != 0) { Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) data[i].Free(); - _elementsCount = _deletedCount = 0; + _elementsCount = 0; } } @@ -377,7 +371,7 @@ public: oldAllocation.Swap(_allocation); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; - _deletedCount = _elementsCount = 0; + _elementsCount = 0; if (capacity != 0 && (capacity & (capacity - 1)) != 0) { // Align capacity value to the next power of two (http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2) @@ -439,7 +433,7 @@ public: bool Add(const ItemType& item) { // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); + EnsureCapacity(_elementsCount + 1); // Find location of the item or place to insert it FindPositionResult pos; @@ -485,7 +479,6 @@ public: { _allocation.Get()[pos.ObjectIndex].Delete(); _elementsCount--; - _deletedCount++; return true; } return false; @@ -504,7 +497,6 @@ public: ASSERT(_allocation.Get()[i._index].IsOccupied()); _allocation.Get()[i._index].Delete(); _elementsCount--; - _deletedCount++; return true; } return false; From 357148f9736a2ecf15ac2aebb957661e9f0852bb Mon Sep 17 00:00:00 2001 From: MineBill Date: Mon, 30 Oct 2023 08:07:46 +0200 Subject: [PATCH 167/546] Include interfaces in FindScript. --- Source/Engine/Level/Actor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 75e3c12d4..addb4861e 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1406,7 +1406,7 @@ Script* Actor::FindScript(const MClass* type) const CHECK_RETURN(type, nullptr); for (auto script : Scripts) { - if (script->GetClass()->IsSubClassOf(type)) + if (script->GetClass()->IsSubClassOf(type) || script->GetClass()->HasInterface(type)) return script; } for (auto child : Children) From 1fc972d6acff80abfa3e700d659efcf3ff3cf16e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Oct 2023 16:45:57 +0100 Subject: [PATCH 168/546] Fix stall in GC collect on iOS with AOT --- Source/Engine/Engine/NativeInterop.Unmanaged.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 0391974b6..5af1569b9 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -1181,7 +1181,10 @@ namespace FlaxEngine.Interop [UnmanagedCallersOnly] internal static void GCCollect(int generation, int mode, bool blocking, bool compacting) { + // TODO: fix stall on iOS with AOT +#if !USE_AOT GC.Collect(generation, (GCCollectionMode)mode, blocking, compacting); +#endif } [UnmanagedCallersOnly] From 2d1d8cc31063177481fd95335525f7bba0aa6f71 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 30 Oct 2023 18:34:20 +0100 Subject: [PATCH 169/546] Fix typo --- Source/Engine/Scripting/Object.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Scripting/Object.cs b/Source/Engine/Scripting/Object.cs index c64532c2b..9fc7d8039 100644 --- a/Source/Engine/Scripting/Object.cs +++ b/Source/Engine/Scripting/Object.cs @@ -320,7 +320,7 @@ namespace FlaxEngine internal static partial Object Internal_Create2(string typeName); [LibraryImport("FlaxEngine", EntryPoint = "ObjectInternal_ManagedInstanceCreated", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(Interop.StringMarshaller))] - internal static partial void Internal_ManagedInstanceCreated(Object managedInstance, IntPtr theKlass); + internal static partial void Internal_ManagedInstanceCreated(Object managedInstance, IntPtr typeClass); [LibraryImport("FlaxEngine", EntryPoint = "ObjectInternal_ManagedInstanceDeleted", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(Interop.StringMarshaller))] internal static partial void Internal_ManagedInstanceDeleted(IntPtr nativeInstance); From 1567c1c8d6c998e043f39c511e2acd562f7dbaa9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 31 Oct 2023 14:15:35 +0100 Subject: [PATCH 170/546] Fix Mono AOT on iOS to not freeze during GC stop-the-world event on memory collection --- Source/Engine/Engine/NativeInterop.Unmanaged.cs | 3 --- Source/Engine/Scripting/Runtime/DotNet.cpp | 7 +++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 5af1569b9..0391974b6 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -1181,10 +1181,7 @@ namespace FlaxEngine.Interop [UnmanagedCallersOnly] internal static void GCCollect(int generation, int mode, bool blocking, bool compacting) { - // TODO: fix stall on iOS with AOT -#if !USE_AOT GC.Collect(generation, (GCCollectionMode)mode, blocking, compacting); -#endif } [UnmanagedCallersOnly] diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index b3e5a0072..91b1d3dde 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include typedef char char_t; #define DOTNET_HOST_MONO_DEBUG 0 @@ -522,7 +523,8 @@ void MCore::GC::FreeMemory(void* ptr, bool coTaskMem) void MCore::Thread::Attach() { -#if DOTNET_HOST_MONO + // TODO: find a way to properly register native thread so Mono Stop The World (stw) won't freeze when native threads (eg. Job System) are running native code only +#if DOTNET_HOST_MONO && !USE_MONO_AOT if (!IsInMainThread() && !mono_domain_get()) { mono_thread_attach(MonoDomainHandle); @@ -2056,7 +2058,7 @@ bool InitHostfxr() // Setup debugger { int32 debuggerLogLevel = 0; - if (CommandLine::Options.MonoLog.IsTrue()) + if (CommandLine::Options.MonoLog.IsTrue() || DOTNET_HOST_MONO_DEBUG) { LOG(Info, "Using detailed Mono logging"); mono_trace_set_level_string("debug"); @@ -2139,6 +2141,7 @@ bool InitHostfxr() LOG(Fatal, "Failed to initialize Mono."); return true; } + mono_gc_init_finalizer_thread(); // Log info char* buildInfo = mono_get_runtime_build_info(); From 4e2ee897bc8ebecc8bff5224cfd3705e23a376d9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 31 Oct 2023 15:22:14 +0100 Subject: [PATCH 171/546] Fix missing codesign for macOS game binaries --- .../Flax.Build/Deploy/Deployment.Platforms.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs index a2846aa14..e1d159ed5 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs @@ -55,6 +55,20 @@ namespace Flax.Deploy CodeSign(Path.Combine(binaries, "FlaxGame.exe")); CodeSign(Path.Combine(binaries, "FlaxEngine.CSharp.dll")); } + else if (platform == TargetPlatform.Mac) + { + var binaries = Path.Combine(dst, "Binaries", "Game", "arm64", "Debug"); + CodeSign(Path.Combine(binaries, "FlaxGame")); + CodeSign(Path.Combine(binaries, "FlaxGame.dylib")); + + binaries = Path.Combine(dst, "Binaries", "Game", "arm64", "Development"); + CodeSign(Path.Combine(binaries, "FlaxGame")); + CodeSign(Path.Combine(binaries, "FlaxGame.dylib")); + + binaries = Path.Combine(dst, "Binaries", "Game", "arm64", "Release"); + CodeSign(Path.Combine(binaries, "FlaxGame")); + CodeSign(Path.Combine(binaries, "FlaxGame.dylib")); + } // Don't distribute engine deps Utilities.DirectoryDelete(Path.Combine(dst, "Binaries", "ThirdParty")); From f9614a48796c1b6e79375f5231062e82d874b440 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 31 Oct 2023 15:31:40 +0100 Subject: [PATCH 172/546] Disable capacity alloc on delegate creation --- Source/Engine/Core/Delegate.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h index 821d7916e..945dd6ce5 100644 --- a/Source/Engine/Core/Delegate.h +++ b/Source/Engine/Core/Delegate.h @@ -511,7 +511,7 @@ public: _locker = New(); ScopeLock lock(*_locker); if (_functions == nullptr) - _functions = New>(32); + _functions = New>(); _functions->Add(f); #endif } From df5dc0c284058c3c3e259174be57a0731a5f4175 Mon Sep 17 00:00:00 2001 From: MineBill Date: Tue, 31 Oct 2023 16:32:57 +0200 Subject: [PATCH 173/546] Extract validation/item handling of AssetPicker in a separate class. --- .../CustomEditors/Editors/AssetRefEditor.cs | 24 +- .../Editors/ModelInstanceEntryEditor.cs | 4 +- Source/Editor/GUI/AssetPicker.cs | 300 +++--------------- .../Timeline/Tracks/SingleMediaAssetTrack.cs | 6 +- .../Archetypes/Animation.MultiBlend.cs | 6 +- Source/Editor/Surface/Archetypes/Function.cs | 6 +- Source/Editor/Surface/Elements/AssetSelect.cs | 4 +- Source/Editor/Tools/Terrain/EditTab.cs | 6 +- .../Editor/Utilities/AssetPickerValidator.cs | 293 +++++++++++++++++ .../Windows/AssetReferencesGraphWindow.cs | 6 +- .../Windows/Assets/SkinnedModelWindow.cs | 10 +- 11 files changed, 367 insertions(+), 298 deletions(-) create mode 100644 Source/Editor/Utilities/AssetPickerValidator.cs diff --git a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs index cfba940c2..1f3359fd5 100644 --- a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs +++ b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs @@ -71,7 +71,7 @@ namespace FlaxEditor.CustomEditors.Editors { // Generic file picker assetType = ScriptType.Null; - Picker.FileExtension = assetReference.TypeName; + Picker.Validator.FileExtension = assetReference.TypeName; } else { @@ -85,7 +85,7 @@ namespace FlaxEditor.CustomEditors.Editors } } - Picker.AssetType = assetType; + Picker.Validator.AssetType = assetType; Picker.Height = height; Picker.SelectedItemChanged += OnSelectedItemChanged; } @@ -95,15 +95,15 @@ namespace FlaxEditor.CustomEditors.Editors if (_isRefreshing) return; if (typeof(AssetItem).IsAssignableFrom(_valueType.Type)) - SetValue(Picker.SelectedItem); + SetValue(Picker.Validator.SelectedItem); else if (_valueType.Type == typeof(Guid)) - SetValue(Picker.SelectedID); + SetValue(Picker.Validator.SelectedID); else if (_valueType.Type == typeof(SceneReference)) - SetValue(new SceneReference(Picker.SelectedID)); + SetValue(new SceneReference(Picker.Validator.SelectedID)); else if (_valueType.Type == typeof(string)) - SetValue(Picker.SelectedPath); + SetValue(Picker.Validator.SelectedPath); else - SetValue(Picker.SelectedAsset); + SetValue(Picker.Validator.SelectedAsset); } /// @@ -115,15 +115,15 @@ namespace FlaxEditor.CustomEditors.Editors { _isRefreshing = true; if (Values[0] is AssetItem assetItem) - Picker.SelectedItem = assetItem; + Picker.Validator.SelectedItem = assetItem; else if (Values[0] is Guid guid) - Picker.SelectedID = guid; + Picker.Validator.SelectedID = guid; else if (Values[0] is SceneReference sceneAsset) - Picker.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID); + Picker.Validator.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID); else if (Values[0] is string path) - Picker.SelectedPath = path; + Picker.Validator.SelectedPath = path; else - Picker.SelectedAsset = Values[0] as Asset; + Picker.Validator.SelectedAsset = Values[0] as Asset; _isRefreshing = false; } } diff --git a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs index 9607680f2..c215a5ab7 100644 --- a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs @@ -72,14 +72,14 @@ namespace FlaxEditor.CustomEditors.Editors return; _isRefreshing = true; var slots = _modelInstance.MaterialSlots; - var material = _materialEditor.Picker.SelectedAsset as MaterialBase; + var material = _materialEditor.Picker.Validator.SelectedAsset as MaterialBase; var defaultMaterial = GPUDevice.Instance.DefaultMaterial; var value = (ModelInstanceEntry)Values[0]; var prevMaterial = value.Material; if (!material) { // Fallback to default material - _materialEditor.Picker.SelectedAsset = defaultMaterial; + _materialEditor.Picker.Validator.SelectedAsset = defaultMaterial; value.Material = defaultMaterial; } else if (material == slots[_entryIndex].Material) diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 8d6b0f9e2..1476832c4 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -5,6 +5,7 @@ using System.IO; using FlaxEditor.Content; using FlaxEditor.GUI.Drag; using FlaxEditor.Scripting; +using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -17,189 +18,21 @@ namespace FlaxEditor.GUI /// /// [HideInEditor] - public class AssetPicker : Control, IContentItemOwner + public class AssetPicker : Control { private const float DefaultIconSize = 64; private const float ButtonsOffset = 2; private const float ButtonsSize = 12; - private Asset _selected; - private ContentItem _selectedItem; - private ScriptType _type; - private string _fileExtension; - private bool _isMouseDown; private Float2 _mouseDownPos; private Float2 _mousePos; private DragItems _dragOverElement; /// - /// Gets or sets the selected item. + /// The asset validator. Used to ensure only appropriate items can be picked. /// - public ContentItem SelectedItem - { - get => _selectedItem; - set - { - if (_selectedItem == value) - return; - if (value == null) - { - if (_selected == null && _selectedItem is SceneItem) - { - // Deselect scene reference - _selectedItem.RemoveReference(this); - _selectedItem = null; - _selected = null; - OnSelectedItemChanged(); - return; - } - - // Deselect - _selectedItem?.RemoveReference(this); - _selectedItem = null; - _selected = null; - OnSelectedItemChanged(); - } - else if (value is SceneItem item) - { - if (_selectedItem == item) - return; - if (!IsValid(item)) - item = null; - - // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue) - _selectedItem?.RemoveReference(this); - _selectedItem = item; - _selected = null; - _selectedItem?.AddReference(this); - OnSelectedItemChanged(); - } - else if (value is AssetItem assetItem) - { - SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID); - } - else - { - // Change value - _selectedItem?.RemoveReference(this); - _selectedItem = value; - _selected = null; - OnSelectedItemChanged(); - } - } - } - - /// - /// Gets or sets the selected asset identifier. - /// - public Guid SelectedID - { - get - { - if (_selected != null) - return _selected.ID; - if (_selectedItem is AssetItem assetItem) - return assetItem.ID; - return Guid.Empty; - } - set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value); - } - - /// - /// Gets or sets the selected content item path. - /// - public string SelectedPath - { - get - { - string path = _selectedItem?.Path ?? _selected?.Path; - if (path != null) - { - // Convert into path relative to the project (cross-platform) - var projectFolder = Globals.ProjectFolder; - if (path.StartsWith(projectFolder)) - path = path.Substring(projectFolder.Length + 1); - } - return path; - } - set - { - if (string.IsNullOrEmpty(value)) - { - SelectedItem = null; - } - else - { - var path = StringUtils.IsRelative(value) ? Path.Combine(Globals.ProjectFolder, value) : value; - SelectedItem = Editor.Instance.ContentDatabase.Find(path); - } - } - } - - /// - /// Gets or sets the selected asset object. - /// - public Asset SelectedAsset - { - get => _selected; - set - { - // Check if value won't change - if (value == _selected) - return; - - // Find item from content database and check it - var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null; - if (item != null && !IsValid(item)) - item = null; - - // Change value - _selectedItem?.RemoveReference(this); - _selectedItem = item; - _selected = value; - _selectedItem?.AddReference(this); - OnSelectedItemChanged(); - } - } - - /// - /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker. - /// - public ScriptType AssetType - { - get => _type; - set - { - if (_type != value) - { - _type = value; - - // Auto deselect if the current value is invalid - if (_selectedItem != null && !IsValid(_selectedItem)) - SelectedItem = null; - } - } - } - - /// - /// Gets or sets the content items extensions filter. Null if unused. - /// - public string FileExtension - { - get => _fileExtension; - set - { - if (_fileExtension != value) - { - _fileExtension = value; - - // Auto deselect if the current value is invalid - if (_selectedItem != null && !IsValid(_selectedItem)) - SelectedItem = null; - } - } - } + public AssetPickerValidator Validator { get; } /// /// Occurs when selected item gets changed. @@ -216,38 +49,6 @@ namespace FlaxEditor.GUI /// public bool CanEdit = true; - private bool IsValid(ContentItem item) - { - if (_fileExtension != null && !item.Path.EndsWith(_fileExtension)) - return false; - if (CheckValid != null && !CheckValid(item)) - return false; - if (_type == ScriptType.Null) - return true; - - if (item is AssetItem assetItem) - { - // Faster path for binary items (in-built) - if (assetItem is BinaryAssetItem binaryItem) - return _type.IsAssignableFrom(new ScriptType(binaryItem.Type)); - - // Type filter - var type = TypeUtils.GetType(assetItem.TypeName); - if (_type.IsAssignableFrom(type)) - return true; - - // Json assets can contain any type of the object defined by the C# type (data oriented design) - if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset))) - return true; - - // Special case for scene asset references - if (_type.Type == typeof(SceneReference) && assetItem is SceneItem) - return true; - } - - return false; - } - /// /// Initializes a new instance of the class. /// @@ -264,7 +65,8 @@ namespace FlaxEditor.GUI public AssetPicker(ScriptType assetType, Float2 location) : base(location, new Float2(DefaultIconSize + ButtonsOffset + ButtonsSize, DefaultIconSize)) { - _type = assetType; + Validator = new AssetPickerValidator(assetType); + Validator.SelectedItemChanged += OnSelectedItemChanged; _mousePos = Float2.Minimum; } @@ -275,10 +77,10 @@ namespace FlaxEditor.GUI { // Update tooltip string tooltip; - if (_selectedItem is AssetItem assetItem) + if (Validator.SelectedItem is AssetItem assetItem) tooltip = assetItem.NamePath; else - tooltip = SelectedPath; + tooltip = Validator.SelectedPath; TooltipText = tooltip; SelectedItemChanged?.Invoke(); @@ -289,37 +91,13 @@ namespace FlaxEditor.GUI // Do the drag drop operation if has selected element if (new Rectangle(Float2.Zero, Size).Contains(ref _mouseDownPos)) { - if (_selected != null) - DoDragDrop(DragAssets.GetDragData(_selected)); - else if (_selectedItem != null) - DoDragDrop(DragItems.GetDragData(_selectedItem)); + if (Validator.SelectedAsset != null) + DoDragDrop(DragAssets.GetDragData(Validator.SelectedAsset)); + else if (Validator.SelectedItem != null) + DoDragDrop(DragItems.GetDragData(Validator.SelectedItem)); } } - /// - public void OnItemDeleted(ContentItem item) - { - // Deselect item - SelectedItem = null; - } - - /// - public void OnItemRenamed(ContentItem item) - { - } - - /// - public void OnItemReimported(ContentItem item) - { - } - - /// - public void OnItemDispose(ContentItem item) - { - // Deselect item - SelectedItem = null; - } - private Rectangle IconRect => new Rectangle(0, 0, Height, Height); private Rectangle Button1Rect => new Rectangle(Height + ButtonsOffset, 0, ButtonsSize, ButtonsSize); @@ -341,10 +119,10 @@ namespace FlaxEditor.GUI if (CanEdit) Render2D.DrawSprite(style.ArrowDown, button1Rect, button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); - if (_selectedItem != null) + if (Validator.SelectedItem != null) { // Draw item preview - _selectedItem.DrawThumbnail(ref iconRect); + Validator.SelectedItem.DrawThumbnail(ref iconRect); // Draw buttons if (CanEdit) @@ -363,7 +141,7 @@ namespace FlaxEditor.GUI { Render2D.DrawText( style.FontSmall, - _selectedItem.ShortName, + Validator.SelectedItem.ShortName, new Rectangle(button1Rect.Right + 2, 0, sizeForTextLeft, ButtonsSize), style.Foreground, TextAlignment.Near, @@ -371,7 +149,7 @@ namespace FlaxEditor.GUI } } // Check if has no item but has an asset (eg. virtual asset) - else if (_selected) + else if (Validator.SelectedAsset) { // Draw remove button Render2D.DrawSprite(style.Cross, button3Rect, button3Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); @@ -380,8 +158,8 @@ namespace FlaxEditor.GUI float sizeForTextLeft = Width - button1Rect.Right; if (sizeForTextLeft > 30) { - var name = _selected.GetType().Name; - if (_selected.IsVirtual) + var name = Validator.SelectedAsset.GetType().Name; + if (Validator.SelectedAsset.IsVirtual) name += " (virtual)"; Render2D.DrawText( style.FontSmall, @@ -407,9 +185,7 @@ namespace FlaxEditor.GUI /// public override void OnDestroy() { - _selectedItem?.RemoveReference(this); - _selectedItem = null; - _selected = null; + Validator.OnDestroy(); base.OnDestroy(); } @@ -463,57 +239,57 @@ namespace FlaxEditor.GUI // Buttons logic if (!CanEdit) { - if (Button1Rect.Contains(location) && _selectedItem != null) + if (Button1Rect.Contains(location) && Validator.SelectedItem != null) { // Select asset - Editor.Instance.Windows.ContentWin.Select(_selectedItem); + Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); } } else if (Button1Rect.Contains(location)) { Focus(); - if (_type != ScriptType.Null) + if (Validator.AssetType != ScriptType.Null) { // Show asset picker popup - var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item => + var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item => { - SelectedItem = item; + Validator.SelectedItem = item; RootWindow.Focus(); Focus(); }); - if (_selected != null) + if (Validator.SelectedAsset != null) { - var selectedAssetName = Path.GetFileNameWithoutExtension(_selected.Path); + var selectedAssetName = Path.GetFileNameWithoutExtension(Validator.SelectedAsset.Path); popup.ScrollToAndHighlightItemByName(selectedAssetName); } } else { // Show content item picker popup - var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item => + var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item => { - SelectedItem = item; + Validator.SelectedItem = item; RootWindow.Focus(); Focus(); }); - if (_selectedItem != null) + if (Validator.SelectedItem != null) { - popup.ScrollToAndHighlightItemByName(_selectedItem.ShortName); + popup.ScrollToAndHighlightItemByName(Validator.SelectedItem.ShortName); } } } - else if (_selected != null || _selectedItem != null) + else if (Validator.SelectedAsset != null || Validator.SelectedItem != null) { - if (Button2Rect.Contains(location) && _selectedItem != null) + if (Button2Rect.Contains(location) && Validator.SelectedItem != null) { // Select asset - Editor.Instance.Windows.ContentWin.Select(_selectedItem); + Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); } else if (Button3Rect.Contains(location)) { // Deselect asset Focus(); - SelectedItem = null; + Validator.SelectedItem = null; } } } @@ -540,10 +316,10 @@ namespace FlaxEditor.GUI { Focus(); - if (_selectedItem != null && IconRect.Contains(location)) + if (Validator.SelectedItem != null && IconRect.Contains(location)) { // Open it - Editor.Instance.ContentEditing.Open(_selectedItem); + Editor.Instance.ContentEditing.Open(Validator.SelectedItem); } // Handled @@ -557,7 +333,7 @@ namespace FlaxEditor.GUI // Check if drop asset if (_dragOverElement == null) - _dragOverElement = new DragItems(IsValid); + _dragOverElement = new DragItems(Validator.IsValid); if (CanEdit && _dragOverElement.OnDragEnter(data)) { } @@ -590,7 +366,7 @@ namespace FlaxEditor.GUI if (CanEdit && _dragOverElement.HasValidDrag) { // Select element - SelectedItem = _dragOverElement.Objects[0]; + Validator.SelectedItem = _dragOverElement.Objects[0]; } // Clear cache diff --git a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs index b7c87cb01..3bbca15ef 100644 --- a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs @@ -39,7 +39,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks if (AssetID == value?.ID) return; AssetID = value?.ID ?? Guid.Empty; - _picker.SelectedAsset = value; + _picker.Validator.SelectedAsset = value; OnAssetChanged(); Timeline?.MarkAsEdited(); } @@ -63,10 +63,10 @@ namespace FlaxEditor.GUI.Timeline.Tracks private void OnPickerSelectedItemChanged() { - if (Asset == (TAsset)_picker.SelectedAsset) + if (Asset == (TAsset)_picker.Validator.SelectedAsset) return; using (new TrackUndoBlock(this)) - Asset = (TAsset)_picker.SelectedAsset; + Asset = (TAsset)_picker.Validator.SelectedAsset; } /// diff --git a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs index 9773e9695..7c3aa4f13 100644 --- a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs +++ b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs @@ -465,7 +465,7 @@ namespace FlaxEditor.Surface.Archetypes if (selectedIndex != -1) { var index = 5 + selectedIndex * 2; - SetValue(index, _animationPicker.SelectedID); + SetValue(index, _animationPicker.Validator.SelectedID); } } @@ -495,7 +495,7 @@ namespace FlaxEditor.Surface.Archetypes { if (isValid) { - _animationPicker.SelectedID = data1; + _animationPicker.Validator.SelectedID = data1; _animationSpeed.Value = data0.W; var path = string.Empty; @@ -505,7 +505,7 @@ namespace FlaxEditor.Surface.Archetypes } else { - _animationPicker.SelectedID = Guid.Empty; + _animationPicker.Validator.SelectedID = Guid.Empty; _animationSpeed.Value = 1.0f; } _animationPicker.Enabled = isValid; diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index 7d12a0625..b7df90728 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -95,7 +95,7 @@ namespace FlaxEditor.Surface.Archetypes private void OnAssetPickerSelectedItemChanged() { - SetValue(0, _assetPicker.SelectedID); + SetValue(0, _assetPicker.Validator.SelectedID); } private void TryRestoreConnections(Box box, Box[] prevBoxes, ref NodeElementArchetype arch) @@ -133,7 +133,7 @@ namespace FlaxEditor.Surface.Archetypes var prevOutputs = _outputs; // Extract function signature parameters (inputs and outputs packed) - _asset = LoadSignature(_assetPicker.SelectedID, out var typeNames, out var names); + _asset = LoadSignature(_assetPicker.Validator.SelectedID, out var typeNames, out var names); if (typeNames != null && names != null) { var types = new Type[typeNames.Length]; @@ -174,7 +174,7 @@ namespace FlaxEditor.Surface.Archetypes _outputs[i] = box; } - Title = _assetPicker.SelectedItem.ShortName; + Title = _assetPicker.Validator.SelectedItem.ShortName; } else { diff --git a/Source/Editor/Surface/Elements/AssetSelect.cs b/Source/Editor/Surface/Elements/AssetSelect.cs index e38989e08..5984ce9aa 100644 --- a/Source/Editor/Surface/Elements/AssetSelect.cs +++ b/Source/Editor/Surface/Elements/AssetSelect.cs @@ -38,13 +38,13 @@ namespace FlaxEditor.Surface.Elements private void OnNodeValuesChanged() { - SelectedID = (Guid)ParentNode.Values[Archetype.ValueIndex]; + Validator.SelectedID = (Guid)ParentNode.Values[Archetype.ValueIndex]; } /// protected override void OnSelectedItemChanged() { - var selectedId = SelectedID; + var selectedId = Validator.SelectedID; if (ParentNode != null && (Guid)ParentNode.Values[Archetype.ValueIndex] != selectedId) { ParentNode.SetValue(Archetype.ValueIndex, selectedId); diff --git a/Source/Editor/Tools/Terrain/EditTab.cs b/Source/Editor/Tools/Terrain/EditTab.cs index 008be64f4..d52c1ae1d 100644 --- a/Source/Editor/Tools/Terrain/EditTab.cs +++ b/Source/Editor/Tools/Terrain/EditTab.cs @@ -290,7 +290,7 @@ namespace FlaxEditor.Tools.Terrain var patchCoord = Gizmo.SelectedPatchCoord; var chunkCoord = Gizmo.SelectedChunkCoord; - var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.SelectedAsset as MaterialBase); + var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.Validator.SelectedAsset as MaterialBase); action.Do(); CarveTab.Editor.Undo.AddAction(action); } @@ -336,12 +336,12 @@ namespace FlaxEditor.Tools.Terrain _isUpdatingUI = true; if (terrain.HasPatch(ref patchCoord)) { - _chunkOverrideMaterial.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord); + _chunkOverrideMaterial.Validator.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord); _chunkOverrideMaterial.Enabled = true; } else { - _chunkOverrideMaterial.SelectedAsset = null; + _chunkOverrideMaterial.Validator.SelectedAsset = null; _chunkOverrideMaterial.Enabled = false; } _isUpdatingUI = false; diff --git a/Source/Editor/Utilities/AssetPickerValidator.cs b/Source/Editor/Utilities/AssetPickerValidator.cs new file mode 100644 index 000000000..356a33534 --- /dev/null +++ b/Source/Editor/Utilities/AssetPickerValidator.cs @@ -0,0 +1,293 @@ +using System; +using System.IO; +using FlaxEditor.Content; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.Utilities; + +namespace FlaxEditor.Utilities; + +/// +/// Manages and converts the selected content item to the appropriate types. Useful for drag operations. +/// +public class AssetPickerValidator: IContentItemOwner +{ + private Asset _selected; + private ContentItem _selectedItem; + private ScriptType _type; + private string _fileExtension; + + /// + /// Gets or sets the selected item. + /// + public ContentItem SelectedItem + { + get => _selectedItem; + set + { + if (_selectedItem == value) + return; + if (value == null) + { + if (_selected == null && _selectedItem is SceneItem) + { + // Deselect scene reference + _selectedItem.RemoveReference(this); + _selectedItem = null; + _selected = null; + OnSelectedItemChanged(); + return; + } + + // Deselect + _selectedItem?.RemoveReference(this); + _selectedItem = null; + _selected = null; + OnSelectedItemChanged(); + } + else if (value is SceneItem item) + { + if (_selectedItem == item) + return; + if (!IsValid(item)) + item = null; + + // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue) + _selectedItem?.RemoveReference(this); + _selectedItem = item; + _selected = null; + _selectedItem?.AddReference(this); + OnSelectedItemChanged(); + } + else if (value is AssetItem assetItem) + { + SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID); + } + else + { + // Change value + _selectedItem?.RemoveReference(this); + _selectedItem = value; + _selected = null; + OnSelectedItemChanged(); + } + } + } + + /// + /// Gets or sets the selected asset identifier. + /// + public Guid SelectedID + { + get + { + if (_selected != null) + return _selected.ID; + if (_selectedItem is AssetItem assetItem) + return assetItem.ID; + return Guid.Empty; + } + set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value); + } + + /// + /// Gets or sets the selected content item path. + /// + public string SelectedPath + { + get + { + string path = _selectedItem?.Path ?? _selected?.Path; + if (path != null) + { + // Convert into path relative to the project (cross-platform) + var projectFolder = Globals.ProjectFolder; + if (path.StartsWith(projectFolder)) + path = path.Substring(projectFolder.Length + 1); + } + return path; + } + set + { + if (string.IsNullOrEmpty(value)) + { + SelectedItem = null; + } + else + { + var path = StringUtils.IsRelative(value) ? Path.Combine(Globals.ProjectFolder, value) : value; + SelectedItem = Editor.Instance.ContentDatabase.Find(path); + } + } + } + + /// + /// Gets or sets the selected asset object. + /// + public Asset SelectedAsset + { + get => _selected; + set + { + // Check if value won't change + if (value == _selected) + return; + + // Find item from content database and check it + var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null; + if (item != null && !IsValid(item)) + item = null; + + // Change value + _selectedItem?.RemoveReference(this); + _selectedItem = item; + _selected = value; + _selectedItem?.AddReference(this); + OnSelectedItemChanged(); + } + } + + /// + /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker. + /// + public ScriptType AssetType + { + get => _type; + set + { + if (_type != value) + { + _type = value; + + // Auto deselect if the current value is invalid + if (_selectedItem != null && !IsValid(_selectedItem)) + SelectedItem = null; + } + } + } + + /// + /// Gets or sets the content items extensions filter. Null if unused. + /// + public string FileExtension + { + get => _fileExtension; + set + { + if (_fileExtension != value) + { + _fileExtension = value; + + // Auto deselect if the current value is invalid + if (_selectedItem != null && !IsValid(_selectedItem)) + SelectedItem = null; + } + } + } + + /// + /// Occurs when selected item gets changed. + /// + public event Action SelectedItemChanged; + + /// + /// The custom callback for assets validation. Cane be used to implement a rule for assets to pick. + /// + public Func CheckValid; + + /// + /// Returns whether item is valid. + /// + /// + /// + public bool IsValid(ContentItem item) + { + if (_fileExtension != null && !item.Path.EndsWith(_fileExtension)) + return false; + if (CheckValid != null && !CheckValid(item)) + return false; + if (_type == ScriptType.Null) + return true; + + if (item is AssetItem assetItem) + { + // Faster path for binary items (in-built) + if (assetItem is BinaryAssetItem binaryItem) + return _type.IsAssignableFrom(new ScriptType(binaryItem.Type)); + + // Type filter + var type = TypeUtils.GetType(assetItem.TypeName); + if (_type.IsAssignableFrom(type)) + return true; + + // Json assets can contain any type of the object defined by the C# type (data oriented design) + if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset))) + return true; + + // Special case for scene asset references + if (_type.Type == typeof(SceneReference) && assetItem is SceneItem) + return true; + } + + return false; + } + + /// + /// Initializes a new instance of the class. + /// + public AssetPickerValidator() + : this(new ScriptType(typeof(Asset))) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The assets types that this picker accepts. + public AssetPickerValidator(ScriptType assetType) + { + _type = assetType; + } + + /// + /// Called when selected item gets changed. + /// + protected virtual void OnSelectedItemChanged() + { + SelectedItemChanged?.Invoke(); + } + + /// + public void OnItemDeleted(ContentItem item) + { + // Deselect item + SelectedItem = null; + } + + /// + public void OnItemRenamed(ContentItem item) + { + } + + /// + public void OnItemReimported(ContentItem item) + { + } + + /// + public void OnItemDispose(ContentItem item) + { + // Deselect item + SelectedItem = null; + } + + /// + /// Call to remove reference from the selected item. + /// + public void OnDestroy() + { + _selectedItem?.RemoveReference(this); + _selectedItem = null; + _selected = null; + } +} diff --git a/Source/Editor/Windows/AssetReferencesGraphWindow.cs b/Source/Editor/Windows/AssetReferencesGraphWindow.cs index d49896e2e..b9e0e7257 100644 --- a/Source/Editor/Windows/AssetReferencesGraphWindow.cs +++ b/Source/Editor/Windows/AssetReferencesGraphWindow.cs @@ -46,14 +46,14 @@ namespace FlaxEditor.Windows if (asset != null) { var path = asset.Path; - picker.SelectedAsset = asset; + picker.Validator.SelectedAsset = asset; Title = System.IO.Path.GetFileNameWithoutExtension(path); TooltipText = asset.TypeName + '\n' + path; } else { - picker.SelectedID = AssetId; - var assetItem = picker.SelectedItem as AssetItem; + picker.Validator.SelectedID = AssetId; + var assetItem = picker.Validator.SelectedItem as AssetItem; if (assetItem != null) { Title = assetItem.ShortName; diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs index d8790172b..ccfd5233c 100644 --- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs +++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs @@ -837,7 +837,7 @@ namespace FlaxEditor.Windows.Assets sourceAssetPicker.CheckValid = CheckSourceAssetValid; sourceAssetPicker.SelectedItemChanged += () => { - proxy.Setups.Add(sourceAssetPicker.SelectedAsset, new SetupProxy()); + proxy.Setups.Add(sourceAssetPicker.Validator.SelectedAsset, new SetupProxy()); proxy.Window.MarkAsEdited(); RebuildLayout(); }; @@ -856,7 +856,7 @@ namespace FlaxEditor.Windows.Assets // Source asset picker var sourceAssetPicker = setupGroup.AddPropertyItem("Source Asset").Custom().CustomControl; - sourceAssetPicker.SelectedAsset = sourceAsset; + sourceAssetPicker.Validator.SelectedAsset = sourceAsset; sourceAssetPicker.CanEdit = false; sourceAssetPicker.Height = 48; @@ -916,12 +916,12 @@ namespace FlaxEditor.Windows.Assets { // Show skeleton asset picker var sourceSkeletonPicker = setupGroup.AddPropertyItem("Skeleton", "Skinned model that contains a skeleton for this animation retargeting.").Custom().CustomControl; - sourceSkeletonPicker.AssetType = new ScriptType(typeof(SkinnedModel)); - sourceSkeletonPicker.SelectedAsset = setup.Value.Skeleton; + sourceSkeletonPicker.Validator.AssetType = new ScriptType(typeof(SkinnedModel)); + sourceSkeletonPicker.Validator.SelectedAsset = setup.Value.Skeleton; sourceSkeletonPicker.Height = 48; sourceSkeletonPicker.SelectedItemChanged += () => { - setup.Value.Skeleton = (SkinnedModel)sourceSkeletonPicker.SelectedAsset; + setup.Value.Skeleton = (SkinnedModel)sourceSkeletonPicker.Validator.SelectedAsset; proxy.Window.MarkAsEdited(); }; } From b0fe99f1ec42cca00c9a54bdc6e96cbce84ce357 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 31 Oct 2023 16:11:09 +0100 Subject: [PATCH 174/546] Disable mono thread attach to fix current freeze on GC STW event --- Source/Engine/Scripting/Runtime/DotNet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 91b1d3dde..47f9b0a7c 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -524,7 +524,7 @@ void MCore::GC::FreeMemory(void* ptr, bool coTaskMem) void MCore::Thread::Attach() { // TODO: find a way to properly register native thread so Mono Stop The World (stw) won't freeze when native threads (eg. Job System) are running native code only -#if DOTNET_HOST_MONO && !USE_MONO_AOT +#if DOTNET_HOST_MONO && 0 if (!IsInMainThread() && !mono_domain_get()) { mono_thread_attach(MonoDomainHandle); From 6cfc8c1b1aec2253dd72e3913e6fbb07168b5ff9 Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Tue, 31 Oct 2023 14:52:09 -0400 Subject: [PATCH 175/546] Add extended buttons support for linux --- Source/Engine/Platform/Linux/LinuxWindow.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Source/Engine/Platform/Linux/LinuxWindow.cpp b/Source/Engine/Platform/Linux/LinuxWindow.cpp index a232dea55..f308a0143 100644 --- a/Source/Engine/Platform/Linux/LinuxWindow.cpp +++ b/Source/Engine/Platform/Linux/LinuxWindow.cpp @@ -594,6 +594,12 @@ void LinuxWindow::OnButtonPress(void* event) case Button3: mouseButton = MouseButton::Right; break; + case 8: + mouseButton = MouseButton::Extended2; + break; + case 9: + mouseButton = MouseButton::Extended1; + break; default: return; } @@ -641,6 +647,12 @@ void LinuxWindow::OnButtonRelease(void* event) case Button5: Input::Mouse->OnMouseWheel(ClientToScreen(mousePos), -1.0f, this); break; + case 8: + Input::Mouse->OnMouseUp(ClientToScreen(mousePos), MouseButton::Extended2, this); + break; + case 9: + Input::Mouse->OnMouseUp(ClientToScreen(mousePos), MouseButton::Extended1, this); + break; default: return; } From f6e9d0431ba90c605f197d73b20f33ba358d86a5 Mon Sep 17 00:00:00 2001 From: MineBill Date: Tue, 31 Oct 2023 17:56:15 +0200 Subject: [PATCH 176/546] Implement drag and drop for list collection. --- .../CustomEditors/Editors/CollectionEditor.cs | 323 ++++++++++++++++-- 1 file changed, 295 insertions(+), 28 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 8922e2d25..aa418404d 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -2,11 +2,17 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; +using FlaxEditor.Content; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Drag; +using FlaxEditor.Options; +using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; +using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -134,15 +140,44 @@ namespace FlaxEditor.CustomEditors.Editors overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type; spacing = collection.Spacing; } + + var dragArea = layout.CustomContainer(); + dragArea.CustomControl.Editor = this; + dragArea.CustomControl.ElementType = ElementType; + + // Check for the AssetReferenceAttribute. In JSON assets, it can be used to filter + // which scripts can be dragged over and dropped on this collection editor. + var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute); + if (assetReference != null) + { + if (string.IsNullOrEmpty(assetReference.TypeName)) + { + } + else if (assetReference.TypeName.Length > 1 && assetReference.TypeName[0] == '.') + { + dragArea.CustomControl.ElementType = ScriptType.Null; + dragArea.CustomControl.FileExtension = assetReference.TypeName; + } + else + { + var customType = TypeUtils.GetType(assetReference.TypeName); + if (customType != ScriptType.Null) + dragArea.CustomControl.ElementType = customType; + else if (!Content.Settings.GameSettings.OptionalPlatformSettings.Contains(assetReference.TypeName)) + Debug.LogWarning(string.Format("Unknown asset type '{0}' to use for drag and drop filter.", assetReference.TypeName)); + else + dragArea.CustomControl.ElementType = ScriptType.Void; + } + } // Size if (_readOnly || (NotNullItems && size == 0)) { - layout.Label("Size", size.ToString()); + dragArea.Label("Size", size.ToString()); } else { - _size = layout.IntegerValue("Size"); + _size = dragArea.IntegerValue("Size"); _size.IntValue.MinValue = 0; _size.IntValue.MaxValue = ushort.MaxValue; _size.IntValue.Value = size; @@ -152,7 +187,7 @@ namespace FlaxEditor.CustomEditors.Editors // Elements if (size > 0) { - var panel = layout.VerticalPanel(); + var panel = dragArea.VerticalPanel(); panel.Panel.BackgroundColor = _background; var elementType = ElementType; @@ -212,37 +247,33 @@ namespace FlaxEditor.CustomEditors.Editors // Add/Remove buttons if (!_readOnly) { - var area = layout.Space(20); - var addButton = new Button(area.ContainerControl.Width - (16 + 16 + 2 + 2), 2, 16, 16) - { - Text = "+", - TooltipText = "Add new item", - AnchorPreset = AnchorPresets.TopRight, - Parent = area.ContainerControl, - Enabled = !NotNullItems || size > 0, - }; - addButton.Clicked += () => - { - if (IsSetBlocked) - return; - - Resize(Count + 1); - }; - var removeButton = new Button(addButton.Right + 2, addButton.Y, 16, 16) - { - Text = "-", - TooltipText = "Remove last item", - AnchorPreset = AnchorPresets.TopRight, - Parent = area.ContainerControl, - Enabled = size > 0, - }; - removeButton.Clicked += () => + var panel = dragArea.HorizontalPanel(); + panel.Panel.Size = new Float2(0, 20); + panel.Panel.Margin = new Margin(2); + + var removeButton = panel.Button("-", "Remove last item"); + removeButton.Button.Size = new Float2(16, 16); + removeButton.Button.Enabled = size > 0; + removeButton.Button.AnchorPreset = AnchorPresets.TopRight; + removeButton.Button.Clicked += () => { if (IsSetBlocked) return; Resize(Count - 1); }; + + var addButton = panel.Button("+", "Add new item"); + addButton.Button.Size = new Float2(16, 16); + addButton.Button.Enabled = !NotNullItems || size > 0; + addButton.Button.AnchorPreset = AnchorPresets.TopRight; + addButton.Button.Clicked += () => + { + if (IsSetBlocked) + return; + + Resize(Count + 1); + }; } } @@ -369,5 +400,241 @@ namespace FlaxEditor.CustomEditors.Editors } return base.OnDirty(editor, value, token); } + + private class DragAreaControl : VerticalPanel + { + // private DragHandlers _dragHandlers; + // private DragAssets _dragAssets; + // private DragActors _dragActors; + private DragItems _dragItems; + private DragActors _dragActors; + private DragHandlers _dragHandlers; + + private AssetPickerValidator _pickerValidator; + private ScriptType _elementType; + + public ScriptType ElementType + { + get => _elementType; + set + { + _pickerValidator = new AssetPickerValidator(value); + _elementType = value; + } + } + + public CollectionEditor Editor { get; set; } + + public string FileExtension + { + set => _pickerValidator.FileExtension = value; + } + + /// + public override void Draw() + { + if (_dragHandlers is {HasValidDrag: true}) + { + var area = new Rectangle(Float2.Zero, Size); + Render2D.FillRectangle(area, Color.Orange * 0.5f); + Render2D.DrawRectangle(area, Color.Black); + } + base.Draw(); + } + + public override void OnDestroy() + { + _pickerValidator.OnDestroy(); + } + + private bool ValidateActors(ActorNode node) + { + return node.Actor.GetScript(ElementType.Type) || ElementType.Type.IsAssignableTo(typeof(Actor)); + } + + /// + public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) + { + var result = base.OnDragEnter(ref location, data); + if (result != DragDropEffect.None) + return result; + + if (_dragHandlers == null) + { + _dragItems = new DragItems(_pickerValidator.IsValid); + _dragActors = new DragActors(ValidateActors); + _dragHandlers = new DragHandlers + { + _dragActors, + _dragItems + }; + } + return _dragHandlers.OnDragEnter(data); + } + + /// + public override DragDropEffect OnDragMove(ref Float2 location, DragData data) + { + var result = base.OnDragMove(ref location, data); + if (result != DragDropEffect.None) + return result; + + return _dragHandlers.Effect; + } + + /// + public override void OnDragLeave() + { + _dragHandlers.OnDragLeave(); + + base.OnDragLeave(); + } + + /// + public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) + { + var result = base.OnDragDrop(ref location, data); + if (result != DragDropEffect.None) + { + _dragHandlers.OnDragDrop(null); + return result; + } + + if (_dragHandlers.HasValidDrag) + { + if (_dragItems.HasValidDrag) + { + var list = Editor.CloneValues(); + if (list == null) + { + if (Editor.Values.Type.IsArray) + { + list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0); + } + else + { + list = Editor.Values.Type.CreateInstance() as IList; + } + } + if (list.IsFixedSize) + { + var oldSize = list.Count; + var newSize = list.Count + _dragItems.Objects.Count; + var type = Editor.Values.Type.GetElementType(); + var array = TypeUtils.CreateArrayInstance(type, newSize); + list.CopyTo(array, 0); + + for (var i = oldSize; i < newSize; i++) + { + var validator = new AssetPickerValidator + { + FileExtension = _pickerValidator.FileExtension, + AssetType = _pickerValidator.AssetType, + SelectedItem = _dragItems.Objects[i - oldSize], + }; + + if (typeof(AssetItem).IsAssignableFrom(ElementType.Type)) + array.SetValue(validator.SelectedItem, i); + else if (ElementType.Type == typeof(Guid)) + array.SetValue(validator.SelectedID, i); + else if (ElementType.Type == typeof(SceneReference)) + array.SetValue(new SceneReference(validator.SelectedID), i); + else if (ElementType.Type == typeof(string)) + array.SetValue(validator.SelectedPath, i); + else + array.SetValue(validator.SelectedAsset, i); + + validator.OnDestroy(); + } + Editor.SetValue(array); + } + else + { + foreach (var item in _dragItems.Objects) + { + var validator = new AssetPickerValidator + { + FileExtension = _pickerValidator.FileExtension, + AssetType = _pickerValidator.AssetType, + SelectedItem = item, + }; + + if (typeof(AssetItem).IsAssignableFrom(ElementType.Type)) + list.Add(validator.SelectedItem); + else if (ElementType.Type == typeof(Guid)) + list.Add(validator.SelectedID); + else if (ElementType.Type == typeof(SceneReference)) + list.Add(new SceneReference(validator.SelectedID)); + else if (ElementType.Type == typeof(string)) + list.Add(validator.SelectedPath); + else + list.Add(validator.SelectedAsset); + + validator.OnDestroy(); + } + Editor.SetValue(list); + } + } + else if (_dragActors.HasValidDrag) + { + var list = Editor.CloneValues(); + if (list == null) + { + if (Editor.Values.Type.IsArray) + { + list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0); + } + else + { + list = Editor.Values.Type.CreateInstance() as IList; + } + } + + if (list.IsFixedSize) + { + var oldSize = list.Count; + var newSize = list.Count + _dragActors.Objects.Count; + var type = Editor.Values.Type.GetElementType(); + var array = TypeUtils.CreateArrayInstance(type, newSize); + list.CopyTo(array, 0); + + for (var i = oldSize; i < newSize; i++) + { + var actor = _dragActors.Objects[i - oldSize].Actor; + if (ElementType.Type.IsAssignableTo(typeof(Actor))) + { + array.SetValue(actor, i); + } + else + { + array.SetValue(actor.GetScript(ElementType.Type), i); + } + } + Editor.SetValue(array); + } + else + { + foreach (var actorNode in _dragActors.Objects) + { + if (ElementType.Type.IsAssignableTo(typeof(Actor))) + { + list.Add(actorNode.Actor); + } + else + { + list.Add(actorNode.Actor.GetScript(ElementType.Type)); + } + } + Editor.SetValue(list); + } + } + + _dragHandlers.OnDragDrop(null); + } + + return result; + } + } } + } From 1a254afd4f610e80dfc8e21ec158d9a726263e35 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 1 Nov 2023 10:29:44 +0100 Subject: [PATCH 177/546] Fix crash when creating empty cloth --- Source/Engine/Physics/Actors/Cloth.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Physics/Actors/Cloth.cpp b/Source/Engine/Physics/Actors/Cloth.cpp index 7c180d98d..b2db80b0b 100644 --- a/Source/Engine/Physics/Actors/Cloth.cpp +++ b/Source/Engine/Physics/Actors/Cloth.cpp @@ -95,14 +95,17 @@ void Cloth::SetFabric(const FabricSettings& value) void Cloth::Rebuild() { #if WITH_CLOTH - // Remove old - if (IsDuringPlay()) - PhysicsBackend::RemoveCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); - DestroyCloth(); + if (_cloth) + { + // Remove old + if (IsDuringPlay()) + PhysicsBackend::RemoveCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); + DestroyCloth(); + } // Create new CreateCloth(); - if (IsDuringPlay()) + if (IsDuringPlay() && _cloth) PhysicsBackend::AddCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); #endif } From c0a8d29453d1388c5f71c7814e9a3d5af5d742a9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 1 Nov 2023 10:46:47 +0100 Subject: [PATCH 178/546] Improve Cloth usage --- .../Dedicated/MeshReferenceEditor.cs | 17 +++++++++++++++-- Source/Editor/Tools/ClothPainting.cs | 5 +++-- Source/Engine/Physics/Actors/Cloth.h | 4 ++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs index 71c5f6daf..4099e5aee 100644 --- a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs @@ -225,8 +225,15 @@ namespace FlaxEditor.CustomEditors.Dedicated } _actor = actor; - var showActorPicker = actor == null || ParentEditor.Values.All(x => x is not Cloth); - if (showActorPicker) + if (ParentEditor.Values.Any(x => x is Cloth)) + { + // Cloth always picks the parent model mesh + if (actor == null) + { + layout.Label("Cloth needs to be added as a child to model actor."); + } + } + else { // Actor reference picker _actorPicker = layout.Custom(); @@ -242,7 +249,10 @@ namespace FlaxEditor.CustomEditors.Dedicated { var model = staticModel.Model; if (model == null || model.WaitForLoaded()) + { + layout.Label("No model."); return; + } var materials = model.MaterialSlots; var lods = model.LODs; meshNames = new string[lods.Length][]; @@ -267,7 +277,10 @@ namespace FlaxEditor.CustomEditors.Dedicated { var skinnedModel = animatedModel.SkinnedModel; if (skinnedModel == null || skinnedModel.WaitForLoaded()) + { + layout.Label("No model."); return; + } var materials = skinnedModel.MaterialSlots; var lods = skinnedModel.LODs; meshNames = new string[lods.Length][]; diff --git a/Source/Editor/Tools/ClothPainting.cs b/Source/Editor/Tools/ClothPainting.cs index 226c7b49c..f12fca9db 100644 --- a/Source/Editor/Tools/ClothPainting.cs +++ b/Source/Editor/Tools/ClothPainting.cs @@ -225,6 +225,7 @@ namespace FlaxEngine.Tools var cloth = _cloth; if (cloth == null) return; + var hasPaintInput = Owner.IsLeftMouseButtonDown && !Owner.IsAltKeyDown; // Perform detailed tracing to find cursor location for the brush var ray = Owner.MouseRay; @@ -240,7 +241,7 @@ namespace FlaxEngine.Tools // Cursor hit other object or nothing PaintEnd(); - if (Owner.IsLeftMouseButtonDown) + if (hasPaintInput) { // Select something else var view = new Ray(Owner.ViewPosition, Owner.ViewDirection); @@ -253,7 +254,7 @@ namespace FlaxEngine.Tools } // Handle painting - if (Owner.IsLeftMouseButtonDown) + if (hasPaintInput) PaintStart(); else PaintEnd(); diff --git a/Source/Engine/Physics/Actors/Cloth.h b/Source/Engine/Physics/Actors/Cloth.h index 4dc81ce3f..6137ec3b6 100644 --- a/Source/Engine/Physics/Actors/Cloth.h +++ b/Source/Engine/Physics/Actors/Cloth.h @@ -232,13 +232,13 @@ private: public: /// - /// Gets the mesh to use for the cloth simulation (single mesh from specific LOD). + /// Gets the mesh to use for the cloth simulation (single mesh from specific LOD). Always from the parent static or animated model actor. /// API_PROPERTY(Attributes="EditorOrder(0), EditorDisplay(\"Cloth\")") ModelInstanceActor::MeshReference GetMesh() const; /// - /// Sets the mesh to use for the cloth simulation (single mesh from specific LOD). + /// Sets the mesh to use for the cloth simulation (single mesh from specific LOD). Always from the parent static or animated model actor. /// API_PROPERTY() void SetMesh(const ModelInstanceActor::MeshReference& value); From 151c6bc6c323f049fd79debdd8b468f5a991bcff Mon Sep 17 00:00:00 2001 From: MineBill Date: Wed, 1 Nov 2023 20:07:58 +0200 Subject: [PATCH 179/546] Fix color picker dialog not staying inside the current screen. --- Source/Editor/GUI/Dialogs/Dialog.cs | 5 +++++ Source/Editor/Modules/UIModule.cs | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Source/Editor/GUI/Dialogs/Dialog.cs b/Source/Editor/GUI/Dialogs/Dialog.cs index 5054aee98..86e24e3b3 100644 --- a/Source/Editor/GUI/Dialogs/Dialog.cs +++ b/Source/Editor/GUI/Dialogs/Dialog.cs @@ -39,6 +39,11 @@ namespace FlaxEditor.GUI.Dialogs /// public DialogResult Result => _result; + /// + /// Returns the size of the dialog. + /// + public Float2 DialogSize => _dialogSize; + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 1369ecd1e..136b6b93a 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -470,13 +470,13 @@ namespace FlaxEditor.Modules // Place dialog nearby the target control var targetControlDesktopCenter = targetControl.PointToScreen(targetControl.Size * 0.5f); var desktopSize = Platform.GetMonitorBounds(targetControlDesktopCenter); - var pos = targetControlDesktopCenter + new Float2(10.0f, -dialog.Height * 0.5f); - var dialogEnd = pos + dialog.Size; + var pos = targetControlDesktopCenter + new Float2(10.0f, -dialog.DialogSize.Y * 0.5f); + var dialogEnd = pos + dialog.DialogSize; var desktopEnd = desktopSize.BottomRight - new Float2(10.0f); if (dialogEnd.X >= desktopEnd.X || dialogEnd.Y >= desktopEnd.Y) - pos = targetControl.PointToScreen(Float2.Zero) - new Float2(10.0f + dialog.Width, dialog.Height); + pos = targetControl.PointToScreen(Float2.Zero) - new Float2(10.0f + dialog.DialogSize.X, dialog.DialogSize.Y); var desktopBounds = Platform.VirtualDesktopBounds; - pos = Float2.Clamp(pos, desktopBounds.UpperLeft, desktopBounds.BottomRight - dialog.Size); + pos = Float2.Clamp(pos, desktopBounds.UpperLeft, desktopBounds.BottomRight - dialog.DialogSize); dialog.RootWindow.Window.Position = pos; // Register for context menu (prevent auto-closing context menu when selecting color) From 087cfd7a8c5f81c60589be36646cb59802f00fbb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 2 Nov 2023 19:59:01 +0100 Subject: [PATCH 180/546] Refactor `HashSet` and `Dictionary` collections capacity to handle rehashing when too many elements were deleted --- Source/Engine/Core/Collections/Config.h | 23 ++- Source/Engine/Core/Collections/Dictionary.h | 142 ++++++++++------ Source/Engine/Core/Collections/HashSet.h | 171 ++++++++++++++++---- 3 files changed, 246 insertions(+), 90 deletions(-) diff --git a/Source/Engine/Core/Collections/Config.h b/Source/Engine/Core/Collections/Config.h index 792ae57c8..ce7656dcd 100644 --- a/Source/Engine/Core/Collections/Config.h +++ b/Source/Engine/Core/Collections/Config.h @@ -2,13 +2,26 @@ #pragma once -/// -/// Default capacity for the dictionaries (amount of space for the elements) -/// -#define DICTIONARY_DEFAULT_CAPACITY 256 +#include "Engine/Platform/Defines.h" /// -/// Function for dictionary that tells how change hash index during iteration (size param is a buckets table size) +/// Default capacity for the dictionaries (amount of space for the elements). +/// +#ifndef DICTIONARY_DEFAULT_CAPACITY +#if PLATFORM_DESKTOP +#define DICTIONARY_DEFAULT_CAPACITY 256 +#else +#define DICTIONARY_DEFAULT_CAPACITY 64 +#endif +#endif + +/// +/// Default slack space divider for the dictionaries. +/// +#define DICTIONARY_DEFAULT_SLACK_SCALE 3 + +/// +/// Function for dictionary that tells how change hash index during iteration (size param is a buckets table size). /// #define DICTIONARY_PROB_FUNC(size, numChecks) (numChecks) //#define DICTIONARY_PROB_FUNC(size, numChecks) (1) diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h index 575863dc9..729bc80aa 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -40,7 +40,7 @@ public: private: State _state; - void Free() + FORCE_INLINE void Free() { if (_state == Occupied) { @@ -50,7 +50,7 @@ public: _state = Empty; } - void Delete() + FORCE_INLINE void Delete() { _state = Deleted; Memory::DestructItem(&Key); @@ -58,7 +58,7 @@ public: } template - void Occupy(const KeyComparableType& key) + FORCE_INLINE void Occupy(const KeyComparableType& key) { Memory::ConstructItems(&Key, &key, 1); Memory::ConstructItem(&Value); @@ -66,7 +66,7 @@ public: } template - void Occupy(const KeyComparableType& key, const ValueType& value) + FORCE_INLINE void Occupy(const KeyComparableType& key, const ValueType& value) { Memory::ConstructItems(&Key, &key, 1); Memory::ConstructItems(&Value, &value, 1); @@ -74,7 +74,7 @@ public: } template - void Occupy(const KeyComparableType& key, ValueType&& value) + FORCE_INLINE void Occupy(const KeyComparableType& key, ValueType&& value) { Memory::ConstructItems(&Key, &key, 1); Memory::MoveItems(&Value, &value, 1); @@ -132,9 +132,6 @@ public: /// /// The other collection to move. Dictionary(Dictionary&& other) noexcept - : _elementsCount(other._elementsCount) - , _deletedCount(other._deletedCount) - , _size(other._size) { _elementsCount = other._elementsCount; _deletedCount = other._deletedCount; @@ -375,8 +372,12 @@ public: template ValueType& At(const KeyComparableType& key) { + // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) + if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE) + Compact(); + // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); + EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount); // Find location of the item or place to insert it FindPositionResult pos; @@ -388,9 +389,9 @@ public: // Insert ASSERT(pos.FreeSlotIndex != -1); + _elementsCount++; Bucket& bucket = _allocation.Get()[pos.FreeSlotIndex]; bucket.Occupy(key); - _elementsCount++; return bucket.Value; } @@ -493,7 +494,7 @@ public: for (Iterator i = Begin(); i.IsNotEnd(); ++i) { if (i->Value) - Delete(i->Value); + ::Delete(i->Value); } Clear(); } @@ -535,11 +536,19 @@ public: Bucket* oldData = oldAllocation.Get(); if (oldElementsCount != 0 && preserveContents) { - // TODO; move keys and values on realloc + FindPositionResult pos; for (int32 i = 0; i < oldSize; i++) { - if (oldData[i].IsOccupied()) - Add(oldData[i].Key, MoveTemp(oldData[i].Value)); + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Key, pos); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1); + Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1); + bucket->_state = Bucket::Occupied; + _elementsCount++; + } } } if (oldElementsCount != 0) @@ -558,9 +567,9 @@ public: { if (_size >= minCapacity) return; - if (minCapacity < DICTIONARY_DEFAULT_CAPACITY) - minCapacity = DICTIONARY_DEFAULT_CAPACITY; - const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + if (capacity < DICTIONARY_DEFAULT_CAPACITY) + capacity = DICTIONARY_DEFAULT_CAPACITY; SetCapacity(capacity, preserveContents); } @@ -584,24 +593,10 @@ public: /// The value. /// Weak reference to the stored bucket. template - Bucket* Add(const KeyComparableType& key, const ValueType& value) + FORCE_INLINE Bucket* Add(const KeyComparableType& key, const ValueType& value) { - // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); - - // Find location of the item or place to insert it - FindPositionResult pos; - FindPosition(key, pos); - - // Ensure key is unknown - ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); - - // Insert - ASSERT(pos.FreeSlotIndex != -1); - Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Bucket* bucket = OnAdd(key); bucket->Occupy(key, value); - _elementsCount++; - return bucket; } @@ -612,24 +607,10 @@ public: /// The value. /// Weak reference to the stored bucket. template - Bucket* Add(const KeyComparableType& key, ValueType&& value) + FORCE_INLINE Bucket* Add(const KeyComparableType& key, ValueType&& value) { - // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); - - // Find location of the item or place to insert it - FindPositionResult pos; - FindPosition(key, pos); - - // Ensure key is unknown - ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); - - // Insert - ASSERT(pos.FreeSlotIndex != -1); - Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Bucket* bucket = OnAdd(key); bucket->Occupy(key, MoveTemp(value)); - _elementsCount++; - return bucket; } @@ -851,7 +832,7 @@ public: return Iterator(this, _size); } -protected: +private: /// /// The result container of the dictionary item lookup searching. /// @@ -911,4 +892,65 @@ protected: result.ObjectIndex = -1; result.FreeSlotIndex = insertPos; } + + template + Bucket* OnAdd(const KeyComparableType& key) + { + // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) + if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE) + Compact(); + + // Ensure to have enough memory for the next item (in case of new element insertion) + EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount); + + // Find location of the item or place to insert it + FindPositionResult pos; + FindPosition(key, pos); + + // Ensure key is unknown + ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); + + // Insert + ASSERT(pos.FreeSlotIndex != -1); + _elementsCount++; + return &_allocation.Get()[pos.FreeSlotIndex]; + } + + void Compact() + { + if (_elementsCount == 0) + { + // Fast path if it's empty + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + } + else + { + // Rebuild entire table completely + AllocationData oldAllocation; + oldAllocation.Swap(_allocation); + _allocation.Allocate(_size); + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + Bucket* oldData = oldAllocation.Get(); + FindPositionResult pos; + for (int32 i = 0; i < _size; i++) + { + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Key, pos); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1); + Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1); + bucket->_state = Bucket::Occupied; + } + } + for (int32 i = 0; i < _size; i++) + oldData[i].Free(); + } + _deletedCount = 0; + } }; diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index fba8ca823..166ca5cc3 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -37,26 +37,33 @@ public: private: State _state; - void Free() + FORCE_INLINE void Free() { if (_state == Occupied) Memory::DestructItem(&Item); _state = Empty; } - void Delete() + FORCE_INLINE void Delete() { _state = Deleted; Memory::DestructItem(&Item); } template - void Occupy(const ItemType& item) + FORCE_INLINE void Occupy(const ItemType& item) { Memory::ConstructItems(&Item, &item, 1); _state = Occupied; } + template + FORCE_INLINE void Occupy(ItemType& item) + { + Memory::MoveItems(&Item, &item, 1); + _state = Occupied; + } + FORCE_INLINE bool IsEmpty() const { return _state == Empty; @@ -82,6 +89,7 @@ public: private: int32 _elementsCount = 0; + int32 _deletedCount = 0; int32 _size = 0; AllocationData _allocation; @@ -107,12 +115,12 @@ public: /// /// The other collection to move. HashSet(HashSet&& other) noexcept - : _elementsCount(other._elementsCount) - , _size(other._size) { _elementsCount = other._elementsCount; + _deletedCount = other._deletedCount; _size = other._size; other._elementsCount = 0; + other._deletedCount = 0; other._size = 0; _allocation.Swap(other._allocation); } @@ -150,8 +158,10 @@ public: Clear(); _allocation.Free(); _elementsCount = other._elementsCount; + _deletedCount = other._deletedCount; _size = other._size; other._elementsCount = 0; + other._deletedCount = 0; other._size = 0; _allocation.Swap(other._allocation); } @@ -163,7 +173,7 @@ public: /// ~HashSet() { - SetCapacity(0, false); + Clear(); } public: @@ -210,6 +220,7 @@ public: HashSet* _collection; int32 _index; + public: Iterator(HashSet* collection, const int32 index) : _collection(collection) , _index(index) @@ -222,7 +233,12 @@ public: { } - public: + Iterator() + : _collection(nullptr) + , _index(-1) + { + } + Iterator(const Iterator& i) : _collection(i._collection) , _index(i._index) @@ -236,6 +252,11 @@ public: } public: + FORCE_INLINE int32 Index() const + { + return _index; + } + FORCE_INLINE bool IsEnd() const { return _index == _collection->_size; @@ -331,12 +352,12 @@ public: /// void Clear() { - if (_elementsCount != 0) + if (_elementsCount + _deletedCount != 0) { Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) data[i].Free(); - _elementsCount = 0; + _elementsCount = _deletedCount = 0; } } @@ -371,7 +392,7 @@ public: oldAllocation.Swap(_allocation); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; - _elementsCount = 0; + _deletedCount = _elementsCount = 0; if (capacity != 0 && (capacity & (capacity - 1)) != 0) { // Align capacity value to the next power of two (http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2) @@ -394,11 +415,18 @@ public: Bucket* oldData = oldAllocation.Get(); if (oldElementsCount != 0 && preserveContents) { - // TODO; move keys and values on realloc + FindPositionResult pos; for (int32 i = 0; i < oldSize; i++) { - if (oldData[i].IsOccupied()) - Add(oldData[i].Item); + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Item, pos); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1); + bucket->_state = Bucket::Occupied; + _elementsCount++; + } } } if (oldElementsCount != 0) @@ -415,14 +443,26 @@ public: /// True if preserve collection data when changing its size, otherwise collection after resize will be empty. void EnsureCapacity(int32 minCapacity, bool preserveContents = true) { - if (Capacity() >= minCapacity) + if (_size >= minCapacity) return; - if (minCapacity < DICTIONARY_DEFAULT_CAPACITY) - minCapacity = DICTIONARY_DEFAULT_CAPACITY; - const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + if (capacity < DICTIONARY_DEFAULT_CAPACITY) + capacity = DICTIONARY_DEFAULT_CAPACITY; SetCapacity(capacity, preserveContents); } + /// + /// Swaps the contents of collection with the other object without copy operation. Performs fast internal data exchange. + /// + /// The other collection. + void Swap(HashSet& other) + { + ::Swap(_elementsCount, other._elementsCount); + ::Swap(_deletedCount, other._deletedCount); + ::Swap(_size, other._size); + _allocation.Swap(other._allocation); + } + public: /// /// Add element to the collection. @@ -432,24 +472,23 @@ public: template bool Add(const ItemType& item) { - // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + 1); + Bucket* bucket = OnAdd(item); + if (bucket) + bucket->Occupy(item); + return bucket != nullptr; + } - // Find location of the item or place to insert it - FindPositionResult pos; - FindPosition(item, pos); - - // Check if object has been already added - if (pos.ObjectIndex != -1) - return false; - - // Insert - ASSERT(pos.FreeSlotIndex != -1); - Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; - bucket->Occupy(item); - _elementsCount++; - - return true; + /// + /// Add element to the collection. + /// + /// The element to add to the set. + /// True if element has been added to the collection, otherwise false if the element is already present. + bool Add(T&& item) + { + Bucket* bucket = OnAdd(item); + if (bucket) + bucket->Occupy(MoveTemp(item)); + return bucket != nullptr; } /// @@ -479,6 +518,7 @@ public: { _allocation.Get()[pos.ObjectIndex].Delete(); _elementsCount--; + _deletedCount++; return true; } return false; @@ -497,6 +537,7 @@ public: ASSERT(_allocation.Get()[i._index].IsOccupied()); _allocation.Get()[i._index].Delete(); _elementsCount--; + _deletedCount++; return true; } return false; @@ -585,7 +626,7 @@ public: return Iterator(this, _size); } -protected: +private: /// /// The result container of the set item lookup searching. /// @@ -646,4 +687,64 @@ protected: result.ObjectIndex = -1; result.FreeSlotIndex = insertPos; } + + template + Bucket* OnAdd(const ItemType& key) + { + // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) + if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE) + Compact(); + + // Ensure to have enough memory for the next item (in case of new element insertion) + EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount); + + // Find location of the item or place to insert it + FindPositionResult pos; + FindPosition(key, pos); + + // Ensure key is unknown + ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); + + // Insert + ASSERT(pos.FreeSlotIndex != -1); + _elementsCount++; + return &_allocation.Get()[pos.FreeSlotIndex]; + } + + void Compact() + { + if (_elementsCount == 0) + { + // Fast path if it's empty + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + } + else + { + // Rebuild entire table completely + AllocationData oldAllocation; + oldAllocation.Swap(_allocation); + _allocation.Allocate(_size); + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + Bucket* oldData = oldAllocation.Get(); + FindPositionResult pos; + for (int32 i = 0; i < _size; i++) + { + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Item, pos); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1); + bucket->_state = Bucket::Occupied; + } + } + for (int32 i = 0; i < _size; i++) + oldData[i].Free(); + } + _deletedCount = 0; + } }; From da72dd4806c6a8caba8eb5e292d97cd6ef0e9f0f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 2 Nov 2023 19:59:27 +0100 Subject: [PATCH 181/546] Add unit test for `HashSet` and `Dictionary` collection types --- Source/Engine/Tests/TestCollections.cpp | 170 ++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/Source/Engine/Tests/TestCollections.cpp b/Source/Engine/Tests/TestCollections.cpp index 21aeb1c36..24c971860 100644 --- a/Source/Engine/Tests/TestCollections.cpp +++ b/Source/Engine/Tests/TestCollections.cpp @@ -3,6 +3,8 @@ #include "Engine/Core/RandomStream.h" #include "Engine/Core/Collections/Array.h" #include "Engine/Core/Collections/BitArray.h" +#include "Engine/Core/Collections/HashSet.h" +#include "Engine/Core/Collections/Dictionary.h" #include TEST_CASE("Array") @@ -108,3 +110,171 @@ TEST_CASE("BitArray") CHECK(a1 == testData); } } + +TEST_CASE("HashSet") +{ + SECTION("Test Allocators") + { + HashSet a1; + HashSet> a2; + HashSet> a3; + for (int32 i = 0; i < 7; i++) + { + a1.Add(i); + a2.Add(i); + a3.Add(i); + } + CHECK(a1.Count() == 7); + CHECK(a2.Count() == 7); + CHECK(a3.Count() == 7); + for (int32 i = 0; i < 7; i++) + { + CHECK(a1.Contains(i)); + CHECK(a2.Contains(i)); + CHECK(a3.Contains(i)); + } + } + + SECTION("Test Resizing") + { + HashSet a1; + for (int32 i = 0; i < 4000; i++) + a1.Add(i); + CHECK(a1.Count() == 4000); + int32 capacity = a1.Capacity(); + for (int32 i = 0; i < 4000; i++) + { + CHECK(a1.Contains(i)); + } + a1.Clear(); + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Add(i); + CHECK(a1.Count() == 4000); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Remove(i); + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Add(i); + CHECK(a1.Count() == 4000); + CHECK(a1.Capacity() == capacity); + } + + SECTION("Test Default Capacity") + { + HashSet a1; + a1.Add(1); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + } + + SECTION("Test Add/Remove") + { + HashSet a1; + for (int32 i = 0; i < 4000; i++) + { + a1.Add(i); + a1.Remove(i); + } + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + a1.Clear(); + for (int32 i = 1; i <= 10; i++) + a1.Add(-i); + for (int32 i = 0; i < 4000; i++) + { + a1.Add(i); + a1.Remove(i); + } + CHECK(a1.Count() == 10); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + } +} + +TEST_CASE("Dictionary") +{ + SECTION("Test Allocators") + { + Dictionary a1; + Dictionary> a2; + Dictionary> a3; + for (int32 i = 0; i < 7; i++) + { + a1.Add(i, i); + a2.Add(i, i); + a3.Add(i, i); + } + CHECK(a1.Count() == 7); + CHECK(a2.Count() == 7); + CHECK(a3.Count() == 7); + for (int32 i = 0; i < 7; i++) + { + CHECK(a1.ContainsKey(i)); + CHECK(a2.ContainsKey(i)); + CHECK(a3.ContainsKey(i)); + CHECK(a1.ContainsValue(i)); + CHECK(a2.ContainsValue(i)); + CHECK(a3.ContainsValue(i)); + } + } + + SECTION("Test Resizing") + { + Dictionary a1; + for (int32 i = 0; i < 4000; i++) + a1.Add(i, i); + CHECK(a1.Count() == 4000); + int32 capacity = a1.Capacity(); + for (int32 i = 0; i < 4000; i++) + { + CHECK(a1.ContainsKey(i)); + CHECK(a1.ContainsValue(i)); + } + a1.Clear(); + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Add(i, i); + CHECK(a1.Count() == 4000); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Remove(i); + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Add(i, i); + CHECK(a1.Count() == 4000); + CHECK(a1.Capacity() == capacity); + } + + SECTION("Test Default Capacity") + { + Dictionary a1; + a1.Add(1, 1); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + } + + SECTION("Test Add/Remove") + { + Dictionary a1; + for (int32 i = 0; i < 4000; i++) + { + a1.Add(i, i); + a1.Remove(i); + } + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + a1.Clear(); + for (int32 i = 1; i <= 10; i++) + a1.Add(-i, -i); + for (int32 i = 0; i < 4000; i++) + { + a1.Add(i, i); + a1.Remove(i); + } + CHECK(a1.Count() == 10); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + } +} From 6f1ee382b5e4ec17a9b5cff0478f207f97bf0238 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 3 Nov 2023 19:07:16 +0100 Subject: [PATCH 182/546] Fix regression from 087cfd7a8c5f81c60589be36646cb59802f00fbb on hashset item duplicate re-add --- Source/Engine/Core/Collections/HashSet.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index 166ca5cc3..d3fcf461d 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -702,8 +702,9 @@ private: FindPositionResult pos; FindPosition(key, pos); - // Ensure key is unknown - ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); + // Check if object has been already added + if (pos.ObjectIndex != -1) + return nullptr; // Insert ASSERT(pos.FreeSlotIndex != -1); From 224e43ea555a21d6c701dec40a273e624d8c69fd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 3 Nov 2023 19:23:38 +0100 Subject: [PATCH 183/546] Use soft asset ref to graphics settings #1852 --- Source/Engine/Graphics/PostProcessSettings.h | 11 ++++++----- Source/Engine/Graphics/Textures/StreamingTexture.cpp | 6 +++--- Source/Engine/Graphics/Textures/Types.h | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Graphics/PostProcessSettings.h b/Source/Engine/Graphics/PostProcessSettings.h index f3440c4a5..d2ace6cea 100644 --- a/Source/Engine/Graphics/PostProcessSettings.h +++ b/Source/Engine/Graphics/PostProcessSettings.h @@ -5,6 +5,7 @@ #include "Engine/Core/Math/Vector3.h" #include "Engine/Core/Math/Vector4.h" #include "Engine/Content/AssetReference.h" +#include "Engine/Content/SoftAssetReference.h" #include "Engine/Core/ISerializable.h" #include "Engine/Content/Assets/Texture.h" #include "Engine/Content/Assets/MaterialBase.h" @@ -850,7 +851,7 @@ API_STRUCT() struct FLAXENGINE_API ColorGradingSettings : ISerializable /// The Lookup Table (LUT) used to perform color correction. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(22), PostProcessSetting((int)ColorGradingSettingsOverride.LutTexture)") - AssetReference LutTexture; + SoftAssetReference LutTexture; /// /// The LUT blending weight (normalized to range 0-1). Default is 1.0. @@ -1277,7 +1278,7 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable /// Fullscreen lens dirt texture. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(8), PostProcessSetting((int)LensFlaresSettingsOverride.LensDirt)") - AssetReference LensDirt; + SoftAssetReference LensDirt; /// /// Fullscreen lens dirt intensity parameter. Allows to tune dirt visibility. @@ -1289,13 +1290,13 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable /// Custom lens color texture (1D) used for lens color spectrum. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(10), PostProcessSetting((int)LensFlaresSettingsOverride.LensColor)") - AssetReference LensColor; + SoftAssetReference LensColor; /// /// Custom lens star texture sampled by lens flares. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(11), PostProcessSetting((int)LensFlaresSettingsOverride.LensStar)") - AssetReference LensStar; + SoftAssetReference LensStar; public: /// @@ -1487,7 +1488,7 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable /// If BokehShape is set to Custom, then this texture will be used for the bokeh shapes. For best performance, use small, compressed, grayscale textures (for instance 32px). /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(11), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehShapeCustom)") - AssetReference BokehShapeCustom; + SoftAssetReference BokehShapeCustom; /// /// The minimum pixel brightness to create bokeh. Pixels with lower brightness will be skipped. diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index 409125838..e1be01252 100644 --- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp +++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp @@ -22,10 +22,10 @@ TextureHeader::TextureHeader() TextureGroup = -1; } -TextureHeader::TextureHeader(TextureHeader_Deprecated& old) +TextureHeader::TextureHeader(const TextureHeader_Deprecated& old) { Platform::MemoryClear(this, sizeof(*this)); - Width = old.Width;; + Width = old.Width; Height = old.Height; MipLevels = old.MipLevels; Format = old.Format; @@ -49,7 +49,7 @@ StreamingTexture::StreamingTexture(ITextureOwner* parent, const String& name) , _texture(nullptr) , _isBlockCompressed(false) { - ASSERT(_owner != nullptr); + ASSERT(parent != nullptr); // Always have created texture object ASSERT(GPUDevice::Instance); diff --git a/Source/Engine/Graphics/Textures/Types.h b/Source/Engine/Graphics/Textures/Types.h index 9593cbbc2..31e0b1afc 100644 --- a/Source/Engine/Graphics/Textures/Types.h +++ b/Source/Engine/Graphics/Textures/Types.h @@ -106,5 +106,5 @@ struct FLAXENGINE_API TextureHeader byte CustomData[10]; TextureHeader(); - TextureHeader(TextureHeader_Deprecated& old); + TextureHeader(const TextureHeader_Deprecated& old); }; From e429e85aae8ae77d378bf8a604adf3c65ab27bbe Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 3 Nov 2023 22:27:58 +0100 Subject: [PATCH 184/546] Continue `Delegate` refactor to use single memory allocation and atomic operations for data access --- Source/Engine/Core/Delegate.h | 189 ++++++++++++++++++---------------- 1 file changed, 101 insertions(+), 88 deletions(-) diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h index 945dd6ce5..f0f8fdef0 100644 --- a/Source/Engine/Core/Delegate.h +++ b/Source/Engine/Core/Delegate.h @@ -226,7 +226,7 @@ public: /// Function result FORCE_INLINE ReturnType operator()(Params... params) const { - ASSERT(_function); + ASSERT_LOW_LAYER(_function); return _function(_callee, Forward(params)...); } @@ -289,8 +289,13 @@ protected: intptr volatile _ptr = 0; intptr volatile _size = 0; #else - HashSet* _functions = nullptr; - CriticalSection* _locker = nullptr; + struct Data + { + HashSet Functions; + CriticalSection Locker; + }; + // Holds pointer to Data with Functions and Locker. Thread-safe access via atomic operations. + intptr volatile _data = 0; #endif typedef void (*StubSignature)(void*, Params...); @@ -314,15 +319,12 @@ public: _ptr = (intptr)newBindings; _size = newSize; #else - if (other._functions == nullptr) + Data* otherData = (Data*)Platform::AtomicRead(&_data); + if (otherData == nullptr) return; - _functions = New>(*other._functions); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) - { - if (i->Item._function && i->Item._lambda) - i->Item.LambdaCtor(); - } - _locker = other._locker; + ScopeLock lock(otherData->Locker); + for (auto i = otherData->Functions.Begin(); i.IsNotEnd(); ++i) + Bind(i->Item); #endif } @@ -334,10 +336,8 @@ public: other._ptr = 0; other._size = 0; #else - _functions = other._functions; - _locker = other._locker; - other._functions = nullptr; - other._locker = nullptr; + _data = other._data; + other._data = 0; #endif } @@ -356,20 +356,11 @@ public: Allocator::Free((void*)_ptr); } #else - if (_locker != nullptr) + Data* data = (Data*)_data; + if (data) { - Allocator::Free(_locker); - _locker = nullptr; - } - if (_functions != nullptr) - { - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) - { - if (i->Item._lambda) - i->Item.LambdaCtor(); - } - Allocator::Free(_functions); - _functions = nullptr; + _data = 0; + Delete(data); } #endif } @@ -385,8 +376,13 @@ public: for (intptr i = 0; i < size; i++) Bind(bindings[i]); #else - for (auto i = other._functions->Begin(); i.IsNotEnd(); ++i) - Bind(i->Item); + Data* otherData = (Data*)Platform::AtomicRead(&_data); + if (otherData != nullptr) + { + ScopeLock lock(otherData->Locker); + for (auto i = otherData->Functions.Begin(); i.IsNotEnd(); ++i) + Bind(i->Item); + } #endif } return *this; @@ -402,10 +398,8 @@ public: other._ptr = 0; other._size = 0; #else - _functions = other._functions; - _locker = other._locker; - other._functions = nullptr; - other._locker = nullptr; + _data = other._data; + other._data = 0; #endif } return *this; @@ -507,12 +501,20 @@ public: Allocator::Free(bindings); } #else - if (_locker == nullptr) - _locker = New(); - ScopeLock lock(*_locker); - if (_functions == nullptr) - _functions = New>(); - _functions->Add(f); + Data* data = (Data*)Platform::AtomicRead(&_data); + while (!data) + { + Data* newData = New(); + Data* oldData = (Data*)Platform::InterlockedCompareExchange(&_data, (intptr)newData, (intptr)data); + if (oldData != data) + { + // Other thread already set the new data so free it and try again + Delete(newData); + } + data = (Data*)Platform::AtomicRead(&_data); + } + ScopeLock lock(data->Locker); + data->Functions.Add(f); #endif } @@ -568,13 +570,22 @@ public: } } #else - if (_locker == nullptr) - _locker = New(); - ScopeLock lock(*_locker); - if (_functions && _functions->Contains(f)) - return; + Data* data = (Data*)Platform::AtomicRead(&_data); + if (data) + { + data->Locker.Lock(); + if (data->Functions.Contains(f)) + { + data->Locker.Unlock(); + return; + } + } #endif Bind(f); +#if !DELEGATE_USE_ATOMIC + if (data) + data->Locker.Unlock(); +#endif } /// @@ -640,10 +651,11 @@ public: Unbind(f); } #else - if (_functions == nullptr) + Data* data = (Data*)Platform::AtomicRead(&_data); + if (!data) return; - ScopeLock lock(*_locker); - _functions->Remove(f); + ScopeLock lock(data->Locker); + data->Functions.Remove(f); #endif } @@ -666,15 +678,11 @@ public: Platform::AtomicStore((intptr volatile*)&bindings[i]._callee, 0); } #else - if (_functions == nullptr) + Data* data = (Data*)Platform::AtomicRead(&_data); + if (!data) return; - ScopeLock lock(*_locker); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) - { - if (i->Item._lambda) - i->Item.LambdaDtor(); - } - _functions->Clear(); + ScopeLock lock(data->Locker); + data->Functions.Clear(); #endif } @@ -684,22 +692,24 @@ public: /// The bound functions count. int32 Count() const { + int32 result = 0; #if DELEGATE_USE_ATOMIC - int32 count = 0; const intptr size = Platform::AtomicRead((intptr volatile*)&_size); FunctionType* bindings = (FunctionType*)Platform::AtomicRead((intptr volatile*)&_ptr); for (intptr i = 0; i < size; i++) { if (Platform::AtomicRead((intptr volatile*)&bindings[i]._function) != 0) - count++; + result++; } - return count; #else - if (_functions == nullptr) - return 0; - ScopeLock lock(*_locker); - return _functions->Count(); + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) + { + ScopeLock lock(data->Locker); + result = data->Functions.Count(); + } #endif + return result; } /// @@ -710,10 +720,14 @@ public: #if DELEGATE_USE_ATOMIC return (int32)Platform::AtomicRead((intptr volatile*)&_size); #else - if (_functions == nullptr) - return 0; - ScopeLock lock(*_locker); - return _functions->Capacity(); + int32 result = 0; + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) + { + ScopeLock lock(data->Locker); + result = data->Functions.Capacity(); + } + return result; #endif } @@ -733,10 +747,14 @@ public: } return false; #else - if (_functions == nullptr) - return false; - ScopeLock lock(*_locker); - return _functions->Count() > 0; + bool result = false; + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) + { + ScopeLock lock(data->Locker); + result = data->Functions.Count() != 0; + } + return result; #endif } @@ -765,18 +783,13 @@ public: } } #else - if (_functions == nullptr) - return 0; - ScopeLock lock(*_locker); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) { - if (i->Item._function != nullptr) + ScopeLock lock(data->Locker); + for (auto i = data->Functions.Begin(); i.IsNotEnd(); ++i) { - buffer[count]._function = (StubSignature)i->Item._function; - buffer[count]._callee = (void*)i->Item._callee; - buffer[count]._lambda = (typename FunctionType::Lambda*)i->Item._lambda; - if (buffer[count]._lambda) - buffer[count].LambdaCtor(); + new(buffer + count) FunctionType((const FunctionType&)i->Item); count++; } } @@ -802,15 +815,15 @@ public: ++bindings; } #else - if (_functions == nullptr) + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (!data) return; - ScopeLock lock(*_locker); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) + ScopeLock lock(data->Locker); + for (auto i = data->Functions.Begin(); i.IsNotEnd(); ++i) { - auto function = (StubSignature)(i->Item._function); - auto callee = (void*)(i->Item._callee); - if (function != nullptr) - function(callee, Forward(params)...); + const FunctionType& item = i->Item; + ASSERT_LOW_LAYER(item._function); + item._function(item._callee, Forward(params)...); } #endif } From 6ddf241ea4b407143f54b9f4f66e0a0fa9ce0e5d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 3 Nov 2023 22:40:18 +0100 Subject: [PATCH 185/546] Minor improvements to reference properties --- Source/Engine/Content/Asset.cpp | 32 +++++++++++++++---- Source/Engine/Content/SoftAssetReference.h | 4 +-- .../Scripting/ScriptingObjectReference.h | 8 +++-- Source/Engine/Scripting/SoftObjectReference.h | 8 +++-- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 613c7c2c2..afe38f0a7 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -16,11 +16,13 @@ AssetReferenceBase::~AssetReferenceBase() { - if (_asset) + Asset* asset = _asset; + if (asset) { - _asset->OnLoaded.Unbind(this); - _asset->OnUnloaded.Unbind(this); - _asset->RemoveReference(); + _asset = nullptr; + asset->OnLoaded.Unbind(this); + asset->OnUnloaded.Unbind(this); + asset->RemoveReference(); } } @@ -70,8 +72,12 @@ void AssetReferenceBase::OnUnloaded(Asset* asset) WeakAssetReferenceBase::~WeakAssetReferenceBase() { - if (_asset) - _asset->OnUnloaded.Unbind(this); + Asset* asset = _asset; + if (asset) + { + _asset = nullptr; + asset->OnUnloaded.Unbind(this); + } } String WeakAssetReferenceBase::ToString() const @@ -101,6 +107,20 @@ void WeakAssetReferenceBase::OnUnloaded(Asset* asset) _asset = nullptr; } +SoftAssetReferenceBase::~SoftAssetReferenceBase() +{ + Asset* asset = _asset; + if (asset) + { + _asset = nullptr; + asset->OnUnloaded.Unbind(this); + asset->RemoveReference(); + } +#if !BUILD_RELEASE + _id = Guid::Empty; +#endif +} + String SoftAssetReferenceBase::ToString() const { return _asset ? _asset->ToString() : (_id.IsValid() ? _id.ToString() : TEXT("")); diff --git a/Source/Engine/Content/SoftAssetReference.h b/Source/Engine/Content/SoftAssetReference.h index fe1cde8c2..d237b5fd7 100644 --- a/Source/Engine/Content/SoftAssetReference.h +++ b/Source/Engine/Content/SoftAssetReference.h @@ -30,9 +30,7 @@ public: /// /// Finalizes an instance of the class. /// - ~SoftAssetReferenceBase() - { - } + ~SoftAssetReferenceBase(); public: /// diff --git a/Source/Engine/Scripting/ScriptingObjectReference.h b/Source/Engine/Scripting/ScriptingObjectReference.h index 0012c89ba..0945d6fc2 100644 --- a/Source/Engine/Scripting/ScriptingObjectReference.h +++ b/Source/Engine/Scripting/ScriptingObjectReference.h @@ -47,8 +47,12 @@ public: /// ~ScriptingObjectReferenceBase() { - if (_object) - _object->Deleted.Unbind(this); + ScriptingObject* obj = _object; + if (obj) + { + _object = nullptr; + obj->Deleted.Unbind(this); + } } public: diff --git a/Source/Engine/Scripting/SoftObjectReference.h b/Source/Engine/Scripting/SoftObjectReference.h index 3366dbbc7..174cf4188 100644 --- a/Source/Engine/Scripting/SoftObjectReference.h +++ b/Source/Engine/Scripting/SoftObjectReference.h @@ -38,8 +38,12 @@ public: /// ~SoftObjectReferenceBase() { - if (_object) - _object->Deleted.Unbind(this); + ScriptingObject* obj = _object; + if (obj) + { + _object = nullptr; + obj->Deleted.Unbind(this); + } } public: From 236e5772cedc0f93b1bc23c7566c7bf7bbd06a1a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 13:49:30 +0100 Subject: [PATCH 186/546] Fix CultureInfo to handle missing country code and fallback to outer language code --- Source/Engine/Localization/CultureInfo.cpp | 45 ++++++++++++++-------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Localization/CultureInfo.cpp b/Source/Engine/Localization/CultureInfo.cpp index 3e02ccf6e..fc678d419 100644 --- a/Source/Engine/Localization/CultureInfo.cpp +++ b/Source/Engine/Localization/CultureInfo.cpp @@ -30,6 +30,20 @@ typedef struct } MonoCultureInfo; #endif +namespace +{ + const CultureInfoEntry* FindEntry(const StringAnsiView& name) + { + for (int32 i = 0; i < NUM_CULTURE_ENTRIES; i++) + { + const CultureInfoEntry& e = culture_entries[i]; + if (name == idx2string(e.name)) + return &e; + } + return nullptr; + } +}; + CultureInfo::CultureInfo(int32 lcid) { _lcid = lcid; @@ -80,23 +94,24 @@ CultureInfo::CultureInfo(const StringAnsiView& name) _englishName = TEXT("Invariant Culture"); return; } - for (int32 i = 0; i < NUM_CULTURE_ENTRIES; i++) + const CultureInfoEntry* e = FindEntry(name); + if (!e && name.Find('-') != -1) { - auto& e = culture_entries[i]; - if (name == idx2string(e.name)) - { - _data = (void*)&e; - _lcid = (int32)e.lcid; - _lcidParent = (int32)e.parent_lcid; - _name.SetUTF8(name.Get(), name.Length()); - const char* nativename = idx2string(e.nativename); - _nativeName.SetUTF8(nativename, StringUtils::Length(nativename)); - const char* englishname = idx2string(e.englishname); - _englishName.SetUTF8(englishname, StringUtils::Length(englishname)); - break; - } + e = FindEntry(name.Substring(0, name.Find('-'))); } - if (!_data) + if (e) + { + _data = (void*)e; + _lcid = (int32)e->lcid; + _lcidParent = (int32)e->parent_lcid; + const char* ename = idx2string(e->name); + _name.SetUTF8(ename, StringUtils::Length(ename)); + const char* nativename = idx2string(e->nativename); + _nativeName.SetUTF8(nativename, StringUtils::Length(nativename)); + const char* englishname = idx2string(e->englishname); + _englishName.SetUTF8(englishname, StringUtils::Length(englishname)); + } + else { _lcid = 127; _lcidParent = 0; From 50bcbf980e432fa3c8ec6a49fdb3cc8a58ae3cad Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 14:08:53 +0100 Subject: [PATCH 187/546] Add SetThreadAffinityMask and SetThreadPriority and thread name for Apple platforms --- Source/Engine/Platform/Apple/ApplePlatform.cpp | 12 ++++++++++-- Source/Engine/Platform/Apple/AppleThread.h | 13 +++++++++---- Source/Engine/Platform/Unix/UnixThread.cpp | 6 ++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Platform/Apple/ApplePlatform.cpp b/Source/Engine/Platform/Apple/ApplePlatform.cpp index d4348b107..c7ab4424d 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.cpp +++ b/Source/Engine/Platform/Apple/ApplePlatform.cpp @@ -19,6 +19,7 @@ #include "Engine/Platform/StringUtils.h" #include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/Clipboard.h" +#include "Engine/Platform/Thread.h" #include "Engine/Platform/IGuiData.h" #include "Engine/Platform/Base/PlatformUtils.h" #include "Engine/Utilities/StringConverter.h" @@ -176,12 +177,19 @@ uint64 ApplePlatform::GetCurrentThreadID() void ApplePlatform::SetThreadPriority(ThreadPriority priority) { - // TODO: impl this + struct sched_param sched; + Platform::MemoryClear(&sched, sizeof(struct sched_param)); + int32 policy = SCHED_RR; + pthread_getschedparam(pthread_self(), &policy, &sched); + sched.sched_priority = AppleThread::GetAppleThreadPriority(priority); + pthread_setschedparam(pthread_self(), policy, &sched); } void ApplePlatform::SetThreadAffinityMask(uint64 affinityMask) { - // TODO: impl this + thread_affinity_policy policy; + policy.affinity_tag = affinityMask; + thread_policy_set(pthread_mach_thread_np(pthread_self()), THREAD_AFFINITY_POLICY, (integer_t*)&policy, THREAD_AFFINITY_POLICY_COUNT); } void ApplePlatform::Sleep(int32 milliseconds) diff --git a/Source/Engine/Platform/Apple/AppleThread.h b/Source/Engine/Platform/Apple/AppleThread.h index 43d9d2966..a578092b6 100644 --- a/Source/Engine/Platform/Apple/AppleThread.h +++ b/Source/Engine/Platform/Apple/AppleThread.h @@ -40,10 +40,7 @@ public: return (AppleThread*)Setup(New(runnable, name, priority), stackSize); } -protected: - - // [UnixThread] - int32 GetThreadPriority(ThreadPriority priority) override + static int32 GetAppleThreadPriority(ThreadPriority priority) { switch (priority) { @@ -60,6 +57,14 @@ protected: } return 31; } + +protected: + + // [UnixThread] + int32 GetThreadPriority(ThreadPriority priority) override + { + return GetAppleThreadPriority(priority); + } int32 Start(pthread_attr_t& attr) override { return pthread_create(&_thread, &attr, ThreadProc, this); diff --git a/Source/Engine/Platform/Unix/UnixThread.cpp b/Source/Engine/Platform/Unix/UnixThread.cpp index f662bdba8..29dc5f881 100644 --- a/Source/Engine/Platform/Unix/UnixThread.cpp +++ b/Source/Engine/Platform/Unix/UnixThread.cpp @@ -26,6 +26,12 @@ int32 UnixThread::Start(pthread_attr_t& attr) void* UnixThread::ThreadProc(void* pThis) { auto thread = (UnixThread*)pThis; +#if PLATFORM_APPLE_FAMILY + // Apple doesn't support creating named thread so assign name here + { + pthread_setname_np(StringAnsi(thread->GetName()).Get()); + } +#endif const int32 exitCode = thread->Run(); return (void*)(uintptr)exitCode; } From 19752e4f3ba1d9a31945bcf97198ec0c1a7a2d96 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 15:26:18 +0100 Subject: [PATCH 188/546] Add storing shader asset includes paths in compact format for portability --- Source/Engine/Core/Types/StringView.h | 24 ++++ .../ShadersCompilation/ShaderCompiler.cpp | 103 ++------------- .../ShadersCompilation/ShadersCompilation.cpp | 122 +++++++++++++++++- .../ShadersCompilation/ShadersCompilation.h | 6 +- 4 files changed, 159 insertions(+), 96 deletions(-) diff --git a/Source/Engine/Core/Types/StringView.h b/Source/Engine/Core/Types/StringView.h index 27c63c999..3e2bbd5f3 100644 --- a/Source/Engine/Core/Types/StringView.h +++ b/Source/Engine/Core/Types/StringView.h @@ -327,6 +327,18 @@ public: bool operator!=(const String& other) const; public: + using StringViewBase::StartsWith; + FORCE_INLINE bool StartsWith(const StringView& prefix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::StartsWith(prefix, searchCase); + } + + using StringViewBase::EndsWith; + FORCE_INLINE bool EndsWith(const StringView& suffix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::EndsWith(suffix, searchCase); + } + /// /// Gets the left most given number of characters. /// @@ -511,6 +523,18 @@ public: bool operator!=(const StringAnsi& other) const; public: + using StringViewBase::StartsWith; + FORCE_INLINE bool StartsWith(const StringAnsiView& prefix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::StartsWith(prefix, searchCase); + } + + using StringViewBase::EndsWith; + FORCE_INLINE bool EndsWith(const StringAnsiView& suffix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::EndsWith(suffix, searchCase); + } + /// /// Retrieves substring created from characters starting from startIndex to the String end. /// diff --git a/Source/Engine/ShadersCompilation/ShaderCompiler.cpp b/Source/Engine/ShadersCompilation/ShaderCompiler.cpp index c0fda4d6e..da67ef3b4 100644 --- a/Source/Engine/ShadersCompilation/ShaderCompiler.cpp +++ b/Source/Engine/ShadersCompilation/ShaderCompiler.cpp @@ -3,6 +3,7 @@ #if COMPILE_WITH_SHADER_COMPILER #include "ShaderCompiler.h" +#include "ShadersCompilation.h" #include "Engine/Core/Log.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Engine/Globals.h" @@ -14,10 +15,6 @@ #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Serialization/MemoryWriteStream.h" #include "Engine/Utilities/StringConverter.h" -#if USE_EDITOR -#include "Editor/Editor.h" -#include "Editor/ProjectInfo.h" -#endif namespace IncludedFiles { @@ -30,31 +27,6 @@ namespace IncludedFiles CriticalSection Locker; Dictionary Files; - -#if USE_EDITOR - bool FindProject(const ProjectInfo* project, HashSet& projects, const StringView& projectName, String& path) - { - if (!project || projects.Contains(project)) - return false; - projects.Add(project); - - // Check the project name - if (project->Name == projectName) - { - path = project->ProjectFolderPath; - return true; - } - - // Initialize referenced projects - for (const auto& reference : project->References) - { - if (reference.Project && FindProject(reference.Project, projects, projectName, path)) - return true; - } - - return false; - } -#endif } bool ShaderCompiler::Compile(ShaderCompilationContext* context) @@ -124,7 +96,8 @@ bool ShaderCompiler::Compile(ShaderCompilationContext* context) output->WriteInt32(context->Includes.Count()); for (auto& include : context->Includes) { - output->WriteString(include.Item, 11); + String compactPath = ShadersCompilation::CompactShaderPath(include.Item); + output->WriteString(compactPath, 11); const auto date = FileSystem::GetFileLastEditTime(include.Item); output->Write(date); } @@ -138,75 +111,17 @@ bool ShaderCompiler::GetIncludedFileSource(ShaderCompilationContext* context, co source = nullptr; sourceLength = 0; - // Skip to the last root start './' but preserve the leading one - const int32 includedFileLength = StringUtils::Length(includedFile); - for (int32 i = includedFileLength - 2; i >= 2; i--) + // Get actual file path + const String includedFileName(includedFile); + String path = ShadersCompilation::ResolveShaderPath(includedFileName); + if (!FileSystem::FileExists(path)) { - if (StringUtils::Compare(includedFile + i, "./", 2) == 0) - { - includedFile = includedFile + i; - break; - } + LOG(Error, "Unknown shader source file '{0}' included in '{1}'.{2}", includedFileName, String(sourceFile), String::Empty); + return true; } ScopeLock lock(IncludedFiles::Locker); - // Find the included file path - String path; -#if USE_EDITOR - if (StringUtils::Compare(includedFile, "./", 2) == 0) - { - int32 projectNameEnd = -1; - for (int32 i = 2; i < includedFileLength; i++) - { - if (includedFile[i] == '/') - { - projectNameEnd = i; - break; - } - } - if (projectNameEnd == -1) - { - LOG(Error, "Unknown shader source file '{0}' included in '{1}'.{2}", String(includedFile), String(sourceFile), TEXT("Missing project name after root path.")); - return true; - } - const StringAsUTF16<120> projectName(includedFile + 2, projectNameEnd - 2); - if (StringUtils::Compare(projectName.Get(), TEXT("FlaxPlatforms")) == 0) - { - // Hard-coded redirect to platform-specific includes - path /= Globals::StartupFolder / TEXT("Source/Platforms"); - } - else - { - HashSet projects; - if (!IncludedFiles::FindProject(Editor::Project, projects, StringView(projectName.Get(), projectNameEnd - 2), path)) - { - LOG(Error, "Unknown shader source file '{0}' included in '{1}'.{2}", String(includedFile), String(sourceFile), TEXT("Failed to find the project of the given name.")); - return true; - } - path /= TEXT("Source/Shaders/"); - } - const StringAsUTF16<250> localPath(includedFile + projectNameEnd + 1, includedFileLength - projectNameEnd - 1); - path /= localPath.Get(); - } -#else - if (StringUtils::Compare(includedFile, "./Flax/", 7) == 0) - { - // Engine project relative shader path - const auto includedFileStr = String(includedFile + 6); - path = Globals::StartupFolder / TEXT("Source/Shaders") / includedFileStr; - } -#endif - else if (FileSystem::FileExists(path = String(includedFile))) - { - // Absolute shader path - } - else - { - LOG(Error, "Unknown shader source file '{0}' included in '{1}'.{2}", String(includedFile), String(sourceFile), String::Empty); - return true; - } - // Try to reuse file IncludedFiles::File* result = nullptr; if (!IncludedFiles::Files.TryGet(path, result) || FileSystem::GetFileLastEditTime(path) > result->LastEditTime) diff --git a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp index de2ba6e54..793a3c1c3 100644 --- a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp +++ b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp @@ -33,7 +33,6 @@ #include "Editor/Editor.h" #include "Editor/ProjectInfo.h" #endif - #if COMPILE_WITH_D3D_SHADER_COMPILER #include "DirectX/ShaderCompilerD3D.h" #endif @@ -55,6 +54,49 @@ namespace ShadersCompilationImpl CriticalSection Locker; Array Compilers; Array ReadyCompilers; + +#if USE_EDITOR + const ProjectInfo* FindProjectByName(const ProjectInfo* project, HashSet& projects, const StringView& projectName) + { + if (!project || projects.Contains(project)) + return nullptr; + projects.Add(project); + + // Check the project name + if (project->Name == projectName) + return project; + + // Search referenced projects + for (const auto& reference : project->References) + { + const ProjectInfo* result = FindProjectByName(reference.Project, projects, projectName); + if (result) + return result; + } + return nullptr; + } + + const ProjectInfo* FindProjectByPath(const ProjectInfo* project, HashSet& projects, const StringView& projectPath) + { + if (!project || projects.Contains(project)) + return nullptr; + projects.Add(project); + + // Search referenced projects (depth first to handle plugin projects first) + for (const auto& reference : project->References) + { + const ProjectInfo* result = FindProjectByPath(reference.Project, projects, projectPath); + if (result) + return result; + } + + // Check the project path + if (projectPath.StartsWith(project->ProjectFolderPath)) + return project; + + return nullptr; + } +#endif } using namespace ShadersCompilationImpl; @@ -361,11 +403,89 @@ void ShadersCompilation::ExtractShaderIncludes(byte* shaderCache, int32 shaderCa { String& include = includes.AddOne(); stream.ReadString(&include, 11); + include = ShadersCompilation::ResolveShaderPath(include); DateTime lastEditTime; stream.Read(lastEditTime); } } +String ShadersCompilation::ResolveShaderPath(StringView path) +{ + // Skip to the last root start './' but preserve the leading one + for (int32 i = path.Length() - 2; i >= 2; i--) + { + if (StringUtils::Compare(path.Get() + i, TEXT("./"), 2) == 0) + { + path = path.Substring(i); + break; + } + } + + // Find the included file path + String result; +#if USE_EDITOR + if (path.StartsWith(StringView(TEXT("./"), 2))) + { + int32 projectNameEnd = -1; + for (int32 i = 2; i < path.Length(); i++) + { + if (path[i] == '/') + { + projectNameEnd = i; + break; + } + } + if (projectNameEnd == -1) + return String::Empty; // Invalid project path + StringView projectName = path.Substring(2, projectNameEnd - 2); + if (projectName.StartsWith(StringView(TEXT("FlaxPlatforms")))) + { + // Hard-coded redirect to platform-specific includes + result = Globals::StartupFolder / TEXT("Source/Platforms"); + } + else + { + HashSet projects; + const ProjectInfo* project = FindProjectByName(Editor::Project, projects, StringView(projectName.Get(), projectNameEnd - 2)); + if (project) + result = project->ProjectFolderPath / TEXT("/Source/Shaders/"); + else + return String::Empty; + } + result /= path.Substring(projectNameEnd + 1); + } +#else + if (path.StartsWith(StringView(TEXT("./Flax/"), 7))) + { + // Engine project relative shader path + result = Globals::StartupFolder / TEXT("Source/Shaders") / path.Substring(6); + } +#endif + else + { + // Absolute shader path + result = path; + } + + return result; +} + +String ShadersCompilation::CompactShaderPath(StringView path) +{ +#if USE_EDITOR + // Try to use file path relative to the project shader sources folder + HashSet projects; + const ProjectInfo* project = FindProjectByPath(Editor::Project, projects, path); + if (project) + { + String projectSourcesPath = project->ProjectFolderPath / TEXT("/Source/Shaders/"); + if (path.StartsWith(projectSourcesPath)) + return String::Format(TEXT("./{}/{}"), project->Name, path.Substring(projectSourcesPath.Length())); + } +#endif + return String(path); +} + #if USE_EDITOR namespace diff --git a/Source/Engine/ShadersCompilation/ShadersCompilation.h b/Source/Engine/ShadersCompilation/ShadersCompilation.h index fca6a6ebe..fd907acf7 100644 --- a/Source/Engine/ShadersCompilation/ShadersCompilation.h +++ b/Source/Engine/ShadersCompilation/ShadersCompilation.h @@ -14,7 +14,6 @@ class Asset; class FLAXENGINE_API ShadersCompilation { public: - /// /// Compiles the shader. /// @@ -43,6 +42,11 @@ public: /// The output included. static void ExtractShaderIncludes(byte* shaderCache, int32 shaderCacheLength, Array& includes); + // Resolves shader path name into absolute file path. Resolves './/ShaderFile.hlsl' cases into a full path. + static String ResolveShaderPath(StringView path); + // Compacts the full shader file path into portable format with project name prefix such as './/ShaderFile.hlsl'. + static String CompactShaderPath(StringView path); + private: static ShaderCompiler* CreateCompiler(ShaderProfile profile); From 3bf7b57dbd6280f0a8d0ff6989c9e1b3ed3d548f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 19:14:45 +0100 Subject: [PATCH 189/546] Fix `BitArray::SetAll()` when the item is not multiple of `8` #1863 --- Source/Engine/Core/Collections/BitArray.h | 24 ++++++++++++++++------- Source/Engine/Tests/TestCollections.cpp | 13 ++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h index 01238d434..78e4f94f4 100644 --- a/Source/Engine/Core/Collections/BitArray.h +++ b/Source/Engine/Core/Collections/BitArray.h @@ -22,6 +22,16 @@ private: int32 _capacity; AllocationData _allocation; + FORCE_INLINE static int32 ToItemCount(int32 size) + { + return Math::DivideAndRoundUp(size, sizeof(ItemType)); + } + + FORCE_INLINE static int32 ToItemCapacity(int32 size) + { + return Math::Max(Math::DivideAndRoundUp(size, sizeof(ItemType)), 1); + } + public: /// /// Initializes a new instance of the class. @@ -41,7 +51,7 @@ public: , _capacity(capacity) { if (capacity > 0) - _allocation.Allocate(Math::Max(capacity / sizeof(ItemType), 1)); + _allocation.Allocate(ToItemCapacity(capacity)); } /// @@ -53,7 +63,7 @@ public: _count = _capacity = other.Count(); if (_capacity > 0) { - const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1); + const uint64 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -69,7 +79,7 @@ public: _count = _capacity = other.Count(); if (_capacity > 0) { - const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1); + const uint64 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -101,7 +111,7 @@ public: { _allocation.Free(); _capacity = other._count; - const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1); + const uint64 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -246,7 +256,7 @@ public: return; ASSERT(capacity >= 0); const int32 count = preserveContents ? (_count < capacity ? _count : capacity) : 0; - _allocation.Relocate(Math::Max(capacity / sizeof(ItemType), 1), _count / sizeof(ItemType), count / sizeof(ItemType)); + _allocation.Relocate(ToItemCapacity(capacity), ToItemCount(_count), ToItemCount(count)); _capacity = capacity; _count = count; } @@ -272,7 +282,7 @@ public: { if (_capacity < minCapacity) { - const int32 capacity = _allocation.CalculateCapacityGrow(Math::Max(_capacity / sizeof(ItemType), 1), minCapacity); + const int32 capacity = _allocation.CalculateCapacityGrow(ToItemCapacity(_capacity), minCapacity); SetCapacity(capacity, preserveContents); } } @@ -284,7 +294,7 @@ public: void SetAll(const bool value) { if (_count != 0) - Platform::MemorySet(_allocation.Get(), Math::Max(_count / sizeof(ItemType), 1), value ? MAX_int32 : 0); + Platform::MemorySet(_allocation.Get(), ToItemCount(_count) * sizeof(ItemType), value ? MAX_uint64 : 0); } /// diff --git a/Source/Engine/Tests/TestCollections.cpp b/Source/Engine/Tests/TestCollections.cpp index 24c971860..ff5f34bf6 100644 --- a/Source/Engine/Tests/TestCollections.cpp +++ b/Source/Engine/Tests/TestCollections.cpp @@ -109,6 +109,19 @@ TEST_CASE("BitArray") a1 = testData; CHECK(a1 == testData); } + + SECTION("Test Set All") + { + BitArray<> a1; + a1.Resize(9); + CHECK(a1.Count() == 9); + a1.SetAll(true); + for (int32 i = 0; i < a1.Count(); i++) + CHECK(a1[i] == true); + a1.SetAll(false); + for (int32 i = 0; i < a1.Count(); i++) + CHECK(a1[i] == false); + } } TEST_CASE("HashSet") From fe3f64f06abc5152e8ca6a4c824eaa0a25e5e506 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 19:15:37 +0100 Subject: [PATCH 190/546] Fix iOS build --- Source/Engine/Platform/Apple/ApplePlatform.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Platform/Apple/ApplePlatform.cpp b/Source/Engine/Platform/Apple/ApplePlatform.cpp index c7ab4424d..c939c1af2 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.cpp +++ b/Source/Engine/Platform/Apple/ApplePlatform.cpp @@ -187,9 +187,11 @@ void ApplePlatform::SetThreadPriority(ThreadPriority priority) void ApplePlatform::SetThreadAffinityMask(uint64 affinityMask) { +#if PLATFORM_MAC thread_affinity_policy policy; policy.affinity_tag = affinityMask; thread_policy_set(pthread_mach_thread_np(pthread_self()), THREAD_AFFINITY_POLICY, (integer_t*)&policy, THREAD_AFFINITY_POLICY_COUNT); +#endif } void ApplePlatform::Sleep(int32 milliseconds) From 8c34fe79332020ae9505f48ccb32873edffcba52 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 19:18:21 +0100 Subject: [PATCH 191/546] Reduce `LargeWorlds::ChunkSize` to `8192` #1854 --- Source/Engine/Level/LargeWorlds.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Level/LargeWorlds.h b/Source/Engine/Level/LargeWorlds.h index 514c1d5ac..2d92b7228 100644 --- a/Source/Engine/Level/LargeWorlds.h +++ b/Source/Engine/Level/LargeWorlds.h @@ -19,7 +19,7 @@ API_CLASS(Static) class FLAXENGINE_API LargeWorlds /// /// Defines the size of a single chunk. Large world (64-bit) gets divided into smaller chunks so all the math operations (32-bit) can be performed relative to the chunk origin without precision loss. /// - API_FIELD() static constexpr Real ChunkSize = 262144; + API_FIELD() static constexpr Real ChunkSize = 8192; /// /// Updates the large world origin to match the input position. The origin is snapped to the best matching chunk location. From 585b6bacad67e6386459200dd8e4d97ed8a3c156 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 19:26:46 +0100 Subject: [PATCH 192/546] Simplify #1807 --- Source/Editor/SceneGraph/GUI/ActorTreeNode.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index 1f2189346..f64e46385 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -266,7 +266,7 @@ namespace FlaxEditor.SceneGraph.GUI /// /// Starts the actor renaming action. /// - public void StartRenaming(EditorWindow window, Panel treePanel) + public void StartRenaming(EditorWindow window, Panel treePanel = null) { // Block renaming during scripts reload if (Editor.Instance.ProgressReporting.CompileScripts.IsActive) @@ -281,9 +281,12 @@ namespace FlaxEditor.SceneGraph.GUI (window as PrefabWindow).ScrollingOnTreeView(false); // Start renaming the actor - treePanel.ScrollViewTo(this, true); - var size = new Float2(treePanel.Width - TextRect.Location.X, TextRect.Height); - var rect = new Rectangle(TextRect.Location, size); + var rect = TextRect; + if (treePanel != null) + { + treePanel.ScrollViewTo(this, true); + rect.Size = new Float2(treePanel.Width - TextRect.Location.X, TextRect.Height); + } var dialog = RenamePopup.Show(this, rect, _actorNode.Name, false); dialog.Renamed += OnRenamed; dialog.Closed += popup => From 22e34cb2b4458b16dfb95961484959e0475705fb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 19:30:16 +0100 Subject: [PATCH 193/546] Fix crash on editor startup without code editor selected #1872 --- Source/Editor/States/LoadingState.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/Editor/States/LoadingState.cs b/Source/Editor/States/LoadingState.cs index 698dc192f..d48918e1b 100644 --- a/Source/Editor/States/LoadingState.cs +++ b/Source/Editor/States/LoadingState.cs @@ -56,12 +56,14 @@ namespace FlaxEditor.States else if (Editor.Options.Options.General.ForceScriptCompilationOnStartup && !skipCompile) { // Generate project files when Cache is missing or was cleared previously - if (!Directory.Exists(Path.Combine(Editor.GameProject?.ProjectFolderPath, "Cache", "Intermediate")) || - !Directory.Exists(Path.Combine(Editor.GameProject?.ProjectFolderPath, "Cache", "Projects"))) + var projectFolderPath = Editor.GameProject?.ProjectFolderPath; + if (!Directory.Exists(Path.Combine(projectFolderPath, "Cache", "Intermediate")) || + !Directory.Exists(Path.Combine(projectFolderPath, "Cache", "Projects"))) { - var customArgs = Editor.Instance.CodeEditing.SelectedEditor.GenerateProjectCustomArgs; + var customArgs = Editor.CodeEditing.SelectedEditor?.GenerateProjectCustomArgs; ScriptsBuilder.GenerateProject(customArgs); } + // Compile scripts before loading any scenes, then we load them and can open scenes ScriptsBuilder.Compile(); } From 4a10878b4525eb40402690c386566a410e3dbb91 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 20:04:40 +0100 Subject: [PATCH 194/546] Refactor `GPUResourceProperty` --- Source/Engine/Content/AssetReference.h | 9 +- .../Async/Tasks/GPUCopyResourceTask.h | 11 +- .../Async/Tasks/GPUCopySubresourceTask.h | 11 +- .../Async/Tasks/GPUUploadBufferTask.h | 9 +- .../Async/Tasks/GPUUploadTextureMipTask.h | 4 +- Source/Engine/Graphics/GPUDevice.cpp | 34 +++++ Source/Engine/Graphics/GPUResourceProperty.h | 142 ++++++++---------- .../Graphics/Textures/StreamingTexture.cpp | 4 +- 8 files changed, 111 insertions(+), 113 deletions(-) diff --git a/Source/Engine/Content/AssetReference.h b/Source/Engine/Content/AssetReference.h index 6aee12246..b9d54f30a 100644 --- a/Source/Engine/Content/AssetReference.h +++ b/Source/Engine/Content/AssetReference.h @@ -9,9 +9,6 @@ /// class FLAXENGINE_API AssetReferenceBase { -public: - typedef Delegate<> EventType; - protected: Asset* _asset = nullptr; @@ -19,17 +16,17 @@ public: /// /// The asset loaded event (fired when asset gets loaded or is already loaded after change). /// - EventType Loaded; + Action Loaded; /// /// The asset unloading event (should cleanup refs to it). /// - EventType Unload; + Action Unload; /// /// Action fired when field gets changed (link a new asset or change to the another value). /// - EventType Changed; + Action Changed; public: NON_COPYABLE(AssetReferenceBase); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h b/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h index 86c723530..f57327fa4 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h @@ -26,12 +26,12 @@ public: , _srcResource(src) , _dstResource(dst) { - _srcResource.OnUnload.Bind(this); - _dstResource.OnUnload.Bind(this); + _srcResource.Released.Bind(this); + _dstResource.Released.Bind(this); } private: - void OnResourceUnload(GPUResourceReference* ref) + void OnResourceReleased() { Cancel(); } @@ -47,14 +47,11 @@ protected: // [GPUTask] Result run(GPUTasksContext* context) override { - if (_srcResource.IsMissing() || _dstResource.IsMissing()) + if (!_srcResource || !_dstResource) return Result::MissingResources; - context->GPU->CopyResource(_dstResource, _srcResource); - return Result::Ok; } - void OnEnd() override { _srcResource.Unlink(); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h b/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h index ab8f1ffad..193eb965d 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h @@ -31,12 +31,12 @@ public: , _srcSubresource(srcSubresource) , _dstSubresource(dstSubresource) { - _srcResource.OnUnload.Bind(this); - _dstResource.OnUnload.Bind(this); + _srcResource.Released.Bind(this); + _dstResource.Released.Bind(this); } private: - void OnResourceUnload(GPUResourceReference* ref) + void OnResourceReleased() { Cancel(); } @@ -52,14 +52,11 @@ protected: // [GPUTask] Result run(GPUTasksContext* context) override { - if (_srcResource.IsMissing() || _dstResource.IsMissing()) + if (!_srcResource || !_dstResource) return Result::MissingResources; - context->GPU->CopySubresource(_dstResource, _dstSubresource, _srcResource, _srcSubresource); - return Result::Ok; } - void OnEnd() override { _srcResource.Unlink(); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h b/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h index d2f20449c..3d38ce58b 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h @@ -31,7 +31,7 @@ public: , _buffer(buffer) , _offset(offset) { - _buffer.OnUnload.Bind(this); + _buffer.Released.Bind(this); if (copyData) _data.Copy(data); @@ -40,7 +40,7 @@ public: } private: - void OnResourceUnload(BufferReference* ref) + void OnResourceReleased() { Cancel(); } @@ -56,14 +56,11 @@ protected: // [GPUTask] Result run(GPUTasksContext* context) override { - if (_buffer.IsMissing()) + if (!_buffer) return Result::MissingResources; - context->GPU->UpdateBuffer(_buffer, _data.Get(), _data.Length(), _offset); - return Result::Ok; } - void OnEnd() override { _buffer.Unlink(); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h b/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h index 6e9cca7fd..2aff3511b 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h @@ -35,7 +35,7 @@ public: , _rowPitch(rowPitch) , _slicePitch(slicePitch) { - _texture.OnUnload.Bind(this); + _texture.Released.Bind(this); if (copyData) _data.Copy(data); @@ -44,7 +44,7 @@ public: } private: - void OnResourceUnload(GPUTextureReference* ref) + void OnResourceReleased() { Cancel(); } diff --git a/Source/Engine/Graphics/GPUDevice.cpp b/Source/Engine/Graphics/GPUDevice.cpp index f0056e26c..2a54c7c4d 100644 --- a/Source/Engine/Graphics/GPUDevice.cpp +++ b/Source/Engine/Graphics/GPUDevice.cpp @@ -3,6 +3,7 @@ #include "GPUDevice.h" #include "RenderTargetPool.h" #include "GPUPipelineState.h" +#include "GPUResourceProperty.h" #include "GPUSwapChain.h" #include "RenderTask.h" #include "RenderTools.h" @@ -25,6 +26,39 @@ #include "Engine/Renderer/RenderList.h" #include "Engine/Scripting/Enums.h" +GPUResourcePropertyBase::~GPUResourcePropertyBase() +{ + const auto e = _resource; + if (e) + { + _resource = nullptr; + e->Releasing.Unbind(this); + } +} + +void GPUResourcePropertyBase::OnSet(GPUResource* resource) +{ + auto e = _resource; + if (e != resource) + { + if (e) + e->Releasing.Unbind(this); + _resource = e = resource; + if (e) + e->Releasing.Bind(this); + } +} + +void GPUResourcePropertyBase::OnReleased() +{ + auto e = _resource; + if (e) + { + _resource = nullptr; + e->Releasing.Unbind(this); + } +} + GPUPipelineState* GPUPipelineState::Spawn(const SpawnParams& params) { return GPUDevice::Instance->CreatePipelineState(); diff --git a/Source/Engine/Graphics/GPUResourceProperty.h b/Source/Engine/Graphics/GPUResourceProperty.h index b3c56007d..0b5d73c40 100644 --- a/Source/Engine/Graphics/GPUResourceProperty.h +++ b/Source/Engine/Graphics/GPUResourceProperty.h @@ -8,28 +8,39 @@ /// /// GPU Resource container utility object. /// -template -class GPUResourceProperty +class FLAXENGINE_API GPUResourcePropertyBase { -private: - T* _resource; +protected: + GPUResource* _resource = nullptr; -private: - // Disable copy actions - GPUResourceProperty(const GPUResourceProperty& other) = delete; +public: + NON_COPYABLE(GPUResourcePropertyBase); + + GPUResourcePropertyBase() = default; + ~GPUResourcePropertyBase(); public: /// - /// Action fired when resource gets unloaded (reference gets cleared bu async tasks should stop execution). + /// Action fired when resource gets released (reference gets cleared bu async tasks should stop execution). /// - Delegate OnUnload; + Action Released; +protected: + void OnSet(GPUResource* resource); + void OnReleased(); +}; + +/// +/// GPU Resource container utility object. +/// +template +class GPUResourceProperty : public GPUResourcePropertyBase +{ public: /// /// Initializes a new instance of the class. /// GPUResourceProperty() - : _resource(nullptr) { } @@ -38,9 +49,37 @@ public: /// /// The resource. GPUResourceProperty(T* resource) - : _resource(nullptr) { - Set(resource); + OnSet(resource); + } + + /// + /// Initializes a new instance of the class. + /// + /// The other value. + GPUResourceProperty(const GPUResourceProperty& other) + { + OnSet(other.Get()); + } + + /// + /// Initializes a new instance of the class. + /// + /// The other value. + GPUResourceProperty(GPUResourceProperty&& other) + { + OnSet(other.Get()); + other.OnSet(nullptr); + } + + GPUResourceProperty& operator=(GPUResourceProperty&& other) + { + if (&other != this) + { + OnSet(other._resource); + other.OnSet(nullptr); + } + return *this; } /// @@ -48,13 +87,6 @@ public: /// ~GPUResourceProperty() { - // Check if object has been binded - if (_resource) - { - // Unlink - _resource->Releasing.template Unbind(this); - _resource = nullptr; - } } public: @@ -63,43 +95,34 @@ public: return Get() == other; } - FORCE_INLINE bool operator==(GPUResourceProperty& other) const + FORCE_INLINE bool operator==(const GPUResourceProperty& other) const { return Get() == other.Get(); } - GPUResourceProperty& operator=(const GPUResourceProperty& other) - { - if (this != &other) - Set(other.Get()); - return *this; - } - FORCE_INLINE GPUResourceProperty& operator=(T& other) { - Set(&other); + OnSet(&other); return *this; } FORCE_INLINE GPUResourceProperty& operator=(T* other) { - Set(other); + OnSet(other); return *this; } /// /// Implicit conversion to GPU Resource /// - /// Resource FORCE_INLINE operator T*() const { - return _resource; + return (T*)_resource; } /// /// Implicit conversion to resource /// - /// True if resource has been binded, otherwise false FORCE_INLINE operator bool() const { return _resource != nullptr; @@ -108,37 +131,17 @@ public: /// /// Implicit conversion to resource /// - /// Resource FORCE_INLINE T* operator->() const { - return _resource; + return (T*)_resource; } /// /// Gets linked resource /// - /// Resource FORCE_INLINE T* Get() const { - return _resource; - } - - /// - /// Checks if resource has been binded - /// - /// True if resource has been binded, otherwise false - FORCE_INLINE bool IsBinded() const - { - return _resource != nullptr; - } - - /// - /// Checks if resource is missing - /// - /// True if resource is missing, otherwise false - FORCE_INLINE bool IsMissing() const - { - return _resource == nullptr; + return (T*)_resource; } public: @@ -148,19 +151,7 @@ public: /// Value to assign void Set(T* value) { - if (_resource != value) - { - // Remove reference from the old one - if (_resource) - _resource->Releasing.template Unbind(this); - - // Change referenced object - _resource = value; - - // Add reference to the new one - if (_resource) - _resource->Releasing.template Bind(this); - } + OnSet(value); } /// @@ -168,22 +159,7 @@ public: /// void Unlink() { - if (_resource) - { - // Remove reference from the old one - _resource->Releasing.template Unbind(this); - _resource = nullptr; - } - } - -private: - void onResourceUnload() - { - if (_resource) - { - _resource = nullptr; - OnUnload(this); - } + OnSet(nullptr); } }; diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index e1be01252..173b0ef85 100644 --- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp +++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp @@ -329,11 +329,11 @@ public: , _dataLock(_streamingTexture->GetOwner()->LockData()) { _streamingTexture->_streamingTasks.Add(this); - _texture.OnUnload.Bind(this); + _texture.Released.Bind(this); } private: - void onResourceUnload2(GPUTextureReference* ref) + void OnResourceReleased2() { // Unlink texture if (_streamingTexture) From ca2106ff5d5dfca65e9f71824a02ec668104efa9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 21:27:57 +0100 Subject: [PATCH 195/546] Remove `SHADOW_MAPS_FORMAT` and support fallback formats for shadow maps --- Source/Engine/Renderer/Config.h | 3 -- Source/Engine/Renderer/ShadowsPass.cpp | 43 ++++++++++++++------------ Source/Engine/Renderer/ShadowsPass.h | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Source/Engine/Renderer/Config.h b/Source/Engine/Renderer/Config.h index 659369a71..876da3100 100644 --- a/Source/Engine/Renderer/Config.h +++ b/Source/Engine/Renderer/Config.h @@ -114,6 +114,3 @@ PACK_STRUCT(struct ProbeData { // Maximum amount of directional light cascades (using CSM technique) #define MAX_CSM_CASCADES 4 - -// Default format for the shadow map textures -#define SHADOW_MAPS_FORMAT PixelFormat::D16_UNorm diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index 7e0cd1053..06381d826 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -4,11 +4,11 @@ #include "GBufferPass.h" #include "VolumetricFogPass.h" #include "Engine/Graphics/Graphics.h" +#include "Engine/Graphics/GPUContext.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/PixelFormatExtensions.h" #include "Engine/Content/Content.h" -#include "Engine/Graphics/GPUContext.h" #include "Engine/Scripting/Enums.h" #if USE_EDITOR #include "Engine/Renderer/Lightmaps.h" @@ -81,19 +81,22 @@ bool ShadowsPass::Init() _shader.Get()->OnReloading.Bind(this); #endif - // If GPU doesn't support linear sampling for the shadow map then fallback to the single sample on lowest quality - const auto formatTexture = PixelFormatExtensions::FindShaderResourceFormat(SHADOW_MAPS_FORMAT, false); - const auto formatFeaturesDepth = GPUDevice::Instance->GetFormatFeatures(SHADOW_MAPS_FORMAT); - const auto formatFeaturesTexture = GPUDevice::Instance->GetFormatFeatures(formatTexture); - _supportsShadows = EnumHasAllFlags(formatFeaturesDepth.Support, FormatSupport::DepthStencil | FormatSupport::Texture2D) - && EnumHasAllFlags(formatFeaturesTexture.Support, FormatSupport::ShaderSample | FormatSupport::ShaderSampleComparison); - // TODO: fallback to 32-bit shadow map format if 16-bit is not supported - if (!_supportsShadows) + // Select format for shadow maps + _shadowMapFormat = PixelFormat::Unknown; + for (const PixelFormat format : { PixelFormat::D16_UNorm, PixelFormat::D24_UNorm_S8_UInt, PixelFormat::D32_Float }) { - LOG(Warning, "GPU doesn't support shadows rendering"); - LOG(Warning, "Format: {0}, features support: {1}", ScriptingEnum::ToString(SHADOW_MAPS_FORMAT), (uint32)formatFeaturesDepth.Support); - LOG(Warning, "Format: {0}, features support: {1}", ScriptingEnum::ToString(formatTexture), (uint32)formatFeaturesTexture.Support); + const auto formatTexture = PixelFormatExtensions::FindShaderResourceFormat(format, false); + const auto formatFeaturesDepth = GPUDevice::Instance->GetFormatFeatures(format); + const auto formatFeaturesTexture = GPUDevice::Instance->GetFormatFeatures(formatTexture); + if (EnumHasAllFlags(formatFeaturesDepth.Support, FormatSupport::DepthStencil | FormatSupport::Texture2D) && + EnumHasAllFlags(formatFeaturesTexture.Support, FormatSupport::ShaderSample | FormatSupport::ShaderSampleComparison)) + { + _shadowMapFormat = format; + break; + } } + if (_shadowMapFormat == PixelFormat::Unknown) + LOG(Warning, "GPU doesn't support shadows rendering"); return false; } @@ -148,7 +151,7 @@ void ShadowsPass::updateShadowMapSize() // Select new size _currentShadowMapsQuality = Graphics::ShadowMapsQuality; - if (_supportsShadows) + if (_shadowMapFormat != PixelFormat::Unknown) { switch (_currentShadowMapsQuality) { @@ -174,18 +177,18 @@ void ShadowsPass::updateShadowMapSize() // Check if size will change if (newSizeCSM > 0 && newSizeCSM != _shadowMapsSizeCSM) { - if (_shadowMapCSM->Init(GPUTextureDescription::New2D(newSizeCSM, newSizeCSM, SHADOW_MAPS_FORMAT, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil, 1, MAX_CSM_CASCADES))) + if (_shadowMapCSM->Init(GPUTextureDescription::New2D(newSizeCSM, newSizeCSM, _shadowMapFormat, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil, 1, MAX_CSM_CASCADES))) { - LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("CSM"), newSizeCSM, (int32)SHADOW_MAPS_FORMAT); + LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("CSM"), newSizeCSM, ScriptingEnum::ToString(_shadowMapFormat)); return; } _shadowMapsSizeCSM = newSizeCSM; } if (newSizeCube > 0 && newSizeCube != _shadowMapsSizeCube) { - if (_shadowMapCube->Init(GPUTextureDescription::NewCube(newSizeCube, SHADOW_MAPS_FORMAT, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil))) + if (_shadowMapCube->Init(GPUTextureDescription::NewCube(newSizeCube, _shadowMapFormat, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil))) { - LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("Cube"), newSizeCube, (int32)SHADOW_MAPS_FORMAT); + LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("Cube"), newSizeCube, ScriptingEnum::ToString(_shadowMapFormat)); return; } _shadowMapsSizeCube = newSizeCube; @@ -586,7 +589,7 @@ bool ShadowsPass::CanRenderShadow(const RenderContext& renderContext, const Rend const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f); const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance); - return fade > ZeroTolerance && _supportsShadows; + return fade > ZeroTolerance && _shadowMapFormat != PixelFormat::Unknown; } bool ShadowsPass::CanRenderShadow(const RenderContext& renderContext, const RendererSpotLightData& light) @@ -598,12 +601,12 @@ bool ShadowsPass::CanRenderShadow(const RenderContext& renderContext, const Rend const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f); const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance); - return fade > ZeroTolerance && _supportsShadows; + return fade > ZeroTolerance && _shadowMapFormat != PixelFormat::Unknown; } bool ShadowsPass::CanRenderShadow(const RenderContext& renderContext, const RendererDirectionalLightData& light) { - return _supportsShadows; + return _shadowMapFormat != PixelFormat::Unknown; } void ShadowsPass::RenderShadow(RenderContextBatch& renderContextBatch, RendererPointLightData& light, GPUTextureView* shadowMask) diff --git a/Source/Engine/Renderer/ShadowsPass.h b/Source/Engine/Renderer/ShadowsPass.h index 176fbd9a0..e58d68d62 100644 --- a/Source/Engine/Renderer/ShadowsPass.h +++ b/Source/Engine/Renderer/ShadowsPass.h @@ -55,7 +55,7 @@ private: GPUPipelineStatePermutationsPs(Quality::MAX) * 2 * 2> _psShadowDir; GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowPoint; GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowSpot; - bool _supportsShadows; + PixelFormat _shadowMapFormat; // Shadow maps stuff int32 _shadowMapsSizeCSM; From 0387f7df8a5fc949dd9229a0fd37bdef8b345335 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 22:21:52 +0100 Subject: [PATCH 196/546] Add name to Android main thread --- Source/Engine/Platform/Android/AndroidPlatform.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Engine/Platform/Android/AndroidPlatform.cpp b/Source/Engine/Platform/Android/AndroidPlatform.cpp index c61e2aadd..c844ab81d 100644 --- a/Source/Engine/Platform/Android/AndroidPlatform.cpp +++ b/Source/Engine/Platform/Android/AndroidPlatform.cpp @@ -665,6 +665,7 @@ void AndroidPlatform::PreInit(android_app* app) app->onInputEvent = OnAppInput; ANativeActivity_setWindowFlags(app->activity, AWINDOW_FLAG_KEEP_SCREEN_ON | AWINDOW_FLAG_TURN_SCREEN_ON | AWINDOW_FLAG_FULLSCREEN | AWINDOW_FLAG_DISMISS_KEYGUARD, 0); ANativeActivity_setWindowFormat(app->activity, WINDOW_FORMAT_RGBA_8888); + pthread_setname_np(pthread_self(), "Main"); } bool AndroidPlatform::Is64BitPlatform() From a7bb236344f9c289bea50c8c78fd75447c09e5f3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 5 Nov 2023 14:07:05 +0100 Subject: [PATCH 197/546] Fix Mono GC threads suspend to not deadlock when attaching native threads to managed runtime #1864 --- Source/Engine/Scripting/Runtime/DotNet.cpp | 28 ++++++++++------------ 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 47f9b0a7c..200efa650 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -183,22 +183,15 @@ Dictionary CachedAssemblyHandles; /// void* GetStaticMethodPointer(const String& methodName); -/// -/// Calls the managed static method in NativeInterop class with given parameters. -/// -template -FORCE_INLINE RetType CallStaticMethodByName(const String& methodName, Args... args) -{ - typedef RetType (CORECLR_DELEGATE_CALLTYPE* fun)(Args...); - return ((fun)GetStaticMethodPointer(methodName))(args...); -} - /// /// Calls the managed static method with given parameters. /// template FORCE_INLINE RetType CallStaticMethod(void* methodPtr, Args... args) { +#if DOTNET_HOST_MONO + ASSERT_LOW_LAYER(mono_domain_get()); // Ensure that Mono runtime has been attached to this thread +#endif typedef RetType (CORECLR_DELEGATE_CALLTYPE* fun)(Args...); return ((fun)methodPtr)(args...); } @@ -274,7 +267,7 @@ bool MCore::LoadEngine() return true; // Prepare managed side - CallStaticMethodByName(TEXT("Init")); + CallStaticMethod(GetStaticMethodPointer(TEXT("Init"))); #ifdef MCORE_MAIN_MODULE_NAME // MCORE_MAIN_MODULE_NAME define is injected by Scripting.Build.cs on platforms that use separate shared library for engine symbols ::String flaxLibraryPath(Platform::GetMainDirectory() / TEXT(MACRO_TO_STR(MCORE_MAIN_MODULE_NAME))); @@ -293,7 +286,8 @@ bool MCore::LoadEngine() MRootDomain = New("Root"); MDomains.Add(MRootDomain); - char* buildInfo = CallStaticMethodByName(TEXT("GetRuntimeInformation")); + void* GetRuntimeInformationPtr = GetStaticMethodPointer(TEXT("GetRuntimeInformation")); + char* buildInfo = CallStaticMethod(GetRuntimeInformationPtr); LOG(Info, ".NET runtime version: {0}", ::String(buildInfo)); MCore::GC::FreeMemory(buildInfo); @@ -305,7 +299,7 @@ void MCore::UnloadEngine() if (!MRootDomain) return; PROFILE_CPU(); - CallStaticMethodByName(TEXT("Exit")); + CallStaticMethod(GetStaticMethodPointer(TEXT("Exit"))); MDomains.ClearDelete(); MRootDomain = nullptr; ShutdownHostfxr(); @@ -523,8 +517,7 @@ void MCore::GC::FreeMemory(void* ptr, bool coTaskMem) void MCore::Thread::Attach() { - // TODO: find a way to properly register native thread so Mono Stop The World (stw) won't freeze when native threads (eg. Job System) are running native code only -#if DOTNET_HOST_MONO && 0 +#if DOTNET_HOST_MONO if (!IsInMainThread() && !mono_domain_get()) { mono_thread_attach(MonoDomainHandle); @@ -1793,6 +1786,7 @@ void* GetStaticMethodPointer(const String& methodName) void* fun; if (CachedFunctions.TryGet(methodName, fun)) return fun; + PROFILE_CPU(); const int rc = get_function_pointer(NativeInteropTypeName, FLAX_CORECLR_STRING(methodName).Get(), UNMANAGEDCALLERSONLY_METHOD, nullptr, nullptr, &fun); if (rc != 0) LOG(Fatal, "Failed to get unmanaged function pointer for method {0}: 0x{1:x}", methodName.Get(), (unsigned int)rc); @@ -2014,6 +2008,9 @@ bool InitHostfxr() //Platform::SetEnvironmentVariable(TEXT("MONO_GC_DEBUG"), TEXT("6:gc-log.txt,check-remset-consistency,nursery-canaries")); #endif + // Adjust GC threads suspending mode to not block attached native threads (eg. Job System) + Platform::SetEnvironmentVariable(TEXT("MONO_THREADS_SUSPEND"), TEXT("preemptive")); + #if defined(USE_MONO_AOT_MODE) // Enable AOT mode (per-platform) mono_jit_set_aot_mode(USE_MONO_AOT_MODE); @@ -2166,6 +2163,7 @@ void* GetStaticMethodPointer(const String& methodName) void* fun; if (CachedFunctions.TryGet(methodName, fun)) return fun; + PROFILE_CPU(); static MonoClass* nativeInteropClass = nullptr; if (!nativeInteropClass) From c5d3954bc8102867d3ffda3f145f2333f1f292a3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 4 Nov 2023 20:32:52 +0100 Subject: [PATCH 198/546] Add `CalculateBoneOffsetMatrices` option to fix some animated model skeletons rendering #1862 --- Source/Engine/Tools/ModelTool/ModelTool.cpp | 20 +++++++++++--------- Source/Engine/Tools/ModelTool/ModelTool.h | 3 +++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 116f63407..9954db2c3 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -367,6 +367,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(ImportLODs); SERIALIZE(ImportVertexColors); SERIALIZE(ImportBlendShapes); + SERIALIZE(CalculateBoneOffsetMatrices); SERIALIZE(LightmapUVsSource); SERIALIZE(CollisionMeshesPrefix); SERIALIZE(Scale); @@ -413,6 +414,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(ImportLODs); DESERIALIZE(ImportVertexColors); DESERIALIZE(ImportBlendShapes); + DESERIALIZE(CalculateBoneOffsetMatrices); DESERIALIZE(LightmapUVsSource); DESERIALIZE(CollisionMeshesPrefix); DESERIALIZE(Scale); @@ -1418,6 +1420,15 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op SkeletonUpdater hierarchyUpdater(data.Nodes); hierarchyUpdater.UpdateMatrices(); + if (options.CalculateBoneOffsetMatrices) + { + // Calculate offset matrix (inverse bind pose transform) for every bone manually + for (SkeletonBone& bone : data.Skeleton.Bones) + { + CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex); + } + } + // Move meshes in the new nodes for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++) { @@ -1439,15 +1450,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op } } - // TODO: allow to link skeleton asset to model to retarget model bones skeleton for an animation - // use SkeletonMapping to map bones? - - // Calculate offset matrix (inverse bind pose transform) for every bone manually - /*for (SkeletonBone& bone : data.Skeleton.Bones) - { - CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex); - }*/ - #if USE_SKELETON_NODES_SORTING // Sort skeleton nodes and bones hierarchy (parents first) // Then it can be used with a simple linear loop update diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index bad10b86e..f5d0b07a7 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -258,6 +258,9 @@ public: // Enable/disable importing blend shapes (morph targets). API_FIELD(Attributes="EditorOrder(85), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowSkinnedModel))") bool ImportBlendShapes = false; + // Enable skeleton bones offset matrices recalculating. + API_FIELD(Attributes="EditorOrder(86), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowSkinnedModel))") + bool CalculateBoneOffsetMatrices = false; // The lightmap UVs source. API_FIELD(Attributes="EditorOrder(90), EditorDisplay(\"Geometry\", \"Lightmap UVs Source\"), VisibleIf(nameof(ShowModel))") ModelLightmapUVsSource LightmapUVsSource = ModelLightmapUVsSource::Disable; From 267e8daba555e87f04787300e0f9df91f320f524 Mon Sep 17 00:00:00 2001 From: MineBill Date: Sun, 5 Nov 2023 19:36:01 +0200 Subject: [PATCH 199/546] Honor the XDG_DATA_HOME env var and use the approved default as per specification. --- Source/Engine/Platform/Linux/LinuxFileSystem.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp index 8da3a378b..97cde4a1c 100644 --- a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp +++ b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp @@ -679,8 +679,14 @@ void LinuxFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& res result = TEXT("/usr/share"); break; case SpecialFolder::LocalAppData: - result = home; + { + String dataHome; + if (!Platform::GetEnvironmentVariable(TEXT("XDG_DATA_HOME"), dataHome)) + result = dataHome; + else + result = home / TEXT(".local/share"); break; + } case SpecialFolder::ProgramData: result = String::Empty; break; From c23f8f2b3015fc07195115e6248622a6741e9a5f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 5 Nov 2023 19:35:33 +0100 Subject: [PATCH 200/546] Fix loading delay-loaded-dll on Windows when using project plugin with native dependencies #1873 --- Source/Engine/Platform/Windows/WindowsPlatform.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.cpp b/Source/Engine/Platform/Windows/WindowsPlatform.cpp index 474a92072..cc2e52725 100644 --- a/Source/Engine/Platform/Windows/WindowsPlatform.cpp +++ b/Source/Engine/Platform/Windows/WindowsPlatform.cpp @@ -1201,8 +1201,8 @@ void* WindowsPlatform::LoadLibrary(const Char* filename) folder = StringView::Empty; if (folder.HasChars()) { - String folderNullTerminated(folder); - SetDllDirectoryW(folderNullTerminated.Get()); + const String folderNullTerminated(folder); + AddDllDirectory(folderNullTerminated.Get()); } // Avoiding windows dialog boxes if missing @@ -1210,7 +1210,10 @@ void* WindowsPlatform::LoadLibrary(const Char* filename) DWORD prevErrorMode = 0; const BOOL hasErrorMode = SetThreadErrorMode(errorMode, &prevErrorMode); - // Load the DLL + // Ensure that dll is properly searched + SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32 | LOAD_LIBRARY_SEARCH_USER_DIRS); + + // Load the library void* handle = ::LoadLibraryW(filename); if (!handle) { @@ -1221,10 +1224,6 @@ void* WindowsPlatform::LoadLibrary(const Char* filename) { SetThreadErrorMode(prevErrorMode, nullptr); } - if (folder.HasChars()) - { - SetDllDirectoryW(nullptr); - } #if CRASH_LOG_ENABLE // Refresh modules info during next stack trace collecting to have valid debug symbols information From 77e6aafc793826ee5853910cd31b80461638ec16 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 5 Nov 2023 19:36:09 +0100 Subject: [PATCH 201/546] Fix crash when not clearing Online Platform in Editor before hot-reload #1873 --- Source/Engine/Online/Online.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Source/Engine/Online/Online.cpp b/Source/Engine/Online/Online.cpp index bb79c6d60..d8f85baea 100644 --- a/Source/Engine/Online/Online.cpp +++ b/Source/Engine/Online/Online.cpp @@ -4,6 +4,9 @@ #include "IOnlinePlatform.h" #include "Engine/Core/Log.h" #include "Engine/Engine/EngineService.h" +#if USE_EDITOR +#include "Engine/Scripting/Scripting.h" +#endif #include "Engine/Scripting/ScriptingObject.h" class OnlineService : public EngineService @@ -25,6 +28,16 @@ IOnlinePlatform* Online::Platform = nullptr; Action Online::PlatformChanged; OnlineService OnlineServiceInstance; +#if USE_EDITOR + +void OnOnlineScriptsReloading() +{ + // Dispose any active platform + Online::Initialize(nullptr); +} + +#endif + bool Online::Initialize(IOnlinePlatform* platform) { if (Platform == platform) @@ -34,6 +47,9 @@ bool Online::Initialize(IOnlinePlatform* platform) if (Platform) { +#if USE_EDITOR + Scripting::ScriptsReloading.Unbind(OnOnlineScriptsReloading); +#endif Platform->Deinitialize(); } Platform = platform; @@ -45,6 +61,9 @@ bool Online::Initialize(IOnlinePlatform* platform) LOG(Error, "Failed to initialize online platform."); return true; } +#if USE_EDITOR + Scripting::ScriptsReloading.Bind(OnOnlineScriptsReloading); +#endif } return false; From 342360f537d44c6d700b59aa65d7208a685c4a30 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 5 Nov 2023 23:21:14 +0100 Subject: [PATCH 202/546] Refactor #1671 --- Source/Editor/Surface/Archetypes/Constants.cs | 130 +++++++++++++++--- Source/Editor/Surface/Archetypes/Textures.cs | 6 +- Source/Editor/Surface/ConvertibleNode.cs | 107 -------------- Source/Editor/Surface/SurfaceParameter.cs | 1 + Source/Editor/Surface/VisjectSurfaceWindow.cs | 3 +- 5 files changed, 117 insertions(+), 130 deletions(-) delete mode 100644 Source/Editor/Surface/ConvertibleNode.cs diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index 76efc9f60..458fd4af3 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -7,11 +7,12 @@ using Real = System.Single; #endif using System; -using System.Reflection; +using System.Linq; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; +using FlaxEditor.Surface.Undo; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -24,6 +25,101 @@ namespace FlaxEditor.Surface.Archetypes [HideInEditor] public static class Constants { + /// + /// A special type of node that adds the functionality to convert nodes to parameters. + /// + internal class ConvertToParameterNode : SurfaceNode + { + private readonly ScriptType _type; + private readonly Func _convertFunction; + + /// + public ConvertToParameterNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type, Func convertFunction = null) + : base(id, context, nodeArch, groupArch) + { + _type = type; + _convertFunction = convertFunction; + } + + /// + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) + { + base.OnShowSecondaryContextMenu(menu, location); + + menu.AddSeparator(); + menu.AddButton("Convert to Parameter", OnConvertToParameter); + } + + private void OnConvertToParameter() + { + if (Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + + Asset asset = Surface.Owner.SurfaceAsset; + if (asset == null || !asset.IsLoaded) + { + Editor.LogError("Asset is null or not loaded"); + return; + } + + // Add parameter to editor + var paramIndex = Surface.Parameters.Count; + var initValue = _convertFunction == null ? Values[0] : _convertFunction.Invoke(Values); + var paramAction = new AddRemoveParamAction + { + Window = window, + IsAdd = true, + Name = Utilities.Utils.IncrementNameNumber("New parameter", OnParameterRenameValidate), + Type = _type, + Index = paramIndex, + InitValue = initValue, + }; + paramAction.Do(); + + // Spawn Get Parameter Node based on the added parameter + Guid parameterGuid = Surface.Parameters[paramIndex].ID; + bool undoEnabled = Surface.Undo.Enabled; + Surface.Undo.Enabled = false; + NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); + SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, Location, new object[] { parameterGuid }); + Surface.Undo.Enabled = undoEnabled; + if (node is not Parameters.SurfaceNodeParamsGet getNode) + throw new Exception("Node is not a ParamsGet node!"); + + // Recreate connections of constant node + // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections + var boxes = GetBoxes(); + for (int i = 0; i < boxes.Count; i++) + { + Box box = boxes[i]; + if (!box.HasAnyConnection) + continue; + if (!getNode.TryGetBox(box.ID, out Box paramBox)) + continue; + + // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true + for (int k = box.Connections.Count - 1; k >= 0; k--) + { + Box connectedBox = box.Connections[k]; + paramBox.CreateConnection(connectedBox); + } + } + + // Add undo actions and remove constant node + var spawnNodeAction = new AddRemoveNodeAction(getNode, true); + var removeConstantAction = new AddRemoveNodeAction(this, false); + Surface.AddBatchedUndoAction(new MultiUndoAction(paramAction, spawnNodeAction, removeConstantAction)); + removeConstantAction.Do(); + } + + private bool OnParameterRenameValidate(string value) + { + if (Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); + } + } + private class EnumNode : SurfaceNode { private EnumComboBox _picker; @@ -356,7 +452,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Bool", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), Description = "Constant boolean value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -389,7 +485,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 2, Title = "Integer", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(int))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(int))), Description = "Constant integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -417,7 +513,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Float", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(float))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(float))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -445,7 +541,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Float2", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Float2))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Float2))), Description = "Constant Float2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -476,7 +572,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 5, Title = "Float3", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), Description = "Constant Float3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -509,7 +605,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 6, Title = "Float4", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Float4))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Float4))), Description = "Constant Float4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), @@ -544,7 +640,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Title = "Color", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Color))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Color))), Description = "RGBA color", Flags = NodeFlags.AllGraphs, Size = new Float2(70, 100), @@ -577,10 +673,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 8, Title = "Rotation", - // TODO: Way too long and ugly - find a better way to add conversion Create = (id, context, arch, groupArch) => - new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Quaternion)), - values => Quaternion.Euler((float)values[0], (float)values[1], (float)values[2])), + new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Quaternion)), values => Quaternion.Euler((float)values[0], (float)values[1], (float)values[2])), Description = "Euler angle rotation", Flags = NodeFlags.AnimGraph | NodeFlags.VisualScriptGraph | NodeFlags.ParticleEmitterGraph, Size = new Float2(110, 60), @@ -605,7 +699,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 9, Title = "String", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(string))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(string))), Description = "Text", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, Size = new Float2(200, 20), @@ -656,8 +750,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 12, Title = "Unsigned Integer", - AlternativeTitles = new[] { "UInt" , "U Int" }, - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), + AlternativeTitles = new[] { "UInt", "U Int" }, + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(170, 20), @@ -697,7 +791,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 15, Title = "Double", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(double))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(double))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -715,7 +809,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 16, Title = "Vector2", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Vector2))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Vector2))), Description = "Constant Vector2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -736,7 +830,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 17, Title = "Vector3", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Vector3))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Vector3))), Description = "Constant Vector3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -759,7 +853,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 18, Title = "Vector4", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Vector4))), + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Vector4))), Description = "Constant Vector4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 85523961f..e35af7192 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -96,7 +96,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Texture", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), + Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), Description = "Two dimensional texture object", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -133,7 +133,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Cube Texture", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(CubeTexture))), + Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(CubeTexture))), Description = "Set of 6 textures arranged in a cube", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -157,7 +157,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Normal Map", - Create = (id, context, arch, groupArch) => new ConvertibleNode(id, context, arch, groupArch, new ScriptType(typeof(NormalMap))), + Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(NormalMap))), Description = "Two dimensional texture object sampled as a normal map", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), diff --git a/Source/Editor/Surface/ConvertibleNode.cs b/Source/Editor/Surface/ConvertibleNode.cs deleted file mode 100644 index 9c4a65ab9..000000000 --- a/Source/Editor/Surface/ConvertibleNode.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Linq; -using FlaxEditor.Scripting; -using FlaxEditor.Surface.Elements; -using FlaxEditor.Surface.Undo; -using FlaxEngine; - -namespace FlaxEditor.Surface -{ - /// - /// A special type of node that adds the functionality to convert nodes to parameters - /// - internal class ConvertibleNode : SurfaceNode - { - private readonly ScriptType _type; - private readonly Func _convertFunction; - - /// - public ConvertibleNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type, Func convertFunction = null) - : base(id, context, nodeArch, groupArch) - { - _type = type; - _convertFunction = convertFunction; - } - - /// - public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) - { - base.OnShowSecondaryContextMenu(menu, location); - - menu.AddSeparator(); - menu.AddButton("Convert to Parameter", OnConvertToParameter); - } - - private void OnConvertToParameter() - { - if (Surface.Owner is not IVisjectSurfaceWindow window) - throw new Exception("Surface owner is not a Visject Surface Window"); - - Asset asset = Surface.Owner.SurfaceAsset; - if (asset == null || !asset.IsLoaded) - { - Editor.LogError("Asset is null or not loaded"); - return; - } - - // Add parameter to editor - var paramIndex = Surface.Parameters.Count; - object initValue = _convertFunction == null ? Values[0] : _convertFunction.Invoke(Values); - var paramAction = new AddRemoveParamAction - { - Window = window, - IsAdd = true, - Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(x)), - Type = _type, - Index = paramIndex, - InitValue = initValue, - }; - paramAction.Do(); - - // Spawn Get Parameter Node based on the added parameter - Guid parameterGuid = Surface.Parameters[paramIndex].ID; - bool undoEnabled = Surface.Undo.Enabled; - Surface.Undo.Enabled = false; - NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); - SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, this.Location, new object[] { parameterGuid }); - Surface.Undo.Enabled = undoEnabled; - - if (node is not Archetypes.Parameters.SurfaceNodeParamsGet getNode) - throw new Exception("Node is not a ParamsGet node!"); - - // Recreate connections of constant node - // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections - var boxes = GetBoxes(); - for (int i = 0; i < boxes.Count; i++) - { - Box box = boxes[i]; - if (!box.HasAnyConnection) - continue; - - if (!getNode.TryGetBox(box.ID, out Box paramBox)) - continue; - - // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true - for (int k = box.Connections.Count - 1; k >= 0; k--) - { - Box connectedBox = box.Connections[k]; - paramBox.CreateConnection(connectedBox); - } - } - - // Add undo actions and remove constant node - var spawnNodeAction = new AddRemoveNodeAction(getNode, true); - var removeConstantAction = new AddRemoveNodeAction(this, false); - - Surface.AddBatchedUndoAction(new MultiUndoAction(paramAction, spawnNodeAction, removeConstantAction)); - removeConstantAction.Do(); - } - - private bool OnParameterRenameValidate(string value) - { - if (Surface.Owner is not IVisjectSurfaceWindow window) - throw new Exception("Surface owner is not a Visject Surface Window"); - return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); - } - } -} diff --git a/Source/Editor/Surface/SurfaceParameter.cs b/Source/Editor/Surface/SurfaceParameter.cs index e74b44eb9..c22e5bde5 100644 --- a/Source/Editor/Surface/SurfaceParameter.cs +++ b/Source/Editor/Surface/SurfaceParameter.cs @@ -55,6 +55,7 @@ namespace FlaxEditor.Surface /// /// The type. /// The name. + /// The initial value to use. Null to use automatic default value. /// The created parameter. public static SurfaceParameter Create(ScriptType type, string name, object initValue = null) { diff --git a/Source/Editor/Surface/VisjectSurfaceWindow.cs b/Source/Editor/Surface/VisjectSurfaceWindow.cs index d2b9822a1..2232b39e5 100644 --- a/Source/Editor/Surface/VisjectSurfaceWindow.cs +++ b/Source/Editor/Surface/VisjectSurfaceWindow.cs @@ -279,7 +279,7 @@ namespace FlaxEditor.Surface public ScriptType Type; /// - /// The value to initialize the parameter with. (Can be null) + /// The value to initialize the parameter with. Can be null to use default one for the parameter type. /// public object InitValue; @@ -1065,7 +1065,6 @@ namespace FlaxEditor.Surface public virtual void OnParamRemoveUndo() { _refreshPropertiesOnLoad = true; - //_propertiesEditor.BuildLayoutOnUpdate(); _propertiesEditor.BuildLayout(); } From 2c5a5acf33978ad90a1450a68b324c20bf34812a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 5 Nov 2023 23:30:39 +0100 Subject: [PATCH 203/546] Add maintaining new param id in undo #1671 --- Source/Editor/Surface/SurfaceParameter.cs | 22 +------------------ Source/Editor/Surface/VisjectSurfaceWindow.cs | 15 ++++++++++++- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/Source/Editor/Surface/SurfaceParameter.cs b/Source/Editor/Surface/SurfaceParameter.cs index c22e5bde5..dae82e111 100644 --- a/Source/Editor/Surface/SurfaceParameter.cs +++ b/Source/Editor/Surface/SurfaceParameter.cs @@ -3,7 +3,6 @@ using System; using FlaxEditor.Scripting; using FlaxEngine; -using FlaxEngine.Utilities; namespace FlaxEditor.Surface { @@ -27,7 +26,7 @@ namespace FlaxEditor.Surface /// /// Parameter unique ID /// - public Guid ID; + public Guid ID = Guid.Empty; /// /// Parameter name @@ -49,24 +48,5 @@ namespace FlaxEditor.Surface /// [NoSerialize, HideInEditor] public readonly SurfaceMeta Meta = new SurfaceMeta(); - - /// - /// Creates the new parameter of the given type. - /// - /// The type. - /// The name. - /// The initial value to use. Null to use automatic default value. - /// The created parameter. - public static SurfaceParameter Create(ScriptType type, string name, object initValue = null) - { - return new SurfaceParameter - { - ID = Guid.NewGuid(), - IsPublic = true, - Name = name, - Type = type, - Value = initValue ?? TypeUtils.GetDefaultValue(type), - }; - } } } diff --git a/Source/Editor/Surface/VisjectSurfaceWindow.cs b/Source/Editor/Surface/VisjectSurfaceWindow.cs index 2232b39e5..cce86d5c0 100644 --- a/Source/Editor/Surface/VisjectSurfaceWindow.cs +++ b/Source/Editor/Surface/VisjectSurfaceWindow.cs @@ -17,6 +17,7 @@ using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Utilities; namespace FlaxEditor.Surface { @@ -258,6 +259,11 @@ namespace FlaxEditor.Surface /// public IVisjectSurfaceWindow Window; + /// + /// The identifier of the parameter. Empty to auto generate it. + /// + public Guid Id = Guid.NewGuid(); + /// /// True if adding, false if removing parameter. /// @@ -309,7 +315,14 @@ namespace FlaxEditor.Surface var type = Type; if (IsAdd && type.Type == typeof(NormalMap)) type = new ScriptType(typeof(Texture)); - var param = SurfaceParameter.Create(type, Name, InitValue); + var param = new SurfaceParameter + { + ID = Id, + IsPublic = true, + Name = Name, + Type = type, + Value = InitValue ?? TypeUtils.GetDefaultValue(type), + }; if (IsAdd && Type.Type == typeof(NormalMap)) param.Value = FlaxEngine.Content.LoadAsyncInternal("Engine/Textures/NormalTexture"); Window.VisjectSurface.Parameters.Insert(Index, param); From d79dd4aaf7bfd189fb0c939b47070e4d4ceeb5a4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 08:41:36 +0100 Subject: [PATCH 204/546] Add undo for `Convert to Parameter` connections change in #1671 --- Source/Editor/Surface/Archetypes/Constants.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index 458fd4af3..e58d917d1 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -75,6 +75,7 @@ namespace FlaxEditor.Surface.Archetypes InitValue = initValue, }; paramAction.Do(); + Surface.AddBatchedUndoAction(paramAction); // Spawn Get Parameter Node based on the added parameter Guid parameterGuid = Surface.Parameters[paramIndex].ID; @@ -85,9 +86,12 @@ namespace FlaxEditor.Surface.Archetypes Surface.Undo.Enabled = undoEnabled; if (node is not Parameters.SurfaceNodeParamsGet getNode) throw new Exception("Node is not a ParamsGet node!"); + Surface.AddBatchedUndoAction(new AddRemoveNodeAction(getNode, true)); // Recreate connections of constant node // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections + var editConnectionsAction1 = new EditNodeConnections(Context, this); + var editConnectionsAction2 = new EditNodeConnections(Context, node); var boxes = GetBoxes(); for (int i = 0; i < boxes.Count; i++) { @@ -104,12 +108,16 @@ namespace FlaxEditor.Surface.Archetypes paramBox.CreateConnection(connectedBox); } } + editConnectionsAction1.End(); + editConnectionsAction2.End(); + Surface.AddBatchedUndoAction(editConnectionsAction1); + Surface.AddBatchedUndoAction(editConnectionsAction2); // Add undo actions and remove constant node - var spawnNodeAction = new AddRemoveNodeAction(getNode, true); var removeConstantAction = new AddRemoveNodeAction(this, false); - Surface.AddBatchedUndoAction(new MultiUndoAction(paramAction, spawnNodeAction, removeConstantAction)); + Surface.AddBatchedUndoAction(removeConstantAction); removeConstantAction.Do(); + Surface.MarkAsEdited(); } private bool OnParameterRenameValidate(string value) From 7e6c8b7b32cbf13d8ce9d17eba3cd9533cc45ea5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 10:33:11 +0100 Subject: [PATCH 205/546] Codestyle fix #1779 --- .../Scripting/Attributes/PluginLoadOrder.cs | 4 +- .../Scripting/Plugins/PluginManager.cpp | 145 ++++++------------ 2 files changed, 53 insertions(+), 96 deletions(-) diff --git a/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs b/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs index d1d02d024..18a6a1dac 100644 --- a/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs +++ b/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace FlaxEngine; /// -/// This attribute allows for specifying initialization and deinitialization order for plugins +/// This attribute allows for specifying initialization and deinitialization order for plugins. /// [Serializable] [AttributeUsage(AttributeTargets.Class)] diff --git a/Source/Engine/Scripting/Plugins/PluginManager.cpp b/Source/Engine/Scripting/Plugins/PluginManager.cpp index 77a3467fd..0f906c68e 100644 --- a/Source/Engine/Scripting/Plugins/PluginManager.cpp +++ b/Source/Engine/Scripting/Plugins/PluginManager.cpp @@ -83,8 +83,47 @@ namespace PluginManagerImpl void OnAssemblyUnloading(MAssembly* assembly); void OnBinaryModuleLoaded(BinaryModule* module); void OnScriptsReloading(); - Array SortGamePlugins(Array& plugins, MClass* pluginLoadOrderAttribute, MField* typeField); - Array SortPlugins(Array plugins, MClass* pluginLoadOrderAttribute, MField* typeField); + + template + Array SortPlugins(Array plugins, MClass* pluginLoadOrderAttribute, MField* typeField) + { + // Sort plugins + Array newPlugins; + for (int i = 0; i < plugins.Count(); i++) + { + PluginType* plugin = plugins[i]; + int32 insertIndex = -1; + for (int j = 0; j < newPlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = newPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + typeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + newPlugins.Add(plugin); + else + newPlugins.Insert(insertIndex, plugin); + } + return newPlugins; + } } using namespace PluginManagerImpl; @@ -227,7 +266,7 @@ void PluginManagerImpl::OnAssemblyUnloading(MAssembly* assembly) } } - auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) { @@ -289,7 +328,7 @@ void PluginManagerImpl::OnScriptsReloading() } } - auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) { @@ -304,88 +343,6 @@ void PluginManagerImpl::OnScriptsReloading() PluginManager::PluginsChanged(); } -Array PluginManagerImpl::SortGamePlugins(Array& plugins, MClass* pluginLoadOrderAttribute, MField* typeField) -{ - // Sort game plugins - Array newPlugins; - for(int i = 0; i < plugins.Count(); i++) - { - GamePlugin* plugin = plugins[i]; - // Sort game plugin as needed - int insertIndex = -1; - for(int j = 0; j < newPlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = newPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - typeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - newPlugins.Add(plugin); - else - newPlugins.Insert(insertIndex, plugin); - } - return newPlugins; -} - -Array PluginManagerImpl::SortPlugins(Array plugins, MClass* pluginLoadOrderAttribute, MField* typeField) -{ - // Sort plugins - Array newPlugins; - for(int i = 0; i < plugins.Count(); i++) - { - Plugin* plugin = plugins[i]; - // Sort plugin as needed - int insertIndex = -1; - for(int j = 0; j < newPlugins.Count(); j++) - { - // Get first instance where a game plugin needs another one before it - auto attribute = newPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); - if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) - continue; - - // Check if attribute references a valid class - MTypeObject* refType = nullptr; - typeField->GetValue(attribute, &refType); - if (refType == nullptr) - continue; - - MType* type = INTERNAL_TYPE_OBJECT_GET(refType); - if (type == nullptr) - continue; - MClass* typeClass = MCore::Type::GetClass(type); - - if (plugin->GetClass() == typeClass) - { - insertIndex = j; - break; - } - } - if (insertIndex == -1) - newPlugins.Add(plugin); - else - newPlugins.Insert(insertIndex, plugin); - } - return newPlugins; -} - bool PluginManagerService::Init() { // Process already loaded modules @@ -409,7 +366,7 @@ bool PluginManagerService::Init() PluginManagerService::InvokeInitialize(plugin); } #endif - + #if USE_EDITOR auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, afterTypeField); @@ -419,7 +376,7 @@ bool PluginManagerService::Init() PluginManagerService::InvokeInitialize(plugin); } #endif - + // Register for new binary modules load actions Scripting::BinaryModuleLoaded.Bind(&OnBinaryModuleLoaded); Scripting::ScriptsReloading.Bind(&OnScriptsReloading); @@ -438,28 +395,28 @@ void PluginManagerService::Dispose() if (pluginsCount == 0) return; LOG(Info, "Unloading {0} plugins", pluginsCount); - + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); ASSERT(beforeTypeField); auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, beforeTypeField); - for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) { auto plugin = editorPlugins[i]; InvokeDeinitialize(plugin); EditorPlugins.Remove(plugin); } - - auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); + + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) { auto plugin = gamePlugins[i]; InvokeDeinitialize(plugin); GamePlugins.Remove(plugin); } + PluginManager::PluginsChanged(); } @@ -531,7 +488,7 @@ void PluginManager::InitializeGamePlugins() auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); ASSERT(afterTypeField); - auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); for (int32 i = 0; i < gamePlugins.Count(); i++) { PluginManagerService::InvokeInitialize(gamePlugins[i]); @@ -547,7 +504,7 @@ void PluginManager::DeinitializeGamePlugins() auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); ASSERT(beforeTypeField); - auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); for (int32 i = gamePlugins.Count() - 1; i >= 0; i--) { PluginManagerService::InvokeDeinitialize(gamePlugins[i]); From f703e7de776d28aa5d9225751334fec16b75f538 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 11:05:31 +0100 Subject: [PATCH 206/546] Fix compile error --- Source/Engine/Scripting/Plugins/PluginManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Scripting/Plugins/PluginManager.cpp b/Source/Engine/Scripting/Plugins/PluginManager.cpp index 0f906c68e..87965fac9 100644 --- a/Source/Engine/Scripting/Plugins/PluginManager.cpp +++ b/Source/Engine/Scripting/Plugins/PluginManager.cpp @@ -358,9 +358,9 @@ bool PluginManagerService::Init() ASSERT(afterTypeField); #if !USE_EDITOR - auto gamePlugins = SortGamePlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); - // Initalize game plugins + // Initialize game plugins for (auto plugin : gamePlugins) { PluginManagerService::InvokeInitialize(plugin); From a0d4714a0fdee38eff496586eac1db4e5f756c1f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 12:23:17 +0100 Subject: [PATCH 207/546] Add always logging unhandled c# exception #1861 --- Source/Engine/Scripting/Scripting.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Scripting/Scripting.cs b/Source/Engine/Scripting/Scripting.cs index 67ba854e0..0630985f3 100644 --- a/Source/Engine/Scripting/Scripting.cs +++ b/Source/Engine/Scripting/Scripting.cs @@ -135,13 +135,10 @@ namespace FlaxEngine { if (e.ExceptionObject is Exception exception) { + Debug.LogError($"Unhandled Exception: {exception.Message}"); + Debug.LogException(exception); if (e.IsTerminating && !System.Diagnostics.Debugger.IsAttached) Platform.Fatal($"Unhandled Exception: {exception}"); - else - { - Debug.LogError($"Unhandled Exception: {exception.Message}"); - Debug.LogException(exception); - } } } From 0930671e909e51454b20323d5d78e8e1d2ebc36b Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 6 Nov 2023 06:25:17 -0600 Subject: [PATCH 208/546] Cache main panel, cleanup cached variables. --- Source/Engine/UI/GUI/Common/Dropdown.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index a6c79ea3f..92067398d 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -28,15 +28,20 @@ namespace FlaxEngine.GUI /// public ContainerControl SelectedControl = null; + /// + /// The main panel used to hold the items. + /// + public Panel MainPanel = null; + /// public override void OnEndContainsFocus() { base.OnEndContainsFocus(); // Dont lose focus when using panel. Does prevent LostFocus even from being called if clicking inside of the panel. - if (Children[0] is Panel panel && panel.IsMouseOver && !panel.ContainsFocus) + if (MainPanel != null && MainPanel.IsMouseOver && !MainPanel.ContainsFocus) { - panel.Focus(); + MainPanel.Focus(); return; } // Call event after this 'focus contains flag' propagation ends to prevent focus issues @@ -136,6 +141,8 @@ namespace FlaxEngine.GUI public override void OnDestroy() { LostFocus = null; + MainPanel = null; + SelectedControl = null; base.OnDestroy(); } @@ -442,6 +449,7 @@ namespace FlaxEngine.GUI AutoFocus = true, Parent = popup, }; + popup.MainPanel = panel; var container = new VerticalPanel { @@ -603,8 +611,8 @@ namespace FlaxEngine.GUI _popup.Parent = root; _popup.Focus(); _popup.StartMouseCapture(); - if (_popup.SelectedControl != null && _popup.Children[0] is Panel panel) - panel.ScrollViewTo(_popup.SelectedControl, true); + if (_popup.SelectedControl != null && _popup.MainPanel != null) + _popup.MainPanel.ScrollViewTo(_popup.SelectedControl, true); OnPopupShow(); } From e57e86cde1c9ee963c7f3afcdc5effa0b933876d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 13:54:01 +0100 Subject: [PATCH 209/546] Fix spawned ui canvas scaller to use the same transform as CanvasScaler #1819 --- Source/Editor/SceneGraph/Actors/UICanvasNode.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs index 41e1c77c6..ed6fc08d6 100644 --- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs +++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs @@ -34,6 +34,7 @@ namespace FlaxEditor.SceneGraph.Actors var uiControl = new UIControl { Name = "Canvas Scalar", + Transform = Actor.Transform, Control = new CanvasScaler() }; Root.Spawn(uiControl, Actor); From e22fa20dc1063d3553b26a2323b7de961d09fe6d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 14:06:10 +0100 Subject: [PATCH 210/546] Minor fixes to #1740 --- Source/Editor/Modules/UIModule.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 9546f3570..3386d411c 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -528,8 +528,7 @@ namespace FlaxEditor.Modules _menuFileSaveAll = cm.AddButton("Save All", inputOptions.Save, Editor.SaveAll); _menuFileSaveScenes = cm.AddButton("Save scenes", inputOptions.SaveScenes, Editor.Scene.SaveScenes); _menuFileCloseScenes = cm.AddButton("Close scenes", inputOptions.CloseScenes, Editor.Scene.CloseAllScenes); - cm.AddSeparator(); - _menuFileReloadScenes = cm.AddButton("Reload Scenes", Editor.Scene.ReloadScenes); + _menuFileReloadScenes = cm.AddButton("Reload scenes", Editor.Scene.ReloadScenes); cm.AddSeparator(); _menuFileOpenScriptsProject = cm.AddButton("Open scripts project", inputOptions.OpenScriptsProject, Editor.CodeEditing.OpenSolution); _menuFileGenerateScriptsProjectFiles = cm.AddButton("Generate scripts project files", inputOptions.GenerateScriptsProject, Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync); @@ -833,6 +832,7 @@ namespace FlaxEditor.Modules _menuFileSaveScenes.Enabled = hasOpenedScene; _menuFileCloseScenes.Enabled = hasOpenedScene; + _menuFileReloadScenes.Enabled = hasOpenedScene; _menuFileGenerateScriptsProjectFiles.Enabled = !Editor.ProgressReporting.GenerateScriptsProjectFiles.IsActive; c.PerformLayout(); From 1e9ded55b0948e88ab5bc87e5d57af276dccff3a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 14:09:39 +0100 Subject: [PATCH 211/546] Fix missing xml doc --- Source/Editor/Surface/VisjectSurfaceContext.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs index 54c456d04..0886996b6 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.cs @@ -306,6 +306,7 @@ namespace FlaxEditor.Surface /// The surface area to create comment. /// The comment title. /// The comment color. + /// The comment order or -1 to use default. /// The comment object public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1) { @@ -325,6 +326,7 @@ namespace FlaxEditor.Surface /// The surface area to create comment. /// The comment title. /// The comment color. + /// The comment order or -1 to use default. /// The comment object public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1) { From 9a9e32d4c5aa7bcb414b3c21560b329763ca94aa Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 14:20:30 +0100 Subject: [PATCH 212/546] Fix incorrect directional light shadows influence on secondary render views #1758 --- Source/Engine/Renderer/Renderer.cpp | 1 + Source/Engine/Renderer/ShadowsPass.cpp | 9 ++++++++- Source/Engine/Renderer/ShadowsPass.h | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp index edac958bc..833daed8e 100644 --- a/Source/Engine/Renderer/Renderer.cpp +++ b/Source/Engine/Renderer/Renderer.cpp @@ -343,6 +343,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont // Prepare renderContext.View.Prepare(renderContext); renderContext.Buffers->Prepare(); + ShadowsPass::Instance()->Prepare(); // Build batch of render contexts (main view and shadow projections) { diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index 06381d826..ba326b728 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -549,10 +549,17 @@ void ShadowsPass::Dispose() SAFE_DELETE_GPU_RESOURCE(_shadowMapCube); } +void ShadowsPass::Prepare() +{ + // Clear cached data + _shadowData.Clear(); + LastDirLightIndex = -1; + LastDirLightShadowMap = nullptr; +} + void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch& renderContextBatch) { PROFILE_CPU(); - _shadowData.Clear(); auto& view = renderContext.View; // Update shadow map diff --git a/Source/Engine/Renderer/ShadowsPass.h b/Source/Engine/Renderer/ShadowsPass.h index e58d68d62..d3589d008 100644 --- a/Source/Engine/Renderer/ShadowsPass.h +++ b/Source/Engine/Renderer/ShadowsPass.h @@ -94,6 +94,8 @@ public: LightShadowData LastDirLight; public: + void Prepare(); + /// /// Setups the shadows rendering for batched scene drawing. Checks which lights will cast a shadow. /// From 51c0a6e10049114c5e74cce9c2ec4705818b1378 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 14:32:04 +0100 Subject: [PATCH 213/546] Fix crash regression on `Dictionary` and `HashSet` capacity set to 0 when it contains elements --- Source/Engine/Core/Collections/Dictionary.h | 4 +++- Source/Engine/Core/Collections/HashSet.h | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h index 729bc80aa..4d65a4123 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -534,7 +534,7 @@ public: } _size = capacity; Bucket* oldData = oldAllocation.Get(); - if (oldElementsCount != 0 && preserveContents) + if (oldElementsCount != 0 && capacity != 0 && preserveContents) { FindPositionResult pos; for (int32 i = 0; i < oldSize; i++) @@ -543,6 +543,7 @@ public: if (oldBucket.IsOccupied()) { FindPosition(oldBucket.Key, pos); + ASSERT(pos.FreeSlotIndex != -1); Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1); Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1); @@ -942,6 +943,7 @@ private: if (oldBucket.IsOccupied()) { FindPosition(oldBucket.Key, pos); + ASSERT(pos.FreeSlotIndex != -1); Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1); Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1); diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index d3fcf461d..4a4f3e924 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -413,7 +413,7 @@ public: } _size = capacity; Bucket* oldData = oldAllocation.Get(); - if (oldElementsCount != 0 && preserveContents) + if (oldElementsCount != 0 && capacity != 0 && preserveContents) { FindPositionResult pos; for (int32 i = 0; i < oldSize; i++) @@ -422,6 +422,7 @@ public: if (oldBucket.IsOccupied()) { FindPosition(oldBucket.Item, pos); + ASSERT(pos.FreeSlotIndex != -1); Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1); bucket->_state = Bucket::Occupied; @@ -738,6 +739,7 @@ private: if (oldBucket.IsOccupied()) { FindPosition(oldBucket.Item, pos); + ASSERT(pos.FreeSlotIndex != -1); Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1); bucket->_state = Bucket::Occupied; From c025b4414cf6ff67d7bce6f9dbd93ccc07d2b36d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 14:42:29 +0100 Subject: [PATCH 214/546] Add support for unlimited window size if `MaximumSize` is set to Zero #1824 --- .../GUI/Docking/FloatWindowDockPanel.cs | 2 +- .../Engine/Platform/CreateWindowSettings.cs | 2 +- Source/Engine/Platform/CreateWindowSettings.h | 4 +-- Source/Engine/Platform/Linux/LinuxWindow.cpp | 4 +-- Source/Engine/Platform/Mac/MacWindow.cpp | 3 +- .../Engine/Platform/Windows/WindowsWindow.cpp | 29 ++++++++++--------- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index f8ffe62ad..7bb85751a 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -72,7 +72,7 @@ namespace FlaxEditor.GUI.Docking settings.Size = size; settings.Position = location; settings.MinimumSize = new Float2(1); - settings.MaximumSize = new Float2(4096); + settings.MaximumSize = Float2.Zero; // Unlimited size settings.Fullscreen = false; settings.HasBorder = true; settings.SupportsTransparency = false; diff --git a/Source/Engine/Platform/CreateWindowSettings.cs b/Source/Engine/Platform/CreateWindowSettings.cs index d4d9ce727..8f7cd8c0d 100644 --- a/Source/Engine/Platform/CreateWindowSettings.cs +++ b/Source/Engine/Platform/CreateWindowSettings.cs @@ -12,7 +12,7 @@ namespace FlaxEngine Position = new Float2(100, 100), Size = new Float2(640, 480), MinimumSize = Float2.One, - MaximumSize = new Float2(4100, 4100), + MaximumSize = Float2.Zero, // Unlimited size StartPosition = WindowStartPosition.CenterParent, HasBorder = true, ShowInTaskbar = true, diff --git a/Source/Engine/Platform/CreateWindowSettings.h b/Source/Engine/Platform/CreateWindowSettings.h index 1ff596df9..9c543a05d 100644 --- a/Source/Engine/Platform/CreateWindowSettings.h +++ b/Source/Engine/Platform/CreateWindowSettings.h @@ -59,9 +59,9 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(CreateWindowSettings); API_FIELD() Float2 MinimumSize = Float2(1, 1); /// - /// The maximum size. + /// The maximum size. Set to 0 to use unlimited size. /// - API_FIELD() Float2 MaximumSize = Float2(8192, 4096); + API_FIELD() Float2 MaximumSize = Float2(0, 0); /// /// The start position mode. diff --git a/Source/Engine/Platform/Linux/LinuxWindow.cpp b/Source/Engine/Platform/Linux/LinuxWindow.cpp index f308a0143..b3bae0276 100644 --- a/Source/Engine/Platform/Linux/LinuxWindow.cpp +++ b/Source/Engine/Platform/Linux/LinuxWindow.cpp @@ -150,9 +150,9 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) { // Set resizing range hints.min_width = (int)settings.MinimumSize.X; - hints.max_width = (int)settings.MaximumSize.X; + hints.max_width = settings.MaximumSize.X > 0 ? (int)settings.MaximumSize.X : MAX_uint16; hints.min_height = (int)settings.MinimumSize.Y; - hints.max_height = (int)settings.MaximumSize.Y; + hints.max_height = settings.MaximumSize.Y > 0 ? (int)settings.MaximumSize.Y : MAX_uint16; hints.flags |= USSize; } // honor the WM placement except for manual (overriding) placements diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index 38e8c95df..4c392fc4e 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -751,7 +751,8 @@ MacWindow::MacWindow(const CreateWindowSettings& settings) [window setWindow:this]; [window setReleasedWhenClosed:NO]; [window setMinSize:NSMakeSize(settings.MinimumSize.X, settings.MinimumSize.Y)]; - [window setMaxSize:NSMakeSize(settings.MaximumSize.X, settings.MaximumSize.Y)]; + if (settings.MaximumSize.SumValues() > 0) + [window setMaxSize:NSMakeSize(settings.MaximumSize.X, settings.MaximumSize.Y)]; [window setOpaque:!settings.SupportsTransparency]; [window setContentView:view]; if (settings.AllowInput) diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp index ff65e9864..174160922 100644 --- a/Source/Engine/Platform/Windows/WindowsWindow.cpp +++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp @@ -1056,22 +1056,23 @@ LRESULT WindowsWindow::WndProc(UINT msg, WPARAM wParam, LPARAM lParam) case WM_GETMINMAXINFO: { const auto minMax = reinterpret_cast(lParam); - - int32 borderWidth = 0, borderHeight = 0; - if (_settings.HasBorder) - { - const DWORD windowStyle = GetWindowLongW(_handle, GWL_STYLE); - const DWORD windowExStyle = GetWindowLongW(_handle, GWL_EXSTYLE); - RECT borderRect = { 0, 0, 0, 0 }; - AdjustWindowRectEx(&borderRect, windowStyle, false, windowExStyle); - borderWidth = borderRect.right - borderRect.left; - borderHeight = borderRect.bottom - borderRect.top; - } - minMax->ptMinTrackSize.x = (int32)_settings.MinimumSize.X; minMax->ptMinTrackSize.y = (int32)_settings.MinimumSize.Y; - minMax->ptMaxTrackSize.x = (int32)_settings.MaximumSize.X + borderWidth; - minMax->ptMaxTrackSize.y = (int32)_settings.MaximumSize.Y + borderHeight; + if (_settings.MaximumSize.SumValues() > 0) + { + int32 borderWidth = 0, borderHeight = 0; + if (_settings.HasBorder) + { + const DWORD windowStyle = GetWindowLongW(_handle, GWL_STYLE); + const DWORD windowExStyle = GetWindowLongW(_handle, GWL_EXSTYLE); + RECT borderRect = { 0, 0, 0, 0 }; + AdjustWindowRectEx(&borderRect, windowStyle, false, windowExStyle); + borderWidth = borderRect.right - borderRect.left; + borderHeight = borderRect.bottom - borderRect.top; + } + minMax->ptMaxTrackSize.x = (int32)_settings.MaximumSize.X + borderWidth; + minMax->ptMaxTrackSize.y = (int32)_settings.MaximumSize.Y + borderHeight; + } // Include Windows task bar size into maximized tool window WINDOWPLACEMENT e; From b47420f2329f019f28764269d0b90c6445faecad Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 16:56:15 +0100 Subject: [PATCH 215/546] Compact various source code chunks --- Source/Engine/Engine/Engine.cpp | 9 - .../Windows/WindowsWindow.DragDrop.cpp | 698 ------------------ .../Engine/Platform/Windows/WindowsWindow.cpp | 662 ++++++++++++++++- Source/Engine/Scripting/Scripting.cpp | 10 +- 4 files changed, 662 insertions(+), 717 deletions(-) delete mode 100644 Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index 1a57c4f10..c0cfad34c 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -20,7 +20,6 @@ #include "Engine/Threading/MainThreadTask.h" #include "Engine/Threading/ThreadRegistry.h" #include "Engine/Graphics/GPUDevice.h" -#include "Engine/Scripting/ManagedCLR/MCore.h" #include "Engine/Scripting/ScriptingType.h" #include "Engine/Content/Content.h" #include "Engine/Content/JsonAsset.h" @@ -327,14 +326,6 @@ void Engine::OnUpdate() // Update services EngineService::OnUpdate(); - -#ifdef USE_NETCORE - // Force GC to run in background periodically to avoid large blocking collections causing hitches - if (Time::Update.TicksCount % 60 == 0) - { - MCore::GC::Collect(MCore::GC::MaxGeneration(), MGCCollectionMode::Forced, false, false); - } -#endif } void Engine::OnLateUpdate() diff --git a/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp b/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp deleted file mode 100644 index b6e636697..000000000 --- a/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp +++ /dev/null @@ -1,698 +0,0 @@ -// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. - -#if PLATFORM_WINDOWS - -#include "WindowsWindow.h" - -#if USE_EDITOR - -#include "Engine/Core/Collections/Array.h" -#include "Engine/Engine/Engine.h" -#include "Engine/Platform/IGuiData.h" -#include "Engine/Input/Input.h" -#include "Engine/Input/Mouse.h" -#include "Engine/Threading/ThreadPoolTask.h" -#include "Engine/Threading/ThreadPool.h" -#include "Engine/Scripting/Scripting.h" -#include "Engine/Scripting/ManagedCLR/MDomain.h" -#include "../Win32/IncludeWindowsHeaders.h" -#include -#include - -HGLOBAL duplicateGlobalMem(HGLOBAL hMem) -{ - auto len = GlobalSize(hMem); - auto source = GlobalLock(hMem); - auto dest = GlobalAlloc(GMEM_FIXED, len); - Platform::MemoryCopy(dest, source, len); - GlobalUnlock(hMem); - return dest; -} - -DWORD dropEffect2OleEnum(DragDropEffect effect) -{ - DWORD result; - switch (effect) - { - case DragDropEffect::None: - result = DROPEFFECT_NONE; - break; - case DragDropEffect::Copy: - result = DROPEFFECT_COPY; - break; - case DragDropEffect::Move: - result = DROPEFFECT_MOVE; - break; - case DragDropEffect::Link: - result = DROPEFFECT_LINK; - break; - default: - result = DROPEFFECT_NONE; - break; - } - return result; -} - -DragDropEffect dropEffectFromOleEnum(DWORD effect) -{ - DragDropEffect result; - switch (effect) - { - case DROPEFFECT_NONE: - result = DragDropEffect::None; - break; - case DROPEFFECT_COPY: - result = DragDropEffect::Copy; - break; - case DROPEFFECT_MOVE: - result = DragDropEffect::Move; - break; - case DROPEFFECT_LINK: - result = DragDropEffect::Link; - break; - default: - result = DragDropEffect::None; - break; - } - return result; -} - -HANDLE StringToHandle(const StringView& str) -{ - // Allocate and lock a global memory buffer. - // Make it fixed data so we don't have to use GlobalLock - const int32 length = str.Length(); - char* ptr = static_cast(GlobalAlloc(GMEM_FIXED, length + 1)); - - // Copy the string into the buffer as ANSI text - StringUtils::ConvertUTF162ANSI(str.Get(), ptr, length); - ptr[length] = '\0'; - - return ptr; -} - -void DeepCopyFormatEtc(FORMATETC* dest, FORMATETC* source) -{ - // Copy the source FORMATETC into dest - *dest = *source; - - if (source->ptd) - { - // Allocate memory for the DVTARGETDEVICE if necessary - dest->ptd = static_cast(CoTaskMemAlloc(sizeof(DVTARGETDEVICE))); - - // Copy the contents of the source DVTARGETDEVICE into dest->ptd - *(dest->ptd) = *(source->ptd); - } -} - -HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc); - -/// -/// GUI data for Windows platform -/// -class WindowsGuiData : public IGuiData -{ -private: - - Type _type; - Array _data; - -public: - - /// - /// Init - /// - WindowsGuiData() - : _type(Type::Unknown) - , _data(1) - { - } - -public: - - /// - /// Init from Ole IDataObject - /// - /// Object - void Init(IDataObject* pDataObj) - { - // Temporary data - FORMATETC fmtetc; - STGMEDIUM stgmed; - - // Clear - _type = Type::Unknown; - _data.Clear(); - - // Check type - fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; - if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) - { - // Text - _type = Type::Text; - - // Get data - char* text = static_cast(GlobalLock(stgmed.hGlobal)); - _data.Add(String(text)); - GlobalUnlock(stgmed.hGlobal); - ReleaseStgMedium(&stgmed); - } - else - { - fmtetc = { CF_UNICODETEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; - if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) - { - // Unicode Text - _type = Type::Text; - - // Get data - Char* text = static_cast(GlobalLock(stgmed.hGlobal)); - _data.Add(String(text)); - GlobalUnlock(stgmed.hGlobal); - ReleaseStgMedium(&stgmed); - } - else - { - fmtetc = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; - if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) - { - // Files - _type = Type::Files; - - // Get data - Char item[MAX_PATH]; - HDROP hdrop = static_cast(GlobalLock(stgmed.hGlobal)); - UINT filesCount = DragQueryFileW(hdrop, 0xFFFFFFFF, nullptr, 0); - for (UINT i = 0; i < filesCount; i++) - { - if (DragQueryFileW(hdrop, i, item, MAX_PATH) != 0) - { - _data.Add(String(item)); - } - } - GlobalUnlock(stgmed.hGlobal); - ReleaseStgMedium(&stgmed); - } - } - } - } - -public: - - // [IGuiData] - Type GetType() const override - { - return _type; - } - - String GetAsText() const override - { - String result; - if (_type == Type::Text) - { - result = _data[0]; - } - return result; - } - - void GetAsFiles(Array* files) const override - { - if (_type == Type::Files) - { - files->Add(_data); - } - } -}; - -/// -/// Tool class for Windows Ole support -/// -class WindowsEnumFormatEtc : public IEnumFORMATETC -{ -private: - - ULONG _refCount; - ULONG _index; - ULONG _formatsCount; - FORMATETC* _formatEtc; - -public: - - WindowsEnumFormatEtc(FORMATETC* pFormatEtc, int32 nNumFormats) - : _refCount(1) - , _index(0) - , _formatsCount(nNumFormats) - , _formatEtc(nullptr) - { - // Allocate memory - _formatEtc = new FORMATETC[nNumFormats]; - - // Copy the FORMATETC structures - for (int32 i = 0; i < nNumFormats; i++) - { - DeepCopyFormatEtc(&_formatEtc[i], &pFormatEtc[i]); - } - } - - ~WindowsEnumFormatEtc() - { - if (_formatEtc) - { - for (uint32 i = 0; i < _formatsCount; i++) - { - if (_formatEtc[i].ptd) - { - CoTaskMemFree(_formatEtc[i].ptd); - } - } - - delete[] _formatEtc; - } - } - -public: - - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR *__RPC_FAR * ppvObject) override - { - // Check to see what interface has been requested - if (riid == IID_IEnumFORMATETC || riid == IID_IUnknown) - { - AddRef(); - *ppvObject = this; - return S_OK; - } - - // No interface - *ppvObject = nullptr; - return E_NOINTERFACE; - } - - ULONG STDMETHODCALLTYPE AddRef() override - { - _InterlockedIncrement(&_refCount); - return _refCount; - } - - ULONG STDMETHODCALLTYPE Release() override - { - ULONG ulRefCount = _InterlockedDecrement(&_refCount); - if (_refCount == 0) - { - delete this; - } - return ulRefCount; - } - - // [IEnumFormatEtc] - HRESULT STDMETHODCALLTYPE Next(ULONG celt, FORMATETC* pFormatEtc, ULONG* pceltFetched) override - { - ULONG copied = 0; - - // validate arguments - if (celt == 0 || pFormatEtc == nullptr) - return E_INVALIDARG; - - // copy FORMATETC structures into caller's buffer - while (_index < _formatsCount && copied < celt) - { - DeepCopyFormatEtc(&pFormatEtc[copied], &_formatEtc[_index]); - copied++; - _index++; - } - - // store result - if (pceltFetched != nullptr) - *pceltFetched = copied; - - // did we copy all that was requested? - return (copied == celt) ? S_OK : S_FALSE; - } - - HRESULT STDMETHODCALLTYPE Skip(ULONG celt) override - { - _index += celt; - return (_index <= _formatsCount) ? S_OK : S_FALSE; - } - - HRESULT STDMETHODCALLTYPE Reset() override - { - _index = 0; - return S_OK; - } - - HRESULT STDMETHODCALLTYPE Clone(IEnumFORMATETC** ppEnumFormatEtc) override - { - HRESULT result; - - // Make a duplicate enumerator - result = CreateEnumFormatEtc(_formatsCount, _formatEtc, ppEnumFormatEtc); - - if (result == S_OK) - { - // Manually set the index state - static_cast(*ppEnumFormatEtc)->_index = _index; - } - - return result; - } -}; - -HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc) -{ - if (nNumFormats == 0 || pFormatEtc == nullptr || ppEnumFormatEtc == nullptr) - return E_INVALIDARG; - - *ppEnumFormatEtc = new WindowsEnumFormatEtc(pFormatEtc, nNumFormats); - - return (*ppEnumFormatEtc) ? S_OK : E_OUTOFMEMORY; -} - -/// -/// Drag drop source and data container for Ole -/// -class WindowsDragSource : public IDataObject, public IDropSource -{ -private: - - ULONG _refCount; - int32 _formatsCount; - FORMATETC* _formatEtc; - STGMEDIUM* _stgMedium; - -public: - - WindowsDragSource(FORMATETC* fmtetc, STGMEDIUM* stgmed, int32 count) - : _refCount(1) - , _formatsCount(count) - , _formatEtc(nullptr) - , _stgMedium(nullptr) - { - // Allocate memory - _formatEtc = new FORMATETC[count]; - _stgMedium = new STGMEDIUM[count]; - - // Copy descriptors - for (int32 i = 0; i < count; i++) - { - _formatEtc[i] = fmtetc[i]; - _stgMedium[i] = stgmed[i]; - } - } - - virtual ~WindowsDragSource() - { - if (_formatEtc) - delete[] _formatEtc; - if (_stgMedium) - delete[] _stgMedium; - } - -public: - - // [IUnknown] - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR *__RPC_FAR * ppvObject) override - { - // Check to see what interface has been requested - if (riid == IID_IDataObject || riid == IID_IUnknown || riid == IID_IDropSource) - { - AddRef(); - *ppvObject = this; - return S_OK; - } - - // No interface - *ppvObject = nullptr; - return E_NOINTERFACE; - } - - ULONG STDMETHODCALLTYPE AddRef() override - { - _InterlockedIncrement(&_refCount); - return _refCount; - } - - ULONG STDMETHODCALLTYPE Release() override - { - ULONG ulRefCount = _InterlockedDecrement(&_refCount); - if (_refCount == 0) - { - delete this; - } - return ulRefCount; - } - - // [IDropSource] - HRESULT STDMETHODCALLTYPE QueryContinueDrag(_In_ BOOL fEscapePressed, _In_ DWORD grfKeyState) override - { - // If the Escape key has been pressed since the last call, cancel the drop - if (fEscapePressed == TRUE || grfKeyState & MK_RBUTTON) - return DRAGDROP_S_CANCEL; - - // If the LeftMouse button has been released, then do the drop! - if ((grfKeyState & MK_LBUTTON) == 0) - return DRAGDROP_S_DROP; - - // Continue with the drag-drop - return S_OK; - } - - HRESULT STDMETHODCALLTYPE GiveFeedback(_In_ DWORD dwEffect) override - { - // TODO: allow to use custom mouse cursor during drop and drag operation - return DRAGDROP_S_USEDEFAULTCURSORS; - } - - // [IDataObject] - HRESULT STDMETHODCALLTYPE GetData(_In_ FORMATETC* pformatetcIn, _Out_ STGMEDIUM* pmedium) override - { - if (pformatetcIn == nullptr || pmedium == nullptr) - return E_INVALIDARG; - - // Try to match the specified FORMATETC with one of our supported formats - int32 index = lookupFormatEtc(pformatetcIn); - if (index == INVALID_INDEX) - return DV_E_FORMATETC; - - // Found a match - transfer data into supplied storage medium - pmedium->tymed = _formatEtc[index].tymed; - pmedium->pUnkForRelease = nullptr; - - // Copy the data into the caller's storage medium - switch (_formatEtc[index].tymed) - { - case TYMED_HGLOBAL: - pmedium->hGlobal = duplicateGlobalMem(_stgMedium[index].hGlobal); - break; - - default: - return DV_E_FORMATETC; - } - - return S_OK; - } - - HRESULT STDMETHODCALLTYPE GetDataHere(_In_ FORMATETC* pformatetc, _Inout_ STGMEDIUM* pmedium) override - { - return DATA_E_FORMATETC; - } - - HRESULT STDMETHODCALLTYPE QueryGetData(__RPC__in_opt FORMATETC* pformatetc) override - { - return lookupFormatEtc(pformatetc) == INVALID_INDEX ? DV_E_FORMATETC : S_OK; - } - - HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(__RPC__in_opt FORMATETC* pformatectIn, __RPC__out FORMATETC* pformatetcOut) override - { - // Apparently we have to set this field to NULL even though we don't do anything else - pformatetcOut->ptd = nullptr; - return E_NOTIMPL; - } - - HRESULT STDMETHODCALLTYPE SetData(_In_ FORMATETC* pformatetc, _In_ STGMEDIUM* pmedium, BOOL fRelease) override - { - return E_NOTIMPL; - } - - HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD dwDirection, __RPC__deref_out_opt IEnumFORMATETC** ppenumFormatEtc) override - { - // Only the get direction is supported for OLE - if (dwDirection == DATADIR_GET) - { - // TODO: use SHCreateStdEnumFmtEtc API call - return CreateEnumFormatEtc(_formatsCount, _formatEtc, ppenumFormatEtc); - } - - // The direction specified is not supported for drag+drop - return E_NOTIMPL; - } - - HRESULT STDMETHODCALLTYPE DAdvise(__RPC__in FORMATETC* pformatetc, DWORD advf, __RPC__in_opt IAdviseSink* pAdvSink, __RPC__out DWORD* pdwConnection) override - { - return OLE_E_ADVISENOTSUPPORTED; - } - - HRESULT STDMETHODCALLTYPE DUnadvise(DWORD dwConnection) override - { - return OLE_E_ADVISENOTSUPPORTED; - } - - HRESULT STDMETHODCALLTYPE EnumDAdvise(__RPC__deref_out_opt IEnumSTATDATA** ppenumAdvise) override - { - return OLE_E_ADVISENOTSUPPORTED; - } - -private: - - int32 lookupFormatEtc(FORMATETC* pFormatEtc) const - { - // Check each of our formats in turn to see if one matches - for (int32 i = 0; i < _formatsCount; i++) - { - if ((_formatEtc[i].tymed & pFormatEtc->tymed) && - _formatEtc[i].cfFormat == pFormatEtc->cfFormat && - _formatEtc[i].dwAspect == pFormatEtc->dwAspect) - { - // Return index of stored format - return i; - } - } - - // Format not found - return INVALID_INDEX; - } -}; - -WindowsGuiData GuiDragDropData; - -/// -/// Async DoDragDrop helper (used for rendering frames during main thread stall). -/// -class DoDragDropJob : public ThreadPoolTask -{ -public: - - int64 ExitFlag = 0; - - // [ThreadPoolTask] - bool Run() override - { - Scripting::GetScriptsDomain()->Dispatch(); - while (Platform::AtomicRead(&ExitFlag) == 0) - { - Engine::OnDraw(); - Platform::Sleep(20); - } - return false; - } -}; - -DragDropEffect WindowsWindow::DoDragDrop(const StringView& data) -{ - // Create background worker that will keep updating GUI (perform rendering) - const auto task = New(); - Task::StartNew(task); - while (task->GetState() == TaskState::Queued) - { - Platform::Sleep(1); - } - - // Create descriptors - FORMATETC fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; - STGMEDIUM stgmed = { TYMED_HGLOBAL, { nullptr }, nullptr }; - - // Create a HGLOBAL inside the storage medium - stgmed.hGlobal = StringToHandle(data); - - // Create drop source - auto dropSource = new WindowsDragSource(&fmtetc, &stgmed, 1); - - // Do the drag drop operation - DWORD dwEffect; - HRESULT result = ::DoDragDrop(dropSource, dropSource, DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK | DROPEFFECT_SCROLL, &dwEffect); - - // Wait for job end - Platform::AtomicStore(&task->ExitFlag, 1); - task->Wait(); - - // Release allocated data - dropSource->Release(); - ReleaseStgMedium(&stgmed); - - // Fix hanging mouse state (Windows doesn't send WM_LBUTTONUP when we end the drag and drop) - if (Input::GetMouseButton(MouseButton::Left)) - { - ::POINT point; - ::GetCursorPos(&point); - Input::Mouse->OnMouseUp(Float2((float)point.x, (float)point.y), MouseButton::Left, this); - } - - return SUCCEEDED(result) ? dropEffectFromOleEnum(dwEffect) : DragDropEffect::None; -} - -HRESULT WindowsWindow::DragEnter(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) -{ - // Call GUI - POINT p = { pt.x, pt.y }; - ::ScreenToClient(_handle, &p); - GuiDragDropData.Init((IDataObject*)pDataObj); - DragDropEffect effect = DragDropEffect::None; - OnDragEnter(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); - - // Focus - Focus(); - - // Translate effect into Ole Api type - *pdwEffect = dropEffect2OleEnum(effect); - - return S_OK; -} - -HRESULT WindowsWindow::DragOver(Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) -{ - // Call GUI - POINT p = { pt.x, pt.y }; - ::ScreenToClient(_handle, &p); - DragDropEffect effect = DragDropEffect::None; - OnDragOver(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); - - // Translate effect into Ole Api type - *pdwEffect = dropEffect2OleEnum(effect); - - return S_OK; -} - -HRESULT WindowsWindow::DragLeave() -{ - // Call GUI - OnDragLeave(); - - return S_OK; -} - -HRESULT WindowsWindow::Drop(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) -{ - // Call GUI - POINT p = { pt.x, pt.y }; - ::ScreenToClient(_handle, &p); - GuiDragDropData.Init((IDataObject*)pDataObj); - DragDropEffect effect = DragDropEffect::None; - OnDragDrop(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); - - // Translate effect into Ole Api type - *pdwEffect = dropEffect2OleEnum(effect); - - return S_OK; -} - -#else - -DragDropEffect WindowsWindow::DoDragDrop(const StringView& data) -{ - // Not supported - return DragDropEffect::None; -} - -#endif - -#endif diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp index 174160922..e29d0fc19 100644 --- a/Source/Engine/Platform/Windows/WindowsWindow.cpp +++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp @@ -10,8 +10,23 @@ #include "Engine/Graphics/GPUSwapChain.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/GPUDevice.h" +#if USE_EDITOR +#include "Engine/Core/Collections/Array.h" +#include "Engine/Engine/Engine.h" +#include "Engine/Platform/IGuiData.h" +#include "Engine/Input/Input.h" +#include "Engine/Input/Mouse.h" +#include "Engine/Threading/ThreadPoolTask.h" +#include "Engine/Threading/ThreadPool.h" +#include "Engine/Scripting/Scripting.h" +#include "Engine/Scripting/ManagedCLR/MDomain.h" +#endif #include "../Win32/IncludeWindowsHeaders.h" #include +#if USE_EDITOR +#include +#include +#endif #define DefaultDPI 96 @@ -139,7 +154,7 @@ WindowsWindow::WindowsWindow(const CreateWindowSettings& settings) const HMODULE user32Dll = LoadLibraryW(L"user32.dll"); if (user32Dll) { - typedef UINT (STDAPICALLTYPE* GetDpiForWindowProc)(HWND hwnd); + typedef UINT (STDAPICALLTYPE*GetDpiForWindowProc)(HWND hwnd); const GetDpiForWindowProc getDpiForWindowProc = (GetDpiForWindowProc)GetProcAddress(user32Dll, "GetDpiForWindow"); if (getDpiForWindowProc) { @@ -262,7 +277,7 @@ void WindowsWindow::Maximize() void WindowsWindow::SetBorderless(bool isBorderless, bool maximized) { ASSERT(HasHWND()); - + if (IsFullscreen()) SetIsFullscreen(false); @@ -278,7 +293,7 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized) { LONG lStyle = GetWindowLong(_handle, GWL_STYLE); lStyle &= ~(WS_THICKFRAME | WS_SYSMENU | WS_OVERLAPPED | WS_BORDER | WS_CAPTION); - lStyle |= WS_POPUP; + lStyle |= WS_POPUP; lStyle |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; #if WINDOWS_USE_NEW_BORDER_LESS if (_settings.IsRegularWindow) @@ -289,8 +304,8 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized) #endif SetWindowLong(_handle, GWL_STYLE, lStyle); - SetWindowPos(_handle, HWND_TOP, 0, 0,0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); - + SetWindowPos(_handle, HWND_TOP, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + if (maximized) { ShowWindow(_handle, SW_SHOWMAXIMIZED); @@ -311,10 +326,10 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized) if (_settings.HasSizingFrame) lStyle |= WS_THICKFRAME; lStyle |= WS_OVERLAPPED | WS_SYSMENU | WS_BORDER | WS_CAPTION; - + SetWindowLong(_handle, GWL_STYLE, lStyle); - SetWindowPos(_handle, nullptr, 0, 0, (int)_settings.Size.X, (int)_settings.Size.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); - + SetWindowPos(_handle, nullptr, 0, 0, (int)_settings.Size.X, (int)_settings.Size.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + if (maximized) { Maximize(); @@ -727,7 +742,7 @@ void WindowsWindow::CheckForWindowResize() MONITORINFO monitorInfo; monitorInfo.cbSize = sizeof(MONITORINFO); GetMonitorInfoW(monitor, &monitorInfo); - + auto cwidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left; auto cheight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top; if (width > cwidth && height > cheight) @@ -1297,4 +1312,633 @@ LRESULT WindowsWindow::WndProc(UINT msg, WPARAM wParam, LPARAM lParam) return DefWindowProc(_handle, msg, wParam, lParam); } +#if USE_EDITOR + +HGLOBAL duplicateGlobalMem(HGLOBAL hMem) +{ + auto len = GlobalSize(hMem); + auto source = GlobalLock(hMem); + auto dest = GlobalAlloc(GMEM_FIXED, len); + Platform::MemoryCopy(dest, source, len); + GlobalUnlock(hMem); + return dest; +} + +DWORD dropEffect2OleEnum(DragDropEffect effect) +{ + DWORD result; + switch (effect) + { + case DragDropEffect::None: + result = DROPEFFECT_NONE; + break; + case DragDropEffect::Copy: + result = DROPEFFECT_COPY; + break; + case DragDropEffect::Move: + result = DROPEFFECT_MOVE; + break; + case DragDropEffect::Link: + result = DROPEFFECT_LINK; + break; + default: + result = DROPEFFECT_NONE; + break; + } + return result; +} + +DragDropEffect dropEffectFromOleEnum(DWORD effect) +{ + DragDropEffect result; + switch (effect) + { + case DROPEFFECT_NONE: + result = DragDropEffect::None; + break; + case DROPEFFECT_COPY: + result = DragDropEffect::Copy; + break; + case DROPEFFECT_MOVE: + result = DragDropEffect::Move; + break; + case DROPEFFECT_LINK: + result = DragDropEffect::Link; + break; + default: + result = DragDropEffect::None; + break; + } + return result; +} + +HANDLE StringToHandle(const StringView& str) +{ + // Allocate and lock a global memory buffer. + // Make it fixed data so we don't have to use GlobalLock + const int32 length = str.Length(); + char* ptr = static_cast(GlobalAlloc(GMEM_FIXED, length + 1)); + + // Copy the string into the buffer as ANSI text + StringUtils::ConvertUTF162ANSI(str.Get(), ptr, length); + ptr[length] = '\0'; + + return ptr; +} + +void DeepCopyFormatEtc(FORMATETC* dest, FORMATETC* source) +{ + // Copy the source FORMATETC into dest + *dest = *source; + + if (source->ptd) + { + // Allocate memory for the DVTARGETDEVICE if necessary + dest->ptd = static_cast(CoTaskMemAlloc(sizeof(DVTARGETDEVICE))); + + // Copy the contents of the source DVTARGETDEVICE into dest->ptd + *(dest->ptd) = *(source->ptd); + } +} + +HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc); + +/// +/// GUI data for Windows platform +/// +class WindowsGuiData : public IGuiData +{ +private: + Type _type; + Array _data; + +public: + /// + /// Init + /// + WindowsGuiData() + : _type(Type::Unknown) + , _data(1) + { + } + +public: + /// + /// Init from Ole IDataObject + /// + /// Object + void Init(IDataObject* pDataObj) + { + // Temporary data + FORMATETC fmtetc; + STGMEDIUM stgmed; + + // Clear + _type = Type::Unknown; + _data.Clear(); + + // Check type + fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) + { + // Text + _type = Type::Text; + + // Get data + char* text = static_cast(GlobalLock(stgmed.hGlobal)); + _data.Add(String(text)); + GlobalUnlock(stgmed.hGlobal); + ReleaseStgMedium(&stgmed); + } + else + { + fmtetc = { CF_UNICODETEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) + { + // Unicode Text + _type = Type::Text; + + // Get data + Char* text = static_cast(GlobalLock(stgmed.hGlobal)); + _data.Add(String(text)); + GlobalUnlock(stgmed.hGlobal); + ReleaseStgMedium(&stgmed); + } + else + { + fmtetc = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) + { + // Files + _type = Type::Files; + + // Get data + Char item[MAX_PATH]; + HDROP hdrop = static_cast(GlobalLock(stgmed.hGlobal)); + UINT filesCount = DragQueryFileW(hdrop, 0xFFFFFFFF, nullptr, 0); + for (UINT i = 0; i < filesCount; i++) + { + if (DragQueryFileW(hdrop, i, item, MAX_PATH) != 0) + { + _data.Add(String(item)); + } + } + GlobalUnlock(stgmed.hGlobal); + ReleaseStgMedium(&stgmed); + } + } + } + } + +public: + // [IGuiData] + Type GetType() const override + { + return _type; + } + String GetAsText() const override + { + String result; + if (_type == Type::Text) + { + result = _data[0]; + } + return result; + } + void GetAsFiles(Array* files) const override + { + if (_type == Type::Files) + { + files->Add(_data); + } + } +}; + +/// +/// Tool class for Windows Ole support +/// +class WindowsEnumFormatEtc : public IEnumFORMATETC +{ +private: + ULONG _refCount; + ULONG _index; + ULONG _formatsCount; + FORMATETC* _formatEtc; + +public: + WindowsEnumFormatEtc(FORMATETC* pFormatEtc, int32 nNumFormats) + : _refCount(1) + , _index(0) + , _formatsCount(nNumFormats) + , _formatEtc(nullptr) + { + // Allocate memory + _formatEtc = new FORMATETC[nNumFormats]; + + // Copy the FORMATETC structures + for (int32 i = 0; i < nNumFormats; i++) + { + DeepCopyFormatEtc(&_formatEtc[i], &pFormatEtc[i]); + } + } + + ~WindowsEnumFormatEtc() + { + if (_formatEtc) + { + for (uint32 i = 0; i < _formatsCount; i++) + { + if (_formatEtc[i].ptd) + { + CoTaskMemFree(_formatEtc[i].ptd); + } + } + + delete[] _formatEtc; + } + } + +public: + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR *__RPC_FAR * ppvObject) override + { + // Check to see what interface has been requested + if (riid == IID_IEnumFORMATETC || riid == IID_IUnknown) + { + AddRef(); + *ppvObject = this; + return S_OK; + } + + // No interface + *ppvObject = nullptr; + return E_NOINTERFACE; + } + ULONG STDMETHODCALLTYPE AddRef() override + { + _InterlockedIncrement(&_refCount); + return _refCount; + } + ULONG STDMETHODCALLTYPE Release() override + { + ULONG ulRefCount = _InterlockedDecrement(&_refCount); + if (_refCount == 0) + { + delete this; + } + return ulRefCount; + } + + // [IEnumFormatEtc] + HRESULT STDMETHODCALLTYPE Next(ULONG celt, FORMATETC* pFormatEtc, ULONG* pceltFetched) override + { + ULONG copied = 0; + + // validate arguments + if (celt == 0 || pFormatEtc == nullptr) + return E_INVALIDARG; + + // copy FORMATETC structures into caller's buffer + while (_index < _formatsCount && copied < celt) + { + DeepCopyFormatEtc(&pFormatEtc[copied], &_formatEtc[_index]); + copied++; + _index++; + } + + // store result + if (pceltFetched != nullptr) + *pceltFetched = copied; + + // did we copy all that was requested? + return (copied == celt) ? S_OK : S_FALSE; + } + HRESULT STDMETHODCALLTYPE Skip(ULONG celt) override + { + _index += celt; + return (_index <= _formatsCount) ? S_OK : S_FALSE; + } + HRESULT STDMETHODCALLTYPE Reset() override + { + _index = 0; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Clone(IEnumFORMATETC** ppEnumFormatEtc) override + { + HRESULT result; + + // Make a duplicate enumerator + result = CreateEnumFormatEtc(_formatsCount, _formatEtc, ppEnumFormatEtc); + + if (result == S_OK) + { + // Manually set the index state + static_cast(*ppEnumFormatEtc)->_index = _index; + } + + return result; + } +}; + +HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc) +{ + if (nNumFormats == 0 || pFormatEtc == nullptr || ppEnumFormatEtc == nullptr) + return E_INVALIDARG; + *ppEnumFormatEtc = new WindowsEnumFormatEtc(pFormatEtc, nNumFormats); + return *ppEnumFormatEtc ? S_OK : E_OUTOFMEMORY; +} + +/// +/// Drag drop source and data container for Ole +/// +class WindowsDragSource : public IDataObject, public IDropSource +{ +private: + ULONG _refCount; + int32 _formatsCount; + FORMATETC* _formatEtc; + STGMEDIUM* _stgMedium; + +public: + WindowsDragSource(FORMATETC* fmtetc, STGMEDIUM* stgmed, int32 count) + : _refCount(1) + , _formatsCount(count) + , _formatEtc(nullptr) + , _stgMedium(nullptr) + { + // Allocate memory + _formatEtc = new FORMATETC[count]; + _stgMedium = new STGMEDIUM[count]; + + // Copy descriptors + for (int32 i = 0; i < count; i++) + { + _formatEtc[i] = fmtetc[i]; + _stgMedium[i] = stgmed[i]; + } + } + + virtual ~WindowsDragSource() + { + delete[] _formatEtc; + delete[] _stgMedium; + } + +public: + // [IUnknown] + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR *__RPC_FAR * ppvObject) override + { + // Check to see what interface has been requested + if (riid == IID_IDataObject || riid == IID_IUnknown || riid == IID_IDropSource) + { + AddRef(); + *ppvObject = this; + return S_OK; + } + + // No interface + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override + { + _InterlockedIncrement(&_refCount); + return _refCount; + } + + ULONG STDMETHODCALLTYPE Release() override + { + ULONG ulRefCount = _InterlockedDecrement(&_refCount); + if (_refCount == 0) + { + delete this; + } + return ulRefCount; + } + + // [IDropSource] + HRESULT STDMETHODCALLTYPE QueryContinueDrag(_In_ BOOL fEscapePressed, _In_ DWORD grfKeyState) override + { + // If the Escape key has been pressed since the last call, cancel the drop + if (fEscapePressed == TRUE || grfKeyState & MK_RBUTTON) + return DRAGDROP_S_CANCEL; + + // If the LeftMouse button has been released, then do the drop! + if ((grfKeyState & MK_LBUTTON) == 0) + return DRAGDROP_S_DROP; + + // Continue with the drag-drop + return S_OK; + } + HRESULT STDMETHODCALLTYPE GiveFeedback(_In_ DWORD dwEffect) override + { + // TODO: allow to use custom mouse cursor during drop and drag operation + return DRAGDROP_S_USEDEFAULTCURSORS; + } + + // [IDataObject] + HRESULT STDMETHODCALLTYPE GetData(_In_ FORMATETC* pformatetcIn, _Out_ STGMEDIUM* pmedium) override + { + if (pformatetcIn == nullptr || pmedium == nullptr) + return E_INVALIDARG; + + // Try to match the specified FORMATETC with one of our supported formats + int32 index = lookupFormatEtc(pformatetcIn); + if (index == INVALID_INDEX) + return DV_E_FORMATETC; + + // Found a match - transfer data into supplied storage medium + pmedium->tymed = _formatEtc[index].tymed; + pmedium->pUnkForRelease = nullptr; + + // Copy the data into the caller's storage medium + switch (_formatEtc[index].tymed) + { + case TYMED_HGLOBAL: + pmedium->hGlobal = duplicateGlobalMem(_stgMedium[index].hGlobal); + break; + + default: + return DV_E_FORMATETC; + } + + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetDataHere(_In_ FORMATETC* pformatetc, _Inout_ STGMEDIUM* pmedium) override + { + return DATA_E_FORMATETC; + } + HRESULT STDMETHODCALLTYPE QueryGetData(__RPC__in_opt FORMATETC* pformatetc) override + { + return lookupFormatEtc(pformatetc) == INVALID_INDEX ? DV_E_FORMATETC : S_OK; + } + HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(__RPC__in_opt FORMATETC* pformatectIn, __RPC__out FORMATETC* pformatetcOut) override + { + // Apparently we have to set this field to NULL even though we don't do anything else + pformatetcOut->ptd = nullptr; + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE SetData(_In_ FORMATETC* pformatetc, _In_ STGMEDIUM* pmedium, BOOL fRelease) override + { + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD dwDirection, __RPC__deref_out_opt IEnumFORMATETC** ppenumFormatEtc) override + { + // Only the get direction is supported for OLE + if (dwDirection == DATADIR_GET) + { + // TODO: use SHCreateStdEnumFmtEtc API call + return CreateEnumFormatEtc(_formatsCount, _formatEtc, ppenumFormatEtc); + } + + // The direction specified is not supported for drag+drop + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE DAdvise(__RPC__in FORMATETC* pformatetc, DWORD advf, __RPC__in_opt IAdviseSink* pAdvSink, __RPC__out DWORD* pdwConnection) override + { + return OLE_E_ADVISENOTSUPPORTED; + } + HRESULT STDMETHODCALLTYPE DUnadvise(DWORD dwConnection) override + { + return OLE_E_ADVISENOTSUPPORTED; + } + HRESULT STDMETHODCALLTYPE EnumDAdvise(__RPC__deref_out_opt IEnumSTATDATA** ppenumAdvise) override + { + return OLE_E_ADVISENOTSUPPORTED; + } + +private: + int32 lookupFormatEtc(FORMATETC* pFormatEtc) const + { + // Check each of our formats in turn to see if one matches + for (int32 i = 0; i < _formatsCount; i++) + { + if ((_formatEtc[i].tymed & pFormatEtc->tymed) && + _formatEtc[i].cfFormat == pFormatEtc->cfFormat && + _formatEtc[i].dwAspect == pFormatEtc->dwAspect) + { + // Return index of stored format + return i; + } + } + + // Format not found + return INVALID_INDEX; + } +}; + +WindowsGuiData GuiDragDropData; + +/// +/// Async DoDragDrop helper (used for rendering frames during main thread stall). +/// +class DoDragDropJob : public ThreadPoolTask +{ +public: + int64 ExitFlag = 0; + + // [ThreadPoolTask] + bool Run() override + { + Scripting::GetScriptsDomain()->Dispatch(); + while (Platform::AtomicRead(&ExitFlag) == 0) + { + Engine::OnDraw(); + Platform::Sleep(20); + } + return false; + } +}; + +DragDropEffect WindowsWindow::DoDragDrop(const StringView& data) +{ + // Create background worker that will keep updating GUI (perform rendering) + const auto task = New(); + Task::StartNew(task); + while (task->GetState() == TaskState::Queued) + { + Platform::Sleep(1); + } + + // Create descriptors + FORMATETC fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + STGMEDIUM stgmed = { TYMED_HGLOBAL, { nullptr }, nullptr }; + + // Create a HGLOBAL inside the storage medium + stgmed.hGlobal = StringToHandle(data); + + // Create drop source + auto dropSource = new WindowsDragSource(&fmtetc, &stgmed, 1); + + // Do the drag drop operation + DWORD dwEffect; + HRESULT result = ::DoDragDrop(dropSource, dropSource, DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK | DROPEFFECT_SCROLL, &dwEffect); + + // Wait for job end + Platform::AtomicStore(&task->ExitFlag, 1); + task->Wait(); + + // Release allocated data + dropSource->Release(); + ReleaseStgMedium(&stgmed); + + // Fix hanging mouse state (Windows doesn't send WM_LBUTTONUP when we end the drag and drop) + if (Input::GetMouseButton(MouseButton::Left)) + { + ::POINT point; + ::GetCursorPos(&point); + Input::Mouse->OnMouseUp(Float2((float)point.x, (float)point.y), MouseButton::Left, this); + } + + return SUCCEEDED(result) ? dropEffectFromOleEnum(dwEffect) : DragDropEffect::None; +} + +HRESULT WindowsWindow::DragEnter(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) +{ + POINT p = { pt.x, pt.y }; + ::ScreenToClient(_handle, &p); + GuiDragDropData.Init((IDataObject*)pDataObj); + DragDropEffect effect = DragDropEffect::None; + OnDragEnter(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); + Focus(); + *pdwEffect = dropEffect2OleEnum(effect); + return S_OK; +} + +HRESULT WindowsWindow::DragOver(Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) +{ + POINT p = { pt.x, pt.y }; + ::ScreenToClient(_handle, &p); + DragDropEffect effect = DragDropEffect::None; + OnDragOver(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); + *pdwEffect = dropEffect2OleEnum(effect); + return S_OK; +} + +HRESULT WindowsWindow::DragLeave() +{ + OnDragLeave(); + return S_OK; +} + +HRESULT WindowsWindow::Drop(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) +{ + POINT p = { pt.x, pt.y }; + ::ScreenToClient(_handle, &p); + GuiDragDropData.Init((IDataObject*)pDataObj); + DragDropEffect effect = DragDropEffect::None; + OnDragDrop(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); + *pdwEffect = dropEffect2OleEnum(effect); + return S_OK; +} + +#else + +DragDropEffect WindowsWindow::DoDragDrop(const StringView& data) +{ + return DragDropEffect::None; +} + +#endif + #endif diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp index a57833314..bdf9f60f5 100644 --- a/Source/Engine/Scripting/Scripting.cpp +++ b/Source/Engine/Scripting/Scripting.cpp @@ -28,8 +28,8 @@ #include "Engine/Content/Content.h" #include "Engine/Engine/EngineService.h" #include "Engine/Engine/Globals.h" +#include "Engine/Engine/Time.h" #include "Engine/Graphics/RenderTask.h" -#include "Engine/Platform/MemoryStats.h" #include "Engine/Serialization/JsonTools.h" extern void registerFlaxEngineInternalCalls(); @@ -193,6 +193,14 @@ void ScriptingService::Update() { PROFILE_CPU_NAMED("Scripting::Update"); INVOKE_EVENT(Update); + +#ifdef USE_NETCORE + // Force GC to run in background periodically to avoid large blocking collections causing hitches + if (Time::Update.TicksCount % 60 == 0) + { + MCore::GC::Collect(MCore::GC::MaxGeneration(), MGCCollectionMode::Forced, false, false); + } +#endif } void ScriptingService::LateUpdate() From e7b1fce3ebcfb1c9f0891e1e757c3e90c0369a00 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 17:03:03 +0100 Subject: [PATCH 216/546] Merge `DoDragDropJob` to hsare the same code for Mac and Windows #1723 --- Source/Engine/Platform/Base/DragDropHelper.h | 31 +++++++++++++++++++ Source/Engine/Platform/Mac/MacWindow.cpp | 22 ++----------- .../Engine/Platform/Windows/WindowsWindow.cpp | 29 +---------------- 3 files changed, 34 insertions(+), 48 deletions(-) create mode 100644 Source/Engine/Platform/Base/DragDropHelper.h diff --git a/Source/Engine/Platform/Base/DragDropHelper.h b/Source/Engine/Platform/Base/DragDropHelper.h new file mode 100644 index 000000000..a607cdaec --- /dev/null +++ b/Source/Engine/Platform/Base/DragDropHelper.h @@ -0,0 +1,31 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Threading/ThreadPoolTask.h" +#include "Engine/Threading/ThreadPool.h" +#include "Engine/Scripting/Scripting.h" +#include "Engine/Scripting/ManagedCLR/MDomain.h" +#include "Engine/Engine/Engine.h" +#include "Engine/Platform/Platform.h" + +/// +/// Async DoDragDrop helper (used for rendering frames during main thread stall). +/// +class DoDragDropJob : public ThreadPoolTask +{ +public: + int64 ExitFlag = 0; + + // [ThreadPoolTask] + bool Run() override + { + Scripting::GetScriptsDomain()->Dispatch(); + while (Platform::AtomicRead(&ExitFlag) == 0) + { + Engine::OnDraw(); + Platform::Sleep(20); + } + return false; + } +}; diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index 4c392fc4e..373d19bae 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -8,9 +8,7 @@ #include "Engine/Platform/IGuiData.h" #if USE_EDITOR #include "Engine/Platform/CriticalSection.h" -#include "Engine/Threading/ThreadPoolTask.h" -#include "Engine/Threading/ThreadPool.h" -#include "Engine/Engine/Engine.h" +#include "Engine/Platform/Base/DragDropHelper.h" #endif #include "Engine/Core/Log.h" #include "Engine/Input/Input.h" @@ -25,23 +23,7 @@ // Data for drawing window while doing drag&drop on Mac (engine is paused during platform tick) CriticalSection MacDragLocker; NSDraggingSession* MacDragSession = nullptr; -class DoDragDropJob* MacDragJob = nullptr; - -class DoDragDropJob : public ThreadPoolTask -{ -public: - int64 ExitFlag = 0; - - bool Run() override - { - while (Platform::AtomicRead(&ExitFlag) == 0) - { - Engine::OnDraw(); - Platform::Sleep(20); - } - return false; - } -}; +DoDragDropJob* MacDragJob = nullptr; #endif inline bool IsWindowInvalid(Window* win) diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp index e29d0fc19..181077125 100644 --- a/Source/Engine/Platform/Windows/WindowsWindow.cpp +++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp @@ -12,14 +12,10 @@ #include "Engine/Graphics/GPUDevice.h" #if USE_EDITOR #include "Engine/Core/Collections/Array.h" -#include "Engine/Engine/Engine.h" #include "Engine/Platform/IGuiData.h" +#include "Engine/Platform/Base/DragDropHelper.h" #include "Engine/Input/Input.h" #include "Engine/Input/Mouse.h" -#include "Engine/Threading/ThreadPoolTask.h" -#include "Engine/Threading/ThreadPool.h" -#include "Engine/Scripting/Scripting.h" -#include "Engine/Scripting/ManagedCLR/MDomain.h" #endif #include "../Win32/IncludeWindowsHeaders.h" #include @@ -1829,36 +1825,13 @@ private: WindowsGuiData GuiDragDropData; -/// -/// Async DoDragDrop helper (used for rendering frames during main thread stall). -/// -class DoDragDropJob : public ThreadPoolTask -{ -public: - int64 ExitFlag = 0; - - // [ThreadPoolTask] - bool Run() override - { - Scripting::GetScriptsDomain()->Dispatch(); - while (Platform::AtomicRead(&ExitFlag) == 0) - { - Engine::OnDraw(); - Platform::Sleep(20); - } - return false; - } -}; - DragDropEffect WindowsWindow::DoDragDrop(const StringView& data) { // Create background worker that will keep updating GUI (perform rendering) const auto task = New(); Task::StartNew(task); while (task->GetState() == TaskState::Queued) - { Platform::Sleep(1); - } // Create descriptors FORMATETC fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; From ae85a94261560c8cfd32fab4434e4035764bb328 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 17:18:20 +0100 Subject: [PATCH 217/546] Fix memory leak while doing drag&drop with Debug Draw in use #1723 --- Source/Engine/Debug/DebugDraw.cpp | 3 +++ Source/Engine/Platform/Base/DragDropHelper.h | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp index 48fa31c91..059ebbd5d 100644 --- a/Source/Engine/Debug/DebugDraw.cpp +++ b/Source/Engine/Debug/DebugDraw.cpp @@ -696,12 +696,15 @@ void* DebugDraw::AllocateContext() void DebugDraw::FreeContext(void* context) { + ASSERT(context); Memory::DestructItem((DebugDrawContext*)context); Allocator::Free(context); } void DebugDraw::UpdateContext(void* context, float deltaTime) { + if (!context) + context = &GlobalContext; ((DebugDrawContext*)context)->DebugDrawDefault.Update(deltaTime); ((DebugDrawContext*)context)->DebugDrawDepthTest.Update(deltaTime); } diff --git a/Source/Engine/Platform/Base/DragDropHelper.h b/Source/Engine/Platform/Base/DragDropHelper.h index a607cdaec..a5382afd6 100644 --- a/Source/Engine/Platform/Base/DragDropHelper.h +++ b/Source/Engine/Platform/Base/DragDropHelper.h @@ -8,6 +8,18 @@ #include "Engine/Scripting/ManagedCLR/MDomain.h" #include "Engine/Engine/Engine.h" #include "Engine/Platform/Platform.h" +#if USE_EDITOR +#if !COMPILE_WITH_DEBUG_DRAW +#define COMPILE_WITH_DEBUG_DRAW 1 +#define COMPILE_WITH_DEBUG_DRAW_HACK +#endif +#include "Engine/Debug/DebugDraw.h" +#ifdef COMPILE_WITH_DEBUG_DRAW_HACK +#undef COMPILE_WITH_DEBUG_DRAW_HACK +#undef COMPILE_WITH_DEBUG_DRAW +#define COMPILE_WITH_DEBUG_DRAW 0 +#endif +#endif /// /// Async DoDragDrop helper (used for rendering frames during main thread stall). @@ -23,6 +35,10 @@ public: Scripting::GetScriptsDomain()->Dispatch(); while (Platform::AtomicRead(&ExitFlag) == 0) { +#if USE_EDITOR + // Flush any single-frame shapes to prevent memory leaking (eg. via terrain collision debug during scene drawing with PhysicsColliders or PhysicsDebug flag) + DebugDraw::UpdateContext(nullptr, 0.0f); +#endif Engine::OnDraw(); Platform::Sleep(20); } From c3d74b690eaea35d9197304f44ad51cfd7372d1e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 18:13:06 +0100 Subject: [PATCH 218/546] Fix editor plugins init regression from #1779 and compact more code --- .../Scripting/Plugins/PluginManager.cpp | 194 +++++++++--------- 1 file changed, 92 insertions(+), 102 deletions(-) diff --git a/Source/Engine/Scripting/Plugins/PluginManager.cpp b/Source/Engine/Scripting/Plugins/PluginManager.cpp index 87965fac9..61d7bc698 100644 --- a/Source/Engine/Scripting/Plugins/PluginManager.cpp +++ b/Source/Engine/Scripting/Plugins/PluginManager.cpp @@ -75,14 +75,16 @@ Action PluginManager::PluginsChanged; namespace PluginManagerImpl { + bool Initialized = false; Array GamePlugins; Array EditorPlugins; - void LoadPlugin(MClass* klass, bool isEditor); void OnAssemblyLoaded(MAssembly* assembly); void OnAssemblyUnloading(MAssembly* assembly); void OnBinaryModuleLoaded(BinaryModule* module); void OnScriptsReloading(); + void InitializePlugins(); + void DeinitializePlugins(); template Array SortPlugins(Array plugins, MClass* pluginLoadOrderAttribute, MField* typeField) @@ -149,7 +151,7 @@ void PluginManagerService::InvokeInitialize(Plugin* plugin) { if (plugin->_initialized) return; - StringAnsiView typeName = plugin->GetType().GetName(); + const StringAnsiView typeName = plugin->GetType().GetName(); PROFILE_CPU(); ZoneName(typeName.Get(), typeName.Length()); @@ -167,7 +169,7 @@ void PluginManagerService::InvokeDeinitialize(Plugin* plugin) { if (!plugin->_initialized) return; - StringAnsiView typeName = plugin->GetType().GetName(); + const StringAnsiView typeName = plugin->GetType().GetName(); PROFILE_CPU(); ZoneName(typeName.Get(), typeName.Length()); @@ -181,26 +183,6 @@ void PluginManagerService::InvokeDeinitialize(Plugin* plugin) PluginManager::PluginUnloaded(plugin); } -void PluginManagerImpl::LoadPlugin(MClass* klass, bool isEditor) -{ - // Create and check if use it - auto plugin = (Plugin*)Scripting::NewObject(klass); - if (!plugin) - return; - - if (!isEditor) - { - GamePlugins.Add((GamePlugin*)plugin); - } -#if USE_EDITOR - else - { - EditorPlugins.Add(plugin); - } -#endif - PluginManager::PluginsChanged(); -} - void PluginManagerImpl::OnAssemblyLoaded(MAssembly* assembly) { PROFILE_CPU_NAMED("Load Assembly Plugins"); @@ -221,6 +203,7 @@ void PluginManagerImpl::OnAssemblyLoaded(MAssembly* assembly) #endif // Process all classes to find plugins + bool loadedAnyPlugin = false; auto& classes = assembly->GetClasses(); for (auto i = classes.Begin(); i.IsNotEnd(); ++i) { @@ -230,17 +213,35 @@ void PluginManagerImpl::OnAssemblyLoaded(MAssembly* assembly) if (mclass->IsGeneric() || mclass->IsStatic() || mclass->IsAbstract()) continue; - if (mclass->IsSubClassOf(gamePluginClass)) - { - LoadPlugin(mclass, false); - } - + if (mclass->IsSubClassOf(gamePluginClass) #if USE_EDITOR - if (mclass->IsSubClassOf(editorPluginClass)) - { - LoadPlugin(mclass, true); - } + || mclass->IsSubClassOf(editorPluginClass) #endif + ) + { + auto plugin = (Plugin*)Scripting::NewObject(mclass); + if (plugin) + { +#if USE_EDITOR + if (mclass->IsSubClassOf(editorPluginClass)) + { + EditorPlugins.Add(plugin); + } + else +#endif + { + GamePlugins.Add((GamePlugin*)plugin); + } + loadedAnyPlugin = true; + } + } + } + + // Send event and initialize newly added plugins (ignore during startup) + if (loadedAnyPlugin && Initialized) + { + InitializePlugins(); + PluginManager::PluginsChanged(); } } @@ -253,8 +254,8 @@ void PluginManagerImpl::OnAssemblyUnloading(MAssembly* assembly) auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); ASSERT(beforeTypeField); +#if USE_EDITOR auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, beforeTypeField); - for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) { auto plugin = editorPlugins[i]; @@ -265,9 +266,9 @@ void PluginManagerImpl::OnAssemblyUnloading(MAssembly* assembly) changed = true; } } +#endif auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); - for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) { auto plugin = gamePlugins[i]; @@ -309,75 +310,83 @@ void PluginManagerImpl::OnBinaryModuleLoaded(BinaryModule* module) void PluginManagerImpl::OnScriptsReloading() { // When scripting is reloading (eg. for hot-reload in Editor) we have to deinitialize plugins (Scripting service destroys C# objects later on) - bool changed = false; + DeinitializePlugins(); +} + +void PluginManagerImpl::InitializePlugins() +{ + if (EditorPlugins.Count() + GamePlugins.Count() == 0) + return; + PROFILE_CPU_NAMED("InitializePlugins"); + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); + ASSERT(afterTypeField); + +#if USE_EDITOR + auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, afterTypeField); + for (auto plugin : editorPlugins) + { + PluginManagerService::InvokeInitialize(plugin); + } +#else + // Game plugins are managed via InitializeGamePlugins/DeinitializeGamePlugins by Editor for play mode + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); + for (auto plugin : gamePlugins) + { + PluginManagerService::InvokeInitialize(plugin); + } +#endif +} + +void PluginManagerImpl::DeinitializePlugins() +{ + if (EditorPlugins.Count() + GamePlugins.Count() == 0) + return; + PROFILE_CPU_NAMED("DeinitializePlugins"); auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); ASSERT(beforeTypeField); +#if USE_EDITOR auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, beforeTypeField); - for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) { auto plugin = editorPlugins[i]; - { - PluginManagerService::InvokeDeinitialize(plugin); - EditorPlugins.Remove(plugin); - changed = true; - } + PluginManagerService::InvokeDeinitialize(plugin); + EditorPlugins.Remove(plugin); } +#endif auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); - for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) { auto plugin = gamePlugins[i]; - { - PluginManagerService::InvokeDeinitialize(plugin); - GamePlugins.Remove(plugin); - changed = true; - } + PluginManagerService::InvokeDeinitialize(plugin); + GamePlugins.Remove(plugin); } - if (changed) - PluginManager::PluginsChanged(); + + PluginManager::PluginsChanged(); } bool PluginManagerService::Init() { + Initialized = false; + // Process already loaded modules for (auto module : BinaryModule::GetModules()) { OnBinaryModuleLoaded(module); } - // Initialize plugins - auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; - auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); - auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); - ASSERT(afterTypeField); - -#if !USE_EDITOR - auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); - - // Initialize game plugins - for (auto plugin : gamePlugins) - { - PluginManagerService::InvokeInitialize(plugin); - } -#endif - -#if USE_EDITOR - auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, afterTypeField); - - // Initialize editor plugins - for (auto plugin : editorPlugins) - { - PluginManagerService::InvokeInitialize(plugin); - } -#endif + // Invoke plugins initialization for all of them + InitializePlugins(); // Register for new binary modules load actions + Initialized = true; Scripting::BinaryModuleLoaded.Bind(&OnBinaryModuleLoaded); Scripting::ScriptsReloading.Bind(&OnScriptsReloading); @@ -386,38 +395,13 @@ bool PluginManagerService::Init() void PluginManagerService::Dispose() { + // Unregister from new modules loading + Initialized = false; Scripting::BinaryModuleLoaded.Unbind(&OnBinaryModuleLoaded); Scripting::ScriptsReloading.Unbind(&OnScriptsReloading); // Cleanup all plugins - PROFILE_CPU_NAMED("Dispose Plugins"); - const int32 pluginsCount = EditorPlugins.Count() + GamePlugins.Count(); - if (pluginsCount == 0) - return; - LOG(Info, "Unloading {0} plugins", pluginsCount); - - auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; - auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); - auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); - ASSERT(beforeTypeField); - - auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, beforeTypeField); - for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) - { - auto plugin = editorPlugins[i]; - InvokeDeinitialize(plugin); - EditorPlugins.Remove(plugin); - } - - auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); - for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) - { - auto plugin = gamePlugins[i]; - InvokeDeinitialize(plugin); - GamePlugins.Remove(plugin); - } - - PluginManager::PluginsChanged(); + DeinitializePlugins(); } const Array& PluginManager::GetGamePlugins() @@ -432,11 +416,13 @@ const Array& PluginManager::GetEditorPlugins() Plugin* PluginManager::GetPlugin(const StringView& name) { +#if USE_EDITOR for (Plugin* p : EditorPlugins) { if (p->GetDescription().Name == name) return p; } +#endif for (GamePlugin* gp : GamePlugins) { if (gp->GetDescription().Name == name) @@ -448,11 +434,13 @@ Plugin* PluginManager::GetPlugin(const StringView& name) Plugin* PluginManager::GetPlugin(const MClass* type) { CHECK_RETURN(type, nullptr); +#if USE_EDITOR for (Plugin* p : EditorPlugins) { if (p->GetClass()->IsSubClassOf(type)) return p; } +#endif for (GamePlugin* gp : GamePlugins) { if (gp->GetClass()->IsSubClassOf(type)) @@ -464,11 +452,13 @@ Plugin* PluginManager::GetPlugin(const MClass* type) Plugin* PluginManager::GetPlugin(const ScriptingTypeHandle& type) { CHECK_RETURN(type, nullptr); +#if USE_EDITOR for (Plugin* p : EditorPlugins) { if (p->Is(type)) return p; } +#endif for (GamePlugin* gp : GamePlugins) { if (gp->Is(type)) From c78ce9697cb3acacd525bed472d7b318ebf8e835 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 18:14:16 +0100 Subject: [PATCH 219/546] Fix incorrect tabs selection handling when tab gets removed #1843 --- Source/Editor/GUI/Tabs/Tab.cs | 22 ++++++++++++++++++++++ Source/Editor/GUI/Tabs/Tabs.cs | 14 ++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Source/Editor/GUI/Tabs/Tab.cs b/Source/Editor/GUI/Tabs/Tab.cs index 3968cc17d..85f805af0 100644 --- a/Source/Editor/GUI/Tabs/Tab.cs +++ b/Source/Editor/GUI/Tabs/Tab.cs @@ -13,6 +13,8 @@ namespace FlaxEditor.GUI.Tabs [HideInEditor] public class Tab : ContainerControl { + internal Tabs _selectedInTabs; + /// /// Gets or sets the text. /// @@ -86,5 +88,25 @@ namespace FlaxEditor.GUI.Tabs { return new Tabs.TabHeader((Tabs)Parent, this); } + + /// + protected override void OnParentChangedInternal() + { + if (_selectedInTabs != null) + _selectedInTabs.SelectedTab = null; + + base.OnParentChangedInternal(); + } + + /// + public override void OnDestroy() + { + if (IsDisposing) + return; + if (_selectedInTabs != null) + _selectedInTabs.SelectedTab = null; + + base.OnDestroy(); + } } } diff --git a/Source/Editor/GUI/Tabs/Tabs.cs b/Source/Editor/GUI/Tabs/Tabs.cs index d830b9e0f..b5e2cfe39 100644 --- a/Source/Editor/GUI/Tabs/Tabs.cs +++ b/Source/Editor/GUI/Tabs/Tabs.cs @@ -263,7 +263,12 @@ namespace FlaxEditor.GUI.Tabs // Check if index will change if (_selectedIndex != index) { - SelectedTab?.OnDeselected(); + var prev = SelectedTab; + if (prev != null) + { + prev._selectedInTabs = null; + prev.OnDeselected(); + } _selectedIndex = index; PerformLayout(); OnSelectedTabChanged(); @@ -342,8 +347,13 @@ namespace FlaxEditor.GUI.Tabs /// protected virtual void OnSelectedTabChanged() { + var selectedTab = SelectedTab; SelectedTabChanged?.Invoke(this); - SelectedTab?.OnSelected(); + if (selectedTab != null) + { + selectedTab._selectedInTabs = this; + selectedTab.OnSelected(); + } } /// From 7dc645c1147e61895ef39500c6c7b451d74b3a35 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 20:50:37 +0100 Subject: [PATCH 220/546] Update Tracy Profiler to `0.10` --- Source/ThirdParty/tracy/TracyClient.cpp | 7 +- .../tracy/client/TracyCallstack.cpp | 9 +- .../tracy/client/TracyCallstack.hpp | 9 +- Source/ThirdParty/tracy/client/TracyLock.hpp | 8 +- .../ThirdParty/tracy/client/TracyOverride.cpp | 26 + .../ThirdParty/tracy/client/TracyProfiler.cpp | 544 +--- .../ThirdParty/tracy/client/TracyProfiler.hpp | 105 +- .../ThirdParty/tracy/client/TracyScoped.hpp | 28 +- .../ThirdParty/tracy/client/TracySysPower.cpp | 164 + .../ThirdParty/tracy/client/TracySysPower.hpp | 44 + .../ThirdParty/tracy/client/TracySysTrace.cpp | 49 +- .../tracy/client/tracy_concurrentqueue.h | 13 +- .../tracy/client/tracy_rpmalloc.cpp | 2 +- .../ThirdParty/tracy/common/TracyProtocol.hpp | 4 +- Source/ThirdParty/tracy/common/TracyQueue.hpp | 32 +- .../ThirdParty/tracy/common/TracySocket.cpp | 6 +- .../ThirdParty/tracy/common/TracySocket.hpp | 2 +- .../ThirdParty/tracy/common/TracySystem.cpp | 97 +- .../ThirdParty/tracy/common/TracySystem.hpp | 9 - .../ThirdParty/tracy/common/TracyVersion.hpp | 2 +- Source/ThirdParty/tracy/libbacktrace/config.h | 4 + .../ThirdParty/tracy/libbacktrace/dwarf.cpp | 88 +- Source/ThirdParty/tracy/libbacktrace/elf.cpp | 2659 ++++++++++++++++- .../tracy/libbacktrace/internal.hpp | 9 + Source/ThirdParty/tracy/tracy/Tracy.hpp | 61 +- 25 files changed, 3169 insertions(+), 812 deletions(-) create mode 100644 Source/ThirdParty/tracy/client/TracyOverride.cpp create mode 100644 Source/ThirdParty/tracy/client/TracySysPower.cpp create mode 100644 Source/ThirdParty/tracy/client/TracySysPower.hpp diff --git a/Source/ThirdParty/tracy/TracyClient.cpp b/Source/ThirdParty/tracy/TracyClient.cpp index 3548c5752..1fbd78f96 100644 --- a/Source/ThirdParty/tracy/TracyClient.cpp +++ b/Source/ThirdParty/tracy/TracyClient.cpp @@ -11,10 +11,10 @@ // Define TRACY_ENABLE to enable profiler. -#ifdef TRACY_ENABLE - #include "common/TracySystem.cpp" +#ifdef TRACY_ENABLE + #ifdef _MSC_VER # pragma warning(push, 0) #endif @@ -22,12 +22,13 @@ #include #include "client/TracyProfiler.cpp" #include "client/TracyCallstack.cpp" +#include "client/TracySysPower.cpp" #include "client/TracySysTime.cpp" #include "client/TracySysTrace.cpp" #include "common/TracySocket.cpp" #include "client/tracy_rpmalloc.cpp" #include "client/TracyAlloc.cpp" - +#include "client/TracyOverride.cpp" #if TRACY_HAS_CALLSTACK == 2 || TRACY_HAS_CALLSTACK == 3 || TRACY_HAS_CALLSTACK == 4 || TRACY_HAS_CALLSTACK == 6 # include "libbacktrace/alloc.cpp" diff --git a/Source/ThirdParty/tracy/client/TracyCallstack.cpp b/Source/ThirdParty/tracy/client/TracyCallstack.cpp index ca19a543b..0de7c9d2e 100644 --- a/Source/ThirdParty/tracy/client/TracyCallstack.cpp +++ b/Source/ThirdParty/tracy/client/TracyCallstack.cpp @@ -154,7 +154,6 @@ void InitCallstack() DBGHELP_LOCK; #endif - //SymInitialize( GetCurrentProcess(), "C:\\Flax\\FlaxEngine\\Binaries\\Editor\\Win64\\Debug;C:\\Flax\\FlaxEngine\\Cache\\Projects", true ); SymInitialize( GetCurrentProcess(), nullptr, true ); SymSetOptions( SYMOPT_LOAD_LINES ); @@ -228,6 +227,10 @@ void InitCallstack() const auto res = GetModuleFileNameA( mod[i], name, 1021 ); if( res > 0 ) { + // This may be a new module loaded since our call to SymInitialize. + // Just in case, force DbgHelp to load its pdb ! + SymLoadModuleEx(proc, NULL, name, NULL, (DWORD64)info.lpBaseOfDll, info.SizeOfImage, NULL, 0); + auto ptr = name + res; while( ptr > name && *ptr != '\\' && *ptr != '/' ) ptr--; if( ptr > name ) ptr++; @@ -683,7 +686,9 @@ void InitCallstackCritical() void InitCallstack() { cb_bts = backtrace_create_state( nullptr, 0, nullptr, nullptr ); +#ifndef TRACY_DEMANGLE ___tracy_init_demangle_buffer(); +#endif #ifdef __linux InitKernelSymbols(); @@ -758,7 +763,9 @@ debuginfod_client* GetDebuginfodClient() void EndCallstack() { +#ifndef TRACY_DEMANGLE ___tracy_free_demangle_buffer(); +#endif #ifdef TRACY_DEBUGINFOD ClearDebugInfoVector( s_di_known ); debuginfod_end( s_debuginfod ); diff --git a/Source/ThirdParty/tracy/client/TracyCallstack.hpp b/Source/ThirdParty/tracy/client/TracyCallstack.hpp index 8cfede8fb..96bee3f51 100644 --- a/Source/ThirdParty/tracy/client/TracyCallstack.hpp +++ b/Source/ThirdParty/tracy/client/TracyCallstack.hpp @@ -10,7 +10,14 @@ #endif -#ifdef TRACY_HAS_CALLSTACK +#ifndef TRACY_HAS_CALLSTACK + +namespace tracy +{ +static tracy_force_inline void* Callstack( int depth ) { return nullptr; } +} + +#else #ifdef TRACY_DEBUGINFOD # include diff --git a/Source/ThirdParty/tracy/client/TracyLock.hpp b/Source/ThirdParty/tracy/client/TracyLock.hpp index 296a41ba1..d12a3c16d 100644 --- a/Source/ThirdParty/tracy/client/TracyLock.hpp +++ b/Source/ThirdParty/tracy/client/TracyLock.hpp @@ -21,7 +21,7 @@ public: , m_active( false ) #endif { - assert( m_id != std::numeric_limits::max() ); + assert( m_id != (std::numeric_limits::max)() ); auto item = Profiler::QueueSerial(); MemWrite( &item->hdr.type, QueueType::LockAnnounce ); @@ -154,7 +154,7 @@ public: tracy_force_inline void CustomName( const char* name, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); auto ptr = (char*)tracy_malloc( size ); memcpy( ptr, name, size ); auto item = Profiler::QueueSerial(); @@ -235,7 +235,7 @@ public: , m_active( false ) #endif { - assert( m_id != std::numeric_limits::max() ); + assert( m_id != (std::numeric_limits::max)() ); auto item = Profiler::QueueSerial(); MemWrite( &item->hdr.type, QueueType::LockAnnounce ); @@ -450,7 +450,7 @@ public: tracy_force_inline void CustomName( const char* name, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); auto ptr = (char*)tracy_malloc( size ); memcpy( ptr, name, size ); auto item = Profiler::QueueSerial(); diff --git a/Source/ThirdParty/tracy/client/TracyOverride.cpp b/Source/ThirdParty/tracy/client/TracyOverride.cpp new file mode 100644 index 000000000..591508a7f --- /dev/null +++ b/Source/ThirdParty/tracy/client/TracyOverride.cpp @@ -0,0 +1,26 @@ +#ifdef TRACY_ENABLE +# ifdef __linux__ +# include "TracyDebug.hpp" +# ifdef TRACY_VERBOSE +# include +# include +# endif + +extern "C" int dlclose( void* hnd ) +{ +#ifdef TRACY_VERBOSE + struct link_map* lm; + if( dlinfo( hnd, RTLD_DI_LINKMAP, &lm ) == 0 ) + { + TracyDebug( "Overriding dlclose for %s\n", lm->l_name ); + } + else + { + TracyDebug( "Overriding dlclose for unknown object (%s)\n", dlerror() ); + } +#endif + return 0; +} + +# endif +#endif diff --git a/Source/ThirdParty/tracy/client/TracyProfiler.cpp b/Source/ThirdParty/tracy/client/TracyProfiler.cpp index dfbb22a83..59c31d6f8 100644 --- a/Source/ThirdParty/tracy/client/TracyProfiler.cpp +++ b/Source/ThirdParty/tracy/client/TracyProfiler.cpp @@ -81,7 +81,9 @@ #endif #ifdef __APPLE__ -# define TRACY_DELAYED_INIT +# ifndef TRACY_DELAYED_INIT +# define TRACY_DELAYED_INIT +# endif #else # ifdef __GNUC__ # define init_order( val ) __attribute__ ((init_priority(val))) @@ -1074,7 +1076,9 @@ static void CrashHandler( int signal, siginfo_t* info, void* /*ucontext*/ ) } closedir( dp ); +#ifdef TRACY_HAS_CALLSTACK if( selfTid == s_symbolTid ) s_symbolThreadGone.store( true, std::memory_order_release ); +#endif TracyLfqPrepare( QueueType::Crash ); TracyLfqCommit; @@ -1355,6 +1359,7 @@ Profiler::Profiler() , m_queryImage( nullptr ) , m_queryData( nullptr ) , m_crashHandlerInstalled( false ) + , m_programName( nullptr ) { assert( !s_instance ); s_instance = this; @@ -1417,7 +1422,9 @@ void Profiler::SpawnWorkerThreads() #if defined _WIN32 && !defined TRACY_UWP && !defined TRACY_NO_CRASH_HANDLER s_profilerThreadId = GetThreadId( s_thread->Handle() ); +# ifdef TRACY_HAS_CALLSTACK s_symbolThreadId = GetThreadId( s_symbolThread->Handle() ); +# endif m_exceptionHandler = AddVectoredExceptionHandler( 1, CrashFilter ); #endif @@ -1456,7 +1463,7 @@ Profiler::~Profiler() if( m_crashHandlerInstalled ) RemoveVectoredExceptionHandler( m_exceptionHandler ); #endif -#ifdef __linux__ +#if defined __linux__ && !defined TRACY_NO_CRASH_HANDLER if( m_crashHandlerInstalled ) { sigaction( TRACY_CRASH_SIGNAL, &m_prevSignal.pwr, nullptr ); @@ -1522,7 +1529,7 @@ bool Profiler::ShouldExit() void Profiler::Worker() { -#ifdef __linux__ +#if defined __linux__ && !defined TRACY_NO_CRASH_HANDLER s_profilerTid = syscall( SYS_gettid ); #endif @@ -1711,6 +1718,9 @@ void Profiler::Worker() if( m_sock ) break; #ifndef TRACY_ON_DEMAND ProcessSysTime(); +# ifdef TRACY_HAS_SYSPOWER + m_sysPower.Tick(); +# endif #endif if( m_broadcast ) @@ -1718,6 +1728,14 @@ void Profiler::Worker() const auto t = std::chrono::high_resolution_clock::now().time_since_epoch().count(); if( t - lastBroadcast > 3000000000 ) // 3s { + m_programNameLock.lock(); + if( m_programName ) + { + broadcastMsg = GetBroadcastMessage( m_programName, strlen( m_programName ), broadcastLen, dataPort ); + m_programName = nullptr; + } + m_programNameLock.unlock(); + lastBroadcast = t; const auto ts = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count(); broadcastMsg.activeTime = int32_t( ts - m_epoch ); @@ -1828,6 +1846,9 @@ void Profiler::Worker() for(;;) { ProcessSysTime(); +#ifdef TRACY_HAS_SYSPOWER + m_sysPower.Tick(); +#endif const auto status = Dequeue( token ); const auto serialStatus = DequeueSerial(); if( status == DequeueStatus::ConnectionLost || serialStatus == DequeueStatus::ConnectionLost ) @@ -3026,9 +3047,9 @@ void Profiler::SendSourceLocation( uint64_t ptr ) MemWrite( &item.srcloc.file, (uint64_t)srcloc->file ); MemWrite( &item.srcloc.function, (uint64_t)srcloc->function ); MemWrite( &item.srcloc.line, srcloc->line ); - MemWrite( &item.srcloc.r, uint8_t( ( srcloc->color ) & 0xFF ) ); + MemWrite( &item.srcloc.b, uint8_t( ( srcloc->color ) & 0xFF ) ); MemWrite( &item.srcloc.g, uint8_t( ( srcloc->color >> 8 ) & 0xFF ) ); - MemWrite( &item.srcloc.b, uint8_t( ( srcloc->color >> 16 ) & 0xFF ) ); + MemWrite( &item.srcloc.r, uint8_t( ( srcloc->color >> 16 ) & 0xFF ) ); AppendData( &item, QueueDataSize[(int)QueueType::SourceLocation] ); } @@ -3331,10 +3352,8 @@ bool Profiler::HandleServerQuery() uint8_t type; uint64_t ptr; - uint32_t extra; memcpy( &type, &payload.type, sizeof( payload.type ) ); memcpy( &ptr, &payload.ptr, sizeof( payload.ptr ) ); - memcpy( &extra, &payload.extra, sizeof( payload.extra ) ); switch( type ) { @@ -3381,7 +3400,7 @@ bool Profiler::HandleServerQuery() break; #ifndef TRACY_NO_CODE_TRANSFER case ServerQuerySymbolCode: - HandleSymbolCodeQuery( ptr, extra ); + HandleSymbolCodeQuery( ptr, payload.extra ); break; #endif case ServerQuerySourceCode: @@ -3398,7 +3417,7 @@ bool Profiler::HandleServerQuery() break; case ServerQueryDataTransferPart: memcpy( m_queryDataPtr, &ptr, 8 ); - memcpy( m_queryDataPtr+8, &extra, 4 ); + memcpy( m_queryDataPtr+8, &payload.extra, 4 ); m_queryDataPtr += 12; AckServerQuery(); break; @@ -3753,7 +3772,7 @@ void Profiler::SendFrameImage( const void* image, uint16_t w, uint16_t h, uint8_ { #ifndef TRACY_NO_FRAME_IMAGE auto& profiler = GetProfiler(); - assert( profiler.m_frameCount.load( std::memory_order_relaxed ) < std::numeric_limits::max() ); + assert( profiler.m_frameCount.load( std::memory_order_relaxed ) < (std::numeric_limits::max)() ); # ifdef TRACY_ON_DEMAND if( !profiler.IsConnected() ) return; # endif @@ -3770,6 +3789,12 @@ void Profiler::SendFrameImage( const void* image, uint16_t w, uint16_t h, uint8_ fi->flip = flip; profiler.m_fiQueue.commit_next(); profiler.m_fiLock.unlock(); +#else + static_cast(image); // unused + static_cast(w); // unused + static_cast(h); // unused + static_cast(offset); // unused + static_cast(flip); // unused #endif } @@ -3827,7 +3852,7 @@ void Profiler::ConfigurePlot( const char* name, PlotFormatType type, bool step, void Profiler::Message( const char* txt, size_t size, int callstack ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); #ifdef TRACY_ON_DEMAND if( !GetProfiler().IsConnected() ) return; #endif @@ -3864,7 +3889,7 @@ void Profiler::Message( const char* txt, int callstack ) void Profiler::MessageColor( const char* txt, size_t size, uint32_t color, int callstack ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); #ifdef TRACY_ON_DEMAND if( !GetProfiler().IsConnected() ) return; #endif @@ -3879,9 +3904,9 @@ void Profiler::MessageColor( const char* txt, size_t size, uint32_t color, int c TracyQueuePrepare( callstack == 0 ? QueueType::MessageColor : QueueType::MessageColorCallstack ); MemWrite( &item->messageColorFat.time, GetTime() ); MemWrite( &item->messageColorFat.text, (uint64_t)ptr ); - MemWrite( &item->messageColorFat.r, uint8_t( ( color ) & 0xFF ) ); + MemWrite( &item->messageColorFat.b, uint8_t( ( color ) & 0xFF ) ); MemWrite( &item->messageColorFat.g, uint8_t( ( color >> 8 ) & 0xFF ) ); - MemWrite( &item->messageColorFat.b, uint8_t( ( color >> 16 ) & 0xFF ) ); + MemWrite( &item->messageColorFat.r, uint8_t( ( color >> 16 ) & 0xFF ) ); MemWrite( &item->messageColorFat.size, (uint16_t)size ); TracyQueueCommit( messageColorFatThread ); } @@ -3899,15 +3924,15 @@ void Profiler::MessageColor( const char* txt, uint32_t color, int callstack ) TracyQueuePrepare( callstack == 0 ? QueueType::MessageLiteralColor : QueueType::MessageLiteralColorCallstack ); MemWrite( &item->messageColorLiteral.time, GetTime() ); MemWrite( &item->messageColorLiteral.text, (uint64_t)txt ); - MemWrite( &item->messageColorLiteral.r, uint8_t( ( color ) & 0xFF ) ); + MemWrite( &item->messageColorLiteral.b, uint8_t( ( color ) & 0xFF ) ); MemWrite( &item->messageColorLiteral.g, uint8_t( ( color >> 8 ) & 0xFF ) ); - MemWrite( &item->messageColorLiteral.b, uint8_t( ( color >> 16 ) & 0xFF ) ); + MemWrite( &item->messageColorLiteral.r, uint8_t( ( color >> 16 ) & 0xFF ) ); TracyQueueCommit( messageColorLiteralThread ); } void Profiler::MessageAppInfo( const char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); auto ptr = (char*)tracy_malloc( size ); memcpy( ptr, txt, size ); TracyLfqPrepare( QueueType::MessageAppInfo ); @@ -3922,7 +3947,7 @@ void Profiler::MessageAppInfo( const char* txt, size_t size ) TracyLfqCommit; } -void Profiler::MemAlloc(const void* ptr, size_t size, bool secure) +void Profiler::MemAlloc( const void* ptr, size_t size, bool secure ) { if( secure && !ProfilerAvailable() ) return; #ifdef TRACY_ON_DEMAND @@ -4085,7 +4110,6 @@ void Profiler::SendCallstack( int depth ) #endif } -void Profiler::ParameterRegister( ParameterCallback cb ) { GetProfiler().m_paramCallback = cb; } void Profiler::ParameterRegister( ParameterCallback cb, void* data ) { auto& profiler = GetProfiler(); @@ -4301,486 +4325,4 @@ int64_t Profiler::GetTimeQpc() } -#if 0 -#ifdef __cplusplus -extern "C" { -#endif - -TRACY_API TracyCZoneCtx ___tracy_emit_zone_begin( const struct ___tracy_source_location_data* srcloc, int active ) -{ - ___tracy_c_zone_context ctx; -#ifdef TRACY_ON_DEMAND - ctx.active = active && tracy::GetProfiler().IsConnected(); -#else - ctx.active = active; -#endif - if( !ctx.active ) return ctx; - const auto id = tracy::GetProfiler().GetNextZoneId(); - ctx.id = id; - -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneBegin ); - tracy::MemWrite( &item->zoneBegin.time, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->zoneBegin.srcloc, (uint64_t)srcloc ); - TracyQueueCommitC( zoneBeginThread ); - } - return ctx; -} - -TRACY_API TracyCZoneCtx ___tracy_emit_zone_begin_callstack( const struct ___tracy_source_location_data* srcloc, int depth, int active ) -{ - ___tracy_c_zone_context ctx; -#ifdef TRACY_ON_DEMAND - ctx.active = active && tracy::GetProfiler().IsConnected(); -#else - ctx.active = active; -#endif - if( !ctx.active ) return ctx; - const auto id = tracy::GetProfiler().GetNextZoneId(); - ctx.id = id; - -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - tracy::GetProfiler().SendCallstack( depth ); - { - TracyQueuePrepareC( tracy::QueueType::ZoneBeginCallstack ); - tracy::MemWrite( &item->zoneBegin.time, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->zoneBegin.srcloc, (uint64_t)srcloc ); - TracyQueueCommitC( zoneBeginThread ); - } - return ctx; -} - -TRACY_API TracyCZoneCtx ___tracy_emit_zone_begin_alloc( uint64_t srcloc, int active ) -{ - ___tracy_c_zone_context ctx; -#ifdef TRACY_ON_DEMAND - ctx.active = active && tracy::GetProfiler().IsConnected(); -#else - ctx.active = active; -#endif - if( !ctx.active ) - { - tracy::tracy_free( (void*)srcloc ); - return ctx; - } - const auto id = tracy::GetProfiler().GetNextZoneId(); - ctx.id = id; - -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneBeginAllocSrcLoc ); - tracy::MemWrite( &item->zoneBegin.time, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->zoneBegin.srcloc, srcloc ); - TracyQueueCommitC( zoneBeginThread ); - } - return ctx; -} - -TRACY_API TracyCZoneCtx ___tracy_emit_zone_begin_alloc_callstack( uint64_t srcloc, int depth, int active ) -{ - ___tracy_c_zone_context ctx; -#ifdef TRACY_ON_DEMAND - ctx.active = active && tracy::GetProfiler().IsConnected(); -#else - ctx.active = active; -#endif - if( !ctx.active ) - { - tracy::tracy_free( (void*)srcloc ); - return ctx; - } - const auto id = tracy::GetProfiler().GetNextZoneId(); - ctx.id = id; - -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - tracy::GetProfiler().SendCallstack( depth ); - { - TracyQueuePrepareC( tracy::QueueType::ZoneBeginAllocSrcLocCallstack ); - tracy::MemWrite( &item->zoneBegin.time, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->zoneBegin.srcloc, srcloc ); - TracyQueueCommitC( zoneBeginThread ); - } - return ctx; -} - -TRACY_API void ___tracy_emit_zone_end( TracyCZoneCtx ctx ) -{ - if( !ctx.active ) return; -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneEnd ); - tracy::MemWrite( &item->zoneEnd.time, tracy::Profiler::GetTime() ); - TracyQueueCommitC( zoneEndThread ); - } -} - -TRACY_API void ___tracy_emit_zone_text( TracyCZoneCtx ctx, const char* txt, size_t size ) -{ - assert( size < std::numeric_limits::max() ); - if( !ctx.active ) return; - auto ptr = (char*)tracy::tracy_malloc( size ); - memcpy( ptr, txt, size ); -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneText ); - tracy::MemWrite( &item->zoneTextFat.text, (uint64_t)ptr ); - tracy::MemWrite( &item->zoneTextFat.size, (uint16_t)size ); - TracyQueueCommitC( zoneTextFatThread ); - } -} - -TRACY_API void ___tracy_emit_zone_name( TracyCZoneCtx ctx, const char* txt, size_t size ) -{ - assert( size < std::numeric_limits::max() ); - if( !ctx.active ) return; - auto ptr = (char*)tracy::tracy_malloc( size ); - memcpy( ptr, txt, size ); -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneName ); - tracy::MemWrite( &item->zoneTextFat.text, (uint64_t)ptr ); - tracy::MemWrite( &item->zoneTextFat.size, (uint16_t)size ); - TracyQueueCommitC( zoneTextFatThread ); - } -} - -TRACY_API void ___tracy_emit_zone_color( TracyCZoneCtx ctx, uint32_t color ) { - if( !ctx.active ) return; -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneColor ); - tracy::MemWrite( &item->zoneColor.r, uint8_t( ( color ) & 0xFF ) ); - tracy::MemWrite( &item->zoneColor.g, uint8_t( ( color >> 8 ) & 0xFF ) ); - tracy::MemWrite( &item->zoneColor.b, uint8_t( ( color >> 16 ) & 0xFF ) ); - TracyQueueCommitC( zoneColorThread ); - } -} - -TRACY_API void ___tracy_emit_zone_value( TracyCZoneCtx ctx, uint64_t value ) -{ - if( !ctx.active ) return; -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneValue ); - tracy::MemWrite( &item->zoneValue.value, value ); - TracyQueueCommitC( zoneValueThread ); - } -} - -TRACY_API void ___tracy_emit_memory_alloc( const void* ptr, size_t size, int secure ) { tracy::Profiler::MemAlloc( ptr, size, secure != 0 ); } -TRACY_API void ___tracy_emit_memory_alloc_callstack( const void* ptr, size_t size, int depth, int secure ) { tracy::Profiler::MemAllocCallstack( ptr, size, depth, secure != 0 ); } -TRACY_API void ___tracy_emit_memory_free( const void* ptr, int secure ) { tracy::Profiler::MemFree( ptr, secure != 0 ); } -TRACY_API void ___tracy_emit_memory_free_callstack( const void* ptr, int depth, int secure ) { tracy::Profiler::MemFreeCallstack( ptr, depth, secure != 0 ); } -TRACY_API void ___tracy_emit_memory_alloc_named( const void* ptr, size_t size, int secure, const char* name ) { tracy::Profiler::MemAllocNamed( ptr, size, secure != 0, name ); } -TRACY_API void ___tracy_emit_memory_alloc_callstack_named( const void* ptr, size_t size, int depth, int secure, const char* name ) { tracy::Profiler::MemAllocCallstackNamed( ptr, size, depth, secure != 0, name ); } -TRACY_API void ___tracy_emit_memory_free_named( const void* ptr, int secure, const char* name ) { tracy::Profiler::MemFreeNamed( ptr, secure != 0, name ); } -TRACY_API void ___tracy_emit_memory_free_callstack_named( const void* ptr, int depth, int secure, const char* name ) { tracy::Profiler::MemFreeCallstackNamed( ptr, depth, secure != 0, name ); } -TRACY_API void ___tracy_emit_frame_mark( const char* name ) { tracy::Profiler::SendFrameMark( name ); } -TRACY_API void ___tracy_emit_frame_mark_start( const char* name ) { tracy::Profiler::SendFrameMark( name, tracy::QueueType::FrameMarkMsgStart ); } -TRACY_API void ___tracy_emit_frame_mark_end( const char* name ) { tracy::Profiler::SendFrameMark( name, tracy::QueueType::FrameMarkMsgEnd ); } -TRACY_API void ___tracy_emit_frame_image( const void* image, uint16_t w, uint16_t h, uint8_t offset, int flip ) { tracy::Profiler::SendFrameImage( image, w, h, offset, flip ); } -TRACY_API void ___tracy_emit_plot( const char* name, double val ) { tracy::Profiler::PlotData( name, val ); } -TRACY_API void ___tracy_emit_message( const char* txt, size_t size, int callstack ) { tracy::Profiler::Message( txt, size, callstack ); } -TRACY_API void ___tracy_emit_messageL( const char* txt, int callstack ) { tracy::Profiler::Message( txt, callstack ); } -TRACY_API void ___tracy_emit_messageC( const char* txt, size_t size, uint32_t color, int callstack ) { tracy::Profiler::MessageColor( txt, size, color, callstack ); } -TRACY_API void ___tracy_emit_messageLC( const char* txt, uint32_t color, int callstack ) { tracy::Profiler::MessageColor( txt, color, callstack ); } -TRACY_API void ___tracy_emit_message_appinfo( const char* txt, size_t size ) { tracy::Profiler::MessageAppInfo( txt, size ); } - -TRACY_API uint64_t ___tracy_alloc_srcloc( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz ) { - return tracy::Profiler::AllocSourceLocation( line, source, sourceSz, function, functionSz ); -} - -TRACY_API uint64_t ___tracy_alloc_srcloc_name( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const char* name, size_t nameSz ) { - return tracy::Profiler::AllocSourceLocation( line, source, sourceSz, function, functionSz, name, nameSz ); -} - -TRACY_API void ___tracy_emit_gpu_zone_begin( const struct ___tracy_gpu_zone_begin_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuZoneBegin ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_callstack( const struct ___tracy_gpu_zone_begin_callstack_data data ) -{ - tracy::GetProfiler().SendCallstack( data.depth ); - TracyLfqPrepareC( tracy::QueueType::GpuZoneBeginCallstack ); - tracy::MemWrite( &item->gpuZoneBegin.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_alloc( const struct ___tracy_gpu_zone_begin_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuZoneBeginAllocSrcLoc ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_alloc_callstack( const struct ___tracy_gpu_zone_begin_callstack_data data ) -{ - tracy::GetProfiler().SendCallstack( data.depth ); - TracyLfqPrepareC( tracy::QueueType::GpuZoneBeginAllocSrcLocCallstack ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_time( const struct ___tracy_gpu_time_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuTime ); - tracy::MemWrite( &item->gpuTime.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuTime.queryId, data.queryId ); - tracy::MemWrite( &item->gpuTime.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_end( const struct ___tracy_gpu_zone_end_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuZoneEnd ); - tracy::MemWrite( &item->gpuZoneEnd.cpuTime, tracy::Profiler::GetTime() ); - memset( &item->gpuZoneEnd.thread, 0, sizeof( item->gpuZoneEnd.thread ) ); - tracy::MemWrite( &item->gpuZoneEnd.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneEnd.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_new_context( ___tracy_gpu_new_context_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuNewContext ); - tracy::MemWrite( &item->gpuNewContext.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuNewContext.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuNewContext.period, data.period ); - tracy::MemWrite( &item->gpuNewContext.context, data.context ); - tracy::MemWrite( &item->gpuNewContext.flags, data.flags ); - tracy::MemWrite( &item->gpuNewContext.type, data.type ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_context_name( const struct ___tracy_gpu_context_name_data data ) -{ - auto ptr = (char*)tracy::tracy_malloc( data.len ); - memcpy( ptr, data.name, data.len ); - - TracyLfqPrepareC( tracy::QueueType::GpuContextName ); - tracy::MemWrite( &item->gpuContextNameFat.context, data.context ); - tracy::MemWrite( &item->gpuContextNameFat.ptr, (uint64_t)ptr ); - tracy::MemWrite( &item->gpuContextNameFat.size, data.len ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_calibration( const struct ___tracy_gpu_calibration_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuCalibration ); - tracy::MemWrite( &item->gpuCalibration.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuCalibration.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuCalibration.cpuDelta, data.cpuDelta ); - tracy::MemWrite( &item->gpuCalibration.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_serial( const struct ___tracy_gpu_zone_begin_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneBeginSerial ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_callstack_serial( const struct ___tracy_gpu_zone_begin_callstack_data data ) -{ - auto item = tracy::Profiler::QueueSerialCallstack( tracy::Callstack( data.depth ) ); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneBeginCallstackSerial ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_alloc_serial( const struct ___tracy_gpu_zone_begin_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneBeginAllocSrcLocSerial ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_alloc_callstack_serial( const struct ___tracy_gpu_zone_begin_callstack_data data ) -{ - auto item = tracy::Profiler::QueueSerialCallstack( tracy::Callstack( data.depth ) ); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneBeginAllocSrcLocCallstackSerial ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_time_serial( const struct ___tracy_gpu_time_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuTime ); - tracy::MemWrite( &item->gpuTime.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuTime.queryId, data.queryId ); - tracy::MemWrite( &item->gpuTime.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_zone_end_serial( const struct ___tracy_gpu_zone_end_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneEndSerial ); - tracy::MemWrite( &item->gpuZoneEnd.cpuTime, tracy::Profiler::GetTime() ); - memset( &item->gpuZoneEnd.thread, 0, sizeof( item->gpuZoneEnd.thread ) ); - tracy::MemWrite( &item->gpuZoneEnd.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneEnd.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_new_context_serial( ___tracy_gpu_new_context_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuNewContext ); - tracy::MemWrite( &item->gpuNewContext.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuNewContext.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuNewContext.period, data.period ); - tracy::MemWrite( &item->gpuNewContext.context, data.context ); - tracy::MemWrite( &item->gpuNewContext.flags, data.flags ); - tracy::MemWrite( &item->gpuNewContext.type, data.type ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_context_name_serial( const struct ___tracy_gpu_context_name_data data ) -{ - auto ptr = (char*)tracy::tracy_malloc( data.len ); - memcpy( ptr, data.name, data.len ); - - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuContextName ); - tracy::MemWrite( &item->gpuContextNameFat.context, data.context ); - tracy::MemWrite( &item->gpuContextNameFat.ptr, (uint64_t)ptr ); - tracy::MemWrite( &item->gpuContextNameFat.size, data.len ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_calibration_serial( const struct ___tracy_gpu_calibration_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuCalibration ); - tracy::MemWrite( &item->gpuCalibration.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuCalibration.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuCalibration.cpuDelta, data.cpuDelta ); - tracy::MemWrite( &item->gpuCalibration.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API int ___tracy_connected( void ) -{ - return tracy::GetProfiler().IsConnected(); -} - -#ifdef TRACY_FIBERS -TRACY_API void ___tracy_fiber_enter( const char* fiber ){ tracy::Profiler::EnterFiber( fiber ); } -TRACY_API void ___tracy_fiber_leave( void ){ tracy::Profiler::LeaveFiber(); } -#endif - -# ifdef TRACY_MANUAL_LIFETIME -TRACY_API void ___tracy_startup_profiler( void ) -{ - tracy::StartupProfiler(); -} - -TRACY_API void ___tracy_shutdown_profiler( void ) -{ - tracy::ShutdownProfiler(); -} -# endif - -#ifdef __cplusplus -} -#endif -#endif - #endif diff --git a/Source/ThirdParty/tracy/client/TracyProfiler.hpp b/Source/ThirdParty/tracy/client/TracyProfiler.hpp index 99ae63e4f..8892fb14f 100644 --- a/Source/ThirdParty/tracy/client/TracyProfiler.hpp +++ b/Source/ThirdParty/tracy/client/TracyProfiler.hpp @@ -10,6 +10,7 @@ #include "tracy_concurrentqueue.h" #include "tracy_SPSCQueue.h" #include "TracyCallstack.hpp" +#include "TracySysPower.hpp" #include "TracySysTime.hpp" #include "TracyFastVector.hpp" #include "../common/TracyQueue.hpp" @@ -190,7 +191,22 @@ public: if( HardwareSupportsInvariantTSC() ) { uint64_t rax, rdx; +#ifdef TRACY_PATCHABLE_NOPSLEDS + // Some external tooling (such as rr) wants to patch our rdtsc and replace it by a + // branch to control the external input seen by a program. This kind of patching is + // not generally possible depending on the surrounding code and can lead to significant + // slowdowns if the compiler generated unlucky code and rr and tracy are used together. + // To avoid this, use the rr-safe `nopl 0(%rax, %rax, 1); rdtsc` instruction sequence, + // which rr promises will be patchable independent of the surrounding code. + asm volatile ( + // This is nopl 0(%rax, %rax, 1), but assemblers are inconsistent about whether + // they emit that as a 4 or 5 byte sequence and we need to be guaranteed to use + // the 5 byte one. + ".byte 0x0f, 0x1f, 0x44, 0x00, 0x00\n\t" + "rdtsc" : "=a" (rax), "=d" (rdx) ); +#else asm volatile ( "rdtsc" : "=a" (rax), "=d" (rdx) ); +#endif return (int64_t)(( rdx << 32 ) + rax); } # else @@ -240,6 +256,30 @@ public: p.m_serialLock.unlock(); } + static void SendFrameMark( const char* name ); + static void SendFrameMark( const char* name, QueueType type ); + static void SendFrameImage( const void* image, uint16_t w, uint16_t h, uint8_t offset, bool flip ); + static void PlotData( const char* name, int64_t val ); + static void PlotData( const char* name, float val ); + static void PlotData( const char* name, double val ); + static void ConfigurePlot( const char* name, PlotFormatType type, bool step, bool fill, uint32_t color ); + static void Message( const char* txt, size_t size, int callstack ); + static void Message( const char* txt, int callstack ); + static void MessageColor( const char* txt, size_t size, uint32_t color, int callstack ); + static void MessageColor( const char* txt, uint32_t color, int callstack ); + static void MessageAppInfo( const char* txt, size_t size ); + static void MemAlloc( const void* ptr, size_t size, bool secure ); + static void MemFree( const void* ptr, bool secure ); + static void MemAllocCallstack( const void* ptr, size_t size, int depth, bool secure ); + static void MemFreeCallstack( const void* ptr, int depth, bool secure ); + static void MemAllocNamed( const void* ptr, size_t size, bool secure, const char* name ); + static void MemFreeNamed( const void* ptr, bool secure, const char* name ); + static void MemAllocCallstackNamed( const void* ptr, size_t size, int depth, bool secure, const char* name ); + static void MemFreeCallstackNamed( const void* ptr, int depth, bool secure, const char* name ); + static void SendCallstack( int depth ); + static void ParameterRegister( ParameterCallback cb, void* data ); + static void ParameterSetup( uint32_t idx, const char* name, bool isBool, int32_t val ); + static tracy_force_inline void SourceCallbackRegister( SourceContentsCallback cb, void* data ) { auto& profiler = GetProfiler(); @@ -264,31 +304,6 @@ public: } #endif - static void SendFrameMark( const char* name ); - static void SendFrameMark( const char* name, QueueType type ); - static void SendFrameImage( const void* image, uint16_t w, uint16_t h, uint8_t offset, bool flip ); - static void PlotData( const char* name, int64_t val ); - static void PlotData( const char* name, float val ); - static void PlotData( const char* name, double val ); - static void ConfigurePlot( const char* name, PlotFormatType type, bool step, bool fill, uint32_t color ); - static void Message( const char* txt, size_t size, int callstack ); - static void Message( const char* txt, int callstack ); - static void MessageColor( const char* txt, size_t size, uint32_t color, int callstack ); - static void MessageColor( const char* txt, uint32_t color, int callstack ); - static void MessageAppInfo( const char* txt, size_t size ); - static void MemAlloc( const void* ptr, size_t size, bool secure ); - static void MemFree( const void* ptr, bool secure ); - static void MemAllocCallstack( const void* ptr, size_t size, int depth, bool secure ); - static void MemFreeCallstack( const void* ptr, int depth, bool secure ); - static void MemAllocNamed( const void* ptr, size_t size, bool secure, const char* name ); - static void MemFreeNamed( const void* ptr, bool secure, const char* name ); - static void MemAllocCallstackNamed( const void* ptr, size_t size, int depth, bool secure, const char* name ); - static void MemFreeCallstackNamed( const void* ptr, int depth, bool secure, const char* name ); - static void SendCallstack( int depth ); - static void ParameterRegister( ParameterCallback cb ); - static void ParameterRegister( ParameterCallback cb, void* data ); - static void ParameterSetup( uint32_t idx, const char* name, bool isBool, int32_t val ); - void SendCallstack( int depth, const char* skipBefore ); static void CutCallstack( void* callstack, const char* skipBefore ); @@ -299,6 +314,13 @@ public: return m_isConnected.load( std::memory_order_acquire ); } + tracy_force_inline void SetProgramName( const char* name ) + { + m_programNameLock.lock(); + m_programName = name; + m_programNameLock.unlock(); + } + #ifdef TRACY_ON_DEMAND tracy_force_inline uint64_t ConnectionId() const { @@ -347,13 +369,13 @@ public: static tracy_force_inline uint64_t AllocSourceLocation( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz ) { - return AllocSourceLocation( line, source, sourceSz, function, functionSz, (const char*)nullptr, 0 ); + return AllocSourceLocation( line, source, sourceSz, function, functionSz, nullptr, 0 ); } static tracy_force_inline uint64_t AllocSourceLocation( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const char* name, size_t nameSz ) { const auto sz32 = uint32_t( 2 + 4 + 4 + functionSz + 1 + sourceSz + 1 + nameSz ); - assert( sz32 <= std::numeric_limits::max() ); + assert( sz32 <= (std::numeric_limits::max)() ); const auto sz = uint16_t( sz32 ); auto ptr = (char*)tracy_malloc( sz ); memcpy( ptr, &sz, 2 ); @@ -370,28 +392,6 @@ public: return uint64_t( ptr ); } - static tracy_force_inline uint64_t AllocSourceLocation( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const Char* name, size_t nameSz ) - { - const auto sz32 = uint32_t( 2 + 4 + 4 + functionSz + 1 + sourceSz + 1 + nameSz ); - assert( sz32 <= std::numeric_limits::max() ); - const auto sz = uint16_t( sz32 ); - auto ptr = (char*)tracy_malloc( sz ); - memcpy( ptr, &sz, 2 ); - memset( ptr + 2, 0, 4 ); - memcpy( ptr + 6, &line, 4 ); - memcpy( ptr + 10, function, functionSz ); - ptr[10 + functionSz] = '\0'; - memcpy( ptr + 10 + functionSz + 1, source, sourceSz ); - ptr[10 + functionSz + 1 + sourceSz] = '\0'; - if( nameSz != 0 ) - { - char* dst = ptr + 10 + functionSz + 1 + sourceSz + 1; - for ( size_t i = 0; i < nameSz; i++) - dst[i] = (char)name[i]; - } - return uint64_t( ptr ); - } - private: enum class DequeueStatus { DataDequeued, ConnectionLost, QueueEmpty }; enum class ThreadCtxStatus { Same, Changed, ConnectionLost }; @@ -586,6 +586,10 @@ private: void ProcessSysTime() {} #endif +#ifdef TRACY_HAS_SYSPOWER + SysPower m_sysPower; +#endif + ParameterCallback m_paramCallback; void* m_paramCallbackData; SourceContentsCallback m_sourceCallback; @@ -604,6 +608,9 @@ private: } m_prevSignal; #endif bool m_crashHandlerInstalled; + + const char* m_programName; + TracyMutex m_programNameLock; }; } diff --git a/Source/ThirdParty/tracy/client/TracyScoped.hpp b/Source/ThirdParty/tracy/client/TracyScoped.hpp index 1e5b6c809..bb916aa57 100644 --- a/Source/ThirdParty/tracy/client/TracyScoped.hpp +++ b/Source/ThirdParty/tracy/client/TracyScoped.hpp @@ -20,19 +20,7 @@ void ScopedZone::Begin(const SourceLocationData* srcloc) TracyLfqPrepare( QueueType::ZoneBegin ); MemWrite( &item->zoneBegin.time, Profiler::GetTime() ); MemWrite( &item->zoneBegin.srcloc, (uint64_t)srcloc ); - TracyLfqCommit; -} - -void ScopedZone::Begin(uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const Char* name, size_t nameSz) -{ -#ifdef TRACY_ON_DEMAND - if (!GetProfiler().IsConnected()) return; -#endif - TracyLfqPrepare( QueueType::ZoneBeginAllocSrcLoc ); - const auto srcloc = Profiler::AllocSourceLocation( line, source, sourceSz, function, functionSz, name, nameSz ); - MemWrite( &item->zoneBegin.time, Profiler::GetTime() ); - MemWrite( &item->zoneBegin.srcloc, srcloc ); - TracyLfqCommit; + TracyQueueCommit( zoneBeginThread ); } void ScopedZone::End() @@ -42,7 +30,7 @@ void ScopedZone::End() #endif TracyLfqPrepare( QueueType::ZoneEnd ); MemWrite( &item->zoneEnd.time, Profiler::GetTime() ); - TracyLfqCommit; + TracyQueueCommit( zoneEndThread ); } ScopedZone::ScopedZone( const SourceLocationData* srcloc, bool is_active ) @@ -132,7 +120,7 @@ ScopedZone::~ScopedZone() void ScopedZone::Text( const char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); if( !m_active ) return; #ifdef TRACY_ON_DEMAND if( GetProfiler().ConnectionId() != m_connectionId ) return; @@ -147,7 +135,7 @@ void ScopedZone::Text( const char* txt, size_t size ) void ScopedZone::Text( const Char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); if( !m_active ) return; #ifdef TRACY_ON_DEMAND if( GetProfiler().ConnectionId() != m_connectionId ) return; @@ -163,7 +151,7 @@ void ScopedZone::Text( const Char* txt, size_t size ) void ScopedZone::Name( const char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); if( !m_active ) return; #ifdef TRACY_ON_DEMAND if( GetProfiler().ConnectionId() != m_connectionId ) return; @@ -178,7 +166,7 @@ void ScopedZone::Name( const char* txt, size_t size ) void ScopedZone::Name( const Char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); if( !m_active ) return; #ifdef TRACY_ON_DEMAND if( GetProfiler().ConnectionId() != m_connectionId ) return; @@ -199,9 +187,9 @@ void ScopedZone::Color( uint32_t color ) if( GetProfiler().ConnectionId() != m_connectionId ) return; #endif TracyQueuePrepare( QueueType::ZoneColor ); - MemWrite( &item->zoneColor.r, uint8_t( ( color ) & 0xFF ) ); + MemWrite( &item->zoneColor.b, uint8_t( ( color ) & 0xFF ) ); MemWrite( &item->zoneColor.g, uint8_t( ( color >> 8 ) & 0xFF ) ); - MemWrite( &item->zoneColor.b, uint8_t( ( color >> 16 ) & 0xFF ) ); + MemWrite( &item->zoneColor.r, uint8_t( ( color >> 16 ) & 0xFF ) ); TracyQueueCommit( zoneColorThread ); } diff --git a/Source/ThirdParty/tracy/client/TracySysPower.cpp b/Source/ThirdParty/tracy/client/TracySysPower.cpp new file mode 100644 index 000000000..bd5939da2 --- /dev/null +++ b/Source/ThirdParty/tracy/client/TracySysPower.cpp @@ -0,0 +1,164 @@ +#include "TracySysPower.hpp" + +#ifdef TRACY_HAS_SYSPOWER + +#include +#include +#include +#include +#include +#include + +#include "TracyDebug.hpp" +#include "TracyProfiler.hpp" +#include "../common/TracyAlloc.hpp" + +namespace tracy +{ + +SysPower::SysPower() + : m_domains( 4 ) + , m_lastTime( 0 ) +{ + ScanDirectory( "/sys/devices/virtual/powercap/intel-rapl", -1 ); +} + +SysPower::~SysPower() +{ + for( auto& v : m_domains ) + { + fclose( v.handle ); + // Do not release v.name, as it may be still needed + } +} + +void SysPower::Tick() +{ + auto t = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + if( t - m_lastTime > 10000000 ) // 10 ms + { + m_lastTime = t; + for( auto& v : m_domains ) + { + char tmp[32]; + if( fread( tmp, 1, 32, v.handle ) > 0 ) + { + rewind( v.handle ); + auto p = (uint64_t)atoll( tmp ); + uint64_t delta; + if( p >= v.value ) + { + delta = p - v.value; + } + else + { + delta = v.overflow - v.value + p; + } + v.value = p; + + TracyLfqPrepare( QueueType::SysPowerReport ); + MemWrite( &item->sysPower.time, Profiler::GetTime() ); + MemWrite( &item->sysPower.delta, delta ); + MemWrite( &item->sysPower.name, (uint64_t)v.name ); + TracyLfqCommit; + } + } + } +} + +void SysPower::ScanDirectory( const char* path, int parent ) +{ + DIR* dir = opendir( path ); + if( !dir ) return; + struct dirent* ent; + uint64_t maxRange = 0; + char* name = nullptr; + FILE* handle = nullptr; + while( ( ent = readdir( dir ) ) ) + { + if( ent->d_type == DT_REG ) + { + if( strcmp( ent->d_name, "max_energy_range_uj" ) == 0 ) + { + char tmp[PATH_MAX]; + snprintf( tmp, PATH_MAX, "%s/max_energy_range_uj", path ); + FILE* f = fopen( tmp, "r" ); + if( f ) + { + fscanf( f, "%" PRIu64, &maxRange ); + fclose( f ); + } + } + else if( strcmp( ent->d_name, "name" ) == 0 ) + { + char tmp[PATH_MAX]; + snprintf( tmp, PATH_MAX, "%s/name", path ); + FILE* f = fopen( tmp, "r" ); + if( f ) + { + char ntmp[128]; + if( fgets( ntmp, 128, f ) ) + { + // Last character is newline, skip it + const auto sz = strlen( ntmp ) - 1; + if( parent < 0 ) + { + name = (char*)tracy_malloc( sz + 1 ); + memcpy( name, ntmp, sz ); + name[sz] = '\0'; + } + else + { + const auto p = m_domains[parent]; + const auto psz = strlen( p.name ); + name = (char*)tracy_malloc( psz + sz + 2 ); + memcpy( name, p.name, psz ); + name[psz] = ':'; + memcpy( name+psz+1, ntmp, sz ); + name[psz+sz+1] = '\0'; + } + } + fclose( f ); + } + } + else if( strcmp( ent->d_name, "energy_uj" ) == 0 ) + { + char tmp[PATH_MAX]; + snprintf( tmp, PATH_MAX, "%s/energy_uj", path ); + handle = fopen( tmp, "r" ); + } + } + if( name && handle && maxRange > 0 ) break; + } + if( name && handle && maxRange > 0 ) + { + parent = (int)m_domains.size(); + Domain* domain = m_domains.push_next(); + domain->value = 0; + domain->overflow = maxRange; + domain->handle = handle; + domain->name = name; + TracyDebug( "Power domain id %i, %s found at %s\n", parent, name, path ); + } + else + { + if( name ) tracy_free( name ); + if( handle ) fclose( handle ); + } + + rewinddir( dir ); + while( ( ent = readdir( dir ) ) ) + { + if( ent->d_type == DT_DIR && strncmp( ent->d_name, "intel-rapl:", 11 ) == 0 ) + { + char tmp[PATH_MAX]; + snprintf( tmp, PATH_MAX, "%s/%s", path, ent->d_name ); + ScanDirectory( tmp, parent ); + } + } + closedir( dir ); +} + +} + +#endif diff --git a/Source/ThirdParty/tracy/client/TracySysPower.hpp b/Source/ThirdParty/tracy/client/TracySysPower.hpp new file mode 100644 index 000000000..210123bce --- /dev/null +++ b/Source/ThirdParty/tracy/client/TracySysPower.hpp @@ -0,0 +1,44 @@ +#ifndef __TRACYSYSPOWER_HPP__ +#define __TRACYSYSPOWER_HPP__ + +#if defined __linux__ +# define TRACY_HAS_SYSPOWER +#endif + +#ifdef TRACY_HAS_SYSPOWER + +#include +#include + +#include "TracyFastVector.hpp" + +namespace tracy +{ + +class SysPower +{ + struct Domain + { + uint64_t value; + uint64_t overflow; + FILE* handle; + const char* name; + }; + +public: + SysPower(); + ~SysPower(); + + void Tick(); + +private: + void ScanDirectory( const char* path, int parent ); + + FastVector m_domains; + uint64_t m_lastTime; +}; + +} +#endif + +#endif diff --git a/Source/ThirdParty/tracy/client/TracySysTrace.cpp b/Source/ThirdParty/tracy/client/TracySysTrace.cpp index 23b1020a5..af0641fef 100644 --- a/Source/ThirdParty/tracy/client/TracySysTrace.cpp +++ b/Source/ThirdParty/tracy/client/TracySysTrace.cpp @@ -409,6 +409,7 @@ bool SysTraceStart( int64_t& samplingPeriod ) return false; } +#ifndef TRACY_NO_SAMPLING if( isOs64Bit ) { CLASSIC_EVENT_ID stackId[2] = {}; @@ -423,6 +424,7 @@ bool SysTraceStart( int64_t& samplingPeriod ) return false; } } +#endif #ifdef UNICODE WCHAR KernelLoggerName[sizeof( KERNEL_LOGGER_NAME )]; @@ -768,6 +770,13 @@ bool SysTraceStart( int64_t& samplingPeriod ) TracyDebug( "sched_wakeup id: %i\n", wakeupId ); TracyDebug( "drm_vblank_event id: %i\n", vsyncId ); +#ifdef TRACY_NO_SAMPLING + const bool noSoftwareSampling = true; +#else + const char* noSoftwareSamplingEnv = GetEnvVar( "TRACY_NO_SAMPLING" ); + const bool noSoftwareSampling = noSoftwareSamplingEnv && noSoftwareSamplingEnv[0] == '1'; +#endif + #ifdef TRACY_NO_SAMPLE_RETIREMENT const bool noRetirement = true; #else @@ -837,28 +846,31 @@ bool SysTraceStart( int64_t& samplingPeriod ) pe.clockid = CLOCK_MONOTONIC_RAW; #endif - TracyDebug( "Setup software sampling\n" ); - ProbePreciseIp( pe, currentPid ); - for( int i=0; i static inline bool circular_less_than(T a, T b) { static_assert(std::is_integral::value && !std::numeric_limits::is_signed, "circular_less_than is intended to be used only with unsigned integer types"); - return static_cast(a - b) > (static_cast(static_cast(1) << static_cast(sizeof(T) * CHAR_BIT - 1))); + return static_cast(a - b) > static_cast(static_cast(1) << (static_cast(sizeof(T) * CHAR_BIT - 1))); + // Note: extra parens around rhs of operator<< is MSVC bug: https://developercommunity2.visualstudio.com/t/C4554-triggers-when-both-lhs-and-rhs-is/10034931 + // silencing the bug requires #pragma warning(disable: 4554) around the calling code and has no effect when done here. } -#ifdef _MSC_VER -#pragma warning(pop) -#endif template static inline char* align_for(char* ptr) diff --git a/Source/ThirdParty/tracy/client/tracy_rpmalloc.cpp b/Source/ThirdParty/tracy/client/tracy_rpmalloc.cpp index 8efa626a9..711505d21 100644 --- a/Source/ThirdParty/tracy/client/tracy_rpmalloc.cpp +++ b/Source/ThirdParty/tracy/client/tracy_rpmalloc.cpp @@ -147,7 +147,7 @@ # if defined(__APPLE__) # include # if !TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR -# include +# include # include # endif # include diff --git a/Source/ThirdParty/tracy/common/TracyProtocol.hpp b/Source/ThirdParty/tracy/common/TracyProtocol.hpp index dd30e5391..5eb1639db 100644 --- a/Source/ThirdParty/tracy/common/TracyProtocol.hpp +++ b/Source/ThirdParty/tracy/common/TracyProtocol.hpp @@ -9,14 +9,14 @@ namespace tracy constexpr unsigned Lz4CompressBound( unsigned isize ) { return isize + ( isize / 255 ) + 16; } -enum : uint32_t { ProtocolVersion = 63 }; +enum : uint32_t { ProtocolVersion = 64 }; enum : uint16_t { BroadcastVersion = 3 }; using lz4sz_t = uint32_t; enum { TargetFrameSize = 256 * 1024 }; enum { LZ4Size = Lz4CompressBound( TargetFrameSize ) }; -static_assert( LZ4Size <= std::numeric_limits::max(), "LZ4Size greater than lz4sz_t" ); +static_assert( LZ4Size <= (std::numeric_limits::max)(), "LZ4Size greater than lz4sz_t" ); static_assert( TargetFrameSize * 2 >= 64 * 1024, "Not enough space for LZ4 stream buffer" ); enum { HandshakeShibbolethSize = 8 }; diff --git a/Source/ThirdParty/tracy/common/TracyQueue.hpp b/Source/ThirdParty/tracy/common/TracyQueue.hpp index 092d26969..051d412ab 100644 --- a/Source/ThirdParty/tracy/common/TracyQueue.hpp +++ b/Source/ThirdParty/tracy/common/TracyQueue.hpp @@ -1,6 +1,7 @@ #ifndef __TRACYQUEUE_HPP__ #define __TRACYQUEUE_HPP__ +#include #include namespace tracy @@ -89,6 +90,7 @@ enum class QueueType : uint8_t GpuNewContext, CallstackFrame, SysTimeReport, + SysPowerReport, TidToPid, HwSampleCpuCycle, HwSampleInstructionRetired, @@ -165,9 +167,9 @@ struct QueueZoneValidationThread : public QueueZoneValidation struct QueueZoneColor { - uint8_t r; - uint8_t g; uint8_t b; + uint8_t g; + uint8_t r; }; struct QueueZoneColorThread : public QueueZoneColor @@ -221,9 +223,9 @@ struct QueueSourceLocation uint64_t function; // ptr uint64_t file; // ptr uint32_t line; - uint8_t r; - uint8_t g; uint8_t b; + uint8_t g; + uint8_t r; }; struct QueueZoneTextFat @@ -324,7 +326,7 @@ struct QueuePlotDataInt : public QueuePlotDataBase int64_t val; }; -struct QueuePlotDataFloat : public QueuePlotDataBase +struct QueuePlotDataFloat : public QueuePlotDataBase { float val; }; @@ -341,9 +343,9 @@ struct QueueMessage struct QueueMessageColor : public QueueMessage { - uint8_t r; - uint8_t g; uint8_t b; + uint8_t g; + uint8_t r; }; struct QueueMessageLiteral : public QueueMessage @@ -562,6 +564,13 @@ struct QueueSysTime float sysTime; }; +struct QueueSysPower +{ + int64_t time; + uint64_t delta; + uint64_t name; // ptr +}; + struct QueueContextSwitch { int64_t time; @@ -590,6 +599,13 @@ struct QueueHwSample int64_t time; }; +enum class PlotFormatType : uint8_t +{ + Number, + Memory, + Percentage +}; + struct QueuePlotConfig { uint64_t name; // ptr @@ -721,6 +737,7 @@ struct QueueItem QueueCrashReport crashReport; QueueCrashReportThread crashReportThread; QueueSysTime sysTime; + QueueSysPower sysPower; QueueContextSwitch contextSwitch; QueueThreadWakeup threadWakeup; QueueTidToPid tidToPid; @@ -824,6 +841,7 @@ static constexpr size_t QueueDataSize[] = { sizeof( QueueHeader ) + sizeof( QueueGpuNewContext ), sizeof( QueueHeader ) + sizeof( QueueCallstackFrame ), sizeof( QueueHeader ) + sizeof( QueueSysTime ), + sizeof( QueueHeader ) + sizeof( QueueSysPower ), sizeof( QueueHeader ) + sizeof( QueueTidToPid ), sizeof( QueueHeader ) + sizeof( QueueHwSample ), // cpu cycle sizeof( QueueHeader ) + sizeof( QueueHwSample ), // instruction retired diff --git a/Source/ThirdParty/tracy/common/TracySocket.cpp b/Source/ThirdParty/tracy/common/TracySocket.cpp index 176bbc7aa..259678989 100644 --- a/Source/ThirdParty/tracy/common/TracySocket.cpp +++ b/Source/ThirdParty/tracy/common/TracySocket.cpp @@ -353,7 +353,7 @@ int Socket::Recv( void* _buf, int len, int timeout ) } } -int Socket::ReadUpTo( void* _buf, int len, int timeout ) +int Socket::ReadUpTo( void* _buf, int len ) { const auto sock = m_sock.load( std::memory_order_relaxed ); auto buf = (char*)_buf; @@ -678,10 +678,10 @@ bool UdpListen::Listen( uint16_t port ) #endif #if defined _WIN32 unsigned long reuse = 1; - setsockopt( m_sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof( reuse ) ); + setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof( reuse ) ); #else int reuse = 1; - setsockopt( m_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) ); + setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) ); #endif #if defined _WIN32 unsigned long broadcast = 1; diff --git a/Source/ThirdParty/tracy/common/TracySocket.hpp b/Source/ThirdParty/tracy/common/TracySocket.hpp index 4b3075e29..f7713aac6 100644 --- a/Source/ThirdParty/tracy/common/TracySocket.hpp +++ b/Source/ThirdParty/tracy/common/TracySocket.hpp @@ -29,7 +29,7 @@ public: int Send( const void* buf, int len ); int GetSendBufSize(); - int ReadUpTo( void* buf, int len, int timeout ); + int ReadUpTo( void* buf, int len ); bool Read( void* buf, int len, int timeout ); template diff --git a/Source/ThirdParty/tracy/common/TracySystem.cpp b/Source/ThirdParty/tracy/common/TracySystem.cpp index 5ca8e1f45..9a477aa31 100644 --- a/Source/ThirdParty/tracy/common/TracySystem.cpp +++ b/Source/ThirdParty/tracy/common/TracySystem.cpp @@ -205,61 +205,60 @@ TRACY_API const char* GetThreadName( uint32_t id ) } ptr = ptr->next; } -#else -# if defined _WIN32 -# ifdef TRACY_UWP - static auto _GetThreadDescription = &::GetThreadDescription; -# else - static auto _GetThreadDescription = (t_GetThreadDescription)GetProcAddress( GetModuleHandleA( "kernel32.dll" ), "GetThreadDescription" ); -# endif +#endif + +#if defined _WIN32 +# ifdef TRACY_UWP + static auto _GetThreadDescription = &::GetThreadDescription; +# else + static auto _GetThreadDescription = (t_GetThreadDescription)GetProcAddress( GetModuleHandleA( "kernel32.dll" ), "GetThreadDescription" ); +# endif if( _GetThreadDescription ) { auto hnd = OpenThread( THREAD_QUERY_LIMITED_INFORMATION, FALSE, (DWORD)id ); if( hnd != 0 ) { PWSTR tmp; - _GetThreadDescription( hnd, &tmp ); - auto ret = wcstombs( buf, tmp, 256 ); - CloseHandle( hnd ); - if( ret != 0 ) + if( SUCCEEDED( _GetThreadDescription( hnd, &tmp ) ) ) { - return buf; + auto ret = wcstombs( buf, tmp, 256 ); + CloseHandle( hnd ); + LocalFree( tmp ); + if( ret != static_cast( -1 ) ) + { + return buf; + } } } } -# elif defined __linux__ - int cs, fd; - char path[32]; -# ifdef __ANDROID__ - int tid = gettid(); -# else - int tid = (int) syscall( SYS_gettid ); -# endif - snprintf( path, sizeof( path ), "/proc/self/task/%d/comm", tid ); - sprintf( buf, "%" PRIu32, id ); -# ifndef __ANDROID__ - pthread_setcancelstate( PTHREAD_CANCEL_DISABLE, &cs ); -# endif - if ( ( fd = open( path, O_RDONLY ) ) > 0) { - int len = read( fd, buf, 255 ); - if( len > 0 ) - { - buf[len] = 0; - if( len > 1 && buf[len-1] == '\n' ) - { - buf[len-1] = 0; - } - } - close( fd ); - } -# ifndef __ANDROID__ - pthread_setcancelstate( cs, 0 ); -# endif - return buf; -# endif +#elif defined __linux__ + int cs, fd; + char path[32]; + snprintf( path, sizeof( path ), "/proc/self/task/%d/comm", id ); + sprintf( buf, "%" PRIu32, id ); +# ifndef __ANDROID__ + pthread_setcancelstate( PTHREAD_CANCEL_DISABLE, &cs ); +# endif + if ( ( fd = open( path, O_RDONLY ) ) > 0) { + int len = read( fd, buf, 255 ); + if( len > 0 ) + { + buf[len] = 0; + if( len > 1 && buf[len-1] == '\n' ) + { + buf[len-1] = 0; + } + } + close( fd ); + } +# ifndef __ANDROID__ + pthread_setcancelstate( cs, 0 ); +# endif + return buf; #endif - sprintf( buf, "%" PRIu32, id ); - return buf; + + sprintf( buf, "%" PRIu32, id ); + return buf; } TRACY_API const char* GetEnvVar( const char* name ) @@ -295,3 +294,13 @@ TRACY_API const char* GetEnvVar( const char* name ) } } + +#ifdef __cplusplus +extern "C" { +#endif + +TRACY_API void ___tracy_set_thread_name( const char* name ) { tracy::SetThreadName( name ); } + +#ifdef __cplusplus +} +#endif diff --git a/Source/ThirdParty/tracy/common/TracySystem.hpp b/Source/ThirdParty/tracy/common/TracySystem.hpp index edcc5cf31..7a88a00b1 100644 --- a/Source/ThirdParty/tracy/common/TracySystem.hpp +++ b/Source/ThirdParty/tracy/common/TracySystem.hpp @@ -25,13 +25,6 @@ namespace tracy { -enum class PlotFormatType : uint8_t -{ - Number, - Memory, - Percentage -}; - typedef void(*ParameterCallback)( void* data, uint32_t idx, int32_t val ); struct TRACY_API SourceLocationData @@ -47,7 +40,6 @@ class TRACY_API ScopedZone { public: static void Begin( const SourceLocationData* srcloc ); - static void Begin( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const Char* name, size_t nameSz ); static void End(); ScopedZone( const ScopedZone& ) = delete; @@ -68,7 +60,6 @@ public: void Name( const Char* txt, size_t size ); void Color( uint32_t color ); void Value( uint64_t value ); - bool IsActive() const; private: const bool m_active; diff --git a/Source/ThirdParty/tracy/common/TracyVersion.hpp b/Source/ThirdParty/tracy/common/TracyVersion.hpp index 983d1c51f..2355279f7 100644 --- a/Source/ThirdParty/tracy/common/TracyVersion.hpp +++ b/Source/ThirdParty/tracy/common/TracyVersion.hpp @@ -6,7 +6,7 @@ namespace tracy namespace Version { enum { Major = 0 }; -enum { Minor = 9 }; +enum { Minor = 10 }; enum { Patch = 0 }; } } diff --git a/Source/ThirdParty/tracy/libbacktrace/config.h b/Source/ThirdParty/tracy/libbacktrace/config.h index aa3259d11..87e38a95b 100644 --- a/Source/ThirdParty/tracy/libbacktrace/config.h +++ b/Source/ThirdParty/tracy/libbacktrace/config.h @@ -1,4 +1,8 @@ #include +#if defined(__linux__) && !defined(__GLIBC__) && !defined(__WORDSIZE) +// include __WORDSIZE headers for musl +# include +#endif #if __WORDSIZE == 64 # define BACKTRACE_ELF_SIZE 64 #else diff --git a/Source/ThirdParty/tracy/libbacktrace/dwarf.cpp b/Source/ThirdParty/tracy/libbacktrace/dwarf.cpp index 246cb9f36..f3899cbce 100644 --- a/Source/ThirdParty/tracy/libbacktrace/dwarf.cpp +++ b/Source/ThirdParty/tracy/libbacktrace/dwarf.cpp @@ -473,7 +473,7 @@ enum attr_val_encoding /* An address. */ ATTR_VAL_ADDRESS, /* An index into the .debug_addr section, whose value is relative to - * the DW_AT_addr_base attribute of the compilation unit. */ + the DW_AT_addr_base attribute of the compilation unit. */ ATTR_VAL_ADDRESS_INDEX, /* A unsigned integer. */ ATTR_VAL_UINT, @@ -611,8 +611,8 @@ struct function struct function_addrs { /* Range is LOW <= PC < HIGH. */ - uint64_t low; - uint64_t high; + uintptr_t low; + uintptr_t high; /* Function for this address range. */ struct function *function; }; @@ -693,8 +693,8 @@ struct unit struct unit_addrs { /* Range is LOW <= PC < HIGH. */ - uint64_t low; - uint64_t high; + uintptr_t low; + uintptr_t high; /* Compilation unit for this address range. */ struct unit *u; }; @@ -1431,7 +1431,7 @@ resolve_addr_index (const struct dwarf_sections *dwarf_sections, uint64_t addr_base, int addrsize, int is_bigendian, uint64_t addr_index, backtrace_error_callback error_callback, void *data, - uint64_t *address) + uintptr_t *address) { uint64_t offset; struct dwarf_buf addr_buf; @@ -1452,7 +1452,7 @@ resolve_addr_index (const struct dwarf_sections *dwarf_sections, addr_buf.data = data; addr_buf.reported_underflow = 0; - *address = read_address (&addr_buf, addrsize); + *address = (uintptr_t) read_address (&addr_buf, addrsize); return 1; } @@ -1531,7 +1531,7 @@ function_addrs_search (const void *vkey, const void *ventry) static int add_unit_addr (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *pvec) { @@ -1867,10 +1867,10 @@ lookup_abbrev (struct abbrevs *abbrevs, uint64_t code, lowpc/highpc is set or ranges is set. */ struct pcrange { - uint64_t lowpc; /* The low PC value. */ + uintptr_t lowpc; /* The low PC value. */ int have_lowpc; /* Whether a low PC value was found. */ int lowpc_is_addr_index; /* Whether lowpc is in .debug_addr. */ - uint64_t highpc; /* The high PC value. */ + uintptr_t highpc; /* The high PC value. */ int have_highpc; /* Whether a high PC value was found. */ int highpc_is_relative; /* Whether highpc is relative to lowpc. */ int highpc_is_addr_index; /* Whether highpc is in .debug_addr. */ @@ -1890,12 +1890,12 @@ update_pcrange (const struct attr* attr, const struct attr_val* val, case DW_AT_low_pc: if (val->encoding == ATTR_VAL_ADDRESS) { - pcrange->lowpc = val->u.uint; + pcrange->lowpc = (uintptr_t) val->u.uint; pcrange->have_lowpc = 1; } else if (val->encoding == ATTR_VAL_ADDRESS_INDEX) { - pcrange->lowpc = val->u.uint; + pcrange->lowpc = (uintptr_t) val->u.uint; pcrange->have_lowpc = 1; pcrange->lowpc_is_addr_index = 1; } @@ -1904,18 +1904,18 @@ update_pcrange (const struct attr* attr, const struct attr_val* val, case DW_AT_high_pc: if (val->encoding == ATTR_VAL_ADDRESS) { - pcrange->highpc = val->u.uint; + pcrange->highpc = (uintptr_t) val->u.uint; pcrange->have_highpc = 1; } else if (val->encoding == ATTR_VAL_UINT) { - pcrange->highpc = val->u.uint; + pcrange->highpc = (uintptr_t) val->u.uint; pcrange->have_highpc = 1; pcrange->highpc_is_relative = 1; } else if (val->encoding == ATTR_VAL_ADDRESS_INDEX) { - pcrange->highpc = val->u.uint; + pcrange->highpc = (uintptr_t) val->u.uint; pcrange->have_highpc = 1; pcrange->highpc_is_addr_index = 1; } @@ -1950,16 +1950,16 @@ add_low_high_range (struct backtrace_state *state, uintptr_t base_address, int is_bigendian, struct unit *u, const struct pcrange *pcrange, int (*add_range) (struct backtrace_state *state, - void *rdata, uint64_t lowpc, - uint64_t highpc, + void *rdata, uintptr_t lowpc, + uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *vec), void *rdata, backtrace_error_callback error_callback, void *data, void *vec) { - uint64_t lowpc; - uint64_t highpc; + uintptr_t lowpc; + uintptr_t highpc; lowpc = pcrange->lowpc; if (pcrange->lowpc_is_addr_index) @@ -1997,10 +1997,10 @@ add_ranges_from_ranges ( struct backtrace_state *state, const struct dwarf_sections *dwarf_sections, uintptr_t base_address, int is_bigendian, - struct unit *u, uint64_t base, + struct unit *u, uintptr_t base, const struct pcrange *pcrange, int (*add_range) (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *vec), void *rdata, @@ -2039,12 +2039,12 @@ add_ranges_from_ranges ( break; if (is_highest_address (low, u->addrsize)) - base = high; + base = (uintptr_t) high; else { if (!add_range (state, rdata, - low + base + base_address, - high + base + base_address, + (uintptr_t) low + base + base_address, + (uintptr_t) high + base + base_address, error_callback, data, vec)) return 0; } @@ -2064,10 +2064,10 @@ add_ranges_from_rnglists ( struct backtrace_state *state, const struct dwarf_sections *dwarf_sections, uintptr_t base_address, int is_bigendian, - struct unit *u, uint64_t base, + struct unit *u, uintptr_t base, const struct pcrange *pcrange, int (*add_range) (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *vec), void *rdata, @@ -2133,8 +2133,8 @@ add_ranges_from_rnglists ( case DW_RLE_startx_endx: { uint64_t index; - uint64_t low; - uint64_t high; + uintptr_t low; + uintptr_t high; index = read_uleb128 (&rnglists_buf); if (!resolve_addr_index (dwarf_sections, u->addr_base, @@ -2156,8 +2156,8 @@ add_ranges_from_rnglists ( case DW_RLE_startx_length: { uint64_t index; - uint64_t low; - uint64_t length; + uintptr_t low; + uintptr_t length; index = read_uleb128 (&rnglists_buf); if (!resolve_addr_index (dwarf_sections, u->addr_base, @@ -2187,16 +2187,16 @@ add_ranges_from_rnglists ( break; case DW_RLE_base_address: - base = read_address (&rnglists_buf, u->addrsize); + base = (uintptr_t) read_address (&rnglists_buf, u->addrsize); break; case DW_RLE_start_end: { - uint64_t low; - uint64_t high; + uintptr_t low; + uintptr_t high; - low = read_address (&rnglists_buf, u->addrsize); - high = read_address (&rnglists_buf, u->addrsize); + low = (uintptr_t) read_address (&rnglists_buf, u->addrsize); + high = (uintptr_t) read_address (&rnglists_buf, u->addrsize); if (!add_range (state, rdata, low + base_address, high + base_address, error_callback, data, vec)) @@ -2206,11 +2206,11 @@ add_ranges_from_rnglists ( case DW_RLE_start_length: { - uint64_t low; - uint64_t length; + uintptr_t low; + uintptr_t length; - low = read_address (&rnglists_buf, u->addrsize); - length = read_uleb128 (&rnglists_buf); + low = (uintptr_t) read_address (&rnglists_buf, u->addrsize); + length = (uintptr_t) read_uleb128 (&rnglists_buf); low += base_address; if (!add_range (state, rdata, low, low + length, error_callback, data, vec)) @@ -2240,9 +2240,9 @@ static int add_ranges (struct backtrace_state *state, const struct dwarf_sections *dwarf_sections, uintptr_t base_address, int is_bigendian, - struct unit *u, uint64_t base, const struct pcrange *pcrange, + struct unit *u, uintptr_t base, const struct pcrange *pcrange, int (*add_range) (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *vec), void *rdata, @@ -3520,7 +3520,7 @@ read_referenced_name (struct dwarf_data *ddata, struct unit *u, static int add_function_range (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *pvec) { @@ -3560,7 +3560,7 @@ add_function_range (struct backtrace_state *state, void *rdata, static int read_function_entry (struct backtrace_state *state, struct dwarf_data *ddata, - struct unit *u, uint64_t base, struct dwarf_buf *unit_buf, + struct unit *u, uintptr_t base, struct dwarf_buf *unit_buf, const struct line_header *lhdr, backtrace_error_callback error_callback, void *data, struct function_vector *vec_function, @@ -3624,7 +3624,7 @@ read_function_entry (struct backtrace_state *state, struct dwarf_data *ddata, && abbrev->attrs[i].name == DW_AT_low_pc) { if (val.encoding == ATTR_VAL_ADDRESS) - base = val.u.uint; + base = (uintptr_t) val.u.uint; else if (val.encoding == ATTR_VAL_ADDRESS_INDEX) { if (!resolve_addr_index (&ddata->dwarf_sections, diff --git a/Source/ThirdParty/tracy/libbacktrace/elf.cpp b/Source/ThirdParty/tracy/libbacktrace/elf.cpp index 9e62f090d..c65bc4e76 100644 --- a/Source/ThirdParty/tracy/libbacktrace/elf.cpp +++ b/Source/ThirdParty/tracy/libbacktrace/elf.cpp @@ -193,6 +193,7 @@ dl_iterate_phdr (int (*callback) (struct dl_phdr_info *, #undef STT_FUNC #undef NT_GNU_BUILD_ID #undef ELFCOMPRESS_ZLIB +#undef ELFCOMPRESS_ZSTD /* Basic types. */ @@ -350,6 +351,7 @@ typedef struct #endif /* BACKTRACE_ELF_SIZE != 32 */ #define ELFCOMPRESS_ZLIB 1 +#define ELFCOMPRESS_ZSTD 2 /* Names of sections, indexed by enum dwarf_section in internal.h. */ @@ -1130,7 +1132,7 @@ elf_uncompress_failed(void) on error. */ static int -elf_zlib_fetch (const unsigned char **ppin, const unsigned char *pinend, +elf_fetch_bits (const unsigned char **ppin, const unsigned char *pinend, uint64_t *pval, unsigned int *pbits) { unsigned int bits; @@ -1177,6 +1179,118 @@ elf_zlib_fetch (const unsigned char **ppin, const unsigned char *pinend, return 1; } +/* This is like elf_fetch_bits, but it fetchs the bits backward, and ensures at + least 16 bits. This is for zstd. */ + +static int +elf_fetch_bits_backward (const unsigned char **ppin, + const unsigned char *pinend, + uint64_t *pval, unsigned int *pbits) +{ + unsigned int bits; + const unsigned char *pin; + uint64_t val; + uint32_t next; + + bits = *pbits; + if (bits >= 16) + return 1; + pin = *ppin; + val = *pval; + + if (unlikely (pin <= pinend)) + { + if (bits == 0) + { + elf_uncompress_failed (); + return 0; + } + return 1; + } + + pin -= 4; + +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) \ + && defined(__ORDER_BIG_ENDIAN__) \ + && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ \ + || __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + /* We've ensured that PIN is aligned. */ + next = *(const uint32_t *)pin; + +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + next = __builtin_bswap32 (next); +#endif +#else + next = pin[0] | (pin[1] << 8) | (pin[2] << 16) | (pin[3] << 24); +#endif + + val <<= 32; + val |= next; + bits += 32; + + if (unlikely (pin < pinend)) + { + val >>= (pinend - pin) * 8; + bits -= (pinend - pin) * 8; + } + + *ppin = pin; + *pval = val; + *pbits = bits; + return 1; +} + +/* Initialize backward fetching when the bitstream starts with a 1 bit in the + last byte in memory (which is the first one that we read). This is used by + zstd decompression. Returns 1 on success, 0 on error. */ + +static int +elf_fetch_backward_init (const unsigned char **ppin, + const unsigned char *pinend, + uint64_t *pval, unsigned int *pbits) +{ + const unsigned char *pin; + unsigned int stream_start; + uint64_t val; + unsigned int bits; + + pin = *ppin; + stream_start = (unsigned int)*pin; + if (unlikely (stream_start == 0)) + { + elf_uncompress_failed (); + return 0; + } + val = 0; + bits = 0; + + /* Align to a 32-bit boundary. */ + while ((((uintptr_t)pin) & 3) != 0) + { + val <<= 8; + val |= (uint64_t)*pin; + bits += 8; + --pin; + } + + val <<= 8; + val |= (uint64_t)*pin; + bits += 8; + + *ppin = pin; + *pval = val; + *pbits = bits; + if (!elf_fetch_bits_backward (ppin, pinend, pval, pbits)) + return 0; + + *pbits -= __builtin_clz (stream_start) - (sizeof (unsigned int) - 1) * 8 + 1; + + if (!elf_fetch_bits_backward (ppin, pinend, pval, pbits)) + return 0; + + return 1; +} + /* Huffman code tables, like the rest of the zlib format, are defined by RFC 1951. We store a Huffman code table as a series of tables stored sequentially in memory. Each entry in a table is 16 bits. @@ -1211,14 +1325,14 @@ elf_zlib_fetch (const unsigned char **ppin, const unsigned char *pinend, /* Number of entries we allocate to for one code table. We get a page for the two code tables we need. */ -#define HUFFMAN_TABLE_SIZE (1024) +#define ZLIB_HUFFMAN_TABLE_SIZE (1024) /* Bit masks and shifts for the values in the table. */ -#define HUFFMAN_VALUE_MASK 0x01ff -#define HUFFMAN_BITS_SHIFT 9 -#define HUFFMAN_BITS_MASK 0x7 -#define HUFFMAN_SECONDARY_SHIFT 12 +#define ZLIB_HUFFMAN_VALUE_MASK 0x01ff +#define ZLIB_HUFFMAN_BITS_SHIFT 9 +#define ZLIB_HUFFMAN_BITS_MASK 0x7 +#define ZLIB_HUFFMAN_SECONDARY_SHIFT 12 /* For working memory while inflating we need two code tables, we need an array of code lengths (max value 15, so we use unsigned char), @@ -1226,17 +1340,17 @@ elf_zlib_fetch (const unsigned char **ppin, const unsigned char *pinend, latter two arrays must be large enough to hold the maximum number of code lengths, which RFC 1951 defines as 286 + 30. */ -#define ZDEBUG_TABLE_SIZE \ - (2 * HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \ +#define ZLIB_TABLE_SIZE \ + (2 * ZLIB_HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \ + (286 + 30) * sizeof (uint16_t) \ + (286 + 30) * sizeof (unsigned char)) -#define ZDEBUG_TABLE_CODELEN_OFFSET \ - (2 * HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \ +#define ZLIB_TABLE_CODELEN_OFFSET \ + (2 * ZLIB_HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \ + (286 + 30) * sizeof (uint16_t)) -#define ZDEBUG_TABLE_WORK_OFFSET \ - (2 * HUFFMAN_TABLE_SIZE * sizeof (uint16_t)) +#define ZLIB_TABLE_WORK_OFFSET \ + (2 * ZLIB_HUFFMAN_TABLE_SIZE * sizeof (uint16_t)) #ifdef BACKTRACE_GENERATE_FIXED_HUFFMAN_TABLE @@ -1269,7 +1383,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, next value after VAL with the same bit length. */ next = (uint16_t *) (((unsigned char *) zdebug_table) - + ZDEBUG_TABLE_WORK_OFFSET); + + ZLIB_TABLE_WORK_OFFSET); memset (&count[0], 0, 16 * sizeof (uint16_t)); for (i = 0; i < codes_len; ++i) @@ -1297,7 +1411,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, /* For each length, fill in the table for the codes of that length. */ - memset (table, 0, HUFFMAN_TABLE_SIZE * sizeof (uint16_t)); + memset (table, 0, ZLIB_HUFFMAN_TABLE_SIZE * sizeof (uint16_t)); /* Handle the values that do not require a secondary table. */ @@ -1331,13 +1445,13 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, /* In the compressed bit stream, the value VAL is encoded as J bits with the value C. */ - if (unlikely ((val & ~HUFFMAN_VALUE_MASK) != 0)) + if (unlikely ((val & ~ZLIB_HUFFMAN_VALUE_MASK) != 0)) { elf_uncompress_failed (); return 0; } - tval = val | ((j - 1) << HUFFMAN_BITS_SHIFT); + tval = val | ((j - 1) << ZLIB_HUFFMAN_BITS_SHIFT); /* The table lookup uses 8 bits. If J is less than 8, we don't know what the other bits will be. We need to fill @@ -1487,7 +1601,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, { /* Start a new secondary table. */ - if (unlikely ((next_secondary & HUFFMAN_VALUE_MASK) + if (unlikely ((next_secondary & ZLIB_HUFFMAN_VALUE_MASK) != next_secondary)) { elf_uncompress_failed (); @@ -1498,22 +1612,23 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, secondary_bits = j - 8; next_secondary += 1 << secondary_bits; table[primary] = (secondary - + ((j - 8) << HUFFMAN_BITS_SHIFT) - + (1U << HUFFMAN_SECONDARY_SHIFT)); + + ((j - 8) << ZLIB_HUFFMAN_BITS_SHIFT) + + (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)); } else { /* There is an existing entry. It had better be a secondary table with enough bits. */ - if (unlikely ((tprimary & (1U << HUFFMAN_SECONDARY_SHIFT)) + if (unlikely ((tprimary + & (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)) == 0)) { elf_uncompress_failed (); return 0; } - secondary = tprimary & HUFFMAN_VALUE_MASK; - secondary_bits = ((tprimary >> HUFFMAN_BITS_SHIFT) - & HUFFMAN_BITS_MASK); + secondary = tprimary & ZLIB_HUFFMAN_VALUE_MASK; + secondary_bits = ((tprimary >> ZLIB_HUFFMAN_BITS_SHIFT) + & ZLIB_HUFFMAN_BITS_MASK); if (unlikely (secondary_bits < j - 8)) { elf_uncompress_failed (); @@ -1524,7 +1639,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, /* Fill in secondary table entries. */ - tval = val | ((j - 8) << HUFFMAN_BITS_SHIFT); + tval = val | ((j - 8) << ZLIB_HUFFMAN_BITS_SHIFT); for (ind = code >> 8; ind < (1U << secondary_bits); @@ -1567,7 +1682,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, #include -static uint16_t table[ZDEBUG_TABLE_SIZE]; +static uint16_t table[ZLIB_TABLE_SIZE]; static unsigned char codes[288]; int @@ -1795,7 +1910,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, const uint16_t *tlit; const uint16_t *tdist; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; last = val & 1; @@ -1883,7 +1998,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, /* Read a Huffman encoding table. The various magic numbers here are from RFC 1951. */ - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; nlit = (val & 0x1f) + 257; @@ -1908,7 +2023,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, /* There are always at least 4 elements in the table. */ - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; codebits[16] = val & 7; @@ -1928,7 +2043,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, if (nclen == 5) goto codebitsdone; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; codebits[7] = val & 7; @@ -1966,7 +2081,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, if (nclen == 10) goto codebitsdone; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; codebits[11] = val & 7; @@ -2004,7 +2119,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, if (nclen == 15) goto codebitsdone; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; codebits[2] = val & 7; @@ -2043,7 +2158,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, at the end of zdebug_table to hold them. */ plenbase = (((unsigned char *) zdebug_table) - + ZDEBUG_TABLE_CODELEN_OFFSET); + + ZLIB_TABLE_CODELEN_OFFSET); plen = plenbase; plenend = plen + nlit + ndist; while (plen < plenend) @@ -2052,24 +2167,25 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, unsigned int b; uint16_t v; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; t = zdebug_table[val & 0xff]; /* The compression here uses bit lengths up to 7, so a secondary table is never necessary. */ - if (unlikely ((t & (1U << HUFFMAN_SECONDARY_SHIFT)) != 0)) + if (unlikely ((t & (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)) + != 0)) { elf_uncompress_failed (); return 0; } - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; + b = (t >> ZLIB_HUFFMAN_BITS_SHIFT) & ZLIB_HUFFMAN_BITS_MASK; val >>= b + 1; bits -= b + 1; - v = t & HUFFMAN_VALUE_MASK; + v = t & ZLIB_HUFFMAN_VALUE_MASK; if (v < 16) *plen++ = v; else if (v == 16) @@ -2086,7 +2202,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, } /* We used up to 7 bits since the last - elf_zlib_fetch, so we have at least 8 bits + elf_fetch_bits, so we have at least 8 bits available here. */ c = 3 + (val & 0x3); @@ -2121,7 +2237,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, /* Store zero 3 to 10 times. */ /* We used up to 7 bits since the last - elf_zlib_fetch, so we have at least 8 bits + elf_fetch_bits, so we have at least 8 bits available here. */ c = 3 + (val & 0x7); @@ -2167,7 +2283,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, /* Store zero 11 to 138 times. */ /* We used up to 7 bits since the last - elf_zlib_fetch, so we have at least 8 bits + elf_fetch_bits, so we have at least 8 bits available here. */ c = 11 + (val & 0x7f); @@ -2204,10 +2320,11 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, zdebug_table)) return 0; if (!elf_zlib_inflate_table (plen + nlit, ndist, zdebug_table, - zdebug_table + HUFFMAN_TABLE_SIZE)) + (zdebug_table + + ZLIB_HUFFMAN_TABLE_SIZE))) return 0; tlit = zdebug_table; - tdist = zdebug_table + HUFFMAN_TABLE_SIZE; + tdist = zdebug_table + ZLIB_HUFFMAN_TABLE_SIZE; } /* Inflate values until the end of the block. This is the @@ -2220,14 +2337,14 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, uint16_t v; unsigned int lit; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; t = tlit[val & 0xff]; - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; - v = t & HUFFMAN_VALUE_MASK; + b = (t >> ZLIB_HUFFMAN_BITS_SHIFT) & ZLIB_HUFFMAN_BITS_MASK; + v = t & ZLIB_HUFFMAN_VALUE_MASK; - if ((t & (1U << HUFFMAN_SECONDARY_SHIFT)) == 0) + if ((t & (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)) == 0) { lit = v; val >>= b + 1; @@ -2236,8 +2353,8 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, else { t = tlit[v + 0x100 + ((val >> 8) & ((1U << b) - 1))]; - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; - lit = t & HUFFMAN_VALUE_MASK; + b = (t >> ZLIB_HUFFMAN_BITS_SHIFT) & ZLIB_HUFFMAN_BITS_MASK; + lit = t & ZLIB_HUFFMAN_VALUE_MASK; val >>= b + 8; bits -= b + 8; } @@ -2282,7 +2399,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, { unsigned int extra; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; /* This is an expression for the table of length @@ -2297,14 +2414,14 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, bits -= extra; } - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; t = tdist[val & 0xff]; - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; - v = t & HUFFMAN_VALUE_MASK; + b = (t >> ZLIB_HUFFMAN_BITS_SHIFT) & ZLIB_HUFFMAN_BITS_MASK; + v = t & ZLIB_HUFFMAN_VALUE_MASK; - if ((t & (1U << HUFFMAN_SECONDARY_SHIFT)) == 0) + if ((t & (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)) == 0) { dist = v; val >>= b + 1; @@ -2313,8 +2430,9 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, else { t = tdist[v + 0x100 + ((val >> 8) & ((1U << b) - 1))]; - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; - dist = t & HUFFMAN_VALUE_MASK; + b = ((t >> ZLIB_HUFFMAN_BITS_SHIFT) + & ZLIB_HUFFMAN_BITS_MASK); + dist = t & ZLIB_HUFFMAN_VALUE_MASK; val >>= b + 8; bits -= b + 8; } @@ -2354,7 +2472,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, { unsigned int extra; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; /* This is an expression for the table of @@ -2559,6 +2677,2354 @@ elf_zlib_inflate_and_verify (const unsigned char *pin, size_t sin, return 1; } +/* For working memory during zstd compression, we need + - a literal length FSE table: 512 64-bit values == 4096 bytes + - a match length FSE table: 512 64-bit values == 4096 bytes + - a offset FSE table: 256 64-bit values == 2048 bytes + - a Huffman tree: 2048 uint16_t values == 4096 bytes + - scratch space, one of + - to build an FSE table: 512 uint16_t values == 1024 bytes + - to build a Huffman tree: 512 uint16_t + 256 uint32_t == 2048 bytes +*/ + +#define ZSTD_TABLE_SIZE \ + (2 * 512 * sizeof (struct elf_zstd_fse_baseline_entry) \ + + 256 * sizeof (struct elf_zstd_fse_baseline_entry) \ + + 2048 * sizeof (uint16_t) \ + + 512 * sizeof (uint16_t) + 256 * sizeof (uint32_t)) + +#define ZSTD_TABLE_LITERAL_FSE_OFFSET (0) + +#define ZSTD_TABLE_MATCH_FSE_OFFSET \ + (512 * sizeof (struct elf_zstd_fse_baseline_entry)) + +#define ZSTD_TABLE_OFFSET_FSE_OFFSET \ + (ZSTD_TABLE_MATCH_FSE_OFFSET \ + + 512 * sizeof (struct elf_zstd_fse_baseline_entry)) + +#define ZSTD_TABLE_HUFFMAN_OFFSET \ + (ZSTD_TABLE_OFFSET_FSE_OFFSET \ + + 256 * sizeof (struct elf_zstd_fse_baseline_entry)) + +#define ZSTD_TABLE_WORK_OFFSET \ + (ZSTD_TABLE_HUFFMAN_OFFSET + 2048 * sizeof (uint16_t)) + +/* An entry in a zstd FSE table. */ + +struct elf_zstd_fse_entry +{ + /* The value that this FSE entry represents. */ + unsigned char symbol; + /* The number of bits to read to determine the next state. */ + unsigned char bits; + /* Add the bits to this base to get the next state. */ + uint16_t base; +}; + +static int +elf_zstd_build_fse (const int16_t *, int, uint16_t *, int, + struct elf_zstd_fse_entry *); + +/* Read a zstd FSE table and build the decoding table in *TABLE, updating *PPIN + as it reads. ZDEBUG_TABLE is scratch space; it must be enough for 512 + uint16_t values (1024 bytes). MAXIDX is the maximum number of symbols + permitted. *TABLE_BITS is the maximum number of bits for symbols in the + table: the size of *TABLE is at least 1 << *TABLE_BITS. This updates + *TABLE_BITS to the actual number of bits. Returns 1 on success, 0 on + error. */ + +static int +elf_zstd_read_fse (const unsigned char **ppin, const unsigned char *pinend, + uint16_t *zdebug_table, int maxidx, + struct elf_zstd_fse_entry *table, int *table_bits) +{ + const unsigned char *pin; + int16_t *norm; + uint16_t *next; + uint64_t val; + unsigned int bits; + int accuracy_log; + uint32_t remaining; + uint32_t threshold; + int bits_needed; + int idx; + int prev0; + + pin = *ppin; + + norm = (int16_t *) zdebug_table; + next = zdebug_table + 256; + + if (unlikely (pin + 3 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + + /* Align PIN to a 32-bit boundary. */ + + val = 0; + bits = 0; + while ((((uintptr_t) pin) & 3) != 0) + { + val |= (uint64_t)*pin << bits; + bits += 8; + ++pin; + } + + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) + return 0; + + accuracy_log = (val & 0xf) + 5; + if (accuracy_log > *table_bits) + { + elf_uncompress_failed (); + return 0; + } + *table_bits = accuracy_log; + val >>= 4; + bits -= 4; + + /* This code is mostly copied from the reference implementation. */ + + /* The number of remaining probabilities, plus 1. This sets the number of + bits that need to be read for the next value. */ + remaining = (1 << accuracy_log) + 1; + + /* The current difference between small and large values, which depends on + the number of remaining values. Small values use one less bit. */ + threshold = 1 << accuracy_log; + + /* The number of bits used to compute threshold. */ + bits_needed = accuracy_log + 1; + + /* The next character value. */ + idx = 0; + + /* Whether the last count was 0. */ + prev0 = 0; + + while (remaining > 1 && idx <= maxidx) + { + uint32_t max; + int32_t count; + + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) + return 0; + + if (prev0) + { + int zidx; + + /* Previous count was 0, so there is a 2-bit repeat flag. If the + 2-bit flag is 0b11, it adds 3 and then there is another repeat + flag. */ + zidx = idx; + while ((val & 0xfff) == 0xfff) + { + zidx += 3 * 6; + val >>= 12; + bits -= 12; + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) + return 0; + } + while ((val & 3) == 3) + { + zidx += 3; + val >>= 2; + bits -= 2; + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) + return 0; + } + /* We have at least 13 bits here, don't need to fetch. */ + zidx += val & 3; + val >>= 2; + bits -= 2; + + if (unlikely (zidx > maxidx)) + { + elf_uncompress_failed (); + return 0; + } + + for (; idx < zidx; idx++) + norm[idx] = 0; + + prev0 = 0; + continue; + } + + max = (2 * threshold - 1) - remaining; + if ((val & (threshold - 1)) < max) + { + /* A small value. */ + count = (int32_t) ((uint32_t) val & (threshold - 1)); + val >>= bits_needed - 1; + bits -= bits_needed - 1; + } + else + { + /* A large value. */ + count = (int32_t) ((uint32_t) val & (2 * threshold - 1)); + if (count >= (int32_t) threshold) + count -= (int32_t) max; + val >>= bits_needed; + bits -= bits_needed; + } + + count--; + if (count >= 0) + remaining -= count; + else + remaining--; + if (unlikely (idx >= 256)) + { + elf_uncompress_failed (); + return 0; + } + norm[idx] = (int16_t) count; + ++idx; + + prev0 = count == 0; + + while (remaining < threshold) + { + bits_needed--; + threshold >>= 1; + } + } + + if (unlikely (remaining != 1)) + { + elf_uncompress_failed (); + return 0; + } + + /* If we've read ahead more than a byte, back up. */ + while (bits >= 8) + { + --pin; + bits -= 8; + } + + *ppin = pin; + + for (; idx <= maxidx; idx++) + norm[idx] = 0; + + return elf_zstd_build_fse (norm, idx, next, *table_bits, table); +} + +/* Build the FSE decoding table from a list of probabilities. This reads from + NORM of length IDX, uses NEXT as scratch space, and writes to *TABLE, whose + size is TABLE_BITS. */ + +static int +elf_zstd_build_fse (const int16_t *norm, int idx, uint16_t *next, + int table_bits, struct elf_zstd_fse_entry *table) +{ + int table_size; + int high_threshold; + int i; + int pos; + int step; + int mask; + + table_size = 1 << table_bits; + high_threshold = table_size - 1; + for (i = 0; i < idx; i++) + { + int16_t n; + + n = norm[i]; + if (n >= 0) + next[i] = (uint16_t) n; + else + { + table[high_threshold].symbol = (unsigned char) i; + high_threshold--; + next[i] = 1; + } + } + + pos = 0; + step = (table_size >> 1) + (table_size >> 3) + 3; + mask = table_size - 1; + for (i = 0; i < idx; i++) + { + int n; + int j; + + n = (int) norm[i]; + for (j = 0; j < n; j++) + { + table[pos].symbol = (unsigned char) i; + pos = (pos + step) & mask; + while (unlikely (pos > high_threshold)) + pos = (pos + step) & mask; + } + } + if (unlikely (pos != 0)) + { + elf_uncompress_failed (); + return 0; + } + + for (i = 0; i < table_size; i++) + { + unsigned char sym; + uint16_t next_state; + int high_bit; + int bits; + + sym = table[i].symbol; + next_state = next[sym]; + ++next[sym]; + + if (next_state == 0) + { + elf_uncompress_failed (); + return 0; + } + high_bit = 31 - __builtin_clz (next_state); + + bits = table_bits - high_bit; + table[i].bits = (unsigned char) bits; + table[i].base = (uint16_t) ((next_state << bits) - table_size); + } + + return 1; +} + +/* Encode the baseline and bits into a single 32-bit value. */ + +#define ZSTD_ENCODE_BASELINE_BITS(baseline, basebits) \ + ((uint32_t)(baseline) | ((uint32_t)(basebits) << 24)) + +#define ZSTD_DECODE_BASELINE(baseline_basebits) \ + ((uint32_t)(baseline_basebits) & 0xffffff) + +#define ZSTD_DECODE_BASEBITS(baseline_basebits) \ + ((uint32_t)(baseline_basebits) >> 24) + +/* Given a literal length code, we need to read a number of bits and add that + to a baseline. For states 0 to 15 the baseline is the state and the number + of bits is zero. */ + +#define ZSTD_LITERAL_LENGTH_BASELINE_OFFSET (16) + +static const uint32_t elf_zstd_literal_length_base[] = +{ + ZSTD_ENCODE_BASELINE_BITS(16, 1), + ZSTD_ENCODE_BASELINE_BITS(18, 1), + ZSTD_ENCODE_BASELINE_BITS(20, 1), + ZSTD_ENCODE_BASELINE_BITS(22, 1), + ZSTD_ENCODE_BASELINE_BITS(24, 2), + ZSTD_ENCODE_BASELINE_BITS(28, 2), + ZSTD_ENCODE_BASELINE_BITS(32, 3), + ZSTD_ENCODE_BASELINE_BITS(40, 3), + ZSTD_ENCODE_BASELINE_BITS(48, 4), + ZSTD_ENCODE_BASELINE_BITS(64, 6), + ZSTD_ENCODE_BASELINE_BITS(128, 7), + ZSTD_ENCODE_BASELINE_BITS(256, 8), + ZSTD_ENCODE_BASELINE_BITS(512, 9), + ZSTD_ENCODE_BASELINE_BITS(1024, 10), + ZSTD_ENCODE_BASELINE_BITS(2048, 11), + ZSTD_ENCODE_BASELINE_BITS(4096, 12), + ZSTD_ENCODE_BASELINE_BITS(8192, 13), + ZSTD_ENCODE_BASELINE_BITS(16384, 14), + ZSTD_ENCODE_BASELINE_BITS(32768, 15), + ZSTD_ENCODE_BASELINE_BITS(65536, 16) +}; + +/* The same applies to match length codes. For states 0 to 31 the baseline is + the state + 3 and the number of bits is zero. */ + +#define ZSTD_MATCH_LENGTH_BASELINE_OFFSET (32) + +static const uint32_t elf_zstd_match_length_base[] = +{ + ZSTD_ENCODE_BASELINE_BITS(35, 1), + ZSTD_ENCODE_BASELINE_BITS(37, 1), + ZSTD_ENCODE_BASELINE_BITS(39, 1), + ZSTD_ENCODE_BASELINE_BITS(41, 1), + ZSTD_ENCODE_BASELINE_BITS(43, 2), + ZSTD_ENCODE_BASELINE_BITS(47, 2), + ZSTD_ENCODE_BASELINE_BITS(51, 3), + ZSTD_ENCODE_BASELINE_BITS(59, 3), + ZSTD_ENCODE_BASELINE_BITS(67, 4), + ZSTD_ENCODE_BASELINE_BITS(83, 4), + ZSTD_ENCODE_BASELINE_BITS(99, 5), + ZSTD_ENCODE_BASELINE_BITS(131, 7), + ZSTD_ENCODE_BASELINE_BITS(259, 8), + ZSTD_ENCODE_BASELINE_BITS(515, 9), + ZSTD_ENCODE_BASELINE_BITS(1027, 10), + ZSTD_ENCODE_BASELINE_BITS(2051, 11), + ZSTD_ENCODE_BASELINE_BITS(4099, 12), + ZSTD_ENCODE_BASELINE_BITS(8195, 13), + ZSTD_ENCODE_BASELINE_BITS(16387, 14), + ZSTD_ENCODE_BASELINE_BITS(32771, 15), + ZSTD_ENCODE_BASELINE_BITS(65539, 16) +}; + +/* An entry in an FSE table used for literal/match/length values. For these we + have to map the symbol to a baseline value, and we have to read zero or more + bits and add that value to the baseline value. Rather than look the values + up in a separate table, we grow the FSE table so that we get better memory + caching. */ + +struct elf_zstd_fse_baseline_entry +{ + /* The baseline for the value that this FSE entry represents.. */ + uint32_t baseline; + /* The number of bits to read to add to the baseline. */ + unsigned char basebits; + /* The number of bits to read to determine the next state. */ + unsigned char bits; + /* Add the bits to this base to get the next state. */ + uint16_t base; +}; + +/* Convert the literal length FSE table FSE_TABLE to an FSE baseline table at + BASELINE_TABLE. Note that FSE_TABLE and BASELINE_TABLE will overlap. */ + +static int +elf_zstd_make_literal_baseline_fse ( + const struct elf_zstd_fse_entry *fse_table, + int table_bits, + struct elf_zstd_fse_baseline_entry *baseline_table) +{ + size_t count; + const struct elf_zstd_fse_entry *pfse; + struct elf_zstd_fse_baseline_entry *pbaseline; + + /* Convert backward to avoid overlap. */ + + count = 1U << table_bits; + pfse = fse_table + count; + pbaseline = baseline_table + count; + while (pfse > fse_table) + { + unsigned char symbol; + unsigned char bits; + uint16_t base; + + --pfse; + --pbaseline; + symbol = pfse->symbol; + bits = pfse->bits; + base = pfse->base; + if (symbol < ZSTD_LITERAL_LENGTH_BASELINE_OFFSET) + { + pbaseline->baseline = (uint32_t)symbol; + pbaseline->basebits = 0; + } + else + { + unsigned int idx; + uint32_t basebits; + + if (unlikely (symbol > 35)) + { + elf_uncompress_failed (); + return 0; + } + idx = symbol - ZSTD_LITERAL_LENGTH_BASELINE_OFFSET; + basebits = elf_zstd_literal_length_base[idx]; + pbaseline->baseline = ZSTD_DECODE_BASELINE(basebits); + pbaseline->basebits = ZSTD_DECODE_BASEBITS(basebits); + } + pbaseline->bits = bits; + pbaseline->base = base; + } + + return 1; +} + +/* Convert the offset length FSE table FSE_TABLE to an FSE baseline table at + BASELINE_TABLE. Note that FSE_TABLE and BASELINE_TABLE will overlap. */ + +static int +elf_zstd_make_offset_baseline_fse ( + const struct elf_zstd_fse_entry *fse_table, + int table_bits, + struct elf_zstd_fse_baseline_entry *baseline_table) +{ + size_t count; + const struct elf_zstd_fse_entry *pfse; + struct elf_zstd_fse_baseline_entry *pbaseline; + + /* Convert backward to avoid overlap. */ + + count = 1U << table_bits; + pfse = fse_table + count; + pbaseline = baseline_table + count; + while (pfse > fse_table) + { + unsigned char symbol; + unsigned char bits; + uint16_t base; + + --pfse; + --pbaseline; + symbol = pfse->symbol; + bits = pfse->bits; + base = pfse->base; + if (unlikely (symbol > 31)) + { + elf_uncompress_failed (); + return 0; + } + + /* The simple way to write this is + + pbaseline->baseline = (uint32_t)1 << symbol; + pbaseline->basebits = symbol; + + That will give us an offset value that corresponds to the one + described in the RFC. However, for offset values > 3, we have to + subtract 3. And for offset values 1, 2, 3 we use a repeated offset. + The baseline is always a power of 2, and is never 0, so for these low + values we will see one entry that is baseline 1, basebits 0, and one + entry that is baseline 2, basebits 1. All other entries will have + baseline >= 4 and basebits >= 2. + + So we can check for RFC offset <= 3 by checking for basebits <= 1. + And that means that we can subtract 3 here and not worry about doing + it in the hot loop. */ + + pbaseline->baseline = (uint32_t)1 << symbol; + if (symbol >= 2) + pbaseline->baseline -= 3; + pbaseline->basebits = symbol; + pbaseline->bits = bits; + pbaseline->base = base; + } + + return 1; +} + +/* Convert the match length FSE table FSE_TABLE to an FSE baseline table at + BASELINE_TABLE. Note that FSE_TABLE and BASELINE_TABLE will overlap. */ + +static int +elf_zstd_make_match_baseline_fse ( + const struct elf_zstd_fse_entry *fse_table, + int table_bits, + struct elf_zstd_fse_baseline_entry *baseline_table) +{ + size_t count; + const struct elf_zstd_fse_entry *pfse; + struct elf_zstd_fse_baseline_entry *pbaseline; + + /* Convert backward to avoid overlap. */ + + count = 1U << table_bits; + pfse = fse_table + count; + pbaseline = baseline_table + count; + while (pfse > fse_table) + { + unsigned char symbol; + unsigned char bits; + uint16_t base; + + --pfse; + --pbaseline; + symbol = pfse->symbol; + bits = pfse->bits; + base = pfse->base; + if (symbol < ZSTD_MATCH_LENGTH_BASELINE_OFFSET) + { + pbaseline->baseline = (uint32_t)symbol + 3; + pbaseline->basebits = 0; + } + else + { + unsigned int idx; + uint32_t basebits; + + if (unlikely (symbol > 52)) + { + elf_uncompress_failed (); + return 0; + } + idx = symbol - ZSTD_MATCH_LENGTH_BASELINE_OFFSET; + basebits = elf_zstd_match_length_base[idx]; + pbaseline->baseline = ZSTD_DECODE_BASELINE(basebits); + pbaseline->basebits = ZSTD_DECODE_BASEBITS(basebits); + } + pbaseline->bits = bits; + pbaseline->base = base; + } + + return 1; +} + +#ifdef BACKTRACE_GENERATE_ZSTD_FSE_TABLES + +/* Used to generate the predefined FSE decoding tables for zstd. */ + +#include + +/* These values are straight from RFC 8878. */ + +static int16_t lit[36] = +{ + 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 1, 1, 1, 1, 1, + -1,-1,-1,-1 +}; + +static int16_t match[53] = +{ + 1, 4, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,-1,-1, + -1,-1,-1,-1,-1 +}; + +static int16_t offset[29] = +{ + 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1,-1,-1,-1,-1,-1 +}; + +static uint16_t next[256]; + +static void +print_table (const struct elf_zstd_fse_baseline_entry *table, size_t size) +{ + size_t i; + + printf ("{\n"); + for (i = 0; i < size; i += 3) + { + int j; + + printf (" "); + for (j = 0; j < 3 && i + j < size; ++j) + printf (" { %u, %d, %d, %d },", table[i + j].baseline, + table[i + j].basebits, table[i + j].bits, + table[i + j].base); + printf ("\n"); + } + printf ("};\n"); +} + +int +main () +{ + struct elf_zstd_fse_entry lit_table[64]; + struct elf_zstd_fse_baseline_entry lit_baseline[64]; + struct elf_zstd_fse_entry match_table[64]; + struct elf_zstd_fse_baseline_entry match_baseline[64]; + struct elf_zstd_fse_entry offset_table[32]; + struct elf_zstd_fse_baseline_entry offset_baseline[32]; + + if (!elf_zstd_build_fse (lit, sizeof lit / sizeof lit[0], next, + 6, lit_table)) + { + fprintf (stderr, "elf_zstd_build_fse failed\n"); + exit (EXIT_FAILURE); + } + + if (!elf_zstd_make_literal_baseline_fse (lit_table, 6, lit_baseline)) + { + fprintf (stderr, "elf_zstd_make_literal_baseline_fse failed\n"); + exit (EXIT_FAILURE); + } + + printf ("static const struct elf_zstd_fse_baseline_entry " + "elf_zstd_lit_table[64] =\n"); + print_table (lit_baseline, + sizeof lit_baseline / sizeof lit_baseline[0]); + printf ("\n"); + + if (!elf_zstd_build_fse (match, sizeof match / sizeof match[0], next, + 6, match_table)) + { + fprintf (stderr, "elf_zstd_build_fse failed\n"); + exit (EXIT_FAILURE); + } + + if (!elf_zstd_make_match_baseline_fse (match_table, 6, match_baseline)) + { + fprintf (stderr, "elf_zstd_make_match_baseline_fse failed\n"); + exit (EXIT_FAILURE); + } + + printf ("static const struct elf_zstd_fse_baseline_entry " + "elf_zstd_match_table[64] =\n"); + print_table (match_baseline, + sizeof match_baseline / sizeof match_baseline[0]); + printf ("\n"); + + if (!elf_zstd_build_fse (offset, sizeof offset / sizeof offset[0], next, + 5, offset_table)) + { + fprintf (stderr, "elf_zstd_build_fse failed\n"); + exit (EXIT_FAILURE); + } + + if (!elf_zstd_make_offset_baseline_fse (offset_table, 5, offset_baseline)) + { + fprintf (stderr, "elf_zstd_make_offset_baseline_fse failed\n"); + exit (EXIT_FAILURE); + } + + printf ("static const struct elf_zstd_fse_baseline_entry " + "elf_zstd_offset_table[32] =\n"); + print_table (offset_baseline, + sizeof offset_baseline / sizeof offset_baseline[0]); + printf ("\n"); + + return 0; +} + +#endif + +/* The fixed tables generated by the #ifdef'ed out main function + above. */ + +static const struct elf_zstd_fse_baseline_entry elf_zstd_lit_table[64] = +{ + { 0, 0, 4, 0 }, { 0, 0, 4, 16 }, { 1, 0, 5, 32 }, + { 3, 0, 5, 0 }, { 4, 0, 5, 0 }, { 6, 0, 5, 0 }, + { 7, 0, 5, 0 }, { 9, 0, 5, 0 }, { 10, 0, 5, 0 }, + { 12, 0, 5, 0 }, { 14, 0, 6, 0 }, { 16, 1, 5, 0 }, + { 20, 1, 5, 0 }, { 22, 1, 5, 0 }, { 28, 2, 5, 0 }, + { 32, 3, 5, 0 }, { 48, 4, 5, 0 }, { 64, 6, 5, 32 }, + { 128, 7, 5, 0 }, { 256, 8, 6, 0 }, { 1024, 10, 6, 0 }, + { 4096, 12, 6, 0 }, { 0, 0, 4, 32 }, { 1, 0, 4, 0 }, + { 2, 0, 5, 0 }, { 4, 0, 5, 32 }, { 5, 0, 5, 0 }, + { 7, 0, 5, 32 }, { 8, 0, 5, 0 }, { 10, 0, 5, 32 }, + { 11, 0, 5, 0 }, { 13, 0, 6, 0 }, { 16, 1, 5, 32 }, + { 18, 1, 5, 0 }, { 22, 1, 5, 32 }, { 24, 2, 5, 0 }, + { 32, 3, 5, 32 }, { 40, 3, 5, 0 }, { 64, 6, 4, 0 }, + { 64, 6, 4, 16 }, { 128, 7, 5, 32 }, { 512, 9, 6, 0 }, + { 2048, 11, 6, 0 }, { 0, 0, 4, 48 }, { 1, 0, 4, 16 }, + { 2, 0, 5, 32 }, { 3, 0, 5, 32 }, { 5, 0, 5, 32 }, + { 6, 0, 5, 32 }, { 8, 0, 5, 32 }, { 9, 0, 5, 32 }, + { 11, 0, 5, 32 }, { 12, 0, 5, 32 }, { 15, 0, 6, 0 }, + { 18, 1, 5, 32 }, { 20, 1, 5, 32 }, { 24, 2, 5, 32 }, + { 28, 2, 5, 32 }, { 40, 3, 5, 32 }, { 48, 4, 5, 32 }, + { 65536, 16, 6, 0 }, { 32768, 15, 6, 0 }, { 16384, 14, 6, 0 }, + { 8192, 13, 6, 0 }, +}; + +static const struct elf_zstd_fse_baseline_entry elf_zstd_match_table[64] = +{ + { 3, 0, 6, 0 }, { 4, 0, 4, 0 }, { 5, 0, 5, 32 }, + { 6, 0, 5, 0 }, { 8, 0, 5, 0 }, { 9, 0, 5, 0 }, + { 11, 0, 5, 0 }, { 13, 0, 6, 0 }, { 16, 0, 6, 0 }, + { 19, 0, 6, 0 }, { 22, 0, 6, 0 }, { 25, 0, 6, 0 }, + { 28, 0, 6, 0 }, { 31, 0, 6, 0 }, { 34, 0, 6, 0 }, + { 37, 1, 6, 0 }, { 41, 1, 6, 0 }, { 47, 2, 6, 0 }, + { 59, 3, 6, 0 }, { 83, 4, 6, 0 }, { 131, 7, 6, 0 }, + { 515, 9, 6, 0 }, { 4, 0, 4, 16 }, { 5, 0, 4, 0 }, + { 6, 0, 5, 32 }, { 7, 0, 5, 0 }, { 9, 0, 5, 32 }, + { 10, 0, 5, 0 }, { 12, 0, 6, 0 }, { 15, 0, 6, 0 }, + { 18, 0, 6, 0 }, { 21, 0, 6, 0 }, { 24, 0, 6, 0 }, + { 27, 0, 6, 0 }, { 30, 0, 6, 0 }, { 33, 0, 6, 0 }, + { 35, 1, 6, 0 }, { 39, 1, 6, 0 }, { 43, 2, 6, 0 }, + { 51, 3, 6, 0 }, { 67, 4, 6, 0 }, { 99, 5, 6, 0 }, + { 259, 8, 6, 0 }, { 4, 0, 4, 32 }, { 4, 0, 4, 48 }, + { 5, 0, 4, 16 }, { 7, 0, 5, 32 }, { 8, 0, 5, 32 }, + { 10, 0, 5, 32 }, { 11, 0, 5, 32 }, { 14, 0, 6, 0 }, + { 17, 0, 6, 0 }, { 20, 0, 6, 0 }, { 23, 0, 6, 0 }, + { 26, 0, 6, 0 }, { 29, 0, 6, 0 }, { 32, 0, 6, 0 }, + { 65539, 16, 6, 0 }, { 32771, 15, 6, 0 }, { 16387, 14, 6, 0 }, + { 8195, 13, 6, 0 }, { 4099, 12, 6, 0 }, { 2051, 11, 6, 0 }, + { 1027, 10, 6, 0 }, +}; + +static const struct elf_zstd_fse_baseline_entry elf_zstd_offset_table[32] = +{ + { 1, 0, 5, 0 }, { 61, 6, 4, 0 }, { 509, 9, 5, 0 }, + { 32765, 15, 5, 0 }, { 2097149, 21, 5, 0 }, { 5, 3, 5, 0 }, + { 125, 7, 4, 0 }, { 4093, 12, 5, 0 }, { 262141, 18, 5, 0 }, + { 8388605, 23, 5, 0 }, { 29, 5, 5, 0 }, { 253, 8, 4, 0 }, + { 16381, 14, 5, 0 }, { 1048573, 20, 5, 0 }, { 1, 2, 5, 0 }, + { 125, 7, 4, 16 }, { 2045, 11, 5, 0 }, { 131069, 17, 5, 0 }, + { 4194301, 22, 5, 0 }, { 13, 4, 5, 0 }, { 253, 8, 4, 16 }, + { 8189, 13, 5, 0 }, { 524285, 19, 5, 0 }, { 2, 1, 5, 0 }, + { 61, 6, 4, 16 }, { 1021, 10, 5, 0 }, { 65533, 16, 5, 0 }, + { 268435453, 28, 5, 0 }, { 134217725, 27, 5, 0 }, { 67108861, 26, 5, 0 }, + { 33554429, 25, 5, 0 }, { 16777213, 24, 5, 0 }, +}; + +/* Read a zstd Huffman table and build the decoding table in *TABLE, reading + and updating *PPIN. This sets *PTABLE_BITS to the number of bits of the + table, such that the table length is 1 << *TABLE_BITS. ZDEBUG_TABLE is + scratch space; it must be enough for 512 uint16_t values + 256 32-bit values + (2048 bytes). Returns 1 on success, 0 on error. */ + +static int +elf_zstd_read_huff (const unsigned char **ppin, const unsigned char *pinend, + uint16_t *zdebug_table, uint16_t *table, int *ptable_bits) +{ + const unsigned char *pin; + unsigned char hdr; + unsigned char *weights; + size_t count; + uint32_t *weight_mark; + size_t i; + uint32_t weight_mask; + size_t table_bits; + + pin = *ppin; + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + hdr = *pin; + ++pin; + + weights = (unsigned char *) zdebug_table; + + if (hdr < 128) + { + /* Table is compressed using FSE. */ + + struct elf_zstd_fse_entry *fse_table; + int fse_table_bits; + uint16_t *scratch; + const unsigned char *pfse; + const unsigned char *pback; + uint64_t val; + unsigned int bits; + unsigned int state1, state2; + + /* SCRATCH is used temporarily by elf_zstd_read_fse. It overlaps + WEIGHTS. */ + scratch = zdebug_table; + fse_table = (struct elf_zstd_fse_entry *) (scratch + 512); + fse_table_bits = 6; + + pfse = pin; + if (!elf_zstd_read_fse (&pfse, pinend, scratch, 255, fse_table, + &fse_table_bits)) + return 0; + + if (unlikely (pin + hdr > pinend)) + { + elf_uncompress_failed (); + return 0; + } + + /* We no longer need SCRATCH. Start recording weights. We need up to + 256 bytes of weights and 64 bytes of rank counts, so it won't overlap + FSE_TABLE. */ + + pback = pin + hdr - 1; + + if (!elf_fetch_backward_init (&pback, pfse, &val, &bits)) + return 0; + + bits -= fse_table_bits; + state1 = (val >> bits) & ((1U << fse_table_bits) - 1); + bits -= fse_table_bits; + state2 = (val >> bits) & ((1U << fse_table_bits) - 1); + + /* There are two independent FSE streams, tracked by STATE1 and STATE2. + We decode them alternately. */ + + count = 0; + while (1) + { + struct elf_zstd_fse_entry *pt; + uint64_t v; + + pt = &fse_table[state1]; + + if (unlikely (pin < pinend) && bits < pt->bits) + { + if (unlikely (count >= 254)) + { + elf_uncompress_failed (); + return 0; + } + weights[count] = (unsigned char) pt->symbol; + weights[count + 1] = (unsigned char) fse_table[state2].symbol; + count += 2; + break; + } + + if (unlikely (pt->bits == 0)) + v = 0; + else + { + if (!elf_fetch_bits_backward (&pback, pfse, &val, &bits)) + return 0; + + bits -= pt->bits; + v = (val >> bits) & (((uint64_t)1 << pt->bits) - 1); + } + + state1 = pt->base + v; + + if (unlikely (count >= 255)) + { + elf_uncompress_failed (); + return 0; + } + + weights[count] = pt->symbol; + ++count; + + pt = &fse_table[state2]; + + if (unlikely (pin < pinend && bits < pt->bits)) + { + if (unlikely (count >= 254)) + { + elf_uncompress_failed (); + return 0; + } + weights[count] = (unsigned char) pt->symbol; + weights[count + 1] = (unsigned char) fse_table[state1].symbol; + count += 2; + break; + } + + if (unlikely (pt->bits == 0)) + v = 0; + else + { + if (!elf_fetch_bits_backward (&pback, pfse, &val, &bits)) + return 0; + + bits -= pt->bits; + v = (val >> bits) & (((uint64_t)1 << pt->bits) - 1); + } + + state2 = pt->base + v; + + if (unlikely (count >= 255)) + { + elf_uncompress_failed (); + return 0; + } + + weights[count] = pt->symbol; + ++count; + } + + pin += hdr; + } + else + { + /* Table is not compressed. Each weight is 4 bits. */ + + count = hdr - 127; + if (unlikely (pin + ((count + 1) / 2) >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + for (i = 0; i < count; i += 2) + { + unsigned char b; + + b = *pin; + ++pin; + weights[i] = b >> 4; + weights[i + 1] = b & 0xf; + } + } + + weight_mark = (uint32_t *) (weights + 256); + memset (weight_mark, 0, 13 * sizeof (uint32_t)); + weight_mask = 0; + for (i = 0; i < count; ++i) + { + unsigned char w; + + w = weights[i]; + if (unlikely (w > 12)) + { + elf_uncompress_failed (); + return 0; + } + ++weight_mark[w]; + if (w > 0) + weight_mask += 1U << (w - 1); + } + if (unlikely (weight_mask == 0)) + { + elf_uncompress_failed (); + return 0; + } + + table_bits = 32 - __builtin_clz (weight_mask); + if (unlikely (table_bits > 11)) + { + elf_uncompress_failed (); + return 0; + } + + /* Work out the last weight value, which is omitted because the weights must + sum to a power of two. */ + { + uint32_t left; + uint32_t high_bit; + + left = ((uint32_t)1 << table_bits) - weight_mask; + if (left == 0) + { + elf_uncompress_failed (); + return 0; + } + high_bit = 31 - __builtin_clz (left); + if (((uint32_t)1 << high_bit) != left) + { + elf_uncompress_failed (); + return 0; + } + + if (unlikely (count >= 256)) + { + elf_uncompress_failed (); + return 0; + } + + weights[count] = high_bit + 1; + ++count; + ++weight_mark[high_bit + 1]; + } + + if (weight_mark[1] < 2 || (weight_mark[1] & 1) != 0) + { + elf_uncompress_failed (); + return 0; + } + + /* Change WEIGHT_MARK from a count of weights to the index of the first + symbol for that weight. We shift the indexes to also store how many we + have seen so far, below. */ + { + uint32_t next; + + next = 0; + for (i = 0; i < table_bits; ++i) + { + uint32_t cur; + + cur = next; + next += weight_mark[i + 1] << i; + weight_mark[i + 1] = cur; + } + } + + for (i = 0; i < count; ++i) + { + unsigned char weight; + uint32_t length; + uint16_t tval; + size_t start; + uint32_t j; + + weight = weights[i]; + if (weight == 0) + continue; + + length = 1U << (weight - 1); + tval = (i << 8) | (table_bits + 1 - weight); + start = weight_mark[weight]; + for (j = 0; j < length; ++j) + table[start + j] = tval; + weight_mark[weight] += length; + } + + *ppin = pin; + *ptable_bits = (int)table_bits; + + return 1; +} + +/* Read and decompress the literals and store them ending at POUTEND. This + works because we are going to use all the literals in the output, so they + must fit into the output buffer. HUFFMAN_TABLE, and PHUFFMAN_TABLE_BITS + store the Huffman table across calls. SCRATCH is used to read a Huffman + table. Store the start of the decompressed literals in *PPLIT. Update + *PPIN. Return 1 on success, 0 on error. */ + +static int +elf_zstd_read_literals (const unsigned char **ppin, + const unsigned char *pinend, + unsigned char *pout, + unsigned char *poutend, + uint16_t *scratch, + uint16_t *huffman_table, + int *phuffman_table_bits, + unsigned char **pplit) +{ + const unsigned char *pin; + unsigned char *plit; + unsigned char hdr; + uint32_t regenerated_size; + uint32_t compressed_size; + int streams; + uint32_t total_streams_size; + unsigned int huffman_table_bits; + uint64_t huffman_mask; + + pin = *ppin; + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + hdr = *pin; + ++pin; + + if ((hdr & 3) == 0 || (hdr & 3) == 1) + { + int raw; + + /* Raw_Literals_Block or RLE_Literals_Block */ + + raw = (hdr & 3) == 0; + + switch ((hdr >> 2) & 3) + { + case 0: case 2: + regenerated_size = hdr >> 3; + break; + case 1: + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = (hdr >> 4) + ((uint32_t)(*pin) << 4); + ++pin; + break; + case 3: + if (unlikely (pin + 1 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = ((hdr >> 4) + + ((uint32_t)*pin << 4) + + ((uint32_t)pin[1] << 12)); + pin += 2; + break; + default: + elf_uncompress_failed (); + return 0; + } + + if (unlikely ((size_t)(poutend - pout) < regenerated_size)) + { + elf_uncompress_failed (); + return 0; + } + + plit = poutend - regenerated_size; + + if (raw) + { + if (unlikely (pin + regenerated_size >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + memcpy (plit, pin, regenerated_size); + pin += regenerated_size; + } + else + { + if (pin >= pinend) + { + elf_uncompress_failed (); + return 0; + } + memset (plit, *pin, regenerated_size); + ++pin; + } + + *ppin = pin; + *pplit = plit; + + return 1; + } + + /* Compressed_Literals_Block or Treeless_Literals_Block */ + + switch ((hdr >> 2) & 3) + { + case 0: case 1: + if (unlikely (pin + 1 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = (hdr >> 4) | ((uint32_t)(*pin & 0x3f) << 4); + compressed_size = (uint32_t)*pin >> 6 | ((uint32_t)pin[1] << 2); + pin += 2; + streams = ((hdr >> 2) & 3) == 0 ? 1 : 4; + break; + case 2: + if (unlikely (pin + 2 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = (((uint32_t)hdr >> 4) + | ((uint32_t)*pin << 4) + | (((uint32_t)pin[1] & 3) << 12)); + compressed_size = (((uint32_t)pin[1] >> 2) + | ((uint32_t)pin[2] << 6)); + pin += 3; + streams = 4; + break; + case 3: + if (unlikely (pin + 3 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = (((uint32_t)hdr >> 4) + | ((uint32_t)*pin << 4) + | (((uint32_t)pin[1] & 0x3f) << 12)); + compressed_size = (((uint32_t)pin[1] >> 6) + | ((uint32_t)pin[2] << 2) + | ((uint32_t)pin[3] << 10)); + pin += 4; + streams = 4; + break; + default: + elf_uncompress_failed (); + return 0; + } + + if (unlikely (pin + compressed_size > pinend)) + { + elf_uncompress_failed (); + return 0; + } + + pinend = pin + compressed_size; + *ppin = pinend; + + if (unlikely ((size_t)(poutend - pout) < regenerated_size)) + { + elf_uncompress_failed (); + return 0; + } + + plit = poutend - regenerated_size; + + *pplit = plit; + + total_streams_size = compressed_size; + if ((hdr & 3) == 2) + { + const unsigned char *ptable; + + /* Compressed_Literals_Block. Read Huffman tree. */ + + ptable = pin; + if (!elf_zstd_read_huff (&ptable, pinend, scratch, huffman_table, + phuffman_table_bits)) + return 0; + + if (unlikely (total_streams_size < (size_t)(ptable - pin))) + { + elf_uncompress_failed (); + return 0; + } + + total_streams_size -= ptable - pin; + pin = ptable; + } + else + { + /* Treeless_Literals_Block. Reuse previous Huffman tree. */ + if (unlikely (*phuffman_table_bits == 0)) + { + elf_uncompress_failed (); + return 0; + } + } + + /* Decompress COMPRESSED_SIZE bytes of data at PIN using the huffman table, + storing REGENERATED_SIZE bytes of decompressed data at PLIT. */ + + huffman_table_bits = (unsigned int)*phuffman_table_bits; + huffman_mask = ((uint64_t)1 << huffman_table_bits) - 1; + + if (streams == 1) + { + const unsigned char *pback; + const unsigned char *pbackend; + uint64_t val; + unsigned int bits; + uint32_t i; + + pback = pin + total_streams_size - 1; + pbackend = pin; + if (!elf_fetch_backward_init (&pback, pbackend, &val, &bits)) + return 0; + + /* This is one of the inner loops of the decompression algorithm, so we + put some effort into optimization. We can't get more than 64 bytes + from a single call to elf_fetch_bits_backward, and we can't subtract + more than 11 bits at a time. */ + + if (regenerated_size >= 64) + { + unsigned char *plitstart; + unsigned char *plitstop; + + plitstart = plit; + plitstop = plit + regenerated_size - 64; + while (plit < plitstop) + { + uint16_t t; + + if (!elf_fetch_bits_backward (&pback, pbackend, &val, &bits)) + return 0; + + if (bits < 16) + break; + + while (bits >= 33) + { + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + } + + while (bits > 11) + { + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + } + } + + regenerated_size -= plit - plitstart; + } + + for (i = 0; i < regenerated_size; ++i) + { + uint16_t t; + + if (!elf_fetch_bits_backward (&pback, pbackend, &val, &bits)) + return 0; + + if (unlikely (bits < huffman_table_bits)) + { + t = huffman_table[(val << (huffman_table_bits - bits)) + & huffman_mask]; + if (unlikely (bits < (t & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + } + + return 1; + } + + { + uint32_t stream_size1, stream_size2, stream_size3, stream_size4; + uint32_t tot; + const unsigned char *pback1, *pback2, *pback3, *pback4; + const unsigned char *pbackend1, *pbackend2, *pbackend3, *pbackend4; + uint64_t val1, val2, val3, val4; + unsigned int bits1, bits2, bits3, bits4; + unsigned char *plit1, *plit2, *plit3, *plit4; + uint32_t regenerated_stream_size; + uint32_t regenerated_stream_size4; + uint16_t t1, t2, t3, t4; + uint32_t i; + uint32_t limit; + + /* Read jump table. */ + if (unlikely (pin + 5 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + stream_size1 = (uint32_t)*pin | ((uint32_t)pin[1] << 8); + pin += 2; + stream_size2 = (uint32_t)*pin | ((uint32_t)pin[1] << 8); + pin += 2; + stream_size3 = (uint32_t)*pin | ((uint32_t)pin[1] << 8); + pin += 2; + tot = stream_size1 + stream_size2 + stream_size3; + if (unlikely (tot > total_streams_size - 6)) + { + elf_uncompress_failed (); + return 0; + } + stream_size4 = total_streams_size - 6 - tot; + + pback1 = pin + stream_size1 - 1; + pbackend1 = pin; + + pback2 = pback1 + stream_size2; + pbackend2 = pback1 + 1; + + pback3 = pback2 + stream_size3; + pbackend3 = pback2 + 1; + + pback4 = pback3 + stream_size4; + pbackend4 = pback3 + 1; + + if (!elf_fetch_backward_init (&pback1, pbackend1, &val1, &bits1)) + return 0; + if (!elf_fetch_backward_init (&pback2, pbackend2, &val2, &bits2)) + return 0; + if (!elf_fetch_backward_init (&pback3, pbackend3, &val3, &bits3)) + return 0; + if (!elf_fetch_backward_init (&pback4, pbackend4, &val4, &bits4)) + return 0; + + regenerated_stream_size = (regenerated_size + 3) / 4; + + plit1 = plit; + plit2 = plit1 + regenerated_stream_size; + plit3 = plit2 + regenerated_stream_size; + plit4 = plit3 + regenerated_stream_size; + + regenerated_stream_size4 = regenerated_size - regenerated_stream_size * 3; + + /* We can't get more than 64 literal bytes from a single call to + elf_fetch_bits_backward. The fourth stream can be up to 3 bytes less, + so use as the limit. */ + + limit = regenerated_stream_size4 <= 64 ? 0 : regenerated_stream_size4 - 64; + i = 0; + while (i < limit) + { + if (!elf_fetch_bits_backward (&pback1, pbackend1, &val1, &bits1)) + return 0; + if (!elf_fetch_bits_backward (&pback2, pbackend2, &val2, &bits2)) + return 0; + if (!elf_fetch_bits_backward (&pback3, pbackend3, &val3, &bits3)) + return 0; + if (!elf_fetch_bits_backward (&pback4, pbackend4, &val4, &bits4)) + return 0; + + /* We can't subtract more than 11 bits at a time. */ + + do + { + t1 = huffman_table[(val1 >> (bits1 - huffman_table_bits)) + & huffman_mask]; + t2 = huffman_table[(val2 >> (bits2 - huffman_table_bits)) + & huffman_mask]; + t3 = huffman_table[(val3 >> (bits3 - huffman_table_bits)) + & huffman_mask]; + t4 = huffman_table[(val4 >> (bits4 - huffman_table_bits)) + & huffman_mask]; + + *plit1 = t1 >> 8; + ++plit1; + bits1 -= t1 & 0xff; + + *plit2 = t2 >> 8; + ++plit2; + bits2 -= t2 & 0xff; + + *plit3 = t3 >> 8; + ++plit3; + bits3 -= t3 & 0xff; + + *plit4 = t4 >> 8; + ++plit4; + bits4 -= t4 & 0xff; + + ++i; + } + while (bits1 > 11 && bits2 > 11 && bits3 > 11 && bits4 > 11); + } + + while (i < regenerated_stream_size) + { + int use4; + + use4 = i < regenerated_stream_size4; + + if (!elf_fetch_bits_backward (&pback1, pbackend1, &val1, &bits1)) + return 0; + if (!elf_fetch_bits_backward (&pback2, pbackend2, &val2, &bits2)) + return 0; + if (!elf_fetch_bits_backward (&pback3, pbackend3, &val3, &bits3)) + return 0; + if (use4) + { + if (!elf_fetch_bits_backward (&pback4, pbackend4, &val4, &bits4)) + return 0; + } + + if (unlikely (bits1 < huffman_table_bits)) + { + t1 = huffman_table[(val1 << (huffman_table_bits - bits1)) + & huffman_mask]; + if (unlikely (bits1 < (t1 & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t1 = huffman_table[(val1 >> (bits1 - huffman_table_bits)) + & huffman_mask]; + + if (unlikely (bits2 < huffman_table_bits)) + { + t2 = huffman_table[(val2 << (huffman_table_bits - bits2)) + & huffman_mask]; + if (unlikely (bits2 < (t2 & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t2 = huffman_table[(val2 >> (bits2 - huffman_table_bits)) + & huffman_mask]; + + if (unlikely (bits3 < huffman_table_bits)) + { + t3 = huffman_table[(val3 << (huffman_table_bits - bits3)) + & huffman_mask]; + if (unlikely (bits3 < (t3 & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t3 = huffman_table[(val3 >> (bits3 - huffman_table_bits)) + & huffman_mask]; + + if (use4) + { + if (unlikely (bits4 < huffman_table_bits)) + { + t4 = huffman_table[(val4 << (huffman_table_bits - bits4)) + & huffman_mask]; + if (unlikely (bits4 < (t4 & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t4 = huffman_table[(val4 >> (bits4 - huffman_table_bits)) + & huffman_mask]; + + *plit4 = t4 >> 8; + ++plit4; + bits4 -= t4 & 0xff; + } + + *plit1 = t1 >> 8; + ++plit1; + bits1 -= t1 & 0xff; + + *plit2 = t2 >> 8; + ++plit2; + bits2 -= t2 & 0xff; + + *plit3 = t3 >> 8; + ++plit3; + bits3 -= t3 & 0xff; + + ++i; + } + } + + return 1; +} + +/* The information used to decompress a sequence code, which can be a literal + length, an offset, or a match length. */ + +struct elf_zstd_seq_decode +{ + const struct elf_zstd_fse_baseline_entry *table; + int table_bits; +}; + +/* Unpack a sequence code compression mode. */ + +static int +elf_zstd_unpack_seq_decode (int mode, + const unsigned char **ppin, + const unsigned char *pinend, + const struct elf_zstd_fse_baseline_entry *predef, + int predef_bits, + uint16_t *scratch, + int maxidx, + struct elf_zstd_fse_baseline_entry *table, + int table_bits, + int (*conv)(const struct elf_zstd_fse_entry *, + int, + struct elf_zstd_fse_baseline_entry *), + struct elf_zstd_seq_decode *decode) +{ + switch (mode) + { + case 0: + decode->table = predef; + decode->table_bits = predef_bits; + break; + + case 1: + { + struct elf_zstd_fse_entry entry; + + if (unlikely (*ppin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + entry.symbol = **ppin; + ++*ppin; + entry.bits = 0; + entry.base = 0; + decode->table_bits = 0; + if (!conv (&entry, 0, table)) + return 0; + } + break; + + case 2: + { + struct elf_zstd_fse_entry *fse_table; + + /* We use the same space for the simple FSE table and the baseline + table. */ + fse_table = (struct elf_zstd_fse_entry *)table; + decode->table_bits = table_bits; + if (!elf_zstd_read_fse (ppin, pinend, scratch, maxidx, fse_table, + &decode->table_bits)) + return 0; + if (!conv (fse_table, decode->table_bits, table)) + return 0; + decode->table = table; + } + break; + + case 3: + if (unlikely (decode->table_bits == -1)) + { + elf_uncompress_failed (); + return 0; + } + break; + + default: + elf_uncompress_failed (); + return 0; + } + + return 1; +} + +/* Decompress a zstd stream from PIN/SIN to POUT/SOUT. Code based on RFC 8878. + Return 1 on success, 0 on error. */ + +static int +elf_zstd_decompress (const unsigned char *pin, size_t sin, + unsigned char *zdebug_table, unsigned char *pout, + size_t sout) +{ + const unsigned char *pinend; + unsigned char *poutstart; + unsigned char *poutend; + struct elf_zstd_seq_decode literal_decode; + struct elf_zstd_fse_baseline_entry *literal_fse_table; + struct elf_zstd_seq_decode match_decode; + struct elf_zstd_fse_baseline_entry *match_fse_table; + struct elf_zstd_seq_decode offset_decode; + struct elf_zstd_fse_baseline_entry *offset_fse_table; + uint16_t *huffman_table; + int huffman_table_bits; + uint32_t repeated_offset1; + uint32_t repeated_offset2; + uint32_t repeated_offset3; + uint16_t *scratch; + unsigned char hdr; + int has_checksum; + uint64_t content_size; + int last_block; + + pinend = pin + sin; + poutstart = pout; + poutend = pout + sout; + + literal_decode.table = NULL; + literal_decode.table_bits = -1; + literal_fse_table = ((struct elf_zstd_fse_baseline_entry *) + (zdebug_table + ZSTD_TABLE_LITERAL_FSE_OFFSET)); + + match_decode.table = NULL; + match_decode.table_bits = -1; + match_fse_table = ((struct elf_zstd_fse_baseline_entry *) + (zdebug_table + ZSTD_TABLE_MATCH_FSE_OFFSET)); + + offset_decode.table = NULL; + offset_decode.table_bits = -1; + offset_fse_table = ((struct elf_zstd_fse_baseline_entry *) + (zdebug_table + ZSTD_TABLE_OFFSET_FSE_OFFSET)); + huffman_table = ((uint16_t *) + (zdebug_table + ZSTD_TABLE_HUFFMAN_OFFSET)); + huffman_table_bits = 0; + scratch = ((uint16_t *) + (zdebug_table + ZSTD_TABLE_WORK_OFFSET)); + + repeated_offset1 = 1; + repeated_offset2 = 4; + repeated_offset3 = 8; + + if (unlikely (sin < 4)) + { + elf_uncompress_failed (); + return 0; + } + + /* These values are the zstd magic number. */ + if (unlikely (pin[0] != 0x28 + || pin[1] != 0xb5 + || pin[2] != 0x2f + || pin[3] != 0xfd)) + { + elf_uncompress_failed (); + return 0; + } + + pin += 4; + + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + + hdr = *pin++; + + /* We expect a single frame. */ + if (unlikely ((hdr & (1 << 5)) == 0)) + { + elf_uncompress_failed (); + return 0; + } + /* Reserved bit must be zero. */ + if (unlikely ((hdr & (1 << 3)) != 0)) + { + elf_uncompress_failed (); + return 0; + } + /* We do not expect a dictionary. */ + if (unlikely ((hdr & 3) != 0)) + { + elf_uncompress_failed (); + return 0; + } + has_checksum = (hdr & (1 << 2)) != 0; + switch (hdr >> 6) + { + case 0: + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + content_size = (uint64_t) *pin++; + break; + case 1: + if (unlikely (pin + 1 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + content_size = (((uint64_t) pin[0]) | (((uint64_t) pin[1]) << 8)) + 256; + pin += 2; + break; + case 2: + if (unlikely (pin + 3 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + content_size = ((uint64_t) pin[0] + | (((uint64_t) pin[1]) << 8) + | (((uint64_t) pin[2]) << 16) + | (((uint64_t) pin[3]) << 24)); + pin += 4; + break; + case 3: + if (unlikely (pin + 7 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + content_size = ((uint64_t) pin[0] + | (((uint64_t) pin[1]) << 8) + | (((uint64_t) pin[2]) << 16) + | (((uint64_t) pin[3]) << 24) + | (((uint64_t) pin[4]) << 32) + | (((uint64_t) pin[5]) << 40) + | (((uint64_t) pin[6]) << 48) + | (((uint64_t) pin[7]) << 56)); + pin += 8; + break; + default: + elf_uncompress_failed (); + return 0; + } + + if (unlikely (content_size != (size_t) content_size + || (size_t) content_size != sout)) + { + elf_uncompress_failed (); + return 0; + } + + last_block = 0; + while (!last_block) + { + uint32_t block_hdr; + int block_type; + uint32_t block_size; + + if (unlikely (pin + 2 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + block_hdr = ((uint32_t) pin[0] + | (((uint32_t) pin[1]) << 8) + | (((uint32_t) pin[2]) << 16)); + pin += 3; + + last_block = block_hdr & 1; + block_type = (block_hdr >> 1) & 3; + block_size = block_hdr >> 3; + + switch (block_type) + { + case 0: + /* Raw_Block */ + if (unlikely ((size_t) block_size > (size_t) (pinend - pin))) + { + elf_uncompress_failed (); + return 0; + } + if (unlikely ((size_t) block_size > (size_t) (poutend - pout))) + { + elf_uncompress_failed (); + return 0; + } + memcpy (pout, pin, block_size); + pout += block_size; + pin += block_size; + break; + + case 1: + /* RLE_Block */ + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + if (unlikely ((size_t) block_size > (size_t) (poutend - pout))) + { + elf_uncompress_failed (); + return 0; + } + memset (pout, *pin, block_size); + pout += block_size; + pin++; + break; + + case 2: + { + const unsigned char *pblockend; + unsigned char *plitstack; + unsigned char *plit; + uint32_t literal_count; + unsigned char seq_hdr; + size_t seq_count; + size_t seq; + const unsigned char *pback; + uint64_t val; + unsigned int bits; + unsigned int literal_state; + unsigned int offset_state; + unsigned int match_state; + + /* Compressed_Block */ + if (unlikely ((size_t) block_size > (size_t) (pinend - pin))) + { + elf_uncompress_failed (); + return 0; + } + + pblockend = pin + block_size; + + /* Read the literals into the end of the output space, and leave + PLIT pointing at them. */ + + if (!elf_zstd_read_literals (&pin, pblockend, pout, poutend, + scratch, huffman_table, + &huffman_table_bits, + &plitstack)) + return 0; + plit = plitstack; + literal_count = poutend - plit; + + seq_hdr = *pin; + pin++; + if (seq_hdr < 128) + seq_count = seq_hdr; + else if (seq_hdr < 255) + { + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + seq_count = ((seq_hdr - 128) << 8) + *pin; + pin++; + } + else + { + if (unlikely (pin + 1 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + seq_count = *pin + (pin[1] << 8) + 0x7f00; + pin += 2; + } + + if (seq_count > 0) + { + int (*pfn)(const struct elf_zstd_fse_entry *, + int, struct elf_zstd_fse_baseline_entry *); + + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + seq_hdr = *pin; + ++pin; + + pfn = elf_zstd_make_literal_baseline_fse; + if (!elf_zstd_unpack_seq_decode ((seq_hdr >> 6) & 3, + &pin, pinend, + &elf_zstd_lit_table[0], 6, + scratch, 35, + literal_fse_table, 9, pfn, + &literal_decode)) + return 0; + + pfn = elf_zstd_make_offset_baseline_fse; + if (!elf_zstd_unpack_seq_decode ((seq_hdr >> 4) & 3, + &pin, pinend, + &elf_zstd_offset_table[0], 5, + scratch, 31, + offset_fse_table, 8, pfn, + &offset_decode)) + return 0; + + pfn = elf_zstd_make_match_baseline_fse; + if (!elf_zstd_unpack_seq_decode ((seq_hdr >> 2) & 3, + &pin, pinend, + &elf_zstd_match_table[0], 6, + scratch, 52, + match_fse_table, 9, pfn, + &match_decode)) + return 0; + } + + pback = pblockend - 1; + if (!elf_fetch_backward_init (&pback, pin, &val, &bits)) + return 0; + + bits -= literal_decode.table_bits; + literal_state = ((val >> bits) + & ((1U << literal_decode.table_bits) - 1)); + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= offset_decode.table_bits; + offset_state = ((val >> bits) + & ((1U << offset_decode.table_bits) - 1)); + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= match_decode.table_bits; + match_state = ((val >> bits) + & ((1U << match_decode.table_bits) - 1)); + + seq = 0; + while (1) + { + const struct elf_zstd_fse_baseline_entry *pt; + uint32_t offset_basebits; + uint32_t offset_baseline; + uint32_t offset_bits; + uint32_t offset_base; + uint32_t offset; + uint32_t match_baseline; + uint32_t match_bits; + uint32_t match_base; + uint32_t match; + uint32_t literal_baseline; + uint32_t literal_bits; + uint32_t literal_base; + uint32_t literal; + uint32_t need; + uint32_t add; + + pt = &offset_decode.table[offset_state]; + offset_basebits = pt->basebits; + offset_baseline = pt->baseline; + offset_bits = pt->bits; + offset_base = pt->base; + + /* This case can be more than 16 bits, which is all that + elf_fetch_bits_backward promises. */ + need = offset_basebits; + add = 0; + if (unlikely (need > 16)) + { + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= 16; + add = (val >> bits) & ((1U << 16) - 1); + need -= 16; + add <<= need; + } + if (need > 0) + { + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= need; + add += (val >> bits) & ((1U << need) - 1); + } + + offset = offset_baseline + add; + + pt = &match_decode.table[match_state]; + need = pt->basebits; + match_baseline = pt->baseline; + match_bits = pt->bits; + match_base = pt->base; + + add = 0; + if (need > 0) + { + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= need; + add = (val >> bits) & ((1U << need) - 1); + } + + match = match_baseline + add; + + pt = &literal_decode.table[literal_state]; + need = pt->basebits; + literal_baseline = pt->baseline; + literal_bits = pt->bits; + literal_base = pt->base; + + add = 0; + if (need > 0) + { + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= need; + add = (val >> bits) & ((1U << need) - 1); + } + + literal = literal_baseline + add; + + /* See the comment in elf_zstd_make_offset_baseline_fse. */ + if (offset_basebits > 1) + { + repeated_offset3 = repeated_offset2; + repeated_offset2 = repeated_offset1; + repeated_offset1 = offset; + } + else + { + if (unlikely (literal == 0)) + ++offset; + switch (offset) + { + case 1: + offset = repeated_offset1; + break; + case 2: + offset = repeated_offset2; + repeated_offset2 = repeated_offset1; + repeated_offset1 = offset; + break; + case 3: + offset = repeated_offset3; + repeated_offset3 = repeated_offset2; + repeated_offset2 = repeated_offset1; + repeated_offset1 = offset; + break; + case 4: + offset = repeated_offset1 - 1; + repeated_offset3 = repeated_offset2; + repeated_offset2 = repeated_offset1; + repeated_offset1 = offset; + break; + } + } + + ++seq; + if (seq < seq_count) + { + uint32_t v; + + /* Update the three states. */ + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + + need = literal_bits; + bits -= need; + v = (val >> bits) & (((uint32_t)1 << need) - 1); + + literal_state = literal_base + v; + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + + need = match_bits; + bits -= need; + v = (val >> bits) & (((uint32_t)1 << need) - 1); + + match_state = match_base + v; + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + + need = offset_bits; + bits -= need; + v = (val >> bits) & (((uint32_t)1 << need) - 1); + + offset_state = offset_base + v; + } + + /* The next sequence is now in LITERAL, OFFSET, MATCH. */ + + /* Copy LITERAL bytes from the literals. */ + + if (unlikely ((size_t)(poutend - pout) < literal)) + { + elf_uncompress_failed (); + return 0; + } + + if (unlikely (literal_count < literal)) + { + elf_uncompress_failed (); + return 0; + } + + literal_count -= literal; + + /* Often LITERAL is small, so handle small cases quickly. */ + switch (literal) + { + case 8: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 7: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 6: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 5: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 4: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 3: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 2: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 1: + *pout++ = *plit++; + break; + + case 0: + break; + + default: + if (unlikely ((size_t)(plit - pout) < literal)) + { + uint32_t move; + + move = plit - pout; + while (literal > move) + { + memcpy (pout, plit, move); + pout += move; + plit += move; + literal -= move; + } + } + + memcpy (pout, plit, literal); + pout += literal; + plit += literal; + } + + if (match > 0) + { + /* Copy MATCH bytes from the decoded output at OFFSET. */ + + if (unlikely ((size_t)(poutend - pout) < match)) + { + elf_uncompress_failed (); + return 0; + } + + if (unlikely ((size_t)(pout - poutstart) < offset)) + { + elf_uncompress_failed (); + return 0; + } + + if (offset >= match) + { + memcpy (pout, pout - offset, match); + pout += match; + } + else + { + while (match > 0) + { + uint32_t copy; + + copy = match < offset ? match : offset; + memcpy (pout, pout - offset, copy); + match -= copy; + pout += copy; + } + } + } + + if (unlikely (seq >= seq_count)) + { + /* Copy remaining literals. */ + if (literal_count > 0 && plit != pout) + { + if (unlikely ((size_t)(poutend - pout) + < literal_count)) + { + elf_uncompress_failed (); + return 0; + } + + if ((size_t)(plit - pout) < literal_count) + { + uint32_t move; + + move = plit - pout; + while (literal_count > move) + { + memcpy (pout, plit, move); + pout += move; + plit += move; + literal_count -= move; + } + } + + memcpy (pout, plit, literal_count); + } + + pout += literal_count; + + break; + } + } + + pin = pblockend; + } + break; + + case 3: + default: + elf_uncompress_failed (); + return 0; + } + } + + if (has_checksum) + { + if (unlikely (pin + 4 > pinend)) + { + elf_uncompress_failed (); + return 0; + } + + /* We don't currently verify the checksum. Currently running GNU ld with + --compress-debug-sections=zstd does not seem to generate a + checksum. */ + + pin += 4; + } + + if (pin != pinend) + { + elf_uncompress_failed (); + return 0; + } + + return 1; +} + +#define ZDEBUG_TABLE_SIZE \ + (ZLIB_TABLE_SIZE > ZSTD_TABLE_SIZE ? ZLIB_TABLE_SIZE : ZSTD_TABLE_SIZE) + /* Uncompress the old compressed debug format, the one emitted by --compress-debug-sections=zlib-gnu. The compressed data is in COMPRESSED / COMPRESSED_SIZE, and the function writes to @@ -2628,6 +5094,8 @@ elf_uncompress_chdr (struct backtrace_state *state, unsigned char **uncompressed, size_t *uncompressed_size) { const b_elf_chdr *chdr; + char *alc; + size_t alc_len; unsigned char *po; *uncompressed = NULL; @@ -2639,31 +5107,50 @@ elf_uncompress_chdr (struct backtrace_state *state, chdr = (const b_elf_chdr *) compressed; - if (chdr->ch_type != ELFCOMPRESS_ZLIB) - { - /* Unsupported compression algorithm. */ - return 1; - } - + alc = NULL; + alc_len = 0; if (*uncompressed != NULL && *uncompressed_size >= chdr->ch_size) po = *uncompressed; else { - po = (unsigned char *) backtrace_alloc (state, chdr->ch_size, - error_callback, data); - if (po == NULL) + alc_len = chdr->ch_size; + alc = (char*)backtrace_alloc (state, alc_len, error_callback, data); + if (alc == NULL) return 0; + po = (unsigned char *) alc; } - if (!elf_zlib_inflate_and_verify (compressed + sizeof (b_elf_chdr), - compressed_size - sizeof (b_elf_chdr), - zdebug_table, po, chdr->ch_size)) - return 1; + switch (chdr->ch_type) + { + case ELFCOMPRESS_ZLIB: + if (!elf_zlib_inflate_and_verify (compressed + sizeof (b_elf_chdr), + compressed_size - sizeof (b_elf_chdr), + zdebug_table, po, chdr->ch_size)) + goto skip; + break; + + case ELFCOMPRESS_ZSTD: + if (!elf_zstd_decompress (compressed + sizeof (b_elf_chdr), + compressed_size - sizeof (b_elf_chdr), + (unsigned char *)zdebug_table, po, + chdr->ch_size)) + goto skip; + break; + + default: + /* Unsupported compression algorithm. */ + goto skip; + } *uncompressed = po; *uncompressed_size = chdr->ch_size; return 1; + + skip: + if (alc != NULL && alc_len > 0) + backtrace_free (state, alc, alc_len, error_callback, data); + return 1; } /* This function is a hook for testing the zlib support. It is only @@ -2692,6 +5179,31 @@ backtrace_uncompress_zdebug (struct backtrace_state *state, return ret; } +/* This function is a hook for testing the zstd support. It is only used by + tests. */ + +int +backtrace_uncompress_zstd (struct backtrace_state *state, + const unsigned char *compressed, + size_t compressed_size, + backtrace_error_callback error_callback, + void *data, unsigned char *uncompressed, + size_t uncompressed_size) +{ + unsigned char *zdebug_table; + int ret; + + zdebug_table = ((unsigned char *) backtrace_alloc (state, ZDEBUG_TABLE_SIZE, + error_callback, data)); + if (zdebug_table == NULL) + return 0; + ret = elf_zstd_decompress (compressed, compressed_size, + zdebug_table, uncompressed, uncompressed_size); + backtrace_free (state, zdebug_table, ZDEBUG_TABLE_SIZE, + error_callback, data); + return ret; +} + /* Number of LZMA states. */ #define LZMA_STATES (12) @@ -4688,7 +7200,7 @@ elf_add (struct backtrace_state *state, const char *filename, int descriptor, if (zdebug_table == NULL) { zdebug_table = ((uint16_t *) - backtrace_alloc (state, ZDEBUG_TABLE_SIZE, + backtrace_alloc (state, ZLIB_TABLE_SIZE, error_callback, data)); if (zdebug_table == NULL) goto fail; @@ -4714,8 +7226,15 @@ elf_add (struct backtrace_state *state, const char *filename, int descriptor, } } + if (zdebug_table != NULL) + { + backtrace_free (state, zdebug_table, ZLIB_TABLE_SIZE, + error_callback, data); + zdebug_table = NULL; + } + /* Uncompress the official ELF format - (--compress-debug-sections=zlib-gabi). */ + (--compress-debug-sections=zlib-gabi, --compress-debug-sections=zstd). */ for (i = 0; i < (int) DEBUG_MAX; ++i) { unsigned char *uncompressed_data; diff --git a/Source/ThirdParty/tracy/libbacktrace/internal.hpp b/Source/ThirdParty/tracy/libbacktrace/internal.hpp index 96c097e02..f871844b6 100644 --- a/Source/ThirdParty/tracy/libbacktrace/internal.hpp +++ b/Source/ThirdParty/tracy/libbacktrace/internal.hpp @@ -371,6 +371,15 @@ extern int backtrace_uncompress_zdebug (struct backtrace_state *, unsigned char **uncompressed, size_t *uncompressed_size); +/* A test-only hook for elf_zstd_decompress. */ + +extern int backtrace_uncompress_zstd (struct backtrace_state *, + const unsigned char *compressed, + size_t compressed_size, + backtrace_error_callback, void *data, + unsigned char *uncompressed, + size_t uncompressed_size); + /* A test-only hook for elf_uncompress_lzma. */ extern int backtrace_uncompress_lzma (struct backtrace_state *, diff --git a/Source/ThirdParty/tracy/tracy/Tracy.hpp b/Source/ThirdParty/tracy/tracy/Tracy.hpp index 8ef26d59e..e9c943d2f 100644 --- a/Source/ThirdParty/tracy/tracy/Tracy.hpp +++ b/Source/ThirdParty/tracy/tracy/Tracy.hpp @@ -1,6 +1,18 @@ #ifndef __TRACY_HPP__ #define __TRACY_HPP__ +#ifndef TracyFunction +# define TracyFunction __FUNCTION__ +#endif + +#ifndef TracyFile +# define TracyFile __FILE__ +#endif + +#ifndef TracyLine +# define TracyLine __LINE__ +#endif + #ifndef TRACY_ENABLE #define ZoneNamed(x,y) @@ -80,6 +92,8 @@ #define TracySourceCallbackRegister(x,y) #define TracyParameterRegister(x,y) #define TracyParameterSetup(x,y,z,w) +#define TracyIsConnected false +#define TracySetProgramName(x) #define TracyFiberEnter(x) #define TracyFiberLeave @@ -124,21 +138,21 @@ public: } #if defined TRACY_HAS_CALLSTACK && defined TRACY_CALLSTACK -# define ZoneNamed( varname, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), TRACY_CALLSTACK, active ) -# define ZoneNamedN( varname, name, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), TRACY_CALLSTACK, active ) -# define ZoneNamedC( varname, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), TRACY_CALLSTACK, active ) -# define ZoneNamedNC( varname, name, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), TRACY_CALLSTACK, active ) +# define ZoneNamed( varname, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), TRACY_CALLSTACK, active ) +# define ZoneNamedN( varname, name, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), TRACY_CALLSTACK, active ) +# define ZoneNamedC( varname, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), TRACY_CALLSTACK, active ) +# define ZoneNamedNC( varname, name, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), TRACY_CALLSTACK, active ) -# define ZoneTransient( varname, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), nullptr, 0, TRACY_CALLSTACK, active ) -# define ZoneTransientN( varname, name, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), name, strlen( name ), TRACY_CALLSTACK, active ) +# define ZoneTransient( varname, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), nullptr, 0, TRACY_CALLSTACK, active ) +# define ZoneTransientN( varname, name, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), name, strlen( name ), TRACY_CALLSTACK, active ) #else -# define ZoneNamed( varname, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), active ) -# define ZoneNamedN( varname, name, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), active ) -# define ZoneNamedC( varname, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), active ) -# define ZoneNamedNC( varname, name, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), active ) +# define ZoneNamed( varname, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), active ) +# define ZoneNamedN( varname, name, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), active ) +# define ZoneNamedC( varname, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), active ) +# define ZoneNamedNC( varname, name, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), active ) -# define ZoneTransient( varname, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), nullptr, 0, active ) -# define ZoneTransientN( varname, name, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), name, strlen( name ), active ) +# define ZoneTransient( varname, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), nullptr, 0, active ) +# define ZoneTransientN( varname, name, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), name, strlen( name ), active ) #endif #define ZoneScoped ZoneNamed( ___tracy_scoped_zone, true ) @@ -164,13 +178,13 @@ public: #define FrameImage( image, width, height, offset, flip ) tracy::Profiler::SendFrameImage( image, width, height, offset, flip ) -#define TracyLockable( type, varname ) tracy::Lockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, #type " " #varname, __FILE__, __LINE__, 0 }; return &srcloc; }() } -#define TracyLockableN( type, varname, desc ) tracy::Lockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, desc, __FILE__, __LINE__, 0 }; return &srcloc; }() } -#define TracySharedLockable( type, varname ) tracy::SharedLockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, #type " " #varname, __FILE__, __LINE__, 0 }; return &srcloc; }() } -#define TracySharedLockableN( type, varname, desc ) tracy::SharedLockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, desc, __FILE__, __LINE__, 0 }; return &srcloc; }() } +#define TracyLockable( type, varname ) tracy::Lockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, #type " " #varname, TracyFile, TracyLine, 0 }; return &srcloc; }() } +#define TracyLockableN( type, varname, desc ) tracy::Lockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, desc, TracyFile, TracyLine, 0 }; return &srcloc; }() } +#define TracySharedLockable( type, varname ) tracy::SharedLockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, #type " " #varname, TracyFile, TracyLine, 0 }; return &srcloc; }() } +#define TracySharedLockableN( type, varname, desc ) tracy::SharedLockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, desc, TracyFile, TracyLine, 0 }; return &srcloc; }() } #define LockableBase( type ) tracy::Lockable #define SharedLockableBase( type ) tracy::SharedLockable -#define LockMark( varname ) static constexpr tracy::SourceLocationData __tracy_lock_location_##varname { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; varname.Mark( &__tracy_lock_location_##varname ) +#define LockMark( varname ) static constexpr tracy::SourceLocationData __tracy_lock_location_##varname { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; varname.Mark( &__tracy_lock_location_##varname ) #define LockableName( varname, txt, size ) varname.CustomName( txt, size ) #define TracyPlot( name, val ) tracy::Profiler::PlotData( name, val ) @@ -211,13 +225,13 @@ public: #endif #ifdef TRACY_HAS_CALLSTACK -# define ZoneNamedS( varname, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), depth, active ) -# define ZoneNamedNS( varname, name, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), depth, active ) -# define ZoneNamedCS( varname, color, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), depth, active ) -# define ZoneNamedNCS( varname, name, color, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), depth, active ) +# define ZoneNamedS( varname, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), depth, active ) +# define ZoneNamedNS( varname, name, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), depth, active ) +# define ZoneNamedCS( varname, color, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), depth, active ) +# define ZoneNamedNCS( varname, name, color, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), depth, active ) -# define ZoneTransientS( varname, depth, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), nullptr, 0, depth, active ) -# define ZoneTransientNS( varname, name, depth, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), name, strlen( name ), depth, active ) +# define ZoneTransientS( varname, depth, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), nullptr, 0, depth, active ) +# define ZoneTransientNS( varname, name, depth, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), name, strlen( name ), depth, active ) # define ZoneScopedS( depth ) ZoneNamedS( ___tracy_scoped_zone, depth, true ) # define ZoneScopedNS( name, depth ) ZoneNamedNS( ___tracy_scoped_zone, name, depth, true ) @@ -272,6 +286,7 @@ public: #define TracyParameterRegister( cb, data ) tracy::Profiler::ParameterRegister( cb, data ) #define TracyParameterSetup( idx, name, isBool, val ) tracy::Profiler::ParameterSetup( idx, name, isBool, val ) #define TracyIsConnected tracy::GetProfiler().IsConnected() +#define TracySetProgramName( name ) tracy::GetProfiler().SetProgramName( name ); #ifdef TRACY_FIBERS # define TracyFiberEnter( fiber ) tracy::Profiler::EnterFiber( fiber ) From 47ca4228dd7db495f3bb15e3929d03e2981555da Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 20:50:52 +0100 Subject: [PATCH 221/546] Fix compile warning --- Source/Engine/Core/Collections/BitArray.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h index 78e4f94f4..15e69087b 100644 --- a/Source/Engine/Core/Collections/BitArray.h +++ b/Source/Engine/Core/Collections/BitArray.h @@ -294,7 +294,7 @@ public: void SetAll(const bool value) { if (_count != 0) - Platform::MemorySet(_allocation.Get(), ToItemCount(_count) * sizeof(ItemType), value ? MAX_uint64 : 0); + Platform::MemorySet(_allocation.Get(), ToItemCount(_count) * sizeof(ItemType), value ? MAX_uint32 : 0); } /// From a685918e1012639cdaea134ee83e9e2de627aaec Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 6 Nov 2023 23:57:54 +0100 Subject: [PATCH 222/546] Add default value for material `Sphere Mask` node to create blob gradient around UV center #1830 --- .../Tools/MaterialGenerator/MaterialGenerator.Material.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index 8aa300731..830a2ab06 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -393,8 +393,8 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) // Sphere Mask case 28: { - const auto a = tryGetValue(node->GetBox(0), 0, Value::Zero); - const auto b = tryGetValue(node->GetBox(1), 1, Value::Zero).Cast(a.Type); + const auto a = tryGetValue(node->GetBox(0), getUVs); + const auto b = tryGetValue(node->GetBox(1), Value::Half).Cast(a.Type); const auto radius = tryGetValue(node->GetBox(2), node->Values[0]).AsFloat(); const auto hardness = tryGetValue(node->GetBox(3), node->Values[1]).AsFloat(); const auto invert = tryGetValue(node->GetBox(4), node->Values[2]).AsBool(); From 42e83117365e241c9b070a62a4f9b2c06ca30541 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 00:13:13 +0100 Subject: [PATCH 223/546] Add engine version in Editor main window title #1847 --- Source/Editor/Modules/WindowsModule.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 05d72598f..cda83b56e 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -171,9 +171,13 @@ namespace FlaxEditor.Modules var mainWindow = MainWindow; if (mainWindow) { - var projectPath = Globals.ProjectFolder.Replace('/', '\\'); - var platformBit = Platform.Is64BitApp ? "64" : "32"; - var title = string.Format("Flax Editor - \'{0}\' ({1}-bit)", projectPath, platformBit); + var projectPath = Globals.ProjectFolder; +#if PLATFORM_WINDOWS + projectPath = projectPath.Replace('/', '\\'); +#endif + var engineVersion = Editor.EngineProject.Version; + var engineVersionText = engineVersion.Revision > 0 ? $"{engineVersion.Major}.{engineVersion.Minor}.{engineVersion.Revision}" : $"{engineVersion.Major}.{engineVersion.Minor}"; + var title = $"Flax Editor {engineVersionText} - \'{projectPath}\'"; mainWindow.Title = title; } } @@ -735,7 +739,6 @@ namespace FlaxEditor.Modules settings.Size = Platform.DesktopSize * 0.75f; settings.StartPosition = WindowStartPosition.CenterScreen; settings.ShowAfterFirstPaint = true; - #if PLATFORM_WINDOWS if (!Editor.Instance.Options.Options.Interface.UseNativeWindowSystem) { @@ -747,12 +750,9 @@ namespace FlaxEditor.Modules #elif PLATFORM_LINUX settings.HasBorder = false; #endif - MainWindow = Platform.CreateWindow(ref settings); - if (MainWindow == null) { - // Error Editor.LogError("Failed to create editor main window!"); return; } From 4b2595e904d38181810f66f42aaa23bead81a55d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 00:19:02 +0100 Subject: [PATCH 224/546] Remove tooltips from `Camera` and use automatic ones from xml comments --- Source/Engine/Level/Actors/Camera.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Level/Actors/Camera.h b/Source/Engine/Level/Actors/Camera.h index 22d950dd4..80b0fb4de 100644 --- a/Source/Engine/Level/Actors/Camera.h +++ b/Source/Engine/Level/Actors/Camera.h @@ -66,7 +66,7 @@ public: /// /// Gets the value indicating if camera should use perspective rendering mode, otherwise it will use orthographic projection. /// - API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Camera\"), Tooltip(\"Enables perspective projection mode, otherwise uses orthographic.\")") + API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Camera\")") bool GetUsePerspective() const; /// @@ -77,7 +77,7 @@ public: /// /// Gets the camera's field of view (in degrees). /// - API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\"), Tooltip(\"Field of view angle in degrees.\")") + API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\")") float GetFieldOfView() const; /// @@ -88,7 +88,7 @@ public: /// /// Gets the custom aspect ratio. 0 if not use custom value. /// - API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\"), Tooltip(\"Custom aspect ratio to use. Set to 0 to disable.\")") + API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\")") float GetCustomAspectRatio() const; /// @@ -99,7 +99,7 @@ public: /// /// Gets camera's near plane distance. /// - API_PROPERTY(Attributes="EditorOrder(30), DefaultValue(10.0f), Limit(0, 1000, 0.05f), EditorDisplay(\"Camera\"), Tooltip(\"Near clipping plane distance\")") + API_PROPERTY(Attributes="EditorOrder(30), DefaultValue(10.0f), Limit(0, 1000, 0.05f), EditorDisplay(\"Camera\")") float GetNearPlane() const; /// @@ -110,7 +110,7 @@ public: /// /// Gets camera's far plane distance. /// - API_PROPERTY(Attributes="EditorOrder(40), DefaultValue(40000.0f), Limit(0, float.MaxValue, 5), EditorDisplay(\"Camera\"), Tooltip(\"Far clipping plane distance\")") + API_PROPERTY(Attributes="EditorOrder(40), DefaultValue(40000.0f), Limit(0, float.MaxValue, 5), EditorDisplay(\"Camera\")") float GetFarPlane() const; /// @@ -121,7 +121,7 @@ public: /// /// Gets the orthographic projection scale. /// - API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\"), Tooltip(\"Orthographic projection scale\")") + API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\")") float GetOrthographicScale() const; /// From a90cf7c28f8af103a6a6b8e4623474f8373b617d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 00:26:14 +0100 Subject: [PATCH 225/546] Add better formatting for automatic tooltips in Editor from xml comments --- .../Modules/SourceCodeEditing/CodeDocsModule.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs index 225bad082..082439402 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs @@ -147,6 +147,17 @@ namespace FlaxEditor.Modules.SourceCodeEditing } if (key != null) xml.TryGetValue(key, out text); + + // Customize tooltips for properties to be more human-readable in UI + if (text != null && memberType.HasFlag(MemberTypes.Property) && text.StartsWith("Gets or sets ", StringComparison.Ordinal)) + { + text = text.Substring(13); + unsafe + { + fixed (char* e = text) + e[0] = char.ToUpper(e[0]); + } + } } } From 4fdeb773a5a6dcf758ddb021d2dc11b073e9f903 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 00:29:29 +0100 Subject: [PATCH 226/546] Add hidding Camera properties based on `UsePerspective` option #1858 --- Source/Engine/Level/Actors/Camera.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Level/Actors/Camera.h b/Source/Engine/Level/Actors/Camera.h index 80b0fb4de..c63a5dcf6 100644 --- a/Source/Engine/Level/Actors/Camera.h +++ b/Source/Engine/Level/Actors/Camera.h @@ -66,7 +66,7 @@ public: /// /// Gets the value indicating if camera should use perspective rendering mode, otherwise it will use orthographic projection. /// - API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Camera\")") + API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(true), EditorDisplay(\"Camera\")") bool GetUsePerspective() const; /// @@ -77,7 +77,7 @@ public: /// /// Gets the camera's field of view (in degrees). /// - API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\")") + API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\"), VisibleIf(nameof(UsePerspective))") float GetFieldOfView() const; /// @@ -88,7 +88,7 @@ public: /// /// Gets the custom aspect ratio. 0 if not use custom value. /// - API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\")") + API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\"), VisibleIf(nameof(UsePerspective))") float GetCustomAspectRatio() const; /// @@ -121,7 +121,7 @@ public: /// /// Gets the orthographic projection scale. /// - API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\")") + API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\"), VisibleIf(nameof(UsePerspective), true)") float GetOrthographicScale() const; /// From 87a9dedba4b3253b8648b13bff18625c952c9144 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 00:56:55 +0100 Subject: [PATCH 227/546] Refactor default `ContactOffset` for `Collider` to be `2` (keep `10` for `CharacterController`) #1728 --- Source/Engine/Physics/Colliders/CharacterController.cpp | 1 + Source/Engine/Physics/Colliders/Collider.cpp | 2 +- Source/Engine/Physics/Colliders/Collider.h | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Physics/Colliders/CharacterController.cpp b/Source/Engine/Physics/Colliders/CharacterController.cpp index 59521e51c..41ee95d04 100644 --- a/Source/Engine/Physics/Colliders/CharacterController.cpp +++ b/Source/Engine/Physics/Colliders/CharacterController.cpp @@ -23,6 +23,7 @@ CharacterController::CharacterController(const SpawnParams& params) , _nonWalkableMode(NonWalkableModes::PreventClimbing) , _lastFlags(CollisionFlags::None) { + _contactOffset = 10.0f; } float CharacterController::GetRadius() const diff --git a/Source/Engine/Physics/Colliders/Collider.cpp b/Source/Engine/Physics/Colliders/Collider.cpp index 49de1f799..6c292bf20 100644 --- a/Source/Engine/Physics/Colliders/Collider.cpp +++ b/Source/Engine/Physics/Colliders/Collider.cpp @@ -19,7 +19,7 @@ Collider::Collider(const SpawnParams& params) , _shape(nullptr) , _staticActor(nullptr) , _cachedScale(1.0f) - , _contactOffset(10.0f) + , _contactOffset(2.0f) { Material.Changed.Bind(this); } diff --git a/Source/Engine/Physics/Colliders/Collider.h b/Source/Engine/Physics/Colliders/Collider.h index 6b9dfcb9d..d8c261c1c 100644 --- a/Source/Engine/Physics/Colliders/Collider.h +++ b/Source/Engine/Physics/Colliders/Collider.h @@ -74,7 +74,7 @@ public: /// /// Colliders whose distance is less than the sum of their ContactOffset values will generate contacts. The contact offset must be positive. Contact offset allows the collision detection system to predictively enforce the contact constraint even when the objects are slightly separated. /// - API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(10.0f), Limit(0, 100), EditorDisplay(\"Collider\")") + API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(2.0f), Limit(0, 100), EditorDisplay(\"Collider\")") FORCE_INLINE float GetContactOffset() const { return _contactOffset; From 6648481d1285cdcfc9ef0f5ff4fbc1103488e0af Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 00:57:18 +0100 Subject: [PATCH 228/546] Improve various doc comments to be more usable as tooltips in Editor --- Source/Engine/Level/Actors/SpotLight.h | 32 ++++------ Source/Engine/Physics/Actors/RigidBody.h | 70 ++++------------------ Source/Engine/Physics/Colliders/Collider.h | 20 ++----- 3 files changed, 28 insertions(+), 94 deletions(-) diff --git a/Source/Engine/Level/Actors/SpotLight.h b/Source/Engine/Level/Actors/SpotLight.h index 37735692a..2c16d38e4 100644 --- a/Source/Engine/Level/Actors/SpotLight.h +++ b/Source/Engine/Level/Actors/SpotLight.h @@ -24,7 +24,7 @@ private: public: /// - /// Light source bulb radius + /// Light source bulb radius. /// API_FIELD(Attributes="EditorOrder(2), DefaultValue(0.0f), EditorDisplay(\"Light\"), Limit(0, 1000, 0.01f)") float SourceRadius = 0.0f; @@ -42,54 +42,51 @@ public: float FallOffExponent = 8.0f; /// - /// IES texture (light profiles from real world measured data) + /// IES texture (light profiles from real world measured data). /// API_FIELD(Attributes="EditorOrder(211), DefaultValue(null), EditorDisplay(\"IES Profile\", \"IES Texture\")") AssetReference IESTexture; /// - /// Enable/disable using light brightness from IES profile + /// Enable/disable using light brightness from IES profile. /// API_FIELD(Attributes="EditorOrder(212), DefaultValue(false), EditorDisplay(\"IES Profile\", \"Use IES Brightness\")") bool UseIESBrightness = false; /// - /// Global scale for IES brightness contribution + /// Global scale for IES brightness contribution. /// API_FIELD(Attributes="EditorOrder(213), DefaultValue(1.0f), Limit(0, 10000, 0.01f), EditorDisplay(\"IES Profile\", \"Brightness Scale\")") float IESBrightnessScale = 1.0f; public: /// - /// Computes light brightness value + /// Computes light brightness value. /// - /// Brightness float ComputeBrightness() const; /// - /// Gets scaled light radius + /// Gets scaled light radius. /// float GetScaledRadius() const; /// - /// Gets light radius + /// Gets light radius. /// - API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(1000.0f), EditorDisplay(\"Light\"), Tooltip(\"Light radius\"), Limit(0, 10000, 0.1f)") + API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(1000.0f), EditorDisplay(\"Light\"), Limit(0, 10000, 0.1f)") FORCE_INLINE float GetRadius() const { return _radius; } /// - /// Sets light radius + /// Sets light radius. /// - /// New radius API_PROPERTY() void SetRadius(float value); /// - /// Gets the spot light's outer cone angle (in degrees) + /// Gets the spot light's outer cone angle (in degrees). /// - /// Outer angle (in degrees) API_PROPERTY(Attributes="EditorOrder(22), DefaultValue(43.0f), EditorDisplay(\"Light\"), Limit(1, 89, 0.1f)") FORCE_INLINE float GetOuterConeAngle() const { @@ -97,15 +94,13 @@ public: } /// - /// Sets the spot light's outer cone angle (in degrees) + /// Sets the spot light's outer cone angle (in degrees). /// - /// Value to assign API_PROPERTY() void SetOuterConeAngle(float value); /// - /// Sets the spot light's inner cone angle (in degrees) + /// Sets the spot light's inner cone angle (in degrees). /// - /// Inner angle (in degrees) API_PROPERTY(Attributes="EditorOrder(21), DefaultValue(10.0f), EditorDisplay(\"Light\"), Limit(1, 89, 0.1f)") FORCE_INLINE float GetInnerConeAngle() const { @@ -113,9 +108,8 @@ public: } /// - /// Sets the spot light's inner cone angle (in degrees) + /// Sets the spot light's inner cone angle (in degrees). /// - /// Value to assign API_PROPERTY() void SetInnerConeAngle(float value); private: diff --git a/Source/Engine/Physics/Actors/RigidBody.h b/Source/Engine/Physics/Actors/RigidBody.h index e203e70fc..e7b929483 100644 --- a/Source/Engine/Physics/Actors/RigidBody.h +++ b/Source/Engine/Physics/Actors/RigidBody.h @@ -41,19 +41,8 @@ protected: public: /// - /// Enables kinematic mode for the rigidbody. + /// Enables kinematic mode for the rigidbody. Kinematic rigidbodies are special dynamic actors that are not influenced by forces(such as gravity), and have no momentum. They are considered to have infinite mass and can push regular dynamic actors out of the way. Kinematics will not collide with static or other kinematic objects but are great for moving platforms or characters, where direct motion control is desired. /// - /// - /// Kinematic rigidbodies are special dynamic actors that are not influenced by forces(such as gravity), and have no momentum. - /// They are considered to have infinite mass and can push regular dynamic actors out of the way. - /// Kinematics will not collide with static or other kinematic objects. - /// - /// Kinematic rigidbodies are great for moving platforms or characters, where direct motion control is desired. - /// - /// - /// Kinematic rigidbodies are incompatible with CCD. - /// - /// API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(false), EditorDisplay(\"Rigid Body\")") FORCE_INLINE bool GetIsKinematic() const { @@ -61,26 +50,13 @@ public: } /// - /// Enables kinematic mode for the rigidbody. + /// Enables kinematic mode for the rigidbody. Kinematic rigidbodies are special dynamic actors that are not influenced by forces(such as gravity), and have no momentum. They are considered to have infinite mass and can push regular dynamic actors out of the way. Kinematics will not collide with static or other kinematic objects but are great for moving platforms or characters, where direct motion control is desired. /// - /// - /// Kinematic rigidbodies are special dynamic actors that are not influenced by forces(such as gravity), and have no momentum. - /// They are considered to have infinite mass and can push regular dynamic actors out of the way. - /// Kinematics will not collide with static or other kinematic objects. - /// - /// Kinematic rigidbodies are great for moving platforms or characters, where direct motion control is desired. - /// - /// - /// Kinematic rigidbodies are incompatible with CCD. - /// - /// - /// The value. API_PROPERTY() void SetIsKinematic(const bool value); /// - /// Gets the 'drag' force added to reduce linear movement. + /// Gets the 'drag' force added to reduce linear movement. Linear damping can be used to slow down an object. The higher the drag the more the object slows down. /// - /// Linear damping can be used to slow down an object. The higher the drag the more the object slows down. API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(0.01f), Limit(0), EditorDisplay(\"Rigid Body\")") FORCE_INLINE float GetLinearDamping() const { @@ -88,16 +64,13 @@ public: } /// - /// Sets the 'drag' force added to reduce linear movement. + /// Sets the 'drag' force added to reduce linear movement. Linear damping can be used to slow down an object. The higher the drag the more the object slows down. /// - /// Linear damping can be used to slow down an object. The higher the drag the more the object slows down. - /// The value. API_PROPERTY() void SetLinearDamping(float value); /// - /// Gets the 'drag' force added to reduce angular movement. + /// Gets the 'drag' force added to reduce angular movement. Angular damping can be used to slow down the rotation of an object. The higher the drag the more the rotation slows down. /// - /// Angular damping can be used to slow down the rotation of an object. The higher the drag the more the rotation slows down. API_PROPERTY(Attributes="EditorOrder(70), DefaultValue(0.05f), Limit(0), EditorDisplay(\"Rigid Body\")") FORCE_INLINE float GetAngularDamping() const { @@ -105,9 +78,8 @@ public: } /// - /// Sets the 'drag' force added to reduce angular movement. + /// Sets the 'drag' force added to reduce angular movement. Angular damping can be used to slow down the rotation of an object. The higher the drag the more the rotation slows down. /// - /// Angular damping can be used to slow down the rotation of an object. The higher the drag the more the rotation slows down. /// The value. API_PROPERTY() void SetAngularDamping(float value); @@ -123,7 +95,6 @@ public: /// /// If true simulation and collisions detection will be enabled for the rigidbody. /// - /// The value. API_PROPERTY() void SetEnableSimulation(bool value); /// @@ -138,7 +109,6 @@ public: /// /// If true Continuous Collision Detection (CCD) will be used for this component. /// - /// The value. API_PROPERTY() void SetUseCCD(const bool value); /// @@ -153,7 +123,6 @@ public: /// /// If object should have the force of gravity applied. /// - /// The value. API_PROPERTY() void SetEnableGravity(bool value); /// @@ -168,7 +137,6 @@ public: /// /// If object should start awake, or if it should initially be sleeping. /// - /// The value. API_PROPERTY() void SetStartAwake(bool value); /// @@ -183,16 +151,11 @@ public: /// /// If true, it will update mass when actor scale changes. /// - /// The value. API_PROPERTY() void SetUpdateMassWhenScaleChanges(bool value); /// - /// Gets the maximum angular velocity that a simulated object can achieve. + /// Gets the maximum angular velocity that a simulated object can achieve. The angular velocity of rigidbodies is clamped to MaxAngularVelocity to avoid numerical instability with fast rotating bodies. Because this may prevent intentional fast rotations on objects such as wheels, you can override this value per rigidbody. /// - /// - /// The angular velocity of rigidbodies is clamped to MaxAngularVelocity to avoid numerical instability with fast rotating bodies. - /// Because this may prevent intentional fast rotations on objects such as wheels, you can override this value per rigidbody. - /// API_PROPERTY(Attributes="EditorOrder(90), DefaultValue(7.0f), Limit(0), EditorDisplay(\"Rigid Body\")") FORCE_INLINE float GetMaxAngularVelocity() const { @@ -200,13 +163,8 @@ public: } /// - /// Sets the maximum angular velocity that a simulated object can achieve. + /// Sets the maximum angular velocity that a simulated object can achieve. The angular velocity of rigidbodies is clamped to MaxAngularVelocity to avoid numerical instability with fast rotating bodies. Because this may prevent intentional fast rotations on objects such as wheels, you can override this value per rigidbody. /// - /// - /// The angular velocity of rigidbodies is clamped to MaxAngularVelocity to avoid numerical instability with fast rotating bodies. - /// Because this may prevent intentional fast rotations on objects such as wheels, you can override this value per rigidbody. - /// - /// The value. API_PROPERTY() void SetMaxAngularVelocity(float value); /// @@ -218,7 +176,6 @@ public: /// /// Override the auto computed mass. /// - /// The value. API_PROPERTY() void SetOverrideMass(bool value); /// @@ -230,8 +187,6 @@ public: /// /// Sets the mass value measured in kilograms (use override value only if OverrideMass is checked). /// - /// If set auto enables mass override. - /// The value. API_PROPERTY() void SetMass(float value); /// @@ -243,7 +198,6 @@ public: /// /// Sets the per-instance scaling of the mass. /// - /// The value. API_PROPERTY() void SetMassScale(float value); /// @@ -258,7 +212,6 @@ public: /// /// Sets the user specified offset for the center of mass of this object, from the calculated location. /// - /// The value. API_PROPERTY() void SetCenterOfMassOffset(const Float3& value); /// @@ -273,28 +226,27 @@ public: /// /// Sets the object movement constraint flags that define degrees of freedom are allowed for the simulation of object. /// - /// The value. API_PROPERTY() void SetConstraints(const RigidbodyConstraints value); public: /// /// Gets the linear velocity of the rigidbody. /// - /// It's used mostly to get the current velocity. Manual modifications may result in unrealistic behaviour. + /// It's used mostly to get the current velocity. Manual modifications may result in unrealistic behaviour. API_PROPERTY(Attributes="HideInEditor") Vector3 GetLinearVelocity() const; /// /// Sets the linear velocity of the rigidbody. /// - /// It's used mostly to get the current velocity. Manual modifications may result in unrealistic behaviour. + /// It's used mostly to get the current velocity. Manual modifications may result in unrealistic behaviour. /// The value. API_PROPERTY() void SetLinearVelocity(const Vector3& value) const; /// /// Gets the angular velocity of the rigidbody measured in radians per second. /// - /// It's used mostly to get the current angular velocity. Manual modifications may result in unrealistic behaviour. + /// It's used mostly to get the current angular velocity. Manual modifications may result in unrealistic behaviour. API_PROPERTY(Attributes="HideInEditor") Vector3 GetAngularVelocity() const; diff --git a/Source/Engine/Physics/Colliders/Collider.h b/Source/Engine/Physics/Colliders/Collider.h index d8c261c1c..d3bae9407 100644 --- a/Source/Engine/Physics/Colliders/Collider.h +++ b/Source/Engine/Physics/Colliders/Collider.h @@ -35,11 +35,8 @@ public: void* GetPhysicsShape() const; /// - /// Gets the 'IsTrigger' flag. + /// Gets the 'IsTrigger' flag. A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter and OnTriggerExit message when a rigidbody enters or exits the trigger volume. /// - /// - /// A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter, OnTriggerExit and OnTriggerStay message when a rigidbody enters or exits the trigger volume. - /// API_PROPERTY(Attributes="EditorOrder(0), DefaultValue(false), EditorDisplay(\"Collider\")") FORCE_INLINE bool GetIsTrigger() const { @@ -47,11 +44,8 @@ public: } /// - /// Sets the `IsTrigger` flag. A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter, OnTriggerExit and OnTriggerStay message when a rigidbody enters or exits the trigger volume. + /// Sets the `IsTrigger` flag. A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter and OnTriggerExit message when a rigidbody enters or exits the trigger volume. /// - /// - /// A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter, OnTriggerExit and OnTriggerStay message when a rigidbody enters or exits the trigger volume. - /// API_PROPERTY() void SetIsTrigger(bool value); /// @@ -69,11 +63,8 @@ public: API_PROPERTY() void SetCenter(const Vector3& value); /// - /// Gets the contact offset. + /// Gets the contact offset. Colliders whose distance is less than the sum of their ContactOffset values will generate contacts. The contact offset must be positive. Contact offset allows the collision detection system to predictively enforce the contact constraint even when the objects are slightly separated. /// - /// - /// Colliders whose distance is less than the sum of their ContactOffset values will generate contacts. The contact offset must be positive. Contact offset allows the collision detection system to predictively enforce the contact constraint even when the objects are slightly separated. - /// API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(2.0f), Limit(0, 100), EditorDisplay(\"Collider\")") FORCE_INLINE float GetContactOffset() const { @@ -81,11 +72,8 @@ public: } /// - /// Sets the contact offset. + /// Sets the contact offset. Colliders whose distance is less than the sum of their ContactOffset values will generate contacts. The contact offset must be positive. Contact offset allows the collision detection system to predictively enforce the contact constraint even when the objects are slightly separated. /// - /// - /// Colliders whose distance is less than the sum of their ContactOffset values will generate contacts. The contact offset must be positive. Contact offset allows the collision detection system to predictively enforce the contact constraint even when the objects are slightly separated. - /// API_PROPERTY() void SetContactOffset(float value); /// From d6e93a7fab1544c50b02242e42d37dabfcc09d66 Mon Sep 17 00:00:00 2001 From: SS Date: Mon, 6 Nov 2023 18:49:30 -0700 Subject: [PATCH 229/546] Fixed issue involving stale scripting assemblies in FlaxEngine.Json dynamic type resolution Added new ExtendedSerializationBinder Added callback to clear serializer cache on scripting assembly reload Added low-cost mechanism to invalidate the SerializerCache after domain reload --- .../Engine/Engine/NativeInterop.Unmanaged.cs | 2 + Source/Engine/Engine/NativeInterop.cs | 65 +++-- .../ExtendedSerializationBinder.cs | 238 ++++++++++++++++++ Source/Engine/Serialization/JsonSerializer.cs | 133 +++++++--- 4 files changed, 381 insertions(+), 57 deletions(-) create mode 100644 Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 0391974b6..574b74580 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -1022,6 +1022,8 @@ namespace FlaxEngine.Interop pair.Value.Free(); classAttributesCacheCollectible.Clear(); + FlaxEngine.Json.JsonSerializer.ResetCache(); + // Unload the ALC bool unloading = true; scriptingAssemblyLoadContext.Unloading += (alc) => { unloading = false; }; diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index fea1fadda..28ac5024d 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -278,44 +278,65 @@ namespace FlaxEngine.Interop if (typeCache.TryGetValue(typeName, out Type type)) return type; - type = Type.GetType(typeName, ResolveAssemblyByName, null); + type = Type.GetType(typeName, ResolveAssembly, null); if (type == null) - { - foreach (var assembly in scriptingAssemblyLoadContext.Assemblies) - { - type = assembly.GetType(typeName); - if (type != null) - break; - } - } + type = ResolveSlow(typeName); if (type == null) { - string oldTypeName = typeName; + string fullTypeName = typeName; typeName = typeName.Substring(0, typeName.IndexOf(',')); - type = Type.GetType(typeName, ResolveAssemblyByName, null); + type = Type.GetType(typeName, ResolveAssembly, null); if (type == null) - { - foreach (var assembly in scriptingAssemblyLoadContext.Assemblies) - { - type = assembly.GetType(typeName); - if (type != null) - break; - } - } - typeName = oldTypeName; + type = ResolveSlow(typeName); + + typeName = fullTypeName; } typeCache.Add(typeName, type); return type; + + /// Resolve the type by manually checking every scripting assembly + static Type ResolveSlow(string typeName) { + foreach (var assembly in scriptingAssemblyLoadContext.Assemblies) { + var type = assembly.GetType(typeName); + if (type != null) + return type; + } + return null; + } + + /// Resolve the assembly by name + static Assembly ResolveAssembly(AssemblyName name) => ResolveScriptingAssemblyByName(name, allowPartial: false); } - private static Assembly ResolveAssemblyByName(AssemblyName assemblyName) + /// Find among the scripting assemblies. + /// The name to find + /// If true, partial names should be allowed to be resolved. + /// The resolved assembly, or null if none could be found. + internal static Assembly ResolveScriptingAssemblyByName(AssemblyName assemblyName, bool allowPartial = false) { foreach (Assembly assembly in scriptingAssemblyLoadContext.Assemblies) - if (assembly.GetName() == assemblyName) + { + var curName = assembly.GetName(); + + if (curName == assemblyName || (allowPartial && curName.Name == assemblyName.Name)) return assembly; + } + + if (allowPartial) // Check partial names if full name isn't found + { + string partialName = assemblyName.Name; + + foreach (Assembly assembly in scriptingAssemblyLoadContext.Assemblies) + { + var curName = assembly.GetName(); + + if (curName.Name == partialName) + return assembly; + } + } return null; } diff --git a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs new file mode 100644 index 000000000..5f976f24f --- /dev/null +++ b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs @@ -0,0 +1,238 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Runtime.Serialization; +using FlaxEngine.Interop; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +#nullable enable + +namespace FlaxEngine.Json.JsonCustomSerializers +{ + internal class ExtendedSerializationBinder : SerializationBinder, ISerializationBinder + { + private record struct TypeKey(string? assemblyName, string typeName); + + private ConcurrentDictionary _cache; + private Func _resolve; + + /// Clear the cache + /// Should be cleared on scripting domain reload to avoid out of date types participating in dynamic type resolution + public void ResetCache() + { + _cache.Clear(); + } + + public override Type BindToType(string? assemblyName, string typeName) + { + return FindCachedType(new(assemblyName, typeName)); + } + + public override void BindToName(Type serializedType, out string? assemblyName, out string? typeName) + { + assemblyName = serializedType.Assembly.FullName; + typeName = serializedType.FullName; + } + + + public ExtendedSerializationBinder() + { + _resolve = ResolveBind; + _cache = new(); + } + + Type ResolveBind(TypeKey key) + { + Type? type = null; + if (key.assemblyName is null) { // No assembly name, attempt to find globally + type = FindTypeGlobal(key.typeName); + } + + if (type is null && key.assemblyName is not null) { // Type not found yet, but we have assembly name + Assembly? assembly = null; + + assembly = FindScriptingAssembly(key.assemblyName); // Attempt to load from scripting assembly + + if (assembly is null) + assembly = FindLoadedAssembly(key.assemblyName); // Attempt to load from loaded assemblies + + if (assembly is null) + assembly = FindUnloadedAssembly(key.assemblyName); // Attempt to load from unloaded assemblies + + if (assembly is null) + throw MakeAsmResolutionException(key.assemblyName); // Assembly failed to resolve + + type = FindTypeInAssembly(key.typeName, assembly); // We have assembly, attempt to load from assembly + } + + //if (type is null) + // type = _fallBack.BindToType(key.assemblyName, key.typeName); // Use fallback + + if (type is null) + throw MakeTypeResolutionException(key.assemblyName, key.typeName); + + return type; + } + + /// Attempt to find the assembly among loaded scripting assemblies + Assembly? FindScriptingAssembly(string assemblyName) + { + return NativeInterop.ResolveScriptingAssemblyByName(new AssemblyName(assemblyName), allowPartial: true); + } + + /// Attempt to find the assembly by name + Assembly? FindLoadedAssembly(string assemblyName) // TODO + { + return null; + } + + /// Attempt to find the assembly by name + Assembly? FindUnloadedAssembly(string assemblyName) + { + Assembly? assembly = null; + + assembly = Assembly.Load(new AssemblyName(assemblyName)); + + if (assembly is null) + assembly = Assembly.LoadWithPartialName(assemblyName); // Copying behavior of DefaultSerializationBinder + + + return assembly; + } + + + + Type? FindTypeInAssembly(string typeName, Assembly assembly) + { + var type = assembly.GetType(typeName); // Attempt to load directly + + if (type is null && typeName.IndexOf('`') >= 0) // Attempt failed, but name has generic variant tick, try resolving generic manually + type = FindTypeGeneric(typeName, assembly); + + return type; + } + + /// Attempt to find unqualified type by only name + Type? FindTypeGlobal(string typeName) + { + return Type.GetType(typeName); + } + + /// Get type from the cache + private Type FindCachedType(TypeKey key) + { + return _cache.GetOrAdd(key, _resolve); + } + + + + /********************************************* + ** Below code is adapted from Newtonsoft.Json + *********************************************/ + + /// Attempt to recursively resolve a generic type + private Type? FindTypeGeneric(string typeName, Assembly assembly) + { + Type? type = null; + int openBracketIndex = typeName.IndexOf('[', StringComparison.Ordinal); + if (openBracketIndex >= 0) { + string genericTypeDefName = typeName.Substring(0, openBracketIndex); // Find the unspecialized type + Type? genericTypeDef = assembly.GetType(genericTypeDefName); + if (genericTypeDef != null) { + List genericTypeArguments = new List(); // Recursively resolve the arguments + int scope = 0; + int typeArgStartIndex = 0; + int endIndex = typeName.Length - 1; + for (int i = openBracketIndex + 1; i < endIndex; ++i) { + char current = typeName[i]; + switch (current) { + case '[': + if (scope == 0) { + typeArgStartIndex = i + 1; + } + ++scope; + break; + case ']': + --scope; + if (scope == 0) { // All arguments resolved, compose our type + string typeArgAssemblyQualifiedName = typeName.Substring(typeArgStartIndex, i - typeArgStartIndex); + + TypeKey typeNameKey = SplitFullyQualifiedTypeName(typeArgAssemblyQualifiedName); + genericTypeArguments.Add(FindCachedType(typeNameKey)); + } + break; + } + } + + type = genericTypeDef.MakeGenericType(genericTypeArguments.ToArray()); + } + } + + return type; + } + + /// Split a fully qualified type name into assembly name, and type name + private static TypeKey SplitFullyQualifiedTypeName(string fullyQualifiedTypeName) + { + int? assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullyQualifiedTypeName); + + ReadOnlySpan typeName; + ReadOnlySpan assemblyName; + + if (assemblyDelimiterIndex != null) { + typeName = fullyQualifiedTypeName.AsSpan().Slice(0, assemblyDelimiterIndex ?? 0); + assemblyName = fullyQualifiedTypeName.AsSpan().Slice((assemblyDelimiterIndex ?? 0) + 1); + } else { + typeName = fullyQualifiedTypeName; + assemblyName = null; + } + + return new(new(assemblyName), new(typeName)); + } + + /// Find the assembly name inside a fully qualified type name + private static int? GetAssemblyDelimiterIndex(string fullyQualifiedTypeName) + { + // we need to get the first comma following all surrounded in brackets because of generic types + // e.g. System.Collections.Generic.Dictionary`2[[System.String, mscorlib,Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + int scope = 0; + for (int i = 0; i < fullyQualifiedTypeName.Length; i++) { + char current = fullyQualifiedTypeName[i]; + switch (current) { + case '[': + scope++; + break; + case ']': + scope--; + break; + case ',': + if (scope == 0) { + return i; + } + break; + } + } + + return null; + } + + + private static JsonSerializationException MakeAsmResolutionException(string asmName) + { + return new($"Could not load assembly '{asmName}'."); + } + + private static JsonSerializationException MakeTypeResolutionException(string? asmName, string typeName) + { + if (asmName is null) + return new($"Could not find '{typeName}'"); + else + return new($"Could not find '{typeName}' in assembly '{asmName}'."); + } + } +} diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index c36e00b6a..2eb9eec99 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -19,6 +19,7 @@ namespace FlaxEngine.Json { internal class SerializerCache { + public readonly JsonSerializerSettings settings; public Newtonsoft.Json.JsonSerializer JsonSerializer; public StringBuilder StringBuilder; public StringWriter StringWriter; @@ -28,37 +29,35 @@ namespace FlaxEngine.Json public StreamReader Reader; public bool IsWriting; public bool IsReading; + public uint cacheVersion; public unsafe SerializerCache(JsonSerializerSettings settings) { - JsonSerializer = Newtonsoft.Json.JsonSerializer.CreateDefault(settings); - JsonSerializer.Formatting = Formatting.Indented; - JsonSerializer.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; + this.settings = settings; + StringBuilder = new StringBuilder(256); StringWriter = new StringWriter(StringBuilder, CultureInfo.InvariantCulture); - SerializerWriter = new JsonSerializerInternalWriter(JsonSerializer); MemoryStream = new UnmanagedMemoryStream((byte*)0, 0); - Reader = new StreamReader(MemoryStream, Encoding.UTF8, false); - JsonWriter = new JsonTextWriter(StringWriter) + + lock (cacheSyncRoot) { - IndentChar = '\t', - Indentation = 1, - Formatting = JsonSerializer.Formatting, - DateFormatHandling = JsonSerializer.DateFormatHandling, - DateTimeZoneHandling = JsonSerializer.DateTimeZoneHandling, - FloatFormatHandling = JsonSerializer.FloatFormatHandling, - StringEscapeHandling = JsonSerializer.StringEscapeHandling, - Culture = JsonSerializer.Culture, - DateFormatString = JsonSerializer.DateFormatString, - }; + BuildSerializer(); + BuildRead(); + BuildWrite(); + + cacheVersion = currentCacheVersion; + } } public void ReadBegin() { + CheckCacheVersionRebuild(); + + // TODO: Reset reading state (eg if previous deserialization got exception) if (IsReading) - { - // TODO: Reset reading state (eg if previous deserialization got exception) - } + BuildRead(); + + IsWriting = false; IsReading = true; } @@ -70,23 +69,12 @@ namespace FlaxEngine.Json public void WriteBegin() { + CheckCacheVersionRebuild(); + + // Reset writing state (eg if previous serialization got exception) if (IsWriting) - { - // Reset writing state (eg if previous serialization got exception) - SerializerWriter = new JsonSerializerInternalWriter(JsonSerializer); - JsonWriter = new JsonTextWriter(StringWriter) - { - IndentChar = '\t', - Indentation = 1, - Formatting = JsonSerializer.Formatting, - DateFormatHandling = JsonSerializer.DateFormatHandling, - DateTimeZoneHandling = JsonSerializer.DateTimeZoneHandling, - FloatFormatHandling = JsonSerializer.FloatFormatHandling, - StringEscapeHandling = JsonSerializer.StringEscapeHandling, - Culture = JsonSerializer.Culture, - DateFormatString = JsonSerializer.DateFormatString, - }; - } + BuildWrite(); + StringBuilder.Clear(); IsWriting = true; IsReading = false; @@ -96,21 +84,83 @@ namespace FlaxEngine.Json { IsWriting = false; } + + /// Check that the cache is up to date, rebuild it if it isn't + private void CheckCacheVersionRebuild() + { + var cCV = currentCacheVersion; + if (cacheVersion == cCV) + return; + + lock (cacheSyncRoot) + { + cCV = currentCacheVersion; + if (cacheVersion == cCV) + return; + + BuildSerializer(); + + BuildRead(); + BuildWrite(); + + cacheVersion = cCV; + } + } + + /// Builds the serializer + private void BuildSerializer() + { + JsonSerializer = Newtonsoft.Json.JsonSerializer.CreateDefault(settings); + JsonSerializer.Formatting = Formatting.Indented; + JsonSerializer.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; + } + + /// Builds the reader state + private void BuildRead() + { + Reader = new StreamReader(MemoryStream, Encoding.UTF8, false); + } + + /// Builds the writer state + private void BuildWrite() + { + SerializerWriter = new JsonSerializerInternalWriter(JsonSerializer); + JsonWriter = new JsonTextWriter(StringWriter) { + IndentChar = '\t', + Indentation = 1, + Formatting = JsonSerializer.Formatting, + DateFormatHandling = JsonSerializer.DateFormatHandling, + DateTimeZoneHandling = JsonSerializer.DateTimeZoneHandling, + FloatFormatHandling = JsonSerializer.FloatFormatHandling, + StringEscapeHandling = JsonSerializer.StringEscapeHandling, + Culture = JsonSerializer.Culture, + DateFormatString = JsonSerializer.DateFormatString, + }; + } } internal static JsonSerializerSettings Settings = CreateDefaultSettings(false); internal static JsonSerializerSettings SettingsManagedOnly = CreateDefaultSettings(true); + internal static ExtendedSerializationBinder SerializationBinder; internal static FlaxObjectConverter ObjectConverter; + internal static ThreadLocal Current = new ThreadLocal(); internal static ThreadLocal Cache = new ThreadLocal(() => new SerializerCache(Settings)); internal static ThreadLocal CacheManagedOnly = new ThreadLocal(() => new SerializerCache(SettingsManagedOnly)); internal static ThreadLocal CachedGuidBuffer = new ThreadLocal(() => Marshal.AllocHGlobal(32 * sizeof(char)), true); internal static string CachedGuidDigits = "0123456789abcdef"; + /// The version of the cache, used to check that a cache is not out of date + internal static uint currentCacheVersion = 0; + /// Used to synchronize cache operations such as rebuild, and + internal static readonly object cacheSyncRoot = new(); internal static JsonSerializerSettings CreateDefaultSettings(bool isManagedOnly) { //Newtonsoft.Json.Utilities.MiscellaneousUtils.ValueEquals = ValueEquals; + if (SerializationBinder is null) + SerializationBinder = new(); + var settings = new JsonSerializerSettings { ContractResolver = new ExtendedDefaultContractResolver(isManagedOnly), @@ -118,6 +168,7 @@ namespace FlaxEngine.Json TypeNameHandling = TypeNameHandling.Auto, NullValueHandling = NullValueHandling.Include, ObjectCreationHandling = ObjectCreationHandling.Auto, + SerializationBinder = SerializationBinder, }; if (ObjectConverter == null) ObjectConverter = new FlaxObjectConverter(); @@ -134,6 +185,18 @@ namespace FlaxEngine.Json return settings; } + /// Called to reset the serialization cache + internal static void ResetCache() + { + lock (cacheSyncRoot) + { + unchecked { currentCacheVersion++; } + + Newtonsoft.Json.JsonSerializer.ClearCache(); + SerializationBinder.ResetCache(); + } + } + internal static void Dispose() { CachedGuidBuffer.Values.ForEach(Marshal.FreeHGlobal); From b72849eafed8003073299ad14e2c60dd22a5cec3 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 6 Nov 2023 19:52:19 -0600 Subject: [PATCH 230/546] Fix anim slot from playing animations more than 1 time based on speed. --- Source/Engine/Animations/Graph/AnimGroup.Animation.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 0518fe248..5cceb31e3 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -2109,7 +2109,8 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu bucket.LoopsLeft--; bucket.LoopsDone++; } - value = SampleAnimation(node, loop, length, 0.0f, bucket.TimePosition, newTimePos, anim, slot.Speed); + // Speed is accounted for in the new time pos, so keep sample speed at 1 + value = SampleAnimation(node, loop, length, 0.0f, bucket.TimePosition, newTimePos, anim, 1); bucket.TimePosition = newTimePos; if (bucket.LoopsLeft == 0 && slot.BlendOutTime > 0.0f && length - slot.BlendOutTime < bucket.TimePosition) { From f6645e5600fff9618f1c87f5d41afba2d50300e2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 09:56:33 +0100 Subject: [PATCH 231/546] Add debug drawing contact offset for selected collider #1728 --- .../Engine/Physics/Colliders/BoxCollider.cpp | 7 +++++++ .../Physics/Colliders/CapsuleCollider.cpp | 20 ++++++++++++------- .../Physics/Colliders/SphereCollider.cpp | 7 +++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Physics/Colliders/BoxCollider.cpp b/Source/Engine/Physics/Colliders/BoxCollider.cpp index 90f0dab38..fde3b4632 100644 --- a/Source/Engine/Physics/Colliders/BoxCollider.cpp +++ b/Source/Engine/Physics/Colliders/BoxCollider.cpp @@ -81,6 +81,13 @@ void BoxCollider::OnDebugDrawSelected() const Color color = Color::GreenYellow; DEBUG_DRAW_WIRE_BOX(_bounds, color * 0.3f, 0, false); + if (_contactOffset > 0) + { + OrientedBoundingBox contactBounds = _bounds; + contactBounds.Extents += Vector3(_contactOffset) / contactBounds.Transformation.Scale; + DEBUG_DRAW_WIRE_BOX(contactBounds, Color::Blue.AlphaMultiplied(0.2f), 0, false); + } + Vector3 corners[8]; _bounds.GetCorners(corners); const float margin = 1.0f; diff --git a/Source/Engine/Physics/Colliders/CapsuleCollider.cpp b/Source/Engine/Physics/Colliders/CapsuleCollider.cpp index ba133e8b6..98346b0a9 100644 --- a/Source/Engine/Physics/Colliders/CapsuleCollider.cpp +++ b/Source/Engine/Physics/Colliders/CapsuleCollider.cpp @@ -42,27 +42,33 @@ void CapsuleCollider::DrawPhysicsDebug(RenderView& view) const BoundingSphere sphere(_sphere.Center - view.Origin, _sphere.Radius); if (!view.CullingFrustum.Intersects(sphere)) return; - Quaternion rot; - Quaternion::Multiply(_transform.Orientation, Quaternion::Euler(0, 90, 0), rot); + Quaternion rotation; + Quaternion::Multiply(_transform.Orientation, Quaternion::Euler(0, 90, 0), rotation); const float scaling = _cachedScale.GetAbsolute().MaxValue(); const float minSize = 0.001f; const float radius = Math::Max(Math::Abs(_radius) * scaling, minSize); const float height = Math::Max(Math::Abs(_height) * scaling, minSize); if (view.Mode == ViewMode::PhysicsColliders && !GetIsTrigger()) - DEBUG_DRAW_TUBE(_transform.LocalToWorld(_center), rot, radius, height, _staticActor ? Color::CornflowerBlue : Color::Orchid, 0, true); + DEBUG_DRAW_TUBE(_transform.LocalToWorld(_center), rotation, radius, height, _staticActor ? Color::CornflowerBlue : Color::Orchid, 0, true); else - DEBUG_DRAW_WIRE_TUBE(_transform.LocalToWorld(_center), rot, radius, height, Color::GreenYellow * 0.8f, 0, true); + DEBUG_DRAW_WIRE_TUBE(_transform.LocalToWorld(_center), rotation, radius, height, Color::GreenYellow * 0.8f, 0, true); } void CapsuleCollider::OnDebugDrawSelected() { - Quaternion rot; - Quaternion::Multiply(_transform.Orientation, Quaternion::Euler(0, 90, 0), rot); + Quaternion rotation; + Quaternion::Multiply(_transform.Orientation, Quaternion::Euler(0, 90, 0), rotation); const float scaling = _cachedScale.GetAbsolute().MaxValue(); const float minSize = 0.001f; const float radius = Math::Max(Math::Abs(_radius) * scaling, minSize); const float height = Math::Max(Math::Abs(_height) * scaling, minSize); - DEBUG_DRAW_WIRE_TUBE(_transform.LocalToWorld(_center), rot, radius, height, Color::GreenYellow, 0, false); + const Vector3 position = _transform.LocalToWorld(_center); + DEBUG_DRAW_WIRE_TUBE(position, rotation, radius, height, Color::GreenYellow, 0, false); + + if (_contactOffset > 0) + { + DEBUG_DRAW_WIRE_TUBE(position, rotation, radius + _contactOffset, height, Color::Blue.AlphaMultiplied(0.2f), 0, false); + } // Base Collider::OnDebugDrawSelected(); diff --git a/Source/Engine/Physics/Colliders/SphereCollider.cpp b/Source/Engine/Physics/Colliders/SphereCollider.cpp index e163484e8..fa2fb1cca 100644 --- a/Source/Engine/Physics/Colliders/SphereCollider.cpp +++ b/Source/Engine/Physics/Colliders/SphereCollider.cpp @@ -40,6 +40,13 @@ void SphereCollider::OnDebugDrawSelected() { DEBUG_DRAW_WIRE_SPHERE(_sphere, Color::GreenYellow, 0, false); + if (_contactOffset > 0) + { + BoundingSphere contactBounds = _sphere; + contactBounds.Radius += _contactOffset; + DEBUG_DRAW_WIRE_SPHERE(contactBounds, Color::Blue.AlphaMultiplied(0.2f), 0, false); + } + // Base Collider::OnDebugDrawSelected(); } From f163edfb7e4873d9075ac15abffdcb9a7f66787a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 10:24:38 +0100 Subject: [PATCH 232/546] Move `AssetPickerValidator` to Editor Content folder #1850 --- Source/Editor/{Utilities => Content}/AssetPickerValidator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename Source/Editor/{Utilities => Content}/AssetPickerValidator.cs (99%) diff --git a/Source/Editor/Utilities/AssetPickerValidator.cs b/Source/Editor/Content/AssetPickerValidator.cs similarity index 99% rename from Source/Editor/Utilities/AssetPickerValidator.cs rename to Source/Editor/Content/AssetPickerValidator.cs index 356a33534..66bb7582f 100644 --- a/Source/Editor/Utilities/AssetPickerValidator.cs +++ b/Source/Editor/Content/AssetPickerValidator.cs @@ -1,11 +1,10 @@ -using System; +using System; using System.IO; -using FlaxEditor.Content; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.Utilities; -namespace FlaxEditor.Utilities; +namespace FlaxEditor.Content; /// /// Manages and converts the selected content item to the appropriate types. Useful for drag operations. From 4238c43f278a9455f07d3a5389c0baeeaf639a99 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 10:54:11 +0100 Subject: [PATCH 233/546] Format code #1850 --- Source/Editor/Content/AssetPickerValidator.cs | 2 +- .../CustomEditors/Editors/CollectionEditor.cs | 46 +++++++------------ 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/Source/Editor/Content/AssetPickerValidator.cs b/Source/Editor/Content/AssetPickerValidator.cs index 66bb7582f..f43ab6a29 100644 --- a/Source/Editor/Content/AssetPickerValidator.cs +++ b/Source/Editor/Content/AssetPickerValidator.cs @@ -9,7 +9,7 @@ namespace FlaxEditor.Content; /// /// Manages and converts the selected content item to the appropriate types. Useful for drag operations. /// -public class AssetPickerValidator: IContentItemOwner +public class AssetPickerValidator : IContentItemOwner { private Asset _selected; private ContentItem _selectedItem; diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index aa418404d..7d92c71c0 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -2,17 +2,14 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Linq; using FlaxEditor.Content; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Drag; -using FlaxEditor.Options; using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; -using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -140,11 +137,11 @@ namespace FlaxEditor.CustomEditors.Editors overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type; spacing = collection.Spacing; } - + var dragArea = layout.CustomContainer(); dragArea.CustomControl.Editor = this; dragArea.CustomControl.ElementType = ElementType; - + // Check for the AssetReferenceAttribute. In JSON assets, it can be used to filter // which scripts can be dragged over and dropped on this collection editor. var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute); @@ -250,7 +247,7 @@ namespace FlaxEditor.CustomEditors.Editors var panel = dragArea.HorizontalPanel(); panel.Panel.Size = new Float2(0, 20); panel.Panel.Margin = new Margin(2); - + var removeButton = panel.Button("-", "Remove last item"); removeButton.Button.Size = new Float2(16, 16); removeButton.Button.Enabled = size > 0; @@ -262,7 +259,7 @@ namespace FlaxEditor.CustomEditors.Editors Resize(Count - 1); }; - + var addButton = panel.Button("+", "Add new item"); addButton.Button.Size = new Float2(16, 16); addButton.Button.Enabled = !NotNullItems || size > 0; @@ -400,27 +397,18 @@ namespace FlaxEditor.CustomEditors.Editors } return base.OnDirty(editor, value, token); } - + private class DragAreaControl : VerticalPanel { - // private DragHandlers _dragHandlers; - // private DragAssets _dragAssets; - // private DragActors _dragActors; private DragItems _dragItems; private DragActors _dragActors; private DragHandlers _dragHandlers; - private AssetPickerValidator _pickerValidator; - private ScriptType _elementType; - + public ScriptType ElementType { - get => _elementType; - set - { - _pickerValidator = new AssetPickerValidator(value); - _elementType = value; - } + get => _pickerValidator?.AssetType ?? ScriptType.Null; + set => _pickerValidator = new AssetPickerValidator(value); } public CollectionEditor Editor { get; set; } @@ -433,12 +421,13 @@ namespace FlaxEditor.CustomEditors.Editors /// public override void Draw() { - if (_dragHandlers is {HasValidDrag: true}) + if (_dragHandlers is { HasValidDrag: true }) { var area = new Rectangle(Float2.Zero, Size); Render2D.FillRectangle(area, Color.Orange * 0.5f); Render2D.DrawRectangle(area, Color.Black); } + base.Draw(); } @@ -523,7 +512,7 @@ namespace FlaxEditor.CustomEditors.Editors var type = Editor.Values.Type.GetElementType(); var array = TypeUtils.CreateArrayInstance(type, newSize); list.CopyTo(array, 0); - + for (var i = oldSize; i < newSize; i++) { var validator = new AssetPickerValidator @@ -543,7 +532,7 @@ namespace FlaxEditor.CustomEditors.Editors array.SetValue(validator.SelectedPath, i); else array.SetValue(validator.SelectedAsset, i); - + validator.OnDestroy(); } Editor.SetValue(array); @@ -558,7 +547,7 @@ namespace FlaxEditor.CustomEditors.Editors AssetType = _pickerValidator.AssetType, SelectedItem = item, }; - + if (typeof(AssetItem).IsAssignableFrom(ElementType.Type)) list.Add(validator.SelectedItem); else if (ElementType.Type == typeof(Guid)) @@ -569,7 +558,7 @@ namespace FlaxEditor.CustomEditors.Editors list.Add(validator.SelectedPath); else list.Add(validator.SelectedAsset); - + validator.OnDestroy(); } Editor.SetValue(list); @@ -589,7 +578,7 @@ namespace FlaxEditor.CustomEditors.Editors list = Editor.Values.Type.CreateInstance() as IList; } } - + if (list.IsFixedSize) { var oldSize = list.Count; @@ -597,7 +586,7 @@ namespace FlaxEditor.CustomEditors.Editors var type = Editor.Values.Type.GetElementType(); var array = TypeUtils.CreateArrayInstance(type, newSize); list.CopyTo(array, 0); - + for (var i = oldSize; i < newSize; i++) { var actor = _dragActors.Objects[i - oldSize].Actor; @@ -628,7 +617,7 @@ namespace FlaxEditor.CustomEditors.Editors Editor.SetValue(list); } } - + _dragHandlers.OnDragDrop(null); } @@ -636,5 +625,4 @@ namespace FlaxEditor.CustomEditors.Editors } } } - } From 7d43a0cc8bc8598cf29a685064772d6823cc4489 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 16:14:09 +0100 Subject: [PATCH 234/546] Fix various cases of closing window tabs when using Dock Windows in Editor #1750 --- Source/Editor/GUI/Docking/DockPanel.cs | 69 +++++++++++++++----------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/Source/Editor/GUI/Docking/DockPanel.cs b/Source/Editor/GUI/Docking/DockPanel.cs index e544285a5..b04aad08c 100644 --- a/Source/Editor/GUI/Docking/DockPanel.cs +++ b/Source/Editor/GUI/Docking/DockPanel.cs @@ -465,36 +465,47 @@ namespace FlaxEditor.GUI.Docking { if (Parent.Parent is SplitPanel splitter) { - // Check if has any child panels - var childPanel = new List(_childPanels); - for (int i = 0; i < childPanel.Count; i++) + // Check if there is another nested dock panel inside this dock panel and extract it here + var childPanels = _childPanels.ToArray(); + if (childPanels.Length != 0) { - // Undock all tabs - var panel = childPanel[i]; - int count = panel.TabsCount; - while (count-- > 0) + // Move tabs from child panels into this one + DockWindow selectedTab = null; + foreach (var childPanel in childPanels) { - panel.GetTab(0).Close(); + var childPanelTabs = childPanel.Tabs.ToArray(); + for (var i = 0; i < childPanelTabs.Length; i++) + { + var childPanelTab = childPanelTabs[i]; + if (selectedTab == null && childPanelTab.IsSelected) + selectedTab = childPanelTab; + childPanel.UndockWindow(childPanelTab); + AddTab(childPanelTab, false); + } } + if (selectedTab != null) + SelectTab(selectedTab); } - - // Unlink splitter - var splitterParent = splitter.Parent; - Assert.IsNotNull(splitterParent); - splitter.Parent = null; - - // Move controls from second split panel to the split panel parent - var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2; - var srcPanelChildrenCount = scrPanel.ChildrenCount; - for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + else { - scrPanel.GetChild(i).Parent = splitterParent; - } - Assert.IsTrue(scrPanel.ChildrenCount == 0); - Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + // Unlink splitter + var splitterParent = splitter.Parent; + Assert.IsNotNull(splitterParent); + splitter.Parent = null; - // Delete - splitter.Dispose(); + // Move controls from second split panel to the split panel parent + var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2; + var srcPanelChildrenCount = scrPanel.ChildrenCount; + for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + { + scrPanel.GetChild(i).Parent = splitterParent; + } + Assert.IsTrue(scrPanel.ChildrenCount == 0); + Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + + // Delete + splitter.Dispose(); + } } else if (!IsMaster) { @@ -582,19 +593,17 @@ namespace FlaxEditor.GUI.Docking /// Adds the tab. /// /// The window to insert as a tab. - protected virtual void AddTab(DockWindow window) + /// True if auto-select newly added tab. + protected virtual void AddTab(DockWindow window, bool autoSelect = true) { - // Dock _tabs.Add(window); window.ParentDockPanel = this; - - // Select tab - SelectTab(window); + if (autoSelect) + SelectTab(window); } private void CreateTabsProxy() { - // Check if has no tabs proxy created if (_tabsProxy == null) { // Create proxy and make set simple full dock From 0d85094ebb933f34364f71d02afb879a7f017b8f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 16:21:48 +0100 Subject: [PATCH 235/546] Fix error during new json asset creation via `ContentContextMenu` if the class is missing empty constructor #1838 --- Source/Editor/Windows/ContentWindow.ContextMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 1071e6b01..f577a94d3 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -192,7 +192,7 @@ namespace FlaxEditor.Windows { p = Editor.ContentDatabase.Proxy.Find(x => x.GetType() == type.Type); } - else + else if (type.CanCreateInstance) { // User can use attribute to put their own assets into the content context menu var generic = typeof(SpawnableJsonAssetProxy<>).MakeGenericType(type.Type); From 22d754e7972fb7c51382e928a2642d7414da8e29 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 16:21:56 +0100 Subject: [PATCH 236/546] Fix error during new json asset creation via `ContentContextMenu` if the class is missing empty constructor #1838 --- Source/Editor/Windows/ContentWindow.ContextMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index f577a94d3..168067977 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -187,7 +187,7 @@ namespace FlaxEditor.Windows continue; // Get context proxy - ContentProxy p; + ContentProxy p = null; if (type.Type.IsSubclassOf(typeof(ContentProxy))) { p = Editor.ContentDatabase.Proxy.Find(x => x.GetType() == type.Type); From 74bcf7d9e598be82635086570555b41cd85b9988 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 7 Nov 2023 18:30:05 +0100 Subject: [PATCH 237/546] Fix custom editor layout rebuilding when it has more editors in use #1616 --- Source/Editor/CustomEditors/CustomEditor.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 03b21e12b..32111e51c 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -157,6 +157,12 @@ namespace FlaxEditor.CustomEditors var values = _values; var presenter = _presenter; var layout = _layout; + if (layout.Editors.Count != 1) + { + // There are more editors using the same layout so rebuild parent editor to prevent removing others editors + _parent?.RebuildLayout(); + return; + } var control = layout.ContainerControl; var parent = _parent; var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; From 77b6a4a68b3725dee0a50ecce222af88954a99dd Mon Sep 17 00:00:00 2001 From: SS Date: Tue, 7 Nov 2023 16:03:03 -0700 Subject: [PATCH 238/546] Fixed issue in NativeInterop Readded check in current app domain to ExtendedSerializationBinder --- Source/Engine/Engine/NativeInterop.cs | 11 +++++-- .../ExtendedSerializationBinder.cs | 33 ++++++++++++------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 28ac5024d..b4635e0b4 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -317,11 +317,16 @@ namespace FlaxEngine.Interop /// The resolved assembly, or null if none could be found. internal static Assembly ResolveScriptingAssemblyByName(AssemblyName assemblyName, bool allowPartial = false) { - foreach (Assembly assembly in scriptingAssemblyLoadContext.Assemblies) + var lc = scriptingAssemblyLoadContext; + + if (lc is null) + return null; + + foreach (Assembly assembly in lc.Assemblies) { var curName = assembly.GetName(); - if (curName == assemblyName || (allowPartial && curName.Name == assemblyName.Name)) + if (curName == assemblyName) return assembly; } @@ -329,7 +334,7 @@ namespace FlaxEngine.Interop { string partialName = assemblyName.Name; - foreach (Assembly assembly in scriptingAssemblyLoadContext.Assemblies) + foreach (Assembly assembly in lc.Assemblies) { var curName = assembly.GetName(); diff --git a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs index 5f976f24f..8199a2bd9 100644 --- a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs +++ b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Reflection; using System.Runtime.Serialization; using FlaxEngine.Interop; @@ -56,13 +57,13 @@ namespace FlaxEngine.Json.JsonCustomSerializers if (type is null && key.assemblyName is not null) { // Type not found yet, but we have assembly name Assembly? assembly = null; - assembly = FindScriptingAssembly(key.assemblyName); // Attempt to load from scripting assembly + assembly = FindScriptingAssembly(new(key.assemblyName)); // Attempt to find in scripting assemblies if (assembly is null) - assembly = FindLoadedAssembly(key.assemblyName); // Attempt to load from loaded assemblies + assembly = FindLoadAssembly(new(key.assemblyName)); // Attempt to load if (assembly is null) - assembly = FindUnloadedAssembly(key.assemblyName); // Attempt to load from unloaded assemblies + assembly = FindDomainAssembly(new(key.assemblyName)); // Attempt to find in the current domain if (assembly is null) throw MakeAsmResolutionException(key.assemblyName); // Assembly failed to resolve @@ -80,27 +81,35 @@ namespace FlaxEngine.Json.JsonCustomSerializers } /// Attempt to find the assembly among loaded scripting assemblies - Assembly? FindScriptingAssembly(string assemblyName) + Assembly? FindScriptingAssembly(AssemblyName assemblyName) { - return NativeInterop.ResolveScriptingAssemblyByName(new AssemblyName(assemblyName), allowPartial: true); + return NativeInterop.ResolveScriptingAssemblyByName(assemblyName, allowPartial: true); } - /// Attempt to find the assembly by name - Assembly? FindLoadedAssembly(string assemblyName) // TODO + /// Attempt to find the assembly in the current domain + Assembly? FindDomainAssembly(AssemblyName assemblyName) { + var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToArray(); + + foreach (Assembly assembly in assemblies) { + var curName = assembly.GetName(); + + if (curName == assemblyName || curName.Name == assemblyName.Name) + return assembly; + } + return null; } - /// Attempt to find the assembly by name - Assembly? FindUnloadedAssembly(string assemblyName) + /// Attempt to load the assembly + Assembly? FindLoadAssembly(AssemblyName assemblyName) { Assembly? assembly = null; - assembly = Assembly.Load(new AssemblyName(assemblyName)); + assembly = Assembly.Load(assemblyName); if (assembly is null) - assembly = Assembly.LoadWithPartialName(assemblyName); // Copying behavior of DefaultSerializationBinder - + assembly = Assembly.LoadWithPartialName(assemblyName.Name); // Copying behavior of DefaultSerializationBinder return assembly; } From 52a1175f967a13a3630e0f36c362d7e98f792756 Mon Sep 17 00:00:00 2001 From: SS Date: Tue, 7 Nov 2023 16:30:50 -0700 Subject: [PATCH 239/546] Seperated out assembly resolution logic to it's own function --- .../ExtendedSerializationBinder.cs | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs index 8199a2bd9..b4f0173ab 100644 --- a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs +++ b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs @@ -19,14 +19,15 @@ namespace FlaxEngine.Json.JsonCustomSerializers { private record struct TypeKey(string? assemblyName, string typeName); - private ConcurrentDictionary _cache; - private Func _resolve; + private ConcurrentDictionary _typeCache; + private Func _resolveType; + /// Clear the cache /// Should be cleared on scripting domain reload to avoid out of date types participating in dynamic type resolution public void ResetCache() { - _cache.Clear(); + _typeCache.Clear(); } public override Type BindToType(string? assemblyName, string typeName) @@ -43,11 +44,11 @@ namespace FlaxEngine.Json.JsonCustomSerializers public ExtendedSerializationBinder() { - _resolve = ResolveBind; - _cache = new(); + _resolveType = ResolveType; + _typeCache = new(); } - Type ResolveBind(TypeKey key) + Type ResolveType(TypeKey key) { Type? type = null; if (key.assemblyName is null) { // No assembly name, attempt to find globally @@ -55,18 +56,7 @@ namespace FlaxEngine.Json.JsonCustomSerializers } if (type is null && key.assemblyName is not null) { // Type not found yet, but we have assembly name - Assembly? assembly = null; - - assembly = FindScriptingAssembly(new(key.assemblyName)); // Attempt to find in scripting assemblies - - if (assembly is null) - assembly = FindLoadAssembly(new(key.assemblyName)); // Attempt to load - - if (assembly is null) - assembly = FindDomainAssembly(new(key.assemblyName)); // Attempt to find in the current domain - - if (assembly is null) - throw MakeAsmResolutionException(key.assemblyName); // Assembly failed to resolve + var assembly = ResolveAssembly(new(key.assemblyName)); type = FindTypeInAssembly(key.typeName, assembly); // We have assembly, attempt to load from assembly } @@ -80,6 +70,25 @@ namespace FlaxEngine.Json.JsonCustomSerializers return type; } + + Assembly ResolveAssembly(AssemblyName name) + { + Assembly? assembly = null; + + assembly = FindScriptingAssembly(name); // Attempt to find in scripting assemblies + + if (assembly is null) + assembly = FindLoadAssembly(name); // Attempt to load + + if (assembly is null) + assembly = FindDomainAssembly(name); // Attempt to find in the current domain + + if (assembly is null) + throw MakeAsmResolutionException(name.FullName); // Assembly failed to resolve + + return assembly; + } + /// Attempt to find the assembly among loaded scripting assemblies Assembly? FindScriptingAssembly(AssemblyName assemblyName) { @@ -91,7 +100,7 @@ namespace FlaxEngine.Json.JsonCustomSerializers { var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToArray(); - foreach (Assembly assembly in assemblies) { + foreach (Assembly assembly in assemblies) { // Looking in domain may be necessary (in case of anon dynamic assembly for example) var curName = assembly.GetName(); if (curName == assemblyName || curName.Name == assemblyName.Name) @@ -115,7 +124,7 @@ namespace FlaxEngine.Json.JsonCustomSerializers } - + /// Attempt to find a type in a specified assembly Type? FindTypeInAssembly(string typeName, Assembly assembly) { var type = assembly.GetType(typeName); // Attempt to load directly @@ -135,10 +144,9 @@ namespace FlaxEngine.Json.JsonCustomSerializers /// Get type from the cache private Type FindCachedType(TypeKey key) { - return _cache.GetOrAdd(key, _resolve); + return _typeCache.GetOrAdd(key, _resolveType); } - - + /********************************************* ** Below code is adapted from Newtonsoft.Json From 2f9343c2365c1ca6cbd9ad04e2bc0505295a85f4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 8 Nov 2023 11:21:29 +0100 Subject: [PATCH 240/546] Add handling `ViewLayersMask` from camera when rendering `UICanvas` #1811 --- Source/Engine/UI/GUI/CanvasContainer.cs | 21 +++++++---- Source/Engine/UI/GUI/CanvasRootControl.cs | 43 ++++++++++++----------- Source/Engine/UI/UICanvas.cs | 16 +++++++++ 3 files changed, 52 insertions(+), 28 deletions(-) diff --git a/Source/Engine/UI/GUI/CanvasContainer.cs b/Source/Engine/UI/GUI/CanvasContainer.cs index f032a99d3..084a22503 100644 --- a/Source/Engine/UI/GUI/CanvasContainer.cs +++ b/Source/Engine/UI/GUI/CanvasContainer.cs @@ -41,11 +41,12 @@ namespace FlaxEngine.GUI protected override void DrawChildren() { // Draw all screen space canvases + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = 0; i < _children.Count; i++) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Is2D) + if (child.Visible && child.Is2D && layerMask.HasLayer(child.Canvas.Layer)) { child.Draw(); } @@ -69,10 +70,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { @@ -91,10 +93,11 @@ namespace FlaxEngine.GUI // Check all children collisions with mouse and fire events for them bool isFirst3DHandled = false; + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled) + if (child.Visible && child.Enabled && layerMask.HasLayer(child.Canvas.Layer)) { // Fire events if (child.Is2D) @@ -156,10 +159,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { @@ -183,10 +187,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { @@ -210,10 +215,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { @@ -237,10 +243,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { diff --git a/Source/Engine/UI/GUI/CanvasRootControl.cs b/Source/Engine/UI/GUI/CanvasRootControl.cs index b4fa067e3..b2ea9aaa0 100644 --- a/Source/Engine/UI/GUI/CanvasRootControl.cs +++ b/Source/Engine/UI/GUI/CanvasRootControl.cs @@ -74,6 +74,8 @@ namespace FlaxEngine.GUI return false; } + private bool SkipEvents => !_canvas.ReceivesEvents || !_canvas.IsVisible(); + /// public override CursorType Cursor { @@ -197,7 +199,7 @@ namespace FlaxEngine.GUI public override void Update(float deltaTime) { // UI navigation - if (_canvas.ReceivesEvents) + if (SkipEvents) { UpdateNavigation(deltaTime, _canvas.NavigateUp.Name, NavDirection.Up, ref _navigationHeldTimeUp, ref _navigationRateTimeUp); UpdateNavigation(deltaTime, _canvas.NavigateDown.Name, NavDirection.Down, ref _navigationHeldTimeDown, ref _navigationRateTimeDown); @@ -267,7 +269,7 @@ namespace FlaxEngine.GUI /// public override bool OnCharInput(char c) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnCharInput(c); @@ -276,7 +278,7 @@ namespace FlaxEngine.GUI /// public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return DragDropEffect.None; return base.OnDragDrop(ref location, data); @@ -285,7 +287,7 @@ namespace FlaxEngine.GUI /// public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return DragDropEffect.None; return base.OnDragEnter(ref location, data); @@ -294,7 +296,7 @@ namespace FlaxEngine.GUI /// public override void OnDragLeave() { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnDragLeave(); @@ -303,7 +305,7 @@ namespace FlaxEngine.GUI /// public override DragDropEffect OnDragMove(ref Float2 location, DragData data) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return DragDropEffect.None; return base.OnDragMove(ref location, data); @@ -312,7 +314,7 @@ namespace FlaxEngine.GUI /// public override bool OnKeyDown(KeyboardKeys key) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnKeyDown(key); @@ -321,7 +323,7 @@ namespace FlaxEngine.GUI /// public override void OnKeyUp(KeyboardKeys key) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnKeyUp(key); @@ -330,7 +332,7 @@ namespace FlaxEngine.GUI /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnMouseDoubleClick(location, button); @@ -339,7 +341,7 @@ namespace FlaxEngine.GUI /// public override bool OnMouseDown(Float2 location, MouseButton button) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnMouseDown(location, button); @@ -348,7 +350,7 @@ namespace FlaxEngine.GUI /// public override void OnMouseEnter(Float2 location) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; _mousePosition = location; @@ -359,8 +361,7 @@ namespace FlaxEngine.GUI public override void OnMouseLeave() { _mousePosition = Float2.Zero; - - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnMouseLeave(); @@ -369,7 +370,7 @@ namespace FlaxEngine.GUI /// public override void OnMouseMove(Float2 location) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; _mousePosition = location; @@ -379,7 +380,7 @@ namespace FlaxEngine.GUI /// public override bool OnMouseUp(Float2 location, MouseButton button) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnMouseUp(location, button); @@ -388,7 +389,7 @@ namespace FlaxEngine.GUI /// public override bool OnMouseWheel(Float2 location, float delta) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnMouseWheel(location, delta); @@ -397,7 +398,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchEnter(Float2 location, int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnTouchEnter(location, pointerId); @@ -406,7 +407,7 @@ namespace FlaxEngine.GUI /// public override bool OnTouchDown(Float2 location, int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnTouchDown(location, pointerId); @@ -415,7 +416,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchMove(Float2 location, int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnTouchMove(location, pointerId); @@ -424,7 +425,7 @@ namespace FlaxEngine.GUI /// public override bool OnTouchUp(Float2 location, int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnTouchUp(location, pointerId); @@ -433,7 +434,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchLeave(int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnTouchLeave(pointerId); diff --git a/Source/Engine/UI/UICanvas.cs b/Source/Engine/UI/UICanvas.cs index edbec7f7d..bc8c360d1 100644 --- a/Source/Engine/UI/UICanvas.cs +++ b/Source/Engine/UI/UICanvas.cs @@ -66,6 +66,8 @@ namespace FlaxEngine /// public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, GPUTexture output) { + if (!Canvas.IsVisible(renderContext.View.RenderLayersMask)) + return; var bounds = Canvas.Bounds; bounds.Transformation.Translation -= renderContext.View.Origin; if (renderContext.View.Frustum.Contains(bounds.GetBoundingBox()) == ContainmentType.Disjoint) @@ -873,6 +875,20 @@ namespace FlaxEngine } } + internal bool IsVisible() + { + return IsVisible(MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default); + } + + internal bool IsVisible(LayersMask layersMask) + { +#if FLAX_EDITOR + if (_editorTask != null || _editorRoot != null) + return true; +#endif + return layersMask.HasLayer(Layer); + } + #if FLAX_EDITOR private SceneRenderTask _editorTask; private ContainerControl _editorRoot; From 785d3e8648790d22081364d9360aef6c581a82bd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 8 Nov 2023 11:40:57 +0100 Subject: [PATCH 241/546] Fix Output Log window to scroll log on startup properly --- Source/Editor/Windows/OutputLogWindow.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 3665b7073..6526d7c8a 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -467,6 +467,7 @@ namespace FlaxEditor.Windows if (_isDirty) { _isDirty = false; + var wasEmpty = _output.TextLength == 0; // Cache fonts _output.DefaultStyle.Font.GetFont(); @@ -589,7 +590,7 @@ namespace FlaxEditor.Windows // Update the output var cachedScrollValue = _vScroll.Value; var cachedSelection = _output.SelectionRange; - var isBottomScroll = _vScroll.Value >= _vScroll.Maximum - 20.0f; + var isBottomScroll = _vScroll.Value >= _vScroll.Maximum - 20.0f || wasEmpty; _output.Text = _textBuffer.ToString(); _textBufferCount = _entries.Count; if (!_vScroll.IsThumbClicked) From 966fb0275ba17b199c90f03df8edabeed229f35b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 8 Nov 2023 11:52:31 +0100 Subject: [PATCH 242/546] Add `SortScore` to Visject node archetypes and use it to favor method overrides in Visual Script --- Source/Editor/Surface/Archetypes/Function.cs | 1 + Source/Editor/Surface/ContextMenu/VisjectCMItem.cs | 1 + Source/Editor/Surface/NodeArchetype.cs | 10 +++++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index b7df90728..53950dad2 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -2470,6 +2470,7 @@ namespace FlaxEditor.Surface.Archetypes Title = string.Empty, Description = "Overrides the base class method with custom implementation", Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste, + SortScore = 10, IsInputCompatible = MethodOverrideNode.IsInputCompatible, IsOutputCompatible = MethodOverrideNode.IsOutputCompatible, Size = new Float2(240, 60), diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs index 37da7d74d..207875a92 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs @@ -78,6 +78,7 @@ namespace FlaxEditor.Surface.ContextMenu if (!Visible) return; + SortScore += _archetype.SortScore; if (selectedBox != null && CanConnectTo(selectedBox)) SortScore += 1; if (Data != null) diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index 0284fc76d..3a77cef84 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -149,6 +149,11 @@ namespace FlaxEditor.Surface /// public object Tag; + /// + /// Custom score value to use when sorting node archetypes in Editor. If positive (eg. 1, 2) can be used to add more importance for a specific node type. + /// + public float SortScore; + /// /// Default node values. This array supports types: bool, int, float, Vector2, Vector3, Vector4, Color, Rectangle, Guid, string, Matrix and byte[]. /// @@ -204,14 +209,17 @@ namespace FlaxEditor.Surface Size = Size, Flags = Flags, Title = Title, - Description = Title, + SubTitle = SubTitle, + Description = Description, AlternativeTitles = (string[])AlternativeTitles?.Clone(), Tag = Tag, + SortScore = SortScore, DefaultValues = (object[])DefaultValues?.Clone(), DefaultType = DefaultType, ConnectionsHints = ConnectionsHints, IndependentBoxes = (int[])IndependentBoxes?.Clone(), DependentBoxes = (int[])DependentBoxes?.Clone(), + DependentBoxFilter = DependentBoxFilter, Elements = (NodeElementArchetype[])Elements?.Clone(), TryParseText = TryParseText, }; From 1a7770fba2bad83f9c4285f1a0fa78aa8af37436 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 8 Nov 2023 14:33:05 +0100 Subject: [PATCH 243/546] Fix deadlock regression when saving Visual Script in Editor with active instance objects #1890 --- Source/Engine/Content/Assets/VisualScript.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp index 105d4ad2d..9748ba60c 100644 --- a/Source/Engine/Content/Assets/VisualScript.cpp +++ b/Source/Engine/Content/Assets/VisualScript.cpp @@ -1428,6 +1428,10 @@ Asset::LoadResult VisualScript::load() #if USE_EDITOR if (_instances.HasItems()) { + // Mark as already loaded so any WaitForLoaded checks during GetDefaultInstance bellow will handle this Visual Script as ready to use + _loadFailed = false; + _isLoaded = true; + // Setup scripting type CacheScriptingType(); @@ -1512,7 +1516,7 @@ void VisualScript::unload(bool isReloading) // Note: preserve the registered scripting type but invalidate the locally cached handle if (_scriptingTypeHandle) { - VisualScriptingModule.Locker.Lock(); + VisualScriptingBinaryModule::Locker.Lock(); auto& type = VisualScriptingModule.Types[_scriptingTypeHandle.TypeIndex]; if (type.Script.DefaultInstance) { @@ -1523,7 +1527,7 @@ void VisualScript::unload(bool isReloading) VisualScriptingModule.Scripts[_scriptingTypeHandle.TypeIndex] = nullptr; _scriptingTypeHandleCached = _scriptingTypeHandle; _scriptingTypeHandle = ScriptingTypeHandle(); - VisualScriptingModule.Locker.Unlock(); + VisualScriptingBinaryModule::Locker.Unlock(); } } @@ -1534,8 +1538,8 @@ AssetChunksFlag VisualScript::getChunksToPreload() const void VisualScript::CacheScriptingType() { + ScopeLock lock(VisualScriptingBinaryModule::Locker); auto& binaryModule = VisualScriptingModule; - ScopeLock lock(binaryModule.Locker); // Find base type const StringAnsi baseTypename(Meta.BaseTypename); From 11b60390b6caddf9d26da581473a2448b8465936 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 8 Nov 2023 14:41:36 +0100 Subject: [PATCH 244/546] Add `GetRotationFromTo` and `FindBetween` utilities to C# `Quaternion` API #1885 --- Source/Engine/Core/Math/Quaternion.cpp | 3 +- Source/Engine/Core/Math/Quaternion.cs | 109 +++++++++++++++++++++++++ Source/Shaders/Math.hlsl | 8 +- 3 files changed, 113 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Core/Math/Quaternion.cpp b/Source/Engine/Core/Math/Quaternion.cpp index bdbe3037d..1ca05a9c3 100644 --- a/Source/Engine/Core/Math/Quaternion.cpp +++ b/Source/Engine/Core/Math/Quaternion.cpp @@ -382,9 +382,8 @@ void Quaternion::GetRotationFromTo(const Float3& from, const Float3& to, Quatern v0.Normalize(); v1.Normalize(); - const float d = Float3::Dot(v0, v1); - // If dot == 1, vectors are the same + const float d = Float3::Dot(v0, v1); if (d >= 1.0f) { result = Identity; diff --git a/Source/Engine/Core/Math/Quaternion.cs b/Source/Engine/Core/Math/Quaternion.cs index ff50a98bf..34ee160f0 100644 --- a/Source/Engine/Core/Math/Quaternion.cs +++ b/Source/Engine/Core/Math/Quaternion.cs @@ -1077,6 +1077,115 @@ namespace FlaxEngine } } + /// + /// Gets the shortest arc quaternion to rotate this vector to the destination vector. + /// + /// The source vector. + /// The destination vector. + /// The result. + /// The fallback axis. + public static void GetRotationFromTo(ref Float3 from, ref Float3 to, out Quaternion result, ref Float3 fallbackAxis) + { + // Based on Stan Melax's article in Game Programming Gems + + Float3 v0 = from; + Float3 v1 = to; + v0.Normalize(); + v1.Normalize(); + + // If dot == 1, vectors are the same + float d = Float3.Dot(ref v0, ref v1); + if (d >= 1.0f) + { + result = Identity; + return; + } + + if (d < 1e-6f - 1.0f) + { + if (fallbackAxis != Float3.Zero) + { + // Rotate 180 degrees about the fallback axis + RotationAxis(ref fallbackAxis, Mathf.Pi, out result); + } + else + { + // Generate an axis + Float3 axis = Float3.Cross(Float3.UnitX, from); + if (axis.LengthSquared < Mathf.Epsilon) // Pick another if colinear + axis = Float3.Cross(Float3.UnitY, from); + axis.Normalize(); + RotationAxis(ref axis, Mathf.Pi, out result); + } + } + else + { + float s = Mathf.Sqrt((1 + d) * 2); + float invS = 1 / s; + Float3.Cross(ref v0, ref v1, out var c); + result.X = c.X * invS; + result.Y = c.Y * invS; + result.Z = c.Z * invS; + result.W = s * 0.5f; + result.Normalize(); + } + } + + /// + /// Gets the shortest arc quaternion to rotate this vector to the destination vector. + /// + /// The source vector. + /// The destination vector. + /// The fallback axis. + /// The rotation. + public static Quaternion GetRotationFromTo(Float3 from, Float3 to, Float3 fallbackAxis) + { + GetRotationFromTo(ref from, ref to, out var result, ref fallbackAxis); + return result; + } + + /// + /// Gets the quaternion that will rotate vector from into vector to, around their plan perpendicular axis.The input vectors don't need to be normalized. + /// + /// The source vector. + /// The destination vector. + /// The result. + public static void FindBetween(ref Float3 from, ref Float3 to, out Quaternion result) + { + // http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final + float normFromNormTo = Mathf.Sqrt(from.LengthSquared * to.LengthSquared); + if (normFromNormTo < Mathf.Epsilon) + { + result = Identity; + return; + } + float w = normFromNormTo + Float3.Dot(from, to); + if (w < 1.0-6f * normFromNormTo) + { + result = Mathf.Abs(from.X) > Mathf.Abs(from.Z) + ? new Quaternion(-from.Y, from.X, 0.0f, 0.0f) + : new Quaternion(0.0f, -from.Z, from.Y, 0.0f); + } + else + { + Float3 cross = Float3.Cross(from, to); + result = new Quaternion(cross.X, cross.Y, cross.Z, w); + } + result.Normalize(); + } + + /// + /// Gets the quaternion that will rotate vector from into vector to, around their plan perpendicular axis.The input vectors don't need to be normalized. + /// + /// The source vector. + /// The destination vector. + /// The rotation. + public static Quaternion FindBetween(Float3 from, Float3 to) + { + FindBetween(ref from, ref to, out var result); + return result; + } + /// /// Creates a left-handed spherical billboard that rotates around a specified object position. /// diff --git a/Source/Shaders/Math.hlsl b/Source/Shaders/Math.hlsl index ad3168016..67ddc1887 100644 --- a/Source/Shaders/Math.hlsl +++ b/Source/Shaders/Math.hlsl @@ -218,10 +218,10 @@ float4 ClampedPow(float4 x, float4 y) float4 FindQuatBetween(float3 from, float3 to) { + // http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final float normAB = 1.0f; float w = normAB + dot(from, to); float4 result; - if (w >= 1e-6f * normAB) { result = float4 @@ -234,12 +234,10 @@ float4 FindQuatBetween(float3 from, float3 to) } else { - w = 0.f; result = abs(from.x) > abs(from.y) - ? float4(-from.z, 0.f, from.x, w) - : float4(0.f, -from.z, from.y, w); + ? float4(-from.z, 0.f, from.x, 0.0f) + : float4(0.f, -from.z, from.y, 0.0f); } - return normalize(result); } From bcaa42dda2bb314355e58de444bd8b51e1494ea7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 8 Nov 2023 15:07:20 +0100 Subject: [PATCH 245/546] Fix crash on negative collection capacity due to `int32` maximum value limit #1886 --- Source/Engine/Core/Collections/BitArray.h | 6 +++--- Source/Engine/Core/Memory/Allocation.h | 17 ++++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h index 15e69087b..eeadc82e9 100644 --- a/Source/Engine/Core/Collections/BitArray.h +++ b/Source/Engine/Core/Collections/BitArray.h @@ -63,7 +63,7 @@ public: _count = _capacity = other.Count(); if (_capacity > 0) { - const uint64 itemsCapacity = ToItemCapacity(_capacity); + const int32 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -79,7 +79,7 @@ public: _count = _capacity = other.Count(); if (_capacity > 0) { - const uint64 itemsCapacity = ToItemCapacity(_capacity); + const int32 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -111,7 +111,7 @@ public: { _allocation.Free(); _capacity = other._count; - const uint64 itemsCapacity = ToItemCapacity(_capacity); + const int32 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } diff --git a/Source/Engine/Core/Memory/Allocation.h b/Source/Engine/Core/Memory/Allocation.h index 89d2f2003..8d7188d91 100644 --- a/Source/Engine/Core/Memory/Allocation.h +++ b/Source/Engine/Core/Memory/Allocation.h @@ -43,14 +43,14 @@ public: return Capacity; } - FORCE_INLINE void Allocate(uint64 capacity) + FORCE_INLINE void Allocate(int32 capacity) { #if ENABLE_ASSERTION_LOW_LAYERS ASSERT(capacity <= Capacity); #endif } - FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount) + FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount) { #if ENABLE_ASSERTION_LOW_LAYERS ASSERT(capacity <= Capacity); @@ -120,12 +120,15 @@ public: capacity |= capacity >> 4; capacity |= capacity >> 8; capacity |= capacity >> 16; - capacity = (capacity + 1) * 2; + uint64 capacity64 = (uint64)(capacity + 1) * 2; + if (capacity64 > MAX_int32) + capacity64 = MAX_int32; + capacity = (int32)capacity64; } return capacity; } - FORCE_INLINE void Allocate(uint64 capacity) + FORCE_INLINE void Allocate(int32 capacity) { #if ENABLE_ASSERTION_LOW_LAYERS ASSERT(!_data); @@ -137,7 +140,7 @@ public: #endif } - FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount) + FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount) { T* newData = capacity != 0 ? (T*)Allocator::Allocate(capacity * sizeof(T)) : nullptr; #if !BUILD_RELEASE @@ -210,7 +213,7 @@ public: return minCapacity <= Capacity ? Capacity : _other.CalculateCapacityGrow(capacity, minCapacity); } - FORCE_INLINE void Allocate(uint64 capacity) + FORCE_INLINE void Allocate(int32 capacity) { if (capacity > Capacity) { @@ -219,7 +222,7 @@ public: } } - FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount) + FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount) { // Check if the new allocation will fit into inlined storage if (capacity <= Capacity) From 6dcadb5131805fdcf6a8149070539e0ddb9e1a72 Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:34:17 +0100 Subject: [PATCH 246/546] Compacted UpdateTransform Matrix math --- Source/Engine/UI/GUI/Control.Bounds.cs | 53 +++++++++++++++++++------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/Source/Engine/UI/GUI/Control.Bounds.cs b/Source/Engine/UI/GUI/Control.Bounds.cs index c6260b648..272b1c5f1 100644 --- a/Source/Engine/UI/GUI/Control.Bounds.cs +++ b/Source/Engine/UI/GUI/Control.Bounds.cs @@ -487,9 +487,12 @@ namespace FlaxEngine.GUI public void UpdateTransform() { // Actual pivot and negative pivot - Float2.Multiply(ref _pivot, ref _bounds.Size, out var v1); - Float2.Negate(ref v1, out var v2); - Float2.Add(ref v1, ref _bounds.Location, out v1); + //Float2.Multiply(ref _pivot, ref _bounds.Size, out var v1); + //Float2.Negate(ref v1, out var v2); + //Float2.Add(ref v1, ref _bounds.Location, out v1); + var v1 = _pivot * _bounds.Size; + var v2 = -v1; + v1 += _bounds.Location; // ------ Matrix3x3 based version: @@ -518,18 +521,42 @@ namespace FlaxEngine.GUI // ------ Matrix2x2 based version: // 2D transformation - Matrix2x2.Scale(ref _scale, out Matrix2x2 m1); - Matrix2x2.Shear(ref _shear, out Matrix2x2 m2); - Matrix2x2.Multiply(ref m1, ref m2, out m1); - Matrix2x2.Rotation(Mathf.DegreesToRadians * _rotation, out m2); - Matrix2x2.Multiply(ref m1, ref m2, out m1); + + //Matrix2x2.Scale(ref _scale, out Matrix2x2 m1); + //Matrix2x2.Shear(ref _shear, out Matrix2x2 m2); + //Matrix2x2.Multiply(ref m1, ref m2, out m1); + + // Scale and Shear + Matrix3x3 m1 = new Matrix3x3 + ( + _scale.X, + _scale.X * (_shear.X == 0 ? 0 : (1.0f / Mathf.Tan(Mathf.DegreesToRadians * (90 - Mathf.Clamp(_shear.X, -89.0f, 89.0f))))), + 0, + _scale.Y * (_shear.Y == 0 ? 0 : (1.0f / Mathf.Tan(Mathf.DegreesToRadians * (90 - Mathf.Clamp(_shear.Y, -89.0f, 89.0f))))), + _scale.Y, + 0, 0, 0, 1 + ); + + + //Matrix2x2.Rotation(Mathf.DegreesToRadians * _rotation, out m2); + float sin = Mathf.Sin(Mathf.DegreesToRadians * _rotation); + float cos = Mathf.Cos(Mathf.DegreesToRadians * _rotation); + + //Matrix2x2.Multiply(ref m1, ref m2, out m1); + m1.M11 = (_scale.X * cos) + (m1.M12 * -sin); + m1.M12 = (_scale.X * sin) + (m1.M12 * cos); + m1.M21 = (m1.M21 * cos) + (_scale.Y * -sin); + m1.M22 = (m1.M21 * sin) + (_scale.Y * cos); // Mix all the stuff - Matrix3x3.Translation2D(ref v2, out Matrix3x3 m3); - Matrix3x3 m4 = (Matrix3x3)m1; - Matrix3x3.Multiply(ref m3, ref m4, out m3); - Matrix3x3.Translation2D(ref v1, out m4); - Matrix3x3.Multiply(ref m3, ref m4, out _cachedTransform); + //Matrix3x3.Translation2D(ref v2, out Matrix3x3 m3); + //Matrix3x3 m4 = (Matrix3x3)m1; + //Matrix3x3.Multiply(ref m3, ref m4, out m3); + //Matrix3x3.Translation2D(ref v1, out Matrix3x3 m4); + //Matrix3x3.Multiply(ref m3, ref m4, out _cachedTransform); + m1.M31 = (v2.X * m1.M11) + (v2.Y * m1.M21) + v1.X; + m1.M32 = (v2.X * m1.M12) + (v2.Y * m1.M22) + v1.Y; + _cachedTransform = m1; // Cache inverted transform Matrix3x3.Invert(ref _cachedTransform, out _cachedTransformInv); From d90b723487f4d593c317ad03664a498e1635de57 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 8 Nov 2023 21:30:31 +0100 Subject: [PATCH 247/546] Code cleanup and usability tweaks for #1600 --- Source/Editor/Surface/Archetypes/Material.cs | 22 +++++++-------- .../MaterialGenerator.Material.cpp | 27 +++++++++---------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 8aba82d86..f65804023 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -886,20 +886,19 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 40, Title = "Rectangle Mask", - Description = "Creates a Rectangle mask", + Description = "Creates a rectangle mask", Flags = NodeFlags.MaterialGraph, Size = new Float2(150, 40), ConnectionsHints = ConnectionsHint.Vector, DefaultValues = new object[] { - new Float2(0, 0), new Float2(0.5f, 0.5f), }, Elements = new[] { NodeElementArchetype.Factory.Input(0, "UV", true, typeof(Float2), 0), - NodeElementArchetype.Factory.Input(1, "Rectangle", true, typeof(Float2), 1), - NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), + NodeElementArchetype.Factory.Input(1, "Rectangle", true, typeof(Float2), 1, 0), + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 2), } }, new NodeArchetype @@ -914,28 +913,27 @@ namespace FlaxEditor.Surface.Archetypes DependentBoxes = new[] { 1 }, Elements = new[] { - NodeElementArchetype.Factory.Input(0, "value", true, null, 0), + NodeElementArchetype.Factory.Input(0, "Value", true, null, 0), NodeElementArchetype.Factory.Output(0, string.Empty, null, 1), } }, new NodeArchetype { TypeID = 42, - Title = "AAStep", - Description = "Smooth version of step", + Title = "AA Step", + Description = "Smooth version of step function with less aliasing", Flags = NodeFlags.MaterialGraph, Size = new Float2(150, 40), ConnectionsHints = ConnectionsHint.Vector, DefaultValues = new object[] { - 1, - 0 + 0.5f }, Elements = new[] { - NodeElementArchetype.Factory.Input(0, "value", true, typeof(float), 0), - NodeElementArchetype.Factory.Input(1, "gradient", true, typeof(float), 1), - NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 5), + NodeElementArchetype.Factory.Input(0, "Value", true, typeof(float), 0), + NodeElementArchetype.Factory.Input(1, "Gradient", true, typeof(float), 1, 0), + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 2), } }, }; diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index 56de05137..f669d36d2 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -519,39 +519,38 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) } break; } - // Ratangle Mask + // Rectangle Mask case 40: { const auto uv = tryGetValue(node->GetBox(0), getUVs).AsFloat2(); - const auto rectangle = tryGetValue(node->GetBox(1), node->Values[1]).AsFloat2(); - - auto d = writeLocal(ValueType::Float2, String::Format(TEXT("abs({0} * 2 - 1) - {1}"),uv.Value, rectangle.Value), node); - auto d2 = writeLocal(ValueType::Float2 , String::Format(TEXT("1 - {0} / fwidth({0})"), d.Value), node); - value = writeLocal(ValueType::Float , String::Format(TEXT("saturate(min({0}.x, {0}.y))"), d2.Value), node); + const auto rectangle = tryGetValue(node->GetBox(1), node->Values[0]).AsFloat2(); + auto d = writeLocal(ValueType::Float2, String::Format(TEXT("abs({0} * 2 - 1) - {1}"), uv.Value, rectangle.Value), node); + auto d2 = writeLocal(ValueType::Float2, String::Format(TEXT("1 - {0} / fwidth({0})"), d.Value), node); + value = writeLocal(ValueType::Float, String::Format(TEXT("saturate(min({0}.x, {0}.y))"), d2.Value), node); break; } // FWidth case 41: { const auto inValue = tryGetValue(node->GetBox(0), 0, Value::Zero); - value = writeLocal(inValue.Type, String::Format(TEXT("fwidth({0})"), inValue.Value), node); + value = writeLocal(inValue.Type, String::Format(TEXT("fwidth({0})"), inValue.Value), node); break; } - //AAStep (smooth version of step) + // AA Step case 42: { - //source https://www.ronja-tutorials.com/post/046-fwidth/#a-better-step + // Reference: https://www.ronja-tutorials.com/post/046-fwidth/#a-better-step const auto compValue = tryGetValue(node->GetBox(0), getUVs).AsFloat(); - const auto gradient = tryGetValue(node->GetBox(1), node->Values[1]).AsFloat(); + const auto gradient = tryGetValue(node->GetBox(1), node->Values[0]).AsFloat(); auto change = writeLocal(ValueType::Float, String::Format(TEXT("fwidth({0})"), gradient.Value), node); - //base the range of the inverse lerp on the change over two pixels - auto lowerEdge = writeLocal(ValueType::Float, String::Format(TEXT("{0} - {1})"), compValue.Value, change.Value), node); - auto upperEdge = writeLocal(ValueType::Float, String::Format(TEXT("{0} + {1})"), compValue.Value, change.Value), node); + // Base the range of the inverse lerp on the change over two pixels + auto lowerEdge = writeLocal(ValueType::Float, String::Format(TEXT("{0} - {1}"), compValue.Value, change.Value), node); + auto upperEdge = writeLocal(ValueType::Float, String::Format(TEXT("{0} + {1}"), compValue.Value, change.Value), node); - //do the inverse interpolation and saturate it + // Do the inverse interpolation and saturate it value = writeLocal(ValueType::Float, String::Format(TEXT("saturate((({0} - {1}) / ({2} - {1})))"), gradient.Value, lowerEdge.Value, upperEdge.Value), node); } default: From d8a54692f044367a5e340eea344c57c66c1c9e51 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 9 Nov 2023 09:10:25 +0100 Subject: [PATCH 248/546] Code cleanup #1785 --- .../Editor/Viewport/Previews/MaterialPreview.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Source/Editor/Viewport/Previews/MaterialPreview.cs b/Source/Editor/Viewport/Previews/MaterialPreview.cs index f05c8413b..46aac1cdf 100644 --- a/Source/Editor/Viewport/Previews/MaterialPreview.cs +++ b/Source/Editor/Viewport/Previews/MaterialPreview.cs @@ -7,7 +7,6 @@ using FlaxEngine.GUI; using FlaxEditor.Viewport.Widgets; using FlaxEditor.GUI.ContextMenu; using Object = FlaxEngine.Object; -using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI; using FlaxEditor.Scripting; @@ -80,17 +79,12 @@ namespace FlaxEditor.Viewport.Previews set { if (value == -1) // Using Custom Model - { return; - } - if (value < 0 || value > Models.Length) throw new ArgumentOutOfRangeException(); if (_customModelPicker != null) - { - _customModelPicker.SelectedAsset = null; - } + _customModelPicker.Validator.SelectedAsset = null; _selectedModelIndex = value; _previewModel.Model = FlaxEngine.Content.LoadAsyncInternal("Editor/Primitives/" + Models[value]); _previewModel.Transform = Transforms[value]; @@ -116,22 +110,21 @@ namespace FlaxEditor.Viewport.Previews _modelWidgetButtonMenu.AddSeparator(); _customModelPicker = new AssetPicker(new ScriptType(typeof(Model)), Float2.Zero); - // Label Button + // Label button var customModelPickerLabel = _modelWidgetButtonMenu.AddButton("Custom Model:"); customModelPickerLabel.CloseMenuOnClick = false; customModelPickerLabel.Checked = _customModel != null; // Container button var customModelPickerButton = _modelWidgetButtonMenu.AddButton(""); - customModelPickerButton.Height = _customModelPicker.Height + 4; customModelPickerButton.CloseMenuOnClick = false; _customModelPicker.Parent = customModelPickerButton; - _customModelPicker.SelectedAsset = _customModel; + _customModelPicker.Validator.SelectedAsset = _customModel; _customModelPicker.SelectedItemChanged += () => { - _customModel = _customModelPicker.SelectedAsset as Model; - if (_customModelPicker.SelectedAsset == null) + _customModel = _customModelPicker.Validator.SelectedAsset as Model; + if (_customModelPicker.Validator.SelectedAsset == null) { SelectedModelIndex = 0; ResetModelContextMenu(); From 710b9275fd5a05f7f22b6a2142a1bf28cd65d818 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 9 Nov 2023 09:13:40 +0100 Subject: [PATCH 249/546] Fix minor typos https://github.com/FlaxEngine/FlaxDocs/pull/123 --- Source/Engine/Graphics/Materials/MaterialInfo.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Graphics/Materials/MaterialInfo.h b/Source/Engine/Graphics/Materials/MaterialInfo.h index 86fab4548..8e4933150 100644 --- a/Source/Engine/Graphics/Materials/MaterialInfo.h +++ b/Source/Engine/Graphics/Materials/MaterialInfo.h @@ -86,7 +86,7 @@ API_ENUM() enum class MaterialBlendMode : byte API_ENUM() enum class MaterialShadingModel : byte { /// - /// The unlit material. Emissive channel is used as an output color. Can perform custom lighting operations or just glow. Won't be affected by the lighting pipeline. + /// The unlit material. The emissive channel is used as an output color. Can perform custom lighting operations or just glow. Won't be affected by the lighting pipeline. /// Unlit = 0, @@ -96,7 +96,7 @@ API_ENUM() enum class MaterialShadingModel : byte Lit = 1, /// - /// The subsurface material. Intended for materials like vax or skin that need light scattering to transport simulation through the object. + /// The subsurface material. Intended for materials like wax or skin that need light scattering to transport simulation through the object. /// Subsurface = 2, @@ -366,12 +366,12 @@ API_ENUM() enum class MaterialDecalBlendingMode : byte API_ENUM() enum class MaterialTransparentLightingMode : byte { /// - /// Default directional lighting evaluated per-pixel at the material surface. Use it for semi-transparent surfaces - with both diffuse and specular lighting component active. + /// Default directional lighting evaluated per-pixel at the material surface. Use it for semi-transparent surfaces - with both diffuse and specular lighting components active. /// Surface = 0, /// - /// Non-directional lighting evaluated per-pixel at material surface. Use it for volumetric objects such as smoke, rain or dust - only diffuse lighting term is active (no specular highlights). + /// Non-directional lighting evaluated per-pixel at material surface. Use it for volumetric objects such as smoke, rain or dust - only the diffuse lighting term is active (no specular highlights). /// SurfaceNonDirectional = 1, }; From 22c8ec53426543c1d8192c2bf5744c0bb37b384c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 9 Nov 2023 11:50:48 +0100 Subject: [PATCH 250/546] Fix crash when rigidbody gets deleted during physical collision #1893 --- .../Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp index 5968bdd78..5781e4641 100644 --- a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp +++ b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp @@ -137,7 +137,11 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c c.ThisActor = static_cast(pair.shapes[0]->userData); c.OtherActor = static_cast(pair.shapes[1]->userData); - ASSERT_LOW_LAYER(c.ThisActor && c.OtherActor); + if (c.ThisActor == nullptr || c.OtherActor == nullptr) + { + // One of the actors was deleted (eg. via RigidBody destroyed by gameplay) then skip processing this collision + continue; + } // Extract contact points while (i.hasNextPatch()) From 4ae57e7769d03899182a1a81b9c264934b8189a5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 9 Nov 2023 14:52:28 +0100 Subject: [PATCH 251/546] Fix issue with asset loading to be properly canceled when reimporting file #1894 --- Source/Engine/Audio/AudioClip.cpp | 1 + Source/Engine/Content/Asset.cpp | 8 ++++++++ Source/Engine/Content/Assets/Model.cpp | 1 + Source/Engine/Content/Assets/SkinnedModel.cpp | 1 + .../Engine/Content/Loading/Tasks/LoadAssetDataTask.h | 4 ++-- Source/Engine/Content/Storage/FlaxStorage.cpp | 10 +++++++--- Source/Engine/Graphics/Textures/TextureBase.cpp | 1 + 7 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Audio/AudioClip.cpp b/Source/Engine/Audio/AudioClip.cpp index d04da7274..689f38d12 100644 --- a/Source/Engine/Audio/AudioClip.cpp +++ b/Source/Engine/Audio/AudioClip.cpp @@ -225,6 +225,7 @@ bool AudioClip::ExtractDataRaw(Array& resultData, AudioDataInfo& resultDat void AudioClip::CancelStreaming() { + Asset::CancelStreaming(); CancelStreamingTasks(); } diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index afe38f0a7..93d904d5e 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -522,6 +522,14 @@ void Asset::InitAsVirtual() void Asset::CancelStreaming() { + // Cancel loading task but go over asset locker to prevent case if other load threads still loads asset while it's reimported on other thread + Locker.Lock(); + ContentLoadTask* loadTask = _loadingTask; + Locker.Unlock(); + if (loadTask) + { + loadTask->Cancel(); + } } #if USE_EDITOR diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp index 5a008645d..691b00a50 100644 --- a/Source/Engine/Content/Assets/Model.cpp +++ b/Source/Engine/Content/Assets/Model.cpp @@ -783,6 +783,7 @@ void Model::InitAsVirtual() void Model::CancelStreaming() { + Asset::CancelStreaming(); CancelStreamingTasks(); } diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index 5871087d9..b823db5a3 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -969,6 +969,7 @@ void SkinnedModel::InitAsVirtual() void SkinnedModel::CancelStreaming() { + Asset::CancelStreaming(); CancelStreamingTasks(); } diff --git a/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h b/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h index 23cf5f787..4a7bbb2bb 100644 --- a/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h +++ b/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h @@ -48,6 +48,8 @@ protected: // [ContentLoadTask] Result run() override { + if (IsCancelRequested()) + return Result::Ok; PROFILE_CPU(); AssetReference ref = _asset.Get(); @@ -67,8 +69,6 @@ protected: { if (IsCancelRequested()) return Result::Ok; - - // Load it #if TRACY_ENABLE ZoneScoped; ZoneName(*name, name.Length()); diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index d530e5456..a47e0bd0e 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -1302,15 +1302,15 @@ void FlaxStorage::CloseFileHandles() // In those situations all the async tasks using this storage should be cancelled externally // Ensure that no one is using this resource - int32 waitTime = 10; + int32 waitTime = 100; while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0) - Platform::Sleep(10); + Platform::Sleep(1); if (Platform::AtomicRead(&_chunksLock) != 0) { // File can be locked by some streaming tasks (eg. AudioClip::StreamingTask or StreamModelLODTask) + Entry e; for (int32 i = 0; i < GetEntriesCount(); i++) { - Entry e; GetEntry(i, e); Asset* asset = Content::GetAsset(e.ID); if (asset) @@ -1320,8 +1320,12 @@ void FlaxStorage::CloseFileHandles() } } } + waitTime = 100; + while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0) + Platform::Sleep(1); ASSERT(_chunksLock == 0); + // Close file handles (from all threads) _file.DeleteAll(); } diff --git a/Source/Engine/Graphics/Textures/TextureBase.cpp b/Source/Engine/Graphics/Textures/TextureBase.cpp index 538b15a4a..181955fce 100644 --- a/Source/Engine/Graphics/Textures/TextureBase.cpp +++ b/Source/Engine/Graphics/Textures/TextureBase.cpp @@ -660,6 +660,7 @@ uint64 TextureBase::GetMemoryUsage() const void TextureBase::CancelStreaming() { + Asset::CancelStreaming(); _texture.CancelStreamingTasks(); } From ea3f02f810b04c60becc22b8d6acb7ef3260a690 Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:34:07 +0100 Subject: [PATCH 252/546] Fix rotacion sheers the UI element --- Source/Engine/UI/GUI/Control.Bounds.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Engine/UI/GUI/Control.Bounds.cs b/Source/Engine/UI/GUI/Control.Bounds.cs index 272b1c5f1..c9d20bfae 100644 --- a/Source/Engine/UI/GUI/Control.Bounds.cs +++ b/Source/Engine/UI/GUI/Control.Bounds.cs @@ -545,14 +545,14 @@ namespace FlaxEngine.GUI //Matrix2x2.Multiply(ref m1, ref m2, out m1); m1.M11 = (_scale.X * cos) + (m1.M12 * -sin); m1.M12 = (_scale.X * sin) + (m1.M12 * cos); - m1.M21 = (m1.M21 * cos) + (_scale.Y * -sin); + float m21 = (m1.M21 * cos) + (_scale.Y * -sin); m1.M22 = (m1.M21 * sin) + (_scale.Y * cos); - + m1.M21 = m21; // Mix all the stuff //Matrix3x3.Translation2D(ref v2, out Matrix3x3 m3); //Matrix3x3 m4 = (Matrix3x3)m1; //Matrix3x3.Multiply(ref m3, ref m4, out m3); - //Matrix3x3.Translation2D(ref v1, out Matrix3x3 m4); + //Matrix3x3.Translation2D(ref v1, out m4); //Matrix3x3.Multiply(ref m3, ref m4, out _cachedTransform); m1.M31 = (v2.X * m1.M11) + (v2.Y * m1.M21) + v1.X; m1.M32 = (v2.X * m1.M12) + (v2.Y * m1.M22) + v1.Y; From 3c71dc99e0aac96348e4a74cb107ae4525fbc89c Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Thu, 9 Nov 2023 14:08:01 -0400 Subject: [PATCH 253/546] Rebuild navigation after apply changes in Navigation asset --- Source/Engine/Navigation/Navigation.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Engine/Navigation/Navigation.cpp b/Source/Engine/Navigation/Navigation.cpp index 8bc94eaaa..09e9b65ba 100644 --- a/Source/Engine/Navigation/Navigation.cpp +++ b/Source/Engine/Navigation/Navigation.cpp @@ -220,6 +220,12 @@ void NavigationSettings::Apply() #endif } } + + // Rebuild all navmeshs after apply changes on navigation + for (auto scene : Level::Scenes) + { + Navigation::BuildNavMesh(scene); + } } void NavigationSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) From 2ae290491e4d56e036bab4c38823a29f7369028d Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Thu, 9 Nov 2023 14:50:56 -0400 Subject: [PATCH 254/546] fix build --- Source/Engine/Navigation/Navigation.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Navigation/Navigation.cpp b/Source/Engine/Navigation/Navigation.cpp index 09e9b65ba..c224389f8 100644 --- a/Source/Engine/Navigation/Navigation.cpp +++ b/Source/Engine/Navigation/Navigation.cpp @@ -221,11 +221,13 @@ void NavigationSettings::Apply() } } +#if USE_EDITOR // Rebuild all navmeshs after apply changes on navigation for (auto scene : Level::Scenes) { Navigation::BuildNavMesh(scene); } +#endif } void NavigationSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) From 39a5b8e6353d43aad2d3c57991dfbd2f7b72e0e1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 10 Nov 2023 01:32:24 +0100 Subject: [PATCH 255/546] Invert check order --- Source/Editor/Managed/ManagedEditor.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Managed/ManagedEditor.cpp b/Source/Editor/Managed/ManagedEditor.cpp index fd9219d35..bed742885 100644 --- a/Source/Editor/Managed/ManagedEditor.cpp +++ b/Source/Editor/Managed/ManagedEditor.cpp @@ -330,14 +330,15 @@ bool ManagedEditor::CanReloadScripts() bool ManagedEditor::CanAutoBuildCSG() { + if (!ManagedEditorOptions.AutoRebuildCSG) + return false; + // Skip calls from non-managed thread (eg. physics worker) if (!MCore::Thread::IsAttached()) return false; if (!HasManagedInstance()) return false; - if (!ManagedEditorOptions.AutoRebuildCSG) - return false; if (Internal_CanAutoBuildCSG == nullptr) { Internal_CanAutoBuildCSG = GetClass()->GetMethod("Internal_CanAutoBuildCSG"); @@ -348,14 +349,15 @@ bool ManagedEditor::CanAutoBuildCSG() bool ManagedEditor::CanAutoBuildNavMesh() { + if (!ManagedEditorOptions.AutoRebuildNavMesh) + return false; + // Skip calls from non-managed thread (eg. physics worker) if (!MCore::Thread::IsAttached()) return false; if (!HasManagedInstance()) return false; - if (!ManagedEditorOptions.AutoRebuildNavMesh) - return false; if (Internal_CanAutoBuildNavMesh == nullptr) { Internal_CanAutoBuildNavMesh = GetClass()->GetMethod("Internal_CanAutoBuildNavMesh"); From 7d70a1503456577b696f0cfe3f2eac1a0da239b3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 10 Nov 2023 10:20:21 +0100 Subject: [PATCH 256/546] Fix color editing control to properly handle mouse event #1782 --- Source/Editor/GUI/Input/ColorValueBox.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Source/Editor/GUI/Input/ColorValueBox.cs b/Source/Editor/GUI/Input/ColorValueBox.cs index 167cc65bb..fdd7d073a 100644 --- a/Source/Editor/GUI/Input/ColorValueBox.cs +++ b/Source/Editor/GUI/Input/ColorValueBox.cs @@ -14,6 +14,8 @@ namespace FlaxEditor.GUI.Input [HideInEditor] public class ColorValueBox : Control { + private bool _isMouseDown; + /// /// Delegate function used for the color picker events handling. /// @@ -134,11 +136,22 @@ namespace FlaxEditor.GUI.Input Render2D.DrawRectangle(r, IsMouseOver || IsNavFocused ? style.BackgroundSelected : Color.Black); } + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + _isMouseDown = true; + return base.OnMouseDown(location, button); + } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { - Focus(); - OnSubmit(); + if (_isMouseDown) + { + _isMouseDown = false; + Focus(); + OnSubmit(); + } return true; } From 9cd8c02911f2d9e8849e505b4b3ce3c043c36e5a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 10 Nov 2023 11:34:52 +0100 Subject: [PATCH 257/546] Fix `NetworkTransform` to properly reject local simulation deltas on incoming authoritative transform data #1907 --- Source/Engine/Networking/Components/NetworkTransform.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Networking/Components/NetworkTransform.cpp b/Source/Engine/Networking/Components/NetworkTransform.cpp index af802649b..6f5943ff1 100644 --- a/Source/Engine/Networking/Components/NetworkTransform.cpp +++ b/Source/Engine/Networking/Components/NetworkTransform.cpp @@ -301,11 +301,8 @@ void NetworkTransform::Deserialize(NetworkStream* stream) _buffer.Clear(); _bufferHasDeltas = true; } - // TODO: items are added in order to do batch removal - for (int32 i = 0; i < _buffer.Count() && _buffer[i].SequenceIndex < sequenceIndex; i++) - { - _buffer.RemoveAtKeepOrder(i); - } + while (_buffer.Count() != 0 && _buffer[0].SequenceIndex < sequenceIndex) + _buffer.RemoveAtKeepOrder(0); // Use received authoritative actor transformation but re-apply all deltas not yet processed by the server due to lag (reconciliation) for (auto& e : _buffer) From ed69f1112118a131b68ebe934ed4d9a6f3ad2e3c Mon Sep 17 00:00:00 2001 From: MineBill Date: Fri, 10 Nov 2023 13:56:19 +0200 Subject: [PATCH 258/546] Don't hardcode appdata path for linux anymore. --- Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp index 7f5ca6f17..de0a30e40 100644 --- a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp @@ -207,9 +207,9 @@ void RiderCodeEditor::FindEditors(Array* output) FileSystem::GetChildDirectories(subDirectories, TEXT("/opt/")); // Versions installed via JetBrains Toolbox - SearchDirectory(&installations, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/rider/")); - FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/Rider/ch-0")); - FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/Rider/ch-1")); // Beta versions + SearchDirectory(&installations, localAppDataPath / TEXT("JetBrains/Toolbox/apps/rider/")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/Rider/ch-0")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/Rider/ch-1")); // Beta versions // Detect Flatpak installations SearchDirectory(&installations, From 36daa38e0fb249c479ccaba767967846fc15e276 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 10 Nov 2023 13:23:32 +0100 Subject: [PATCH 259/546] Fix `CollectionEditor` to properly support editing multiple arrays #1818 --- Source/Editor/CustomEditors/Editors/CollectionEditor.cs | 2 +- .../CustomEditors/Editors/ModelInstanceEntryEditor.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 7d92c71c0..6f623fb23 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -113,7 +113,7 @@ namespace FlaxEditor.CustomEditors.Editors public override void Initialize(LayoutElementsContainer layout) { // No support for different collections for now - if (HasDifferentValues || HasDifferentTypes) + if (HasDifferentTypes) return; var size = Count; diff --git a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs index c215a5ab7..f901b20d9 100644 --- a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs @@ -28,14 +28,16 @@ namespace FlaxEditor.CustomEditors.Editors var group = layout.Group("Entry"); _group = group; - if (ParentEditor == null) + if (ParentEditor == null || HasDifferentTypes) return; var entry = (ModelInstanceEntry)Values[0]; var entryIndex = ParentEditor.ChildrenEditors.IndexOf(this); var materialLabel = new PropertyNameLabel("Material"); materialLabel.TooltipText = "The mesh surface material used for the rendering."; - if (ParentEditor.ParentEditor?.Values[0] is ModelInstanceActor modelInstance) + var parentEditorValues = ParentEditor.ParentEditor?.Values; + if (parentEditorValues?[0] is ModelInstanceActor modelInstance) { + // TODO: store _modelInstance and _material in array for each selected model instance actor _entryIndex = entryIndex; _modelInstance = modelInstance; var slots = modelInstance.MaterialSlots; @@ -56,6 +58,8 @@ namespace FlaxEditor.CustomEditors.Editors // Create material picker var materialValue = new CustomValueContainer(new ScriptType(typeof(MaterialBase)), _material, (instance, index) => _material, (instance, index, value) => _material = value as MaterialBase); + for (var i = 1; i < parentEditorValues.Count; i++) + materialValue.Add(_material); var materialEditor = (AssetRefEditor)_group.Property(materialLabel, materialValue); materialEditor.Values.SetDefaultValue(defaultValue); materialEditor.RefreshDefaultValue(); From 626cde118bfee87d0a675cef27d72601499fe40d Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" <79365912+RuanLucasGD@users.noreply.github.com> Date: Fri, 10 Nov 2023 09:45:46 -0300 Subject: [PATCH 260/546] Add verification to rebuild navigation --- Source/Engine/Navigation/Navigation.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Navigation/Navigation.cpp b/Source/Engine/Navigation/Navigation.cpp index c224389f8..73e11e630 100644 --- a/Source/Engine/Navigation/Navigation.cpp +++ b/Source/Engine/Navigation/Navigation.cpp @@ -9,6 +9,8 @@ #include "Engine/Content/JsonAsset.h" #include "Engine/Threading/Threading.h" #if USE_EDITOR +#include "Editor/Editor.h" +#include "Editor/Managed/ManagedEditor.h" #include "Engine/Level/Level.h" #include "Engine/Level/Scene/Scene.h" #endif @@ -222,10 +224,13 @@ void NavigationSettings::Apply() } #if USE_EDITOR - // Rebuild all navmeshs after apply changes on navigation - for (auto scene : Level::Scenes) + if (!Editor::IsPlayMode && Editor::Managed && Editor::Managed->CanAutoBuildNavMesh()) { - Navigation::BuildNavMesh(scene); + // Rebuild all navmeshs after apply changes on navigation + for (auto scene : Level::Scenes) + { + Navigation::BuildNavMesh(scene); + } } #endif } From 057d1fbcc681ecf0191006624261eb4221e4aaee Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 10 Nov 2023 15:07:37 +0100 Subject: [PATCH 261/546] Fix unpacking `Float3` and other inbuilt `Variant` types via Unpack node in Visual Script #1903 --- Source/Engine/Core/Types/Variant.cpp | 57 ++++++++++++++++++++++++++ Source/Engine/Core/Types/Variant.h | 3 ++ Source/Engine/Visject/VisjectGraph.cpp | 6 ++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index df044b299..952e648e6 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -2821,7 +2821,10 @@ void Variant::Inline() type = VariantType::Types::Vector4; } if (type != VariantType::Null) + { + ASSERT(sizeof(data) >= AsBlob.Length); Platform::MemoryCopy(data, AsBlob.Data, AsBlob.Length); + } } if (type != VariantType::Null) { @@ -2912,6 +2915,60 @@ void Variant::Inline() } } +void Variant::InvertInline() +{ + byte data[sizeof(Matrix)]; + switch (Type.Type) + { + case VariantType::Bool: + case VariantType::Int: + case VariantType::Uint: + case VariantType::Int64: + case VariantType::Uint64: + case VariantType::Float: + case VariantType::Double: + case VariantType::Pointer: + case VariantType::String: + case VariantType::Float2: + case VariantType::Float3: + case VariantType::Float4: + case VariantType::Color: +#if !USE_LARGE_WORLDS + case VariantType::BoundingSphere: + case VariantType::BoundingBox: + case VariantType::Ray: +#endif + case VariantType::Guid: + case VariantType::Quaternion: + case VariantType::Rectangle: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: + case VariantType::Int16: + case VariantType::Uint16: + case VariantType::Double2: + case VariantType::Double3: + case VariantType::Double4: + static_assert(sizeof(data) >= sizeof(AsData), "Invalid memory size."); + Platform::MemoryCopy(data, AsData, sizeof(AsData)); + break; +#if USE_LARGE_WORLDS + case VariantType::BoundingSphere: + case VariantType::BoundingBox: + case VariantType::Ray: +#endif + case VariantType::Transform: + case VariantType::Matrix: + ASSERT(sizeof(data) >= AsBlob.Length); + Platform::MemoryCopy(data, AsBlob.Data, AsBlob.Length); + break; + default: + return; // Not used + } + SetType(VariantType(VariantType::Structure, InBuiltTypesTypeNames[Type.Type])); + CopyStructure(data); +} + Variant Variant::NewValue(const StringAnsiView& typeName) { Variant v; diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 8cc1e133b..adb49249f 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -372,6 +372,9 @@ public: // Inlines potential value type into in-built format (eg. Vector3 stored as Structure, or String stored as ManagedObject). void Inline(); + // Inverts the inlined value from in-built format into generic storage (eg. Float3 from inlined format into Structure). + void InvertInline(); + // Allocates the Variant of the specific type (eg. structure or object or value). static Variant NewValue(const StringAnsiView& typeName); diff --git a/Source/Engine/Visject/VisjectGraph.cpp b/Source/Engine/Visject/VisjectGraph.cpp index 1cee5d46e..4e94473f6 100644 --- a/Source/Engine/Visject/VisjectGraph.cpp +++ b/Source/Engine/Visject/VisjectGraph.cpp @@ -685,7 +685,7 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value) case 36: { // Get value with structure data - const Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection()); + Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection()); if (!node->GetBox(0)->HasConnection()) return; @@ -741,7 +741,9 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value) return; } const ScriptingType& type = typeHandle.GetType(); - if (structureValue.Type.Type != VariantType::Structure || StringUtils::Compare(typeNameAnsi.Get(), structureValue.Type.TypeName) != 0) + structureValue.InvertInline(); // Extract any Float3/Int32 into Structure type from inlined format + const ScriptingTypeHandle structureValueTypeHandle = Scripting::FindScriptingType(structureValue.Type.GetTypeName()); + if (structureValue.Type.Type != VariantType::Structure || typeHandle != structureValueTypeHandle) { OnError(node, box, String::Format(TEXT("Cannot unpack value of type {0} to structure of type {1}"), structureValue.Type, typeName)); return; From dc7170c51e0e8d6d0d6d505197052b47033a2798 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Fri, 10 Nov 2023 21:32:18 +0200 Subject: [PATCH 262/546] Expose Freetype font style flags in `FontAsset` --- Source/Engine/Render2D/FontAsset.cpp | 10 ++++++++++ Source/Engine/Render2D/FontAsset.h | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index f000eb3c6..a477e5f4d 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -87,6 +87,16 @@ bool FontAsset::Init() return error; } +FontFlags FontAsset::GetStyle() const +{ + FontFlags flags = FontFlags::None; + if ((_face->style_flags & FT_STYLE_FLAG_ITALIC) != 0) + flags |= FontFlags::Italic; + if ((_face->style_flags & FT_STYLE_FLAG_BOLD) != 0) + flags |= FontFlags::Bold; + return flags; +} + void FontAsset::SetOptions(const FontOptions& value) { _options = value; diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index 17839d49b..4dea84a5b 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -128,6 +128,11 @@ public: return _options; } + /// + /// Gets the font style flags. + /// + API_PROPERTY() FontFlags GetStyle() const; + /// /// Sets the font options. /// From ddaa5f9161fceb4c1120a49fa434e8668605c919 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 11 Nov 2023 14:47:20 +0100 Subject: [PATCH 263/546] Fix regression in Custom Editor UI from 74bcf7d9e598be82635086570555b41cd85b9988 #1616 #1911 --- Source/Editor/CustomEditors/CustomEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 32111e51c..9de330213 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -157,7 +157,7 @@ namespace FlaxEditor.CustomEditors var values = _values; var presenter = _presenter; var layout = _layout; - if (layout.Editors.Count != 1) + if (layout.Editors.Count > 1) { // There are more editors using the same layout so rebuild parent editor to prevent removing others editors _parent?.RebuildLayout(); From be90f47585da048cb243d830c906f89951a99bfd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 11 Nov 2023 15:37:12 +0100 Subject: [PATCH 264/546] Documentation improvements from https://github.com/FlaxEngine/FlaxDocs/pull/125 --- Source/Engine/Streaming/TextureGroup.h | 8 ++--- Source/Engine/Tools/ModelTool/ModelTool.h | 30 +++++++++---------- Source/Engine/Tools/TextureTool/TextureTool.h | 10 +++---- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Source/Engine/Streaming/TextureGroup.h b/Source/Engine/Streaming/TextureGroup.h index 42b308baf..c7db3f14c 100644 --- a/Source/Engine/Streaming/TextureGroup.h +++ b/Source/Engine/Streaming/TextureGroup.h @@ -36,7 +36,7 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(TextureGroup); int32 MaxAnisotropy = 16; /// - /// The quality scale factor applied to textures in this group. Can be used to increase or decrease textures resolution. In range 0-1 where 0 means lowest quality, 1 means full quality. + /// The quality scale factor applied to textures in this group. Can be used to increase or decrease textures resolution. In the range 0-1 where 0 means lowest quality, 1 means full quality. /// API_FIELD(Attributes="EditorOrder(20), Limit(0, 1)") float Quality = 1.0f; @@ -60,20 +60,20 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(TextureGroup); int32 MipLevelsMin = 0; /// - /// The maximum amount of loaded mip levels for textures in this group. Defines the maximum amount of the mips that can be loaded. Overriden per-platform. Lower values reduce textures quality and improve performance. + /// The maximum amount of loaded mip levels for textures in this group. Defines the maximum amount of mips that can be loaded. Overriden per-platform. Lower values reduce texture quality and improve performance. /// API_FIELD(Attributes="EditorOrder(40), Limit(1, 14)") int32 MipLevelsMax = 14; /// - /// The loaded mip levels bias for textures in this group. Can be used to increase or decrease quality of the streaming for textures in this group (eg. bump up the quality during cinematic sequence). + /// The loaded mip levels bias for textures in this group. Can be used to increase or decrease the quality of streaming for textures in this group (eg. bump up the quality during cinematic sequence). /// API_FIELD(Attributes="EditorOrder(50), Limit(-14, 14)") int32 MipLevelsBias = 0; #if USE_EDITOR /// - /// The per-platform maximum amount of mip levels for textures in this group. Can be used to strip textures quality when cooking game for a target platform. + /// The per-platform maximum amount of mip levels for textures in this group. Can be used to strip textures quality when cooking the game for a target platform. /// API_FIELD(Attributes="EditorOrder(50)") Dictionary MipLevelsMaxPerPlatform; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index c7876e826..ac8cb231f 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -228,25 +228,25 @@ public: public: // Geometry - // Enable model normal vectors recalculating. + // Enable model normal vectors re-calculating. API_FIELD(Attributes="EditorOrder(20), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") bool CalculateNormals = false; - // Specifies the maximum angle (in degrees) that may be between two face normals at the same vertex position that their are smoothed together. The default value is 175. + // Specifies the maximum angle (in degrees) that may be between two face normals at the same vertex position before they are smoothed together. The default value is 175. API_FIELD(Attributes="EditorOrder(30), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowSmoothingNormalsAngle)), Limit(0, 175, 0.1f)") float SmoothingNormalsAngle = 175.0f; // If checked, the imported normal vectors of the mesh will be flipped (scaled by -1). API_FIELD(Attributes="EditorOrder(35), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") bool FlipNormals = false; - // Enable model tangent vectors recalculating. + // Enable model tangent vectors re-calculating. API_FIELD(Attributes="EditorOrder(40), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") bool CalculateTangents = false; - // Specifies the maximum angle (in degrees) that may be between two vertex tangents that their tangents and bi-tangents are smoothed. The default value is 45. + // Specifies the maximum angle (in degrees) that may be between two vertex tangents before their tangents and bi-tangents are smoothed. The default value is 45. API_FIELD(Attributes="EditorOrder(45), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowSmoothingTangentsAngle)), Limit(0, 45, 0.1f)") float SmoothingTangentsAngle = 45.0f; // Enable/disable meshes geometry optimization. API_FIELD(Attributes="EditorOrder(50), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") bool OptimizeMeshes = true; - // Enable/disable geometry merge for meshes with the same materials. + // Enable/disable geometry merge for meshes with the same materials. Index buffer will be reordered to improve performance and other modifications will be applied. However, importing time will be increased. API_FIELD(Attributes="EditorOrder(60), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") bool MergeMeshes = true; // Enable/disable importing meshes Level of Details. @@ -258,16 +258,16 @@ public: // Enable/disable importing blend shapes (morph targets). API_FIELD(Attributes="EditorOrder(85), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowSkinnedModel))") bool ImportBlendShapes = false; - // Enable skeleton bones offset matrices recalculating. + // Enable skeleton bones offset matrices re-calculating. API_FIELD(Attributes="EditorOrder(86), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowSkinnedModel))") bool CalculateBoneOffsetMatrices = false; // The lightmap UVs source. API_FIELD(Attributes="EditorOrder(90), EditorDisplay(\"Geometry\", \"Lightmap UVs Source\"), VisibleIf(nameof(ShowModel))") ModelLightmapUVsSource LightmapUVsSource = ModelLightmapUVsSource::Disable; - // If specified, all meshes which name starts with this prefix will be imported as a separate collision data (excluded used for rendering). + // If specified, all meshes that name starts with this prefix in the name will be imported as a separate collision data asset (excluded used for rendering). API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") String CollisionMeshesPrefix = TEXT(""); - // The type of collision that should be generated if has collision prefix specified. + // The type of collision that should be generated if the mesh has a collision prefix specified. API_FIELD(Attributes = "EditorOrder(105), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") CollisionDataType CollisionType = CollisionDataType::TriangleMesh; @@ -291,19 +291,19 @@ public: public: // Animation - // Imported animation duration mode. Can use the original value or overriden by settings. + // Imported animation duration mode. Can use the original value or be overriden by settings. API_FIELD(Attributes="EditorOrder(1000), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowAnimation))") AnimationDuration Duration = AnimationDuration::Imported; // Imported animation first/last frame index. Used only if Duration mode is set to Custom. API_FIELD(Attributes="EditorOrder(1010), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowFramesRange)), Limit(0)") Float2 FramesRange = Float2::Zero; - // The imported animation default frame rate. Can specify the default frames per second amount for imported animation. If value is 0 then the original animation frame rate will be used. + // The imported animation default frame rate. Can specify the default frames per second amount for imported animations. If the value is 0 then the original animation frame rate will be used. API_FIELD(Attributes="EditorOrder(1020), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowAnimation)), Limit(0, 1000, 0.01f)") float DefaultFrameRate = 0.0f; // The imported animation sampling rate. If value is 0 then the original animation speed will be used. API_FIELD(Attributes="EditorOrder(1030), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowAnimation)), Limit(0, 1000, 0.01f)") float SamplingRate = 0.0f; - // The imported animation will have removed tracks with no keyframes or unspecified data. + // The imported animation will have tracks with no keyframes or unspecified data removed. API_FIELD(Attributes="EditorOrder(1040), EditorDisplay(\"Animation\"), VisibleIf(nameof(ShowAnimation))") bool SkipEmptyCurves = true; // The imported animation channels will be optimized to remove redundant keyframes. @@ -348,7 +348,7 @@ public: // If checked, the importer will create the model's materials as instances of a base material. API_FIELD(Attributes = "EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials))") bool ImportMaterialsAsInstances = false; - // The material to import the model's materials as an instance of. + // The material used as the base material that will be instanced as the imported model's material. API_FIELD(Attributes = "EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances))") AssetReference InstanceToImportAs; // If checked, the importer will import texture files used by the model and any embedded texture resources. @@ -369,16 +369,16 @@ public: public: // Splitting - // If checked, the imported mesh/animations are splitted into separate assets. Used if ObjectIndex is set to -1. + // If checked, the imported mesh/animations are split into separate assets. Used if ObjectIndex is set to -1. API_FIELD(Attributes="EditorOrder(2000), EditorDisplay(\"Splitting\")") bool SplitObjects = false; - // The zero-based index for the mesh/animation clip to import. If the source file has more than one mesh/animation it can be used to pick a desire object. Default -1 imports all objects. + // The zero-based index for the mesh/animation clip to import. If the source file has more than one mesh/animation it can be used to pick a desired object. Default -1 imports all objects. API_FIELD(Attributes="EditorOrder(2010), EditorDisplay(\"Splitting\")") int32 ObjectIndex = -1; public: // Other - // If specified, will be used as sub-directory name for automatically imported sub assets such as textures and materials. Set to whitespace (single space) to import to the same directory. + // If specified, the specified folder will be used as sub-directory name for automatically imported sub assets such as textures and materials. Set to whitespace (single space) to import to the same directory. API_FIELD(Attributes="EditorOrder(3030), EditorDisplay(\"Other\")") String SubAssetFolder = TEXT(""); diff --git a/Source/Engine/Tools/TextureTool/TextureTool.h b/Source/Engine/Tools/TextureTool/TextureTool.h index 98ebf1d89..60e9df3af 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.h +++ b/Source/Engine/Tools/TextureTool/TextureTool.h @@ -57,11 +57,11 @@ API_CLASS(Namespace="FlaxEngine.Tools", Static) class FLAXENGINE_API TextureTool API_FIELD(Attributes="EditorOrder(70)") bool FlipY = false; - // Texture size scale. Default is 1. + // Texture size scale. Allows increasing or decreasing the imported texture resolution. Default is 1. API_FIELD(Attributes="EditorOrder(80), Limit(0.0001f, 1000.0f, 0.01f)") float Scale = 1.0f; - // Maximum size of the texture (for both width and height). Higher resolution textures will be resized during importing process. + // Maximum size of the texture (for both width and height). Higher resolution textures will be resized during importing process. Used to clip textures that are too big. API_FIELD(Attributes="HideInEditor") int32 MaxSize = 8192; @@ -69,11 +69,11 @@ API_CLASS(Namespace="FlaxEngine.Tools", Static) class FLAXENGINE_API TextureTool API_FIELD(Attributes="EditorOrder(100)") bool Resize = false; - // The width of the imported texture. If Resize property is set to true then texture will be resized during the import to this value. Otherwise it will be ignored. + // The width of the imported texture. If Resize property is set to true then texture will be resized during the import to this value during the import, otherwise it will be ignored. API_FIELD(Attributes="HideInEditor") int32 SizeX = 1024; - // The height of the imported texture. If Resize property is set to true then texture will be resized during the import to this value. Otherwise it will be ignored. + // The height of the imported texture. If Resize property is set to true then texture will be resized during the import to this value during the import, otherwise it will be ignored. API_FIELD(Attributes="HideInEditor") int32 SizeY = 1024; @@ -85,7 +85,7 @@ API_CLASS(Namespace="FlaxEngine.Tools", Static) class FLAXENGINE_API TextureTool API_FIELD(Attributes="EditorOrder(210), VisibleIf(\"PreserveAlphaCoverage\")") float PreserveAlphaCoverageReference = 0.5f; - // Texture group for streaming (negative if unused). See Streaming Settings. + // The texture group for streaming (negative if unused). See Streaming Settings. API_FIELD(Attributes="EditorOrder(300), CustomEditorAlias(\"FlaxEditor.CustomEditors.Dedicated.TextureGroupEditor\")") int32 TextureGroup = -1; From 31ce41c5a4ee8469d3895a68c0b4b44abf43f8cd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 11 Nov 2023 17:57:54 +0100 Subject: [PATCH 265/546] Fix marking scene as dirty when creating prefab from existing actor #1916 --- Source/Editor/Modules/PrefabsModule.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/Modules/PrefabsModule.cs b/Source/Editor/Modules/PrefabsModule.cs index adb5f7685..6abf8e45c 100644 --- a/Source/Editor/Modules/PrefabsModule.cs +++ b/Source/Editor/Modules/PrefabsModule.cs @@ -124,6 +124,7 @@ namespace FlaxEditor.Modules if (!Editor.StateMachine.CurrentState.CanEditScene) return; undo = Editor.Undo; + Editor.Scene.MarkSceneEdited(actor.Scene); } // Record undo for prefab creating (backend links the target instance with the prefab) From cc1e98db3cdd06bb77c8a3fef52024cb3053503c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 12 Nov 2023 01:06:06 +0100 Subject: [PATCH 266/546] Bump up build number --- Flax.flaxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flax.flaxproj b/Flax.flaxproj index a3157a032..b3de36d70 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 7, "Revision": 0, - "Build": 6404 + "Build": 6405 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.", From 0f14672e3bd22d82699c434d2e0bf2f24815c8fc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 13 Nov 2023 09:45:33 +0100 Subject: [PATCH 267/546] Codestyle formatting and fixes for #1888 --- .../Handlers/CompileScriptsProgress.cs | 9 -- Source/Editor/States/ReloadingScriptsState.cs | 1 - Source/Engine/Engine/NativeInterop.cs | 8 +- .../ExtendedSerializationBinder.cs | 83 +++++++++---------- Source/Engine/Serialization/JsonSerializer.cs | 60 ++++++++------ 5 files changed, 80 insertions(+), 81 deletions(-) diff --git a/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs b/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs index 310612a73..cbb383b4c 100644 --- a/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs +++ b/Source/Editor/Progress/Handlers/CompileScriptsProgress.cs @@ -25,7 +25,6 @@ namespace FlaxEditor.Progress.Handlers ScriptsBuilder.ScriptsReloadCalled += () => OnUpdate(0.8f, "Reloading scripts..."); ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; ScriptsBuilder.ScriptsReloadEnd += OnScriptsReloadEnd; - ScriptsBuilder.ScriptsReload += OnScriptsReload; } private void OnScriptsReloadBegin() @@ -38,14 +37,6 @@ namespace FlaxEditor.Progress.Handlers Editor.Instance.Scene.ClearRefsToSceneObjects(true); } - private void OnScriptsReload() - { -#if !USE_NETCORE - // Clear types cache - Newtonsoft.Json.JsonSerializer.ClearCache(); -#endif - } - private void OnCompilationFailed() { OnFail("Scripts compilation failed"); diff --git a/Source/Editor/States/ReloadingScriptsState.cs b/Source/Editor/States/ReloadingScriptsState.cs index 4b8866202..d9024d5c8 100644 --- a/Source/Editor/States/ReloadingScriptsState.cs +++ b/Source/Editor/States/ReloadingScriptsState.cs @@ -1,7 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using FlaxEngine; -using FlaxEditor.Utilities; using FlaxEngine.Utilities; namespace FlaxEditor.States diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index b4635e0b4..3f19f5951 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -297,9 +297,10 @@ namespace FlaxEngine.Interop return type; - /// Resolve the type by manually checking every scripting assembly - static Type ResolveSlow(string typeName) { - foreach (var assembly in scriptingAssemblyLoadContext.Assemblies) { + static Type ResolveSlow(string typeName) + { + foreach (var assembly in scriptingAssemblyLoadContext.Assemblies) + { var type = assembly.GetType(typeName); if (type != null) return type; @@ -307,7 +308,6 @@ namespace FlaxEngine.Interop return null; } - /// Resolve the assembly by name static Assembly ResolveAssembly(AssemblyName name) => ResolveScriptingAssemblyByName(name, allowPartial: false); } diff --git a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs index b4f0173ab..f239a3384 100644 --- a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs +++ b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedSerializationBinder.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Globalization; -using System.Linq; using System.Reflection; using System.Runtime.Serialization; using FlaxEngine.Interop; @@ -22,7 +20,6 @@ namespace FlaxEngine.Json.JsonCustomSerializers private ConcurrentDictionary _typeCache; private Func _resolveType; - /// Clear the cache /// Should be cleared on scripting domain reload to avoid out of date types participating in dynamic type resolution public void ResetCache() @@ -51,14 +48,19 @@ namespace FlaxEngine.Json.JsonCustomSerializers Type ResolveType(TypeKey key) { Type? type = null; - if (key.assemblyName is null) { // No assembly name, attempt to find globally + if (key.assemblyName is null) + { + // No assembly name, attempt to find globally type = FindTypeGlobal(key.typeName); } - if (type is null && key.assemblyName is not null) { // Type not found yet, but we have assembly name + if (type is null && key.assemblyName is not null) + { + // Type not found yet, but we have assembly name var assembly = ResolveAssembly(new(key.assemblyName)); - type = FindTypeInAssembly(key.typeName, assembly); // We have assembly, attempt to load from assembly + // We have assembly, attempt to load from assembly + type = FindTypeInAssembly(key.typeName, assembly); } //if (type is null) @@ -74,18 +76,13 @@ namespace FlaxEngine.Json.JsonCustomSerializers Assembly ResolveAssembly(AssemblyName name) { Assembly? assembly = null; - assembly = FindScriptingAssembly(name); // Attempt to find in scripting assemblies - if (assembly is null) assembly = FindLoadAssembly(name); // Attempt to load - if (assembly is null) assembly = FindDomainAssembly(name); // Attempt to find in the current domain - if (assembly is null) throw MakeAsmResolutionException(name.FullName); // Assembly failed to resolve - return assembly; } @@ -98,15 +95,14 @@ namespace FlaxEngine.Json.JsonCustomSerializers /// Attempt to find the assembly in the current domain Assembly? FindDomainAssembly(AssemblyName assemblyName) { - var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToArray(); - - foreach (Assembly assembly in assemblies) { // Looking in domain may be necessary (in case of anon dynamic assembly for example) + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (Assembly assembly in assemblies) + { + // Looking in domain may be necessary (in case of anon dynamic assembly for example) var curName = assembly.GetName(); - if (curName == assemblyName || curName.Name == assemblyName.Name) return assembly; } - return null; } @@ -114,24 +110,20 @@ namespace FlaxEngine.Json.JsonCustomSerializers Assembly? FindLoadAssembly(AssemblyName assemblyName) { Assembly? assembly = null; - assembly = Assembly.Load(assemblyName); - - if (assembly is null) + if (assembly is null && assemblyName.Name is not null) +#pragma warning disable CS0618 // Type or member is obsolete assembly = Assembly.LoadWithPartialName(assemblyName.Name); // Copying behavior of DefaultSerializationBinder - +#pragma warning restore CS0618 // Type or member is obsolete return assembly; } - /// Attempt to find a type in a specified assembly Type? FindTypeInAssembly(string typeName, Assembly assembly) { var type = assembly.GetType(typeName); // Attempt to load directly - if (type is null && typeName.IndexOf('`') >= 0) // Attempt failed, but name has generic variant tick, try resolving generic manually type = FindTypeGeneric(typeName, assembly); - return type; } @@ -146,7 +138,6 @@ namespace FlaxEngine.Json.JsonCustomSerializers { return _typeCache.GetOrAdd(key, _resolveType); } - /********************************************* ** Below code is adapted from Newtonsoft.Json @@ -157,26 +148,33 @@ namespace FlaxEngine.Json.JsonCustomSerializers { Type? type = null; int openBracketIndex = typeName.IndexOf('[', StringComparison.Ordinal); - if (openBracketIndex >= 0) { + if (openBracketIndex >= 0) + { string genericTypeDefName = typeName.Substring(0, openBracketIndex); // Find the unspecialized type Type? genericTypeDef = assembly.GetType(genericTypeDefName); - if (genericTypeDef != null) { + if (genericTypeDef != null) + { List genericTypeArguments = new List(); // Recursively resolve the arguments int scope = 0; int typeArgStartIndex = 0; int endIndex = typeName.Length - 1; - for (int i = openBracketIndex + 1; i < endIndex; ++i) { + for (int i = openBracketIndex + 1; i < endIndex; ++i) + { char current = typeName[i]; - switch (current) { + switch (current) + { case '[': - if (scope == 0) { + if (scope == 0) + { typeArgStartIndex = i + 1; } ++scope; break; case ']': --scope; - if (scope == 0) { // All arguments resolved, compose our type + if (scope == 0) + { + // All arguments resolved, compose our type string typeArgAssemblyQualifiedName = typeName.Substring(typeArgStartIndex, i - typeArgStartIndex); TypeKey typeNameKey = SplitFullyQualifiedTypeName(typeArgAssemblyQualifiedName); @@ -189,7 +187,6 @@ namespace FlaxEngine.Json.JsonCustomSerializers type = genericTypeDef.MakeGenericType(genericTypeArguments.ToArray()); } } - return type; } @@ -197,18 +194,17 @@ namespace FlaxEngine.Json.JsonCustomSerializers private static TypeKey SplitFullyQualifiedTypeName(string fullyQualifiedTypeName) { int? assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullyQualifiedTypeName); - - ReadOnlySpan typeName; - ReadOnlySpan assemblyName; - - if (assemblyDelimiterIndex != null) { + ReadOnlySpan typeName, assemblyName; + if (assemblyDelimiterIndex != null) + { typeName = fullyQualifiedTypeName.AsSpan().Slice(0, assemblyDelimiterIndex ?? 0); assemblyName = fullyQualifiedTypeName.AsSpan().Slice((assemblyDelimiterIndex ?? 0) + 1); - } else { + } + else + { typeName = fullyQualifiedTypeName; assemblyName = null; } - return new(new(assemblyName), new(typeName)); } @@ -218,9 +214,11 @@ namespace FlaxEngine.Json.JsonCustomSerializers // we need to get the first comma following all surrounded in brackets because of generic types // e.g. System.Collections.Generic.Dictionary`2[[System.String, mscorlib,Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 int scope = 0; - for (int i = 0; i < fullyQualifiedTypeName.Length; i++) { + for (int i = 0; i < fullyQualifiedTypeName.Length; i++) + { char current = fullyQualifiedTypeName[i]; - switch (current) { + switch (current) + { case '[': scope++; break; @@ -228,7 +226,8 @@ namespace FlaxEngine.Json.JsonCustomSerializers scope--; break; case ',': - if (scope == 0) { + if (scope == 0) + { return i; } break; @@ -243,7 +242,7 @@ namespace FlaxEngine.Json.JsonCustomSerializers { return new($"Could not load assembly '{asmName}'."); } - + private static JsonSerializationException MakeTypeResolutionException(string? asmName, string typeName) { if (asmName is null) diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index 2eb9eec99..a5d6e0771 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -19,7 +19,7 @@ namespace FlaxEngine.Json { internal class SerializerCache { - public readonly JsonSerializerSettings settings; + public readonly JsonSerializerSettings JsonSettings; public Newtonsoft.Json.JsonSerializer JsonSerializer; public StringBuilder StringBuilder; public StringWriter StringWriter; @@ -29,23 +29,27 @@ namespace FlaxEngine.Json public StreamReader Reader; public bool IsWriting; public bool IsReading; - public uint cacheVersion; +#if FLAX_EDITOR + public uint CacheVersion; +#endif public unsafe SerializerCache(JsonSerializerSettings settings) { - this.settings = settings; - + JsonSettings = settings; StringBuilder = new StringBuilder(256); StringWriter = new StringWriter(StringBuilder, CultureInfo.InvariantCulture); MemoryStream = new UnmanagedMemoryStream((byte*)0, 0); - lock (cacheSyncRoot) +#if FLAX_EDITOR + lock (CurrentCacheSyncRoot) +#endif { BuildSerializer(); BuildRead(); BuildWrite(); - - cacheVersion = currentCacheVersion; +#if FLAX_EDITOR + CacheVersion = Json.JsonSerializer.CurrentCacheVersion; +#endif } } @@ -57,7 +61,6 @@ namespace FlaxEngine.Json if (IsReading) BuildRead(); - IsWriting = false; IsReading = true; } @@ -88,29 +91,30 @@ namespace FlaxEngine.Json /// Check that the cache is up to date, rebuild it if it isn't private void CheckCacheVersionRebuild() { - var cCV = currentCacheVersion; - if (cacheVersion == cCV) +#if FLAX_EDITOR + var version = Json.JsonSerializer.CurrentCacheVersion; + if (CacheVersion == version) return; - lock (cacheSyncRoot) + lock (CurrentCacheSyncRoot) { - cCV = currentCacheVersion; - if (cacheVersion == cCV) + version = Json.JsonSerializer.CurrentCacheVersion; + if (CacheVersion == version) return; BuildSerializer(); - BuildRead(); BuildWrite(); - cacheVersion = cCV; + CacheVersion = version; } +#endif } /// Builds the serializer private void BuildSerializer() { - JsonSerializer = Newtonsoft.Json.JsonSerializer.CreateDefault(settings); + JsonSerializer = Newtonsoft.Json.JsonSerializer.CreateDefault(Settings); JsonSerializer.Formatting = Formatting.Indented; JsonSerializer.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; } @@ -125,7 +129,8 @@ namespace FlaxEngine.Json private void BuildWrite() { SerializerWriter = new JsonSerializerInternalWriter(JsonSerializer); - JsonWriter = new JsonTextWriter(StringWriter) { + JsonWriter = new JsonTextWriter(StringWriter) + { IndentChar = '\t', Indentation = 1, Formatting = JsonSerializer.Formatting, @@ -143,24 +148,24 @@ namespace FlaxEngine.Json internal static JsonSerializerSettings SettingsManagedOnly = CreateDefaultSettings(true); internal static ExtendedSerializationBinder SerializationBinder; internal static FlaxObjectConverter ObjectConverter; - internal static ThreadLocal Current = new ThreadLocal(); internal static ThreadLocal Cache = new ThreadLocal(() => new SerializerCache(Settings)); internal static ThreadLocal CacheManagedOnly = new ThreadLocal(() => new SerializerCache(SettingsManagedOnly)); internal static ThreadLocal CachedGuidBuffer = new ThreadLocal(() => Marshal.AllocHGlobal(32 * sizeof(char)), true); internal static string CachedGuidDigits = "0123456789abcdef"; +#if FLAX_EDITOR /// The version of the cache, used to check that a cache is not out of date - internal static uint currentCacheVersion = 0; + internal static uint CurrentCacheVersion = 0; + /// Used to synchronize cache operations such as rebuild, and - internal static readonly object cacheSyncRoot = new(); + internal static readonly object CurrentCacheSyncRoot = new(); +#endif internal static JsonSerializerSettings CreateDefaultSettings(bool isManagedOnly) { //Newtonsoft.Json.Utilities.MiscellaneousUtils.ValueEquals = ValueEquals; - if (SerializationBinder is null) SerializationBinder = new(); - var settings = new JsonSerializerSettings { ContractResolver = new ExtendedDefaultContractResolver(isManagedOnly), @@ -185,17 +190,22 @@ namespace FlaxEngine.Json return settings; } - /// Called to reset the serialization cache +#if FLAX_EDITOR + /// Resets the serialization cache. internal static void ResetCache() { - lock (cacheSyncRoot) + lock (CurrentCacheSyncRoot) { - unchecked { currentCacheVersion++; } + unchecked + { + CurrentCacheVersion++; + } Newtonsoft.Json.JsonSerializer.ClearCache(); SerializationBinder.ResetCache(); } } +#endif internal static void Dispose() { From 930b1b978cad606c4db54230015fd0cd3f07d375 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 13 Nov 2023 10:25:46 +0100 Subject: [PATCH 268/546] Fix incorrect spline length calculation if first point is not at spline origin #1876 --- Source/Engine/Level/Actors/Spline.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp index 194dbd9a9..df93ea840 100644 --- a/Source/Engine/Level/Actors/Spline.cpp +++ b/Source/Engine/Level/Actors/Spline.cpp @@ -152,6 +152,11 @@ float Spline::GetSplineLength() const const int32 slices = 20; const float step = 1.0f / (float)slices; Vector3 prevPoint = Vector3::Zero; + if (Curve.GetKeyframes().Count() != 0) + { + const auto& a = Curve[0]; + prevPoint = a.Value.Translation * _transform.Scale; + } for (int32 i = 1; i < Curve.GetKeyframes().Count(); i++) { const auto& a = Curve[i - 1]; From 418918920e3f804c5a7bb38e8d506bf7105a14b9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 13 Nov 2023 11:38:37 +0100 Subject: [PATCH 269/546] Add `GetSplineSegmentLength` to get spline segment length #1879 --- Source/Engine/Level/Actors/Spline.cpp | 35 +++++++++++++++++++++++++-- Source/Engine/Level/Actors/Spline.h | 7 ++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Level/Actors/Spline.cpp b/Source/Engine/Level/Actors/Spline.cpp index df93ea840..dfe7c2be8 100644 --- a/Source/Engine/Level/Actors/Spline.cpp +++ b/Source/Engine/Level/Actors/Spline.cpp @@ -149,8 +149,8 @@ float Spline::GetSplineDuration() const float Spline::GetSplineLength() const { float sum = 0.0f; - const int32 slices = 20; - const float step = 1.0f / (float)slices; + constexpr int32 slices = 20; + constexpr float step = 1.0f / (float)slices; Vector3 prevPoint = Vector3::Zero; if (Curve.GetKeyframes().Count() != 0) { @@ -181,6 +181,37 @@ float Spline::GetSplineLength() const return Math::Sqrt(sum); } +float Spline::GetSplineSegmentLength(int32 index) const +{ + if (index == 0) + return 0.0f; + CHECK_RETURN(index > 0 && index < GetSplinePointsCount(), 0.0f); + float sum = 0.0f; + constexpr int32 slices = 20; + constexpr float step = 1.0f / (float)slices; + const auto& a = Curve[index - 1]; + const auto& b = Curve[index]; + Vector3 startPoint = a.Value.Translation * _transform.Scale; + { + const float length = Math::Abs(b.Time - a.Time); + Vector3 leftTangent, rightTangent; + AnimationUtils::GetTangent(a.Value.Translation, a.TangentOut.Translation, length, leftTangent); + AnimationUtils::GetTangent(b.Value.Translation, b.TangentIn.Translation, length, rightTangent); + + // TODO: implement sth more analytical than brute-force solution + for (int32 slice = 0; slice < slices; slice++) + { + const float t = (float)slice * step; + Vector3 pos; + AnimationUtils::Bezier(a.Value.Translation, leftTangent, rightTangent, b.Value.Translation, t, pos); + pos *= _transform.Scale; + sum += (float)Vector3::DistanceSquared(pos, startPoint); + startPoint = pos; + } + } + return Math::Sqrt(sum); +} + float Spline::GetSplineTime(int32 index) const { CHECK_RETURN(index >= 0 && index < GetSplinePointsCount(), 0.0f) diff --git a/Source/Engine/Level/Actors/Spline.h b/Source/Engine/Level/Actors/Spline.h index cd022890b..4a81ee186 100644 --- a/Source/Engine/Level/Actors/Spline.h +++ b/Source/Engine/Level/Actors/Spline.h @@ -167,6 +167,13 @@ public: /// API_PROPERTY() float GetSplineLength() const; + /// + /// Gets the length of the spline segment (distance between pair of two points). + /// + /// The index of the segment end index. Zero-based, smaller than GetSplinePointsCount(). + /// The spline segment length. + API_FUNCTION() float GetSplineSegmentLength(int32 index) const; + /// /// Gets the time of the spline keyframe. /// From 422fb34c695ddff26275254103fd6437230dd190 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 13 Nov 2023 14:41:33 +0100 Subject: [PATCH 270/546] Rename `NumberOfItemsToShow` to `ShowMaxItemsCount` #1826 --- Source/Engine/UI/GUI/Common/Dropdown.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index 92067398d..ecca2978f 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -258,10 +258,10 @@ namespace FlaxEngine.GUI public bool ShowAllItems { get; set; } = true; /// - /// Gets or sets the number of items to show. Only used if ShowAllItems is false. + /// Gets or sets the maximum number of items to show at once. Only used if ShowAllItems is false. /// [EditorOrder(4), VisibleIf(nameof(ShowAllItems), true), Limit(1), Tooltip("The number of items to show in the drop down.")] - public int NumberOfItemsToShow { get; set; } = 5; + public int ShowMaxItemsCount { get; set; } = 5; /// /// Event fired when selected index gets changed. @@ -527,14 +527,14 @@ namespace FlaxEngine.GUI } } - if (ShowAllItems || _items.Count < NumberOfItemsToShow) + if (ShowAllItems || _items.Count < ShowMaxItemsCount) { popup.Size = new Float2(itemsWidth, height); panel.Size = popup.Size; } else { - popup.Size = new Float2(itemsWidth, (itemsHeight + container.Spacing) * NumberOfItemsToShow); + popup.Size = new Float2(itemsWidth, (itemsHeight + container.Spacing) * ShowMaxItemsCount); panel.Size = popup.Size; } From 46f82aabcdc64162b35489bcc0311de73f6fba7e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 13 Nov 2023 14:48:40 +0100 Subject: [PATCH 271/546] Fix ui navigation regression from 2f9343c2365c1ca6cbd9ad04e2bc0505295a85f4 --- Source/Engine/UI/GUI/CanvasRootControl.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Source/Engine/UI/GUI/CanvasRootControl.cs b/Source/Engine/UI/GUI/CanvasRootControl.cs index b2ea9aaa0..54ad75295 100644 --- a/Source/Engine/UI/GUI/CanvasRootControl.cs +++ b/Source/Engine/UI/GUI/CanvasRootControl.cs @@ -198,8 +198,12 @@ namespace FlaxEngine.GUI /// public override void Update(float deltaTime) { - // UI navigation + // Update navigation if (SkipEvents) + { + _navigationHeldTimeUp = _navigationHeldTimeDown = _navigationHeldTimeLeft = _navigationHeldTimeRight = 0; + _navigationRateTimeUp = _navigationRateTimeDown = _navigationRateTimeLeft = _navigationRateTimeRight = 0; + } { UpdateNavigation(deltaTime, _canvas.NavigateUp.Name, NavDirection.Up, ref _navigationHeldTimeUp, ref _navigationRateTimeUp); UpdateNavigation(deltaTime, _canvas.NavigateDown.Name, NavDirection.Down, ref _navigationHeldTimeDown, ref _navigationRateTimeDown); @@ -207,11 +211,6 @@ namespace FlaxEngine.GUI UpdateNavigation(deltaTime, _canvas.NavigateRight.Name, NavDirection.Right, ref _navigationHeldTimeRight, ref _navigationRateTimeRight); UpdateNavigation(deltaTime, _canvas.NavigateSubmit.Name, ref _navigationHeldTimeSubmit, ref _navigationRateTimeSubmit, SubmitFocused); } - else - { - _navigationHeldTimeUp = _navigationHeldTimeDown = _navigationHeldTimeLeft = _navigationHeldTimeRight = 0; - _navigationRateTimeUp = _navigationRateTimeDown = _navigationRateTimeLeft = _navigationRateTimeRight = 0; - } base.Update(deltaTime); } From 97a28d443162c8fe0fd08fa27a1e61dfc2c26a5c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 13 Nov 2023 15:48:12 +0100 Subject: [PATCH 272/546] Add security lockers for managed typeinfo access --- Source/Engine/Scripting/BinaryModule.cpp | 1 + Source/Engine/Scripting/Runtime/DotNet.cpp | 26 +++++++++++++++++++--- Source/Engine/Scripting/Scripting.cpp | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index 679ead4e3..5bbaf8d0b 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -906,6 +906,7 @@ void ManagedBinaryModule::OnLoaded(MAssembly* assembly) #if !COMPILE_WITHOUT_CSHARP PROFILE_CPU(); ASSERT(ClassToTypeIndex.IsEmpty()); + ScopeLock lock(Locker); const auto& classes = assembly->GetClasses(); diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 200efa650..c4371736d 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -719,6 +719,7 @@ void GetAssemblyName(void* assemblyHandle, StringAnsi& name, StringAnsi& fullnam DEFINE_INTERNAL_CALL(void) NativeInterop_CreateClass(NativeClassDefinitions* managedClass, void* assemblyHandle) { + ScopeLock lock(BinaryModule::Locker); MAssembly* assembly = GetAssembly(assemblyHandle); if (assembly == nullptr) { @@ -732,7 +733,18 @@ DEFINE_INTERNAL_CALL(void) NativeInterop_CreateClass(NativeClassDefinitions* man MClass* klass = New(assembly, managedClass->typeHandle, managedClass->name, managedClass->fullname, managedClass->namespace_, managedClass->typeAttributes); if (assembly != nullptr) { - const_cast(assembly->GetClasses()).Add(klass->GetFullName(), klass); + auto& classes = const_cast(assembly->GetClasses()); + MClass* oldKlass; + if (classes.TryGet(klass->GetFullName(), oldKlass)) + { + LOG(Warning, "Class '{0}' was already added to assembly '{1}'", String(klass->GetFullName()), String(assembly->GetName())); + Delete(klass); + klass = oldKlass; + } + else + { + classes.Add(klass->GetFullName(), klass); + } } managedClass->nativePointer = klass; } @@ -873,7 +885,7 @@ MClass::MClass(const MAssembly* parentAssembly, void* handle, const char* name, static void* TypeIsEnumPtr = GetStaticMethodPointer(TEXT("TypeIsEnum")); _isEnum = CallStaticMethod(TypeIsEnumPtr, handle); - CachedClassHandles.Add(handle, this); + CachedClassHandles[handle] = this; } bool MAssembly::ResolveMissingFile(String& assemblyPath) const @@ -1551,6 +1563,7 @@ const Array& MProperty::GetAttributes() const MAssembly* GetAssembly(void* assemblyHandle) { + ScopeLock lock(BinaryModule::Locker); MAssembly* assembly; if (CachedAssemblyHandles.TryGet(assemblyHandle, assembly)) return assembly; @@ -1559,6 +1572,7 @@ MAssembly* GetAssembly(void* assemblyHandle) MClass* GetClass(MType* typeHandle) { + ScopeLock lock(BinaryModule::Locker); MClass* klass = nullptr; CachedClassHandles.TryGet(typeHandle, klass); return nullptr; @@ -1568,6 +1582,7 @@ MClass* GetOrCreateClass(MType* typeHandle) { if (!typeHandle) return nullptr; + ScopeLock lock(BinaryModule::Locker); MClass* klass; if (!CachedClassHandles.TryGet(typeHandle, klass)) { @@ -1579,7 +1594,12 @@ MClass* GetOrCreateClass(MType* typeHandle) klass = New(assembly, classInfo.typeHandle, classInfo.name, classInfo.fullname, classInfo.namespace_, classInfo.typeAttributes); if (assembly != nullptr) { - const_cast(assembly->GetClasses()).Add(klass->GetFullName(), klass); + auto& classes = const_cast(assembly->GetClasses()); + if (classes.ContainsKey(klass->GetFullName())) + { + LOG(Warning, "Class '{0}' was already added to assembly '{1}'", String(klass->GetFullName()), String(assembly->GetName())); + } + classes[klass->GetFullName()] = klass; } if (typeHandle != classInfo.typeHandle) diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp index bdf9f60f5..e9dc0421c 100644 --- a/Source/Engine/Scripting/Scripting.cpp +++ b/Source/Engine/Scripting/Scripting.cpp @@ -436,6 +436,7 @@ bool Scripting::Load() PROFILE_CPU(); // Note: this action can be called from main thread (due to Mono problems with assemblies actions from other threads) ASSERT(IsInMainThread()); + ScopeLock lock(BinaryModule::Locker); #if USE_CSHARP // Load C# core assembly From 618273977c7bc2bba2dc061b2d02a8f8fca9d21c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 13 Nov 2023 17:17:05 +0100 Subject: [PATCH 273/546] Minor improvements to code style #1541 --- Source/Editor/GUI/AssetPicker.cs | 2 +- Source/Editor/GUI/Row.cs | 2 +- Source/Editor/GUI/Timeline/Timeline.cs | 9 +++++---- Source/Editor/Options/OptionsModule.cs | 10 +++------- Source/Editor/Options/ThemeOptions.cs | 7 +++++-- Source/Editor/Windows/Profiler/Assets.cs | 5 +++-- Source/Editor/Windows/Profiler/CPU.cs | 5 +++-- Source/Editor/Windows/Profiler/GPU.cs | 5 +++-- Source/Editor/Windows/Profiler/MemoryGPU.cs | 5 +++-- Source/Editor/Windows/Profiler/Network.cs | 5 +++-- Source/Editor/Windows/Profiler/SingleChart.cs | 6 +++--- 11 files changed, 33 insertions(+), 28 deletions(-) diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 7e7b9816f..84f58daf1 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -173,7 +173,7 @@ namespace FlaxEditor.GUI else { // No element selected - Render2D.FillRectangle(iconRect, Style.Current.BackgroundNormal); + Render2D.FillRectangle(iconRect, style.BackgroundNormal); Render2D.DrawText(style.FontMedium, "No asset\nselected", iconRect, Color.Orange, TextAlignment.Center, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, Height / DefaultIconSize); } diff --git a/Source/Editor/GUI/Row.cs b/Source/Editor/GUI/Row.cs index 81a9d459d..f6bd5b02a 100644 --- a/Source/Editor/GUI/Row.cs +++ b/Source/Editor/GUI/Row.cs @@ -98,7 +98,7 @@ namespace FlaxEditor.GUI rect.Width -= leftDepthMargin; Render2D.PushClip(rect); - Render2D.DrawText(style.FontMedium, text, rect, Style.Current.Foreground, column.CellAlignment, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, text, rect, style.Foreground, column.CellAlignment, TextAlignment.Center); Render2D.PopClip(); x += width; diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index a32b35692..6b4d7bf4c 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -627,10 +627,11 @@ namespace FlaxEditor.GUI.Timeline Parent = this }; + var style = Style.Current; var headerTopArea = new ContainerControl { AutoFocus = false, - BackgroundColor = Style.Current.LightBackground, + BackgroundColor = style.LightBackground, AnchorPreset = AnchorPresets.HorizontalStretchTop, Offsets = new Margin(0, 0, 0, HeaderTopAreaHeight), Parent = _splitter.Panel1 @@ -683,7 +684,7 @@ namespace FlaxEditor.GUI.Timeline { AutoFocus = false, ClipChildren = false, - BackgroundColor = Style.Current.LightBackground, + BackgroundColor = style.LightBackground, AnchorPreset = AnchorPresets.HorizontalStretchBottom, Offsets = new Margin(0, 0, -playbackButtonsSize, playbackButtonsSize), Parent = _splitter.Panel1 @@ -845,7 +846,7 @@ namespace FlaxEditor.GUI.Timeline _timeIntervalsHeader = new TimeIntervalsHeader(this) { AutoFocus = false, - BackgroundColor = Style.Current.Background.RGBMultiplied(0.9f), + BackgroundColor = style.Background.RGBMultiplied(0.9f), AnchorPreset = AnchorPresets.HorizontalStretchTop, Offsets = new Margin(0, 0, 0, HeaderTopAreaHeight), Parent = _splitter.Panel2 @@ -854,7 +855,7 @@ namespace FlaxEditor.GUI.Timeline { AutoFocus = false, ClipChildren = false, - BackgroundColor = Style.Current.Background.RGBMultiplied(0.7f), + BackgroundColor = style.Background.RGBMultiplied(0.7f), AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, HeaderTopAreaHeight, 0), Parent = _splitter.Panel2 diff --git a/Source/Editor/Options/OptionsModule.cs b/Source/Editor/Options/OptionsModule.cs index 51aa70257..1137e4c37 100644 --- a/Source/Editor/Options/OptionsModule.cs +++ b/Source/Editor/Options/OptionsModule.cs @@ -208,13 +208,13 @@ namespace FlaxEditor.Options // If a non-default style was chosen, switch to that style string styleName = themeOptions.SelectedStyle; - if (styleName != "Default" && styleName != "LightDefault" && themeOptions.Styles.TryGetValue(styleName, out var style) && style != null) + if (styleName != ThemeOptions.DefaultName && styleName != ThemeOptions.LightDefault && themeOptions.Styles.TryGetValue(styleName, out var style) && style != null) { Style.Current = style; } else { - if (styleName == "LightDefault") + if (styleName == ThemeOptions.LightDefault) { Style.Current = CreateLightStyle(); } @@ -231,7 +231,6 @@ namespace FlaxEditor.Options /// The style object. public Style CreateDefaultStyle() { - // Metro Style colors var options = Options; var style = new Style { @@ -282,7 +281,6 @@ namespace FlaxEditor.Options SharedTooltip = new Tooltip(), }; style.DragWindow = style.BackgroundSelected * 0.7f; - return style; } @@ -292,7 +290,6 @@ namespace FlaxEditor.Options /// The style object. public Style CreateLightStyle() { - // Metro Style colors var options = Options; var style = new Style { @@ -334,9 +331,8 @@ namespace FlaxEditor.Options Scale = Editor.Icons.Scale32, Scalar = Editor.Icons.Scalar32, - SharedTooltip = new Tooltip() + SharedTooltip = new Tooltip(), }; - return style; } diff --git a/Source/Editor/Options/ThemeOptions.cs b/Source/Editor/Options/ThemeOptions.cs index a033b34da..674e281af 100644 --- a/Source/Editor/Options/ThemeOptions.cs +++ b/Source/Editor/Options/ThemeOptions.cs @@ -15,6 +15,9 @@ namespace FlaxEditor.Options [CustomEditor(typeof(ThemeOptionsEditor))] public sealed class ThemeOptions { + internal const string DefaultName = "Default"; + internal const string LightDefault = "LightDefault"; + internal class ThemeOptionsEditor : Editor { private LabelElement _infoLabel; @@ -64,8 +67,8 @@ namespace FlaxEditor.Options { var themeOptions = (ThemeOptions)ParentEditor.Values[0]; var options = new string[themeOptions.Styles.Count + 2]; - options[0] = "Default"; - options[1] = "LightDefault"; + options[0] = DefaultName; + options[1] = LightDefault; int i = 0; foreach (var styleName in themeOptions.Styles.Keys) diff --git a/Source/Editor/Windows/Profiler/Assets.cs b/Source/Editor/Windows/Profiler/Assets.cs index b8b898246..536d65a74 100644 --- a/Source/Editor/Windows/Profiler/Assets.cs +++ b/Source/Editor/Windows/Profiler/Assets.cs @@ -62,8 +62,9 @@ namespace FlaxEditor.Windows.Profiler _memoryUsageChart.SelectedSampleChanged += OnSelectedSampleChanged; // Table - var headerColor = Style.Current.LightBackground; - var textColor = Style.Current.Foreground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; _table = new Table { Columns = new[] diff --git a/Source/Editor/Windows/Profiler/CPU.cs b/Source/Editor/Windows/Profiler/CPU.cs index df4d5b59b..0cbb3fa9b 100644 --- a/Source/Editor/Windows/Profiler/CPU.cs +++ b/Source/Editor/Windows/Profiler/CPU.cs @@ -92,8 +92,9 @@ namespace FlaxEditor.Windows.Profiler }; // Table - var headerColor = Style.Current.LightBackground; - var textColor = Style.Current.Foreground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; _table = new Table { Columns = new[] diff --git a/Source/Editor/Windows/Profiler/GPU.cs b/Source/Editor/Windows/Profiler/GPU.cs index e6a93df72..d2c34d335 100644 --- a/Source/Editor/Windows/Profiler/GPU.cs +++ b/Source/Editor/Windows/Profiler/GPU.cs @@ -63,8 +63,9 @@ namespace FlaxEditor.Windows.Profiler }; // Table - var headerColor = Style.Current.LightBackground; - var textColor = Style.Current.Foreground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; _table = new Table { Columns = new[] diff --git a/Source/Editor/Windows/Profiler/MemoryGPU.cs b/Source/Editor/Windows/Profiler/MemoryGPU.cs index e894ef0e2..e7c085362 100644 --- a/Source/Editor/Windows/Profiler/MemoryGPU.cs +++ b/Source/Editor/Windows/Profiler/MemoryGPU.cs @@ -63,8 +63,9 @@ namespace FlaxEditor.Windows.Profiler _memoryUsageChart.SelectedSampleChanged += OnSelectedSampleChanged; // Table - var headerColor = Style.Current.LightBackground; - var textColor = Style.Current.Foreground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; _table = new Table { Columns = new[] diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs index 669cf4a65..1ac9777c1 100644 --- a/Source/Editor/Windows/Profiler/Network.cs +++ b/Source/Editor/Windows/Profiler/Network.cs @@ -252,8 +252,9 @@ namespace FlaxEditor.Windows.Profiler private static Table InitTable(ContainerControl parent, string name) { - var headerColor = Style.Current.LightBackground; - var textColor = Style.Current.Foreground; + var style = Style.Current; + var headerColor = style.LightBackground; + var textColor = style.Foreground; var table = new Table { Columns = new[] diff --git a/Source/Editor/Windows/Profiler/SingleChart.cs b/Source/Editor/Windows/Profiler/SingleChart.cs index 29780150a..4f36692e5 100644 --- a/Source/Editor/Windows/Profiler/SingleChart.cs +++ b/Source/Editor/Windows/Profiler/SingleChart.cs @@ -105,7 +105,7 @@ namespace FlaxEditor.Windows.Profiler if (_selectedSampleIndex != -1) { float selectedX = Width - (_samples.Count - _selectedSampleIndex - 1) * PointsOffset; - Render2D.DrawLine(new Float2(selectedX, 0), new Float2(selectedX, chartHeight), Style.Current.Foreground, 1.5f); + Render2D.DrawLine(new Float2(selectedX, 0), new Float2(selectedX, chartHeight), style.Foreground, 1.5f); } int samplesInViewCount = Math.Min((int)(Width / PointsOffset), _samples.Count) - 1; @@ -138,8 +138,8 @@ namespace FlaxEditor.Windows.Profiler var headerRect = new Rectangle(0, chartHeight, Width, TitleHeight); var headerTextRect = new Rectangle(2, chartHeight, Width - 4, TitleHeight); Render2D.FillRectangle(headerRect, style.BackgroundNormal); - Render2D.DrawText(style.FontMedium, Title, headerTextRect, Style.Current.ForegroundGrey, TextAlignment.Near, TextAlignment.Center); - Render2D.DrawText(style.FontMedium, _sample, headerTextRect, Style.Current.Foreground, TextAlignment.Far, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, Title, headerTextRect, style.ForegroundGrey, TextAlignment.Near, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, _sample, headerTextRect, style.Foreground, TextAlignment.Far, TextAlignment.Center); } private void OnClick(ref Float2 location) From a9e1568edc7a6276475926bfdc3941dc8380891e Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Mon, 13 Nov 2023 14:26:42 -0400 Subject: [PATCH 274/546] Auto WakeUp rigidbodies when set "isKinematic" to false --- Source/Engine/Physics/Actors/RigidBody.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Physics/Actors/RigidBody.cpp b/Source/Engine/Physics/Actors/RigidBody.cpp index a090104ff..da8006c7d 100644 --- a/Source/Engine/Physics/Actors/RigidBody.cpp +++ b/Source/Engine/Physics/Actors/RigidBody.cpp @@ -34,8 +34,12 @@ void RigidBody::SetIsKinematic(const bool value) if (value == GetIsKinematic()) return; _isKinematic = value; - if (_actor) + if (_actor && _isActive) + { PhysicsBackend::SetRigidDynamicActorFlag(_actor, PhysicsBackend::RigidDynamicFlags::Kinematic, value); + if (!value) + WakeUp(); + } } void RigidBody::SetLinearDamping(float value) From 4ceed361e254c155dfd2a79f73bfaad3ae3f2c81 Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Mon, 13 Nov 2023 14:28:16 -0400 Subject: [PATCH 275/546] small fix --- Source/Engine/Physics/Actors/RigidBody.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Physics/Actors/RigidBody.cpp b/Source/Engine/Physics/Actors/RigidBody.cpp index da8006c7d..4aaf3d285 100644 --- a/Source/Engine/Physics/Actors/RigidBody.cpp +++ b/Source/Engine/Physics/Actors/RigidBody.cpp @@ -34,10 +34,10 @@ void RigidBody::SetIsKinematic(const bool value) if (value == GetIsKinematic()) return; _isKinematic = value; - if (_actor && _isActive) + if (_actor) { PhysicsBackend::SetRigidDynamicActorFlag(_actor, PhysicsBackend::RigidDynamicFlags::Kinematic, value); - if (!value) + if (!value && _isActive) WakeUp(); } } From eaafb72ca935d090e9d23ed289e1a904dd848dea Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 13 Nov 2023 23:54:07 +0100 Subject: [PATCH 276/546] Optimize some includes and use automatic serializers for platform settings --- .../Engine/Core/Config/PlatformSettingsBase.h | 2 +- .../Engine/Graphics/Materials/MaterialInfo.h | 1 - .../DirectX/DX11/GPUDeviceDX11.h | 1 + .../DirectX/DX12/GPUPipelineStateDX12.h | 1 + .../Android/AndroidPlatformSettings.h | 17 +++---------- .../Platform/Apple/ApplePlatformSettings.h | 9 +------ .../Engine/Platform/GDK/GDKPlatformSettings.h | 25 ++----------------- .../Platform/Linux/LinuxPlatformSettings.h | 19 ++------------ .../Engine/Platform/Mac/MacPlatformSettings.h | 13 +--------- .../Engine/Platform/UWP/UWPPlatformSettings.h | 18 ++----------- .../Windows/WindowsPlatformSettings.h | 22 ++-------------- .../Engine/Platform/iOS/iOSPlatformSettings.h | 12 +-------- Source/Engine/Serialization/Serialization.h | 1 + .../Engine/Serialization/SerializationFwd.h | 3 ++- 14 files changed, 20 insertions(+), 124 deletions(-) diff --git a/Source/Engine/Core/Config/PlatformSettingsBase.h b/Source/Engine/Core/Config/PlatformSettingsBase.h index 6d7e8601e..b372e3dd5 100644 --- a/Source/Engine/Core/Config/PlatformSettingsBase.h +++ b/Source/Engine/Core/Config/PlatformSettingsBase.h @@ -3,7 +3,7 @@ #pragma once #include "Engine/Core/Config/Settings.h" -#include "Engine/Serialization/Serialization.h" +#include "Engine/Serialization/SerializationFwd.h" /// /// Specifies the display mode of a game window. diff --git a/Source/Engine/Graphics/Materials/MaterialInfo.h b/Source/Engine/Graphics/Materials/MaterialInfo.h index 8e4933150..afa23a6cc 100644 --- a/Source/Engine/Graphics/Materials/MaterialInfo.h +++ b/Source/Engine/Graphics/Materials/MaterialInfo.h @@ -3,7 +3,6 @@ #pragma once #include "../Enums.h" -#include "Engine/Core/Math/Math.h" /// /// Material domain type. Material domain defines the target usage of the material shader. diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h index 6205a628d..c0e0d8fd5 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h +++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUDeviceDX11.h @@ -4,6 +4,7 @@ #include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/GPUResource.h" +#include "Engine/Core/Collections/Dictionary.h" #include "../GPUDeviceDX.h" #include "../IncludeDirectXHeaders.h" diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h index 2764ce235..27aba0c4b 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h +++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUPipelineStateDX12.h @@ -7,6 +7,7 @@ #include "Engine/Graphics/GPUPipelineState.h" #include "GPUDeviceDX12.h" #include "Types.h" +#include "Engine/Core/Collections/Dictionary.h" #include "../IncludeDirectXHeaders.h" class GPUTextureViewDX12; diff --git a/Source/Engine/Platform/Android/AndroidPlatformSettings.h b/Source/Engine/Platform/Android/AndroidPlatformSettings.h index 0a877fc18..d5e7190f2 100644 --- a/Source/Engine/Platform/Android/AndroidPlatformSettings.h +++ b/Source/Engine/Platform/Android/AndroidPlatformSettings.h @@ -6,16 +6,15 @@ #include "Engine/Core/Config/PlatformSettingsBase.h" #include "Engine/Scripting/SoftObjectReference.h" - -class Texture; +#include "Engine/Content/Assets/Texture.h" /// /// Android platform settings. /// API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API AndroidPlatformSettings : public SettingsBase { -DECLARE_SCRIPTING_TYPE_MINIMAL(AndroidPlatformSettings); -public: + DECLARE_SCRIPTING_TYPE_MINIMAL(AndroidPlatformSettings); + API_AUTO_SERIALIZATION(); /// /// The application package name (eg. com.company.product). Custom tokens: ${PROJECT_NAME}, ${COMPANY_NAME}. @@ -35,20 +34,10 @@ public: API_FIELD(Attributes="EditorOrder(1030), EditorDisplay(\"Other\")") SoftObjectReference OverrideIcon; -public: - /// /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. /// static AndroidPlatformSettings* Get(); - - // [SettingsBase] - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override - { - DESERIALIZE(PackageName); - DESERIALIZE(Permissions); - DESERIALIZE(OverrideIcon); - } }; #if PLATFORM_ANDROID diff --git a/Source/Engine/Platform/Apple/ApplePlatformSettings.h b/Source/Engine/Platform/Apple/ApplePlatformSettings.h index 57ad58449..a03cdd630 100644 --- a/Source/Engine/Platform/Apple/ApplePlatformSettings.h +++ b/Source/Engine/Platform/Apple/ApplePlatformSettings.h @@ -16,6 +16,7 @@ class Texture; API_CLASS(Abstract, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API ApplePlatformSettings : public SettingsBase { DECLARE_SCRIPTING_TYPE_MINIMAL(ApplePlatformSettings); + API_AUTO_SERIALIZATION(); /// /// The app identifier (reversed DNS, eg. com.company.product). Custom tokens: ${PROJECT_NAME}, ${COMPANY_NAME}. @@ -28,14 +29,6 @@ API_CLASS(Abstract, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_AP /// API_FIELD(Attributes="EditorOrder(1000), EditorDisplay(\"Other\")") SoftObjectReference OverrideIcon; - -public: - // [SettingsBase] - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override - { - DESERIALIZE(AppIdentifier); - DESERIALIZE(OverrideIcon); - } }; #endif diff --git a/Source/Engine/Platform/GDK/GDKPlatformSettings.h b/Source/Engine/Platform/GDK/GDKPlatformSettings.h index 88822047d..1827427c8 100644 --- a/Source/Engine/Platform/GDK/GDKPlatformSettings.h +++ b/Source/Engine/Platform/GDK/GDKPlatformSettings.h @@ -16,7 +16,8 @@ class Texture; API_CLASS(Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API GDKPlatformSettings : public SettingsBase { DECLARE_SCRIPTING_TYPE_MINIMAL(GDKPlatformSettings); -public: + API_AUTO_SERIALIZATION(); + /// /// Game identity name stored in game package manifest (for store). If empty the product name will be used from Game Settings. /// @@ -118,28 +119,6 @@ public: /// API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Media Capture\")") bool BlockGameDVR = false; - -public: - // [SettingsBase] - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override - { - DESERIALIZE(Name); - DESERIALIZE(PublisherName); - DESERIALIZE(PublisherDisplayName); - DESERIALIZE(Square150x150Logo); - DESERIALIZE(Square480x480Logo); - DESERIALIZE(Square44x44Logo); - DESERIALIZE(SplashScreenImage); - DESERIALIZE(StoreLogo); - DESERIALIZE(BackgroundColor); - DESERIALIZE(TitleId); - DESERIALIZE(StoreId); - DESERIALIZE(RequiresXboxLive); - DESERIALIZE(SCID); - DESERIALIZE(GameDVRSystemComponent); - DESERIALIZE(BlockBroadcast); - DESERIALIZE(BlockGameDVR); - } }; #endif diff --git a/Source/Engine/Platform/Linux/LinuxPlatformSettings.h b/Source/Engine/Platform/Linux/LinuxPlatformSettings.h index 8384b3d51..429149367 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatformSettings.h +++ b/Source/Engine/Platform/Linux/LinuxPlatformSettings.h @@ -14,8 +14,8 @@ class Texture; /// API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API LinuxPlatformSettings : public SettingsBase { -DECLARE_SCRIPTING_TYPE_MINIMAL(LinuxPlatformSettings); -public: + DECLARE_SCRIPTING_TYPE_MINIMAL(LinuxPlatformSettings); + API_AUTO_SERIALIZATION(); /// /// The default game window mode. @@ -65,25 +65,10 @@ public: API_FIELD(Attributes="EditorOrder(2000), DefaultValue(true), EditorDisplay(\"Graphics\")") bool SupportVulkan = true; -public: - /// /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. /// static LinuxPlatformSettings* Get(); - - // [SettingsBase] - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override - { - DESERIALIZE(WindowMode); - DESERIALIZE(ScreenWidth); - DESERIALIZE(ScreenHeight); - DESERIALIZE(RunInBackground); - DESERIALIZE(ResizableWindow); - DESERIALIZE(ForceSingleInstance); - DESERIALIZE(OverrideIcon); - DESERIALIZE(SupportVulkan); - } }; #if PLATFORM_LINUX diff --git a/Source/Engine/Platform/Mac/MacPlatformSettings.h b/Source/Engine/Platform/Mac/MacPlatformSettings.h index d3ef21fc4..c7e334bcd 100644 --- a/Source/Engine/Platform/Mac/MacPlatformSettings.h +++ b/Source/Engine/Platform/Mac/MacPlatformSettings.h @@ -12,6 +12,7 @@ API_CLASS(Sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API MacPlatformSettings : public ApplePlatformSettings { DECLARE_SCRIPTING_TYPE_MINIMAL(MacPlatformSettings); + API_AUTO_SERIALIZATION(); /// /// The default game window mode. @@ -43,22 +44,10 @@ API_CLASS(Sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API API_FIELD(Attributes="EditorOrder(1010), EditorDisplay(\"Other\", \"Run In Background\")") bool RunInBackground = false; -public: /// /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. /// static MacPlatformSettings* Get(); - - // [SettingsBase] - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override - { - ApplePlatformSettings::Deserialize(stream, modifier); - DESERIALIZE(WindowMode); - DESERIALIZE(ScreenWidth); - DESERIALIZE(ScreenHeight); - DESERIALIZE(ResizableWindow); - DESERIALIZE(RunInBackground); - } }; #if PLATFORM_MAC diff --git a/Source/Engine/Platform/UWP/UWPPlatformSettings.h b/Source/Engine/Platform/UWP/UWPPlatformSettings.h index 98a2fedce..2c67898bf 100644 --- a/Source/Engine/Platform/UWP/UWPPlatformSettings.h +++ b/Source/Engine/Platform/UWP/UWPPlatformSettings.h @@ -11,8 +11,8 @@ /// API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API UWPPlatformSettings : public SettingsBase { -DECLARE_SCRIPTING_TYPE_MINIMAL(UWPPlatformSettings); -public: + DECLARE_SCRIPTING_TYPE_MINIMAL(UWPPlatformSettings); + API_AUTO_SERIALIZATION(); /// /// The preferred launch windowing mode. @@ -66,8 +66,6 @@ public: All = Landscape | LandscapeFlipped | Portrait | PortraitFlipped }; -public: - /// /// The preferred launch windowing mode. Always fullscreen on Xbox. /// @@ -98,22 +96,10 @@ public: API_FIELD(Attributes="EditorOrder(2010), DefaultValue(false), EditorDisplay(\"Graphics\", \"Support DirectX 10\")") bool SupportDX10 = false; -public: - /// /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. /// static UWPPlatformSettings* Get(); - - // [SettingsBase] - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override - { - DESERIALIZE(PreferredLaunchWindowingMode); - DESERIALIZE(AutoRotationPreferences); - DESERIALIZE(CertificateLocation); - DESERIALIZE(SupportDX11); - DESERIALIZE(SupportDX10); - } }; #if PLATFORM_UWP diff --git a/Source/Engine/Platform/Windows/WindowsPlatformSettings.h b/Source/Engine/Platform/Windows/WindowsPlatformSettings.h index 4c380fda3..02bfc2bc6 100644 --- a/Source/Engine/Platform/Windows/WindowsPlatformSettings.h +++ b/Source/Engine/Platform/Windows/WindowsPlatformSettings.h @@ -14,8 +14,8 @@ class Texture; /// API_CLASS(sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API WindowsPlatformSettings : public SettingsBase { -DECLARE_SCRIPTING_TYPE_MINIMAL(WindowsPlatformSettings); -public: + DECLARE_SCRIPTING_TYPE_MINIMAL(WindowsPlatformSettings); + API_AUTO_SERIALIZATION(); /// /// The default game window mode. @@ -83,28 +83,10 @@ public: API_FIELD(Attributes="EditorOrder(2030), DefaultValue(false), EditorDisplay(\"Graphics\")") bool SupportVulkan = false; -public: - /// /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. /// static WindowsPlatformSettings* Get(); - - // [SettingsBase] - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override - { - DESERIALIZE(WindowMode); - DESERIALIZE(ScreenWidth); - DESERIALIZE(ScreenHeight); - DESERIALIZE(RunInBackground); - DESERIALIZE(ResizableWindow); - DESERIALIZE(ForceSingleInstance); - DESERIALIZE(OverrideIcon); - DESERIALIZE(SupportDX12); - DESERIALIZE(SupportDX11); - DESERIALIZE(SupportDX10); - DESERIALIZE(SupportVulkan); - } }; #if PLATFORM_WINDOWS diff --git a/Source/Engine/Platform/iOS/iOSPlatformSettings.h b/Source/Engine/Platform/iOS/iOSPlatformSettings.h index 53218e0f6..9eed39f41 100644 --- a/Source/Engine/Platform/iOS/iOSPlatformSettings.h +++ b/Source/Engine/Platform/iOS/iOSPlatformSettings.h @@ -12,6 +12,7 @@ API_CLASS(Sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API iOSPlatformSettings : public ApplePlatformSettings { DECLARE_SCRIPTING_TYPE_MINIMAL(ApplePlatformSettings); + API_AUTO_SERIALIZATION(); /// /// The app export destination methods. @@ -79,17 +80,6 @@ API_CLASS(Sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. /// static iOSPlatformSettings* Get(); - - // [SettingsBase] - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) final override - { - ApplePlatformSettings::Deserialize(stream, modifier); - DESERIALIZE(AppTeamId); - DESERIALIZE(AppVersion); - DESERIALIZE(ExportMethod); - DESERIALIZE(SupportedInterfaceOrientationsiPhone); - DESERIALIZE(SupportedInterfaceOrientationsiPad); - } }; #if PLATFORM_IOS diff --git a/Source/Engine/Serialization/Serialization.h b/Source/Engine/Serialization/Serialization.h index f9dc0870a..ed1008922 100644 --- a/Source/Engine/Serialization/Serialization.h +++ b/Source/Engine/Serialization/Serialization.h @@ -3,6 +3,7 @@ #pragma once #include "SerializationFwd.h" +#include "ISerializeModifier.h" #include "Engine/Core/Collections/Array.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Scripting/ScriptingObject.h" diff --git a/Source/Engine/Serialization/SerializationFwd.h b/Source/Engine/Serialization/SerializationFwd.h index 6315ec058..2ac7ae1de 100644 --- a/Source/Engine/Serialization/SerializationFwd.h +++ b/Source/Engine/Serialization/SerializationFwd.h @@ -3,10 +3,11 @@ #pragma once #include "Engine/Core/ISerializable.h" -#include "ISerializeModifier.h" #include "Json.h" #include "JsonWriter.h" +class ISerializeModifier; + // The floating-point values serialization epsilon for equality checks precision #define SERIALIZE_EPSILON 1e-7f #define SERIALIZE_EPSILON_DOUBLE 1e-17 From 05ea803582a00cd407eec7d6497c57b520a7b7f0 Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Mon, 13 Nov 2023 20:05:05 -0400 Subject: [PATCH 277/546] add check for GetStartAwake --- Source/Engine/Physics/Actors/RigidBody.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Physics/Actors/RigidBody.cpp b/Source/Engine/Physics/Actors/RigidBody.cpp index 4aaf3d285..afbac6611 100644 --- a/Source/Engine/Physics/Actors/RigidBody.cpp +++ b/Source/Engine/Physics/Actors/RigidBody.cpp @@ -37,7 +37,7 @@ void RigidBody::SetIsKinematic(const bool value) if (_actor) { PhysicsBackend::SetRigidDynamicActorFlag(_actor, PhysicsBackend::RigidDynamicFlags::Kinematic, value); - if (!value && _isActive) + if (!value && _isActive && _startAwake) WakeUp(); } } From ab5534da7f4e08f26de5dcb53eb6b1e5e9502369 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 13 Nov 2023 20:38:04 -0600 Subject: [PATCH 278/546] Add limits to cloth brush values. --- Source/Editor/Tools/ClothPainting.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Editor/Tools/ClothPainting.cs b/Source/Editor/Tools/ClothPainting.cs index f12fca9db..28536ea83 100644 --- a/Source/Editor/Tools/ClothPainting.cs +++ b/Source/Editor/Tools/ClothPainting.cs @@ -18,21 +18,25 @@ namespace FlaxEngine.Tools /// /// Brush radius (world-space). /// + [Limit(0)] public float BrushSize = 50.0f; /// /// Brush paint intensity. /// + [Limit(0)] public float BrushStrength = 2.0f; /// /// Brush paint falloff. Hardens or softens painting. /// + [Limit(0)] public float BrushFalloff = 1.5f; /// /// Value to paint with. Hold Ctrl hey to paint with inverse value (1 - value). /// + [Limit(0, 1, 0.01f)] public float PaintValue = 0.0f; /// From f7fb366233e7dbb57202020eddea394a77caa1fb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 14 Nov 2023 10:31:11 +0100 Subject: [PATCH 279/546] Minor doc updates --- Source/Engine/Level/Actors/DirectionalLight.h | 2 +- Source/Engine/Level/Actors/Light.h | 10 +++++----- Source/Engine/Renderer/Lightmaps.h | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Level/Actors/DirectionalLight.h b/Source/Engine/Level/Actors/DirectionalLight.h index 3f57e74bb..d5f31324a 100644 --- a/Source/Engine/Level/Actors/DirectionalLight.h +++ b/Source/Engine/Level/Actors/DirectionalLight.h @@ -13,7 +13,7 @@ class FLAXENGINE_API DirectionalLight : public LightWithShadow DECLARE_SCENE_OBJECT(DirectionalLight); public: /// - /// The number of cascades used for slicing the range of depth covered by the light. Values are 1, 2 or 4 cascades; a typical scene uses 4 cascades. + /// The number of cascades used for slicing the range of depth covered by the light during shadow rendering. Values are 1, 2 or 4 cascades; a typical scene uses 4 cascades. /// API_FIELD(Attributes="EditorOrder(65), DefaultValue(4), Limit(1, 4), EditorDisplay(\"Shadow\")") int32 CascadeCount = 4; diff --git a/Source/Engine/Level/Actors/Light.h b/Source/Engine/Level/Actors/Light.h index 86ca59ef5..efb137b60 100644 --- a/Source/Engine/Level/Actors/Light.h +++ b/Source/Engine/Level/Actors/Light.h @@ -29,7 +29,7 @@ public: float Brightness = 3.14f; /// - /// Controls light visibility range. The distance at which the light be completely faded. Use value 0 to always draw light. + /// Controls light visibility range. The distance at which the light becomes completely faded. Use a value of 0 to always draw light. /// API_FIELD(Attributes="EditorOrder(35), Limit(0, float.MaxValue, 10.0f), EditorDisplay(\"Light\")") float ViewDistance = 0.0f; @@ -87,19 +87,19 @@ public: float MinRoughness = 0.04f; /// - /// The light shadows casting distance from view. + /// Shadows casting distance from view. /// API_FIELD(Attributes="EditorOrder(80), EditorDisplay(\"Shadow\", \"Distance\"), Limit(0, 1000000)") float ShadowsDistance = 5000.0f; /// - /// The light shadows fade off distance + /// Shadows fade off distance. /// API_FIELD(Attributes="EditorOrder(90), EditorDisplay(\"Shadow\", \"Fade Distance\"), Limit(0.0f, 10000.0f, 0.1f)") float ShadowsFadeDistance = 500.0f; /// - /// The light shadows edges sharpness + /// TheShadows edges sharpness. /// API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Shadow\", \"Sharpness\"), Limit(1.0f, 10.0f, 0.001f)") float ShadowsSharpness = 1.0f; @@ -129,7 +129,7 @@ public: float ContactShadowsLength = 0.0f; /// - /// Shadows casting mode by this visual element + /// Describes how a visual element casts shadows. /// API_FIELD(Attributes="EditorOrder(60), EditorDisplay(\"Shadow\", \"Mode\")") ShadowsCastingMode ShadowsMode = ShadowsCastingMode::All; diff --git a/Source/Engine/Renderer/Lightmaps.h b/Source/Engine/Renderer/Lightmaps.h index ee3a8ab39..ee4b56495 100644 --- a/Source/Engine/Renderer/Lightmaps.h +++ b/Source/Engine/Renderer/Lightmaps.h @@ -108,19 +108,19 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(LightmapSettings); }; /// - /// Controls how much all lights will contribute indirect lighting. + /// Controls how much all lights will contribute to indirect lighting. /// API_FIELD(Attributes="EditorOrder(0), Limit(0, 100.0f, 0.1f)") float IndirectLightingIntensity = 1.0f; /// - /// Global scale for objects in lightmap to increase quality + /// Global scale for objects in the lightmap to increase quality /// API_FIELD(Attributes="EditorOrder(10), Limit(0, 100.0f, 0.1f)") float GlobalObjectsScale = 1.0f; /// - /// Amount of pixels space between charts in lightmap atlas + /// Amount of pixel space between charts in lightmap atlas /// API_FIELD(Attributes="EditorOrder(20), Limit(0, 16, 0.1f)") int32 ChartsPadding = 3; From c0f0bd87aa8a1c5efca38d90a1df1154f26b449e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 14 Nov 2023 10:33:40 +0100 Subject: [PATCH 280/546] Fix error in `Tabs.SelectedTab` #1932 --- Source/Editor/GUI/Tabs/Tabs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/GUI/Tabs/Tabs.cs b/Source/Editor/GUI/Tabs/Tabs.cs index b5e2cfe39..3c70363e7 100644 --- a/Source/Editor/GUI/Tabs/Tabs.cs +++ b/Source/Editor/GUI/Tabs/Tabs.cs @@ -239,7 +239,7 @@ namespace FlaxEditor.GUI.Tabs /// public Tab SelectedTab { - get => _selectedIndex == -1 && Children.Count > _selectedIndex + 1 ? null : Children[_selectedIndex + 1] as Tab; + get => _selectedIndex < 0 || Children.Count <= _selectedIndex ? null : Children[_selectedIndex + 1] as Tab; set => SelectedTabIndex = value != null ? Children.IndexOf(value) - 1 : -1; } From 7c53b1e99ad1a4b424d4595009f0ac6832382d49 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 14 Nov 2023 11:22:17 +0100 Subject: [PATCH 281/546] Add clearing BT memory in non-release builds to make issues spotting easier --- Source/Engine/AI/BehaviorKnowledge.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Engine/AI/BehaviorKnowledge.cpp b/Source/Engine/AI/BehaviorKnowledge.cpp index a33738d31..ffc011818 100644 --- a/Source/Engine/AI/BehaviorKnowledge.cpp +++ b/Source/Engine/AI/BehaviorKnowledge.cpp @@ -150,7 +150,13 @@ void BehaviorKnowledge::InitMemory(BehaviorTree* tree) RelevantNodes.Resize(tree->Graph.NodesCount, false); RelevantNodes.SetAll(false); if (!Memory && tree->Graph.NodesStatesSize) + { Memory = Allocator::Allocate(tree->Graph.NodesStatesSize); +#if !BUILD_RELEASE + // Clear memory to make it easier to spot missing data issues (eg. zero GCHandle in C# BT node due to missing state init) + Platform::MemoryClear(Memory, tree->Graph.NodesStatesSize); +#endif + } } void BehaviorKnowledge::FreeMemory() From e0de6744e2aebd74ae133292ed9813c0afb545f3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 14 Nov 2023 11:22:41 +0100 Subject: [PATCH 282/546] Add better errors logging to BT nodes in case of issues --- Source/Engine/AI/BehaviorTree.cs | 16 +++++++++++----- Source/Engine/AI/BehaviorTreeNodes.cpp | 2 ++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Source/Engine/AI/BehaviorTree.cs b/Source/Engine/AI/BehaviorTree.cs index 699c72976..a1c863e6b 100644 --- a/Source/Engine/AI/BehaviorTree.cs +++ b/Source/Engine/AI/BehaviorTree.cs @@ -95,12 +95,16 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref T GetState(IntPtr memory) where T : struct { - var ptr = IntPtr.Add(memory, _memoryOffset).ToPointer(); - var handle = GCHandle.FromIntPtr(Unsafe.Read(ptr)); + var ptr = Unsafe.Read(IntPtr.Add(memory, _memoryOffset).ToPointer()); +#if !BUILD_RELEASE + if (ptr == IntPtr.Zero) + throw new Exception($"Missing state '{typeof(T).FullName}' for node '{GetType().FullName}'"); +#endif + var handle = GCHandle.FromIntPtr(ptr); var state = handle.Target; #if !BUILD_RELEASE if (state == null) - throw new NullReferenceException(); + throw new Exception($"Missing state '{typeof(T).FullName}' for node '{GetType().FullName}'"); #endif return ref Unsafe.Unbox(state); } @@ -111,8 +115,10 @@ namespace FlaxEngine [MethodImpl(MethodImplOptions.AggressiveInlining)] public void FreeState(IntPtr memory) { - var ptr = IntPtr.Add(memory, _memoryOffset).ToPointer(); - var handle = GCHandle.FromIntPtr(Unsafe.Read(ptr)); + var ptr = Unsafe.Read(IntPtr.Add(memory, _memoryOffset).ToPointer()); + if (ptr == IntPtr.Zero) + return; + var handle = GCHandle.FromIntPtr(ptr); handle.Free(); } } diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp index 4be336b35..fb49a007e 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.cpp +++ b/Source/Engine/AI/BehaviorTreeNodes.cpp @@ -85,6 +85,8 @@ BehaviorUpdateResult BehaviorTreeNode::InvokeUpdate(const BehaviorUpdateContext& result = BehaviorUpdateResult::Failed; else result = Update(context); + if ((int32)result < 0 || (int32)result > (int32)BehaviorUpdateResult::Failed) + result = BehaviorUpdateResult::Failed; // Invalid value is a failure // Post-process result from decorators for (BehaviorTreeDecorator* decorator : _decorators) From 3320c76e141965cb035da42371c39c7c8cd2438d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 14 Nov 2023 11:23:15 +0100 Subject: [PATCH 283/546] Add soft check for null managed object value for unboxing --- Source/Engine/Scripting/ManagedCLR/MUtils.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.h b/Source/Engine/Scripting/ManagedCLR/MUtils.h index a9b2d8414..9a79a83c9 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.h +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.h @@ -64,8 +64,8 @@ struct MConverter, TNot& data) From 0360f7786d65c68c02a3948aec5760c369fe8b3d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 14 Nov 2023 11:47:44 +0100 Subject: [PATCH 284/546] Fix crash hen reading`BehaviorKnowledgeSelector` value in C# when type doesn't match exactly --- Source/Engine/AI/BehaviorKnowledgeSelector.cs | 4 +- Source/Engine/Utilities/VariantUtils.cs | 73 ++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/Source/Engine/AI/BehaviorKnowledgeSelector.cs b/Source/Engine/AI/BehaviorKnowledgeSelector.cs index 84e923ccf..5c642e92a 100644 --- a/Source/Engine/AI/BehaviorKnowledgeSelector.cs +++ b/Source/Engine/AI/BehaviorKnowledgeSelector.cs @@ -202,7 +202,7 @@ namespace FlaxEngine public T Get(BehaviorKnowledge knowledge) { if (knowledge != null && knowledge.Get(Path, out var value)) - return (T)value; + return Utilities.VariantUtils.Cast(value); return default; } @@ -218,7 +218,7 @@ namespace FlaxEngine object tmp = null; bool result = knowledge != null && knowledge.Get(Path, out tmp); if (result) - value = (T)tmp; + value = Utilities.VariantUtils.Cast(tmp); return result; } diff --git a/Source/Engine/Utilities/VariantUtils.cs b/Source/Engine/Utilities/VariantUtils.cs index 465a72f04..81520ae1b 100644 --- a/Source/Engine/Utilities/VariantUtils.cs +++ b/Source/Engine/Utilities/VariantUtils.cs @@ -8,7 +8,6 @@ using System.IO; using FlaxEditor.Scripting; using FlaxEditor.Utilities; #endif -using FlaxEngine; using Newtonsoft.Json; namespace FlaxEngine.Utilities @@ -18,6 +17,78 @@ namespace FlaxEngine.Utilities /// public static class VariantUtils { + /// + /// Casts the generic value to a given type. Matches native Variant casting logic that favors returning null/zero value rather than error. + /// + /// The destination value type. + /// The input value to cast. + /// The result value. + internal static T Cast(object value) + { + if (value == null) + return default; + var type = value.GetType(); + if (type != typeof(T)) + { + if (typeof(T) == typeof(Vector2)) + { + if (value is Float2 asFloat2) + return (T)(object)new Vector2(asFloat2.X, asFloat2.Y); + if (value is Float3 asFloat3) + return (T)(object)new Vector2(asFloat3.X, asFloat3.Y); + if (value is Float4 asFloat4) + return (T)(object)new Vector2(asFloat4.X, asFloat4.Y); + } + else if (typeof(T) == typeof(Vector3)) + { + if (value is Float2 asFloat2) + return (T)(object)new Vector3(asFloat2.X, asFloat2.Y, 0); + if (value is Float3 asFloat3) + return (T)(object)new Vector3(asFloat3.X, asFloat3.Y, asFloat3.Z); + if (value is Float4 asFloat4) + return (T)(object)new Vector3(asFloat4.X, asFloat4.Y, asFloat4.Z); + } + else if (typeof(T) == typeof(Vector4)) + { + if (value is Float2 asFloat2) + return (T)(object)new Vector4(asFloat2.X, asFloat2.Y, 0, 0); + if (value is Float3 asFloat3) + return (T)(object)new Vector4(asFloat3.X, asFloat3.Y, asFloat3.Z, 0); + if (value is Vector4 asFloat4) + return (T)(object)new Vector4(asFloat4.X, asFloat4.Y, asFloat4.Z, asFloat4.W); + } + else if (typeof(T) == typeof(Float2)) + { + if (value is Vector2 asVector2) + return (T)(object)new Float2(asVector2.X, asVector2.Y); + if (value is Vector3 asVector3) + return (T)(object)new Float2(asVector3.X, asVector3.Y); + if (value is Vector4 asVector4) + return (T)(object)new Float2(asVector4.X, asVector4.Y); + } + else if (typeof(T) == typeof(Float3)) + { + if (value is Vector2 asVector2) + return (T)(object)new Float3(asVector2.X, asVector2.Y, 0); + if (value is Vector3 asVector3) + return (T)(object)new Float3(asVector3.X, asVector3.Y, asVector3.Z); + if (value is Vector4 asFloat4) + return (T)(object)new Float3(asFloat4.X, asFloat4.Y, asFloat4.Z); + } + else if (typeof(T) == typeof(Float4)) + { + if (value is Vector2 asVector2) + return (T)(object)new Float4(asVector2.X, asVector2.Y, 0, 0); + if (value is Vector3 asVector3) + return (T)(object)new Float4(asVector3.X, asVector3.Y, asVector3.Z, 0); + if (value is Vector4 asVector4) + return (T)(object)new Float4(asVector4.X, asVector4.Y, asVector4.Z, asVector4.W); + } + return (T)Convert.ChangeType(value, typeof(T)); + } + return (T)value; + } + internal enum VariantType { Null = 0, From 6fd34bf5cc50769f28f5acad921469a3b4e418d2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 14 Nov 2023 12:15:51 +0100 Subject: [PATCH 285/546] Fix compilation regression --- .../Platform/Android/AndroidPlatformSettings.h | 3 ++- .../Flax.Build/Bindings/BindingsGenerator.Cpp.cs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Platform/Android/AndroidPlatformSettings.h b/Source/Engine/Platform/Android/AndroidPlatformSettings.h index d5e7190f2..9cfe7f880 100644 --- a/Source/Engine/Platform/Android/AndroidPlatformSettings.h +++ b/Source/Engine/Platform/Android/AndroidPlatformSettings.h @@ -6,7 +6,8 @@ #include "Engine/Core/Config/PlatformSettingsBase.h" #include "Engine/Scripting/SoftObjectReference.h" -#include "Engine/Content/Assets/Texture.h" + +class Texture; /// /// Android platform settings. diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 10e4d5846..991ff196b 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -1793,6 +1793,18 @@ namespace Flax.Build.Bindings var apiTypeInfo = FindApiTypeInfo(buildData, typeInfo, caller); if (apiTypeInfo != null && apiTypeInfo.IsInterface) return true; + + // Add includes to properly compile bindings (eg. SoftObjectReference) + CppReferencesFiles.Add(apiTypeInfo?.File); + if (typeInfo.GenericArgs != null) + { + for (int i = 0; i < typeInfo.GenericArgs.Count; i++) + { + var t = FindApiTypeInfo(buildData, typeInfo.GenericArgs[i], caller); + CppReferencesFiles.Add(t?.File); + } + } + return false; } From 8d3cb8953c7e5e2d43d706fe4809dd479365750d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 14 Nov 2023 12:41:59 +0100 Subject: [PATCH 286/546] Add casting to variant in large worlds build --- Source/Engine/Utilities/VariantUtils.cs | 42 ++++++++++++++----------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/Source/Engine/Utilities/VariantUtils.cs b/Source/Engine/Utilities/VariantUtils.cs index 81520ae1b..8cccfb1ab 100644 --- a/Source/Engine/Utilities/VariantUtils.cs +++ b/Source/Engine/Utilities/VariantUtils.cs @@ -1,5 +1,11 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +#if USE_LARGE_WORLDS +using Real = System.Double; +#else +using Real = System.Single; +#endif + using System; using System.Collections; using System.Collections.Generic; @@ -33,56 +39,56 @@ namespace FlaxEngine.Utilities if (typeof(T) == typeof(Vector2)) { if (value is Float2 asFloat2) - return (T)(object)new Vector2(asFloat2.X, asFloat2.Y); + return (T)(object)new Vector2((Real)asFloat2.X, (Real)asFloat2.Y); if (value is Float3 asFloat3) - return (T)(object)new Vector2(asFloat3.X, asFloat3.Y); + return (T)(object)new Vector2((Real)asFloat3.X, (Real)asFloat3.Y); if (value is Float4 asFloat4) - return (T)(object)new Vector2(asFloat4.X, asFloat4.Y); + return (T)(object)new Vector2((Real)asFloat4.X, (Real)asFloat4.Y); } else if (typeof(T) == typeof(Vector3)) { if (value is Float2 asFloat2) - return (T)(object)new Vector3(asFloat2.X, asFloat2.Y, 0); + return (T)(object)new Vector3((Real)asFloat2.X, (Real)asFloat2.Y, (Real)0); if (value is Float3 asFloat3) - return (T)(object)new Vector3(asFloat3.X, asFloat3.Y, asFloat3.Z); + return (T)(object)new Vector3((Real)asFloat3.X, (Real)asFloat3.Y, (Real)asFloat3.Z); if (value is Float4 asFloat4) - return (T)(object)new Vector3(asFloat4.X, asFloat4.Y, asFloat4.Z); + return (T)(object)new Vector3((Real)asFloat4.X, (Real)asFloat4.Y, (Real)asFloat4.Z); } else if (typeof(T) == typeof(Vector4)) { if (value is Float2 asFloat2) - return (T)(object)new Vector4(asFloat2.X, asFloat2.Y, 0, 0); + return (T)(object)new Vector4((Real)asFloat2.X, (Real)asFloat2.Y, (Real)0, (Real)0); if (value is Float3 asFloat3) - return (T)(object)new Vector4(asFloat3.X, asFloat3.Y, asFloat3.Z, 0); + return (T)(object)new Vector4((Real)asFloat3.X, (Real)asFloat3.Y, (Real)asFloat3.Z, (Real)0); if (value is Vector4 asFloat4) - return (T)(object)new Vector4(asFloat4.X, asFloat4.Y, asFloat4.Z, asFloat4.W); + return (T)(object)new Vector4((Real)asFloat4.X, (Real)asFloat4.Y, (Real)asFloat4.Z, (Real)asFloat4.W); } else if (typeof(T) == typeof(Float2)) { if (value is Vector2 asVector2) - return (T)(object)new Float2(asVector2.X, asVector2.Y); + return (T)(object)new Float2((float)asVector2.X, (float)asVector2.Y); if (value is Vector3 asVector3) - return (T)(object)new Float2(asVector3.X, asVector3.Y); + return (T)(object)new Float2((float)asVector3.X, (float)asVector3.Y); if (value is Vector4 asVector4) - return (T)(object)new Float2(asVector4.X, asVector4.Y); + return (T)(object)new Float2((float)asVector4.X, (float)asVector4.Y); } else if (typeof(T) == typeof(Float3)) { if (value is Vector2 asVector2) - return (T)(object)new Float3(asVector2.X, asVector2.Y, 0); + return (T)(object)new Float3((float)asVector2.X, (float)asVector2.Y, (float)0); if (value is Vector3 asVector3) - return (T)(object)new Float3(asVector3.X, asVector3.Y, asVector3.Z); + return (T)(object)new Float3((float)asVector3.X, (float)asVector3.Y, (float)asVector3.Z); if (value is Vector4 asFloat4) - return (T)(object)new Float3(asFloat4.X, asFloat4.Y, asFloat4.Z); + return (T)(object)new Float3((float)asFloat4.X, (float)asFloat4.Y, (float)asFloat4.Z); } else if (typeof(T) == typeof(Float4)) { if (value is Vector2 asVector2) - return (T)(object)new Float4(asVector2.X, asVector2.Y, 0, 0); + return (T)(object)new Float4((float)asVector2.X, (float)asVector2.Y, (float)0, (float)0); if (value is Vector3 asVector3) - return (T)(object)new Float4(asVector3.X, asVector3.Y, asVector3.Z, 0); + return (T)(object)new Float4((float)asVector3.X, (float)asVector3.Y, (float)asVector3.Z, (float)0); if (value is Vector4 asVector4) - return (T)(object)new Float4(asVector4.X, asVector4.Y, asVector4.Z, asVector4.W); + return (T)(object)new Float4((float)asVector4.X, (float)asVector4.Y, (float)asVector4.Z, (float)asVector4.W); } return (T)Convert.ChangeType(value, typeof(T)); } From 3f5c92e2fa9ef23d7db17736baba887d27f395bd Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 14 Nov 2023 17:00:04 +0200 Subject: [PATCH 287/546] Improve .NET 8 runtime and SDK detection Setting the environment variable `DOTNET_ROLL_FORWARD_TO_PRERELEASE=1` is required to enable runtime support for release candidate builds of future major .NET releases. --- Source/Engine/Scripting/Runtime/DotNet.cpp | 17 +++++------ .../FlaxEngine.CSharp.runtimeconfig.json | 3 +- .../Flax.Build/Build/DotNet/DotNetSdk.cs | 29 ++++++++++++------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index c4371736d..976c992ea 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -1650,9 +1650,9 @@ bool InitHostfxr() const ::String csharpLibraryPath = Globals::BinariesFolder / TEXT("FlaxEngine.CSharp.dll"); const ::String csharpRuntimeConfigPath = Globals::BinariesFolder / TEXT("FlaxEngine.CSharp.runtimeconfig.json"); if (!FileSystem::FileExists(csharpLibraryPath)) - LOG(Fatal, "Failed to initialize managed runtime, missing file: {0}", csharpLibraryPath); + LOG(Fatal, "Failed to initialize .NET runtime, missing file: {0}", csharpLibraryPath); if (!FileSystem::FileExists(csharpRuntimeConfigPath)) - LOG(Fatal, "Failed to initialize managed runtime, missing file: {0}", csharpRuntimeConfigPath); + LOG(Fatal, "Failed to initialize .NET runtime, missing file: {0}", csharpRuntimeConfigPath); const FLAX_CORECLR_STRING& libraryPath = FLAX_CORECLR_STRING(csharpLibraryPath); // Get path to hostfxr library @@ -1703,9 +1703,9 @@ bool InitHostfxr() Platform::OpenUrl(TEXT("https://dotnet.microsoft.com/en-us/download/dotnet/7.0")); #endif #if USE_EDITOR - LOG(Fatal, "Missing .NET 7 SDK installation required to run Flax Editor."); + LOG(Fatal, "Missing .NET 7 or later SDK installation required to run Flax Editor."); #else - LOG(Fatal, "Missing .NET 7 Runtime installation required to run this application."); + LOG(Fatal, "Missing .NET 7 or later Runtime installation required to run this application."); #endif return true; } @@ -1735,14 +1735,13 @@ bool InitHostfxr() return true; } - // TODO: Implement picking different version of hostfxr, currently prefers highest available version. - // Allow future and preview versions of .NET - String dotnetRollForward; + // TODO: Implement support for picking RC/beta updates of .NET runtime + // Uncomment for enabling support for upcoming .NET major release candidates +#if 0 String dotnetRollForwardPr; - if (Platform::GetEnvironmentVariable(TEXT("DOTNET_ROLL_FORWARD"), dotnetRollForward)) - Platform::SetEnvironmentVariable(TEXT("DOTNET_ROLL_FORWARD"), TEXT("LatestMajor")); if (Platform::GetEnvironmentVariable(TEXT("DOTNET_ROLL_FORWARD_TO_PRERELEASE"), dotnetRollForwardPr)) Platform::SetEnvironmentVariable(TEXT("DOTNET_ROLL_FORWARD_TO_PRERELEASE"), TEXT("1")); +#endif // Initialize hosting component const char_t* argv[1] = { libraryPath.Get() }; diff --git a/Source/ThirdParty/nethost/FlaxEngine.CSharp.runtimeconfig.json b/Source/ThirdParty/nethost/FlaxEngine.CSharp.runtimeconfig.json index 202425667..134a0ef98 100644 --- a/Source/ThirdParty/nethost/FlaxEngine.CSharp.runtimeconfig.json +++ b/Source/ThirdParty/nethost/FlaxEngine.CSharp.runtimeconfig.json @@ -3,7 +3,8 @@ "tfm": "net7.0", "framework": { "name": "Microsoft.NETCore.App", - "version": "7.0.0" + "version": "7.0.0", + "rollForward": "latestMajor" } } } diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs index eff24dbf3..c2999999d 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs @@ -118,6 +118,11 @@ namespace Flax.Build /// public static Version MinimumVersion => new Version(7, 0); + /// + /// The maximum SDK version. + /// + public static Version MaximumVersion => new Version(8, 0); + /// public override TargetPlatform[] Platforms { @@ -245,21 +250,24 @@ namespace Flax.Build dotnetSdkVersions = GetVersions(Path.Combine(dotnetPath, "sdk")); if (dotnetRuntimeVersions == null) dotnetRuntimeVersions = GetVersions(Path.Combine(dotnetPath, "shared/Microsoft.NETCore.App")); - string dotnetSdkVersion = dotnetSdkVersions.OrderByDescending(ParseVersion).FirstOrDefault(); - string dotnetRuntimeVersion = dotnetRuntimeVersions.OrderByDescending(ParseVersion).FirstOrDefault(); + + dotnetSdkVersions = dotnetSdkVersions.OrderByDescending(ParseVersion); + dotnetRuntimeVersions = dotnetRuntimeVersions.OrderByDescending(ParseVersion); + + string dotnetSdkVersion = dotnetSdkVersions.FirstOrDefault(x => ParseVersion(x).Major >= MinimumVersion.Major && ParseVersion(x).Major <= MaximumVersion.Major); + string dotnetRuntimeVersion = dotnetRuntimeVersions.FirstOrDefault(x => ParseVersion(x).Major >= MinimumVersion.Major && ParseVersion(x).Major <= MaximumVersion.Major); if (string.IsNullOrEmpty(dotnetSdkVersion)) dotnetSdkVersion = dotnetPath; + if (dotnetSdkVersion == null && dotnetSdkVersions.Count() > 0) + { + Log.Warning($"Unsupported .NET SDK {dotnetSdkVersions.First()} version found. Minimum version required is .NET {MinimumVersion}."); + return; + } if (string.IsNullOrEmpty(dotnetSdkVersion) || string.IsNullOrEmpty(dotnetRuntimeVersion)) { Log.Warning("Missing .NET SDK"); return; } - int majorVersion = int.Parse(dotnetSdkVersion.Substring(0, dotnetSdkVersion.IndexOf("."))); - if (majorVersion < MinimumVersion.Major) - { - Log.Warning($"Unsupported .NET SDK {dotnetSdkVersion} version found. Minimum version required is .NET {MinimumVersion}."); - return; - } RootPath = dotnetPath; Version = ParseVersion(dotnetSdkVersion); VersionName = dotnetSdkVersion; @@ -452,8 +460,9 @@ namespace Flax.Build private static string GetVersion(IEnumerable versions) { - // TODO: reject 'future' versions like .Net 8? - return versions.OrderByDescending(ParseVersion).FirstOrDefault(); + return versions.OrderByDescending(ParseVersion) + .Where(x => ParseVersion(x).Major >= MinimumVersion.Major && ParseVersion(x).Major <= MaximumVersion.Major) + .FirstOrDefault(); } private static string SearchForDotnetLocationLinux() From bc3107d1db6cc8744cf7d24797f74a1d0258c894 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 14 Nov 2023 19:03:46 +0100 Subject: [PATCH 288/546] Fix build warnings on the latest XCode --- Source/Engine/Foliage/FoliageType.h | 2 +- Source/Engine/Level/Actor.h | 12 ++++++------ Source/Engine/Physics/Actors/RigidBody.h | 16 ++++++++-------- Source/Engine/Renderer/RenderList.h | 20 ++++++++++---------- Source/Engine/Scripting/Script.h | 16 ++++++++-------- Source/ThirdParty/pugixml/pugixml.cpp | 24 ++++++++++++++++++++++++ 6 files changed, 57 insertions(+), 33 deletions(-) diff --git a/Source/Engine/Foliage/FoliageType.h b/Source/Engine/Foliage/FoliageType.h index 0bc556fc9..4251b6924 100644 --- a/Source/Engine/Foliage/FoliageType.h +++ b/Source/Engine/Foliage/FoliageType.h @@ -46,7 +46,7 @@ API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API FoliageType : public ScriptingOb DECLARE_SCRIPTING_TYPE_NO_SPAWN(FoliageType); friend Foliage; private: - int8 _isReady : 1; + uint8 _isReady : 1; public: /// diff --git a/Source/Engine/Level/Actor.h b/Source/Engine/Level/Actor.h index a1f2dc78f..0ce9a0dbc 100644 --- a/Source/Engine/Level/Actor.h +++ b/Source/Engine/Level/Actor.h @@ -33,12 +33,12 @@ API_CLASS(Abstract) class FLAXENGINE_API Actor : public SceneObject friend Prefab; friend PrefabInstanceData; protected: - int16 _isActive : 1; - int16 _isActiveInHierarchy : 1; - int16 _isPrefabRoot : 1; - int16 _isEnabled : 1; - int16 _drawNoCulling : 1; - int16 _drawCategory : 4; + uint16 _isActive : 1; + uint16 _isActiveInHierarchy : 1; + uint16 _isPrefabRoot : 1; + uint16 _isEnabled : 1; + uint16 _drawNoCulling : 1; + uint16 _drawCategory : 4; byte _layer; StaticFlags _staticFlags; Transform _localTransform; diff --git a/Source/Engine/Physics/Actors/RigidBody.h b/Source/Engine/Physics/Actors/RigidBody.h index e7b929483..2ef50ca7a 100644 --- a/Source/Engine/Physics/Actors/RigidBody.h +++ b/Source/Engine/Physics/Actors/RigidBody.h @@ -30,14 +30,14 @@ protected: Float3 _centerOfMassOffset; RigidbodyConstraints _constraints; - int32 _enableSimulation : 1; - int32 _isKinematic : 1; - int32 _useCCD : 1; - int32 _enableGravity : 1; - int32 _startAwake : 1; - int32 _updateMassWhenScaleChanges : 1; - int32 _overrideMass : 1; - int32 _isUpdatingTransform : 1; + uint32 _enableSimulation : 1; + uint32 _isKinematic : 1; + uint32 _useCCD : 1; + uint32 _enableGravity : 1; + uint32 _startAwake : 1; + uint32 _updateMassWhenScaleChanges : 1; + uint32 _overrideMass : 1; + uint32 _isUpdatingTransform : 1; public: /// diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h index 493f80101..a2cd48696 100644 --- a/Source/Engine/Renderer/RenderList.h +++ b/Source/Engine/Renderer/RenderList.h @@ -41,8 +41,8 @@ struct RendererDirectionalLightData StaticFlags StaticFlags; float IndirectLightingIntensity; int16 ShadowDataIndex = -1; - int8 CastVolumetricShadow : 1; - int8 RenderedVolumetricFog : 1; + uint8 CastVolumetricShadow : 1; + uint8 RenderedVolumetricFog : 1; float ShadowsDistance; int32 CascadeCount; @@ -86,9 +86,9 @@ struct RendererSpotLightData StaticFlags StaticFlags; int16 ShadowDataIndex = -1; - int8 CastVolumetricShadow : 1; - int8 RenderedVolumetricFog : 1; - int8 UseInverseSquaredFalloff : 1; + uint8 CastVolumetricShadow : 1; + uint8 RenderedVolumetricFog : 1; + uint8 UseInverseSquaredFalloff : 1; GPUTexture* IESTexture; Guid ID; @@ -124,9 +124,9 @@ struct RendererPointLightData StaticFlags StaticFlags; int16 ShadowDataIndex = -1; - int8 CastVolumetricShadow : 1; - int8 RenderedVolumetricFog : 1; - int8 UseInverseSquaredFalloff : 1; + uint8 CastVolumetricShadow : 1; + uint8 RenderedVolumetricFog : 1; + uint8 UseInverseSquaredFalloff : 1; GPUTexture* IESTexture; Guid ID; @@ -146,8 +146,8 @@ struct RendererSkyLightData float IndirectLightingIntensity; StaticFlags StaticFlags; - int8 CastVolumetricShadow : 1; - int8 RenderedVolumetricFog : 1; + uint8 CastVolumetricShadow : 1; + uint8 RenderedVolumetricFog : 1; CubeTexture* Image; Guid ID; diff --git a/Source/Engine/Scripting/Script.h b/Source/Engine/Scripting/Script.h index 49cba0ec4..8cf1d0670 100644 --- a/Source/Engine/Scripting/Script.h +++ b/Source/Engine/Scripting/Script.h @@ -15,15 +15,15 @@ API_CLASS(Abstract) class FLAXENGINE_API Script : public SceneObject friend SceneTicking; friend class PrefabInstanceData; protected: - int32 _enabled : 1; - int32 _tickFixedUpdate : 1; - int32 _tickUpdate : 1; - int32 _tickLateUpdate : 1; - int32 _tickLateFixedUpdate : 1; - int32 _wasStartCalled : 1; - int32 _wasEnableCalled : 1; + uint16 _enabled : 1; + uint16 _tickFixedUpdate : 1; + uint16 _tickUpdate : 1; + uint16 _tickLateUpdate : 1; + uint16 _tickLateFixedUpdate : 1; + uint16 _wasStartCalled : 1; + uint16 _wasEnableCalled : 1; #if USE_EDITOR - int32 _executeInEditor : 1; + uint16 _executeInEditor : 1; #endif public: diff --git a/Source/ThirdParty/pugixml/pugixml.cpp b/Source/ThirdParty/pugixml/pugixml.cpp index 56860bb90..c4d4ec845 100644 --- a/Source/ThirdParty/pugixml/pugixml.cpp +++ b/Source/ThirdParty/pugixml/pugixml.cpp @@ -3967,7 +3967,11 @@ PUGI__NS_BEGIN PUGI__FN bool set_value_convert(char_t*& dest, uintptr_t& header, uintptr_t header_mask, int value) { char buf[128]; +#if __APPLE__ + snprintf(buf, sizeof(buf), "%d", value); +#else sprintf(buf, "%d", value); +#endif return set_value_buffer(dest, header, header_mask, buf); } @@ -3975,7 +3979,11 @@ PUGI__NS_BEGIN PUGI__FN bool set_value_convert(char_t*& dest, uintptr_t& header, uintptr_t header_mask, unsigned int value) { char buf[128]; +#if __APPLE__ + snprintf(buf, sizeof(buf), "%u", value); +#else sprintf(buf, "%u", value); +#endif return set_value_buffer(dest, header, header_mask, buf); } @@ -3983,7 +3991,11 @@ PUGI__NS_BEGIN PUGI__FN bool set_value_convert(char_t*& dest, uintptr_t& header, uintptr_t header_mask, float value) { char buf[128]; +#if __APPLE__ + snprintf(buf, sizeof(buf), "%.9g", value); +#else sprintf(buf, "%.9g", value); +#endif return set_value_buffer(dest, header, header_mask, buf); } @@ -3991,7 +4003,11 @@ PUGI__NS_BEGIN PUGI__FN bool set_value_convert(char_t*& dest, uintptr_t& header, uintptr_t header_mask, double value) { char buf[128]; +#if __APPLE__ + snprintf(buf, sizeof(buf), "%.17g", value); +#else sprintf(buf, "%.17g", value); +#endif return set_value_buffer(dest, header, header_mask, buf); } @@ -4005,7 +4021,11 @@ PUGI__NS_BEGIN PUGI__FN bool set_value_convert(char_t*& dest, uintptr_t& header, uintptr_t header_mask, long long value) { char buf[128]; +#if __APPLE__ + snprintf(buf, sizeof(buf), "%lld", value); +#else sprintf(buf, "%lld", value); +#endif return set_value_buffer(dest, header, header_mask, buf); } @@ -4013,7 +4033,11 @@ PUGI__NS_BEGIN PUGI__FN bool set_value_convert(char_t*& dest, uintptr_t& header, uintptr_t header_mask, unsigned long long value) { char buf[128]; +#if __APPLE__ + snprintf(buf, sizeof(buf), "%llu", value); +#else sprintf(buf, "%llu", value); +#endif return set_value_buffer(dest, header, header_mask, buf); } From d533cf554afc07a7d47cf7ee76b8bd9cd8d24496 Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" Date: Tue, 14 Nov 2023 14:47:35 -0400 Subject: [PATCH 289/546] Avoid crash when try build navmesh with null scene --- Source/Engine/Navigation/NavMeshBuilder.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index b0959cb34..894dc1e52 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -1067,6 +1067,12 @@ void NavMeshBuilder::Update() void NavMeshBuilder::Build(Scene* scene, float timeoutMs) { + if (!scene) + { + LOG(Warning, "Could not generate navmesh without scene."); + return; + } + // Early out if scene is not using navigation if (scene->Navigation.Volumes.IsEmpty()) { @@ -1098,6 +1104,17 @@ void NavMeshBuilder::Build(Scene* scene, float timeoutMs) void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs) { + if (!scene) + { + LOG(Warning, "Could not generate navmesh without scene."); + return; + } + if (!&dirtyBounds) + { + LOG(Warning, "Could not generate navmesh without dirty bounds."); + return; + } + // Early out if scene is not using navigation if (scene->Navigation.Volumes.IsEmpty()) { From 3a59cfcf20028e49a7206d48d09fa861dda20645 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 15 Nov 2023 11:57:53 +0100 Subject: [PATCH 290/546] Change `Globals.BuildErrors` into `BuildException` #1673 --- .../Build/NativeCpp/Builder.NativeCpp.cs | 10 ++-------- Source/Tools/Flax.Build/Globals.cs | 5 ----- Source/Tools/Flax.Build/Program.cs | 2 +- .../VisualStudio/VisualStudioProjectGenerator.cs | 2 +- .../Tools/Flax.Build/Utilities/BuildException.cs | 14 ++++++++++++++ 5 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 Source/Tools/Flax.Build/Utilities/BuildException.cs diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs index 23d2be157..3efe33b38 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs @@ -628,10 +628,7 @@ namespace Flax.Build } } if (failed) - { - Globals.BuildErrors = true; - throw new Exception($"Failed to build target {target.Name}. See log."); - } + throw new BuildException($"Failed to build target {target.Name}. See log."); } else { @@ -692,10 +689,7 @@ namespace Flax.Build } } if (failed) - { - Globals.BuildErrors = true; - throw new Exception($"Failed to build target {target.Name}. See log."); - } + throw new BuildException($"Failed to build target {target.Name}. See log."); } else { diff --git a/Source/Tools/Flax.Build/Globals.cs b/Source/Tools/Flax.Build/Globals.cs index 994094124..b5f3088d6 100644 --- a/Source/Tools/Flax.Build/Globals.cs +++ b/Source/Tools/Flax.Build/Globals.cs @@ -22,11 +22,6 @@ namespace Flax.Build /// public static ProjectInfo Project; - /// - /// Set when any build related errors were raised. - /// - public static bool BuildErrors = false; - /// /// All platforms array. /// diff --git a/Source/Tools/Flax.Build/Program.cs b/Source/Tools/Flax.Build/Program.cs index bac1caaa8..d6a70de30 100644 --- a/Source/Tools/Flax.Build/Program.cs +++ b/Source/Tools/Flax.Build/Program.cs @@ -172,7 +172,7 @@ namespace Flax.Build catch (Exception ex) { // Ignore exception logging for build errors - if (!Globals.BuildErrors) + if (!(ex is BuildException)) Log.Exception(ex); failed = true; } diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs index 48ecb6425..eda2f7a95 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs @@ -525,7 +525,7 @@ namespace Flax.Build.Projects.VisualStudio // Build C# projects (needed for Rider solution wide analysis) build |= project.Type == TargetType.DotNetCore; - // + // Always build the project named after solution if main project was not set build |= solution.MainProject == null && project.Name == solution.Name; } else if (firstPlatformMatch != -1 && !configuration.Name.StartsWith("Editor.")) diff --git a/Source/Tools/Flax.Build/Utilities/BuildException.cs b/Source/Tools/Flax.Build/Utilities/BuildException.cs new file mode 100644 index 000000000..5118c4154 --- /dev/null +++ b/Source/Tools/Flax.Build/Utilities/BuildException.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; + +namespace Flax.Build +{ + internal class BuildException : Exception + { + public BuildException(string message) + : base(message) + { + } + } +} From 6aea001e94b37e51a81cd67761a6428424b8fd88 Mon Sep 17 00:00:00 2001 From: "Mr. Capybara" <79365912+RuanLucasGD@users.noreply.github.com> Date: Wed, 15 Nov 2023 12:49:20 -0300 Subject: [PATCH 291/546] Remove unnecessary check --- Source/Engine/Navigation/NavMeshBuilder.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index 894dc1e52..ccd9bef32 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -1109,11 +1109,6 @@ void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float t LOG(Warning, "Could not generate navmesh without scene."); return; } - if (!&dirtyBounds) - { - LOG(Warning, "Could not generate navmesh without dirty bounds."); - return; - } // Early out if scene is not using navigation if (scene->Navigation.Volumes.IsEmpty()) From 307129b4a185c7838a56567bde0b4bf7c35d0d26 Mon Sep 17 00:00:00 2001 From: NoriteSC <53096989+NoriteSC@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:50:44 +0100 Subject: [PATCH 292/546] added c++ PingPong and flipped sheer --- Source/Engine/Core/Math/Math.h | 11 +++++++++++ Source/Engine/UI/GUI/Control.Bounds.cs | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Core/Math/Math.h b/Source/Engine/Core/Math/Math.h index 276f64884..49a052771 100644 --- a/Source/Engine/Core/Math/Math.h +++ b/Source/Engine/Core/Math/Math.h @@ -890,6 +890,17 @@ namespace Math { return Lerp(a, b, alpha < 0.5f ? InterpCircularIn(0.f, 1.f, alpha * 2.f) * 0.5f : InterpCircularOut(0.f, 1.f, alpha * 2.f - 1.f) * 0.5f + 0.5f); } + /// + /// PingPongs the value , so that it is never larger than and never smaller than 0. + /// + /// + /// + /// + template + static FORCE_INLINE T PingPong(const T& t, T length) + { + return length - Abs(Repeat(t, length * 2.0f) - length); + } // Rotates position about the given axis by the given angle, in radians, and returns the offset to position Vector3 FLAXENGINE_API RotateAboutAxis(const Vector3& normalizedRotationAxis, float angle, const Vector3& positionOnAxis, const Vector3& position); diff --git a/Source/Engine/UI/GUI/Control.Bounds.cs b/Source/Engine/UI/GUI/Control.Bounds.cs index c9d20bfae..52a4f78f7 100644 --- a/Source/Engine/UI/GUI/Control.Bounds.cs +++ b/Source/Engine/UI/GUI/Control.Bounds.cs @@ -530,9 +530,9 @@ namespace FlaxEngine.GUI Matrix3x3 m1 = new Matrix3x3 ( _scale.X, - _scale.X * (_shear.X == 0 ? 0 : (1.0f / Mathf.Tan(Mathf.DegreesToRadians * (90 - Mathf.Clamp(_shear.X, -89.0f, 89.0f))))), + _scale.X * (_shear.Y == 0 ? 0 : (1.0f / Mathf.Tan(Mathf.DegreesToRadians * (90 - Mathf.Clamp(_shear.Y, -89.0f, 89.0f))))), 0, - _scale.Y * (_shear.Y == 0 ? 0 : (1.0f / Mathf.Tan(Mathf.DegreesToRadians * (90 - Mathf.Clamp(_shear.Y, -89.0f, 89.0f))))), + _scale.Y * (_shear.X == 0 ? 0 : (1.0f / Mathf.Tan(Mathf.DegreesToRadians * (90 - Mathf.Clamp(_shear.X, -89.0f, 89.0f))))), _scale.Y, 0, 0, 0, 1 ); From d33bf2fa6a31abe7f3020c3cfb8b25faafd0fac8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Nov 2023 13:55:45 +0100 Subject: [PATCH 293/546] Update docs comments based on https://github.com/FlaxEngine/FlaxDocs/pull/127 --- Source/Engine/Graphics/Enums.h | 14 +++--- Source/Engine/Graphics/PostProcessSettings.h | 46 +++++++++---------- .../Level/Actors/ExponentialHeightFog.h | 6 +-- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h index 4d1422f96..bbf9c0de8 100644 --- a/Source/Engine/Graphics/Enums.h +++ b/Source/Engine/Graphics/Enums.h @@ -591,37 +591,37 @@ API_ENUM() enum class Quality : byte API_ENUM() enum class MaterialPostFxLocation : byte { /// - /// The 'after' post processing pass using LDR input frame. + /// Render the material after the post processing pass using *LDR* input frame. /// AfterPostProcessingPass = 0, /// - /// The 'before' post processing pass using HDR input frame. + /// Render the material before the post processing pass using *HDR* input frame. /// BeforePostProcessingPass = 1, /// - /// The 'before' forward pass but after GBuffer with HDR input frame. + /// Render the material before the forward pass but after *GBuffer* with *HDR* input frame. /// BeforeForwardPass = 2, /// - /// The 'after' custom post effects. + /// Render the material after custom post effects (scripted). /// AfterCustomPostEffects = 3, /// - /// The 'before' Reflections pass. After the Light pass. Can be used to implement a custom light types that accumulate lighting to the light buffer. + /// Render the material before the reflections pass but after the lighting pass using *HDR* input frame. It can be used to implement a custom light types that accumulate lighting to the light buffer. /// BeforeReflectionsPass = 4, /// - /// The 'after' AA filter pass. Rendering is done to the output backbuffer. + /// Render the material after anti-aliasing into the output backbuffer. /// AfterAntiAliasingPass = 5, /// - /// The 'after' forward pass but before any post processing. + /// Render the material after the forward pass but before any post processing. /// AfterForwardPass = 6, diff --git a/Source/Engine/Graphics/PostProcessSettings.h b/Source/Engine/Graphics/PostProcessSettings.h index d2ace6cea..1d695f353 100644 --- a/Source/Engine/Graphics/PostProcessSettings.h +++ b/Source/Engine/Graphics/PostProcessSettings.h @@ -447,13 +447,13 @@ API_STRUCT() struct FLAXENGINE_API BloomSettings : ISerializable bool Enabled = true; /// - /// Bloom effect strength. Value 0 disabled is, while higher values increase the effect. + /// Bloom effect strength. Set a value of 0 to disabled it, while higher values increase the effect. /// API_FIELD(Attributes="Limit(0, 20.0f, 0.01f), EditorOrder(1), PostProcessSetting((int)BloomSettingsOverride.Intensity)") float Intensity = 1.0f; /// - /// Minimum pixel brightness value to start blowing. Values below the threshold are skipped. + /// Minimum pixel brightness value to start blooming. Values below this threshold are skipped. /// API_FIELD(Attributes="Limit(0, 15.0f, 0.01f), EditorOrder(2), PostProcessSetting((int)BloomSettingsOverride.Threshold)") float Threshold = 3.0f; @@ -987,13 +987,13 @@ API_STRUCT() struct FLAXENGINE_API EyeAdaptationSettings : ISerializable float MaxBrightness = 2.0f; /// - /// The lower bound for the luminance histogram of the scene color. Value is in percent and limits the pixels below this brightness. Use values from range 60-80. Used only in AutomaticHistogram mode. + /// The lower bound for the luminance histogram of the scene color. This value is in percent and limits the pixels below this brightness. Use values in the range of 60-80. Used only in AutomaticHistogram mode. /// API_FIELD(Attributes="Limit(1, 99, 0.001f), EditorOrder(3), PostProcessSetting((int)EyeAdaptationSettingsOverride.HistogramLowPercent)") float HistogramLowPercent = 70.0f; /// - /// The upper bound for the luminance histogram of the scene color. Value is in percent and limits the pixels above this brightness. Use values from range 80-95. Used only in AutomaticHistogram mode. + /// The upper bound for the luminance histogram of the scene color. This value is in percent and limits the pixels above this brightness. Use values in the range of 80-95. Used only in AutomaticHistogram mode. /// API_FIELD(Attributes="Limit(1, 99, 0.001f), EditorOrder(3), PostProcessSetting((int)EyeAdaptationSettingsOverride.HistogramHighPercent)") float HistogramHighPercent = 98.0f; @@ -1091,13 +1091,13 @@ API_STRUCT() struct FLAXENGINE_API CameraArtifactsSettings : ISerializable Float3 VignetteColor = Float3(0, 0, 0.001f); /// - /// Controls shape of the vignette. Values near 0 produce rectangle shape. Higher values result in round shape. The default value is 0.125. + /// Controls the shape of the vignette. Values near 0 produce a rectangular shape. Higher values result in a rounder shape. The default value is 0.125. /// API_FIELD(Attributes="Limit(0.0001f, 2.0f, 0.001f), EditorOrder(2), PostProcessSetting((int)CameraArtifactsSettingsOverride.VignetteShapeFactor)") float VignetteShapeFactor = 0.125f; /// - /// Intensity of the grain filter. Value 0 hides it. The default value is 0.005. + /// Intensity of the grain filter. A value of 0 hides it. The default value is 0.005. /// API_FIELD(Attributes="Limit(0.0f, 2.0f, 0.005f), EditorOrder(3), PostProcessSetting((int)CameraArtifactsSettingsOverride.GrainAmount)") float GrainAmount = 0.006f; @@ -1109,19 +1109,19 @@ API_STRUCT() struct FLAXENGINE_API CameraArtifactsSettings : ISerializable float GrainParticleSize = 1.6f; /// - /// Speed of the grain particles animation. + /// Speed of the grain particle animation. /// API_FIELD(Attributes="Limit(0.0f, 10.0f, 0.01f), EditorOrder(5), PostProcessSetting((int)CameraArtifactsSettingsOverride.GrainSpeed)") float GrainSpeed = 1.0f; /// - /// Controls chromatic aberration effect strength. Value 0 hides it. + /// Controls the chromatic aberration effect strength. A value of 0 hides it. /// API_FIELD(Attributes="Limit(0.0f, 1.0f, 0.01f), EditorOrder(6), PostProcessSetting((int)CameraArtifactsSettingsOverride.ChromaticDistortion)") float ChromaticDistortion = 0.0f; /// - /// Screen tint color (alpha channel defines the blending factor). + /// Screen tint color (the alpha channel defines the blending factor). /// API_FIELD(Attributes="DefaultValue(typeof(Color), \"0,0,0,0\"), EditorOrder(7), PostProcessSetting((int)CameraArtifactsSettingsOverride.ScreenFadeColor)") Color ScreenFadeColor = Color::Transparent; @@ -1227,7 +1227,7 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable LensFlaresSettingsOverride OverrideFlags = Override::None; /// - /// Strength of the effect. Value 0 disabled it. + /// Strength of the effect. A value of 0 disables it. /// API_FIELD(Attributes="Limit(0, 10.0f, 0.01f), EditorOrder(0), PostProcessSetting((int)LensFlaresSettingsOverride.Intensity)") float Intensity = 1.0f; @@ -1281,7 +1281,7 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable SoftAssetReference LensDirt; /// - /// Fullscreen lens dirt intensity parameter. Allows to tune dirt visibility. + /// Fullscreen lens dirt intensity parameter. Allows tuning dirt visibility. /// API_FIELD(Attributes="Limit(0, 100, 0.01f), EditorOrder(9), PostProcessSetting((int)LensFlaresSettingsOverride.LensDirtIntensity)") float LensDirtIntensity = 1.0f; @@ -1419,13 +1419,13 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable DepthOfFieldSettingsOverride OverrideFlags = Override::None; /// - /// If checked, depth of field effect will be visible. + /// If checked, the depth of field effect will be visible. /// API_FIELD(Attributes="EditorOrder(0), PostProcessSetting((int)DepthOfFieldSettingsOverride.Enabled)") bool Enabled = false; /// - /// The blur intensity in the out-of-focus areas. Allows reducing blur amount by scaling down the Gaussian Blur radius. Normalized to range 0-1. + /// The blur intensity in the out-of-focus areas. Allows reducing the blur amount by scaling down the Gaussian Blur radius. Normalized to range 0-1. /// API_FIELD(Attributes="Limit(0, 1, 0.01f), EditorOrder(1), PostProcessSetting((int)DepthOfFieldSettingsOverride.BlurStrength)") float BlurStrength = 1.0f; @@ -1479,7 +1479,7 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable float BokehBrightness = 1.0f; /// - /// Defines bokeh shapes type. + /// Defines the type of the bokeh shapes. /// API_FIELD(Attributes="EditorOrder(10), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehShape)") BokehShapeType BokehShape = BokehShapeType::Octagon; @@ -1491,19 +1491,19 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable SoftAssetReference BokehShapeCustom; /// - /// The minimum pixel brightness to create bokeh. Pixels with lower brightness will be skipped. + /// The minimum pixel brightness to create the bokeh. Pixels with lower brightness will be skipped. /// API_FIELD(Attributes="Limit(0, 10000.0f, 0.01f), EditorOrder(12), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehBrightnessThreshold)") float BokehBrightnessThreshold = 3.0f; /// - /// Depth of Field bokeh shapes blur threshold. + /// Depth of Field bokeh shape blur threshold. /// API_FIELD(Attributes="Limit(0, 1.0f, 0.001f), EditorOrder(13), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehBlurThreshold)") float BokehBlurThreshold = 0.05f; /// - /// Controls bokeh shapes brightness falloff. Higher values reduce bokeh visibility. + /// Controls bokeh shape brightness falloff. Higher values reduce bokeh visibility. /// API_FIELD(Attributes="Limit(0, 2.0f, 0.001f), EditorOrder(14), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehFalloff)") float BokehFalloff = 0.5f; @@ -1575,25 +1575,25 @@ API_STRUCT() struct FLAXENGINE_API MotionBlurSettings : ISerializable MotionBlurSettingsOverride OverrideFlags = Override::None; /// - /// If checked, motion blur effect will be rendered. + /// If checked, the motion blur effect will be rendered. /// API_FIELD(Attributes="EditorOrder(0), PostProcessSetting((int)MotionBlurSettingsOverride.Enabled)") bool Enabled = true; /// - /// The blur effect strength. Value 0 disabled is, while higher values increase the effect. + /// The blur effect strength. A value of 0 disables it, while higher values increase the effect. /// API_FIELD(Attributes="Limit(0, 5, 0.01f), EditorOrder(1), PostProcessSetting((int)MotionBlurSettingsOverride.Scale)") float Scale = 1.0f; /// - /// The amount of sample points used during motion blur rendering. It affects quality and performance. + /// The amount of sample points used during motion blur rendering. It affects blur quality and performance. /// API_FIELD(Attributes="Limit(4, 32, 0.1f), EditorOrder(2), PostProcessSetting((int)MotionBlurSettingsOverride.SampleCount)") int32 SampleCount = 10; /// - /// The motion vectors texture resolution. Motion blur uses per-pixel motion vectors buffer that contains objects movement information. Use lower resolution to improve performance. + /// The motion vectors texture resolution. Motion blur uses a per-pixel motion vector buffer that contains an objects movement information. Use a lower resolution to improve performance. /// API_FIELD(Attributes="EditorOrder(3), PostProcessSetting((int)MotionBlurSettingsOverride.MotionVectorsResolution)") ResolutionMode MotionVectorsResolution = ResolutionMode::Half; @@ -1898,13 +1898,13 @@ API_STRUCT() struct FLAXENGINE_API AntiAliasingSettings : ISerializable float TAA_Sharpness = 0.0f; /// - /// The blend coefficient for stationary fragments. Controls the percentage of history sample blended into final color for fragments with minimal active motion. + /// The blend coefficient for stationary fragments. Controls the percentage of history samples blended into the final color for fragments with minimal active motion. /// API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(3), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_StationaryBlending), EditorDisplay(null, \"TAA Stationary Blending\")") float TAA_StationaryBlending = 0.95f; /// - /// The blending coefficient for moving fragments. Controls the percentage of history sample blended into the final color for fragments with significant active motion. + /// The blending coefficient for moving fragments. Controls the percentage of history samples blended into the final color for fragments with significant active motion. /// API_FIELD(Attributes="Limit(0, 0.99f, 0.001f), EditorOrder(4), PostProcessSetting((int)AntiAliasingSettingsOverride.TAA_MotionBlending), EditorDisplay(null, \"TAA Motion Blending\")") float TAA_MotionBlending = 0.7f; diff --git a/Source/Engine/Level/Actors/ExponentialHeightFog.h b/Source/Engine/Level/Actors/ExponentialHeightFog.h index b0e5751d9..7b400fe4e 100644 --- a/Source/Engine/Level/Actors/ExponentialHeightFog.h +++ b/Source/Engine/Level/Actors/ExponentialHeightFog.h @@ -29,7 +29,7 @@ public: float FogDensity = 0.02f; /// - /// The fog height density factor that controls how the density increases as height decreases. The smaller values produce more visible transition larger. + /// The fog height density factor that controls how the density increases as height decreases. Smaller values produce a more visible transition layer. /// API_FIELD(Attributes="EditorOrder(20), DefaultValue(0.2f), Limit(0.0001f, 10.0f, 0.001f), EditorDisplay(\"Exponential Height Fog\")") float FogHeightFalloff = 0.2f; @@ -55,7 +55,7 @@ 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. + /// 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. /// API_FIELD(Attributes="EditorOrder(60), DefaultValue(0.0f), Limit(0), EditorDisplay(\"Exponential Height Fog\")") float FogCutoffDistance = 0.0f; @@ -111,7 +111,7 @@ public: Color VolumetricFogAlbedo = Color::White; /// - /// Light emitted by height fog. This is a density so more light is emitted the further you are looking through the fog. + /// Light emitted by height fog. This is a density value so more light is emitted the further you are looking through the fog. /// In most cases using a Skylight is a better choice, however, it may be useful in certain scenarios. /// API_FIELD(Attributes="EditorOrder(330), DefaultValue(typeof(Color), \"0,0,0,1\"), EditorDisplay(\"Volumetric Fog\", \"Emissive\")") From 9738fd435437ef6a63fb63ef6e6b02fce943e060 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Nov 2023 14:09:05 +0100 Subject: [PATCH 294/546] Fix crash on hot-reload in Editor due to leftover scripting events in `ScriptingEvents::EventsTable` #1925 --- Source/Engine/Scripting/BinaryModule.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index 5bbaf8d0b..e9465096f 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -693,6 +693,14 @@ void BinaryModule::Destroy(bool isReloading) } } + // Remove any scripting events + for (auto i = ScriptingEvents::EventsTable.Begin(); i.IsNotEnd(); ++i) + { + const ScriptingTypeHandle type = i->Key.First; + if (type.Module == this) + ScriptingEvents::EventsTable.Remove(i); + } + // Unregister GetModules().RemoveKeepOrder(this); } From f0865c398933491a1e87f1a57152b626a1e905c4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Nov 2023 14:18:29 +0100 Subject: [PATCH 295/546] Fix crash in Global Surface Atlas when dirty object is missing --- Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp b/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp index 8be3172a6..9d0e065e1 100644 --- a/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp +++ b/Source/Engine/Renderer/GI/GlobalSurfaceAtlasPass.cpp @@ -565,7 +565,10 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co _vertexBuffer->Data.EnsureCapacity(_dirtyObjectsBuffer.Count() * 6 * sizeof(AtlasTileVertex)); for (void* actorObject : _dirtyObjectsBuffer) { - const auto& object = ((const Dictionary&)surfaceAtlasData.Objects)[actorObject]; + const GlobalSurfaceAtlasObject* objectPtr = surfaceAtlasData.Objects.TryGet(actorObject); + if (!objectPtr) + continue; + const GlobalSurfaceAtlasObject& object = *objectPtr; for (int32 tileIndex = 0; tileIndex < 6; tileIndex++) { auto* tile = object.Tiles[tileIndex]; @@ -587,7 +590,10 @@ bool GlobalSurfaceAtlasPass::Render(RenderContext& renderContext, GPUContext* co int32 tilesDrawn = 0; for (void* actorObject : _dirtyObjectsBuffer) { - const auto& object = ((const Dictionary&)surfaceAtlasData.Objects)[actorObject]; + const GlobalSurfaceAtlasObject* objectPtr = surfaceAtlasData.Objects.TryGet(actorObject); + if (!objectPtr) + continue; + const GlobalSurfaceAtlasObject& object = *objectPtr; // Clear draw calls list renderContextTiles.List->DrawCalls.Clear(); From bec878cc11f33500b2208d189176f5a222c4da53 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Nov 2023 15:47:42 +0100 Subject: [PATCH 296/546] Fix crashes in various dictionary usages caused by duplicated keys #1925 #1924 --- Source/Engine/Level/Prefabs/Prefab.Apply.cpp | 16 +++++++--------- Source/Engine/Level/Prefabs/PrefabManager.cpp | 4 ++-- Source/Engine/Scripting/Scripting.cpp | 12 +++++------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index 454b68006..c10ea532d 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -253,7 +253,7 @@ void PrefabInstanceData::SerializePrefabInstances(PrefabInstancesData& prefabIns for (int32 i = 0; i < sceneObjects->Count(); i++) { SceneObject* obj = sceneObjects.Value->At(i); - instance.PrefabInstanceIdToDataIndex.Add(obj->GetSceneObjectId(), i); + instance.PrefabInstanceIdToDataIndex[obj->GetSceneObjectId()] = i; } } tmpBuffer.Clear(); @@ -313,15 +313,13 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI continue; } - modifier.Value->IdsMapping.Add(obj->GetPrefabObjectID(), obj->GetSceneObjectId()); + modifier.Value->IdsMapping[obj->GetPrefabObjectID()] = obj->GetSceneObjectId(); } } // Generate new IDs for the added objects (objects in prefab has to have a unique Ids, other than the targetActor instance objects to prevent Id collisions) for (int32 i = 0; i < newPrefabObjectIds.Count(); i++) - { - modifier->IdsMapping.Add(newPrefabObjectIds[i], Guid::New()); - } + modifier->IdsMapping[newPrefabObjectIds[i]] = Guid::New(); // Create new objects added to prefab int32 deserializeSceneObjectIndex = sceneObjects->Count(); @@ -786,7 +784,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr } // Cache connection for fast lookup - diffPrefabObjectIdToDataIndex.Add(obj->GetPrefabObjectID(), i); + diffPrefabObjectIdToDataIndex[obj->GetPrefabObjectID()] = i; // Strip unwanted data data.RemoveMember("ID"); @@ -796,7 +794,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr else { // Object if a new thing - newPrefabInstanceIdToDataIndex.Add(obj->GetSceneObjectId(), i); + newPrefabInstanceIdToDataIndex[obj->GetSceneObjectId()] = i; } } @@ -836,8 +834,8 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr for (auto i = newPrefabInstanceIdToDataIndex.Begin(); i.IsNotEnd(); ++i) { const auto prefabObjectId = Guid::New(); - newPrefabInstanceIdToPrefabObjectId.Add(i->Key, prefabObjectId); - modifier->IdsMapping.Add(i->Key, prefabObjectId); + newPrefabInstanceIdToPrefabObjectId[i->Key] = prefabObjectId; + modifier->IdsMapping[i->Key] = prefabObjectId; } // Add inverse IDs mapping to link added objects and references inside them to the prefab objects diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index de164343b..c7b42b7d2 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -374,13 +374,13 @@ bool PrefabManager::CreatePrefab(Actor* targetActor, const StringView& outputPat if (targetActor->HasParent()) { // Unlink from parent actor - objectInstanceIdToPrefabObjectId.Add(targetActor->GetParent()->GetID(), Guid::Empty); + objectInstanceIdToPrefabObjectId[targetActor->GetParent()->GetID()] = Guid::Empty; } for (int32 i = 0; i < sceneObjects->Count(); i++) { // Generate new IDs for the prefab objects (other than reference instance used to create prefab) const SceneObject* obj = sceneObjects->At(i); - objectInstanceIdToPrefabObjectId.Add(obj->GetSceneObjectId(), Guid::New()); + objectInstanceIdToPrefabObjectId[obj->GetSceneObjectId()] = Guid::New(); } { // Parse json to DOM document diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp index e9dc0421c..6cabbc0dd 100644 --- a/Source/Engine/Scripting/Scripting.cpp +++ b/Source/Engine/Scripting/Scripting.cpp @@ -1026,30 +1026,28 @@ bool Scripting::IsTypeFromGameScripts(MClass* type) void Scripting::RegisterObject(ScriptingObject* obj) { + const Guid id = obj->GetID(); ScopeLock lock(_objectsLocker); //ASSERT(!_objectsDictionary.ContainsValue(obj)); #if ENABLE_ASSERTION #if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING ScriptingObjectData other; - if (_objectsDictionary.TryGet(obj->GetID(), other)) + if (_objectsDictionary.TryGet(id, other)) #else ScriptingObject* other; - if (_objectsDictionary.TryGet(obj->GetID(), other)) + if (_objectsDictionary.TryGet(id, other)) #endif { // Something went wrong... - LOG(Error, "Objects registry already contains object with ID={0} (type '{3}')! Trying to register object {1} (type '{2}').", obj->GetID(), obj->ToString(), String(obj->GetClass()->GetFullName()), String(other->GetClass()->GetFullName())); - _objectsDictionary.Remove(obj->GetID()); + LOG(Error, "Objects registry already contains object with ID={0} (type '{3}')! Trying to register object {1} (type '{2}').", id, obj->ToString(), String(obj->GetClass()->GetFullName()), String(other->GetClass()->GetFullName())); } -#else - ASSERT(!_objectsDictionary.ContainsKey(obj->_id)); #endif #if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING LOG(Info, "[RegisterObject] obj = 0x{0:x}, {1}", (uint64)obj, String(ScriptingObjectData(obj).TypeName)); #endif - _objectsDictionary.Add(obj->GetID(), obj); + _objectsDictionary[id] = obj; } void Scripting::UnregisterObject(ScriptingObject* obj) From e177aec5fadf112232b8f819b88331d86cd6989f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Nov 2023 12:19:54 +0100 Subject: [PATCH 297/546] Codestyle --- Source/Engine/Core/Math/Math.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Core/Math/Math.h b/Source/Engine/Core/Math/Math.h index 49a052771..a3c05fce4 100644 --- a/Source/Engine/Core/Math/Math.h +++ b/Source/Engine/Core/Math/Math.h @@ -728,9 +728,7 @@ namespace Math /// /// Returns value based on comparand. The main purpose of this function is to avoid branching based on floating point comparison which can be avoided via compiler intrinsics. /// - /// - /// Please note that this doesn't define what happens in the case of NaNs as there might be platform specific differences. - /// + /// Please note that this doesn't define what happens in the case of NaNs as there might be platform specific differences. /// Comparand the results are based on. /// The result value if comparand >= 0. /// The result value if comparand < 0. @@ -890,8 +888,9 @@ namespace Math { return Lerp(a, b, alpha < 0.5f ? InterpCircularIn(0.f, 1.f, alpha * 2.f) * 0.5f : InterpCircularOut(0.f, 1.f, alpha * 2.f - 1.f) * 0.5f + 0.5f); } + /// - /// PingPongs the value , so that it is never larger than and never smaller than 0. + /// Ping pongs the value , so that it is never larger than and never smaller than 0. /// /// /// From 719498e99bd0468cfd8aeb726b03ce9144480401 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Nov 2023 13:08:37 +0100 Subject: [PATCH 298/546] Adjust `MissingScriptEditor` layout for UI --- .../Dedicated/MissingScriptEditor.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index 8fb742b5e..7e0c6f38c 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -37,41 +37,32 @@ public class MissingScriptEditor : GenericEditor Parent = _dropPanel, Height = 64, }; - _replaceScriptButton = new Button { Text = "Replace Script", TooltipText = "Replaces the missing script with a given script type", AnchorPreset = AnchorPresets.TopCenter, - Width = 240, - Height = 24, - X = -120, - Y = 0, + Bounds = new Rectangle(-120, 0, 240, 24), Parent = replaceScriptPanel, }; _replaceScriptButton.Clicked += OnReplaceScriptButtonClicked; - var replaceAllLabel = new Label { Text = "Replace all matching missing scripts", TooltipText = "Whether or not to apply this script change to all scripts missing the same type.", AnchorPreset = AnchorPresets.BottomCenter, - Y = -34, + Y = -38, Parent = replaceScriptPanel, }; - replaceAllLabel.X -= FlaxEngine.GUI.Style.Current.FontSmall.MeasureText(replaceAllLabel.Text).X; - _shouldReplaceAllCheckbox = new CheckBox { TooltipText = replaceAllLabel.TooltipText, AnchorPreset = AnchorPresets.BottomCenter, - Y = -34, + Y = -38, Parent = replaceScriptPanel, }; - - float centerDifference = (_shouldReplaceAllCheckbox.Right - replaceAllLabel.Left) / 2; - replaceAllLabel.X += centerDifference; - _shouldReplaceAllCheckbox.X += centerDifference; + _shouldReplaceAllCheckbox.X -= _replaceScriptButton.Width * 0.5f + 0.5f; + replaceAllLabel.X -= 52; base.Initialize(layout); } From d9b90c952028f019bd21ba364a748936ef492bd8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Nov 2023 13:50:18 +0100 Subject: [PATCH 299/546] Fix crash in `MissingScript` if script is still missing after deserialization #1924 --- .../Engine/Level/Components/MissingScript.h | 19 +--------------- Source/Engine/Level/SceneObjectsFactory.cpp | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Source/Engine/Level/Components/MissingScript.h b/Source/Engine/Level/Components/MissingScript.h index bfb73498f..7b351bd82 100644 --- a/Source/Engine/Level/Components/MissingScript.h +++ b/Source/Engine/Level/Components/MissingScript.h @@ -42,24 +42,7 @@ public: /// /// Field for assigning new script to transfer data to. /// - API_PROPERTY() void SetReferenceScript(const ScriptingObjectReference