diff --git a/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs b/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs index ec71348cf..5b2cfc144 100644 --- a/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ParticleEffectEditor.cs @@ -25,9 +25,20 @@ namespace FlaxEditor.CustomEditors.Dedicated get { // All selected particle effects use the same system - var effect = (ParticleEffect)Values[0]; - var system = effect.ParticleSystem; - return system != null && Values.TrueForAll(x => (x as ParticleEffect)?.ParticleSystem == system); + var effect = Values[0] as ParticleEffect; + var system = effect ? effect.ParticleSystem : null; + if (system && Values.TrueForAll(x => x is ParticleEffect fx && fx && fx.ParticleSystem == system)) + { + // All parameters can be accessed + var parameters = effect.Parameters; + foreach (var parameter in parameters) + { + if (!parameter) + return false; + } + return true; + } + return false; } } diff --git a/Source/Editor/CustomEditors/Editors/InputEditor.cs b/Source/Editor/CustomEditors/Editors/InputEditor.cs index 416626e81..7b2682a84 100644 --- a/Source/Editor/CustomEditors/Editors/InputEditor.cs +++ b/Source/Editor/CustomEditors/Editors/InputEditor.cs @@ -1,8 +1,10 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System.Collections.Generic; +using FlaxEditor.CustomEditors.GUI; +using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; using FlaxEngine; -using FlaxEngine.GUI; namespace FlaxEditor.CustomEditors.Editors { @@ -12,7 +14,7 @@ namespace FlaxEditor.CustomEditors.Editors [CustomEditor(typeof(InputEvent)), DefaultEditor] public class InputEventEditor : CustomEditor { - private Dropdown _dropdown; + private ComboBox _comboBox; /// public override DisplayStyle Style => DisplayStyle.Inline; @@ -20,23 +22,30 @@ namespace FlaxEditor.CustomEditors.Editors /// public override void Initialize(LayoutElementsContainer layout) { - var dropdownElement = layout.Custom(); - _dropdown = dropdownElement.CustomControl; - var names = new List(); + LinkedLabel.SetupContextMenu += OnSetupContextMenu; + var comboBoxElement = layout.ComboBox(); + _comboBox = comboBoxElement.ComboBox; + var names = new List(); foreach (var mapping in Input.ActionMappings) { if (!names.Contains(mapping.Name)) names.Add(mapping.Name); } - _dropdown.Items = names; + _comboBox.Items = names; if (Values[0] is InputEvent inputEvent && names.Contains(inputEvent.Name)) - _dropdown.SelectedItem = inputEvent.Name; - _dropdown.SelectedIndexChanged += OnSelectedIndexChanged; + _comboBox.SelectedItem = inputEvent.Name; + _comboBox.SelectedIndexChanged += OnSelectedIndexChanged; } - private void OnSelectedIndexChanged(Dropdown dropdown) + private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkededitor) { - SetValue(new InputEvent(dropdown.SelectedItem)); + var button = menu.AddButton("Set to null"); + button.Clicked += () => _comboBox.SelectedItem = null; + } + + private void OnSelectedIndexChanged(ComboBox comboBox) + { + SetValue(comboBox.SelectedItem == null ? null : new InputEvent(comboBox.SelectedItem)); } /// @@ -49,17 +58,21 @@ namespace FlaxEditor.CustomEditors.Editors } else { - if (Values[0] is InputEvent inputEvent && _dropdown.Items.Contains(inputEvent.Name)) - _dropdown.SelectedItem = inputEvent.Name; + if (Values[0] is InputEvent inputEvent && _comboBox.Items.Contains(inputEvent.Name)) + _comboBox.SelectedItem = inputEvent.Name; + else + _comboBox.SelectedItem = null; } } /// protected override void Deinitialize() { - if (_dropdown != null) - _dropdown.SelectedIndexChanged -= OnSelectedIndexChanged; - _dropdown = null; + if (LinkedLabel != null) + LinkedLabel.SetupContextMenu -= OnSetupContextMenu; + if (_comboBox != null) + _comboBox.SelectedIndexChanged -= OnSelectedIndexChanged; + _comboBox = null; } } @@ -69,7 +82,7 @@ namespace FlaxEditor.CustomEditors.Editors [CustomEditor(typeof(InputAxis)), DefaultEditor] public class InputAxisEditor : CustomEditor { - private Dropdown _dropdown; + private ComboBox _comboBox; /// public override DisplayStyle Style => DisplayStyle.Inline; @@ -77,23 +90,30 @@ namespace FlaxEditor.CustomEditors.Editors /// public override void Initialize(LayoutElementsContainer layout) { - var dropdownElement = layout.Custom(); - _dropdown = dropdownElement.CustomControl; - var names = new List(); + LinkedLabel.SetupContextMenu += OnSetupContextMenu; + var comboBoxElement = layout.ComboBox(); + _comboBox = comboBoxElement.ComboBox; + var names = new List(); foreach (var mapping in Input.AxisMappings) { if (!names.Contains(mapping.Name)) names.Add(mapping.Name); } - _dropdown.Items = names; + _comboBox.Items = names; if (Values[0] is InputAxis inputAxis && names.Contains(inputAxis.Name)) - _dropdown.SelectedItem = inputAxis.Name; - _dropdown.SelectedIndexChanged += OnSelectedIndexChanged; + _comboBox.SelectedItem = inputAxis.Name; + _comboBox.SelectedIndexChanged += OnSelectedIndexChanged; } - private void OnSelectedIndexChanged(Dropdown dropdown) + private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkededitor) { - SetValue(new InputAxis(dropdown.SelectedItem)); + var button = menu.AddButton("Set to null"); + button.Clicked += () => _comboBox.SelectedItem = null; + } + + private void OnSelectedIndexChanged(ComboBox comboBox) + { + SetValue(comboBox.SelectedItem == null ? null : new InputAxis(comboBox.SelectedItem)); } /// @@ -106,17 +126,21 @@ namespace FlaxEditor.CustomEditors.Editors } else { - if (Values[0] is InputAxis inputAxis && _dropdown.Items.Contains(inputAxis.Name)) - _dropdown.SelectedItem = inputAxis.Name; + if (Values[0] is InputAxis inputAxis && _comboBox.Items.Contains(inputAxis.Name)) + _comboBox.SelectedItem = inputAxis.Name; + else + _comboBox.SelectedItem = null; } } /// protected override void Deinitialize() { - if (_dropdown != null) - _dropdown.SelectedIndexChanged -= OnSelectedIndexChanged; - _dropdown = null; + if (LinkedLabel != null) + LinkedLabel.SetupContextMenu -= OnSetupContextMenu; + if (_comboBox != null) + _comboBox.SelectedIndexChanged -= OnSelectedIndexChanged; + _comboBox = null; } } } diff --git a/Source/Editor/CustomEditors/Editors/TagEditor.cs b/Source/Editor/CustomEditors/Editors/TagEditor.cs index 3d2dd86aa..61e0bba7d 100644 --- a/Source/Editor/CustomEditors/Editors/TagEditor.cs +++ b/Source/Editor/CustomEditors/Editors/TagEditor.cs @@ -674,9 +674,9 @@ namespace FlaxEditor.CustomEditors.Editors } set { - if (Values[0] is Tag[]) + if (Values[0] is Tag[] || Values.Type.Type == typeof(Tag[])) SetValue(value); - if (Values[0] is List) + else if (Values[0] is List || Values.Type.Type == typeof(List)) SetValue(new List(value)); } } diff --git a/Source/Editor/GUI/Dialogs/Dialog.cs b/Source/Editor/GUI/Dialogs/Dialog.cs index 8910cfe49..07fc3ff0d 100644 --- a/Source/Editor/GUI/Dialogs/Dialog.cs +++ b/Source/Editor/GUI/Dialogs/Dialog.cs @@ -290,7 +290,11 @@ namespace FlaxEditor.GUI.Dialogs OnCancel(); return true; case KeyboardKeys.Tab: - Root?.Navigate(NavDirection.Next); + if (Root != null) + { + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + Root.Navigate(shiftDown ? NavDirection.Previous : NavDirection.Next); + } return true; } return false; diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index 23ca7246b..ec878a407 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -221,6 +221,23 @@ namespace FlaxEditor.Surface return; } + if (_middleMouseDown) + { + // Calculate delta + var delta = location - _middleMouseDownPos; + if (delta.LengthSquared > 0.01f) + { + // Move view + _mouseMoveAmount += delta.Length; + _rootControl.Location += delta; + _middleMouseDownPos = location; + Cursor = CursorType.SizeAll; + } + + // Handled + return; + } + // Check if user is selecting or moving node(s) if (_leftMouseDown) { @@ -280,6 +297,11 @@ namespace FlaxEditor.Surface _rightMouseDown = false; Cursor = CursorType.Default; } + if (_middleMouseDown) + { + _middleMouseDown = false; + Cursor = CursorType.Default; + } _isMovingSelection = false; ConnectingEnd(null); @@ -391,6 +413,7 @@ namespace FlaxEditor.Surface _isMovingSelection = false; _rightMouseDown = false; _leftMouseDown = false; + _middleMouseDown = false; return true; } @@ -410,6 +433,11 @@ namespace FlaxEditor.Surface _rightMouseDown = true; _rightMouseDownPos = location; } + if (button == MouseButton.Middle) + { + _middleMouseDown = true; + _middleMouseDownPos = location; + } // Check if any node is under the mouse SurfaceControl controlUnderMouse = GetControlUnderMouse(); @@ -455,7 +483,7 @@ namespace FlaxEditor.Surface Focus(); return true; } - if (_rightMouseDown) + if (_rightMouseDown || _middleMouseDown) { // Start navigating StartMouseCapture(); @@ -524,6 +552,13 @@ namespace FlaxEditor.Surface } _mouseMoveAmount = 0; } + if (_middleMouseDown && button == MouseButton.Middle) + { + _middleMouseDown = false; + EndMouseCapture(); + Cursor = CursorType.Default; + _mouseMoveAmount = 0; + } // Base bool handled = base.OnMouseUp(location, button); @@ -534,6 +569,7 @@ namespace FlaxEditor.Surface // Clear flags _rightMouseDown = false; _leftMouseDown = false; + _middleMouseDown = false; return true; } diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 2b965dfbb..3fe722091 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -60,6 +60,11 @@ namespace FlaxEditor.Surface /// protected bool _rightMouseDown; + /// + /// The middle mouse down flag. + /// + protected bool _middleMouseDown; + /// /// The left mouse down position. /// @@ -70,6 +75,11 @@ namespace FlaxEditor.Surface /// protected Float2 _rightMouseDownPos = Float2.Minimum; + /// + /// The middle mouse down position. + /// + protected Float2 _middleMouseDownPos = Float2.Minimum; + /// /// The mouse position. /// diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index d0fc07f6b..4192f21b0 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -46,7 +46,7 @@ namespace FlaxEditor.Windows private TextBox _itemsSearchBox; private ViewDropdown _viewDropdown; private SortType _sortType; - private bool _showEngineFiles = true, _showPluginsFiles = true, _showAllFiles = true; + private bool _showEngineFiles = true, _showPluginsFiles = true, _showAllFiles = true, _showGeneratedFiles = false; private RootContentTreeNode _root; @@ -106,6 +106,19 @@ namespace FlaxEditor.Windows } } + internal bool ShowGeneratedFiles + { + get => _showGeneratedFiles; + set + { + if (_showGeneratedFiles != value) + { + _showGeneratedFiles = value; + RefreshView(); + } + } + } + internal bool ShowAllFiles { get => _showAllFiles; @@ -314,6 +327,12 @@ namespace FlaxEditor.Windows b.Checked = ShowPluginsFiles; b.CloseMenuOnClick = false; b.AutoCheck = true; + + b = show.ContextMenu.AddButton("Generated files", () => ShowGeneratedFiles = !ShowGeneratedFiles); + b.TooltipText = "Shows generated files"; + b.Checked = ShowGeneratedFiles; + b.CloseMenuOnClick = false; + b.AutoCheck = true; b = show.ContextMenu.AddButton("All files", () => ShowAllFiles = !ShowAllFiles); b.TooltipText = "Shows all files including other than assets and source code"; @@ -973,6 +992,8 @@ namespace FlaxEditor.Windows var items = target.Folder.Children; if (!_showAllFiles) items = items.Where(x => !(x is FileItem)).ToList(); + if (!_showGeneratedFiles) + items = items.Where(x => !(x.Path.EndsWith(".Gen.cs", StringComparison.Ordinal) || x.Path.EndsWith(".Gen.h", StringComparison.Ordinal) || x.Path.EndsWith(".Gen.cpp", StringComparison.Ordinal) || x.Path.EndsWith(".csproj", StringComparison.Ordinal) || x.Path.Contains(".CSharp"))).ToList(); _view.ShowItems(items, _sortType, false, true); } } @@ -1149,6 +1170,7 @@ namespace FlaxEditor.Windows writer.WriteAttributeString("ShowEngineFiles", ShowEngineFiles.ToString()); writer.WriteAttributeString("ShowPluginsFiles", ShowPluginsFiles.ToString()); writer.WriteAttributeString("ShowAllFiles", ShowAllFiles.ToString()); + writer.WriteAttributeString("ShowGeneratedFiles", ShowGeneratedFiles.ToString()); writer.WriteAttributeString("ViewType", _view.ViewType.ToString()); } @@ -1166,6 +1188,8 @@ namespace FlaxEditor.Windows ShowPluginsFiles = value2; if (bool.TryParse(node.GetAttribute("ShowAllFiles"), out value2)) ShowAllFiles = value2; + if (bool.TryParse(node.GetAttribute("ShowGeneratedFiles"), out value2)) + ShowGeneratedFiles = value2; if (Enum.TryParse(node.GetAttribute("ViewType"), out ContentViewType viewType)) _view.ViewType = viewType; } diff --git a/Source/Editor/Windows/EditorWindow.cs b/Source/Editor/Windows/EditorWindow.cs index 1ff0363f1..ff0673c11 100644 --- a/Source/Editor/Windows/EditorWindow.cs +++ b/Source/Editor/Windows/EditorWindow.cs @@ -192,6 +192,13 @@ namespace FlaxEditor.Windows /// public override bool OnKeyDown(KeyboardKeys key) { + // Prevent closing the editor window when using RMB + Ctrl + W to slow down the camera flight + if (Editor.Options.Options.Input.CloseTab.Process(this, key)) + { + if (Root.GetMouseButton(MouseButton.Right)) + return true; + } + if (base.OnKeyDown(key)) return true; @@ -207,7 +214,8 @@ namespace FlaxEditor.Windows case KeyboardKeys.Tab: if (CanUseNavigation && Root != null) { - Root.Navigate(NavDirection.Next); + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + Root.Navigate(shiftDown ? NavDirection.Previous : NavDirection.Next); return true; } break; diff --git a/Source/Editor/Windows/Profiler/CPU.cs b/Source/Editor/Windows/Profiler/CPU.cs index 36fbf21f4..fd4061276 100644 --- a/Source/Editor/Windows/Profiler/CPU.cs +++ b/Source/Editor/Windows/Profiler/CPU.cs @@ -429,21 +429,8 @@ namespace FlaxEditor.Windows.Profiler private void UpdateTable(ref ViewRange viewRange) { _table.IsLayoutLocked = true; - int idx = 0; - while (_table.Children.Count > idx) - { - var child = _table.Children[idx]; - if (child is Row row) - { - _tableRowsCache.Add(row); - child.Parent = null; - } - else - { - idx++; - } - } + RecycleTableRows(_table, _tableRowsCache); UpdateTableInner(ref viewRange); _table.UnlockChildrenRecursive(); diff --git a/Source/Editor/Windows/Profiler/GPU.cs b/Source/Editor/Windows/Profiler/GPU.cs index 2cf75a9aa..4ed18691a 100644 --- a/Source/Editor/Windows/Profiler/GPU.cs +++ b/Source/Editor/Windows/Profiler/GPU.cs @@ -298,21 +298,7 @@ namespace FlaxEditor.Windows.Profiler private void UpdateTable() { _table.IsLayoutLocked = true; - int idx = 0; - while (_table.Children.Count > idx) - { - var child = _table.Children[idx]; - if (child is Row row) - { - _tableRowsCache.Add(row); - child.Parent = null; - } - else - { - idx++; - } - } - _table.LockChildrenRecursive(); + RecycleTableRows(_table, _tableRowsCache); UpdateTableInner(); diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs index f0b93a03c..dbee0e8e7 100644 --- a/Source/Editor/Windows/Profiler/Network.cs +++ b/Source/Editor/Windows/Profiler/Network.cs @@ -1,8 +1,32 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System; +using System.Collections.Generic; +using FlaxEditor.GUI; using FlaxEngine; using FlaxEngine.GUI; +namespace FlaxEngine +{ + partial class ProfilingTools + { + partial struct NetworkEventStat + { + /// + /// Gets the event name. + /// + public unsafe string Name + { + get + { + fixed (byte* name = Name0) + return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(new IntPtr(name)); + } + } + } + } +} + namespace FlaxEditor.Windows.Profiler { /// @@ -13,6 +37,10 @@ namespace FlaxEditor.Windows.Profiler { private readonly SingleChart _dataSentChart; private readonly SingleChart _dataReceivedChart; + private readonly Table _tableRpc; + private readonly Table _tableRep; + private SamplesBuffer _events; + private List _tableRowsCache; private FlaxEngine.Networking.NetworkDriverStats _prevStats; public Network() @@ -48,11 +76,10 @@ namespace FlaxEditor.Windows.Profiler Parent = layout, }; _dataReceivedChart.SelectedSampleChanged += OnSelectedSampleChanged; - } - private static string FormatSampleBytes(float v) - { - return Utilities.Utils.FormatBytesCount((ulong)v); + // Tables + _tableRpc = InitTable(layout, "RPC Name"); + _tableRep = InitTable(layout, "Replication Name"); } /// @@ -60,21 +87,30 @@ namespace FlaxEditor.Windows.Profiler { _dataSentChart.Clear(); _dataReceivedChart.Clear(); + _events?.Clear(); } /// public override void Update(ref SharedUpdateData sharedData) { - var peer = FlaxEngine.Networking.NetworkManager.Peer; - if (peer == null) + // Gather peer stats + var peers = FlaxEngine.Networking.NetworkPeer.Peers; + var stats = new FlaxEngine.Networking.NetworkDriverStats(); + foreach (var peer in peers) { - _prevStats = new FlaxEngine.Networking.NetworkDriverStats(); - return; + var peerStats = peer.NetworkDriver.GetStats(); + stats.TotalDataSent += peerStats.TotalDataSent; + stats.TotalDataReceived += peerStats.TotalDataReceived; } - var stats = peer.NetworkDriver.GetStats(); _dataSentChart.AddSample(Mathf.Max((long)stats.TotalDataSent - (long)_prevStats.TotalDataSent, 0)); _dataReceivedChart.AddSample(Mathf.Max((long)stats.TotalDataReceived - (long)_prevStats.TotalDataReceived, 0)); _prevStats = stats; + + // Gather network events + var events = ProfilingTools.EventsNetwork; + if (_events == null) + _events = new SamplesBuffer(); + _events.Add(events); } /// @@ -82,6 +118,159 @@ namespace FlaxEditor.Windows.Profiler { _dataSentChart.SelectedSampleIndex = selectedFrame; _dataReceivedChart.SelectedSampleIndex = selectedFrame; + + // Update events tables + if (_events != null) + { + if (_tableRowsCache == null) + _tableRowsCache = new List(); + _tableRpc.IsLayoutLocked = true; + _tableRep.IsLayoutLocked = true; + RecycleTableRows(_tableRpc, _tableRowsCache); + RecycleTableRows(_tableRep, _tableRowsCache); + + var events = _events.Get(selectedFrame); + var rowCount = Int2.Zero; + if (events != null && events.Length != 0) + { + var rowColor2 = Style.Current.Background * 1.4f; + for (int i = 0; i < events.Length; i++) + { + var e = events[i]; + var name = e.Name; + var isRpc = name.Contains("::", StringComparison.Ordinal); + + Row row; + if (_tableRowsCache.Count != 0) + { + var last = _tableRowsCache.Count - 1; + row = _tableRowsCache[last]; + _tableRowsCache.RemoveAt(last); + } + else + { + row = new Row + { + Values = new object[5], + }; + } + { + // Name + row.Values[0] = name; + + // Count + row.Values[1] = (int)e.Count; + + // Data Size + row.Values[2] = (int)e.DataSize; + + // Message Size + row.Values[3] = (int)e.MessageSize; + + // Receivers + row.Values[4] = (float)e.Receivers / (float)e.Count; + } + + var table = isRpc ? _tableRpc : _tableRep; + row.Width = table.Width; + row.BackgroundColor = rowCount[isRpc ? 0 : 1] % 2 == 0 ? rowColor2 : Color.Transparent; + row.Parent = table; + if (isRpc) + rowCount.X++; + else + rowCount.Y++; + } + } + + _tableRpc.Visible = rowCount.X != 0; + _tableRep.Visible = rowCount.Y != 0; + _tableRpc.Children.Sort(SortRows); + _tableRep.Children.Sort(SortRows); + + _tableRpc.UnlockChildrenRecursive(); + _tableRpc.PerformLayout(); + _tableRep.UnlockChildrenRecursive(); + _tableRep.PerformLayout(); + } + } + + /// + public override void OnDestroy() + { + _tableRowsCache?.Clear(); + + base.OnDestroy(); + } + + private static Table InitTable(ContainerControl parent, string name) + { + var headerColor = Style.Current.LightBackground; + var table = new Table + { + Columns = new[] + { + new ColumnDefinition + { + UseExpandCollapseMode = true, + CellAlignment = TextAlignment.Near, + Title = name, + TitleBackgroundColor = headerColor, + }, + new ColumnDefinition + { + Title = "Count", + TitleBackgroundColor = headerColor, + }, + new ColumnDefinition + { + Title = "Data Size", + TitleBackgroundColor = headerColor, + FormatValue = FormatCellBytes, + }, + new ColumnDefinition + { + Title = "Message Size", + TitleBackgroundColor = headerColor, + FormatValue = FormatCellBytes, + }, + new ColumnDefinition + { + Title = "Receivers", + TitleBackgroundColor = headerColor, + }, + }, + Splits = new[] + { + 0.40f, + 0.15f, + 0.15f, + 0.15f, + 0.15f, + }, + Parent = parent, + }; + return table; + } + + private static string FormatSampleBytes(float v) + { + return Utilities.Utils.FormatBytesCount((ulong)v); + } + + private static string FormatCellBytes(object x) + { + return Utilities.Utils.FormatBytesCount((int)x); + } + + private static int SortRows(Control x, Control y) + { + if (x is Row xRow && y is Row yRow) + { + var xDataSize = (int)xRow.Values[2]; + var yDataSize = (int)yRow.Values[2]; + return yDataSize - xDataSize; + } + return 0; } } } diff --git a/Source/Editor/Windows/Profiler/ProfilerMode.cs b/Source/Editor/Windows/Profiler/ProfilerMode.cs index 0cc1a39ee..b8d2f0c4c 100644 --- a/Source/Editor/Windows/Profiler/ProfilerMode.cs +++ b/Source/Editor/Windows/Profiler/ProfilerMode.cs @@ -1,6 +1,8 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using System.Collections.Generic; +using FlaxEditor.GUI; using FlaxEditor.GUI.Tabs; using FlaxEngine; @@ -135,5 +137,28 @@ namespace FlaxEditor.Windows.Profiler { SelectedSampleChanged?.Invoke(frameIndex); } + + /// + /// Recycles all table rows to be reused. + /// + /// The table. + /// The output cache. + protected static void RecycleTableRows(Table table, List rowsCache) + { + int idx = 0; + while (table.Children.Count > idx) + { + var child = table.Children[idx]; + if (child is Row row) + { + rowsCache.Add(row); + child.Parent = null; + } + else + { + idx++; + } + } + } } } diff --git a/Source/Editor/Windows/Profiler/ProfilerWindow.cs b/Source/Editor/Windows/Profiler/ProfilerWindow.cs index f5a5c6f86..f68021358 100644 --- a/Source/Editor/Windows/Profiler/ProfilerWindow.cs +++ b/Source/Editor/Windows/Profiler/ProfilerWindow.cs @@ -93,7 +93,7 @@ namespace FlaxEditor.Windows.Profiler _liveRecordingButton = toolstrip.AddButton(editor.Icons.Play64); _liveRecordingButton.LinkTooltip("Live profiling events recording"); _liveRecordingButton.AutoCheck = true; - _liveRecordingButton.Clicked += () => _liveRecordingButton.Icon = LiveRecording ? editor.Icons.Stop64 : editor.Icons.Play64; + _liveRecordingButton.Clicked += OnLiveRecordingChanged; _clearButton = toolstrip.AddButton(editor.Icons.Rotate32, Clear); _clearButton.LinkTooltip("Clear data"); toolstrip.AddSeparator(); @@ -118,6 +118,12 @@ namespace FlaxEditor.Windows.Profiler _tabs.SelectedTabChanged += OnSelectedTabChanged; } + private void OnLiveRecordingChanged() + { + _liveRecordingButton.Icon = LiveRecording ? Editor.Icons.Stop64 : Editor.Icons.Play64; + ProfilingTools.Enabled = LiveRecording; + } + /// /// Adds the mode. /// diff --git a/Source/Editor/Windows/Profiler/SamplesBuffer.cs b/Source/Editor/Windows/Profiler/SamplesBuffer.cs index abc99cfd5..999156dca 100644 --- a/Source/Editor/Windows/Profiler/SamplesBuffer.cs +++ b/Source/Editor/Windows/Profiler/SamplesBuffer.cs @@ -49,6 +49,8 @@ namespace FlaxEditor.Windows.Profiler /// The sample value public T Get(int index) { + if (index >= _data.Length || _data.Length == 0) + return default; return index == -1 ? _data[_count - 1] : _data[index]; } diff --git a/Source/Engine/ContentImporters/ImportShader.cpp b/Source/Engine/ContentImporters/ImportShader.cpp index 9cfb1242c..bf5b5581d 100644 --- a/Source/Engine/ContentImporters/ImportShader.cpp +++ b/Source/Engine/ContentImporters/ImportShader.cpp @@ -32,14 +32,21 @@ CreateAssetResult ImportShader::Import(CreateAssetContext& context) LOG(Warning, "Empty shader source file."); return CreateAssetResult::Error; } + + // Ensure the source code has an empty line at the end (expected by glslang) + auto sourceCodeChunkSize = sourceCodeSize + 1; + if (sourceCodeText[sourceCodeSize - 1] != '\n') + sourceCodeChunkSize++; + const auto& sourceCodeChunk = context.Data.Header.Chunks[SourceCodeChunk]; - sourceCodeChunk->Data.Allocate(sourceCodeSize + 1); + sourceCodeChunk->Data.Allocate(sourceCodeChunkSize); const auto sourceCode = sourceCodeChunk->Get(); Platform::MemoryCopy(sourceCode, sourceCodeText.Get(), sourceCodeSize); + sourceCode[sourceCodeChunkSize - 2] = '\n'; // Encrypt source code - Encryption::EncryptBytes(sourceCode, sourceCodeSize); - sourceCode[sourceCodeSize] = 0; + Encryption::EncryptBytes(sourceCode, sourceCodeChunkSize - 1); + sourceCode[sourceCodeChunkSize - 1] = 0; // Set Custom Data with Header ShaderStorage::Header20 shaderHeader; diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index bafcd30b3..ec56861ba 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -102,9 +102,6 @@ int32 Engine::Main(const Char* cmdLine) Platform::SetHighDpiAwarenessEnabled(!CommandLine::Options.LowDPI.IsTrue()); Time::StartupTime = DateTime::Now(); -#if COMPILE_WITH_PROFILER - ProfilerCPU::Enabled = true; -#endif Globals::StartupFolder = Globals::BinariesFolder = Platform::GetMainDirectory(); #if USE_EDITOR Globals::StartupFolder /= TEXT("../../../.."); diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 423aae6a5..cfa71f97f 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -851,39 +851,46 @@ namespace FlaxEngine.Interop *assemblyFullName = NativeAllocStringAnsi(flaxEngineAssembly.FullName); return GetAssemblyHandle(flaxEngineAssembly); } + try + { + string assemblyPath = Marshal.PtrToStringUni(assemblyPathPtr); - string assemblyPath = Marshal.PtrToStringAnsi(assemblyPathPtr); - - Assembly assembly; + Assembly assembly; #if FLAX_EDITOR - // Load assembly from loaded bytes to prevent file locking in Editor - var assemblyBytes = File.ReadAllBytes(assemblyPath); - using MemoryStream stream = new MemoryStream(assemblyBytes); - var pdbPath = Path.ChangeExtension(assemblyPath, "pdb"); - if (File.Exists(pdbPath)) - { - // Load including debug symbols - using FileStream pdbStream = new FileStream(Path.ChangeExtension(assemblyPath, "pdb"), FileMode.Open); - assembly = scriptingAssemblyLoadContext.LoadFromStream(stream, pdbStream); - } - else - { - assembly = scriptingAssemblyLoadContext.LoadFromStream(stream); - } + // Load assembly from loaded bytes to prevent file locking in Editor + var assemblyBytes = File.ReadAllBytes(assemblyPath); + using MemoryStream stream = new MemoryStream(assemblyBytes); + var pdbPath = Path.ChangeExtension(assemblyPath, "pdb"); + if (File.Exists(pdbPath)) + { + // Load including debug symbols + using FileStream pdbStream = new FileStream(Path.ChangeExtension(assemblyPath, "pdb"), FileMode.Open); + assembly = scriptingAssemblyLoadContext.LoadFromStream(stream, pdbStream); + } + else + { + assembly = scriptingAssemblyLoadContext.LoadFromStream(stream); + } #else - // Load assembly from file - assembly = scriptingAssemblyLoadContext.LoadFromAssemblyPath(assemblyPath); + // Load assembly from file + assembly = scriptingAssemblyLoadContext.LoadFromAssemblyPath(assemblyPath); #endif - if (assembly == null) - return new ManagedHandle(); - NativeLibrary.SetDllImportResolver(assembly, NativeLibraryImportResolver); + if (assembly == null) + return new ManagedHandle(); + NativeLibrary.SetDllImportResolver(assembly, NativeLibraryImportResolver); - // Assemblies loaded via streams have no Location: https://github.com/dotnet/runtime/issues/12822 - AssemblyLocations.Add(assembly.FullName, assemblyPath); + // Assemblies loaded via streams have no Location: https://github.com/dotnet/runtime/issues/12822 + AssemblyLocations.Add(assembly.FullName, assemblyPath); - *assemblyName = NativeAllocStringAnsi(assembly.GetName().Name); - *assemblyFullName = NativeAllocStringAnsi(assembly.FullName); - return GetAssemblyHandle(assembly); + *assemblyName = NativeAllocStringAnsi(assembly.GetName().Name); + *assemblyFullName = NativeAllocStringAnsi(assembly.FullName); + return GetAssemblyHandle(assembly); + } + catch (Exception ex) + { + Debug.LogException(ex); + } + return new ManagedHandle(); } [UnmanagedCallersOnly] diff --git a/Source/Engine/Main/Main.Build.cs b/Source/Engine/Main/Main.Build.cs index 27c9fb611..120d0bc5c 100644 --- a/Source/Engine/Main/Main.Build.cs +++ b/Source/Engine/Main/Main.Build.cs @@ -81,5 +81,6 @@ public class Main : EngineModule /// public override void GetFilesToDeploy(List files) { + files.Add(Path.Combine(FolderPath, "Android/android_native_app_glue.h")); } } diff --git a/Source/Engine/Networking/NetworkInternal.h b/Source/Engine/Networking/NetworkInternal.h index 521e8a7a2..9484337d1 100644 --- a/Source/Engine/Networking/NetworkInternal.h +++ b/Source/Engine/Networking/NetworkInternal.h @@ -3,6 +3,9 @@ #pragma once #include "Types.h" +#if COMPILE_WITH_PROFILER +#include "Engine/Core/Collections/Dictionary.h" +#endif enum class NetworkMessageIDs : uint8 { @@ -35,4 +38,22 @@ public: static void OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer); static void OnNetworkMessageObjectRole(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer); static void OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer); + +#if COMPILE_WITH_PROFILER + + struct ProfilerEvent + { + uint16 Count = 0; + uint16 DataSize = 0; + uint16 MessageSize = 0; + uint16 Receivers = 0; + }; + + /// + /// Enables network usage profiling tools. Captures network objects replication and RPCs send statistics. + /// + static bool EnableProfiling; + + static Dictionary, ProfilerEvent> ProfilerEvents; +#endif }; diff --git a/Source/Engine/Networking/NetworkPeer.cpp b/Source/Engine/Networking/NetworkPeer.cpp index d86824156..b39b617e0 100644 --- a/Source/Engine/Networking/NetworkPeer.cpp +++ b/Source/Engine/Networking/NetworkPeer.cpp @@ -8,9 +8,10 @@ #include "Engine/Platform/CPUInfo.h" #include "Engine/Profiler/ProfilerCPU.h" +Array NetworkPeer::Peers; + namespace { - Array Peers; uint32 LastHostId = 0; } diff --git a/Source/Engine/Networking/NetworkPeer.h b/Source/Engine/Networking/NetworkPeer.h index 06d8c1ab9..6b36e3278 100644 --- a/Source/Engine/Networking/NetworkPeer.h +++ b/Source/Engine/Networking/NetworkPeer.h @@ -15,6 +15,9 @@ API_CLASS(sealed, NoSpawn, Namespace = "FlaxEngine.Networking") class FLAXENGINE { DECLARE_SCRIPTING_TYPE_WITH_CONSTRUCTOR_IMPL(NetworkPeer, ScriptingObject); + // List with all active peers. + API_FIELD(ReadOnly) static Array Peers; + public: int HostId = -1; NetworkConfig Config; diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index e83a5b8c8..a44072347 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -40,6 +40,11 @@ bool NetworkReplicator::EnableLog = false; #define NETWORK_REPLICATOR_LOG(messageType, format, ...) #endif +#if COMPILE_WITH_PROFILER +bool NetworkInternal::EnableProfiling = false; +Dictionary, NetworkInternal::ProfilerEvent> NetworkInternal::ProfilerEvents; +#endif + PACK_STRUCT(struct NetworkMessageObjectReplicate { NetworkMessageIDs ID = NetworkMessageIDs::ObjectReplicate; @@ -1806,6 +1811,7 @@ void NetworkInternal::NetworkReplicatorUpdate() NetworkMessage msg = peer->BeginSendMessage(); msg.WriteStructure(msgData); msg.WriteBytes(stream->GetBuffer(), msgDataSize); + uint32 dataSize = msgDataSize, messageSize = msg.Length; if (isClient) peer->EndSendMessage(NetworkChannelType::Unreliable, msg); else @@ -1824,6 +1830,8 @@ void NetworkInternal::NetworkReplicatorUpdate() msg = peer->BeginSendMessage(); msg.WriteStructure(msgDataPart); msg.WriteBytes(stream->GetBuffer() + msgDataPart.PartStart, msgDataPart.PartSize); + messageSize += msg.Length; + dataSize += msgDataPart.PartSize; dataStart += msgDataPart.PartSize; if (isClient) peer->EndSendMessage(NetworkChannelType::Unreliable, msg); @@ -1832,7 +1840,18 @@ void NetworkInternal::NetworkReplicatorUpdate() } ASSERT_LOW_LAYER(dataStart == size); - // TODO: stats for bytes send per object type +#if COMPILE_WITH_PROFILER + // Network stats recording + if (EnableProfiling) + { + const Pair name(obj->GetTypeHandle(), StringAnsiView::Empty); + auto& profileEvent = ProfilerEvents[name]; + profileEvent.Count++; + profileEvent.DataSize += dataSize; + profileEvent.MessageSize += messageSize; + profileEvent.Receivers += isClient ? 1 : CachedTargets.Count(); + } +#endif } } @@ -1873,6 +1892,7 @@ void NetworkInternal::NetworkReplicatorUpdate() NetworkMessage msg = peer->BeginSendMessage(); msg.WriteStructure(msgData); msg.WriteBytes(e.ArgsData.Get(), e.ArgsData.Length()); + uint32 dataSize = e.ArgsData.Length(), messageSize = msg.Length, receivers = 0; NetworkChannelType channel = (NetworkChannelType)e.Info.Channel; if (e.Info.Server && isClient) { @@ -1882,13 +1902,27 @@ void NetworkInternal::NetworkReplicatorUpdate() NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}::{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString()); #endif peer->EndSendMessage(channel, msg); + receivers = 1; } else if (e.Info.Client && (isServer || isHost)) { // Server -> Client(s) BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId); peer->EndSendMessage(channel, msg, CachedTargets); + receivers = CachedTargets.Count(); } + +#if COMPILE_WITH_PROFILER + // Network stats recording + if (EnableProfiling && receivers) + { + auto& profileEvent = ProfilerEvents[e.Name]; + profileEvent.Count++; + profileEvent.DataSize += dataSize; + profileEvent.MessageSize += messageSize; + profileEvent.Receivers += receivers; + } +#endif } RpcQueue.Clear(); } diff --git a/Source/Engine/Networking/NetworkStats.h b/Source/Engine/Networking/NetworkStats.h index 42c946aa0..e6f031f3b 100644 --- a/Source/Engine/Networking/NetworkStats.h +++ b/Source/Engine/Networking/NetworkStats.h @@ -8,7 +8,7 @@ /// /// The network transport driver statistics container. Contains information about INetworkDriver usage and performance. /// -API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkDriverStats +API_STRUCT(Namespace="FlaxEngine.Networking", NoDefault) struct FLAXENGINE_API NetworkDriverStats { DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkDriverStats); diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index 8a767ba22..788a4263f 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -402,7 +402,7 @@ SceneRenderTask* ParticleEffect::GetRenderTask() const #if USE_EDITOR -Array ParticleEffect::GetParametersOverrides() +Array& ParticleEffect::GetParametersOverrides() { CacheModifiedParameters(); return _parametersOverrides; @@ -461,7 +461,6 @@ void ParticleEffect::CacheModifiedParameters() { if (_parameters.IsEmpty()) return; - _parametersOverrides.Clear(); auto& parameters = GetParameters(); for (auto& param : parameters) diff --git a/Source/Engine/Particles/ParticleEffect.h b/Source/Engine/Particles/ParticleEffect.h index bfee23a3b..9e9792a4c 100644 --- a/Source/Engine/Particles/ParticleEffect.h +++ b/Source/Engine/Particles/ParticleEffect.h @@ -382,7 +382,7 @@ public: #if USE_EDITOR protected: // Exposed parameters overrides for Editor Undo. - API_PROPERTY(Attributes="HideInEditor, Serialize") Array GetParametersOverrides(); + API_PROPERTY(Attributes="HideInEditor, Serialize") Array& GetParametersOverrides(); API_PROPERTY() void SetParametersOverrides(const Array& value); #endif diff --git a/Source/Engine/Profiler/ProfilerCPU.cpp b/Source/Engine/Profiler/ProfilerCPU.cpp index 417178747..26f9e49c2 100644 --- a/Source/Engine/Profiler/ProfilerCPU.cpp +++ b/Source/Engine/Profiler/ProfilerCPU.cpp @@ -205,7 +205,7 @@ int32 ProfilerCPU::BeginEvent(const char* name) void ProfilerCPU::EndEvent(int32 index) { - if (Enabled && Thread::Current) + if (index != -1 && Thread::Current) Thread::Current->EndEvent(index); } diff --git a/Source/Engine/Profiler/ProfilerGPU.cpp b/Source/Engine/Profiler/ProfilerGPU.cpp index c100b8df0..3054abe67 100644 --- a/Source/Engine/Profiler/ProfilerGPU.cpp +++ b/Source/Engine/Profiler/ProfilerGPU.cpp @@ -14,7 +14,7 @@ RenderStatsData RenderStatsData::Counter; int32 ProfilerGPU::_depth = 0; Array ProfilerGPU::_timerQueriesPool; Array ProfilerGPU::_timerQueriesFree; -bool ProfilerGPU::Enabled = true; +bool ProfilerGPU::Enabled = false; int32 ProfilerGPU::CurrentBuffer = 0; ProfilerGPU::EventBuffer ProfilerGPU::Buffers[PROFILER_GPU_EVENTS_FRAMES]; diff --git a/Source/Engine/Profiler/ProfilingTools.cpp b/Source/Engine/Profiler/ProfilingTools.cpp index 6ce8082ba..61c0e2c33 100644 --- a/Source/Engine/Profiler/ProfilingTools.cpp +++ b/Source/Engine/Profiler/ProfilingTools.cpp @@ -3,14 +3,17 @@ #if COMPILE_WITH_PROFILER #include "ProfilingTools.h" +#include "Engine/Core/Types/Pair.h" #include "Engine/Engine/Engine.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/EngineService.h" #include "Engine/Graphics/GPUDevice.h" +#include "Engine/Networking/NetworkInternal.h" ProfilingTools::MainStats ProfilingTools::Stats; Array> ProfilingTools::EventsCPU; Array ProfilingTools::EventsGPU; +Array ProfilingTools::EventsNetwork; class ProfilingToolsService : public EngineService { @@ -120,6 +123,40 @@ void ProfilingToolsService::Update() frame.Extract(ProfilingTools::EventsGPU); } + // Get the last events from networking runtime + { + auto& networkEvents = ProfilingTools::EventsNetwork; + networkEvents.Resize(NetworkInternal::ProfilerEvents.Count()); + int32 i = 0; + for (const auto& e : NetworkInternal::ProfilerEvents) + { + const auto& src = e.Value; + auto& dst = networkEvents[i++]; + dst.Count = src.Count; + dst.DataSize = src.DataSize; + dst.MessageSize = src.MessageSize; + dst.Receivers = src.Receivers; + const StringAnsiView& typeName = e.Key.First.GetType().Fullname; + uint64 len = Math::Min(typeName.Length(), ARRAY_COUNT(dst.Name) - 10); + Platform::MemoryCopy(dst.Name, typeName.Get(), len); + const StringAnsiView& name = e.Key.Second; + if (name.HasChars()) + { + uint64 pos = len; + dst.Name[pos++] = ':'; + dst.Name[pos++] = ':'; + len = Math::Min(name.Length(), ARRAY_COUNT(dst.Name) - pos - 1); + Platform::MemoryCopy(dst.Name + pos, name.Get(), len); + dst.Name[pos + len] = 0; + } + else + { + dst.Name[len] = 0; + } + } + NetworkInternal::ProfilerEvents.Clear(); + } + #if 0 // Print CPU events to the log { @@ -173,6 +210,19 @@ void ProfilingToolsService::Dispose() ProfilingTools::EventsCPU.Clear(); ProfilingTools::EventsCPU.SetCapacity(0); ProfilingTools::EventsGPU.SetCapacity(0); + ProfilingTools::EventsNetwork.SetCapacity(0); +} + +bool ProfilingTools::GetEnabled() +{ + return ProfilerCPU::Enabled && ProfilerGPU::Enabled; +} + +void ProfilingTools::SetEnabled(bool enabled) +{ + ProfilerCPU::Enabled = enabled; + ProfilerGPU::Enabled = enabled; + NetworkInternal::EnableProfiling = enabled; } #endif diff --git a/Source/Engine/Profiler/ProfilingTools.h b/Source/Engine/Profiler/ProfilingTools.h index e1c010d77..f4039472f 100644 --- a/Source/Engine/Profiler/ProfilingTools.h +++ b/Source/Engine/Profiler/ProfilingTools.h @@ -105,7 +105,35 @@ public: API_FIELD() Array Events; }; + /// + /// The network stat. + /// + API_STRUCT(NoDefault) struct NetworkEventStat + { + DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkEventStat); + + // Amount of occurrences. + API_FIELD() uint16 Count; + // Transferred data size (in bytes). + API_FIELD() uint16 DataSize; + // Transferred message (data+header) size (in bytes). + API_FIELD() uint16 MessageSize; + // Amount of peers that will receive this message. + API_FIELD() uint16 Receivers; + API_FIELD(Private, NoArray) byte Name[120]; + }; + public: + /// + /// Controls the engine profiler (CPU, GPU, etc.) usage. + /// + API_PROPERTY() static bool GetEnabled(); + + /// + /// Controls the engine profiler (CPU, GPU, etc.) usage. + /// + API_PROPERTY() static void SetEnabled(bool enabled); + /// /// The current collected main stats by the profiler from the local session. Updated every frame. /// @@ -120,6 +148,11 @@ public: /// The GPU rendering profiler events. /// API_FIELD(ReadOnly) static Array EventsGPU; + + /// + /// The networking profiler events. + /// + API_FIELD(ReadOnly) static Array EventsNetwork; }; #endif diff --git a/Source/Engine/Scripting/Attributes/SerializeAttribute.cs b/Source/Engine/Scripting/Attributes/SerializeAttribute.cs index 5a557902d..da7f8c308 100644 --- a/Source/Engine/Scripting/Attributes/SerializeAttribute.cs +++ b/Source/Engine/Scripting/Attributes/SerializeAttribute.cs @@ -5,7 +5,8 @@ using System; namespace FlaxEngine { /// - /// Indicates that a field or a property of a serializable class should be serialized. This class cannot be inherited. + /// Indicates that a field or a property of a serializable class should be serialized. + /// The attribute is required to show hidden fields in the editor. /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public sealed class SerializeAttribute : Attribute diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index c859fa961..685e0ec91 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -703,13 +703,12 @@ bool MAssembly::LoadImage(const String& assemblyPath, const StringView& nativePa { // TODO: Use new hostfxr delegate load_assembly_bytes? (.NET 8+) // Open .Net assembly - const StringAnsi assemblyPathAnsi = assemblyPath.ToStringAnsi(); - const char* name; - const char* fullname; + const char* name = nullptr; + const char* fullname = nullptr; static void* LoadAssemblyImagePtr = GetStaticMethodPointer(TEXT("LoadAssemblyImage")); - _handle = CallStaticMethod(LoadAssemblyImagePtr, assemblyPathAnsi.Get(), &name, &fullname); - _name = name; - _fullname = fullname; + _handle = CallStaticMethod(LoadAssemblyImagePtr, assemblyPath.Get(), &name, &fullname); + _name = StringAnsi(name); + _fullname = StringAnsi(fullname); MCore::GC::FreeMemory((void*)name); MCore::GC::FreeMemory((void*)fullname); if (_handle == nullptr) diff --git a/Source/Engine/UI/GUI/Common/Button.cs b/Source/Engine/UI/GUI/Common/Button.cs index 88ee0437e..b50f3dd46 100644 --- a/Source/Engine/UI/GUI/Common/Button.cs +++ b/Source/Engine/UI/GUI/Common/Button.cs @@ -84,17 +84,23 @@ namespace FlaxEngine.GUI /// [EditorDisplay("Border Style"), EditorOrder(2010), ExpandGroups] public bool HasBorder { get; set; } = true; + + /// + /// Gets or sets the border thickness. + /// + [EditorDisplay("Border Style"), EditorOrder(2011), Limit(0)] + public float BorderThickness { get; set; } = 1.0f; /// /// Gets or sets the color of the border. /// - [EditorDisplay("Border Style"), EditorOrder(2011), ExpandGroups] + [EditorDisplay("Border Style"), EditorOrder(2012)] public Color BorderColor { get; set; } /// /// Gets or sets the border color when button is highlighted. /// - [EditorDisplay("Border Style"), EditorOrder(2012)] + [EditorDisplay("Border Style"), EditorOrder(2013)] public Color BorderColorHighlighted { get; set; } /// @@ -252,7 +258,7 @@ namespace FlaxEngine.GUI else Render2D.FillRectangle(clientRect, backgroundColor); if (HasBorder) - Render2D.DrawRectangle(clientRect, borderColor); + Render2D.DrawRectangle(clientRect, borderColor, BorderThickness); // Draw text Render2D.DrawText(_font?.GetFont(), TextMaterial, _text, clientRect, textColor, TextAlignment.Center, TextAlignment.Center); diff --git a/Source/Engine/UI/GUI/Common/CheckBox.cs b/Source/Engine/UI/GUI/Common/CheckBox.cs index 939708f36..2f1ff42a9 100644 --- a/Source/Engine/UI/GUI/Common/CheckBox.cs +++ b/Source/Engine/UI/GUI/Common/CheckBox.cs @@ -107,17 +107,29 @@ namespace FlaxEngine.GUI CacheBox(); } } + + /// + /// Gets or sets whether to have a border. + /// + [EditorDisplay("Border Style"), EditorOrder(2010), Tooltip("Whether to have a border."), ExpandGroups] + public bool HasBorder { get; set; } = true; + + /// + /// Gets or sets the border thickness. + /// + [EditorDisplay("Border Style"), EditorOrder(2011), Tooltip("The thickness of the border."), Limit(0)] + public float BorderThickness { get; set; } = 1.0f; /// /// Gets or sets the color of the border. /// - [EditorDisplay("Border Style"), EditorOrder(2010), ExpandGroups] + [EditorDisplay("Border Style"), EditorOrder(2012)] public Color BorderColor { get; set; } /// /// Gets or sets the border color when checkbox is hovered. /// - [EditorDisplay("Border Style"), EditorOrder(2011)] + [EditorDisplay("Border Style"), EditorOrder(2013)] public Color BorderColorHighlighted { get; set; } /// @@ -221,12 +233,15 @@ namespace FlaxEngine.GUI bool enabled = EnabledInHierarchy; // Border - Color borderColor = BorderColor; - if (!enabled) - borderColor *= 0.5f; - else if (_isPressed || _mouseOverBox || IsNavFocused) - borderColor = BorderColorHighlighted; - Render2D.DrawRectangle(_box.MakeExpanded(-2.0f), borderColor); + if (HasBorder) + { + Color borderColor = BorderColor; + if (!enabled) + borderColor *= 0.5f; + else if (_isPressed || _mouseOverBox || IsNavFocused) + borderColor = BorderColorHighlighted; + Render2D.DrawRectangle(_box.MakeExpanded(-2.0f), borderColor, BorderThickness); + } // Icon if (_state != CheckBoxState.Default) diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs index 7ea2accef..6dbd5082c 100644 --- a/Source/Engine/UI/GUI/Common/Slider.cs +++ b/Source/Engine/UI/GUI/Common/Slider.cs @@ -268,7 +268,7 @@ public class Slider : ContainerControl // Draw track fill if (FillTrack) { - var fillLineRect = new Rectangle(_thumbSize.X / 2, (Height - TrackHeight - 2) / 2, Width - (Width - _thumbCenter) - _thumbSize.X / 2, TrackHeight + 2); + var fillLineRect = new Rectangle(_thumbSize.X / 2 - 1, (Height - TrackHeight - 2) / 2, Width - (Width - _thumbCenter) - _thumbSize.X / 2, TrackHeight + 2); Render2D.PushClip(ref fillLineRect); if (FillTrackBrush != null) FillTrackBrush.Draw(lineRect, TrackFillLineColor); diff --git a/Source/Engine/UI/GUI/Common/TextBox.cs b/Source/Engine/UI/GUI/Common/TextBox.cs index 5ec86a94e..ee4f744a6 100644 --- a/Source/Engine/UI/GUI/Common/TextBox.cs +++ b/Source/Engine/UI/GUI/Common/TextBox.cs @@ -155,7 +155,8 @@ namespace FlaxEngine.GUI if (IsMouseOver || IsNavFocused) backColor = BackgroundSelectedColor; Render2D.FillRectangle(rect, backColor); - Render2D.DrawRectangle(rect, IsFocused ? BorderSelectedColor : BorderColor); + if (HasBorder) + Render2D.DrawRectangle(rect, IsFocused ? BorderSelectedColor : BorderColor, BorderThickness); // Apply view offset and clip mask if (ClipText) diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index a4ccbe5d2..05183d57a 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -11,6 +11,11 @@ namespace FlaxEngine.GUI /// public abstract class TextBoxBase : ContainerControl { + /// + /// The delete control character (used for text filtering). + /// + protected const char DelChar = (char)0x7F; + /// /// The text separators (used for words skipping). /// @@ -270,16 +275,28 @@ namespace FlaxEngine.GUI [EditorDisplay("Background Style"), EditorOrder(2002), Tooltip("The speed of the selection background flashing animation.")] public float BackgroundSelectedFlashSpeed { get; set; } = 6.0f; + /// + /// Gets or sets whether to have a border. + /// + [EditorDisplay("Border Style"), EditorOrder(2010), Tooltip("Whether to have a border."), ExpandGroups] + public bool HasBorder { get; set; } = true; + + /// + /// Gets or sets the border thickness. + /// + [EditorDisplay("Border Style"), EditorOrder(2011), Tooltip("The thickness of the border."), Limit(0)] + public float BorderThickness { get; set; } = 1.0f; + /// /// Gets or sets the color of the border (Transparent if not used). /// - [EditorDisplay("Border Style"), EditorOrder(2010), Tooltip("The color of the border (Transparent if not used)."), ExpandGroups] + [EditorDisplay("Border Style"), EditorOrder(2012), Tooltip("The color of the border (Transparent if not used).")] public Color BorderColor { get; set; } /// /// Gets or sets the color of the border when control is focused (Transparent if not used). /// - [EditorDisplay("Border Style"), EditorOrder(2011), Tooltip("The color of the border when control is focused (Transparent if not used)")] + [EditorDisplay("Border Style"), EditorOrder(2013), Tooltip("The color of the border when control is focused (Transparent if not used)")] public Color BorderSelectedColor { get; set; } /// @@ -351,6 +368,10 @@ namespace FlaxEngine.GUI if (value.IndexOf('\r') != -1) value = value.Replace("\r", ""); + // Filter text (handle backspace control character) + if (value.IndexOf(DelChar) != -1) + value = value.Replace(DelChar.ToString(), ""); + // Clamp length if (value.Length > MaxLength) value = value.Substring(0, MaxLength); @@ -673,6 +694,8 @@ namespace FlaxEngine.GUI // Filter text if (str.IndexOf('\r') != -1) str = str.Replace("\r", ""); + if (str.IndexOf(DelChar) != -1) + str = str.Replace(DelChar.ToString(), ""); if (!IsMultiline && str.IndexOf('\n') != -1) str = str.Replace("\n", ""); @@ -1327,6 +1350,15 @@ namespace FlaxEngine.GUI if (IsReadOnly) return true; + if (ctrDown) + { + int prevWordBegin = FindPrevWordBegin(); + _text = _text.Remove(prevWordBegin, CaretPosition - prevWordBegin); + SetSelection(prevWordBegin); + OnTextChanged(); + return true; + } + int left = SelectionLeft; if (HasSelection) { diff --git a/Source/Engine/UI/GUI/ContainerControl.cs b/Source/Engine/UI/GUI/ContainerControl.cs index 2de96d0b3..69a75aacb 100644 --- a/Source/Engine/UI/GUI/ContainerControl.cs +++ b/Source/Engine/UI/GUI/ContainerControl.cs @@ -360,7 +360,7 @@ namespace FlaxEngine.GUI { var containerControl = child as ContainerControl; var childAtRecursive = containerControl?.GetChildAtRecursive(childLocation); - if (childAtRecursive != null) + if (childAtRecursive != null && childAtRecursive.Visible) { child = childAtRecursive; } @@ -507,15 +507,19 @@ namespace FlaxEngine.GUI // Perform automatic navigation based on the layout var result = NavigationRaycast(direction, location, visited); - if (result == null && direction == NavDirection.Next) + var rightMostLocation = location; + if (result == null && (direction == NavDirection.Next || direction == NavDirection.Previous)) { // Try wrap the navigation over the layout based on the direction var visitedWrap = new List(visited); - result = NavigationWrap(direction, location, visitedWrap); + result = NavigationWrap(direction, location, visitedWrap, out rightMostLocation); } if (result != null) { - result = result.OnNavigate(direction, result.PointFromParent(location), this, visited); + // HACK: only the 'previous' direction needs the rightMostLocation so i used a ternary conditional operator. + // The rightMostLocation can probably become a 'desired raycast origin' that gets calculated correctly in the NavigationWrap method. + var useLocation = direction == NavDirection.Previous ? rightMostLocation : location; + result = result.OnNavigate(direction, result.PointFromParent(useLocation), this, visited); if (result != null) return result; } @@ -551,8 +555,9 @@ namespace FlaxEngine.GUI /// The navigation direction. /// The navigation start location (in the control-space). /// The list with visited controls. Used to skip recursive navigation calls when doing traversal across the UI hierarchy. + /// Returns the rightmost location of the parent container for the raycast used by the child container /// The target navigation control or null if didn't performed any navigation. - protected virtual Control NavigationWrap(NavDirection direction, Float2 location, List visited) + protected virtual Control NavigationWrap(NavDirection direction, Float2 location, List visited, out Float2 rightMostLocation) { // This searches form a child that calls this navigation event (see Control.OnNavigate) to determinate the layout wrapping size based on that child size var currentChild = RootWindow?.FocusedControl; @@ -566,15 +571,22 @@ namespace FlaxEngine.GUI case NavDirection.Next: predictedLocation = new Float2(0, location.Y + layoutSize.Y); break; + case NavDirection.Previous: + predictedLocation = new Float2(Size.X, location.Y - layoutSize.Y); + break; } if (new Rectangle(Float2.Zero, Size).Contains(ref predictedLocation)) { var result = NavigationRaycast(direction, predictedLocation, visited); if (result != null) - return result; + { + rightMostLocation = predictedLocation; + return result; + } } } - return Parent?.NavigationWrap(direction, PointToParent(ref location), visited); + rightMostLocation = location; + return Parent?.NavigationWrap(direction, PointToParent(ref location), visited, out rightMostLocation); } private static bool CanGetAutoFocus(Control c) @@ -613,6 +625,10 @@ namespace FlaxEngine.GUI uiDir1 = new Float2(1, 0); uiDir2 = new Float2(0, 1); break; + case NavDirection.Previous: + uiDir1 = new Float2(-1, 0); + uiDir2 = new Float2(0, -1); + break; } Control result = null; var minDistance = float.MaxValue; diff --git a/Source/Engine/UI/GUI/Control.cs b/Source/Engine/UI/GUI/Control.cs index 3bc2610a4..5cb9501c0 100644 --- a/Source/Engine/UI/GUI/Control.cs +++ b/Source/Engine/UI/GUI/Control.cs @@ -634,6 +634,7 @@ namespace FlaxEngine.GUI case NavDirection.Left: return new Float2(0, size.Y * 0.5f); case NavDirection.Right: return new Float2(size.X, size.Y * 0.5f); case NavDirection.Next: return Float2.Zero; + case NavDirection.Previous: return size; default: return size * 0.5f; } } diff --git a/Source/Engine/UI/GUI/Enums.cs b/Source/Engine/UI/GUI/Enums.cs index d0b4fb61c..9672dcb9b 100644 --- a/Source/Engine/UI/GUI/Enums.cs +++ b/Source/Engine/UI/GUI/Enums.cs @@ -202,5 +202,10 @@ namespace FlaxEngine.GUI /// The next item (right with layout wrapping). /// Next, + + /// + /// The previous item (left with layout wrapping). + /// + Previous, } } diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs index 6ffdabea0..2d58df0c0 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs @@ -968,7 +968,7 @@ namespace Flax.Build.Bindings CppParamsWrappersCache[i] = result; } - string eventSignature; + string eventType; if (useCustomDelegateSignature) { contents.Append(indent).Append($"/// The delegate for event {eventInfo.Name}.").AppendLine(); @@ -983,24 +983,25 @@ namespace Flax.Build.Bindings contents.Append(CppParamsWrappersCache[i]).Append(" arg").Append(i); } contents.Append(");").AppendLine().AppendLine(); - eventSignature = "event " + eventInfo.Name + "Delegate"; + eventType = eventInfo.Name + "Delegate"; } else { - eventSignature = "event Action"; + eventType = "Action"; if (paramsCount != 0) { - eventSignature += '<'; + eventType += '<'; for (var i = 0; i < paramsCount; i++) { if (i != 0) - eventSignature += ", "; + eventType += ", "; CppParamsWrappersCache[i] = GenerateCSharpNativeToManaged(buildData, eventInfo.Type.GenericArgs[i], classInfo); - eventSignature += CppParamsWrappersCache[i]; + eventType += CppParamsWrappersCache[i]; } - eventSignature += '>'; + eventType += '>'; } } + string eventSignature = "event " + eventType; GenerateCSharpComment(contents, indent, eventInfo.Comment, true); GenerateCSharpAttributes(buildData, contents, indent, classInfo, eventInfo, useUnmanaged); @@ -1013,11 +1014,7 @@ namespace Flax.Build.Bindings indent += " "; var eventInstance = eventInfo.IsStatic ? string.Empty : "__unmanagedPtr, "; contents.Append(indent).Append($"add {{ Internal_{eventInfo.Name} += value; if (Internal_{eventInfo.Name}_Count++ == 0) Internal_{eventInfo.Name}_Bind({eventInstance}true); }}").AppendLine(); - contents.Append(indent).Append("remove { ").AppendLine(); - contents.Append("#if FLAX_EDITOR || BUILD_DEBUG").AppendLine(); - contents.Append(indent).Append($"if (Internal_{eventInfo.Name} != null) {{ bool invalid = true; foreach (Delegate e in Internal_{eventInfo.Name}.GetInvocationList()) {{ if (e == (Delegate)value) {{ invalid = false; break; }} }} if (invalid) throw new Exception(\"Cannot unregister from event if not registered before.\"); }}").AppendLine(); - contents.Append("#endif").AppendLine(); - contents.Append(indent).Append($"Internal_{eventInfo.Name} -= value; if (--Internal_{eventInfo.Name}_Count == 0) Internal_{eventInfo.Name}_Bind({eventInstance}false); }}").AppendLine(); + contents.Append(indent).Append($"remove {{ var __delegate = ({eventType})Delegate.Remove(Internal_{eventInfo.Name}, value); if (__delegate != Internal_{eventInfo.Name}) {{ Internal_{eventInfo.Name} = __delegate; if (--Internal_{eventInfo.Name}_Count == 0) Internal_{eventInfo.Name}_Bind({eventInstance}false); }} }}").AppendLine(); indent = indent.Substring(0, indent.Length - 4); contents.Append(indent).Append('}').AppendLine(); @@ -1034,6 +1031,7 @@ namespace Flax.Build.Bindings contents.Append("static "); contents.Append($"{eventSignature} Internal_{eventInfo.Name};"); contents.AppendLine(); + contents.Append("#pragma warning restore 67").AppendLine(); contents.AppendLine(); contents.Append(indent).Append("internal "); @@ -1515,8 +1513,8 @@ namespace Flax.Build.Bindings type = "IntPtr"; else if (type == "bool") type = "byte"; - else if (type == "object") - type = "NativeVariant"; + else if (fieldInfo.Type.Type == "Variant") + type = "IntPtr"; else if (internalType) { internalTypeMarshaller = type + "Marshaller"; @@ -1533,9 +1531,6 @@ namespace Flax.Build.Bindings if (fieldInfo.NoArray && fieldInfo.Type.IsArray) continue; - if (type == "NativeVariant") - continue; // TODO: FIXME - if (useSeparator) { toManagedContent.Append(", "); @@ -1637,6 +1632,12 @@ namespace Flax.Build.Bindings toManagedContent.Append($"managed.{fieldInfo.Name} != 0"); toNativeContent.Append($"managed.{fieldInfo.Name} ? (byte)1 : (byte)0"); } + else if (fieldInfo.Type.Type == "Variant") + { + // Variant passed as boxed object handle + toManagedContent.Append($"ManagedHandleMarshaller.NativeToManaged.ConvertToManaged(managed.{fieldInfo.Name})"); + toNativeContent.Append($"ManagedHandleMarshaller.NativeToManaged.ConvertToUnmanaged(managed.{fieldInfo.Name})"); + } else if (internalType) { toManagedContent.Append($"{internalTypeMarshaller}.ToManaged(managed.{fieldInfo.Name})"); @@ -1770,6 +1771,10 @@ namespace Flax.Build.Bindings // char's are not blittable, store as short instead contents.Append($"fixed short {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}]; // {managedType}*").AppendLine(); } + else if (managedType == "byte") + { + contents.Append($"fixed byte {fieldInfo.Name}0[{fieldInfo.Type.ArraySize}]; // {managedType}*").AppendLine(); + } else #endif {