diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings
index fe1d1463e..e8af1461c 100644
--- a/Flax.sln.DotSettings
+++ b/Flax.sln.DotSettings
@@ -248,6 +248,7 @@
True
True
True
+ True
True
True
True
diff --git a/Source/Editor/CustomEditors/Editors/IBrushEditor.cs b/Source/Editor/CustomEditors/Editors/IBrushEditor.cs
index beb28ca80..387364df6 100644
--- a/Source/Editor/CustomEditors/Editors/IBrushEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/IBrushEditor.cs
@@ -16,6 +16,7 @@ namespace FlaxEditor.CustomEditors.Editors
///
protected override OptionType[] Options => new[]
{
+ new OptionType("null", null),
new OptionType("Texture", typeof(TextureBrush)),
new OptionType("Sprite", typeof(SpriteBrush)),
new OptionType("GPU Texture", typeof(GPUTextureBrush)),
diff --git a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs
index 25a058d86..4d0c0f662 100644
--- a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs
@@ -158,7 +158,9 @@ namespace FlaxEditor.CustomEditors.Editors
if (comboBox.SelectedIndex != -1)
{
var option = _options[comboBox.SelectedIndex];
- value = option.Creator(option.Type);
+ if (option.Type != null)
+ value = option.Creator(option.Type);
+
}
SetValue(value);
RebuildLayoutOnRefresh();
diff --git a/Source/Editor/Editor.Build.cs b/Source/Editor/Editor.Build.cs
index cc7c48d23..a971dbefa 100644
--- a/Source/Editor/Editor.Build.cs
+++ b/Source/Editor/Editor.Build.cs
@@ -42,6 +42,10 @@ public class Editor : EditorModule
options.ScriptingAPI.SystemReferences.Add("System.Text.RegularExpressions");
options.ScriptingAPI.SystemReferences.Add("System.ComponentModel.TypeConverter");
+ // Enable optimizations for Editor, disable this for debugging the editor
+ if (options.Configuration == TargetConfiguration.Development)
+ options.ScriptingAPI.Optimization = true;
+
options.PublicDependencies.Add("Engine");
options.PrivateDependencies.Add("pugixml");
options.PrivateDependencies.Add("curl");
diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs
index 1a1fd3b2d..7f0359331 100644
--- a/Source/Editor/Editor.cs
+++ b/Source/Editor/Editor.cs
@@ -1579,7 +1579,7 @@ namespace FlaxEditor
private static void RequestStartPlayOnEditMode()
{
if (Instance.StateMachine.IsEditMode)
- Instance.Simulation.RequestStartPlay();
+ Instance.Simulation.RequestStartPlayScenes();
if (Instance.StateMachine.IsPlayMode)
Instance.StateMachine.StateChanged -= RequestStartPlayOnEditMode;
}
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuSingleSelectGroup.cs b/Source/Editor/GUI/ContextMenu/ContextMenuSingleSelectGroup.cs
index 5abb52b4a..c89e1bb61 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuSingleSelectGroup.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuSingleSelectGroup.cs
@@ -23,6 +23,7 @@ namespace FlaxEditor.GUI.ContextMenu
private List _menus = new List();
private List _items = new List();
+ private bool _hasSelected = false;
private SingleSelectGroupItem _selectedItem;
public T Selected
@@ -31,7 +32,7 @@ namespace FlaxEditor.GUI.ContextMenu
set
{
var index = _items.FindIndex(x => x.Value.Equals(value));
- if (index != -1 && !_selectedItem.Value.Equals(value))
+ if (index != -1 && (!_hasSelected || !_selectedItem.Value.Equals(value)))
{
SetSelected(_items[index]);
}
@@ -70,7 +71,7 @@ namespace FlaxEditor.GUI.ContextMenu
if (item.Tooltip != null)
btn.TooltipText = item.Tooltip;
item.Buttons.Add(btn);
- if (item.Equals(_selectedItem))
+ if (_hasSelected && item.Equals(_selectedItem))
btn.Checked = true;
}
@@ -82,6 +83,7 @@ namespace FlaxEditor.GUI.ContextMenu
btn.Checked = false;
}
_selectedItem = item;
+ _hasSelected = true;
SelectedChanged?.Invoke(item.Value);
item.Selected?.Invoke();
diff --git a/Source/Editor/GUI/Docking/DockPanelProxy.cs b/Source/Editor/GUI/Docking/DockPanelProxy.cs
index ac5e0bc4f..15ff2cad0 100644
--- a/Source/Editor/GUI/Docking/DockPanelProxy.cs
+++ b/Source/Editor/GUI/Docking/DockPanelProxy.cs
@@ -253,6 +253,12 @@ namespace FlaxEditor.GUI.Docking
tabColor = style.BackgroundHighlighted;
Render2D.FillRectangle(tabRect, tabColor);
}
+ else
+ {
+ tabColor = style.BackgroundHighlighted;
+ Render2D.DrawLine(tabRect.BottomLeft - new Float2(0 , 1), tabRect.UpperLeft, tabColor);
+ Render2D.DrawLine(tabRect.BottomRight - new Float2(0 , 1), tabRect.UpperRight, tabColor);
+ }
if (tab.Icon.IsValid)
{
diff --git a/Source/Editor/GUI/Timeline/GUI/Background.cs b/Source/Editor/GUI/Timeline/GUI/Background.cs
index dc4909abb..b9eff562a 100644
--- a/Source/Editor/GUI/Timeline/GUI/Background.cs
+++ b/Source/Editor/GUI/Timeline/GUI/Background.cs
@@ -282,7 +282,7 @@ namespace FlaxEditor.GUI.Timeline.GUI
var x = time * zoom + Timeline.StartOffset;
// Header line
- var lineRect = new Rectangle(x - 0.5f, -verticalLinesHeaderExtend + timeAxisHeaderOffset, 1.0f, verticalLinesHeaderExtend);
+ var lineRect = new Rectangle(x - 0.5f, -verticalLinesHeaderExtend * 0.6f + timeAxisHeaderOffset, 1.0f, verticalLinesHeaderExtend * 0.6f);
Render2D.FillRectangle(lineRect, lineColor);
// Time label
@@ -300,7 +300,7 @@ namespace FlaxEditor.GUI.Timeline.GUI
break;
default: throw new ArgumentOutOfRangeException();
}
- var labelRect = new Rectangle(x + 2, -verticalLinesHeaderExtend + timeAxisHeaderOffset, 50, verticalLinesHeaderExtend);
+ var labelRect = new Rectangle(x + 2, -verticalLinesHeaderExtend * 0.8f + timeAxisHeaderOffset, 50, verticalLinesHeaderExtend);
Render2D.DrawText(style.FontSmall, labelText, labelRect, labelColor, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, 0.8f);
}
}
diff --git a/Source/Editor/GUI/Timeline/GUI/PositionHandle.cs b/Source/Editor/GUI/Timeline/GUI/PositionHandle.cs
index 7b42e86ab..bedb61a5e 100644
--- a/Source/Editor/GUI/Timeline/GUI/PositionHandle.cs
+++ b/Source/Editor/GUI/Timeline/GUI/PositionHandle.cs
@@ -1,5 +1,7 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+using System;
+using System.Globalization;
using FlaxEngine;
using FlaxEngine.GUI;
@@ -30,11 +32,33 @@ namespace FlaxEditor.GUI.Timeline.GUI
var timeAxisOverlap = Timeline.HeaderTopAreaHeight * 0.5f;
var timeAxisHeaderOffset = -_timeline.MediaBackground.ViewOffset.Y - timeAxisOverlap;
+ // Time label
+ string labelText;
+ switch (_timeline.TimeShowMode)
+ {
+ case Timeline.TimeShowModes.Frames:
+ labelText = _timeline.CurrentFrame.ToString("###0", CultureInfo.InvariantCulture);
+ break;
+ case Timeline.TimeShowModes.Seconds:
+ labelText = _timeline.CurrentTime.ToString("###0.##'s'", CultureInfo.InvariantCulture);
+ break;
+ case Timeline.TimeShowModes.Time:
+ labelText = TimeSpan.FromSeconds(_timeline.CurrentTime).ToString("g");
+ break;
+ default: throw new ArgumentOutOfRangeException();
+ }
+ var color = (_timeline.IsMovingPositionHandle ? style.ProgressNormal : style.Foreground).AlphaMultiplied(0.6f);
Matrix3x3.RotationZ(Mathf.PiOverTwo, out var m1);
var m2 = Matrix3x3.Translation2D(0, timeAxisHeaderOffset);
Matrix3x3.Multiply(ref m1, ref m2, out var m3);
Render2D.PushTransform(ref m3);
- Render2D.DrawSprite(icon, new Rectangle(new Float2(4, -Width), Size), _timeline.IsMovingPositionHandle ? style.ProgressNormal : style.Foreground);
+ // TODO: Convert to its own sprite or 9 slice
+ Render2D.DrawSprite(icon, new Rectangle(new Float2(10, -icon.Size.X * 0.5f - 1), Size + new Float2(0, 1)), color);
+ Render2D.FillRectangle(new Rectangle(new Float2(-6, -icon.Size.Y * 0.5f + 7), new Float2(timeAxisOverlap, 5)), color);
+ Render2D.PopTransform();
+ var textMatrix = Matrix3x3.Translation2D(12, timeAxisHeaderOffset);
+ Render2D.PushTransform(ref textMatrix);
+ Render2D.DrawText(style.FontSmall, labelText, style.Foreground, new Float2(2, -6));
Render2D.PopTransform();
Render2D.FillRectangle(new Rectangle(Width * 0.5f, Height + timeAxisHeaderOffset, 1, _timeline.MediaPanel.Height - timeAxisHeaderOffset - timeAxisOverlap), _timeline.IsMovingPositionHandle ? style.ProgressNormal : style.Foreground.RGBMultiplied(0.8f));
diff --git a/Source/Editor/GUI/Timeline/Timeline.UI.cs b/Source/Editor/GUI/Timeline/Timeline.UI.cs
index a977d487e..604db541c 100644
--- a/Source/Editor/GUI/Timeline/Timeline.UI.cs
+++ b/Source/Editor/GUI/Timeline/Timeline.UI.cs
@@ -50,6 +50,27 @@ namespace FlaxEditor.GUI.Timeline
}
}
+ ///
+ public override void OnMouseEnter(Float2 location)
+ {
+ base.OnMouseEnter(location);
+ Cursor = CursorType.Hand;
+ }
+
+ ///
+ public override void OnMouseLeave()
+ {
+ Cursor = CursorType.Default;
+ base.OnMouseLeave();
+ }
+
+ ///
+ public override void Defocus()
+ {
+ Cursor = CursorType.Default;
+ base.Defocus();
+ }
+
private void Seek(ref Float2 location)
{
if (_timeline.PlaybackState == PlaybackStates.Disabled)
diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs
index 9800b2105..a32b35692 100644
--- a/Source/Editor/GUI/Timeline/Timeline.cs
+++ b/Source/Editor/GUI/Timeline/Timeline.cs
@@ -167,7 +167,7 @@ namespace FlaxEditor.GUI.Timeline
///
/// The header top area height (in pixels).
///
- public static readonly float HeaderTopAreaHeight = 22.0f;
+ public static readonly float HeaderTopAreaHeight = 40.0f;
///
/// The timeline units per second (on time axis).
diff --git a/Source/Editor/GUI/Timeline/Tracks/AnimationEventTrack.cs b/Source/Editor/GUI/Timeline/Tracks/AnimationEventTrack.cs
index 4e78ca8ae..11a1bfe47 100644
--- a/Source/Editor/GUI/Timeline/Tracks/AnimationEventTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/AnimationEventTrack.cs
@@ -298,7 +298,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
var animEventTypes = Editor.Instance.CodeEditing.All.Get().Where(x => new ScriptType(typeof(AnimEvent)).IsAssignableFrom(x));
foreach (var type in animEventTypes)
{
- if (type.IsAbstract || !type.CanCreateInstance)
+ if (type.IsAbstract || !type.CanCreateInstance || type.HasAttribute(typeof(HideInEditorAttribute), true))
continue;
var add = new ScriptType(typeof(AnimContinuousEvent)).IsAssignableFrom(type) ? addContinuousEvent : addEvent;
var b = add.ContextMenu.AddButton(type.Name);
@@ -307,6 +307,10 @@ namespace FlaxEditor.GUI.Timeline.Tracks
b.Parent.Tag = time;
b.ButtonClicked += OnAddAnimEvent;
}
+ if (!addEvent.ContextMenu.Items.Any())
+ addEvent.ContextMenu.AddButton("No Anim Events found").CloseMenuOnClick = false;
+ if (!addContinuousEvent.ContextMenu.Items.Any())
+ addContinuousEvent.ContextMenu.AddButton("No Continuous Anim Events found").CloseMenuOnClick = false;
}
diff --git a/Source/Editor/Modules/SceneEditingModule.cs b/Source/Editor/Modules/SceneEditingModule.cs
index 58c2f21dc..2b8bf718e 100644
--- a/Source/Editor/Modules/SceneEditingModule.cs
+++ b/Source/Editor/Modules/SceneEditingModule.cs
@@ -503,7 +503,7 @@ namespace FlaxEditor.Modules
// Set paste target if only one actor is selected and no target provided
if (pasteTargetActor == null && SelectionCount == 1 && Selection[0] is ActorNode actorNode)
{
- pasteTargetActor = actorNode.Actor;
+ pasteTargetActor = actorNode.Actor.Scene == actorNode.Actor ? actorNode.Actor : actorNode.Actor.Parent;
}
// Create paste action
diff --git a/Source/Editor/Modules/SimulationModule.cs b/Source/Editor/Modules/SimulationModule.cs
index cc081c23c..72b197123 100644
--- a/Source/Editor/Modules/SimulationModule.cs
+++ b/Source/Editor/Modules/SimulationModule.cs
@@ -20,6 +20,7 @@ namespace FlaxEditor.Modules
private bool _updateOrFixedUpdateWasCalled;
private long _breakpointHangFlag;
private EditorWindow _enterPlayFocusedWindow;
+ private Scene[] _scenesToReload;
internal SimulationModule(Editor editor)
: base(editor)
@@ -68,6 +69,22 @@ namespace FlaxEditor.Modules
BreakpointHangEnd?.Invoke();
}
+ ///
+ /// Delegates between playing game and playing scenes in editor based on the user's editor preference.
+ ///
+ public void DelegatePlayOrStopPlayInEditor()
+ {
+ switch (Editor.Options.Options.Interface.PlayButtonAction)
+ {
+ case Options.InterfaceOptions.PlayAction.PlayGame:
+ Editor.Simulation.RequestPlayGameOrStopPlay();
+ return;
+ case Options.InterfaceOptions.PlayAction.PlayScenes:
+ Editor.Simulation.RequestPlayScenesOrStopPlay();
+ return;
+ }
+ }
+
///
/// Returns true if play mode has been requested.
///
@@ -76,7 +93,7 @@ namespace FlaxEditor.Modules
///
/// Requests start playing in editor.
///
- public void RequestStartPlay()
+ public void RequestStartPlayScenes()
{
if (Editor.StateMachine.IsEditMode)
{
@@ -89,6 +106,57 @@ namespace FlaxEditor.Modules
}
}
+ ///
+ /// Requests playing game start or stop in editor from the project's configured FirstScene.
+ ///
+ public void RequestPlayGameOrStopPlay()
+ {
+ if (Editor.StateMachine.IsPlayMode)
+ {
+ RequestStopPlay();
+ }
+ else
+ {
+ RequestStartPlayGame();
+ }
+ }
+
+ ///
+ /// Requests start playing in editor from the project's configured FirstScene.
+ ///
+ public void RequestStartPlayGame()
+ {
+ if (!Editor.StateMachine.IsEditMode)
+ {
+ return;
+ }
+
+ var firstScene = Content.Settings.GameSettings.Load().FirstScene;
+ if (firstScene == Guid.Empty)
+ {
+ if (Level.IsAnySceneLoaded)
+ Editor.Simulation.RequestStartPlayScenes();
+ return;
+ }
+
+ _scenesToReload = Level.Scenes;
+ Level.UnloadAllScenes();
+ Level.LoadScene(firstScene);
+
+ Editor.PlayModeEnd += OnPlayGameEnd;
+ RequestPlayScenesOrStopPlay();
+ }
+
+ private void OnPlayGameEnd()
+ {
+ Editor.PlayModeEnd -= OnPlayGameEnd;
+
+ Level.UnloadAllScenes();
+
+ foreach (var scene in _scenesToReload)
+ Level.LoadScene(scene.ID);
+ }
+
///
/// Requests stop playing in editor.
///
@@ -106,14 +174,14 @@ namespace FlaxEditor.Modules
}
///
- /// Requests the playing start or stop in editor.
+ /// Requests the playing scenes start or stop in editor.
///
- public void RequestPlayOrStopPlay()
+ public void RequestPlayScenesOrStopPlay()
{
if (Editor.StateMachine.IsPlayMode)
RequestStopPlay();
else
- RequestStartPlay();
+ RequestStartPlayScenes();
}
///
diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs
index 38c61942a..08ec09de8 100644
--- a/Source/Editor/Modules/UIModule.cs
+++ b/Source/Editor/Modules/UIModule.cs
@@ -39,7 +39,6 @@ namespace FlaxEditor.Modules
private bool _progressFailed;
ContextMenuSingleSelectGroup _numberOfClientsGroup = new ContextMenuSingleSelectGroup();
- private Scene[] _scenesToReload;
private ContextMenuButton _menuFileSaveScenes;
private ContextMenuButton _menuFileCloseScenes;
@@ -556,8 +555,8 @@ namespace FlaxEditor.Modules
cm = MenuGame.ContextMenu;
cm.VisibleChanged += OnMenuGameShowHide;
- _menuGamePlayGame = cm.AddButton("Play Game", PlayGame);
- _menuGamePlayCurrentScenes = cm.AddButton("Play Current Scenes", inputOptions.Play.ToString(), PlayScenes);
+ _menuGamePlayGame = cm.AddButton("Play Game", Editor.Simulation.RequestPlayGameOrStopPlay);
+ _menuGamePlayCurrentScenes = cm.AddButton("Play Current Scenes", Editor.Simulation.RequestPlayScenesOrStopPlay);
_menuGameStop = cm.AddButton("Stop Game", Editor.Simulation.RequestStopPlay);
_menuGamePause = cm.AddButton("Pause", inputOptions.Pause.ToString(), Editor.Simulation.RequestPausePlay);
@@ -566,8 +565,8 @@ namespace FlaxEditor.Modules
_numberOfClientsGroup.AddItemsToContextMenu(numberOfClientsMenu.ContextMenu);
cm.AddSeparator();
- cm.AddButton("Cook & Run", CookAndRun).LinkTooltip("Runs Game Cooker to build the game for this platform and runs the game after.");
- cm.AddButton("Run cooked game", RunCookedGame).LinkTooltip("Runs the game build from the last cooking output. Use Cook&Play or Game Cooker first.");
+ cm.AddButton("Cook & Run", Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip("Runs Game Cooker to build the game for this platform and runs the game after.");
+ cm.AddButton("Run cooked game", Editor.Windows.GameCookerWin.RunCooked).LinkTooltip("Runs the game build from the last cooking output. Use Cook&Play or Game Cooker first.");
// Tools
MenuTools = MainMenu.AddButton("Tools");
@@ -658,23 +657,23 @@ namespace FlaxEditor.Modules
Parent = mainWindow,
};
- _toolStripSaveAll = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Save64, Editor.SaveAll).LinkTooltip("Save all (Ctrl+S)");
+ _toolStripSaveAll = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Save64, Editor.SaveAll).LinkTooltip($"Save all ({inputOptions.Save})");
ToolStrip.AddSeparator();
- _toolStripUndo = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Undo64, Editor.PerformUndo).LinkTooltip("Undo (Ctrl+Z)");
- _toolStripRedo = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Redo64, Editor.PerformRedo).LinkTooltip("Redo (Ctrl+Y)");
+ _toolStripUndo = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Undo64, Editor.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})");
+ _toolStripRedo = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Redo64, Editor.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})");
ToolStrip.AddSeparator();
- _toolStripTranslate = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Translate32, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip("Change Gizmo tool mode to Translate (1)");
- _toolStripRotate = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Rotate32, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip("Change Gizmo tool mode to Rotate (2)");
- _toolStripScale = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Scale32, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip("Change Gizmo tool mode to Scale (3)");
+ _toolStripTranslate = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Translate32, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip($"Change Gizmo tool mode to Translate ({inputOptions.TranslateMode})");
+ _toolStripRotate = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Rotate32, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip($"Change Gizmo tool mode to Rotate ({inputOptions.RotateMode})");
+ _toolStripScale = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Scale32, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip($"Change Gizmo tool mode to Scale ({inputOptions.ScaleMode})");
ToolStrip.AddSeparator();
// Cook scenes
_toolStripBuildScenes = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Build64, Editor.BuildScenesOrCancel).LinkTooltip("Build scenes data - CSG, navmesh, static lighting, env probes - configurable via Build Actions in editor options (Ctrl+F10)");
// Cook and run
- _toolStripCook = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.ShipIt64, CookAndRun).LinkTooltip("Cook & Run - build game for the current platform and run it locally");
+ _toolStripCook = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.ShipIt64, Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip("Cook & Run - build game for the current platform and run it locally");
_toolStripCook.ContextMenu = new ContextMenu();
- _toolStripCook.ContextMenu.AddButton("Run cooked game", RunCookedGame);
+ _toolStripCook.ContextMenu.AddButton("Run cooked game", Editor.Windows.GameCookerWin.RunCooked);
_toolStripCook.ContextMenu.AddSeparator();
var numberOfClientsMenu = _toolStripCook.ContextMenu.AddChildMenu("Number of game clients");
_numberOfClientsGroup.AddItemsToContextMenu(numberOfClientsMenu.ContextMenu);
@@ -682,7 +681,7 @@ namespace FlaxEditor.Modules
ToolStrip.AddSeparator();
// Play
- _toolStripPlay = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Play64, OnPlayPressed).LinkTooltip("Play Game");
+ _toolStripPlay = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Play64, Editor.Simulation.DelegatePlayOrStopPlayInEditor).LinkTooltip($"Play In Editor ({inputOptions.Play})");
_toolStripPlay.ContextMenu = new ContextMenu();
var playSubMenu = _toolStripPlay.ContextMenu.AddChildMenu("Play button action");
var playActionGroup = new ContextMenuSingleSelectGroup();
@@ -1039,65 +1038,6 @@ namespace FlaxEditor.Modules
Editor.Options.Apply(options);
}
- private void OnPlayPressed()
- {
- switch (Editor.Options.Options.Interface.PlayButtonAction)
- {
- case InterfaceOptions.PlayAction.PlayGame:
- if (Editor.IsPlayMode)
- Editor.Simulation.RequestStopPlay();
- else
- PlayGame();
- return;
- case InterfaceOptions.PlayAction.PlayScenes:
- PlayScenes();
- return;
- }
- }
-
- private void PlayGame()
- {
- var firstScene = GameSettings.Load().FirstScene;
- if (firstScene == Guid.Empty)
- {
- if (Level.IsAnySceneLoaded)
- Editor.Simulation.RequestStartPlay();
- return;
- }
-
- _scenesToReload = Level.Scenes;
- Level.UnloadAllScenes();
- Level.LoadScene(firstScene);
-
- Editor.PlayModeEnd += OnPlayGameSceneEnding;
- Editor.Simulation.RequestPlayOrStopPlay();
- }
-
- private void OnPlayGameSceneEnding()
- {
- Editor.PlayModeEnd -= OnPlayGameSceneEnding;
-
- Level.UnloadAllScenes();
-
- foreach (var scene in _scenesToReload)
- Level.LoadScene(scene.ID);
- }
-
- private void PlayScenes()
- {
- Editor.Simulation.RequestPlayOrStopPlay();
- }
-
- private void CookAndRun()
- {
- Editor.Windows.GameCookerWin.BuildAndRun();
- }
-
- private void RunCookedGame()
- {
- Editor.Windows.GameCookerWin.RunCooked();
- }
-
private void OnMainWindowClosing()
{
// Clear UI references (GUI cannot be used after window closing)
diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs
index 4f2b40514..e0a51622b 100644
--- a/Source/Editor/Options/InputOptions.cs
+++ b/Source/Editor/Options/InputOptions.cs
@@ -60,6 +60,10 @@ namespace FlaxEditor.Options
[EditorDisplay("Common"), EditorOrder(200)]
public InputBinding FocusSelection = new InputBinding(KeyboardKeys.F);
+ [DefaultValue(typeof(InputBinding), "Shift+F")]
+ [EditorDisplay("Common"), EditorOrder(200)]
+ public InputBinding LockFocusSelection = new InputBinding(KeyboardKeys.F, KeyboardKeys.Shift);
+
[DefaultValue(typeof(InputBinding), "Ctrl+F")]
[EditorDisplay("Common"), EditorOrder(210)]
public InputBinding Search = new InputBinding(KeyboardKeys.F, KeyboardKeys.Control);
diff --git a/Source/Editor/Undo/Actions/PasteActorsAction.cs b/Source/Editor/Undo/Actions/PasteActorsAction.cs
index c90e94189..8b0d040d8 100644
--- a/Source/Editor/Undo/Actions/PasteActorsAction.cs
+++ b/Source/Editor/Undo/Actions/PasteActorsAction.cs
@@ -138,6 +138,8 @@ namespace FlaxEditor.Actions
}
}
+ // Store previously looked up names and the results
+ Dictionary foundNamesResults = new();
for (int i = 0; i < nodeParents.Count; i++)
{
var node = nodeParents[i];
@@ -145,15 +147,28 @@ namespace FlaxEditor.Actions
var parent = actor?.Parent;
if (parent != null)
{
+ bool IsNameValid(string name)
+ {
+ if (!foundNamesResults.TryGetValue(name, out bool found))
+ {
+ found = parent.GetChild(name) != null;
+ foundNamesResults.Add(name, found);
+ }
+ return !found;
+ }
+
// Fix name collisions
var name = actor.Name;
- for (int j = 0; j < parent.ChildrenCount; j++)
+ var children = parent.Children;
+ for (int j = 0; j < children.Length; j++)
{
- var child = parent.Children[j];
- if (child != actor && child.Name == actor.Name)
+ var child = children[j];
+ if (child != actor && child.Name == name)
{
- var children = parent.Children;
- actor.Name = Utilities.Utils.IncrementNameNumber(name, x => children.All(y => y.Name != x));
+ string newName = Utilities.Utils.IncrementNameNumber(name, x => IsNameValid(x));
+ foundNamesResults[newName] = true;
+ actor.Name = newName;
+ // Multiple actors may have the same name, continue
}
}
}
@@ -162,10 +177,7 @@ namespace FlaxEditor.Actions
}
for (int i = 0; i < nodeParents.Count; i++)
- {
- var node = nodeParents[i];
- node.PostPaste();
- }
+ nodeParents[i].PostPaste();
}
///
diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs
index 3281d130a..ad7e06ba5 100644
--- a/Source/Editor/Viewport/EditorViewport.cs
+++ b/Source/Editor/Viewport/EditorViewport.cs
@@ -479,6 +479,7 @@ namespace FlaxEditor.Viewport
{
InitFpsCounter();
_showFpsButon = ViewWidgetShowMenu.AddButton("FPS Counter", () => ShowFpsCounter = !ShowFpsCounter);
+ _showFpsButon.CloseMenuOnClick = false;
}
}
diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
index 48cd4fd98..9cbfce562 100644
--- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
@@ -133,6 +133,8 @@ namespace FlaxEditor.Viewport
}
}
+ private bool _lockedFocus;
+ private double _lockedFocusOffset;
private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32);
private StaticModel _previewStaticModel;
private int _previewModelEntryIndex;
@@ -369,9 +371,11 @@ namespace FlaxEditor.Viewport
// Show grid widget
_showGridButton = ViewWidgetShowMenu.AddButton("Grid", () => Grid.Enabled = !Grid.Enabled);
_showGridButton.Icon = Style.Current.CheckBoxTick;
+ _showGridButton.CloseMenuOnClick = false;
// Show navigation widget
_showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", () => ShowNavigation = !ShowNavigation);
+ _showNavigationButton.CloseMenuOnClick = false;
// Create camera widget
ViewWidgetButtonMenu.AddSeparator();
@@ -386,11 +390,43 @@ namespace FlaxEditor.Viewport
InputActions.Add(options => options.TranslateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate);
InputActions.Add(options => options.RotateMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate);
InputActions.Add(options => options.ScaleMode, () => TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale);
+ InputActions.Add(options => options.LockFocusSelection, LockFocusSelection);
InputActions.Add(options => options.FocusSelection, FocusSelection);
InputActions.Add(options => options.RotateSelection, RotateSelection);
InputActions.Add(options => options.Delete, _editor.SceneEditing.Delete);
}
+ ///
+ public override void Update(float deltaTime)
+ {
+ base.Update(deltaTime);
+
+ var selection = TransformGizmo.SelectedParents;
+ var requestUnlockFocus = FlaxEngine.Input.Mouse.GetButtonDown(MouseButton.Right) || FlaxEngine.Input.Mouse.GetButtonDown(MouseButton.Left);
+ if (TransformGizmo.SelectedParents.Count == 0 || (requestUnlockFocus && ContainsFocus))
+ {
+ UnlockFocusSelection();
+ }
+ else if (_lockedFocus)
+ {
+ var selectionBounds = BoundingSphere.Empty;
+ for (int i = 0; i < selection.Count; i++)
+ {
+ selection[i].GetEditorSphere(out var sphere);
+ BoundingSphere.Merge(ref selectionBounds, ref sphere, out selectionBounds);
+ }
+
+ if (ContainsFocus)
+ {
+ var viewportFocusDistance = Vector3.Distance(ViewPosition, selectionBounds.Center) / 10f;
+ _lockedFocusOffset -= FlaxEngine.Input.Mouse.ScrollDelta * viewportFocusDistance;
+ }
+
+ var focusDistance = Mathf.Max(selectionBounds.Radius * 2d, 100d);
+ ViewPosition = selectionBounds.Center + (-ViewDirection * (focusDistance + _lockedFocusOffset));
+ }
+ }
+
///
/// Overrides the selection outline effect or restored the default one.
///
@@ -753,6 +789,23 @@ namespace FlaxEditor.Viewport
FocusSelection(ref orientation);
}
+ ///
+ /// Lock focus on the current selection gizmo.
+ ///
+ public void LockFocusSelection()
+ {
+ _lockedFocus = true;
+ }
+
+ ///
+ /// Unlock focus on the current selection.
+ ///
+ public void UnlockFocusSelection()
+ {
+ _lockedFocus = false;
+ _lockedFocusOffset = 0f;
+ }
+
///
/// Focuses the viewport on the current selection of the gizmo.
///
diff --git a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs
index 12371f947..d96d7a8f9 100644
--- a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs
+++ b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs
@@ -194,16 +194,20 @@ namespace FlaxEditor.Viewport.Previews
{
// Show Bounds
_showBoundsButton = ViewWidgetShowMenu.AddButton("Bounds", () => ShowBounds = !ShowBounds);
+ _showBoundsButton.CloseMenuOnClick = false;
// Show Skeleton
_showNodesButton = ViewWidgetShowMenu.AddButton("Skeleton", () => ShowNodes = !ShowNodes);
+ _showNodesButton.CloseMenuOnClick = false;
// Show Skeleton Names
_showNodesNamesButton = ViewWidgetShowMenu.AddButton("Skeleton Names", () => ShowNodesNames = !ShowNodesNames);
+ _showNodesNamesButton.CloseMenuOnClick = false;
// Show Floor
_showFloorButton = ViewWidgetShowMenu.AddButton("Floor", button => ShowFloor = !ShowFloor);
_showFloorButton.IndexInParent = 1;
+ _showFloorButton.CloseMenuOnClick = false;
}
// Enable shadows
diff --git a/Source/Editor/Viewport/Previews/AssetPreview.cs b/Source/Editor/Viewport/Previews/AssetPreview.cs
index 0bcc4e115..0fb274b5d 100644
--- a/Source/Editor/Viewport/Previews/AssetPreview.cs
+++ b/Source/Editor/Viewport/Previews/AssetPreview.cs
@@ -171,6 +171,7 @@ namespace FlaxEditor.Viewport.Previews
// Show Default Scene
_showDefaultSceneButton = ViewWidgetShowMenu.AddButton("Default Scene", () => ShowDefaultSceneActors = !ShowDefaultSceneActors);
_showDefaultSceneButton.Checked = true;
+ _showDefaultSceneButton.CloseMenuOnClick = false;
}
// Setup preview scene
diff --git a/Source/Editor/Viewport/Previews/ModelPreview.cs b/Source/Editor/Viewport/Previews/ModelPreview.cs
index a0b6d0ca0..a6496aafe 100644
--- a/Source/Editor/Viewport/Previews/ModelPreview.cs
+++ b/Source/Editor/Viewport/Previews/ModelPreview.cs
@@ -199,13 +199,18 @@ namespace FlaxEditor.Viewport.Previews
if (useWidgets)
{
_showBoundsButton = ViewWidgetShowMenu.AddButton("Bounds", () => ShowBounds = !ShowBounds);
+ _showBoundsButton.CloseMenuOnClick = false;
_showNormalsButton = ViewWidgetShowMenu.AddButton("Normals", () => ShowNormals = !ShowNormals);
+ _showNormalsButton.CloseMenuOnClick = false;
_showTangentsButton = ViewWidgetShowMenu.AddButton("Tangents", () => ShowTangents = !ShowTangents);
+ _showTangentsButton.CloseMenuOnClick = false;
_showBitangentsButton = ViewWidgetShowMenu.AddButton("Bitangents", () => ShowBitangents = !ShowBitangents);
+ _showBitangentsButton.CloseMenuOnClick = false;
// Show Floor
_showFloorButton = ViewWidgetShowMenu.AddButton("Floor", button => ShowFloor = !ShowFloor);
_showFloorButton.IndexInParent = 1;
+ _showFloorButton.CloseMenuOnClick = false;
// Show current LOD widget
_showCurrentLODButton = ViewWidgetShowMenu.AddButton("Current LOD", button =>
@@ -214,6 +219,7 @@ namespace FlaxEditor.Viewport.Previews
_showCurrentLODButton.Icon = _showCurrentLOD ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
});
_showCurrentLODButton.IndexInParent = 2;
+ _showCurrentLODButton.CloseMenuOnClick = false;
// Preview LODs mode widget
var PreviewLODsMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
diff --git a/Source/Editor/Viewport/Previews/ParticleSystemPreview.cs b/Source/Editor/Viewport/Previews/ParticleSystemPreview.cs
index 1e0c89528..fc7fcdbe1 100644
--- a/Source/Editor/Viewport/Previews/ParticleSystemPreview.cs
+++ b/Source/Editor/Viewport/Previews/ParticleSystemPreview.cs
@@ -186,8 +186,11 @@ namespace FlaxEditor.Viewport.Previews
if (!useWidgets)
return;
_showBoundsButton = ViewWidgetShowMenu.AddButton("Bounds", () => ShowBounds = !ShowBounds);
+ _showBoundsButton.CloseMenuOnClick = false;
_showOriginButton = ViewWidgetShowMenu.AddButton("Origin", () => ShowOrigin = !ShowOrigin);
+ _showOriginButton.CloseMenuOnClick = false;
_showParticleCounterButton = ViewWidgetShowMenu.AddButton("Particles Counter", () => ShowParticlesCounter = !ShowParticlesCounter);
+ _showParticleCounterButton.CloseMenuOnClick = false;
// Play/Pause widget
{
diff --git a/Source/Editor/Viewport/Previews/SkinnedModelPreview.cs b/Source/Editor/Viewport/Previews/SkinnedModelPreview.cs
index 08f8d8509..8bcc506d9 100644
--- a/Source/Editor/Viewport/Previews/SkinnedModelPreview.cs
+++ b/Source/Editor/Viewport/Previews/SkinnedModelPreview.cs
@@ -49,6 +49,7 @@ namespace FlaxEditor.Viewport.Previews
_showCurrentLODButton.Icon = _showCurrentLOD ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
});
_showCurrentLODButton.IndexInParent = 2;
+ _showCurrentLODButton.CloseMenuOnClick = false;
// PreviewLODS mode widget
var PreviewLODSMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs
index e6606f35f..47ce09274 100644
--- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs
+++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs
@@ -23,6 +23,7 @@ namespace FlaxEditor.Windows.Assets
private readonly Undo _undo;
private object _object;
private bool _isRegisteredForScriptsReload;
+ private Label _typeText;
///
/// Gets the instance of the Json asset object that is being edited.
@@ -136,6 +137,22 @@ namespace FlaxEditor.Windows.Assets
}
}
_presenter.Select(_object);
+
+ if (_typeText != null)
+ _typeText.Dispose();
+ var typeText = new ClickableLabel
+ {
+ Text = $"{Asset.DataTypeName}",
+ TooltipText = "Asset data type (full name)",
+ AnchorPreset = AnchorPresets.TopRight,
+ AutoWidth = true,
+ Parent = this,
+ };
+ typeText.LocalX += -(typeText.Width + 4);
+ typeText.LocalY += (_toolstrip.Height - typeText.Height) * 0.5f;
+ typeText.RightClick = () => Clipboard.Text = Asset.DataTypeName;
+ _typeText = typeText;
+
_undo.Clear();
ClearEditedFlag();
@@ -175,6 +192,7 @@ namespace FlaxEditor.Windows.Assets
_isRegisteredForScriptsReload = false;
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
}
+ _typeText = null;
base.OnDestroy();
}
diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs b/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs
index 3246e51b5..6de553a30 100644
--- a/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs
+++ b/Source/Editor/Windows/Assets/PrefabWindow.Actions.cs
@@ -144,7 +144,7 @@ namespace FlaxEditor.Windows.Assets
// Set paste target if only one actor is selected and no target provided
if (pasteTargetActor == null && Selection.Count == 1 && Selection[0] is ActorNode actorNode)
{
- pasteTargetActor = actorNode.Actor;
+ pasteTargetActor = actorNode.Actor.IsPrefabRoot ? actorNode.Actor : actorNode.Actor.Parent;
}
// Create paste action
diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs
index f993b79f9..b1f04c8f5 100644
--- a/Source/Editor/Windows/ContentWindow.cs
+++ b/Source/Editor/Windows/ContentWindow.cs
@@ -247,8 +247,17 @@ namespace FlaxEditor.Windows
for (int i = 0; i < _viewDropdown.Items.Count; i++)
{
var filterButton = filters.ContextMenu.AddButton(_viewDropdown.Items[i], OnFilterClicked);
+ filterButton.CloseMenuOnClick = false;
filterButton.Tag = i;
}
+ filters.ContextMenu.ButtonClicked += button =>
+ {
+ foreach (var item in (filters.ContextMenu).Items)
+ {
+ if (item is ContextMenuButton filterButton)
+ filterButton.Checked = _viewDropdown.IsSelected(filterButton.Text);
+ }
+ };
filters.ContextMenu.VisibleChanged += control =>
{
if (!control.Visible)
@@ -340,6 +349,9 @@ namespace FlaxEditor.Windows
/// The created renaming popup.
public void Rename(ContentItem item)
{
+ if (!item.CanRename)
+ return;
+
// Show element in the view
Select(item, true);
diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs
index 46c0f33f4..513542fdc 100644
--- a/Source/Editor/Windows/GameWindow.cs
+++ b/Source/Editor/Windows/GameWindow.cs
@@ -303,7 +303,7 @@ namespace FlaxEditor.Windows
Editor.Options.OptionsChanged += OnOptionsChanged;
OnOptionsChanged(Editor.Options.Options);
- InputActions.Add(options => options.Play, Editor.Simulation.RequestPlayOrStopPlay);
+ InputActions.Add(options => options.Play, Editor.Simulation.DelegatePlayOrStopPlayInEditor);
InputActions.Add(options => options.Pause, Editor.Simulation.RequestResumeOrPause);
InputActions.Add(options => options.StepFrame, Editor.Simulation.RequestPlayOneFrame);
}
diff --git a/Source/Editor/Windows/SceneEditorWindow.cs b/Source/Editor/Windows/SceneEditorWindow.cs
index f55ff4ee4..72ff3be33 100644
--- a/Source/Editor/Windows/SceneEditorWindow.cs
+++ b/Source/Editor/Windows/SceneEditorWindow.cs
@@ -38,7 +38,7 @@ namespace FlaxEditor.Windows
InputActions.Add(options => options.SelectAll, Editor.SceneEditing.SelectAllScenes);
InputActions.Add(options => options.Delete, Editor.SceneEditing.Delete);
InputActions.Add(options => options.Search, () => Editor.Windows.SceneWin.Search());
- InputActions.Add(options => options.Play, Editor.Simulation.RequestPlayOrStopPlay);
+ InputActions.Add(options => options.Play, Editor.Simulation.DelegatePlayOrStopPlayInEditor);
InputActions.Add(options => options.Pause, Editor.Simulation.RequestResumeOrPause);
InputActions.Add(options => options.StepFrame, Editor.Simulation.RequestPlayOneFrame);
}
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index 82842376e..73ddbc41c 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -233,11 +233,10 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
// Get nested animation time position
float nestedAnimPrevPos = animPrevPos - nestedAnim.Time;
const float nestedAnimLength = nestedAnim.Anim->GetLength();
- const float nestedAnimDuration = nestedAnim.Anim->GetDuration();
const float nestedAnimSpeed = nestedAnim.Speed * speed;
- const float frameRateMatchScale = (float)nestedAnim.Anim->Data.FramesPerSecond / (float)anim->Data.FramesPerSecond;
- nestedAnimPos = nestedAnimPos / nestedAnimDuration * nestedAnimSpeed * frameRateMatchScale;
- nestedAnimPrevPos = nestedAnimPrevPos / nestedAnimDuration * nestedAnimSpeed * frameRateMatchScale;
+ const float frameRateMatchScale = nestedAnimSpeed / (float)anim->Data.FramesPerSecond;
+ nestedAnimPos = nestedAnimPos * frameRateMatchScale;
+ nestedAnimPrevPos = nestedAnimPrevPos * frameRateMatchScale;
GetAnimSamplePos(nestedAnim.Loop, nestedAnimLength, nestedAnim.StartTime, nestedAnimPrevPos, nestedAnimPos, nestedAnimPos, nestedAnimPrevPos);
ProcessAnimation(nodes, node, true, nestedAnimLength, nestedAnimPos, nestedAnimPrevPos, nestedAnim.Anim, 1.0f, weight, mode);
diff --git a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp
index 698a6025a..f90c58875 100644
--- a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp
+++ b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp
@@ -5,6 +5,7 @@
#include "AudioBackendXAudio2.h"
#include "Engine/Audio/AudioSettings.h"
#include "Engine/Core/Collections/Array.h"
+#include "Engine/Core/Collections/ChunkedArray.h"
#include "Engine/Core/Log.h"
#include "Engine/Audio/Audio.h"
#include "Engine/Audio/AudioSource.h"
@@ -208,8 +209,8 @@ namespace XAudio2
bool ForceDirty = true;
Listener Listeners[AUDIO_MAX_LISTENERS];
CriticalSection Locker;
- Array Sources(32); // TODO: use ChunkedArray for better performance
- Array Buffers(64); // TODO: use ChunkedArray for better performance or use buffers pool?
+ ChunkedArray Sources;
+ ChunkedArray Buffers; // TODO: use ChunkedArray for better performance or use buffers pool?
EngineCallback Callback;
Listener* GetListener()
diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp
index 973e494f1..ca89b3966 100644
--- a/Source/Engine/Core/Types/Variant.cpp
+++ b/Source/Engine/Core/Types/Variant.cpp
@@ -495,8 +495,13 @@ Variant::Variant(Variant&& other) noexcept
other.AsDictionary = nullptr;
break;
case VariantType::ManagedObject:
+#if USE_NETCORE
+ AsUint64 = other.AsUint64;
+ other.AsUint64 = 0;
+#elif USE_MONO
AsUint = other.AsUint;
other.AsUint = 0;
+#endif
break;
case VariantType::Null:
case VariantType::Void:
@@ -1019,8 +1024,13 @@ Variant& Variant::operator=(Variant&& other)
other.AsDictionary = nullptr;
break;
case VariantType::ManagedObject:
+#if USE_NETCORE
+ AsUint64 = other.AsUint64;
+ other.AsUint64 = 0;
+#elif USE_MONO
AsUint = other.AsUint;
other.AsUint = 0;
+#endif
break;
case VariantType::Null:
case VariantType::Void:
@@ -2419,7 +2429,11 @@ void Variant::SetType(const VariantType& type)
AsDictionary = New>();
break;
case VariantType::ManagedObject:
+#if USE_NETCORE
+ AsUint64 = 0;
+#elif USE_MONO
AsUint = 0;
+#endif
break;
case VariantType::Structure:
AllocStructure();
@@ -2532,7 +2546,11 @@ void Variant::SetType(VariantType&& type)
AsDictionary = New>();
break;
case VariantType::ManagedObject:
+#if USE_NETCORE
+ AsUint64 = 0;
+#elif USE_MONO
AsUint = 0;
+#endif
break;
case VariantType::Structure:
AllocStructure();
@@ -2671,7 +2689,11 @@ void Variant::SetManagedObject(MObject* object)
{
if (Type.Type != VariantType::ManagedObject || Type.TypeName)
SetType(VariantType(VariantType::ManagedObject));
+#if USE_NETCORE
+ AsUint64 = 0;
+#elif USE_MONO
AsUint = 0;
+#endif
}
#endif
}
diff --git a/Source/Engine/Engine/NativeInterop.Invoker.cs b/Source/Engine/Engine/NativeInterop.Invoker.cs
index 2fc54597c..381e6dac0 100644
--- a/Source/Engine/Engine/NativeInterop.Invoker.cs
+++ b/Source/Engine/Engine/NativeInterop.Invoker.cs
@@ -8,6 +8,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Diagnostics;
+using System.Collections.Generic;
namespace FlaxEngine.Interop
{
@@ -20,26 +21,135 @@ namespace FlaxEngine.Interop
{
// TODO: Use .NET8 Unsafe.BitCast(returnValue) for more efficient casting of value types over boxing cast
- internal static IntPtr MarshalReturnValue(ref TRet returnValue)
+ internal static class InvokerMarshallers
+ {
+ internal delegate IntPtr Delegate(ref T value);
+ internal static Delegate deleg;
+ internal static Delegate delegThunk;
+
+ static InvokerMarshallers()
+ {
+ Type type = typeof(T);
+ if (type == typeof(string))
+ deleg = typeof(Invoker).GetMethod(nameof(MarshalReturnValueString), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(ManagedHandle))
+ deleg = typeof(Invoker).GetMethod(nameof(MarshalReturnValueManagedHandle), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(Type))
+ deleg = typeof(Invoker).GetMethod(nameof(MarshalReturnValueType), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type.IsArray)
+ deleg = typeof(Invoker).GetMethod(nameof(MarshalReturnValueArray), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(type).CreateDelegate();
+ else if (type == typeof(bool))
+ deleg = typeof(Invoker).GetMethod(nameof(MarshalReturnValueBool), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else
+ deleg = typeof(Invoker).GetMethod(nameof(MarshalReturnValueWrapped), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(type).CreateDelegate();
+
+ if (type == typeof(string))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueString), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(ManagedHandle))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueManagedHandle), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(Type))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueType), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type.IsArray)
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueArray), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(type).CreateDelegate();
+ else if (type == typeof(System.Boolean))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueMonoBoolean), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(IntPtr))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueIntPtr), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(System.Int16))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueInt16), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(System.Int32))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueInt32), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(System.Int64))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueInt64), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(System.UInt16))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueUInt16), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(System.UInt32))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueUInt32), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else if (type == typeof(System.UInt64))
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueUInt64), BindingFlags.Static | BindingFlags.NonPublic).CreateDelegate();
+ else
+ delegThunk = typeof(Invoker).GetMethod(nameof(MarshalReturnValueWrapped), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(type).CreateDelegate();
+ }
+ }
+
+ internal static IntPtr MarshalReturnValueString(ref string returnValue)
+ {
+ return returnValue != null ? ManagedString.ToNativeWeak(returnValue) : IntPtr.Zero;
+ }
+
+ internal static IntPtr MarshalReturnValueManagedHandle(ref ManagedHandle returnValue)
+ {
+ return returnValue != null ? ManagedHandle.ToIntPtr(returnValue) : IntPtr.Zero;
+ }
+
+ internal static IntPtr MarshalReturnValueType(ref Type returnValue)
+ {
+ return returnValue != null ? ManagedHandle.ToIntPtr(GetTypeGCHandle(returnValue)) : IntPtr.Zero;
+ }
+
+ internal static IntPtr MarshalReturnValueArray(ref TRet returnValue)
{
if (returnValue == null)
return IntPtr.Zero;
- if (typeof(TRet) == typeof(string))
- return ManagedString.ToNativeWeak(Unsafe.As(returnValue));
- if (typeof(TRet) == typeof(ManagedHandle))
- return ManagedHandle.ToIntPtr((ManagedHandle)(object)returnValue);
- if (typeof(TRet) == typeof(bool))
- return (bool)(object)returnValue ? boolTruePtr : boolFalsePtr;
- if (typeof(TRet) == typeof(Type))
- return ManagedHandle.ToIntPtr(GetTypeGCHandle(Unsafe.As(returnValue)));
- if (typeof(TRet).IsArray)
- {
- var elementType = typeof(TRet).GetElementType();
- if (ArrayFactory.GetMarshalledType(elementType) == elementType)
- return ManagedHandle.ToIntPtr(ManagedArray.WrapNewArray(Unsafe.As(returnValue)), GCHandleType.Weak);
- return ManagedHandle.ToIntPtr(ManagedArrayToGCHandleWrappedArray(Unsafe.As(returnValue)), GCHandleType.Weak);
- }
- return ManagedHandle.ToIntPtr(returnValue, GCHandleType.Weak);
+ var elementType = typeof(TRet).GetElementType();
+ if (ArrayFactory.GetMarshalledType(elementType) == elementType)
+ return ManagedHandle.ToIntPtr(ManagedArray.WrapNewArray(Unsafe.As(returnValue)), GCHandleType.Weak);
+ return ManagedHandle.ToIntPtr(ManagedArrayToGCHandleWrappedArray(Unsafe.As(returnValue)), GCHandleType.Weak);
+ }
+
+ internal static IntPtr MarshalReturnValueBool(ref bool returnValue)
+ {
+ return returnValue ? boolTruePtr : boolFalsePtr;
+ }
+
+ internal static IntPtr MarshalReturnValueIntPtr(ref IntPtr returnValue)
+ {
+ return returnValue;
+ }
+
+ internal static IntPtr MarshalReturnValueMonoBoolean(ref bool returnValue)
+ {
+ return returnValue ? 1 : 0;
+ }
+
+ internal static IntPtr MarshalReturnValueInt16(ref Int16 returnValue)
+ {
+ return returnValue;
+ }
+
+ internal static IntPtr MarshalReturnValueInt32(ref Int32 returnValue)
+ {
+ return returnValue;
+ }
+
+ internal static IntPtr MarshalReturnValueInt64(ref Int64 returnValue)
+ {
+ return new IntPtr(returnValue);
+ }
+
+ internal static IntPtr MarshalReturnValueUInt16(ref UInt16 returnValue)
+ {
+ return returnValue;
+ }
+
+ internal static IntPtr MarshalReturnValueUInt32(ref UInt32 returnValue)
+ {
+ return new IntPtr(returnValue);
+ }
+
+ internal static IntPtr MarshalReturnValueUInt64(ref UInt64 returnValue)
+ {
+ return new IntPtr((long)returnValue);
+ }
+
+ internal static IntPtr MarshalReturnValueWrapped(ref TRet returnValue)
+ {
+ return returnValue != null ? ManagedHandle.ToIntPtr(returnValue, GCHandleType.Weak) : IntPtr.Zero;
+ }
+
+ internal static IntPtr MarshalReturnValue(ref TRet returnValue)
+ {
+ return InvokerMarshallers.deleg(ref returnValue);
}
internal static IntPtr MarshalReturnValueGeneric(Type returnType, object returnObject)
@@ -63,39 +173,7 @@ namespace FlaxEngine.Interop
internal static IntPtr MarshalReturnValueThunk(ref TRet returnValue)
{
- if (returnValue == null)
- return IntPtr.Zero;
- if (typeof(TRet) == typeof(string))
- return ManagedString.ToNativeWeak(Unsafe.As(returnValue));
- if (typeof(TRet) == typeof(IntPtr))
- return (IntPtr)(object)returnValue;
- if (typeof(TRet) == typeof(ManagedHandle))
- return ManagedHandle.ToIntPtr((ManagedHandle)(object)returnValue);
- if (typeof(TRet) == typeof(Type))
- return ManagedHandle.ToIntPtr(GetTypeGCHandle(Unsafe.As(returnValue)));
- if (typeof(TRet).IsArray)
- {
- var elementType = typeof(TRet).GetElementType();
- if (ArrayFactory.GetMarshalledType(elementType) == elementType)
- return ManagedHandle.ToIntPtr(ManagedArray.WrapNewArray(Unsafe.As(returnValue)), GCHandleType.Weak);
- return ManagedHandle.ToIntPtr(ManagedArrayToGCHandleWrappedArray(Unsafe.As(returnValue)), GCHandleType.Weak);
- }
- // Match Mono bindings and pass value as pointer to prevent boxing it
- if (typeof(TRet) == typeof(System.Boolean))
- return new IntPtr(((System.Boolean)(object)returnValue) ? 1 : 0);
- if (typeof(TRet) == typeof(System.Int16))
- return new IntPtr((int)(System.Int16)(object)returnValue);
- if (typeof(TRet) == typeof(System.Int32))
- return new IntPtr((int)(System.Int32)(object)returnValue);
- if (typeof(TRet) == typeof(System.Int64))
- return new IntPtr((long)(System.Int64)(object)returnValue);
- if (typeof(TRet) == typeof(System.UInt16))
- return (IntPtr)new UIntPtr((ulong)(System.UInt16)(object)returnValue);
- if (typeof(TRet) == typeof(System.UInt32))
- return (IntPtr)new UIntPtr((ulong)(System.UInt32)(object)returnValue);
- if (typeof(TRet) == typeof(System.UInt64))
- return (IntPtr)new UIntPtr((ulong)(System.UInt64)(object)returnValue);
- return ManagedHandle.ToIntPtr(returnValue, GCHandleType.Weak);
+ return InvokerMarshallers.delegThunk(ref returnValue);
}
internal static IntPtr MarshalReturnValueThunkGeneric(Type returnType, object returnObject)
@@ -205,7 +283,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -215,7 +293,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -242,7 +320,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -262,7 +340,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -291,7 +369,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -317,7 +395,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -347,7 +425,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -379,7 +457,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -410,7 +488,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -448,7 +526,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -480,7 +558,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -490,7 +568,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -517,7 +595,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -537,7 +615,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -566,7 +644,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -592,7 +670,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -622,7 +700,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -654,7 +732,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -685,7 +763,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -723,7 +801,7 @@ namespace FlaxEngine.Interop
return IntPtr.Zero;
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -755,7 +833,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -765,7 +843,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -792,7 +870,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -812,7 +890,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -841,7 +919,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -867,7 +945,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -897,7 +975,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -929,7 +1007,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -960,7 +1038,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -998,7 +1076,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -1030,7 +1108,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -1040,7 +1118,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -1067,7 +1145,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -1087,7 +1165,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -1116,7 +1194,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -1142,7 +1220,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -1172,7 +1250,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -1204,7 +1282,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
@@ -1235,7 +1313,7 @@ namespace FlaxEngine.Interop
return Unsafe.As(CreateDelegateFromMethod(method, false));
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static IntPtr MarshalAndInvoke(object delegateContext, ManagedHandle instancePtr, IntPtr paramPtr)
{
(Type[] types, InvokerDelegate deleg) = (Tuple)(delegateContext);
@@ -1273,7 +1351,7 @@ namespace FlaxEngine.Interop
return MarshalReturnValue(ref ret);
}
- [DebuggerStepThrough]
+ //[DebuggerStepThrough]
internal static unsafe IntPtr InvokeThunk(object delegateContext, ManagedHandle instancePtr, IntPtr* paramPtrs)
{
ThunkInvokerDelegate deleg = Unsafe.As(delegateContext);
diff --git a/Source/Engine/Engine/NativeInterop.Managed.cs b/Source/Engine/Engine/NativeInterop.Managed.cs
index 95146a95c..e4dc449bc 100644
--- a/Source/Engine/Engine/NativeInterop.Managed.cs
+++ b/Source/Engine/Engine/NativeInterop.Managed.cs
@@ -29,6 +29,8 @@ namespace FlaxEngine.Interop
private int _elementSize;
private int _length;
+ [ThreadStatic] private static Dictionary pooledArrayHandles;
+
public static ManagedArray WrapNewArray(Array arr) => new ManagedArray(arr, arr.GetType());
public static ManagedArray WrapNewArray(Array arr, Type arrayType) => new ManagedArray(arr, arrayType);
@@ -37,22 +39,38 @@ namespace FlaxEngine.Interop
/// Returns an instance of ManagedArray from shared pool.
///
/// The resources must be released by calling FreePooled() instead of Free()-method.
- public static ManagedArray WrapPooledArray(Array arr)
+ public static (ManagedHandle managedHandle, ManagedArray managedArray) WrapPooledArray(Array arr)
{
ManagedArray managedArray = ManagedArrayPool.Get();
managedArray.WrapArray(arr, arr.GetType());
- return managedArray;
+
+ if (pooledArrayHandles == null)
+ pooledArrayHandles = new();
+ if (!pooledArrayHandles.TryGetValue(managedArray, out ManagedHandle handle))
+ {
+ handle = ManagedHandle.Alloc(managedArray);
+ pooledArrayHandles.Add(managedArray, handle);
+ }
+ return (handle, managedArray);
}
///
/// Returns an instance of ManagedArray from shared pool.
///
/// The resources must be released by calling FreePooled() instead of Free()-method.
- public static ManagedArray WrapPooledArray(Array arr, Type arrayType)
+ public static ManagedHandle WrapPooledArray(Array arr, Type arrayType)
{
ManagedArray managedArray = ManagedArrayPool.Get(arr.Length * NativeInterop.GetTypeSize(arr.GetType().GetElementType()));
managedArray.WrapArray(arr, arrayType);
- return managedArray;
+
+ if (pooledArrayHandles == null)
+ pooledArrayHandles = new();
+ if (!pooledArrayHandles.TryGetValue(managedArray, out ManagedHandle handle))
+ {
+ handle = ManagedHandle.Alloc(managedArray);
+ pooledArrayHandles.Add(managedArray, handle);
+ }
+ return handle;
}
internal static ManagedArray AllocateNewArray(int length, Type arrayType, Type elementType)
@@ -64,12 +82,20 @@ namespace FlaxEngine.Interop
///
/// Returns an instance of ManagedArray from shared pool.
///
- /// The resources must be released by calling FreePooled() instead of Free()-method.
- public static ManagedArray AllocatePooledArray(int length) where T : unmanaged
+ /// The resources must be released by calling FreePooled() instead of Free()-method. Do not release the returned ManagedHandle.
+ public static (ManagedHandle managedHandle, ManagedArray managedArray) AllocatePooledArray(int length) where T : unmanaged
{
ManagedArray managedArray = ManagedArrayPool.Get(length * Unsafe.SizeOf());
managedArray.Allocate(length);
- return managedArray;
+
+ if (pooledArrayHandles == null)
+ pooledArrayHandles = new();
+ if (!pooledArrayHandles.TryGetValue(managedArray, out ManagedHandle handle))
+ {
+ handle = ManagedHandle.Alloc(managedArray);
+ pooledArrayHandles.Add(managedArray, handle);
+ }
+ return (handle, managedArray);
}
public ManagedArray(Array arr, Type elementType) => WrapArray(arr, elementType);
@@ -91,18 +117,19 @@ namespace FlaxEngine.Interop
internal void Allocate(int length) where T : unmanaged
{
+ _length = length;
+ _arrayType = typeof(T[]);
+ _elementType = typeof(T);
+ _elementSize = Unsafe.SizeOf();
+
// Try to reuse existing allocated buffer
- if (length * Unsafe.SizeOf() > _unmanagedAllocationSize)
+ if (length * _elementSize > _unmanagedAllocationSize)
{
if (_unmanagedAllocationSize > 0)
NativeInterop.NativeFree(_unmanagedData.ToPointer());
- _unmanagedData = (IntPtr)NativeInterop.NativeAlloc(length, Unsafe.SizeOf());
- _unmanagedAllocationSize = Unsafe.SizeOf() * length;
+ _unmanagedData = (IntPtr)NativeInterop.NativeAlloc(length, _elementSize);
+ _unmanagedAllocationSize = _elementSize * length;
}
- _length = length;
- _arrayType = typeof(T).MakeArrayType();
- _elementType = typeof(T);
- _elementSize = Unsafe.SizeOf();
}
private ManagedArray()
@@ -112,12 +139,12 @@ namespace FlaxEngine.Interop
private ManagedArray(IntPtr ptr, int length, Type arrayType, Type elementType)
{
Assert.IsTrue(arrayType.IsArray);
+ _elementType = elementType;
+ _elementSize = NativeInterop.GetTypeSize(elementType);
_unmanagedData = ptr;
- _unmanagedAllocationSize = Marshal.SizeOf(elementType) * length;
+ _unmanagedAllocationSize = _elementSize * length;
_length = length;
_arrayType = arrayType;
- _elementType = elementType;
- _elementSize = NativeInterop.GetTypeSize(_elementType);
}
~ManagedArray()
@@ -330,15 +357,9 @@ namespace FlaxEngine.Interop
{
private IntPtr handle;
- private ManagedHandle(IntPtr handle)
- {
- this.handle = handle;
- }
+ private ManagedHandle(IntPtr handle) => this.handle = handle;
- private ManagedHandle(object value, GCHandleType type)
- {
- handle = ManagedHandlePool.AllocateHandle(value, type);
- }
+ private ManagedHandle(object value, GCHandleType type) => handle = ManagedHandlePool.AllocateHandle(value, type);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ManagedHandle Alloc(object value) => new ManagedHandle(value, GCHandleType.Normal);
@@ -383,7 +404,7 @@ namespace FlaxEngine.Interop
public override int GetHashCode() => handle.GetHashCode();
- public override bool Equals(object o) => o is ManagedHandle other && Equals(other);
+ public override bool Equals(object obj) => obj is ManagedHandle other && handle == other.handle;
public bool Equals(ManagedHandle other) => handle == other.handle;
@@ -391,42 +412,44 @@ namespace FlaxEngine.Interop
public static bool operator !=(ManagedHandle a, ManagedHandle b) => a.handle != b.handle;
- private static class ManagedHandlePool
+ internal static class ManagedHandlePool
{
private const int WeakPoolCollectionSizeThreshold = 10000000;
private const int WeakPoolCollectionTimeThreshold = 500;
- private static ulong normalHandleAccumulator = 0;
- private static ulong pinnedHandleAccumulator = 0;
- private static ulong weakHandleAccumulator = 0;
+ // Rolling numbers for handles, two bits reserved for the type
+ private static ulong normalHandleAccumulator = ((ulong)GCHandleType.Normal << 62) & 0xC000000000000000;
+ private static ulong pinnedHandleAccumulator = ((ulong)GCHandleType.Pinned << 62) & 0xC000000000000000;
+ private static ulong weakHandleAccumulator = ((ulong)GCHandleType.Weak << 62) & 0xC000000000000000;
- private static object poolLock = new object();
- private static Dictionary persistentPool = new Dictionary();
- private static Dictionary pinnedPool = new Dictionary();
+ // Dictionaries for storing the valid handles.
+ // Note: Using locks seems to be generally the fastest when adding or fetching from the dictionary.
+ // Concurrent dictionaries could also be considered, but they perform much slower when adding to the dictionary.
+ private static Dictionary persistentPool = new();
+ private static Dictionary pinnedPool = new();
- // Manage double-buffered pool for weak handles in order to avoid collecting in-flight handles
- [ThreadStatic] private static Dictionary weakPool;
- [ThreadStatic] private static Dictionary weakPoolOther;
- [ThreadStatic] private static ulong nextWeakPoolCollection;
- [ThreadStatic] private static int nextWeakPoolGCCollection;
- [ThreadStatic] private static long lastWeakPoolCollectionTime;
+ // TODO: Performance of pinned handles are poor at the moment due to GCHandle wrapping.
+ // TODO: .NET8: Experiment with pinned arrays for faster pinning: https://github.com/dotnet/runtime/pull/89293
+
+ // Manage double-buffered pool for weak handles in order to avoid collecting in-flight handles.
+ // Periodically when the pools are being accessed and conditions are met, the other pool is cleared and swapped.
+ private static Dictionary weakPool = new();
+ private static Dictionary weakPoolOther = new();
+ private static object weakPoolLock = new object();
+ private static ulong nextWeakPoolCollection;
+ private static int nextWeakPoolGCCollection;
+ private static long lastWeakPoolCollectionTime;
///
/// Tries to free all references to old weak handles so GC can collect them.
///
- private static void TryCollectWeakHandles()
+ internal static void TryCollectWeakHandles()
{
if (weakHandleAccumulator < nextWeakPoolCollection)
return;
nextWeakPoolCollection = weakHandleAccumulator + 1000;
- if (weakPool == null)
- {
- weakPool = new Dictionary();
- weakPoolOther = new Dictionary();
- nextWeakPoolGCCollection = GC.CollectionCount(0) + 1;
- return;
- }
+
// Try to swap pools after garbage collection or whenever the pool gets too large
var gc0CollectionCount = GC.CollectionCount(0);
if (gc0CollectionCount < nextWeakPoolGCCollection && weakPool.Count < WeakPoolCollectionSizeThreshold)
@@ -443,130 +466,157 @@ namespace FlaxEngine.Interop
weakPool.Clear();
}
- private static IntPtr NewHandle(GCHandleType type)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static IntPtr NewHandle(GCHandleType type) => type switch
{
- IntPtr handle;
- if (type == GCHandleType.Normal)
- handle = (IntPtr)Interlocked.Increment(ref normalHandleAccumulator);
- else if (type == GCHandleType.Pinned)
- handle = (IntPtr)Interlocked.Increment(ref pinnedHandleAccumulator);
- else //if (type == GCHandleType.Weak || type == GCHandleType.WeakTrackResurrection)
- handle = (IntPtr)Interlocked.Increment(ref weakHandleAccumulator);
-
- // Two bits reserved for the type
- handle |= (IntPtr)(((ulong)type << 62) & 0xC000000000000000);
- return handle;
- }
+ GCHandleType.Normal => (IntPtr)Interlocked.Increment(ref normalHandleAccumulator),
+ GCHandleType.Pinned => (IntPtr)Interlocked.Increment(ref pinnedHandleAccumulator),
+ GCHandleType.Weak => (IntPtr)Interlocked.Increment(ref weakHandleAccumulator),
+ GCHandleType.WeakTrackResurrection => (IntPtr)Interlocked.Increment(ref weakHandleAccumulator),
+ _ => throw new NotImplementedException(type.ToString())
+ };
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static GCHandleType GetHandleType(IntPtr handle)
- {
- return (GCHandleType)(((ulong)handle & 0xC000000000000000) >> 62);
- }
+ private static GCHandleType GetHandleType(IntPtr handle) => (GCHandleType)(((ulong)handle & 0xC000000000000000) >> 62);
internal static IntPtr AllocateHandle(object value, GCHandleType type)
{
- TryCollectWeakHandles();
IntPtr handle = NewHandle(type);
- if (type == GCHandleType.Normal)
+ switch (type)
{
- lock (poolLock)
+ case GCHandleType.Normal:
+ lock (persistentPool)
persistentPool.Add(handle, value);
- }
- else if (type == GCHandleType.Pinned)
- {
- lock (poolLock)
+ break;
+ case GCHandleType.Pinned:
+ lock (pinnedPool)
pinnedPool.Add(handle, GCHandle.Alloc(value, GCHandleType.Pinned));
+ break;
+ case GCHandleType.Weak:
+ case GCHandleType.WeakTrackResurrection:
+ lock (weakPoolLock)
+ {
+ TryCollectWeakHandles();
+ weakPool.Add(handle, value);
+ }
+ break;
}
- else if (type == GCHandleType.Weak || type == GCHandleType.WeakTrackResurrection)
- weakPool.Add(handle, value);
-
return handle;
}
internal static object GetObject(IntPtr handle)
{
- TryCollectWeakHandles();
- object value;
- GCHandleType type = GetHandleType(handle);
- if (type == GCHandleType.Normal)
+ switch (GetHandleType(handle))
{
- lock (poolLock)
+ case GCHandleType.Normal:
+ lock (persistentPool)
{
- if (persistentPool.TryGetValue(handle, out value))
+ if (persistentPool.TryGetValue(handle, out object value))
return value;
}
- }
- else if (type == GCHandleType.Pinned)
- {
- lock (poolLock)
+ break;
+ case GCHandleType.Pinned:
+ lock (pinnedPool)
{
- if (pinnedPool.TryGetValue(handle, out GCHandle gchandle))
- return gchandle.Target;
+ if (pinnedPool.TryGetValue(handle, out GCHandle gcHandle))
+ return gcHandle.Target;
}
+ break;
+ case GCHandleType.Weak:
+ case GCHandleType.WeakTrackResurrection:
+ lock (weakPoolLock)
+ {
+ TryCollectWeakHandles();
+ if (weakPool.TryGetValue(handle, out object value))
+ return value;
+ else if (weakPoolOther.TryGetValue(handle, out value))
+ return value;
+ }
+ break;
}
- else if (weakPool.TryGetValue(handle, out value))
- return value;
- else if (weakPoolOther.TryGetValue(handle, out value))
- return value;
-
throw new NativeInteropException("Invalid ManagedHandle");
}
internal static void SetObject(IntPtr handle, object value)
{
- TryCollectWeakHandles();
- GCHandleType type = GetHandleType(handle);
- if (type == GCHandleType.Normal)
+ switch (GetHandleType(handle))
{
- lock (poolLock)
+ case GCHandleType.Normal:
+ lock (persistentPool)
{
- if (persistentPool.ContainsKey(handle))
- persistentPool[handle] = value;
+ ref object obj = ref CollectionsMarshal.GetValueRefOrNullRef(persistentPool, handle);
+ if (!Unsafe.IsNullRef(ref obj))
+ {
+ obj = value;
+ return;
+ }
}
- }
- else if (type == GCHandleType.Pinned)
- {
- lock (poolLock)
+ break;
+ case GCHandleType.Pinned:
+ lock (pinnedPool)
{
- if (pinnedPool.TryGetValue(handle, out GCHandle gchandle))
- gchandle.Target = value;
+ ref GCHandle gcHandle = ref CollectionsMarshal.GetValueRefOrNullRef(pinnedPool, handle);
+ if (!Unsafe.IsNullRef(ref gcHandle))
+ {
+ gcHandle.Target = value;
+ return;
+ }
}
+ break;
+ case GCHandleType.Weak:
+ case GCHandleType.WeakTrackResurrection:
+ lock (weakPoolLock)
+ {
+ TryCollectWeakHandles();
+ {
+ ref object obj = ref CollectionsMarshal.GetValueRefOrNullRef(weakPool, handle);
+ if (!Unsafe.IsNullRef(ref obj))
+ {
+ obj = value;
+ return;
+ }
+ }
+ {
+ ref object obj = ref CollectionsMarshal.GetValueRefOrNullRef(weakPoolOther, handle);
+ if (!Unsafe.IsNullRef(ref obj))
+ {
+ obj = value;
+ return;
+ }
+ }
+ }
+ break;
}
- else if (weakPool.ContainsKey(handle))
- weakPool[handle] = value;
- else if (weakPoolOther.ContainsKey(handle))
- weakPoolOther[handle] = value;
-
throw new NativeInteropException("Invalid ManagedHandle");
}
internal static void FreeHandle(IntPtr handle)
{
- TryCollectWeakHandles();
- GCHandleType type = GetHandleType(handle);
- if (type == GCHandleType.Normal)
+ switch (GetHandleType(handle))
{
- lock (poolLock)
+ case GCHandleType.Normal:
+ lock (persistentPool)
{
if (persistentPool.Remove(handle))
return;
}
- }
- else if (type == GCHandleType.Pinned)
- {
- lock (poolLock)
+ break;
+ case GCHandleType.Pinned:
+ lock (pinnedPool)
{
- if (pinnedPool.Remove(handle, out GCHandle gchandle))
+ if (pinnedPool.Remove(handle, out GCHandle gcHandle))
{
- gchandle.Free();
+ gcHandle.Free();
return;
}
}
- }
- else
+ break;
+ case GCHandleType.Weak:
+ case GCHandleType.WeakTrackResurrection:
+ lock (weakPoolLock)
+ TryCollectWeakHandles();
return;
-
+ }
throw new NativeInteropException("Invalid ManagedHandle");
}
}
diff --git a/Source/Engine/Engine/NativeInterop.Marshallers.cs b/Source/Engine/Engine/NativeInterop.Marshallers.cs
index 2e3bc9183..3ea8a5faa 100644
--- a/Source/Engine/Engine/NativeInterop.Marshallers.cs
+++ b/Source/Engine/Engine/NativeInterop.Marshallers.cs
@@ -200,14 +200,13 @@ namespace FlaxEngine.Interop
public void FromManaged(Array managed)
{
if (managed != null)
- managedArray = ManagedArray.WrapPooledArray(managed);
+ (handle, managedArray) = ManagedArray.WrapPooledArray(managed);
}
public IntPtr ToUnmanaged()
{
if (managedArray == null)
return IntPtr.Zero;
- handle = ManagedHandle.Alloc(managedArray, GCHandleType.Weak);
return ManagedHandle.ToIntPtr(handle);
}
@@ -216,7 +215,6 @@ namespace FlaxEngine.Interop
if (managedArray == null)
return;
managedArray.FreePooled();
- //handle.Free(); // No need to free weak handles
}
}
@@ -335,7 +333,6 @@ namespace FlaxEngine.Interop
#endif
[CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), MarshalMode.ManagedToUnmanagedIn, typeof(ArrayMarshaller<,>.ManagedToNative))]
[CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), MarshalMode.UnmanagedToManagedOut, typeof(ArrayMarshaller<,>.ManagedToNative))]
- [CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), MarshalMode.ElementIn, typeof(ArrayMarshaller<,>.ManagedToNative))]
[CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), MarshalMode.ManagedToUnmanagedOut, typeof(ArrayMarshaller<,>.NativeToManaged))]
[CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), MarshalMode.UnmanagedToManagedIn, typeof(ArrayMarshaller<,>.NativeToManaged))]
[CustomMarshaller(typeof(CustomMarshallerAttribute.GenericPlaceholder[]), MarshalMode.ElementOut, typeof(ArrayMarshaller<,>.NativeToManaged))]
@@ -388,38 +385,27 @@ namespace FlaxEngine.Interop
#if FLAX_EDITOR
[HideInEditor]
#endif
- public static class ManagedToNative
+ public ref struct ManagedToNative
{
- public static TUnmanagedElement* AllocateContainerForUnmanagedElements(T[] managed, out int numElements)
+ T[] sourceArray;
+ ManagedArray managedArray;
+ ManagedHandle managedHandle;
+
+ public void FromManaged(T[] managed)
{
if (managed is null)
- {
- numElements = 0;
- return null;
- }
- numElements = managed.Length;
- ManagedArray managedArray = ManagedArray.AllocatePooledArray(managed.Length);
- return (TUnmanagedElement*)ManagedHandle.ToIntPtr(managedArray, GCHandleType.Normal);
- }
-
- public static ReadOnlySpan GetManagedValuesSource(T[] managed) => managed;
-
- public static Span GetUnmanagedValuesDestination(TUnmanagedElement* unmanaged, int numElements)
- {
- if (unmanaged == null)
- return Span.Empty;
- ManagedArray managedArray = Unsafe.As(ManagedHandle.FromIntPtr(new IntPtr(unmanaged)).Target);
- return managedArray.ToSpan();
- }
-
- public static void Free(TUnmanagedElement* unmanaged)
- {
- if (unmanaged == null)
return;
- ManagedHandle handle = ManagedHandle.FromIntPtr(new IntPtr(unmanaged));
- (Unsafe.As(handle.Target)).FreePooled();
- //handle.Free(); // No need to free weak handles
+ sourceArray = managed;
+ (managedHandle, managedArray) = ManagedArray.AllocatePooledArray(managed.Length);
}
+
+ public ReadOnlySpan GetManagedValuesSource() => sourceArray;
+
+ public Span GetUnmanagedValuesDestination() => managedArray != null ? managedArray.ToSpan() : Span.Empty;
+
+ public TUnmanagedElement* ToUnmanaged() => (TUnmanagedElement*)ManagedHandle.ToIntPtr(managedHandle);
+
+ public void Free() => managedArray?.FreePooled();
}
#if FLAX_EDITOR
@@ -427,26 +413,25 @@ namespace FlaxEngine.Interop
#endif
public struct Bidirectional
{
- T[] managedArray;
- ManagedArray unmanagedArray;
+ T[] sourceArray;
+ ManagedArray managedArray;
ManagedHandle handle;
public void FromManaged(T[] managed)
{
if (managed == null)
return;
- managedArray = managed;
- unmanagedArray = ManagedArray.AllocatePooledArray(managed.Length);
- handle = ManagedHandle.Alloc(unmanagedArray);
+ sourceArray = managed;
+ (handle, managedArray) = ManagedArray.AllocatePooledArray(managed.Length);
}
- public ReadOnlySpan GetManagedValuesSource() => managedArray;
+ public ReadOnlySpan GetManagedValuesSource() => sourceArray;
public Span GetUnmanagedValuesDestination()
{
- if (unmanagedArray == null)
+ if (managedArray == null)
return Span.Empty;
- return unmanagedArray.ToSpan();
+ return managedArray.ToSpan();
}
public TUnmanagedElement* ToUnmanaged() => (TUnmanagedElement*)ManagedHandle.ToIntPtr(handle);
@@ -454,26 +439,22 @@ namespace FlaxEngine.Interop
public void FromUnmanaged(TUnmanagedElement* unmanaged)
{
ManagedArray arr = Unsafe.As(ManagedHandle.FromIntPtr(new IntPtr(unmanaged)).Target);
- if (managedArray == null || managedArray.Length != arr.Length)
- managedArray = new T[arr.Length];
+ if (sourceArray == null || sourceArray.Length != arr.Length)
+ sourceArray = new T[arr.Length];
}
public ReadOnlySpan GetUnmanagedValuesSource(int numElements)
{
- if (unmanagedArray == null)
+ if (managedArray == null)
return ReadOnlySpan.Empty;
- return unmanagedArray.ToSpan();
+ return managedArray.ToSpan();
}
- public Span GetManagedValuesDestination(int numElements) => managedArray;
+ public Span GetManagedValuesDestination(int numElements) => sourceArray;
- public T[] ToManaged() => managedArray;
+ public T[] ToManaged() => sourceArray;
- public void Free()
- {
- unmanagedArray.FreePooled();
- handle.Free();
- }
+ public void Free() => managedArray.FreePooled();
}
public static TUnmanagedElement* AllocateContainerForUnmanagedElements(T[] managed, out int numElements)
@@ -484,9 +465,8 @@ namespace FlaxEngine.Interop
return null;
}
numElements = managed.Length;
- ManagedArray managedArray = ManagedArray.AllocatePooledArray(managed.Length);
- IntPtr handle = ManagedHandle.ToIntPtr(managedArray);
- return (TUnmanagedElement*)handle;
+ (ManagedHandle managedArrayHandle, _) = ManagedArray.AllocatePooledArray(managed.Length);
+ return (TUnmanagedElement*)ManagedHandle.ToIntPtr(managedArrayHandle);
}
public static ReadOnlySpan GetManagedValuesSource(T[] managed) => managed;
@@ -517,7 +497,6 @@ namespace FlaxEngine.Interop
return;
ManagedHandle handle = ManagedHandle.FromIntPtr(new IntPtr(unmanaged));
Unsafe.As(handle.Target).FreePooled();
- handle.Free();
}
}
diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs
index d2a8ca55e..879f804d2 100644
--- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs
+++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs
@@ -526,7 +526,7 @@ namespace FlaxEngine.Interop
{
Type elementType = Unsafe.As(typeHandle.Target);
Type marshalledType = ArrayFactory.GetMarshalledType(elementType);
- Type arrayType = elementType.MakeArrayType();
+ Type arrayType = ArrayFactory.GetArrayType(elementType);
if (marshalledType.IsValueType)
{
ManagedArray managedArray = ManagedArray.AllocateNewArray((int)size, arrayType, marshalledType);
@@ -544,7 +544,7 @@ namespace FlaxEngine.Interop
internal static ManagedHandle GetArrayTypeFromElementType(ManagedHandle elementTypeHandle)
{
Type elementType = Unsafe.As(elementTypeHandle.Target);
- Type classType = elementType.MakeArrayType();
+ Type classType = ArrayFactory.GetArrayType(elementType);
return GetTypeGCHandle(classType);
}
@@ -649,8 +649,6 @@ namespace FlaxEngine.Interop
Type type = value.GetType();
if (!type.IsValueType)
return ManagedHandle.ToIntPtr(handle);
-
- // HACK: Get the address of a non-pinned value
return ValueTypeUnboxer.GetPointer(value, type);
}
diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs
index da372ac90..dd6087b98 100644
--- a/Source/Engine/Engine/NativeInterop.cs
+++ b/Source/Engine/Engine/NativeInterop.cs
@@ -17,6 +17,7 @@ using FlaxEngine.Assertions;
using System.Collections.Concurrent;
using System.IO;
using System.Text;
+using System.Threading;
namespace FlaxEngine.Interop
{
@@ -967,6 +968,7 @@ namespace FlaxEngine.Interop
private delegate Array CreateArrayDelegate(long size);
private static ConcurrentDictionary marshalledTypes = new ConcurrentDictionary(1, 3);
+ private static ConcurrentDictionary arrayTypes = new ConcurrentDictionary(1, 3);
private static ConcurrentDictionary createArrayDelegates = new ConcurrentDictionary(1, 3);
internal static Type GetMarshalledType(Type elementType)
@@ -986,6 +988,15 @@ namespace FlaxEngine.Interop
return marshalledTypes.GetOrAdd(elementType, Factory);
}
+ internal static Type GetArrayType(Type elementType)
+ {
+ static Type Factory(Type type) => type.MakeArrayType();
+
+ if (arrayTypes.TryGetValue(elementType, out var arrayType))
+ return arrayType;
+ return arrayTypes.GetOrAdd(elementType, Factory);
+ }
+
internal static Array CreateArray(Type type, long size)
{
static CreateArrayDelegate Factory(Type type)
@@ -1009,44 +1020,79 @@ namespace FlaxEngine.Interop
internal static class ValueTypeUnboxer
{
- private delegate IntPtr UnboxerDelegate(object value);
+ private static GCHandle[] pinnedBoxedValues = new GCHandle[256];
+ private static uint pinnedBoxedValuesPointer = 0;
+ private static (IntPtr ptr, int size)[] pinnedAllocations = new (IntPtr ptr, int size)[256];
+ private static uint pinnedAllocationsPointer = 0;
+
+ private delegate TInternal ToNativeDelegate(T value);
+ private delegate IntPtr UnboxerDelegate(object value, object converter);
- private static ConcurrentDictionary unboxers = new ConcurrentDictionary(1, 3);
+ private static ConcurrentDictionary unboxers = new (1, 3);
private static MethodInfo unboxerMethod = typeof(ValueTypeUnboxer).GetMethod(nameof(ValueTypeUnboxer.UnboxPointer), BindingFlags.Static | BindingFlags.NonPublic);
private static MethodInfo unboxerToNativeMethod = typeof(ValueTypeUnboxer).GetMethod(nameof(ValueTypeUnboxer.UnboxPointerWithConverter), BindingFlags.Static | BindingFlags.NonPublic);
internal static IntPtr GetPointer(object value, Type type)
{
- if (!unboxers.TryGetValue(type, out var deleg))
+ if (!unboxers.TryGetValue(type, out var tuple))
{
// Non-POD structures use internal layout (eg. SpriteHandleManaged in C++ with SpriteHandleMarshaller.SpriteHandleInternal in C#) so convert C# data into C++ data
var attr = type.GetCustomAttribute();
var toNativeMethod = attr?.NativeType.GetMethod("ToNative", BindingFlags.Static | BindingFlags.NonPublic);
if (toNativeMethod != null)
{
- deleg = unboxerToNativeMethod.MakeGenericMethod(toNativeMethod.ReturnType).CreateDelegate();
+ tuple.deleg = unboxerToNativeMethod.MakeGenericMethod(type, toNativeMethod.ReturnType).CreateDelegate();
+ tuple.toNativeDeleg = toNativeMethod.CreateDelegate(typeof(ToNativeDelegate<,>).MakeGenericType(type, toNativeMethod.ReturnType));
}
else
{
- deleg = unboxerMethod.MakeGenericMethod(type).CreateDelegate();
+ tuple.deleg = unboxerMethod.MakeGenericMethod(type).CreateDelegate();
}
- deleg = unboxers.GetOrAdd(type, deleg);
+ tuple = unboxers.GetOrAdd(type, tuple);
}
- return deleg(value);
+ return tuple.deleg(value, tuple.toNativeDeleg);
}
- private static IntPtr UnboxPointer(object value) where T : struct
+ private static void PinValue(object value)
{
+ // Prevent garbage collector from relocating the boxed value by pinning it temporarily.
+ // The pointer should remain valid quite long time but will be eventually unpinned.
+ uint index = Interlocked.Increment(ref pinnedBoxedValuesPointer) % (uint)pinnedBoxedValues.Length;
+ ref GCHandle handle = ref pinnedBoxedValues[index];
+ if (handle.IsAllocated)
+ handle.Free();
+ handle = GCHandle.Alloc(value, GCHandleType.Pinned);
+ }
+
+ private static IntPtr PinValue(T value) where T : struct
+ {
+ // Store the converted value in unmanaged memory so it will not be relocated by the garbage collector.
+ int size = Unsafe.SizeOf();
+ uint index = Interlocked.Increment(ref pinnedAllocationsPointer) % (uint)pinnedAllocations.Length;
+ ref (IntPtr ptr, int size) alloc = ref pinnedAllocations[index];
+ if (alloc.size < size)
+ {
+ if (alloc.ptr != IntPtr.Zero)
+ NativeFree(alloc.ptr.ToPointer());
+ alloc.ptr = new IntPtr(NativeAlloc(size));
+ alloc.size = size;
+ }
+ Unsafe.Write(alloc.ptr.ToPointer(), value);
+ return alloc.ptr;
+ }
+
+ private static IntPtr UnboxPointer(object value, object converter) where T : struct
+ {
+ if (RuntimeHelpers.IsReferenceOrContainsReferences()) // Cannot pin structure with references
+ return IntPtr.Zero;
+ PinValue(value);
return new IntPtr(Unsafe.AsPointer(ref Unsafe.Unbox(value)));
}
- private static IntPtr UnboxPointerWithConverter(object value) where T : struct
+ private static IntPtr UnboxPointerWithConverter(object value, object converter) where T : struct where TInternal : struct
{
- var type = value.GetType();
- var attr = type.GetCustomAttribute();
- var toNative = attr.NativeType.GetMethod("ToNative", BindingFlags.Static | BindingFlags.NonPublic);
- value = toNative.Invoke(null, new[] { value });
- return new IntPtr(Unsafe.AsPointer(ref Unsafe.Unbox(value)));
+ ToNativeDelegate toNative = Unsafe.As>(converter);
+ return PinValue(toNative(Unsafe.Unbox(value)));
}
}
diff --git a/Source/Engine/Engine/Screen.cpp b/Source/Engine/Engine/Screen.cpp
index 2fc58c7a3..2bc6e1f58 100644
--- a/Source/Engine/Engine/Screen.cpp
+++ b/Source/Engine/Engine/Screen.cpp
@@ -135,6 +135,52 @@ void Screen::SetCursorLock(CursorLockMode mode)
CursorLock = mode;
}
+GameWindowMode Screen::GetGameWindowMode()
+{
+ GameWindowMode result = GameWindowMode::Windowed;
+#if !USE_EDITOR
+ auto win = Engine::MainWindow;
+ if (win)
+ {
+ if (GetIsFullscreen() || win->IsFullscreen())
+ result = GameWindowMode::Fullscreen;
+ else if (win->GetSettings().HasBorder)
+ result = GameWindowMode::Windowed;
+ else if (win->GetClientPosition().IsZero() && win->GetSize() == Platform::GetDesktopSize())
+ result = GameWindowMode::FullscreenBorderless;
+ else
+ result = GameWindowMode::Borderless;
+ }
+#endif
+ return result;
+}
+
+void Screen::SetGameWindowMode(GameWindowMode windowMode)
+{
+#if !USE_EDITOR
+ auto win = Engine::MainWindow;
+ if (!win)
+ return;
+ switch (windowMode)
+ {
+ case GameWindowMode::Windowed:
+ if (GetIsFullscreen())
+ SetIsFullscreen(false);
+ win->SetBorderless(false, false);
+ break;
+ case GameWindowMode::Fullscreen:
+ SetIsFullscreen(true);
+ break;
+ case GameWindowMode::Borderless:
+ win->SetBorderless(true, false);
+ break;
+ case GameWindowMode::FullscreenBorderless:
+ win->SetBorderless(true, true);
+ break;
+ }
+#endif
+}
+
void ScreenService::Update()
{
#if USE_EDITOR
diff --git a/Source/Engine/Engine/Screen.h b/Source/Engine/Engine/Screen.h
index 532529980..42be20a38 100644
--- a/Source/Engine/Engine/Screen.h
+++ b/Source/Engine/Engine/Screen.h
@@ -2,6 +2,7 @@
#pragma once
+#include "Engine/Core/Config/PlatformSettingsBase.h"
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Input/Enums.h"
#include "Engine/Core/Math/Vector2.h"
@@ -80,4 +81,19 @@ DECLARE_SCRIPTING_TYPE_NO_SPAWN(Screen);
///
/// The mode.
API_PROPERTY() static void SetCursorLock(CursorLockMode mode);
+
+ ///
+ /// Gets the game window mode.
+ ///
+ /// The current window mode.
+ API_PROPERTY() static GameWindowMode GetGameWindowMode();
+
+ ///
+ /// Sets the game window mode.
+ ///
+ ///
+ /// A fullscreen mode switch may not happen immediately. It will be performed before next frame rendering. Will not work in editor.
+ ///
+ /// The window mode.
+ API_PROPERTY() static void SetGameWindowMode(GameWindowMode windowMode);
};
diff --git a/Source/Engine/Input/Input.cpp b/Source/Engine/Input/Input.cpp
index 22969c752..81411ed76 100644
--- a/Source/Engine/Input/Input.cpp
+++ b/Source/Engine/Input/Input.cpp
@@ -333,6 +333,8 @@ void Keyboard::OnCharInput(Char c, Window* target)
void Keyboard::OnKeyUp(KeyboardKeys key, Window* target)
{
+ if (key >= KeyboardKeys::MAX)
+ return;
Event& e = _queue.AddOne();
e.Type = EventType::KeyUp;
e.Target = target;
@@ -341,6 +343,8 @@ void Keyboard::OnKeyUp(KeyboardKeys key, Window* target)
void Keyboard::OnKeyDown(KeyboardKeys key, Window* target)
{
+ if (key >= KeyboardKeys::MAX)
+ return;
Event& e = _queue.AddOne();
e.Type = EventType::KeyDown;
e.Target = target;
diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
index 53453afe9..5ea8ad0a4 100644
--- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
+++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
@@ -675,20 +675,12 @@ bool Prefab::ApplyAll(Actor* targetActor)
}
}
- // Setup default instances
- for (int32 i = 0; i < allPrefabs.Count(); i++)
+ // Setup default instances (skip invalid prefabs)
+ for (int32 i = allPrefabs.Count() - 1; i >= 0; i--)
{
Prefab* prefab = allPrefabs[i];
- if (prefab->WaitForLoaded())
- {
- LOG(Warning, "Waiting for nesting prefab asset load failed.");
- return true;
- }
- if (prefab->GetDefaultInstance() == nullptr)
- {
- LOG(Warning, "Failed to create default prefab instance for the nested prefab asset.");
- return true;
- }
+ if (prefab->WaitForLoaded() || prefab->GetDefaultInstance() == nullptr)
+ allPrefabs.RemoveAt(i);
}
}
diff --git a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp
index 85a022c82..89e76a9bc 100644
--- a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp
+++ b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp
@@ -34,10 +34,10 @@ void NavMeshBoundsVolume::Deserialize(DeserializeStream& stream, ISerializeModif
void NavMeshBoundsVolume::OnEnable()
{
+ GetScene()->Navigation.Volumes.Add(this);
+
// Base
Actor::OnEnable();
-
- GetScene()->Navigation.Volumes.Add(this);
}
void NavMeshBoundsVolume::OnDisable()
diff --git a/Source/Engine/Networking/NetworkInternal.h b/Source/Engine/Networking/NetworkInternal.h
index efcabad5f..521e8a7a2 100644
--- a/Source/Engine/Networking/NetworkInternal.h
+++ b/Source/Engine/Networking/NetworkInternal.h
@@ -12,6 +12,7 @@ enum class NetworkMessageIDs : uint8
ObjectReplicate,
ObjectReplicatePart,
ObjectSpawn,
+ ObjectSpawnPart,
ObjectDespawn,
ObjectRole,
ObjectRpc,
@@ -30,6 +31,7 @@ public:
static void OnNetworkMessageObjectReplicate(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
static void OnNetworkMessageObjectReplicatePart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
static void OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
+ static void OnNetworkMessageObjectSpawnPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
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);
diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp
index b18136e23..ee2395f2a 100644
--- a/Source/Engine/Networking/NetworkManager.cpp
+++ b/Source/Engine/Networking/NetworkManager.cpp
@@ -15,7 +15,7 @@
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Scripting/Scripting.h"
-#define NETWORK_PROTOCOL_VERSION 2
+#define NETWORK_PROTOCOL_VERSION 3
float NetworkManager::NetworkFPS = 60.0f;
NetworkPeer* NetworkManager::Peer = nullptr;
@@ -131,6 +131,7 @@ namespace
NetworkInternal::OnNetworkMessageObjectReplicate,
NetworkInternal::OnNetworkMessageObjectReplicatePart,
NetworkInternal::OnNetworkMessageObjectSpawn,
+ NetworkInternal::OnNetworkMessageObjectSpawnPart,
NetworkInternal::OnNetworkMessageObjectDespawn,
NetworkInternal::OnNetworkMessageObjectRole,
NetworkInternal::OnNetworkMessageObjectRpc,
diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp
index 279391e31..8133b6cf2 100644
--- a/Source/Engine/Networking/NetworkReplicator.cpp
+++ b/Source/Engine/Networking/NetworkReplicator.cpp
@@ -66,8 +66,17 @@ PACK_STRUCT(struct NetworkMessageObjectSpawn
{
NetworkMessageIDs ID = NetworkMessageIDs::ObjectSpawn;
uint32 OwnerClientId;
+ uint32 OwnerSpawnId; // Unique for peer who spawned it and matches OwnerSpawnId inside following part messages
Guid PrefabId;
- uint16 ItemsCount;
+ uint16 ItemsCount; // Total items count
+ uint8 UseParts : 1; // True if spawn message is header-only and all items come in the separate parts
+ });
+
+PACK_STRUCT(struct NetworkMessageObjectSpawnPart
+ {
+ NetworkMessageIDs ID = NetworkMessageIDs::ObjectSpawnPart;
+ uint32 OwnerClientId;
+ uint32 OwnerSpawnId;
});
PACK_STRUCT(struct NetworkMessageObjectSpawnItem
@@ -173,6 +182,12 @@ struct SpawnItem
NetworkObjectRole Role;
};
+struct SpawnItemParts
+{
+ NetworkMessageObjectSpawn MsgData;
+ Array Items;
+};
+
struct SpawnGroup
{
Array> Items;
@@ -198,6 +213,7 @@ namespace
CriticalSection ObjectsLock;
HashSet Objects;
Array ReplicationParts;
+ Array SpawnParts;
Array SpawnQueue;
Array DespawnQueue;
Array RpcQueue;
@@ -213,6 +229,7 @@ namespace
Dictionary CSharpCachedNames;
#endif
Array DespawnedObjects;
+ uint32 SpawnId = 0;
}
class NetworkReplicationService : public EngineService
@@ -258,7 +275,7 @@ NetworkReplicatedObject* ResolveObject(Guid objectId)
return it != Objects.End() ? &it->Item : nullptr;
}
-NetworkReplicatedObject* ResolveObject(Guid objectId, Guid parentId, char objectTypeName[128])
+NetworkReplicatedObject* ResolveObject(Guid objectId, Guid parentId, const char objectTypeName[128])
{
// Lookup object
NetworkReplicatedObject* obj = ResolveObject(objectId);
@@ -398,8 +415,33 @@ FORCE_INLINE void GetNetworkName(char buffer[128], const StringAnsiView& name)
buffer[name.Length()] = 0;
}
+void SetupObjectSpawnMessageItem(SpawnItem* e, NetworkMessage& msg)
+{
+ ScriptingObject* obj = e->Object.Get();
+ auto it = Objects.Find(obj->GetID());
+ const auto& item = it->Item;
+
+ // Add object into spawn message
+ NetworkMessageObjectSpawnItem msgDataItem;
+ msgDataItem.ObjectId = item.ObjectId;
+ msgDataItem.ParentId = item.ParentId;
+ if (NetworkManager::IsClient())
+ {
+ // Remap local client object ids into server ids
+ IdsRemappingTable.KeyOf(msgDataItem.ObjectId, &msgDataItem.ObjectId);
+ IdsRemappingTable.KeyOf(msgDataItem.ParentId, &msgDataItem.ParentId);
+ }
+ msgDataItem.PrefabObjectID = Guid::Empty;
+ auto* objScene = ScriptingObject::Cast(obj);
+ if (objScene && objScene->HasPrefabLink())
+ msgDataItem.PrefabObjectID = objScene->GetPrefabObjectID();
+ GetNetworkName(msgDataItem.ObjectTypeName, obj->GetType().Fullname);
+ msg.WriteStructure(msgDataItem);
+}
+
void SendObjectSpawnMessage(const SpawnGroup& group, const Array& clients)
{
+ PROFILE_CPU();
const bool isClient = NetworkManager::IsClient();
auto* peer = NetworkManager::Peer;
NetworkMessage msg = peer->BeginSendMessage();
@@ -415,37 +457,56 @@ void SendObjectSpawnMessage(const SpawnGroup& group, const Array
// Setup clients that should receive this spawn message
auto it = Objects.Find(obj->GetID());
- auto& item = it->Item;
+ const auto& item = it->Item;
BuildCachedTargets(clients, item.TargetClientIds);
}
- msg.WriteStructure(msgData);
- for (SpawnItem* e : group.Items)
- {
- ScriptingObject* obj = e->Object.Get();
- auto it = Objects.Find(obj->GetID());
- auto& item = it->Item;
- // Add object into spawn message
- NetworkMessageObjectSpawnItem msgDataItem;
- msgDataItem.ObjectId = item.ObjectId;
- msgDataItem.ParentId = item.ParentId;
+ // Network Peer has fixed size of messages so split spawn message into parts if there are too many objects to fit at once
+ msgData.OwnerSpawnId = ++SpawnId;
+ msgData.UseParts = msg.BufferSize - msg.Position < group.Items.Count() * sizeof(NetworkMessageObjectSpawnItem);
+ msg.WriteStructure(msgData);
+ if (msgData.UseParts)
+ {
if (isClient)
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg);
+ else
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
+
+ // Send spawn items in separate parts
+ NetworkMessageObjectSpawnPart msgDataPart;
+ msgDataPart.OwnerClientId = msgData.OwnerClientId;
+ msgDataPart.OwnerSpawnId = msgData.OwnerSpawnId;
+ uint16 itemIndex = 0;
+ constexpr uint32 spawnItemMaxSize = sizeof(uint16) + sizeof(NetworkMessageObjectSpawnItem); // Index + Data
+ while (itemIndex < msgData.ItemsCount)
{
- // Remap local client object ids into server ids
- IdsRemappingTable.KeyOf(msgDataItem.ObjectId, &msgDataItem.ObjectId);
- IdsRemappingTable.KeyOf(msgDataItem.ParentId, &msgDataItem.ParentId);
+ msg = peer->BeginSendMessage();
+ msg.WriteStructure(msgDataPart);
+
+ // Write as many items as possible into this message
+ while (msg.Position + spawnItemMaxSize <= msg.BufferSize && itemIndex < msgData.ItemsCount)
+ {
+ msg.WriteUInt16(itemIndex);
+ SetupObjectSpawnMessageItem(group.Items[itemIndex], msg);
+ itemIndex++;
+ }
+
+ if (isClient)
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg);
+ else
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
}
- msgDataItem.PrefabObjectID = Guid::Empty;
- auto* objScene = ScriptingObject::Cast(obj);
- if (objScene && objScene->HasPrefabLink())
- msgDataItem.PrefabObjectID = objScene->GetPrefabObjectID();
- GetNetworkName(msgDataItem.ObjectTypeName, obj->GetType().Fullname);
- msg.WriteStructure(msgDataItem);
}
- if (isClient)
- peer->EndSendMessage(NetworkChannelType::Reliable, msg);
else
- peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
+ {
+ // Send all spawn items within the spawn message
+ for (SpawnItem* e : group.Items)
+ SetupObjectSpawnMessageItem(e, msg);
+ if (isClient)
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg);
+ else
+ peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
+ }
}
void SendObjectRoleMessage(const NetworkReplicatedObject& item, const NetworkClient* excludedClient = nullptr)
@@ -655,6 +716,235 @@ void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, b
DirtyObjectImpl(item, obj);
}
+void InvokeObjectSpawn(const NetworkMessageObjectSpawn& msgData, const NetworkMessageObjectSpawnItem* msgDataItems)
+{
+ ScopeLock lock(ObjectsLock);
+
+ // Check if that object has been already spawned
+ auto& rootItem = msgDataItems[0];
+ NetworkReplicatedObject* root = ResolveObject(rootItem.ObjectId, rootItem.ParentId, rootItem.ObjectTypeName);
+ if (root)
+ {
+ // Object already exists locally so just synchronize the ownership (and mark as spawned)
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ NetworkReplicatedObject* e = ResolveObject(msgDataItem.ObjectId, msgDataItem.ParentId, msgDataItem.ObjectTypeName);
+ auto& item = *e;
+ item.Spawned = true;
+ if (NetworkManager::IsClient())
+ {
+ // Server always knows the best so update ownership of the existing object
+ item.OwnerClientId = msgData.OwnerClientId;
+ if (item.Role == NetworkObjectRole::OwnedAuthoritative)
+ {
+ if (Hierarchy)
+ Hierarchy->AddObject(item.Object);
+ item.Role = NetworkObjectRole::Replicated;
+ }
+ }
+ else if (item.OwnerClientId != msgData.OwnerClientId)
+ {
+ // Other client spawned object with a different owner
+ // TODO: send reply message to inform about proper object ownership that client
+ }
+ }
+ return;
+ }
+
+ // Recreate object locally (spawn only root)
+ Actor* prefabInstance = nullptr;
+ Array objects;
+ if (msgData.PrefabId.IsValid())
+ {
+ const NetworkReplicatedObject* parent = ResolveObject(rootItem.ParentId);
+ Actor* parentActor = parent && parent->Object && parent->Object->Is() ? parent->Object.As() : nullptr;
+ if (parentActor && parentActor->GetPrefabID() == msgData.PrefabId)
+ {
+ // Reuse parent object as prefab instance
+ prefabInstance = parentActor;
+ }
+ else if ((parentActor = Scripting::TryFindObject(rootItem.ParentId)))
+ {
+ // Try to find that spawned prefab (eg. prefab with networked script was spawned before so now we need to link it)
+ for (Actor* child : parentActor->Children)
+ {
+ if (child->GetPrefabID() == msgData.PrefabId)
+ {
+ if (Objects.Contains(child->GetID()))
+ {
+ ScriptingObject* obj = FindPrefabObject(child, rootItem.PrefabObjectID);
+ if (Objects.Contains(obj->GetID()))
+ {
+ // Other instance with already spawned network object
+ obj = nullptr;
+ }
+ else
+ {
+ // Reuse already spawned object within a parent
+ prefabInstance = child;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!prefabInstance)
+ {
+ // Spawn prefab
+ auto prefab = (Prefab*)LoadAsset(msgData.PrefabId, Prefab::TypeInitializer);
+ if (!prefab)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
+ return;
+ }
+ prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
+ if (!prefabInstance)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString());
+ return;
+ }
+ }
+
+ // Resolve objects from prefab instance
+ objects.Resize(msgData.ItemsCount);
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ ScriptingObject* obj = FindPrefabObject(prefabInstance, msgDataItem.PrefabObjectID);
+ if (!obj)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", msgDataItem.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
+ Delete(prefabInstance);
+ return;
+ }
+ objects[i] = obj;
+ }
+ }
+ else if (msgData.ItemsCount == 1)
+ {
+ // Spawn object
+ const ScriptingTypeHandle objectType = Scripting::FindScriptingType(rootItem.ObjectTypeName);
+ ScriptingObject* obj = ScriptingObject::NewObject(objectType);
+ if (!obj)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(rootItem.ObjectTypeName));
+ return;
+ }
+ objects.Add(obj);
+ }
+ else
+ {
+ // Spawn objects
+ objects.Resize(msgData.ItemsCount);
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ const ScriptingTypeHandle objectType = Scripting::FindScriptingType(msgDataItem.ObjectTypeName);
+ ScriptingObject* obj = ScriptingObject::NewObject(objectType);
+ if (!obj)
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(msgDataItem.ObjectTypeName));
+ for (ScriptingObject* e : objects)
+ Delete(e);
+ return;
+ }
+ objects[i] = obj;
+ if (i != 0)
+ {
+ // Link hierarchy of spawned objects before calling any networking code for them
+ if (auto sceneObject = ScriptingObject::Cast(obj))
+ {
+ Actor* parent = nullptr;
+ for (int32 j = 0; j < i; j++)
+ {
+ if (msgDataItems[j].ObjectId == msgDataItem.ParentId)
+ {
+ parent = ScriptingObject::Cast(objects[j]);
+ break;
+ }
+ }
+ if (parent)
+ sceneObject->SetParent(parent);
+ }
+ }
+ }
+ }
+
+ // Add all newly spawned objects
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ ScriptingObject* obj = objects[i];
+ if (!obj->IsRegistered())
+ obj->RegisterObject();
+ const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
+
+ // Add object to the list
+ NetworkReplicatedObject item;
+ item.Object = obj;
+ item.AsNetworkObject = ScriptingObject::ToInterface(obj);
+ item.ObjectId = obj->GetID();
+ item.ParentId = parent ? parent->ObjectId : Guid::Empty;
+ item.OwnerClientId = msgData.OwnerClientId;
+ item.Role = NetworkObjectRole::Replicated;
+ if (item.OwnerClientId == NetworkManager::LocalClientId)
+ {
+ // Upgrade ownership automatically (eg. server spawned object that local client should own)
+ item.Role = NetworkObjectRole::OwnedAuthoritative;
+ }
+ item.Spawned = true;
+ NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->Object->GetType().ToString() : String::Empty);
+ Objects.Add(MoveTemp(item));
+ if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
+ Hierarchy->AddObject(obj);
+
+ // Boost future lookups by using indirection
+ NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgDataItem.ObjectId, item.ToString(), obj->GetType().ToString());
+ IdsRemappingTable.Add(msgDataItem.ObjectId, item.ObjectId);
+ }
+
+ // Spawn all newly spawned objects (ensure to have valid ownership hierarchy set before spawning object)
+ for (int32 i = 0; i < msgData.ItemsCount; i++)
+ {
+ auto& msgDataItem = msgDataItems[i];
+ ScriptingObject* obj = objects[i];
+ auto it = Objects.Find(obj->GetID());
+ auto& item = it->Item;
+ const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
+
+ // Automatic parenting for scene objects
+ auto sceneObject = ScriptingObject::Cast(obj);
+ if (sceneObject)
+ {
+ if (parent && parent->Object.Get() && parent->Object->Is())
+ sceneObject->SetParent(parent->Object.As());
+ else if (auto* parentActor = Scripting::TryFindObject(msgDataItem.ParentId))
+ sceneObject->SetParent(parentActor);
+ else if (msgDataItem.ParentId.IsValid())
+ {
+#if USE_NETWORK_REPLICATOR_LOG
+ // Ignore case when parent object in a message was a scene (eg. that is already unloaded on a client)
+ AssetInfo assetInfo;
+ if (!Content::GetAssetInfo(msgDataItem.ParentId, assetInfo) || assetInfo.TypeName != TEXT("FlaxEngine.SceneAsset"))
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
+ }
+#endif
+ }
+ }
+ else if (!parent && msgDataItem.ParentId.IsValid())
+ {
+ NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
+ }
+
+ if (item.AsNetworkObject)
+ item.AsNetworkObject->OnNetworkSpawn();
+ }
+
+ // TODO: if we're server then spawn this object further on other clients (use TargetClientIds for that object - eg. object spawned by client on client for certain set of other clients only)
+}
+
NetworkRpcParams::NetworkRpcParams(const NetworkStream* stream)
: SenderId(stream->SenderId)
{
@@ -941,6 +1231,13 @@ bool NetworkReplicator::HasObject(const ScriptingObject* obj)
return false;
}
+ScriptingObject* NetworkReplicator::ResolveForeignObject(Guid objectId)
+{
+ if (const auto& object = ResolveObject(objectId))
+ return object->Object.Get();
+ return nullptr;
+}
+
uint32 NetworkReplicator::GetObjectOwnerClientId(const ScriptingObject* obj)
{
uint32 id = NetworkManager::ServerClientId;
@@ -1127,13 +1424,6 @@ bool NetworkReplicator::EndInvokeRPC(ScriptingObject* obj, const ScriptingTypeHa
rpc.Info = *info;
rpc.ArgsData.Copy(Span(argsStream->GetBuffer(), argsStream->GetPosition()));
rpc.Targets.Copy(targetIds);
-#if USE_EDITOR || !BUILD_RELEASE
- auto it = Objects.Find(obj->GetID());
- if (it == Objects.End())
- {
- LOG(Error, "Cannot invoke RPC method '{0}.{1}' on object '{2}' that is not registered in networking (use 'NetworkReplicator.AddObject').", type.ToString(), String(name), obj->GetID());
- }
-#endif
ObjectsLock.Unlock();
// Check if skip local execution (eg. server rpc called from client or client rpc with specific targets)
@@ -1388,7 +1678,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
auto& e = ReplicationParts[i];
if (e.PartsLeft > 0)
{
- // TODO: remove replication items after some TTL to prevent memory leaks
+ // TODO: remove replication items after some TTL to reduce memory usage
continue;
}
ScriptingObject* obj = e.Object.Get();
@@ -1408,13 +1698,15 @@ void NetworkInternal::NetworkReplicatorUpdate()
}
}
+ // TODO: remove items from SpawnParts after some TTL to reduce memory usage
+
// Replicate all owned networked objects with other clients or server
if (!CachedReplicationResult)
CachedReplicationResult = New();
CachedReplicationResult->Init();
- if (!isClient && NetworkManager::Clients.IsEmpty())
+ if ((!isClient && NetworkManager::Clients.IsEmpty()) || NetworkManager::NetworkFPS < -ZeroTolerance)
{
- // No need to update replication when nobody's around
+ // No need to update replication when nobody's around or when replication is disabled
}
else if (Hierarchy)
{
@@ -1554,7 +1846,13 @@ void NetworkInternal::NetworkReplicatorUpdate()
continue;
auto it = Objects.Find(obj->GetID());
if (it == Objects.End())
+ {
+#if USE_EDITOR || !BUILD_RELEASE
+ if (!DespawnedObjects.Contains(obj->GetID()))
+ LOG(Error, "Cannot invoke RPC method '{0}.{1}' on object '{2}' that is not registered in networking (use 'NetworkReplicator.AddObject').", e.Name.First.ToString(), String(e.Name.Second), obj->GetID());
+#endif
continue;
+ }
auto& item = it->Item;
// Send RPC message
@@ -1649,234 +1947,60 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl
PROFILE_CPU();
NetworkMessageObjectSpawn msgData;
event.Message.ReadStructure(msgData);
- auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem));
if (msgData.ItemsCount == 0)
return;
- ScopeLock lock(ObjectsLock);
-
- // Check if that object has been already spawned
- auto& rootItem = msgDataItems[0];
- NetworkReplicatedObject* root = ResolveObject(rootItem.ObjectId, rootItem.ParentId, rootItem.ObjectTypeName);
- if (root)
+ if (msgData.UseParts)
{
- // Object already exists locally so just synchronize the ownership (and mark as spawned)
- for (int32 i = 0; i < msgData.ItemsCount; i++)
- {
- auto& msgDataItem = msgDataItems[i];
- NetworkReplicatedObject* e = ResolveObject(msgDataItem.ObjectId, msgDataItem.ParentId, msgDataItem.ObjectTypeName);
- auto& item = *e;
- item.Spawned = true;
- if (NetworkManager::IsClient())
- {
- // Server always knows the best so update ownership of the existing object
- item.OwnerClientId = msgData.OwnerClientId;
- if (item.Role == NetworkObjectRole::OwnedAuthoritative)
- {
- if (Hierarchy)
- Hierarchy->AddObject(item.Object);
- item.Role = NetworkObjectRole::Replicated;
- }
- }
- else if (item.OwnerClientId != msgData.OwnerClientId)
- {
- // Other client spawned object with a different owner
- // TODO: send reply message to inform about proper object ownership that client
- }
- }
- return;
- }
-
- // Recreate object locally (spawn only root)
- Actor* prefabInstance = nullptr;
- Array objects;
- if (msgData.PrefabId.IsValid())
- {
- const NetworkReplicatedObject* parent = ResolveObject(rootItem.ParentId);
- Actor* parentActor = parent && parent->Object && parent->Object->Is() ? parent->Object.As() : nullptr;
- if (parentActor && parentActor->GetPrefabID() == msgData.PrefabId)
- {
- // Reuse parent object as prefab instance
- prefabInstance = parentActor;
- }
- else if ((parentActor = Scripting::TryFindObject(rootItem.ParentId)))
- {
- // Try to find that spawned prefab (eg. prefab with networked script was spawned before so now we need to link it)
- for (Actor* child : parentActor->Children)
- {
- if (child->GetPrefabID() == msgData.PrefabId)
- {
- if (Objects.Contains(child->GetID()))
- {
- ScriptingObject* obj = FindPrefabObject(child, rootItem.PrefabObjectID);
- if (Objects.Contains(obj->GetID()))
- {
- // Other instance with already spawned network object
- obj = nullptr;
- }
- else
- {
- // Reuse already spawned object within a parent
- prefabInstance = child;
- break;
- }
- }
- }
- }
- }
- if (!prefabInstance)
- {
- // Spawn prefab
- auto prefab = (Prefab*)LoadAsset(msgData.PrefabId, Prefab::TypeInitializer);
- if (!prefab)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
- return;
- }
- prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
- if (!prefabInstance)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString());
- return;
- }
- }
-
- // Resolve objects from prefab instance
- objects.Resize(msgData.ItemsCount);
- for (int32 i = 0; i < msgData.ItemsCount; i++)
- {
- auto& msgDataItem = msgDataItems[i];
- ScriptingObject* obj = FindPrefabObject(prefabInstance, msgDataItem.PrefabObjectID);
- if (!obj)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", msgDataItem.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
- Delete(prefabInstance);
- return;
- }
- objects[i] = obj;
- }
- }
- else if (msgData.ItemsCount == 1)
- {
- // Spawn object
- const ScriptingTypeHandle objectType = Scripting::FindScriptingType(rootItem.ObjectTypeName);
- ScriptingObject* obj = ScriptingObject::NewObject(objectType);
- if (!obj)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(rootItem.ObjectTypeName));
- return;
- }
- objects.Add(obj);
+ // Allocate spawn message parts collecting
+ auto& parts = SpawnParts.AddOne();
+ parts.MsgData = msgData;
+ parts.Items.Resize(msgData.ItemsCount);
+ for (auto& item : parts.Items)
+ item.ObjectId = Guid::Empty; // Mark as not yet received
}
else
{
- // Spawn objects
- objects.Resize(msgData.ItemsCount);
- for (int32 i = 0; i < msgData.ItemsCount; i++)
- {
- auto& msgDataItem = msgDataItems[i];
- const ScriptingTypeHandle objectType = Scripting::FindScriptingType(msgDataItem.ObjectTypeName);
- ScriptingObject* obj = ScriptingObject::NewObject(objectType);
- if (!obj)
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(msgDataItem.ObjectTypeName));
- for (ScriptingObject* e : objects)
- Delete(e);
- return;
- }
- objects[i] = obj;
- if (i != 0)
- {
- // Link hierarchy of spawned objects before calling any networking code for them
- if (auto sceneObject = ScriptingObject::Cast(obj))
- {
- Actor* parent = nullptr;
- for (int32 j = 0; j < i; j++)
- {
- if (msgDataItems[j].ObjectId == msgDataItem.ParentId)
- {
- parent = ScriptingObject::Cast(objects[j]);
- break;
- }
- }
- if (parent)
- sceneObject->SetParent(parent);
- }
- }
- }
+ const auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem));
+ InvokeObjectSpawn(msgData, msgDataItems);
}
+}
- // Add all newly spawned objects
- for (int32 i = 0; i < msgData.ItemsCount; i++)
+void NetworkInternal::OnNetworkMessageObjectSpawnPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
+{
+ PROFILE_CPU();
+ NetworkMessageObjectSpawnPart msgData;
+ event.Message.ReadStructure(msgData);
+ int32 spawnPartsIndex;
+ for (spawnPartsIndex = 0; spawnPartsIndex < SpawnParts.Count(); spawnPartsIndex++)
{
- auto& msgDataItem = msgDataItems[i];
- ScriptingObject* obj = objects[i];
- if (!obj->IsRegistered())
- obj->RegisterObject();
- const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
-
- // Add object to the list
- NetworkReplicatedObject item;
- item.Object = obj;
- item.AsNetworkObject = ScriptingObject::ToInterface(obj);
- item.ObjectId = obj->GetID();
- item.ParentId = parent ? parent->ObjectId : Guid::Empty;
- item.OwnerClientId = msgData.OwnerClientId;
- item.Role = NetworkObjectRole::Replicated;
- if (item.OwnerClientId == NetworkManager::LocalClientId)
- {
- // Upgrade ownership automatically (eg. server spawned object that local client should own)
- item.Role = NetworkObjectRole::OwnedAuthoritative;
- }
- item.Spawned = true;
- NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->Object->GetType().ToString() : String::Empty);
- Objects.Add(MoveTemp(item));
- if (Hierarchy && item.Role == NetworkObjectRole::OwnedAuthoritative)
- Hierarchy->AddObject(obj);
-
- // Boost future lookups by using indirection
- NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgDataItem.ObjectId, item.ToString(), obj->GetType().ToString());
- IdsRemappingTable.Add(msgDataItem.ObjectId, item.ObjectId);
+ // Find spawn parts container that matches this spawn message (unique pair of sender and id assigned by sender)
+ const auto& e = SpawnParts.Get()[spawnPartsIndex];
+ if (e.MsgData.OwnerClientId == msgData.OwnerClientId && e.MsgData.OwnerSpawnId == msgData.OwnerSpawnId)
+ break;
}
-
- // Spawn all newly spawned objects (ensure to have valid ownership hierarchy set before spawning object)
- for (int32 i = 0; i < msgData.ItemsCount; i++)
+ if (spawnPartsIndex >= SpawnParts.Count())
{
- auto& msgDataItem = msgDataItems[i];
- ScriptingObject* obj = objects[i];
- auto it = Objects.Find(obj->GetID());
- auto& item = it->Item;
- const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
+ // Invalid part or data, ignore it
+ return;
+ }
+ auto& spawnParts = SpawnParts.Get()[spawnPartsIndex];
- // Automatic parenting for scene objects
- auto sceneObject = ScriptingObject::Cast(obj);
- if (sceneObject)
- {
- if (parent && parent->Object.Get() && parent->Object->Is())
- sceneObject->SetParent(parent->Object.As());
- else if (auto* parentActor = Scripting::TryFindObject(msgDataItem.ParentId))
- sceneObject->SetParent(parentActor);
- else if (msgDataItem.ParentId.IsValid())
- {
-#if USE_NETWORK_REPLICATOR_LOG
- // Ignore case when parent object in a message was a scene (eg. that is already unloaded on a client)
- AssetInfo assetInfo;
- if (!Content::GetAssetInfo(msgDataItem.ParentId, assetInfo) || assetInfo.TypeName != TEXT("FlaxEngine.SceneAsset"))
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
- }
-#endif
- }
- }
- else if (!parent && msgDataItem.ParentId.IsValid())
- {
- NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} as parent to spawned object", msgDataItem.ParentId.ToString());
- }
-
- if (item.AsNetworkObject)
- item.AsNetworkObject->OnNetworkSpawn();
+ // Read all items from this part
+ constexpr uint32 spawnItemMaxSize = sizeof(uint16) + sizeof(NetworkMessageObjectSpawnItem); // Index + Data
+ while (event.Message.Position + spawnItemMaxSize <= event.Message.BufferSize)
+ {
+ const uint16 itemIndex = event.Message.ReadUInt16();
+ event.Message.ReadStructure(spawnParts.Items[itemIndex]);
}
- // TODO: if we're server then spawn this object further on other clients (use TargetClientIds for that object - eg. object spawned by client on client for certain set of other clients only)
+ // Invoke spawning if we've got all items
+ for (auto& e : spawnParts.Items)
+ {
+ if (!e.ObjectId.IsValid())
+ return;
+ }
+ InvokeObjectSpawn(spawnParts.MsgData, spawnParts.Items.Get());
+ SpawnParts.RemoveAt(spawnPartsIndex);
}
void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
diff --git a/Source/Engine/Networking/NetworkReplicator.h b/Source/Engine/Networking/NetworkReplicator.h
index ecccc52cd..3806f8c9d 100644
--- a/Source/Engine/Networking/NetworkReplicator.h
+++ b/Source/Engine/Networking/NetworkReplicator.h
@@ -116,6 +116,13 @@ public:
/// The network object.
/// True if object exists in networking, otherwise false.
API_FUNCTION() static bool HasObject(const ScriptingObject* obj);
+
+ ///
+ /// Resolves foreign Guid into a local ScriptingObject
+ ///
+ /// The Guid of a foreign object.
+ /// Object if managed to resolve, otherwise null.
+ API_FUNCTION() static ScriptingObject* ResolveForeignObject(Guid objectId);
///
/// Gets the Client Id of the network object owner.
diff --git a/Source/Engine/Particles/Graph/ParticleEmitterGraph.h b/Source/Engine/Particles/Graph/ParticleEmitterGraph.h
index e184b0660..cfe259a72 100644
--- a/Source/Engine/Particles/Graph/ParticleEmitterGraph.h
+++ b/Source/Engine/Particles/Graph/ParticleEmitterGraph.h
@@ -8,7 +8,8 @@
#include "Engine/Particles/Types.h"
#include "Engine/Particles/ParticlesSimulation.h"
#include "Engine/Particles/ParticlesData.h"
-#include "Engine/Core/Types/CommonValue.h"
+#include "Engine/Core/Math/Vector4.h"
+#include "Engine/Core/Types/BaseTypes.h"
class ParticleEffect;
diff --git a/Source/Engine/Physics/Actors/WheeledVehicle.cpp b/Source/Engine/Physics/Actors/WheeledVehicle.cpp
index f5f729d39..286f5b24e 100644
--- a/Source/Engine/Physics/Actors/WheeledVehicle.cpp
+++ b/Source/Engine/Physics/Actors/WheeledVehicle.cpp
@@ -40,6 +40,31 @@ const Array& WheeledVehicle::GetWheels() const
void WheeledVehicle::SetWheels(const Array& value)
{
+#if WITH_VEHICLE
+ // Don't recreate whole vehicle when some wheel properties are only changed (eg. suspension)
+ if (_actor && _vehicle && _wheels.Count() == value.Count() && _wheelsData.Count() == value.Count())
+ {
+ bool softUpdate = true;
+ for (int32 wheelIndex = 0; wheelIndex < value.Count(); wheelIndex++)
+ {
+ auto& oldWheel = _wheels.Get()[wheelIndex];
+ auto& newWheel = value.Get()[wheelIndex];
+ if (oldWheel.Type != newWheel.Type ||
+ Math::NotNearEqual(oldWheel.SuspensionForceOffset, newWheel.SuspensionForceOffset) ||
+ oldWheel.Collider != newWheel.Collider)
+ {
+ softUpdate = false;
+ break;
+ }
+ }
+ if (softUpdate)
+ {
+ _wheels = value;
+ PhysicsBackend::UpdateVehicleWheels(this);
+ return;
+ }
+ }
+#endif
_wheels = value;
Setup();
}
@@ -51,6 +76,10 @@ WheeledVehicle::EngineSettings WheeledVehicle::GetEngine() const
void WheeledVehicle::SetEngine(const EngineSettings& value)
{
+#if WITH_VEHICLE
+ if (_vehicle)
+ PhysicsBackend::SetVehicleEngine(_vehicle, &value);
+#endif
_engine = value;
}
@@ -61,6 +90,10 @@ WheeledVehicle::DifferentialSettings WheeledVehicle::GetDifferential() const
void WheeledVehicle::SetDifferential(const DifferentialSettings& value)
{
+#if WITH_VEHICLE
+ if (_vehicle)
+ PhysicsBackend::SetVehicleDifferential(_vehicle, &value);
+#endif
_differential = value;
}
diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
index 69dfeaa27..d4429bfcb 100644
--- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
+++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp
@@ -29,6 +29,7 @@
#include
#include
#if WITH_VEHICLE
+#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Physics/Actors/WheeledVehicle.h"
#include
#include
@@ -1001,7 +1002,7 @@ void PhysicsBackend::EndSimulateScene(void* scene)
int32 wheelsCount = 0;
for (auto wheelVehicle : scenePhysX->WheelVehicles)
{
- if (!wheelVehicle->IsActiveInHierarchy())
+ if (!wheelVehicle->IsActiveInHierarchy() || !wheelVehicle->GetEnableSimulation())
continue;
auto drive = (PxVehicleWheels*)wheelVehicle->_vehicle;
ASSERT(drive);
@@ -1216,7 +1217,7 @@ void PhysicsBackend::EndSimulateScene(void* scene)
for (int32 i = 0, ii = 0; i < scenePhysX->WheelVehicles.Count(); i++)
{
auto wheelVehicle = scenePhysX->WheelVehicles[i];
- if (!wheelVehicle->IsActiveInHierarchy())
+ if (!wheelVehicle->IsActiveInHierarchy() || !wheelVehicle->GetEnableSimulation())
continue;
auto drive = (PxVehicleWheels*)scenePhysX->WheelVehicles[ii]->_vehicle;
auto& perVehicle = WheelVehiclesResultsPerVehicle[ii];
@@ -1237,7 +1238,7 @@ void PhysicsBackend::EndSimulateScene(void* scene)
for (int32 i = 0, ii = 0; i < scenePhysX->WheelVehicles.Count(); i++)
{
auto wheelVehicle = scenePhysX->WheelVehicles[i];
- if (!wheelVehicle->IsActiveInHierarchy())
+ if (!wheelVehicle->IsActiveInHierarchy() || !wheelVehicle->GetEnableSimulation())
continue;
auto drive = WheelVehiclesCache[ii];
auto& perVehicle = WheelVehiclesResultsPerVehicle[ii];
@@ -2630,8 +2631,20 @@ int32 PhysicsBackend::MoveController(void* controller, void* shape, const Vector
#if WITH_VEHICLE
+bool SortWheels(WheeledVehicle::Wheel const& a, WheeledVehicle::Wheel const& b)
+{
+ return (int32)a.Type < (int32)b.Type;
+}
+
void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
{
+ // TODO: handle PxVehicleDrive4WWheelOrder internally rather than sorting wheels directly on the vehicle
+ if (actor->_driveType == WheeledVehicle::DriveTypes::Drive4W)
+ {
+ // Drive4W requires wheels to match order from PxVehicleDrive4WWheelOrder enum
+ Sorting::QuickSort(actor->_wheels.Get(), actor->_wheels.Count(), SortWheels);
+ }
+
// Get wheels
Array> wheels;
for (auto& wheel : actor->_wheels)
@@ -2676,10 +2689,7 @@ void* PhysicsBackend::CreateVehicle(WheeledVehicle* actor)
// Initialize wheels simulation data
PxVec3 offsets[PX_MAX_NB_WHEELS];
for (int32 i = 0; i < wheels.Count(); i++)
- {
- auto& wheel = *wheels[i];
- offsets[i] = C2P(wheel.Collider->GetLocalPosition());
- }
+ offsets[i] = C2P(wheels[i]->Collider->GetLocalPosition());
PxF32 sprungMasses[PX_MAX_NB_WHEELS];
const float mass = actorPhysX->getMass();
// TODO: get gravityDirection from scenePhysX->Scene->getGravity()
@@ -2923,12 +2933,163 @@ void PhysicsBackend::DestroyVehicle(void* vehicle, int32 driveType)
}
}
+void PhysicsBackend::UpdateVehicleWheels(WheeledVehicle* actor)
+{
+ auto drive = (PxVehicleWheels*)actor->_vehicle;
+ PxVehicleWheelsSimData* wheelsSimData = &drive->mWheelsSimData;
+ for (uint32 i = 0; i < wheelsSimData->getNbWheels(); i++)
+ {
+ auto& wheel = actor->_wheels[i];
+
+ // Update suspension data
+ PxVehicleSuspensionData suspensionData = wheelsSimData->getSuspensionData(i);
+ const float suspensionFrequency = 7.0f;
+ suspensionData.mMaxCompression = wheel.SuspensionMaxRaise;
+ suspensionData.mMaxDroop = wheel.SuspensionMaxDrop;
+ suspensionData.mSpringStrength = Math::Square(suspensionFrequency) * suspensionData.mSprungMass;
+ suspensionData.mSpringDamperRate = wheel.SuspensionDampingRate * 2.0f * Math::Sqrt(suspensionData.mSpringStrength * suspensionData.mSprungMass);
+ wheelsSimData->setSuspensionData(i, suspensionData);
+
+ // Update tire data
+ PxVehicleTireData tire;
+ int32 tireIndex = WheelTireTypes.Find(wheel.TireFrictionScale);
+ if (tireIndex == -1)
+ {
+ // New tire type
+ tireIndex = WheelTireTypes.Count();
+ WheelTireTypes.Add(wheel.TireFrictionScale);
+ WheelTireFrictionsDirty = true;
+ }
+ tire.mType = tireIndex;
+ tire.mLatStiffX = wheel.TireLateralMax;
+ tire.mLatStiffY = wheel.TireLateralStiffness;
+ tire.mLongitudinalStiffnessPerUnitGravity = wheel.TireLongitudinalStiffness;
+ wheelsSimData->setTireData(i, tire);
+
+ // Update wheel data
+ PxVehicleWheelData wheelData;
+ wheelData.mMass = wheel.Mass;
+ wheelData.mRadius = wheel.Radius;
+ wheelData.mWidth = wheel.Width;
+ wheelData.mMOI = 0.5f * wheelData.mMass * Math::Square(wheelData.mRadius);
+ wheelData.mDampingRate = M2ToCm2(wheel.DampingRate);
+ wheelData.mMaxSteer = wheel.MaxSteerAngle * DegreesToRadians;
+ wheelData.mMaxBrakeTorque = M2ToCm2(wheel.MaxBrakeTorque);
+ wheelData.mMaxHandBrakeTorque = M2ToCm2(wheel.MaxHandBrakeTorque);
+ wheelsSimData->setWheelData(i, wheelData);
+ }
+}
+
+void PhysicsBackend::SetVehicleEngine(void* vehicle, const void* value)
+{
+ auto drive = (PxVehicleDrive*)vehicle;
+ auto& engine = *(const WheeledVehicle::EngineSettings*)value;
+ switch (drive->getVehicleType())
+ {
+ case PxVehicleTypes::eDRIVE4W:
+ {
+ auto drive4W = (PxVehicleDrive4W*)drive;
+ PxVehicleDriveSimData4W& driveSimData = drive4W->mDriveSimData;
+ PxVehicleEngineData engineData;
+ engineData.mMOI = M2ToCm2(engine.MOI);
+ engineData.mPeakTorque = M2ToCm2(engine.MaxTorque);
+ engineData.mMaxOmega = RpmToRadPerS(engine.MaxRotationSpeed);
+ engineData.mDampingRateFullThrottle = M2ToCm2(0.15f);
+ engineData.mDampingRateZeroThrottleClutchEngaged = M2ToCm2(2.0f);
+ engineData.mDampingRateZeroThrottleClutchDisengaged = M2ToCm2(0.35f);
+ driveSimData.setEngineData(engineData);
+ break;
+ }
+ case PxVehicleTypes::eDRIVENW:
+ {
+ auto drive4W = (PxVehicleDriveNW*)drive;
+ PxVehicleDriveSimDataNW& driveSimData = drive4W->mDriveSimData;
+ PxVehicleEngineData engineData;
+ engineData.mMOI = M2ToCm2(engine.MOI);
+ engineData.mPeakTorque = M2ToCm2(engine.MaxTorque);
+ engineData.mMaxOmega = RpmToRadPerS(engine.MaxRotationSpeed);
+ engineData.mDampingRateFullThrottle = M2ToCm2(0.15f);
+ engineData.mDampingRateZeroThrottleClutchEngaged = M2ToCm2(2.0f);
+ engineData.mDampingRateZeroThrottleClutchDisengaged = M2ToCm2(0.35f);
+ driveSimData.setEngineData(engineData);
+ break;
+ }
+ }
+}
+
+void PhysicsBackend::SetVehicleDifferential(void* vehicle, const void* value)
+{
+ auto drive = (PxVehicleDrive*)vehicle;
+ auto& differential = *(const WheeledVehicle::DifferentialSettings*)value;
+ switch (drive->getVehicleType())
+ {
+ case PxVehicleTypes::eDRIVE4W:
+ {
+ auto drive4W = (PxVehicleDrive4W*)drive;
+ PxVehicleDriveSimData4W& driveSimData = drive4W->mDriveSimData;
+ PxVehicleDifferential4WData differential4WData;
+ differential4WData.mType = (PxVehicleDifferential4WData::Enum)differential.Type;
+ differential4WData.mFrontRearSplit = differential.FrontRearSplit;
+ differential4WData.mFrontLeftRightSplit = differential.FrontLeftRightSplit;
+ differential4WData.mRearLeftRightSplit = differential.RearLeftRightSplit;
+ differential4WData.mCentreBias = differential.CentreBias;
+ differential4WData.mFrontBias = differential.FrontBias;
+ differential4WData.mRearBias = differential.RearBias;
+ driveSimData.setDiffData(differential4WData);
+ break;
+ }
+ }
+}
+
void PhysicsBackend::SetVehicleGearbox(void* vehicle, const void* value)
{
auto drive = (PxVehicleDrive*)vehicle;
auto& gearbox = *(const WheeledVehicle::GearboxSettings*)value;
drive->mDriveDynData.setUseAutoGears(gearbox.AutoGear);
drive->mDriveDynData.setAutoBoxSwitchTime(Math::Max(gearbox.SwitchTime, 0.0f));
+ switch (drive->getVehicleType())
+ {
+ case PxVehicleTypes::eDRIVE4W:
+ {
+ auto drive4W = (PxVehicleDrive4W*)drive;
+ PxVehicleDriveSimData4W& driveSimData = drive4W->mDriveSimData;
+
+ // Gears
+ PxVehicleGearsData gears;
+ gears.mSwitchTime = Math::Max(gearbox.SwitchTime, 0.0f);
+ driveSimData.setGearsData(gears);
+
+ // Auto Box
+ PxVehicleAutoBoxData autoBox;
+ driveSimData.setAutoBoxData(autoBox);
+
+ // Clutch
+ PxVehicleClutchData clutch;
+ clutch.mStrength = M2ToCm2(gearbox.ClutchStrength);
+ driveSimData.setClutchData(clutch);
+ break;
+ }
+ case PxVehicleTypes::eDRIVENW:
+ {
+ auto drive4W = (PxVehicleDriveNW*)drive;
+ PxVehicleDriveSimDataNW& driveSimData = drive4W->mDriveSimData;
+
+ // Gears
+ PxVehicleGearsData gears;
+ gears.mSwitchTime = Math::Max(gearbox.SwitchTime, 0.0f);
+ driveSimData.setGearsData(gears);
+
+ // Auto Box
+ PxVehicleAutoBoxData autoBox;
+ driveSimData.setAutoBoxData(autoBox);
+
+ // Clutch
+ PxVehicleClutchData clutch;
+ clutch.mStrength = M2ToCm2(gearbox.ClutchStrength);
+ driveSimData.setClutchData(clutch);
+ break;
+ }
+ }
}
int32 PhysicsBackend::GetVehicleTargetGear(void* vehicle)
diff --git a/Source/Engine/Physics/PhysicsBackend.h b/Source/Engine/Physics/PhysicsBackend.h
index f72c7f679..6c761a776 100644
--- a/Source/Engine/Physics/PhysicsBackend.h
+++ b/Source/Engine/Physics/PhysicsBackend.h
@@ -245,6 +245,9 @@ public:
// Vehicles
static void* CreateVehicle(class WheeledVehicle* actor);
static void DestroyVehicle(void* vehicle, int32 driveType);
+ static void UpdateVehicleWheels(WheeledVehicle* actor);
+ static void SetVehicleEngine(void* vehicle, const void* value);
+ static void SetVehicleDifferential(void* vehicle, const void* value);
static void SetVehicleGearbox(void* vehicle, const void* value);
static int32 GetVehicleTargetGear(void* vehicle);
static void SetVehicleTargetGear(void* vehicle, int32 value);
diff --git a/Source/Engine/Physics/PhysicsBackendEmpty.cpp b/Source/Engine/Physics/PhysicsBackendEmpty.cpp
index 516562e28..1a418af0c 100644
--- a/Source/Engine/Physics/PhysicsBackendEmpty.cpp
+++ b/Source/Engine/Physics/PhysicsBackendEmpty.cpp
@@ -732,6 +732,18 @@ void PhysicsBackend::DestroyVehicle(void* vehicle, int32 driveType)
{
}
+void PhysicsBackend::UpdateVehicleWheels(WheeledVehicle* actor)
+{
+}
+
+void PhysicsBackend::SetVehicleEngine(void* vehicle, const void* value)
+{
+}
+
+void PhysicsBackend::SetVehicleDifferential(void* vehicle, const void* value)
+{
+}
+
void PhysicsBackend::SetVehicleGearbox(void* vehicle, const void* value)
{
}
diff --git a/Source/Engine/Platform/Base/WindowBase.h b/Source/Engine/Platform/Base/WindowBase.h
index 4220f1054..e4e7d2080 100644
--- a/Source/Engine/Platform/Base/WindowBase.h
+++ b/Source/Engine/Platform/Base/WindowBase.h
@@ -445,6 +445,15 @@ public:
{
}
+ ///
+ /// Sets the window to be borderless or not and to be fullscreen.
+ ///
+ /// Whether or not to have borders on window.
+ /// Whether or not to make the borderless window fullscreen (maximize to cover whole screen).
+ API_FUNCTION() virtual void SetBorderless(bool isBorderless, bool maximized = false)
+ {
+ }
+
///
/// Restores the window state before minimizing or maximizing.
///
diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp
index 0434d8414..ab314ab72 100644
--- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp
+++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp
@@ -2191,6 +2191,7 @@ bool LinuxPlatform::Init()
// Initialize "X11 keycode" -> "Flax KeyboardKeys" map
KeyCodeMap.Resize(desc->max_key_code + 1);
+ Platform::MemoryClear(KeyCodeMap.Get(), KeyCodeMap.Count() * sizeof(KeyboardKeys));
XkbFreeNames(desc, XkbKeyNamesMask, 1);
X11::XkbFreeKeyboard(desc, 0, 1);
for (int32 keyIdx = (int32)KeyboardKeys::None; keyIdx < MAX_uint8; keyIdx++)
diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp
index aeee93e37..000accbfa 100644
--- a/Source/Engine/Platform/Mac/MacWindow.cpp
+++ b/Source/Engine/Platform/Mac/MacWindow.cpp
@@ -202,8 +202,7 @@ void GetDragDropData(const MacWindow* window, id sender, Float2&
{
NSRect frame = [(NSWindow*)window->GetNativePtr() frame];
NSPoint point = [sender draggingLocation];
- Float2 titleSize = GetWindowTitleSize(window);
- mousePos = Float2(point.x, frame.size.height - point.y) - titleSize;
+ mousePos = Float2(point.x, frame.size.height - point.y) * MacPlatform::ScreenScale - GetWindowTitleSize(window);
NSPasteboard* pasteboard = [sender draggingPasteboard];
if ([[pasteboard types] containsObject:NSPasteboardTypeString])
{
diff --git a/Source/Engine/Platform/Windows/WindowsFileSystem.cpp b/Source/Engine/Platform/Windows/WindowsFileSystem.cpp
index 83f042730..38384f05f 100644
--- a/Source/Engine/Platform/Windows/WindowsFileSystem.cpp
+++ b/Source/Engine/Platform/Windows/WindowsFileSystem.cpp
@@ -316,7 +316,8 @@ bool WindowsFileSystem::ShowBrowseFolderDialog(Window* parentWindow, const Strin
if (SUCCEEDED(SHCreateItemFromParsingName(initialDirectory.Get(), NULL, IID_PPV_ARGS(&defaultFolder))))
fd->SetFolder(defaultFolder);
- if (SUCCEEDED(fd->Show(parentWindow->GetHWND())))
+ HWND hwndOwner = parentWindow ? parentWindow->GetHWND() : NULL;
+ if (SUCCEEDED(fd->Show(hwndOwner)))
{
ComPtr si;
if (SUCCEEDED(fd->GetResult(&si)))
diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp
index 687eeed28..d712102fa 100644
--- a/Source/Engine/Platform/Windows/WindowsWindow.cpp
+++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp
@@ -260,6 +260,75 @@ void WindowsWindow::Maximize()
_isDuringMaximize = false;
}
+void WindowsWindow::SetBorderless(bool isBorderless, bool maximized)
+{
+ ASSERT(HasHWND());
+
+ if (IsFullscreen())
+ SetIsFullscreen(false);
+
+ // Fixes issue of borderless window not going full screen
+ if (IsMaximized())
+ Restore();
+
+ _settings.HasBorder = !isBorderless;
+
+ BringToFront();
+
+ if (isBorderless)
+ {
+ LONG lStyle = GetWindowLong(_handle, GWL_STYLE);
+ lStyle &= ~(WS_THICKFRAME | WS_SYSMENU | WS_OVERLAPPED | WS_BORDER | WS_CAPTION);
+ lStyle |= WS_POPUP;
+ lStyle |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
+#if WINDOWS_USE_NEW_BORDER_LESS
+ if (_settings.IsRegularWindow)
+ style |= WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_SYSMENU | WS_THICKFRAME | WS_GROUP;
+#elif WINDOWS_USE_NEWER_BORDER_LESS
+ if (_settings.IsRegularWindow)
+ lStyle |= WS_THICKFRAME | WS_SYSMENU;
+#endif
+
+ SetWindowLong(_handle, GWL_STYLE, lStyle);
+ SetWindowPos(_handle, HWND_TOP, 0, 0,0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
+
+ if (maximized)
+ {
+ ShowWindow(_handle, SW_SHOWMAXIMIZED);
+ }
+ else
+ {
+ ShowWindow(_handle, SW_SHOW);
+ }
+ }
+ else
+ {
+ LONG lStyle = GetWindowLong(_handle, GWL_STYLE);
+ lStyle &= ~(WS_POPUP | WS_CLIPCHILDREN | WS_CLIPSIBLINGS);
+ if (_settings.AllowMaximize)
+ lStyle |= WS_MAXIMIZEBOX;
+ if (_settings.AllowMinimize)
+ lStyle |= WS_MINIMIZEBOX;
+ if (_settings.HasSizingFrame)
+ lStyle |= WS_THICKFRAME;
+ lStyle |= WS_OVERLAPPED | WS_SYSMENU | WS_BORDER | WS_CAPTION;
+
+ SetWindowLong(_handle, GWL_STYLE, lStyle);
+ SetWindowPos(_handle, nullptr, 0, 0, (int)_settings.Size.X, (int)_settings.Size.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
+
+ if (maximized)
+ {
+ Maximize();
+ }
+ else
+ {
+ ShowWindow(_handle, SW_SHOW);
+ }
+ }
+
+ CheckForWindowResize();
+}
+
void WindowsWindow::Restore()
{
ASSERT(HasHWND());
diff --git a/Source/Engine/Platform/Windows/WindowsWindow.h b/Source/Engine/Platform/Windows/WindowsWindow.h
index 74e923f86..e15f86a45 100644
--- a/Source/Engine/Platform/Windows/WindowsWindow.h
+++ b/Source/Engine/Platform/Windows/WindowsWindow.h
@@ -100,6 +100,7 @@ public:
void Hide() override;
void Minimize() override;
void Maximize() override;
+ void SetBorderless(bool isBorderless, bool maximized = false) override;
void Restore() override;
bool IsClosed() const override;
bool IsForegroundWindow() const override;
diff --git a/Source/Engine/Scripting/Internal/ManagedDictionary.cpp b/Source/Engine/Scripting/Internal/ManagedDictionary.cpp
new file mode 100644
index 000000000..7cb4d1cfb
--- /dev/null
+++ b/Source/Engine/Scripting/Internal/ManagedDictionary.cpp
@@ -0,0 +1,14 @@
+#include "ManagedDictionary.h"
+
+Dictionary ManagedDictionary::CachedDictionaryTypes;
+#if !USE_MONO_AOT
+ManagedDictionary::MakeGenericTypeThunk ManagedDictionary::MakeGenericType;
+ManagedDictionary::CreateInstanceThunk ManagedDictionary::CreateInstance;
+ManagedDictionary::AddDictionaryItemThunk ManagedDictionary::AddDictionaryItem;
+ManagedDictionary::GetDictionaryKeysThunk ManagedDictionary::GetDictionaryKeys;
+#else
+MMethod* ManagedDictionary::MakeGenericType;
+MMethod* ManagedDictionary::CreateInstance;
+MMethod* ManagedDictionary::AddDictionaryItem;
+MMethod* ManagedDictionary::GetDictionaryKeys;
+#endif
diff --git a/Source/Engine/Scripting/Internal/ManagedDictionary.h b/Source/Engine/Scripting/Internal/ManagedDictionary.h
index c42eb1d27..f957dfcec 100644
--- a/Source/Engine/Scripting/Internal/ManagedDictionary.h
+++ b/Source/Engine/Scripting/Internal/ManagedDictionary.h
@@ -12,17 +12,96 @@
#include "Engine/Scripting/ManagedCLR/MAssembly.h"
#include "Engine/Scripting/ManagedCLR/MException.h"
#include "Engine/Scripting/Internal/StdTypesContainer.h"
+#include "Engine/Core/Collections/Dictionary.h"
///
/// Utility interop between C++ and C# for Dictionary collection.
///
struct FLAXENGINE_API ManagedDictionary
{
+public:
+ struct KeyValueType
+ {
+ MType* keyType;
+ MType* valueType;
+
+ bool operator==(const KeyValueType& other) const
+ {
+ return keyType == other.keyType && valueType == other.valueType;
+ }
+ };
+
+private:
+ static Dictionary CachedDictionaryTypes;
+
+#if !USE_MONO_AOT
+ typedef MTypeObject* (*MakeGenericTypeThunk)(MObject* instance, MTypeObject* genericType, MArray* genericArgs, MObject** exception);
+ static MakeGenericTypeThunk MakeGenericType;
+
+ typedef MObject* (*CreateInstanceThunk)(MObject* instance, MTypeObject* type, void* arr, MObject** exception);
+ static CreateInstanceThunk CreateInstance;
+
+ typedef void (*AddDictionaryItemThunk)(MObject* instance, MObject* dictionary, MObject* key, MObject* value, MObject** exception);
+ static AddDictionaryItemThunk AddDictionaryItem;
+
+ typedef MArray* (*GetDictionaryKeysThunk)(MObject* instance, MObject* dictionary, MObject** exception);
+ static GetDictionaryKeysThunk GetDictionaryKeys;
+#else
+ static MMethod* MakeGenericType;
+ static MMethod* CreateInstance;
+ static MMethod* AddDictionaryItem;
+ static MMethod* GetDictionaryKeys;
+#endif
+
+public:
MObject* Instance;
ManagedDictionary(MObject* instance = nullptr)
{
Instance = instance;
+
+#if !USE_MONO_AOT
+ // Cache the thunks of the dictionary helper methods
+ if (MakeGenericType == nullptr)
+ {
+ MClass* scriptingClass = Scripting::GetStaticClass();
+ CHECK(scriptingClass);
+
+ MMethod* makeGenericTypeMethod = scriptingClass->GetMethod("MakeGenericType", 2);
+ CHECK(makeGenericTypeMethod);
+ MakeGenericType = (MakeGenericTypeThunk)makeGenericTypeMethod->GetThunk();
+
+ MMethod* createInstanceMethod = StdTypesContainer::Instance()->ActivatorClass->GetMethod("CreateInstance", 2);
+ CHECK(createInstanceMethod);
+ CreateInstance = (CreateInstanceThunk)createInstanceMethod->GetThunk();
+
+ MMethod* addDictionaryItemMethod = scriptingClass->GetMethod("AddDictionaryItem", 3);
+ CHECK(addDictionaryItemMethod);
+ AddDictionaryItem = (AddDictionaryItemThunk)addDictionaryItemMethod->GetThunk();
+
+ MMethod* getDictionaryKeysItemMethod = scriptingClass->GetMethod("GetDictionaryKeys", 1);
+ CHECK(getDictionaryKeysItemMethod);
+ GetDictionaryKeys = (GetDictionaryKeysThunk)getDictionaryKeysItemMethod->GetThunk();
+ }
+#else
+ if (MakeGenericType == nullptr)
+ {
+ MClass* scriptingClass = Scripting::GetStaticClass();
+ CHECK(scriptingClass);
+
+ MakeGenericType = scriptingClass->GetMethod("MakeGenericType", 2);
+ CHECK(MakeGenericType);
+
+ CreateInstance = StdTypesContainer::Instance()->ActivatorClass->GetMethod("CreateInstance", 2);
+ CHECK(CreateInstance);
+
+ AddDictionaryItem = scriptingClass->GetMethod("AddDictionaryItem", 3);
+ CHECK(AddDictionaryItem);
+
+ GetDictionaryKeys = scriptingClass->GetMethod("GetDictionaryKeys", 1);
+ CHECK(GetDictionaryKeys);
+ }
+#endif
}
template
@@ -76,10 +155,11 @@ struct FLAXENGINE_API ManagedDictionary
static MTypeObject* GetClass(MType* keyType, MType* valueType)
{
- MClass* scriptingClass = Scripting::GetStaticClass();
- CHECK_RETURN(scriptingClass, nullptr);
- MMethod* makeGenericMethod = scriptingClass->GetMethod("MakeGenericType", 2);
- CHECK_RETURN(makeGenericMethod, nullptr);
+ // Check if the generic type was generated earlier
+ KeyValueType cacheKey = { keyType, valueType };
+ MTypeObject* dictionaryType;
+ if (CachedDictionaryTypes.TryGet(cacheKey, dictionaryType))
+ return dictionaryType;
MTypeObject* genericType = MUtils::GetType(StdTypesContainer::Instance()->DictionaryClass);
#if USE_NETCORE
@@ -91,18 +171,23 @@ struct FLAXENGINE_API ManagedDictionary
genericArgsPtr[0] = INTERNAL_TYPE_GET_OBJECT(keyType);
genericArgsPtr[1] = INTERNAL_TYPE_GET_OBJECT(valueType);
+ MObject* exception = nullptr;
+#if !USE_MONO_AOT
+ dictionaryType = MakeGenericType(nullptr, genericType, genericArgs, &exception);
+#else
void* params[2];
params[0] = genericType;
params[1] = genericArgs;
- MObject* exception = nullptr;
- MObject* dictionaryType = makeGenericMethod->Invoke(nullptr, params, &exception);
+ dictionaryType = (MTypeObject*)MakeGenericType->Invoke(nullptr, params, &exception);
+#endif
if (exception)
{
MException ex(exception);
ex.Log(LogType::Error, TEXT(""));
return nullptr;
}
- return (MTypeObject*)dictionaryType;
+ CachedDictionaryTypes.Add(cacheKey, dictionaryType);
+ return dictionaryType;
}
static ManagedDictionary New(MType* keyType, MType* valueType)
@@ -112,16 +197,15 @@ struct FLAXENGINE_API ManagedDictionary
if (!dictionaryType)
return result;
- MClass* scriptingClass = Scripting::GetStaticClass();
- CHECK_RETURN(scriptingClass, result);
- MMethod* createMethod = StdTypesContainer::Instance()->ActivatorClass->GetMethod("CreateInstance", 2);
- CHECK_RETURN(createMethod, result);
-
MObject* exception = nullptr;
+#if !USE_MONO_AOT
+ MObject* instance = CreateInstance(nullptr, dictionaryType, nullptr, &exception);
+#else
void* params[2];
params[0] = dictionaryType;
params[1] = nullptr;
- MObject* instance = createMethod->Invoke(nullptr, params, &exception);
+ MObject* instance = CreateInstance->Invoke(nullptr, params, &exception);
+#endif
if (exception)
{
MException ex(exception);
@@ -136,16 +220,17 @@ struct FLAXENGINE_API ManagedDictionary
void Add(MObject* key, MObject* value)
{
CHECK(Instance);
- MClass* scriptingClass = Scripting::GetStaticClass();
- CHECK(scriptingClass);
- MMethod* addDictionaryItemMethod = scriptingClass->GetMethod("AddDictionaryItem", 3);
- CHECK(addDictionaryItemMethod);
+
+ MObject* exception = nullptr;
+#if !USE_MONO_AOT
+ AddDictionaryItem(nullptr, Instance, key, value, &exception);
+#else
void* params[3];
params[0] = Instance;
params[1] = key;
params[2] = value;
- MObject* exception = nullptr;
- addDictionaryItemMethod->Invoke(Instance, params, &exception);
+ AddDictionaryItem->Invoke(Instance, params, &exception);
+#endif
if (exception)
{
MException ex(exception);
@@ -156,13 +241,13 @@ struct FLAXENGINE_API ManagedDictionary
MArray* GetKeys() const
{
CHECK_RETURN(Instance, nullptr);
- MClass* scriptingClass = Scripting::GetStaticClass();
- CHECK_RETURN(scriptingClass, nullptr);
- MMethod* getDictionaryKeysMethod = scriptingClass->GetMethod("GetDictionaryKeys", 1);
- CHECK_RETURN(getDictionaryKeysMethod, nullptr);
+#if !USE_MONO_AOT
+ return GetDictionaryKeys(nullptr, Instance, nullptr);
+#else
void* params[1];
params[0] = Instance;
- return (MArray*)getDictionaryKeysMethod->Invoke( nullptr, params, nullptr);
+ return (MArray*)GetDictionaryKeys->Invoke(nullptr, params, nullptr);
+#endif
}
MObject* GetValue(MObject* key) const
@@ -177,4 +262,11 @@ struct FLAXENGINE_API ManagedDictionary
}
};
+inline uint32 GetHash(const ManagedDictionary::KeyValueType& other)
+{
+ uint32 hash = ::GetHash((void*)other.keyType);
+ CombineHash(hash, ::GetHash((void*)other.valueType));
+ return hash;
+}
+
#endif
diff --git a/Source/Engine/Scripting/Object.cs b/Source/Engine/Scripting/Object.cs
index 0180c0935..170367b7e 100644
--- a/Source/Engine/Scripting/Object.cs
+++ b/Source/Engine/Scripting/Object.cs
@@ -179,6 +179,18 @@ namespace FlaxEngine
Internal_Destroy(GetUnmanagedPtr(obj), timeLeft);
}
+ ///
+ /// Destroys the specified object and clears the reference variable.
+ /// The object obj will be destroyed immediately.
+ /// If obj is a Script it will be removed from the Actor and deleted.
+ /// If obj is an Actor it will be removed from the Scene and deleted as well as all its Scripts and all children of the Actor.
+ ///
+ /// The object to destroy.
+ public static void DestroyNow(Object obj)
+ {
+ Internal_DestroyNow(GetUnmanagedPtr(obj));
+ }
+
///
/// Destroys the specified object and clears the reference variable.
/// The object obj will be destroyed now or after the time specified in seconds from now.
@@ -316,6 +328,9 @@ namespace FlaxEngine
[LibraryImport("FlaxEngine", EntryPoint = "ObjectInternal_Destroy", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(Interop.StringMarshaller))]
internal static partial void Internal_Destroy(IntPtr obj, float timeLeft);
+ [LibraryImport("FlaxEngine", EntryPoint = "ObjectInternal_DestroyNow", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(Interop.StringMarshaller))]
+ internal static partial void Internal_DestroyNow(IntPtr obj);
+
[LibraryImport("FlaxEngine", EntryPoint = "ObjectInternal_GetTypeName", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(Interop.StringMarshaller))]
internal static partial string Internal_GetTypeName(IntPtr obj);
diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp
index 36f1f16af..f4ae5b1ec 100644
--- a/Source/Engine/Scripting/Runtime/DotNet.cpp
+++ b/Source/Engine/Scripting/Runtime/DotNet.cpp
@@ -878,7 +878,7 @@ MMethod* MClass::GetMethod(const char* name, int32 numParams) const
GetMethods();
for (int32 i = 0; i < _methods.Count(); i++)
{
- if (_methods[i]->GetName() == name && _methods[i]->GetParametersCount() == numParams)
+ if (_methods[i]->GetParametersCount() == numParams && _methods[i]->GetName() == name)
return _methods[i];
}
return nullptr;
diff --git a/Source/Engine/Scripting/ScriptingObject.cpp b/Source/Engine/Scripting/ScriptingObject.cpp
index 9ece58f2a..12144385d 100644
--- a/Source/Engine/Scripting/ScriptingObject.cpp
+++ b/Source/Engine/Scripting/ScriptingObject.cpp
@@ -681,6 +681,12 @@ DEFINE_INTERNAL_CALL(void) ObjectInternal_Destroy(ScriptingObject* obj, float ti
obj->DeleteObject(timeLeft, useGameTime);
}
+DEFINE_INTERNAL_CALL(void) ObjectInternal_DestroyNow(ScriptingObject* obj)
+{
+ if (obj)
+ obj->DeleteObjectNow();
+}
+
DEFINE_INTERNAL_CALL(MString*) ObjectInternal_GetTypeName(ScriptingObject* obj)
{
INTERNAL_CALL_CHECK_RETURN(obj, nullptr);
@@ -777,6 +783,7 @@ public:
ADD_INTERNAL_CALL("FlaxEngine.Object::Internal_ManagedInstanceCreated", &ObjectInternal_ManagedInstanceCreated);
ADD_INTERNAL_CALL("FlaxEngine.Object::Internal_ManagedInstanceDeleted", &ObjectInternal_ManagedInstanceDeleted);
ADD_INTERNAL_CALL("FlaxEngine.Object::Internal_Destroy", &ObjectInternal_Destroy);
+ ADD_INTERNAL_CALL("FlaxEngine.Object::Internal_DestroyNow", &ObjectInternal_DestroyNow);
ADD_INTERNAL_CALL("FlaxEngine.Object::Internal_GetTypeName", &ObjectInternal_GetTypeName);
ADD_INTERNAL_CALL("FlaxEngine.Object::Internal_FindObject", &ObjectInternal_FindObject);
ADD_INTERNAL_CALL("FlaxEngine.Object::Internal_TryFindObject", &ObjectInternal_TryFindObject);
diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp
index d98827ce1..92a8c09d3 100644
--- a/Source/Engine/Terrain/TerrainPatch.cpp
+++ b/Source/Engine/Terrain/TerrainPatch.cpp
@@ -1125,42 +1125,67 @@ bool TerrainPatch::InitializeHeightMap()
float* TerrainPatch::GetHeightmapData()
{
+ PROFILE_CPU_NAMED("Terrain.GetHeightmapData");
+
if (_cachedHeightMap.HasItems())
return _cachedHeightMap.Get();
- PROFILE_CPU_NAMED("Terrain.GetHeightmapData");
-
CacheHeightData();
return _cachedHeightMap.Get();
}
+void TerrainPatch::ClearHeightmapCache()
+{
+ PROFILE_CPU_NAMED("Terrain.ClearHeightmapCache");
+ _cachedHeightMap.Clear();
+}
+
byte* TerrainPatch::GetHolesMaskData()
{
+ PROFILE_CPU_NAMED("Terrain.GetHolesMaskData");
+
if (_cachedHolesMask.HasItems())
return _cachedHolesMask.Get();
- PROFILE_CPU_NAMED("Terrain.GetHolesMaskData");
-
CacheHeightData();
return _cachedHolesMask.Get();
}
+void TerrainPatch::ClearHolesMaskCache()
+{
+ PROFILE_CPU_NAMED("Terrain.ClearHolesMaskCache");
+ _cachedHolesMask.Clear();
+}
+
Color32* TerrainPatch::GetSplatMapData(int32 index)
{
ASSERT(index >= 0 && index < TERRAIN_MAX_SPLATMAPS_COUNT);
+ PROFILE_CPU_NAMED("Terrain.GetSplatMapData");
+
if (_cachedSplatMap[index].HasItems())
return _cachedSplatMap[index].Get();
- PROFILE_CPU_NAMED("Terrain.GetSplatMapData");
-
CacheSplatData();
return _cachedSplatMap[index].Get();
}
+void TerrainPatch::ClearSplatMapCache()
+{
+ PROFILE_CPU_NAMED("Terrain.ClearSplatMapCache");
+ _cachedSplatMap->Clear();
+}
+
+void TerrainPatch::ClearCache()
+{
+ ClearHeightmapCache();
+ ClearHolesMaskCache();
+ ClearSplatMapCache();
+}
+
void TerrainPatch::CacheHeightData()
{
PROFILE_CPU_NAMED("Terrain.CacheHeightData");
diff --git a/Source/Engine/Terrain/TerrainPatch.h b/Source/Engine/Terrain/TerrainPatch.h
index 44a768f90..689c629c5 100644
--- a/Source/Engine/Terrain/TerrainPatch.h
+++ b/Source/Engine/Terrain/TerrainPatch.h
@@ -229,12 +229,22 @@ public:
/// The heightmap data.
float* GetHeightmapData();
+ ///
+ /// Clears cache of the heightmap data.
+ ///
+ void ClearHeightmapCache();
+
///
/// Gets the raw pointer to the holes mask data.
///
/// The holes mask data.
byte* GetHolesMaskData();
+ ///
+ /// Clears cache of the holes mask data.
+ ///
+ void ClearHolesMaskCache();
+
///
/// Gets the raw pointer to the splat map data.
///
@@ -242,6 +252,16 @@ public:
/// The splat map data.
Color32* GetSplatMapData(int32 index);
+ ///
+ /// Clears cache of the splat map data.
+ ///
+ void ClearSplatMapCache();
+
+ ///
+ /// Clears all caches.
+ ///
+ void ClearCache();
+
///
/// Modifies the terrain patch heightmap with the given samples.
///
diff --git a/Source/Engine/UI/GUI/Common/Button.cs b/Source/Engine/UI/GUI/Common/Button.cs
index 8ab6366eb..88ee0437e 100644
--- a/Source/Engine/UI/GUI/Common/Button.cs
+++ b/Source/Engine/UI/GUI/Common/Button.cs
@@ -80,21 +80,27 @@ namespace FlaxEngine.GUI
public Color BackgroundColorSelected { get; set; }
///
- /// Gets or sets the color of the border.
+ /// Gets or sets whether the button has a border.
///
[EditorDisplay("Border Style"), EditorOrder(2010), ExpandGroups]
+ public bool HasBorder { get; set; } = true;
+
+ ///
+ /// Gets or sets the color of the border.
+ ///
+ [EditorDisplay("Border Style"), EditorOrder(2011), ExpandGroups]
public Color BorderColor { get; set; }
///
/// Gets or sets the border color when button is highlighted.
///
- [EditorDisplay("Border Style"), EditorOrder(2011)]
+ [EditorDisplay("Border Style"), EditorOrder(2012)]
public Color BorderColorHighlighted { get; set; }
///
/// Gets or sets the border color when button is selected.
///
- [EditorDisplay("Border Style"), EditorOrder(2012)]
+ [EditorDisplay("Border Style"), EditorOrder(2013)]
public Color BorderColorSelected { get; set; }
///
@@ -245,7 +251,8 @@ namespace FlaxEngine.GUI
BackgroundBrush.Draw(clientRect, backgroundColor);
else
Render2D.FillRectangle(clientRect, backgroundColor);
- Render2D.DrawRectangle(clientRect, borderColor);
+ if (HasBorder)
+ Render2D.DrawRectangle(clientRect, borderColor);
// Draw text
Render2D.DrawText(_font?.GetFont(), TextMaterial, _text, clientRect, textColor, TextAlignment.Center, TextAlignment.Center);
diff --git a/Source/Engine/UI/GUI/Common/ProgressBar.cs b/Source/Engine/UI/GUI/Common/ProgressBar.cs
index a742d3b43..760f031a7 100644
--- a/Source/Engine/UI/GUI/Common/ProgressBar.cs
+++ b/Source/Engine/UI/GUI/Common/ProgressBar.cs
@@ -10,6 +10,48 @@ namespace FlaxEngine.GUI
///
public class ProgressBar : ContainerControl
{
+ ///
+ /// The method used to effect the bar.
+ ///
+ public enum BarMethod
+ {
+ ///
+ /// Stretch the bar.
+ ///
+ Stretch,
+
+ ///
+ /// Clip the bar.
+ ///
+ Clip,
+ }
+
+ ///
+ /// The origin to move the progress bar to.
+ ///
+ public enum BarOrigin
+ {
+ ///
+ /// Move the bar horizontally to the left.
+ ///
+ HorizontalLeft,
+
+ ///
+ /// Move the bar horizontally to the right.
+ ///
+ HorizontalRight,
+
+ ///
+ /// Move the bar vertically up.
+ ///
+ VerticalTop,
+
+ ///
+ /// Move the bar vertically down.
+ ///
+ VerticalBottom,
+ }
+
///
/// The value.
///
@@ -41,6 +83,18 @@ namespace FlaxEngine.GUI
///
public bool UseSmoothing => !Mathf.IsZero(SmoothingScale);
+ ///
+ /// The method used to effect the bar.
+ ///
+ [EditorOrder(41), Tooltip("The method used to effect the bar.")]
+ public BarMethod Method = BarMethod.Stretch;
+
+ ///
+ /// The origin or where the bar decreases to.
+ ///
+ [EditorOrder(42), Tooltip("The origin or where the bar decreases to.")]
+ public BarOrigin Origin = BarOrigin.HorizontalLeft;
+
///
/// Gets or sets the minimum value.
///
@@ -168,12 +222,44 @@ namespace FlaxEngine.GUI
float progressNormalized = (_current - _minimum) / _maximum;
if (progressNormalized > 0.001f)
{
- var barRect = new Rectangle(0, 0, Width * progressNormalized, Height);
- BarMargin.ShrinkRectangle(ref barRect);
- if (BarBrush != null)
- BarBrush.Draw(barRect, BarColor);
- else
- Render2D.FillRectangle(barRect, BarColor);
+ Rectangle barRect = new Rectangle(0, 0, Width * progressNormalized, Height);
+ switch (Origin)
+ {
+ case BarOrigin.HorizontalLeft:
+ break;
+ case BarOrigin.HorizontalRight:
+ barRect = new Rectangle(Width - Width * progressNormalized, 0, Width * progressNormalized, Height);
+ break;
+ case BarOrigin.VerticalTop:
+ barRect = new Rectangle(0, 0, Width, Height * progressNormalized);
+ break;
+ case BarOrigin.VerticalBottom:
+ barRect = new Rectangle(0, Height - Height * progressNormalized, Width, Height * progressNormalized);
+ break;
+ default: break;
+ }
+
+ switch (Method)
+ {
+ case BarMethod.Stretch:
+ BarMargin.ShrinkRectangle(ref barRect);
+ if (BarBrush != null)
+ BarBrush.Draw(barRect, BarColor);
+ else
+ Render2D.FillRectangle(barRect, BarColor);
+ break;
+ case BarMethod.Clip:
+ var rect = new Rectangle(0, 0, Width, Height);
+ BarMargin.ShrinkRectangle(ref rect);
+ Render2D.PushClip(ref barRect);
+ if (BarBrush != null)
+ BarBrush.Draw(rect, BarColor);
+ else
+ Render2D.FillRectangle(rect, BarColor);
+ Render2D.PopClip();
+ break;
+ default: break;
+ }
}
}
}
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
index fc3e6a534..f399dd67c 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.CSharp.cs
@@ -1596,7 +1596,9 @@ namespace Flax.Build.Bindings
toManagedContent.Append($"managed.{fieldInfo.Name} != IntPtr.Zero ? NativeInterop.GCHandleArrayToManagedArray<{originalElementType}>(Unsafe.As(ManagedHandle.FromIntPtr(managed.{fieldInfo.Name}).Target)) : null");
toNativeContent.Append($"managed.{fieldInfo.Name}?.Length > 0 ? ManagedHandle.ToIntPtr(NativeInterop.ManagedArrayToGCHandleWrappedArray(managed.{fieldInfo.Name}), GCHandleType.Weak) : IntPtr.Zero");
freeContents.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle handle = ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}); Span ptrs = (Unsafe.As(handle.Target)).ToSpan(); foreach (var ptr in ptrs) {{ if (ptr != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(ptr).Free(); }} }} (Unsafe.As(handle.Target)).Free(); handle.Free(); }}");
- freeContents2.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle handle = ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}); Span ptrs = (Unsafe.As(handle.Target)).ToSpan(); foreach (var ptr in ptrs) {{ if (ptr != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(ptr).Free(); }} }} (Unsafe.As(handle.Target)).Free(); handle.Free(); }}");
+
+ // Permanent ScriptingObject handle is passed from native side, do not release it
+ //freeContents2.AppendLine($"if (unmanaged.{fieldInfo.Name} != IntPtr.Zero) {{ ManagedHandle handle = ManagedHandle.FromIntPtr(unmanaged.{fieldInfo.Name}); Span ptrs = (Unsafe.As(handle.Target)).ToSpan(); foreach (var ptr in ptrs) {{ if (ptr != IntPtr.Zero) {{ ManagedHandle.FromIntPtr(ptr).Free(); }} }} (Unsafe.As(handle.Target)).Free(); handle.Free(); }}");
}
else
{
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
index d69b8dcbf..da94a0056 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
@@ -1116,7 +1116,7 @@ namespace Flax.Build.Bindings
var signatureEnd = contents.Length;
if (useSeparateImpl)
{
- // Write declarion only, function definition wil be put in the end of the file
+ // Write declaration only, function definition wil be put in the end of the file
CppContentsEnd.AppendFormat("{0} {2}::{1}(", returnValueType, functionInfo.UniqueName, callerName);
CppContentsEnd.Append(contents.ToString(signatureStart, signatureEnd - signatureStart));
contents.Append(';').AppendLine();
@@ -2016,7 +2016,7 @@ namespace Flax.Build.Bindings
var indent = " ";
if (useSeparateImpl)
{
- // Write declarion only, function definition wil be put in the end of the file
+ // Write declaration only, function definition wil be put in the end of the file
CppContentsEnd.AppendFormat("void {1}::{0}_ManagedBind(", eventInfo.Name, internalTypeName);
var sig = contents.ToString(signatureStart, contents.Length - signatureStart);
CppContentsEnd.Append(contents.ToString(signatureStart, contents.Length - signatureStart));
@@ -2027,6 +2027,8 @@ namespace Flax.Build.Bindings
contents.AppendLine().Append(indent).Append('{').AppendLine();
if (buildData.Toolchain?.Compiler == TargetCompiler.MSVC)
contents.Append(indent).AppendLine($" MSVC_FUNC_EXPORT(\"{classTypeNameManaged}::Internal_{eventInfo.Name}_Bind\")"); // Export generated function binding under the C# name
+ if (!eventInfo.IsStatic)
+ contents.Append(indent).Append(" if (__obj == nullptr) return;").AppendLine();
contents.Append(indent).Append(" Function Path.Combine(Globals.EngineRoot, "Source", "Platforms", Platform.Target.ToString(), "Binaries", "ThirdParty", Architecture.ToString());
///
- /// The scripting API building options.
+ /// The C# scripting API building options.
///
public struct ScriptingAPIOptions
{
@@ -224,6 +224,11 @@ namespace Flax.Build.NativeCpp
///
public CSharpNullableReferences CSharpNullableReferences = CSharpNullableReferences.Disable;
+ ///
+ /// Enable code optimization.
+ ///
+ public bool? Optimization;
+
public ScriptingAPIOptions()
{
}
@@ -232,13 +237,19 @@ namespace Flax.Build.NativeCpp
/// Adds the other options into this.
///
/// The other.
- public void Add(ScriptingAPIOptions other)
+ public void Add(ScriptingAPIOptions other, bool addBuildOptions = true)
{
Defines.AddRange(other.Defines);
SystemReferences.AddRange(other.SystemReferences);
FileReferences.AddRange(other.FileReferences);
Analyzers.AddRange(other.Analyzers);
IgnoreMissingDocumentationWarnings |= other.IgnoreMissingDocumentationWarnings;
+
+ if (addBuildOptions)
+ {
+ if (other.Optimization.HasValue)
+ Optimization |= other.Optimization;
+ }
}
}
diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs
index 225e46ba7..1e8550c4e 100644
--- a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs
+++ b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs
@@ -403,7 +403,7 @@ namespace Flax.Build
moduleOptions.PrivateIncludePaths.AddRange(dependencyOptions.PublicIncludePaths);
moduleOptions.Libraries.AddRange(dependencyOptions.Libraries);
moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries);
- moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI);
+ moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI, false);
moduleOptions.ExternalModules.AddRange(dependencyOptions.ExternalModules);
}
}
@@ -418,7 +418,7 @@ namespace Flax.Build
moduleOptions.PublicIncludePaths.AddRange(dependencyOptions.PublicIncludePaths);
moduleOptions.Libraries.AddRange(dependencyOptions.Libraries);
moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries);
- moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI);
+ moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI, false);
moduleOptions.ExternalModules.AddRange(dependencyOptions.ExternalModules);
}
}
diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VCProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VCProjectGenerator.cs
index 4538ea8c1..3e2a6c9b7 100644
--- a/Source/Tools/Flax.Build/Projects/VisualStudio/VCProjectGenerator.cs
+++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VCProjectGenerator.cs
@@ -1,5 +1,6 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
+using Flax.Build.NativeCpp;
using System;
using System.Collections.Generic;
using System.IO;
@@ -107,7 +108,7 @@ namespace Flax.Build.Projects.VisualStudio
vcProjectFileContent.AppendLine(" false");
vcProjectFileContent.AppendLine(" ./");
vcProjectFileContent.AppendLine(" ");
-
+
// Default properties
vcProjectFileContent.AppendLine(" ");
@@ -317,12 +318,31 @@ namespace Flax.Build.Projects.VisualStudio
vcFiltersFileContent.AppendLine(" ");
// IntelliSense information
+
+ var additionalOptions = new List();
+ switch (project.Configurations[0].TargetBuildOptions.CompileEnv.CppVersion)
+ {
+ case CppVersion.Cpp14:
+ additionalOptions.Add("/std:c++14");
+ break;
+ case CppVersion.Cpp17:
+ additionalOptions.Add("/std:c++17");
+ break;
+ case CppVersion.Cpp20:
+ additionalOptions.Add("/std:c++20");
+ break;
+ case CppVersion.Latest:
+ additionalOptions.Add("/std:c++latest");
+ break;
+ }
+
vcProjectFileContent.AppendLine(" ");
vcProjectFileContent.AppendLine(string.Format(" $(NMakePreprocessorDefinitions){0}", (project.Defines.Count > 0 ? (";" + string.Join(";", project.Defines)) : "")));
vcProjectFileContent.AppendLine(string.Format(" $(NMakeIncludeSearchPath){0}", (project.SearchPaths.Length > 0 ? (";" + string.Join(";", project.SearchPaths)) : "")));
vcProjectFileContent.AppendLine(" $(NMakeForcedIncludes)");
vcProjectFileContent.AppendLine(" $(NMakeAssemblySearchPath)");
vcProjectFileContent.AppendLine(" $(NMakeForcedUsingAssemblies)");
+ vcProjectFileContent.AppendLine(string.Format(" {0}", string.Join(" ", additionalOptions)));
vcProjectFileContent.AppendLine(" ");
foreach (var platform in platforms)