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)