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
{