diff --git a/.github/ISSUE_TEMPLATE/1-bug.yaml b/.github/ISSUE_TEMPLATE/1-bug.yaml index 2e2c65485..a75003f63 100644 --- a/.github/ISSUE_TEMPLATE/1-bug.yaml +++ b/.github/ISSUE_TEMPLATE/1-bug.yaml @@ -31,7 +31,7 @@ body: - '1.10' - '1.11' - master branch - default: 2 + default: 3 validations: required: true - type: textarea diff --git a/Flax.flaxproj b/Flax.flaxproj index fcdd215f7..f6cae9501 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 11, "Revision": 0, - "Build": 6802 + "Build": 6804 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2025 Wojciech Figat. All rights reserved.", diff --git a/Source/Editor/CustomEditors/Dedicated/EnvironmentProbeEditor.cs b/Source/Editor/CustomEditors/Dedicated/EnvironmentProbeEditor.cs index 8f2173a4e..7649da514 100644 --- a/Source/Editor/CustomEditors/Dedicated/EnvironmentProbeEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/EnvironmentProbeEditor.cs @@ -1,6 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.CustomEditors.Dedicated { @@ -11,7 +12,7 @@ namespace FlaxEditor.CustomEditors.Dedicated [CustomEditor(typeof(EnvironmentProbe)), DefaultEditor] public class EnvironmentProbeEditor : ActorEditor { - private FlaxEngine.GUI.Button _bake; + private Button _bake; /// public override void Initialize(LayoutElementsContainer layout) @@ -20,8 +21,9 @@ namespace FlaxEditor.CustomEditors.Dedicated if (Values.HasDifferentTypes == false) { - layout.Space(10); - _bake = layout.Button("Bake").Button; + var group = layout.Group("Bake"); + group.Panel.ItemsMargin = new Margin(Utilities.Constants.UIMargin * 2); + _bake = group.Button("Bake").Button; _bake.Clicked += BakeButtonClicked; } } diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs index 356ae5ee4..954599347 100644 --- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs @@ -914,9 +914,11 @@ namespace FlaxEditor.CustomEditors.Dedicated // Remove drop down arrows and containment lines if no objects in the group if (group.Children.Count == 0) { + group.Panel.Close(); group.Panel.ArrowImageOpened = null; group.Panel.ArrowImageClosed = null; group.Panel.EnableContainmentLines = false; + group.Panel.CanOpenClose = false; } // Scripts arrange bar diff --git a/Source/Editor/CustomEditors/Dedicated/SkyLightEditor.cs b/Source/Editor/CustomEditors/Dedicated/SkyLightEditor.cs index 0a38e0dfe..ee3f2a504 100644 --- a/Source/Editor/CustomEditors/Dedicated/SkyLightEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/SkyLightEditor.cs @@ -1,6 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.CustomEditors.Dedicated { @@ -19,8 +20,9 @@ namespace FlaxEditor.CustomEditors.Dedicated if (Values.HasDifferentTypes == false) { // Add 'Bake' button - layout.Space(10); - var button = layout.Button("Bake"); + var group = layout.Group("Bake"); + group.Panel.ItemsMargin = new Margin(Utilities.Constants.UIMargin * 2); + var button = group.Button("Bake"); button.Button.Clicked += BakeButtonClicked; } } diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 69bacee1a..b3fff5644 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -650,7 +650,7 @@ namespace FlaxEditor.CustomEditors.Editors panel.Panel.Size = new Float2(0, 18); panel.Panel.Margin = new Margin(0, 0, Utilities.Constants.UIMargin, 0); - var removeButton = panel.Button("-", "Remove the last item"); + var removeButton = panel.Button("-", "Remove the last item."); removeButton.Button.Size = new Float2(16, 16); removeButton.Button.Enabled = size > _minCount; removeButton.Button.AnchorPreset = AnchorPresets.TopRight; @@ -661,7 +661,7 @@ namespace FlaxEditor.CustomEditors.Editors Resize(Count - 1); }; - var addButton = panel.Button("+", "Add a new item"); + var addButton = panel.Button("+", "Add a new item."); addButton.Button.Size = new Float2(16, 16); addButton.Button.Enabled = (!NotNullItems || size > 0) && size < _maxCount; addButton.Button.AnchorPreset = AnchorPresets.TopRight; diff --git a/Source/Editor/CustomEditors/Editors/TypeEditor.cs b/Source/Editor/CustomEditors/Editors/TypeEditor.cs index 1ed5d6cf8..462902181 100644 --- a/Source/Editor/CustomEditors/Editors/TypeEditor.cs +++ b/Source/Editor/CustomEditors/Editors/TypeEditor.cs @@ -104,7 +104,7 @@ namespace FlaxEditor.CustomEditors.Editors public event Action TypePickerValueChanged; /// - /// The custom callback for types validation. Cane be used to implement a rule for types to pick. + /// The custom callback for types validation. Can be used to implement a rule for types to pick. /// public Func CheckValid; @@ -353,7 +353,13 @@ namespace FlaxEditor.CustomEditors.Editors } if (!string.IsNullOrEmpty(typeReference.CheckMethod)) { - var parentType = ParentEditor.Values[0].GetType(); + var parentEditor = ParentEditor; + // Find actual parent editor if parent editor is collection editor + while (parentEditor.GetType().IsAssignableTo(typeof(CollectionEditor))) + parentEditor = parentEditor.ParentEditor; + + var parentType = parentEditor.Values[0].GetType(); + var method = parentType.GetMethod(typeReference.CheckMethod, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (method != null) { diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index 85e579ec9..66328926d 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -1587,7 +1587,7 @@ namespace FlaxEditor if (dockedTo != null && dockedTo.SelectedTab != gameWin && dockedTo.SelectedTab != null) result = dockedTo.SelectedTab.Size; else - result = gameWin.Viewport.Size; + result = gameWin.Viewport.ContentSize; result *= root.DpiScale; result = Float2.Round(result); diff --git a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs index 7facfc807..e66f38398 100644 --- a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs +++ b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs @@ -726,7 +726,7 @@ namespace FlaxEditor.Surface.Archetypes private void OnSurfaceMouseUp(ref Float2 mouse, MouseButton buttons, ref bool handled) { - if (handled) + if (handled || Surface.Context != Context) return; // Check click over the connection @@ -751,7 +751,7 @@ namespace FlaxEditor.Surface.Archetypes private void OnSurfaceMouseDoubleClick(ref Float2 mouse, MouseButton buttons, ref bool handled) { - if (handled) + if (handled || Surface.Context != Context) return; // Check double click over the connection diff --git a/Source/Editor/Surface/AttributesEditor.cs b/Source/Editor/Surface/AttributesEditor.cs index 99bf8bd1a..29d5860c3 100644 --- a/Source/Editor/Surface/AttributesEditor.cs +++ b/Source/Editor/Surface/AttributesEditor.cs @@ -2,11 +2,8 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.Loader; -using System.Runtime.Serialization.Formatters.Binary; using FlaxEditor.CustomEditors; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI.ContextMenu; @@ -18,6 +15,7 @@ namespace FlaxEditor.Surface class AttributesEditor : ContextMenuBase { private CustomEditorPresenter _presenter; + private Proxy _proxy; private byte[] _oldData; private class Proxy @@ -72,11 +70,11 @@ namespace FlaxEditor.Surface /// Initializes a new instance of the class. /// /// The attributes list to edit. - /// The allowed attribute types to use. - public AttributesEditor(Attribute[] attributes, IList attributeType) + /// The allowed attribute types to use. + public AttributesEditor(Attribute[] attributes, IList attributeTypes) { // Context menu dimensions - const float width = 340.0f; + const float width = 375.0f; const float height = 370.0f; Size = new Float2(width, height); @@ -88,61 +86,68 @@ namespace FlaxEditor.Surface Parent = this }; - // Buttons - float buttonsWidth = (width - 16.0f) * 0.5f; + // Ok and Cancel Buttons + float buttonsWidth = (width - 12.0f) * 0.5f; float buttonsHeight = 20.0f; - var cancelButton = new Button(4.0f, title.Bottom + 4.0f, buttonsWidth, buttonsHeight) + var okButton = new Button(4.0f, Bottom - 4.0f - buttonsHeight, buttonsWidth, buttonsHeight) + { + Text = "Ok", + Parent = this + }; + okButton.Clicked += OnOkButtonClicked; + var cancelButton = new Button(okButton.Right + 4.0f, okButton.Y, buttonsWidth, buttonsHeight) { Text = "Cancel", Parent = this }; cancelButton.Clicked += Hide; - var okButton = new Button(cancelButton.Right + 4.0f, cancelButton.Y, buttonsWidth, buttonsHeight) - { - Text = "OK", - Parent = this - }; - okButton.Clicked += OnOkButtonClicked; - // Actual panel + // Actual panel used to display attributes var panel1 = new Panel(ScrollBars.Vertical) { - Bounds = new Rectangle(0, okButton.Bottom + 4.0f, width, height - okButton.Bottom - 2.0f), + Bounds = new Rectangle(0, title.Bottom + 4.0f, width, height - buttonsHeight - title.Height - 14.0f), Parent = this }; var editor = new CustomEditorPresenter(null); editor.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop; editor.Panel.IsScrollable = true; editor.Panel.Parent = panel1; - editor.Panel.Tag = attributeType; + editor.Panel.Tag = attributeTypes; _presenter = editor; // Cache 'previous' state to check if attributes were edited after operation _oldData = SurfaceMeta.GetAttributesData(attributes); - editor.Select(new Proxy + _proxy = new Proxy { Value = attributes, - }); + }; + editor.Select(_proxy); + + _presenter.Modified += OnPresenterModified; + OnPresenterModified(); + } + + private void OnPresenterModified() + { + if (_proxy.Value.Length == 0) + { + var label = _presenter.Label("No attributes.\nPress the \"+\" button to add a new one and then select an attribute type using the \"Type\" dropdown.", TextAlignment.Center); + label.Label.Wrapping = TextWrapping.WrapWords; + label.Control.Height = 35f; + label.Label.Margin = new Margin(10f); + label.Label.TextColor = label.Label.TextColorHighlighted = Style.Current.ForegroundGrey; + } } private void OnOkButtonClicked() { var newValue = ((Proxy)_presenter.Selection[0]).Value; - for (int i = 0; i < newValue.Length; i++) - { - if (newValue[i] == null) - { - MessageBox.Show("One of the attributes is null. Please set it to the valid object.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - return; - } - } + newValue = newValue.Where(v => v != null).ToArray(); var newData = SurfaceMeta.GetAttributesData(newValue); if (!_oldData.SequenceEqual(newData)) - { Edited?.Invoke(newValue); - } Hide(); } @@ -183,7 +188,9 @@ namespace FlaxEditor.Surface { _presenter = null; _oldData = null; + _proxy = null; Edited = null; + _presenter.Modified -= OnPresenterModified; base.OnDestroy(); } diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 0138a1b66..10e9fc776 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -214,22 +214,25 @@ namespace FlaxEditor.Surface if (!_isRenaming) Render2D.DrawText(style.FontLarge, Title, _headerRect, style.Foreground, TextAlignment.Center, TextAlignment.Center); - // Close button - Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - - // Color button - Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - - // Check if is resizing - if (_isResizing) + if (Surface.CanEdit) { - // Draw overlay - Render2D.FillRectangle(_resizeButtonRect, style.Selection); - Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); - } + // Close button + Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - // Resize button - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); + // Color button + Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); + + // Check if is resizing + if (_isResizing) + { + // Draw overlay + Render2D.FillRectangle(_resizeButtonRect, style.Selection); + Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); + } + + // Resize button + Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); + } // Selection outline if (_isSelected) diff --git a/Source/Editor/Windows/AssetReferencesGraphWindow.cs b/Source/Editor/Windows/AssetReferencesGraphWindow.cs index bc1437d97..dcaf98462 100644 --- a/Source/Editor/Windows/AssetReferencesGraphWindow.cs +++ b/Source/Editor/Windows/AssetReferencesGraphWindow.cs @@ -140,7 +140,7 @@ namespace FlaxEditor.Windows } private string _cacheFolder; - private Guid _assetId; + private AssetItem _item; private Surface _surface; private Label _loadingLabel; private CancellationTokenSource _token; @@ -163,13 +163,13 @@ namespace FlaxEditor.Windows public AssetReferencesGraphWindow(Editor editor, AssetItem assetItem) : base(editor, false, ScrollBars.None) { - Title = assetItem.ShortName + " References"; + _item = assetItem; + Title = _item.ShortName + " References"; _tempFolder = StringUtils.NormalizePath(Path.GetDirectoryName(Globals.TemporaryFolder)); _cacheFolder = Path.Combine(Globals.ProjectCacheFolder, "References"); if (!Directory.Exists(_cacheFolder)) Directory.CreateDirectory(_cacheFolder); - _assetId = assetItem.ID; _surface = new Surface(this) { AnchorPreset = AnchorPresets.StretchAll, @@ -194,6 +194,7 @@ namespace FlaxEditor.Windows _nodesAssets.Add(assetId); var node = new AssetNode((uint)_nodes.Count + 1, _surface.Context, GraphNodes[0], GraphGroups[0], assetId); _nodes.Add(node); + return node; } @@ -392,8 +393,7 @@ namespace FlaxEditor.Windows _nodesAssets = new HashSet(); var searchLevel = 4; // TODO: make it as an option (somewhere in window UI) // TODO: add option to filter assets by type (eg. show only textures as leaf nodes) - var assetNode = SpawnNode(_assetId); - // TODO: add some outline or tint color to the main node + var assetNode = SpawnNode(_item.ID); BuildGraph(assetNode, searchLevel, false); ArrangeGraph(assetNode, false); BuildGraph(assetNode, searchLevel, true); @@ -402,6 +402,10 @@ namespace FlaxEditor.Windows return; _progress = 100.0f; + var commentRect = assetNode.EditorBounds; + commentRect.Expand(80f); + _surface.Context.CreateComment(ref commentRect, _item.ShortName, Color.Green); + // Update UI FlaxEngine.Scripting.InvokeOnUpdate(() => { diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index 3d2ec4a66..41382e60c 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -142,6 +142,7 @@ namespace FlaxEditor.Windows { Title = "Content"; Icon = editor.Icons.Folder32; + var style = Style.Current; FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); @@ -164,6 +165,8 @@ namespace FlaxEditor.Windows _navigationBar = new NavigationBar { Parent = _toolStrip, + ScrollbarTrackColor = style.Background, + ScrollbarThumbColor = style.ForegroundGrey, }; // Split panel @@ -179,7 +182,7 @@ namespace FlaxEditor.Windows var headerPanel = new ContainerControl { AnchorPreset = AnchorPresets.HorizontalStretchTop, - BackgroundColor = Style.Current.Background, + BackgroundColor = style.Background, IsScrollable = false, Offsets = new Margin(0, 0, 0, 18 + 6), }; diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index 96c8dad22..5f9ad71fd 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -10,17 +10,117 @@ using FlaxEditor.Modules; using FlaxEditor.Options; using FlaxEngine; using FlaxEngine.GUI; -using FlaxEngine.Json; namespace FlaxEditor.Windows { + /// + /// Render output control with content scaling support. + /// + public class ScaledRenderOutputControl : RenderOutputControl + { + /// + /// Custom scale. + /// + public float ContentScale = 1.0f; + + /// + /// Actual bounds size for content (incl. scale). + /// + public Float2 ContentSize => Size / ContentScale; + + /// + public ScaledRenderOutputControl(SceneRenderTask task) + : base(task) + { + } + + /// + public override void Draw() + { + DrawSelf(); + + // Draw children with scale + var scaling = new Float3(ContentScale, ContentScale, 1); + Matrix3x3.Scaling(ref scaling, out Matrix3x3 scale); + Render2D.PushTransform(scale); + if (ClipChildren) + { + GetDesireClientArea(out var clientArea); + Render2D.PushClip(ref clientArea); + DrawChildren(); + Render2D.PopClip(); + } + else + { + DrawChildren(); + } + + Render2D.PopTransform(); + } + + /// + public override void GetDesireClientArea(out Rectangle rect) + { + // Scale the area for the client controls + rect = new Rectangle(Float2.Zero, Size / ContentScale); + } + + /// + public override bool IntersectsContent(ref Float2 locationParent, out Float2 location) + { + // Skip local PointFromParent but use base code + location = base.PointFromParent(ref locationParent); + return ContainsPoint(ref location); + } + + /// + public override bool IntersectsChildContent(Control child, Float2 location, out Float2 childSpaceLocation) + { + location /= ContentScale; + return base.IntersectsChildContent(child, location, out childSpaceLocation); + } + + /// + public override bool ContainsPoint(ref Float2 location, bool precise = false) + { + if (precise) // Ignore as utility-only element + return false; + return base.ContainsPoint(ref location, precise); + } + + /// + public override bool RayCast(ref Float2 location, out Control hit) + { + var p = location / ContentScale; + if (RayCastChildren(ref p, out hit)) + return true; + return base.RayCast(ref location, out hit); + } + + /// + public override Float2 PointToParent(ref Float2 location) + { + var result = base.PointToParent(ref location); + result *= ContentScale; + return result; + } + + /// + public override Float2 PointFromParent(ref Float2 location) + { + var result = base.PointFromParent(ref location); + result /= ContentScale; + return result; + } + } + /// /// Provides in-editor play mode simulation. /// /// public class GameWindow : EditorWindow { - private readonly RenderOutputControl _viewport; + private readonly ScaledRenderOutputControl _viewport; private readonly GameRoot _guiRoot; private bool _showGUI = true, _editGUI = true; private bool _showDebugDraw = false; @@ -77,7 +177,7 @@ namespace FlaxEditor.Windows /// /// Gets the viewport. /// - public RenderOutputControl Viewport => _viewport; + public ScaledRenderOutputControl Viewport => _viewport; /// /// Gets or sets a value indicating whether show game GUI in the view or keep it hidden. @@ -295,7 +395,7 @@ namespace FlaxEditor.Windows var task = MainRenderTask.Instance; // Setup viewport - _viewport = new RenderOutputControl(task) + _viewport = new ScaledRenderOutputControl(task) { AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, @@ -396,11 +496,8 @@ namespace FlaxEditor.Windows { if (v == null) return; - if (v.Size.Y <= 0 || v.Size.X <= 0) - { return; - } if (string.Equals(v.Label, "Free Aspect", StringComparison.Ordinal) && v.Size == new Int2(1, 1)) { @@ -448,15 +545,7 @@ namespace FlaxEditor.Windows private void ResizeViewport() { - if (!_freeAspect) - { - _windowAspectRatio = Width / Height; - } - else - { - _windowAspectRatio = 1; - } - + _windowAspectRatio = _freeAspect ? 1 : Width / Height; var scaleWidth = _viewportAspectRatio / _windowAspectRatio; var scaleHeight = _windowAspectRatio / _viewportAspectRatio; @@ -468,6 +557,24 @@ namespace FlaxEditor.Windows { _viewport.Bounds = new Rectangle(Width * (1 - scaleWidth) / 2, 0, Width * scaleWidth, Height); } + + if (_viewport.KeepAspectRatio) + { + var resolution = _viewport.CustomResolution.HasValue ? (Float2)_viewport.CustomResolution.Value : Size; + if (scaleHeight < 1) + { + _viewport.ContentScale = _viewport.Width / resolution.X; + } + else + { + _viewport.ContentScale = _viewport.Height / resolution.Y; + } + } + else + { + _viewport.ContentScale = 1; + } + _viewport.SyncBackbufferSize(); PerformLayout(); } @@ -911,6 +1018,7 @@ namespace FlaxEditor.Windows return child.OnNavigate(direction, Float2.Zero, this, visited); } } + return null; } @@ -961,7 +1069,7 @@ namespace FlaxEditor.Windows else _defaultScaleActiveIndex = 0; } - + if (_customScaleActiveIndex != -1) { var options = Editor.UI.CustomViewportScaleOptions; diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index cb711013b..9fd4cee9c 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -666,7 +666,7 @@ void Asset::onLoaded() { onLoaded_MainThread(); } - else if (OnLoaded.IsBinded()) + else if (OnLoaded.IsBinded() || _references.HasItems()) { Function action; action.Bind(this); diff --git a/Source/Engine/Content/Assets/MaterialInstance.cpp b/Source/Engine/Content/Assets/MaterialInstance.cpp index b3710ebbd..78a276a8a 100644 --- a/Source/Engine/Content/Assets/MaterialInstance.cpp +++ b/Source/Engine/Content/Assets/MaterialInstance.cpp @@ -218,10 +218,14 @@ Asset::LoadResult MaterialInstance::load() Guid baseMaterialId; headerStream.Read(baseMaterialId); auto baseMaterial = Content::LoadAsync(baseMaterialId); + if (baseMaterial) + baseMaterial->AddReference(); // Load parameters if (Params.Load(&headerStream)) { + if (baseMaterial) + baseMaterial->RemoveReference(); LOG(Warning, "Cannot load material parameters."); return LoadResult::CannotLoadData; } @@ -239,6 +243,7 @@ Asset::LoadResult MaterialInstance::load() ParamsChanged(); } + baseMaterial->RemoveReference(); return LoadResult::Ok; } diff --git a/Source/Engine/Content/Storage/FlaxChunk.h b/Source/Engine/Content/Storage/FlaxChunk.h index 6e9887574..1d3bdce1a 100644 --- a/Source/Engine/Content/Storage/FlaxChunk.h +++ b/Source/Engine/Content/Storage/FlaxChunk.h @@ -87,6 +87,10 @@ public: /// double LastAccessTime = 0.0; + /// + /// Flag set to indicate that chunk is during loading (atomic access to sync multiple reading threads). + /// + int64 IsLoading = 0; /// /// The chunk data. /// @@ -146,7 +150,7 @@ public: /// FORCE_INLINE bool IsLoaded() const { - return Data.IsValid(); + return Data.IsValid() && Platform::AtomicRead(&IsLoading) == 0; } /// @@ -154,7 +158,7 @@ public: /// FORCE_INLINE bool IsMissing() const { - return Data.IsInvalid(); + return !IsLoaded(); } /// diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index ed8b623ae..1c6be4971 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -5,6 +5,7 @@ #include "FlaxPackage.h" #include "ContentStorageManager.h" #include "Engine/Core/Log.h" +#include "Engine/Core/ScopeExit.h" #include "Engine/Core/Types/TimeSpan.h" #include "Engine/Platform/File.h" #include "Engine/Profiler/ProfilerCPU.h" @@ -246,6 +247,7 @@ FlaxStorage::~FlaxStorage() ASSERT(IsDisposed()); CHECK(_chunksLock == 0); CHECK(_refCount == 0); + CHECK(_isUnloadingData == 0); ASSERT(_chunks.IsEmpty()); #if USE_EDITOR @@ -261,6 +263,22 @@ FlaxStorage::~FlaxStorage() #endif } +void FlaxStorage::LockChunks() +{ +RETRY: + Platform::InterlockedIncrement(&_chunksLock); + if (Platform::AtomicRead(&_isUnloadingData) != 0) + { + // Someone else is closing file handles or freeing chunks so wait for it to finish and retry + Platform::InterlockedDecrement(&_chunksLock); + do + { + Platform::Sleep(1); + } while (Platform::AtomicRead(&_isUnloadingData) != 0); + goto RETRY; + } +} + FlaxStorage::LockData FlaxStorage::LockSafe() { auto lock = LockData(this); @@ -689,7 +707,6 @@ bool FlaxStorage::LoadAssetHeader(const Guid& id, AssetInitData& data) return true; } - // Load header return LoadAssetHeader(e, data); } @@ -699,7 +716,10 @@ bool FlaxStorage::LoadAssetChunk(FlaxChunk* chunk) ASSERT(IsLoaded()); ASSERT(chunk != nullptr && _chunks.Contains(chunk)); - // Check if already loaded + // Protect against loading the same chunk from multiple threads at once + while (Platform::InterlockedCompareExchange(&chunk->IsLoading, 1, 0) != 0) + Platform::Sleep(1); + SCOPE_EXIT{ Platform::AtomicStore(&chunk->IsLoading, 0); }; if (chunk->IsLoaded()) return false; @@ -776,12 +796,10 @@ bool FlaxStorage::LoadAssetChunk(FlaxChunk* chunk) // Raw data chunk->Data.Read(stream, size); } - ASSERT(chunk->IsLoaded()); chunk->RegisterUsage(); } UnlockChunks(); - return failed; } @@ -1420,10 +1438,12 @@ FileReadStream* FlaxStorage::OpenFile() bool FlaxStorage::CloseFileHandles() { + // Guard the whole process so if new thread wants to lock the chunks will need to wait for this to end + Platform::InterlockedIncrement(&_isUnloadingData); + SCOPE_EXIT{ Platform::InterlockedDecrement(&_isUnloadingData); }; + if (Platform::AtomicRead(&_chunksLock) == 0 && Platform::AtomicRead(&_files) == 0) - { - return false; - } + return false; // Early out when no files are opened PROFILE_CPU(); PROFILE_MEM(ContentFiles); @@ -1496,9 +1516,21 @@ void FlaxStorage::Tick(double time) { auto chunk = _chunks.Get()[i]; const bool wasUsed = (time - chunk->LastAccessTime) < unusedDataChunksLifetime; - if (!wasUsed && chunk->IsLoaded() && EnumHasNoneFlags(chunk->Flags, FlaxChunkFlags::KeepInMemory)) + if (!wasUsed && + chunk->IsLoaded() && + EnumHasNoneFlags(chunk->Flags, FlaxChunkFlags::KeepInMemory) && + Platform::AtomicRead(&chunk->IsLoading) == 0) { + // Guard the unloading so if other thread wants to lock the chunks will need to wait for this to end + Platform::InterlockedIncrement(&_isUnloadingData); + if (Platform::AtomicRead(&_chunksLock) != 0 || Platform::AtomicRead(&chunk->IsLoading) != 0) + { + // Someone started loading so skip ticking + Platform::InterlockedDecrement(&_isUnloadingData); + return; + } chunk->Unload(); + Platform::InterlockedDecrement(&_isUnloadingData); } wasAnyUsed |= wasUsed; } diff --git a/Source/Engine/Content/Storage/FlaxStorage.h b/Source/Engine/Content/Storage/FlaxStorage.h index 450de9808..6de462214 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.h +++ b/Source/Engine/Content/Storage/FlaxStorage.h @@ -90,6 +90,7 @@ protected: int64 _refCount = 0; int64 _chunksLock = 0; int64 _files = 0; + int64 _isUnloadingData = 0; double _lastRefLostTime; CriticalSection _loadLocker; @@ -129,10 +130,7 @@ public: /// /// Locks the storage chunks data to prevent disposing them. Also ensures that file handles won't be closed while chunks are locked. /// - FORCE_INLINE void LockChunks() - { - Platform::InterlockedIncrement(&_chunksLock); - } + void LockChunks(); /// /// Unlocks the storage chunks data. diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index 735bc162e..71d209301 100644 --- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp +++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp @@ -338,10 +338,10 @@ public: StreamTextureMipTask(StreamingTexture* texture, int32 mipIndex, Task* rootTask) : GPUUploadTextureMipTask(texture->GetTexture(), mipIndex, Span(nullptr, 0), 0, 0, false) , _streamingTexture(texture) - , _rootTask(rootTask ? rootTask : this) + , _rootTask(rootTask) , _dataLock(_streamingTexture->GetOwner()->LockData()) { - _streamingTexture->_streamingTasks.Add(_rootTask); + _streamingTexture->_streamingTasks.Add(this); _texture.Released.Bind(this); } @@ -357,7 +357,7 @@ private: if (_streamingTexture) { ScopeLock lock(_streamingTexture->GetOwner()->GetOwnerLocker()); - _streamingTexture->_streamingTasks.Remove(_rootTask); + _streamingTexture->_streamingTasks.Remove(this); _streamingTexture = nullptr; } } @@ -422,6 +422,15 @@ protected: GPUUploadTextureMipTask::OnFail(); } + + void OnCancel() override + { + GPUUploadTextureMipTask::OnCancel(); + + // Cancel the root task too (eg. mip loading from asset) + if (_rootTask != nullptr) + _rootTask->Cancel(); + } }; Task* StreamingTexture::CreateStreamingTask(int32 residency) diff --git a/Source/Engine/Level/Actors/EnvironmentProbe.h b/Source/Engine/Level/Actors/EnvironmentProbe.h index 09f771437..4a77a811e 100644 --- a/Source/Engine/Level/Actors/EnvironmentProbe.h +++ b/Source/Engine/Level/Actors/EnvironmentProbe.h @@ -42,38 +42,38 @@ public: /// /// The reflections texture resolution. /// - API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"Probe\")") + API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"Quality\")") ProbeCubemapResolution CubemapResolution = ProbeCubemapResolution::UseGraphicsSettings; + /// + /// The probe update mode. + /// + API_FIELD(Attributes = "EditorOrder(10), EditorDisplay(\"Quality\")") + ProbeUpdateMode UpdateMode = ProbeUpdateMode::Manual; + /// /// The reflections brightness. /// - API_FIELD(Attributes="EditorOrder(10), Limit(0, 1000, 0.01f), EditorDisplay(\"Probe\")") + API_FIELD(Attributes="EditorOrder(0), Limit(0, 1000, 0.01f), EditorDisplay(\"Probe\")") float Brightness = 1.0f; /// /// The probe rendering order. The higher values are render later (on top). /// - API_FIELD(Attributes = "EditorOrder(25), EditorDisplay(\"Probe\")") + API_FIELD(Attributes = "EditorOrder(20), EditorDisplay(\"Probe\")") int32 SortOrder = 0; - /// - /// The probe update mode. - /// - API_FIELD(Attributes="EditorOrder(30), EditorDisplay(\"Probe\")") - ProbeUpdateMode UpdateMode = ProbeUpdateMode::Manual; - /// /// The probe capture camera near plane distance. /// - API_FIELD(Attributes="EditorOrder(30), Limit(0, float.MaxValue, 0.01f), EditorDisplay(\"Probe\")") + API_FIELD(Attributes="EditorOrder(25), Limit(0, float.MaxValue, 0.01f), EditorDisplay(\"Probe\")") float CaptureNearPlane = 10.0f; public: /// /// Gets the probe radius. /// - API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(3000.0f), Limit(0), EditorDisplay(\"Probe\")") + API_PROPERTY(Attributes="EditorOrder(15), DefaultValue(3000.0f), Limit(0), EditorDisplay(\"Probe\")") float GetRadius() const; /// diff --git a/Source/Engine/Networking/NetworkInternal.h b/Source/Engine/Networking/NetworkInternal.h index 7eb4f7d52..2aade3811 100644 --- a/Source/Engine/Networking/NetworkInternal.h +++ b/Source/Engine/Networking/NetworkInternal.h @@ -8,7 +8,7 @@ #endif // Internal version number of networking implementation. Updated once engine changes serialization or connection rules. -#define NETWORK_PROTOCOL_VERSION 4 +#define NETWORK_PROTOCOL_VERSION 5 // Enables encoding object ids and typenames via uint32 keys rather than full data send. #define USE_NETWORK_KEYS 1 @@ -29,6 +29,7 @@ enum class NetworkMessageIDs : uint8 ObjectDespawn, ObjectRole, ObjectRpc, + ObjectRpcPart, MAX, }; @@ -48,6 +49,7 @@ public: static void OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer); static void OnNetworkMessageObjectRole(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer); static void OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer); + static void OnNetworkMessageObjectRpcPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer); #if COMPILE_WITH_PROFILER diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp index 784bbf51e..36ff94c5c 100644 --- a/Source/Engine/Networking/NetworkManager.cpp +++ b/Source/Engine/Networking/NetworkManager.cpp @@ -391,6 +391,7 @@ namespace NetworkInternal::OnNetworkMessageObjectDespawn, NetworkInternal::OnNetworkMessageObjectRole, NetworkInternal::OnNetworkMessageObjectRpc, + NetworkInternal::OnNetworkMessageObjectRpcPart, }; } diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index fba916891..c584d3526 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -55,14 +55,14 @@ PACK_STRUCT(struct NetworkMessageObjectReplicate uint32 OwnerFrame; }); -PACK_STRUCT(struct NetworkMessageObjectReplicatePayload +PACK_STRUCT(struct NetworkMessageObjectPartPayload { uint16 DataSize; uint16 PartsCount; uint16 PartSize; }); -PACK_STRUCT(struct NetworkMessageObjectReplicatePart +PACK_STRUCT(struct NetworkMessageObjectPart { NetworkMessageIDs ID = NetworkMessageIDs::ObjectReplicatePart; uint32 OwnerFrame; @@ -111,7 +111,7 @@ PACK_STRUCT(struct NetworkMessageObjectRole PACK_STRUCT(struct NetworkMessageObjectRpc { NetworkMessageIDs ID = NetworkMessageIDs::ObjectRpc; - uint16 ArgsSize; + uint32 OwnerFrame; }); struct NetworkReplicatedObject @@ -182,13 +182,14 @@ struct Serializer void* Tags[2]; }; -struct ReplicateItem +struct PartsItem { ScriptingObjectReference Object; Guid ObjectId; uint16 PartsLeft; uint32 OwnerFrame; uint32 OwnerClientId; + const void* Tag; Array Data; }; @@ -220,7 +221,7 @@ struct DespawnItem DataContainer Targets; }; -struct RpcItem +struct RpcSendItem { ScriptingObjectReference Object; NetworkRpcName Name; @@ -233,11 +234,12 @@ namespace { CriticalSection ObjectsLock; HashSet Objects; - Array ReplicationParts; + Array ReplicationParts; + Array RpcParts; Array SpawnParts; Array SpawnQueue; Array DespawnQueue; - Array RpcQueue; + Array RpcQueue; Dictionary IdsRemappingTable; NetworkStream* CachedWriteStream = nullptr; NetworkStream* CachedReadStream = nullptr; @@ -251,6 +253,7 @@ namespace #endif Array DespawnedObjects; uint32 SpawnId = 0; + uint32 RpcId = 0; #if USE_EDITOR void OnScriptsReloading() @@ -505,6 +508,76 @@ void SetupObjectSpawnMessageItem(SpawnItem* e, NetworkMessage& msg) msg.WriteStructure(msgDataItem); } +void SendInParts(NetworkPeer* peer, NetworkChannelType channel, const byte* data, const uint16 dataSize, NetworkMessage& msg, const NetworkRpcName& name, bool toServer, const Guid& objectId, uint32 ownerFrame, NetworkMessageIDs partId) +{ + NetworkMessageObjectPartPayload msgDataPayload; + msgDataPayload.DataSize = dataSize; + const uint32 networkKeyIdWorstCaseSize = sizeof(uint32) + sizeof(Guid); + const uint32 msgMaxData = peer->Config.MessageSize - msg.Position - sizeof(NetworkMessageObjectPartPayload); + const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectPart) - networkKeyIdWorstCaseSize; + uint32 partsCount = 1; + uint32 dataStart = 0; + uint32 msgDataSize = dataSize; + if (dataSize > msgMaxData) + { + // Send msgMaxData within first message + msgDataSize = msgMaxData; + dataStart += msgMaxData; + + // Send rest of the data in separate parts + partsCount += Math::DivideAndRoundUp(dataSize - dataStart, partMaxData); + + // TODO: promote channel to Ordered when using parts? + } + else + dataStart += dataSize; + ASSERT(partsCount <= MAX_uint8); + msgDataPayload.PartsCount = partsCount; + msgDataPayload.PartSize = msgDataSize; + msg.WriteStructure(msgDataPayload); + msg.WriteBytes(data, msgDataSize); + uint32 messageSize = msg.Length; + if (toServer) + peer->EndSendMessage(channel, msg); + else + peer->EndSendMessage(channel, msg, CachedTargets); + + // Send all other parts + for (uint32 partIndex = 1; partIndex < partsCount; partIndex++) + { + NetworkMessageObjectPart msgDataPart; + msgDataPart.ID = partId; + msgDataPart.OwnerFrame = ownerFrame; + msgDataPart.DataSize = msgDataPayload.DataSize; + msgDataPart.PartsCount = msgDataPayload.PartsCount; + msgDataPart.PartStart = dataStart; + msgDataPart.PartSize = Math::Min(dataSize - dataStart, partMaxData); + msg = peer->BeginSendMessage(); + msg.WriteStructure(msgDataPart); + msg.WriteNetworkId(objectId); + msg.WriteBytes(data + msgDataPart.PartStart, msgDataPart.PartSize); + messageSize += msg.Length; + dataStart += msgDataPart.PartSize; + if (toServer) + peer->EndSendMessage(channel, msg); + else + peer->EndSendMessage(channel, msg, CachedTargets); + } + ASSERT_LOW_LAYER(dataStart == dataSize); + +#if COMPILE_WITH_PROFILER + // Network stats recording + if (NetworkInternal::EnableProfiling) + { + auto& profileEvent = NetworkInternal::ProfilerEvents[name]; + profileEvent.Count++; + profileEvent.DataSize += dataSize; + profileEvent.MessageSize += messageSize; + profileEvent.Receivers += toServer ? 1 : CachedTargets.Count(); + } +#endif +} + void SendObjectSpawnMessage(const SpawnGroup& group, const Array& clients) { PROFILE_CPU(); @@ -589,7 +662,7 @@ void SendObjectRoleMessage(const NetworkReplicatedObject& item, const NetworkCli msg.WriteNetworkId(objectId); if (NetworkManager::IsClient()) { - NetworkManager::Peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg); + peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg); } else { @@ -598,6 +671,160 @@ void SendObjectRoleMessage(const NetworkReplicatedObject& item, const NetworkCli } } +void SendDespawn(DespawnItem& e) +{ + NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object ID={}", e.Id.ToString()); + NetworkMessageObjectDespawn msgData; + Guid objectId = e.Id; + { + // Remap local client object ids into server ids + IdsRemappingTable.KeyOf(objectId, &objectId); + } + auto peer = NetworkManager::Peer; + NetworkMessage msg = peer->BeginSendMessage(); + msg.WriteStructure(msgData); + msg.WriteNetworkId(objectId); + BuildCachedTargets(NetworkManager::Clients, e.Targets); + if (NetworkManager::IsClient()) + peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg); + else + peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets); +} + +void SendReplication(ScriptingObject* obj, NetworkClientsMask targetClients) +{ + auto it = Objects.Find(obj->GetID()); + if (it.IsEnd()) + return; + auto& item = it->Item; + const bool isClient = NetworkManager::IsClient(); + + // Skip serialization of objects that none will receive + if (!isClient) + { + BuildCachedTargets(item, targetClients); + if (CachedTargets.Count() == 0) + return; + } + + if (item.AsNetworkObject) + item.AsNetworkObject->OnNetworkSerialize(); + + // Serialize object + NetworkStream* stream = CachedWriteStream; + stream->Initialize(); + const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true); + if (failed) + { + //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString()); + return; + } + const uint32 size = stream->GetPosition(); + if (size > MAX_uint16) + { + LOG(Error, "Too much data for object {} replication ({} bytes provided while limit is {}).", item.ToString(), size, MAX_uint16); + return; + } + +#if USE_NETWORK_REPLICATOR_CACHE + // Process replication cache to skip sending object data if it didn't change + if (item.RepCache.Data.Length() == size && + item.RepCache.Mask == targetClients && + Platform::MemoryCompare(item.RepCache.Data.Get(), stream->GetBuffer(), size) == 0) + { + return; + } + item.RepCache.Mask = targetClients; + item.RepCache.Data.Copy(stream->GetBuffer(), size); +#endif + // TODO: use Unreliable for dynamic objects that are replicated every frame? (eg. player state) + constexpr NetworkChannelType repChannel = NetworkChannelType::Reliable; + + // Send object to clients + NetworkMessageObjectReplicate msgData; + msgData.OwnerFrame = NetworkManager::Frame; + Guid objectId = item.ObjectId, parentId = item.ParentId; + { + // Remap local client object ids into server ids + IdsRemappingTable.KeyOf(objectId, &objectId); + IdsRemappingTable.KeyOf(parentId, &parentId); + } + NetworkPeer* peer = NetworkManager::Peer; + NetworkMessage msg = peer->BeginSendMessage(); + msg.WriteStructure(msgData); + msg.WriteNetworkId(objectId); + msg.WriteNetworkId(parentId); + msg.WriteNetworkName(obj->GetType().Fullname); + const NetworkRpcName name(obj->GetTypeHandle(), StringAnsiView::Empty); + SendInParts(peer, repChannel, stream->GetBuffer(), size, msg, name, isClient, objectId, msgData.OwnerFrame, NetworkMessageIDs::ObjectReplicatePart); +} + +void SendRpc(RpcSendItem& e) +{ + ScriptingObject* obj = e.Object.Get(); + if (!obj) + return; + auto it = Objects.Find(obj->GetID()); + if (it == Objects.End()) + { +#if !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(), e.Name.Second.ToString(), obj->GetID()); +#endif + return; + } + auto& item = it->Item; + if (e.ArgsData.Length() > MAX_uint16) + { + LOG(Error, "Too much data for object RPC method '{}.{}' on object '{}' ({} bytes provided while limit is {}).", e.Name.First.ToString(), e.Name.Second.ToString(), obj->GetID(), e.ArgsData.Length(), MAX_uint16); + return; + } + const NetworkManagerMode mode = NetworkManager::Mode; + NetworkPeer* peer = NetworkManager::Peer; + + bool toServer; + if (e.Info.Server && mode == NetworkManagerMode::Client) + { + // Client -> Server +#if USE_NETWORK_REPLICATOR_LOG + if (e.Targets.Length() != 0) + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}.{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString()); +#endif + toServer = true; + } + else if (e.Info.Client && (mode == NetworkManagerMode::Server || mode == NetworkManagerMode::Host)) + { + // Server -> Client(s) + BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId); + if (CachedTargets.IsEmpty()) + return; + toServer = false; + } + else + return; + + // Send RPC message + //NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}.{} object ID={}", e.Name.First.ToString(), e.Name.Second.ToString(), item.ToString()); + NetworkMessageObjectRpc msgData; + msgData.OwnerFrame = ++RpcId; + Guid objectId = item.ObjectId; + Guid parentId = item.ParentId; + { + // Remap local client object ids into server ids + IdsRemappingTable.KeyOf(objectId, &objectId); + IdsRemappingTable.KeyOf(parentId, &parentId); + } + NetworkMessage msg = peer->BeginSendMessage(); + msg.WriteStructure(msgData); + msg.WriteNetworkId(objectId); + msg.WriteNetworkId(parentId); + msg.WriteNetworkName(obj->GetType().Fullname); + msg.WriteNetworkName(e.Name.First.GetType().Fullname); + msg.WriteNetworkName(e.Name.Second); + NetworkChannelType channel = (NetworkChannelType)e.Info.Channel; + SendInParts(peer, channel, e.ArgsData.Get(), e.ArgsData.Length(), msg, e.Name, toServer, objectId, msgData.OwnerFrame, NetworkMessageIDs::ObjectRpcPart); +} + void DeleteNetworkObject(ScriptingObject* obj) { // Remove from the mapping table @@ -708,38 +935,43 @@ FORCE_INLINE void DirtyObjectImpl(NetworkReplicatedObject& item, ScriptingObject Hierarchy->DirtyObject(obj); } -ReplicateItem* AddObjectReplicateItem(NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId) +PartsItem* AddPartsItem(Array& items, NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId) { // Reuse or add part item - ReplicateItem* replicateItem = nullptr; - for (auto& e : ReplicationParts) + PartsItem* item = nullptr; + for (auto& e : items) { if (e.OwnerFrame == ownerFrame && e.Data.Count() == dataSize && e.ObjectId == objectId) { // Reuse - replicateItem = &e; + item = &e; break; } } - if (!replicateItem) + if (!item) { // Add - replicateItem = &ReplicationParts.AddOne(); - replicateItem->ObjectId = objectId; - replicateItem->PartsLeft = partsCount; - replicateItem->OwnerFrame = ownerFrame; - replicateItem->OwnerClientId = senderClientId; - replicateItem->Data.Resize(dataSize); + item = &items.AddOne(); + item->ObjectId = objectId; + item->PartsLeft = partsCount; + item->OwnerFrame = ownerFrame; + item->OwnerClientId = senderClientId; + item->Data.Resize(dataSize); } // Copy part data - ASSERT(replicateItem->PartsLeft > 0); - replicateItem->PartsLeft--; - ASSERT(partStart + partSize <= replicateItem->Data.Count()); + ASSERT(item->PartsLeft > 0); + item->PartsLeft--; + ASSERT(partStart + partSize <= item->Data.Count()); const void* partData = event.Message.SkipBytes(partSize); - Platform::MemoryCopy(replicateItem->Data.Get() + partStart, partData, partSize); + Platform::MemoryCopy(item->Data.Get() + partStart, partData, partSize); - return replicateItem; + return item; +} + +FORCE_INLINE PartsItem* AddObjectReplicateItem(NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId) +{ + return AddPartsItem(ReplicationParts, event, ownerFrame, partsCount, dataSize, objectId, partStart, partSize, senderClientId); } void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, byte* data, uint32 dataSize, uint32 senderClientId) @@ -787,6 +1019,24 @@ void InvokeObjectReplication(NetworkReplicatedObject& item, uint32 ownerFrame, b DirtyObjectImpl(item, obj); } +FORCE_INLINE PartsItem* AddObjectRpcItem(NetworkEvent& event, uint32 ownerFrame, uint16 partsCount, uint16 dataSize, const Guid& objectId, uint16 partStart, uint16 partSize, uint32 senderClientId) +{ + return AddPartsItem(RpcParts, event, ownerFrame, partsCount, dataSize, objectId, partStart, partSize, senderClientId); +} + +void InvokeObjectRpc(const NetworkRpcInfo* info, byte* data, uint32 dataSize, uint32 senderClientId, ScriptingObject* obj) +{ + // Setup message reading stream + if (CachedReadStream == nullptr) + CachedReadStream = New(); + NetworkStream* stream = CachedReadStream; + stream->SenderId = senderClientId; + stream->Initialize(data, dataSize); + + // Execute RPC + info->Execute(obj, stream, info->Tag); +} + void InvokeObjectSpawn(const NetworkMessageObjectSpawn& msgData, const Guid& prefabId, const NetworkMessageObjectSpawnItem* msgDataItems) { ScopeLock lock(ObjectsLock); @@ -1652,9 +1902,6 @@ void NetworkInternal::NetworkReplicatorUpdate() if (Objects.Count() == 0) return; const bool isClient = NetworkManager::IsClient(); - const bool isServer = NetworkManager::IsServer(); - const bool isHost = NetworkManager::IsHost(); - NetworkPeer* peer = NetworkManager::Peer; if (!isClient && NewClients.Count() != 0) { @@ -1694,22 +1941,7 @@ void NetworkInternal::NetworkReplicatorUpdate() PROFILE_CPU_NAMED("DespawnQueue"); for (DespawnItem& e : DespawnQueue) { - // Send despawn message - NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Despawn object ID={}", e.Id.ToString()); - NetworkMessageObjectDespawn msgData; - Guid objectId = e.Id; - { - // Remap local client object ids into server ids - IdsRemappingTable.KeyOf(objectId, &objectId); - } - NetworkMessage msg = peer->BeginSendMessage(); - msg.WriteStructure(msgData); - msg.WriteNetworkId(objectId); - BuildCachedTargets(NetworkManager::Clients, e.Targets); - if (isClient) - peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg); - else - peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets); + SendDespawn(e); } DespawnQueue.Clear(); } @@ -1830,6 +2062,7 @@ void NetworkInternal::NetworkReplicatorUpdate() } } + // TODO: remove items from RpcParts after some TTL to reduce memory usage // TODO: remove items from SpawnParts after some TTL to reduce memory usage // Replicate all owned networked objects with other clients or server @@ -1871,136 +2104,11 @@ void NetworkInternal::NetworkReplicatorUpdate() PROFILE_CPU_NAMED("Replication"); if (CachedWriteStream == nullptr) CachedWriteStream = New(); - NetworkStream* stream = CachedWriteStream; - stream->SenderId = NetworkManager::LocalClientId; + CachedWriteStream->SenderId = NetworkManager::LocalClientId; // TODO: use Job System when replicated objects count is large for (auto& e : CachedReplicationResult->_entries) { - ScriptingObject* obj = e.Object; - auto it = Objects.Find(obj->GetID()); - if (it.IsEnd()) - continue; - auto& item = it->Item; - - // Skip serialization of objects that none will receive - if (!isClient) - { - BuildCachedTargets(item, e.TargetClients); - if (CachedTargets.Count() == 0) - continue; - } - - if (item.AsNetworkObject) - item.AsNetworkObject->OnNetworkSerialize(); - - // Serialize object - stream->Initialize(); - const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true); - if (failed) - { - //NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString()); - continue; - } - const uint32 size = stream->GetPosition(); - if (size > MAX_uint16) - { - LOG(Error, "Too much data for object {} replication ({} bytes provided while limit is {}).", item.ToString(), size, MAX_uint16); - continue; - } - -#if USE_NETWORK_REPLICATOR_CACHE - // Process replication cache to skip sending object data if it didn't change - if (item.RepCache.Data.Length() == size && - item.RepCache.Mask == e.TargetClients && - Platform::MemoryCompare(item.RepCache.Data.Get(), stream->GetBuffer(), size) == 0) - { - continue; - } - item.RepCache.Mask = e.TargetClients; - item.RepCache.Data.Copy(stream->GetBuffer(), size); -#endif - // TODO: use Unreliable for dynamic objects that are replicated every frame? (eg. player state) - constexpr NetworkChannelType repChannel = NetworkChannelType::Reliable; - - // Send object to clients - NetworkMessageObjectReplicate msgData; - msgData.OwnerFrame = NetworkManager::Frame; - Guid objectId = item.ObjectId, parentId = item.ParentId; - { - // Remap local client object ids into server ids - IdsRemappingTable.KeyOf(objectId, &objectId); - IdsRemappingTable.KeyOf(parentId, &parentId); - } - NetworkMessage msg = peer->BeginSendMessage(); - msg.WriteStructure(msgData); - msg.WriteNetworkId(objectId); - msg.WriteNetworkId(parentId); - msg.WriteNetworkName(obj->GetType().Fullname); - NetworkMessageObjectReplicatePayload msgDataPayload; - msgDataPayload.DataSize = size; - const uint32 networkKeyIdWorstCaseSize = sizeof(uint32) + sizeof(Guid); - const uint32 msgMaxData = peer->Config.MessageSize - msg.Position - sizeof(NetworkMessageObjectReplicatePayload); - const uint32 partMaxData = peer->Config.MessageSize - sizeof(NetworkMessageObjectReplicatePart) - networkKeyIdWorstCaseSize; - uint32 partsCount = 1; - uint32 dataStart = 0; - uint32 msgDataSize = size; - if (size > msgMaxData) - { - // Send msgMaxData within first message - msgDataSize = msgMaxData; - dataStart += msgMaxData; - - // Send rest of the data in separate parts - partsCount += Math::DivideAndRoundUp(size - dataStart, partMaxData); - } - else - dataStart += size; - ASSERT(partsCount <= MAX_uint8); - msgDataPayload.PartsCount = partsCount; - msgDataPayload.PartSize = msgDataSize; - msg.WriteStructure(msgDataPayload); - msg.WriteBytes(stream->GetBuffer(), msgDataSize); - uint32 dataSize = msgDataSize, messageSize = msg.Length; - if (isClient) - peer->EndSendMessage(repChannel, msg); - else - peer->EndSendMessage(repChannel, msg, CachedTargets); - - // Send all other parts - for (uint32 partIndex = 1; partIndex < partsCount; partIndex++) - { - NetworkMessageObjectReplicatePart msgDataPart; - msgDataPart.OwnerFrame = msgData.OwnerFrame; - msgDataPart.DataSize = msgDataPayload.DataSize; - msgDataPart.PartsCount = msgDataPayload.PartsCount; - msgDataPart.PartStart = dataStart; - msgDataPart.PartSize = Math::Min(size - dataStart, partMaxData); - msg = peer->BeginSendMessage(); - msg.WriteStructure(msgDataPart); - msg.WriteNetworkId(objectId); - msg.WriteBytes(stream->GetBuffer() + msgDataPart.PartStart, msgDataPart.PartSize); - messageSize += msg.Length; - dataSize += msgDataPart.PartSize; - dataStart += msgDataPart.PartSize; - if (isClient) - peer->EndSendMessage(repChannel, msg); - else - peer->EndSendMessage(repChannel, msg, CachedTargets); - } - ASSERT_LOW_LAYER(dataStart == size); - -#if COMPILE_WITH_PROFILER - // Network stats recording - if (EnableProfiling) - { - const Pair name(obj->GetTypeHandle(), StringAnsiView::Empty); - auto& profileEvent = ProfilerEvents[name]; - profileEvent.Count++; - profileEvent.DataSize += dataSize; - profileEvent.MessageSize += messageSize; - profileEvent.Receivers += isClient ? 1 : CachedTargets.Count(); - } -#endif + SendReplication(e.Object, e.TargetClients); } } @@ -2009,70 +2117,7 @@ void NetworkInternal::NetworkReplicatorUpdate() PROFILE_CPU_NAMED("Rpc"); for (auto& e : RpcQueue) { - ScriptingObject* obj = e.Object.Get(); - if (!obj) - 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 - //NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Rpc {}::{} object ID={}", e.Name.First.ToString(), String(e.Name.Second), item.ToString()); - NetworkMessageObjectRpc msgData; - Guid msgObjectId = item.ObjectId; - Guid msgParentId = item.ParentId; - { - // Remap local client object ids into server ids - IdsRemappingTable.KeyOf(msgObjectId, &msgObjectId); - IdsRemappingTable.KeyOf(msgParentId, &msgParentId); - } - msgData.ArgsSize = (uint16)e.ArgsData.Length(); - NetworkMessage msg = peer->BeginSendMessage(); - msg.WriteStructure(msgData); - msg.WriteNetworkId(msgObjectId); - msg.WriteNetworkId(msgParentId); - msg.WriteNetworkName(obj->GetType().Fullname); - msg.WriteNetworkName(e.Name.First.GetType().Fullname); - msg.WriteNetworkName(e.Name.Second); - msg.WriteBytes(e.ArgsData.Get(), e.ArgsData.Length()); - uint32 dataSize = e.ArgsData.Length(), messageSize = msg.Length, receivers = 0; - NetworkChannelType channel = (NetworkChannelType)e.Info.Channel; - if (e.Info.Server && isClient) - { - // Client -> Server -#if USE_NETWORK_REPLICATOR_LOG - if (e.Targets.Length() != 0) - NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Server RPC '{}::{}' called with non-empty list of targets is not supported (only server will receive it)", e.Name.First.ToString(), e.Name.Second.ToString()); -#endif - peer->EndSendMessage(channel, msg); - receivers = 1; - } - else if (e.Info.Client && (isServer || isHost)) - { - // Server -> Client(s) - BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, e.Targets, NetworkManager::LocalClientId); - peer->EndSendMessage(channel, msg, CachedTargets); - receivers = CachedTargets.Count(); - } - -#if COMPILE_WITH_PROFILER - // Network stats recording - if (EnableProfiling && receivers) - { - auto& profileEvent = ProfilerEvents[e.Name]; - profileEvent.Count++; - profileEvent.DataSize += dataSize; - profileEvent.MessageSize += messageSize; - profileEvent.Receivers += receivers; - } -#endif + SendRpc(e); } RpcQueue.Clear(); } @@ -2085,7 +2130,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo { PROFILE_CPU(); NetworkMessageObjectReplicate msgData; - NetworkMessageObjectReplicatePayload msgDataPayload; + NetworkMessageObjectPartPayload msgDataPayload; Guid objectId, parentId; StringAnsiView objectTypeName; event.Message.ReadStructure(msgData); @@ -2095,7 +2140,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo event.Message.ReadStructure(msgDataPayload); ScopeLock lock(ObjectsLock); if (DespawnedObjects.Contains(objectId)) - return; // Skip replicating not-existing objects + return; // Skip replicating non-existing objects NetworkReplicatedObject* e = ResolveObject(objectId, parentId, objectTypeName); if (!e) return; @@ -2114,7 +2159,7 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo else { // Add to replication from multiple parts - ReplicateItem* replicateItem = AddObjectReplicateItem(event, msgData.OwnerFrame, msgDataPayload.PartsCount, msgDataPayload.DataSize, objectId, 0, msgDataPayload.PartSize, senderClientId); + PartsItem* replicateItem = AddObjectReplicateItem(event, msgData.OwnerFrame, msgDataPayload.PartsCount, msgDataPayload.DataSize, objectId, 0, msgDataPayload.PartSize, senderClientId); replicateItem->Object = e->Object; } } @@ -2122,13 +2167,13 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo void NetworkInternal::OnNetworkMessageObjectReplicatePart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) { PROFILE_CPU(); - NetworkMessageObjectReplicatePart msgData; + NetworkMessageObjectPart msgData; Guid objectId; event.Message.ReadStructure(msgData); event.Message.ReadNetworkId(objectId); ScopeLock lock(ObjectsLock); if (DespawnedObjects.Contains(objectId)) - return; // Skip replicating not-existing objects + return; // Skip replicating non-existing objects const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId; AddObjectReplicateItem(event, msgData.OwnerFrame, msgData.PartsCount, msgData.DataSize, objectId, msgData.PartStart, msgData.PartSize, senderClientId); @@ -2288,14 +2333,16 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie { PROFILE_CPU(); NetworkMessageObjectRpc msgData; - Guid msgObjectId, msgParentId; + NetworkMessageObjectPartPayload msgDataPayload; + Guid objectId, parentId; StringAnsiView objectTypeName, rpcTypeName, rpcName; event.Message.ReadStructure(msgData); - event.Message.ReadNetworkId(msgObjectId); - event.Message.ReadNetworkId(msgParentId); + event.Message.ReadNetworkId(objectId); + event.Message.ReadNetworkId(parentId); event.Message.ReadNetworkName(objectTypeName); event.Message.ReadNetworkName(rpcTypeName); event.Message.ReadNetworkName(rpcName); + event.Message.ReadStructure(msgDataPayload); ScopeLock lock(ObjectsLock); // Find RPC info @@ -2305,11 +2352,11 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie const NetworkRpcInfo* info = NetworkRpcInfo::RPCsTable.TryGet(name); if (!info) { - NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown RPC {}::{} for object {}", String(rpcTypeName), String(rpcName), msgObjectId); + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown RPC {}::{} for object {}", String(rpcTypeName), String(rpcName), objectId); return; } - NetworkReplicatedObject* e = ResolveObject(msgObjectId, msgParentId, objectTypeName); + NetworkReplicatedObject* e = ResolveObject(objectId, parentId, objectTypeName); if (e) { auto& item = *e; @@ -2329,18 +2376,50 @@ void NetworkInternal::OnNetworkMessageObjectRpc(NetworkEvent& event, NetworkClie return; } - // Setup message reading stream - if (CachedReadStream == nullptr) - CachedReadStream = New(); - NetworkStream* stream = CachedReadStream; - stream->SenderId = client ? client->ClientId : NetworkManager::ServerClientId; - stream->Initialize(event.Message.Buffer + event.Message.Position, msgData.ArgsSize); - - // Execute RPC - info->Execute(obj, stream, info->Tag); + const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId; + if (msgDataPayload.PartsCount == 1) + { + // Call RPC + InvokeObjectRpc(info, event.Message.Buffer + event.Message.Position, msgDataPayload.DataSize, senderClientId, obj); + } + else + { + // Add to RPC from multiple parts + PartsItem* rpcItem = AddObjectRpcItem(event, msgData.OwnerFrame, msgDataPayload.PartsCount, msgDataPayload.DataSize, objectId, 0, msgDataPayload.PartSize, senderClientId); + rpcItem->Object = e->Object; + rpcItem->Tag = info; + } } else if (info->Channel != static_cast(NetworkChannelType::Unreliable) && info->Channel != static_cast(NetworkChannelType::UnreliableOrdered)) { - NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown object {} RPC {}::{}", msgObjectId, String(rpcTypeName), String(rpcName)); + NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Unknown object {} RPC {}::{}", objectId, String(rpcTypeName), String(rpcName)); + } +} + +void NetworkInternal::OnNetworkMessageObjectRpcPart(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer) +{ + PROFILE_CPU(); + NetworkMessageObjectPart msgData; + Guid objectId; + event.Message.ReadStructure(msgData); + event.Message.ReadNetworkId(objectId); + ScopeLock lock(ObjectsLock); + if (DespawnedObjects.Contains(objectId)) + return; // Skip replicating non-existing objects + + const uint32 senderClientId = client ? client->ClientId : NetworkManager::ServerClientId; + PartsItem* rpcItem = AddObjectRpcItem(event, msgData.OwnerFrame, msgData.PartsCount, msgData.DataSize, objectId, msgData.PartStart, msgData.PartSize, senderClientId); + if (rpcItem && rpcItem->PartsLeft == 0) + { + // Got all parts so invoke RPC + ScriptingObject* obj = rpcItem->Object.Get(); + if (obj) + { + InvokeObjectRpc((const NetworkRpcInfo*)rpcItem->Tag, rpcItem->Data.Get(), rpcItem->Data.Count(), rpcItem->OwnerClientId, obj); + } + + // Remove item + int32 partIndex = (int32)((RpcParts.Get() - rpcItem) / sizeof(rpcItem)); + RpcParts.RemoveAt(partIndex); } } diff --git a/Source/Engine/Scripting/ScriptingType.h b/Source/Engine/Scripting/ScriptingType.h index b9735a5af..e1fb3dc04 100644 --- a/Source/Engine/Scripting/ScriptingType.h +++ b/Source/Engine/Scripting/ScriptingType.h @@ -5,6 +5,9 @@ #include "Types.h" #include "Engine/Core/Types/StringView.h" #include "Engine/Core/Types/Guid.h" +#if PLATFORM_ARCH_ARM64 +#include "Engine/Core/Core.h" +#endif class MMethod; class BinaryModule; diff --git a/Source/Engine/UI/GUI/Panels/DropPanel.cs b/Source/Engine/UI/GUI/Panels/DropPanel.cs index 0bfa799c2..de80f9fc5 100644 --- a/Source/Engine/UI/GUI/Panels/DropPanel.cs +++ b/Source/Engine/UI/GUI/Panels/DropPanel.cs @@ -104,13 +104,20 @@ namespace FlaxEngine.GUI /// /// Gets or sets a value indicating whether enable drop down icon drawing. /// - [EditorOrder(1)] + [EditorOrder(2)] public bool EnableDropDownIcon { get; set; } + /// + /// Get or sets a value indicating whether the panel can be opened or closed via the user interacting with the ui. + /// Changing the open/ closed state from code or the Properties panel will still work regardless. + /// + [EditorOrder(1)] + public bool CanOpenClose { get; set; } = true; + /// /// Gets or sets a value indicating whether to enable containment line drawing, /// - [EditorOrder(2)] + [EditorOrder(3)] public bool EnableContainmentLines { get; set; } = false; /// @@ -369,7 +376,7 @@ namespace FlaxEngine.GUI } // Header - var color = _mouseOverHeader ? HeaderColorMouseOver : HeaderColor; + var color = _mouseOverHeader && CanOpenClose ? HeaderColorMouseOver : HeaderColor; if (color.A > 0.0f) { Render2D.FillRectangle(new Rectangle(0, 0, Width, HeaderHeight), color); @@ -510,7 +517,7 @@ namespace FlaxEngine.GUI if (button == MouseButton.Left && _mouseButtonLeftDown) { _mouseButtonLeftDown = false; - if (_mouseOverHeader) + if (_mouseOverHeader && CanOpenClose) Toggle(); return true; } @@ -540,7 +547,7 @@ namespace FlaxEngine.GUI if (button == MouseButton.Left && _mouseButtonLeftDown) { _mouseButtonLeftDown = false; - if (_mouseOverHeader) + if (_mouseOverHeader && CanOpenClose) Toggle(); return true; } diff --git a/Source/Engine/UI/GUI/RenderOutputControl.cs b/Source/Engine/UI/GUI/RenderOutputControl.cs index 9025cbc04..8dc6707bf 100644 --- a/Source/Engine/UI/GUI/RenderOutputControl.cs +++ b/Source/Engine/UI/GUI/RenderOutputControl.cs @@ -180,7 +180,7 @@ namespace FlaxEngine.GUI } /// - public override void Draw() + public override void DrawSelf() { var bounds = new Rectangle(Float2.Zero, Size); @@ -205,21 +205,6 @@ namespace FlaxEngine.GUI Render2D.DrawTexture(buffer, bounds, color); else Render2D.FillRectangle(bounds, Color.Black); - - // Push clipping mask - if (ClipChildren) - { - GetDesireClientArea(out var clientArea); - Render2D.PushClip(ref clientArea); - } - - DrawChildren(); - - // Pop clipping mask - if (ClipChildren) - { - Render2D.PopClip(); - } } /// diff --git a/Source/Tools/Flax.Build/Flax.Build.csproj b/Source/Tools/Flax.Build/Flax.Build.csproj index d19d931ce..4b22d6924 100644 --- a/Source/Tools/Flax.Build/Flax.Build.csproj +++ b/Source/Tools/Flax.Build/Flax.Build.csproj @@ -37,22 +37,22 @@ - ..\..\..\Source\Platforms\DotNet\Ionic.Zip.Reduced.dll + ..\..\Platforms\DotNet\Ionic.Zip.Reduced.dll - ..\..\..\Source\Platforms\DotNet\System.Text.Encoding.CodePages.dll + ..\..\Platforms\DotNet\System.Text.Encoding.CodePages.dll - ..\..\..\Source\Platforms\DotNet\Mono.Cecil.dll + ..\..\Platforms\DotNet\Mono.Cecil.dll - ..\..\..\Source\Platforms\DotNet\Microsoft.VisualStudio.Setup.Configuration.Interop.dll + ..\..\Platforms\DotNet\Microsoft.VisualStudio.Setup.Configuration.Interop.dll - ..\..\..\Source\Platforms\DotNet\Microsoft.CodeAnalysis.CSharp.dll + ..\..\Platforms\DotNet\Microsoft.CodeAnalysis.CSharp.dll - ..\..\..\Source\Platforms\DotNet\Microsoft.CodeAnalysis.dll + ..\..\Platforms\DotNet\Microsoft.CodeAnalysis.dll diff --git a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchain.cs b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchain.cs index fddbdb3de..0c2d44c5d 100644 --- a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchain.cs @@ -19,7 +19,7 @@ namespace Flax.Build /// /// Specifies the minimum CPU architecture type to support (on x86/x64). /// - [CommandLine("winCpuArch", "", "Specifies the minimum CPU architecture type to support (om x86/x64).")] + [CommandLine("winCpuArch", "", "Specifies the minimum CPU architecture type to support (on x86/x64).")] public static CpuArchitecture WindowsCpuArch = CpuArchitecture.SSE4_2; // 99.78% support on PC according to Steam Hardware & Software Survey: September 2025 (https://store.steampowered.com/hwsurvey/) } } @@ -76,22 +76,27 @@ namespace Flax.Build.Platforms options.LinkEnv.InputLibraries.Add("oleaut32.lib"); options.LinkEnv.InputLibraries.Add("delayimp.lib"); - if (options.Architecture == TargetArchitecture.ARM64) + options.CompileEnv.CpuArchitecture = Configuration.WindowsCpuArch; + + if (options.Architecture == TargetArchitecture.x64) + { + if (_minVersion.Major <= 7 && options.CompileEnv.CpuArchitecture == CpuArchitecture.AVX2) + { + // Old Windows had lower support ratio for latest CPU features + options.CompileEnv.CpuArchitecture = CpuArchitecture.AVX; + } + if (_minVersion.Major >= 11 && options.CompileEnv.CpuArchitecture == CpuArchitecture.AVX) + { + // Windows 11 has hard requirement on SSE4.2 + options.CompileEnv.CpuArchitecture = CpuArchitecture.SSE4_2; + } + } + else if (options.Architecture == TargetArchitecture.ARM64) { options.CompileEnv.PreprocessorDefinitions.Add("USE_SOFT_INTRINSICS"); options.LinkEnv.InputLibraries.Add("softintrin.lib"); - } - - options.CompileEnv.CpuArchitecture = Configuration.WindowsCpuArch; - if (_minVersion.Major <= 7 && options.CompileEnv.CpuArchitecture == CpuArchitecture.AVX2) - { - // Old Windows had lower support ratio for latest CPU features - options.CompileEnv.CpuArchitecture = CpuArchitecture.AVX; - } - if (_minVersion.Major >= 11 && options.CompileEnv.CpuArchitecture == CpuArchitecture.AVX) - { - // Windows 11 has hard requirement on SSE4.2 - options.CompileEnv.CpuArchitecture = CpuArchitecture.SSE4_2; + if (options.CompileEnv.CpuArchitecture != CpuArchitecture.None) + options.CompileEnv.CpuArchitecture = CpuArchitecture.NEON; } }