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;
}
}