diff --git a/Flax.flaxproj b/Flax.flaxproj
index 5d926a87b..bb2e37a0b 100644
--- a/Flax.flaxproj
+++ b/Flax.flaxproj
@@ -4,10 +4,10 @@
"Major": 1,
"Minor": 11,
"Revision": 0,
- "Build": 6806
+ "Build": 6807
},
"Company": "Flax",
- "Copyright": "Copyright (c) 2012-2025 Wojciech Figat. All rights reserved.",
+ "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.",
"GameTarget": "FlaxGame",
"EditorTarget": "FlaxEditor",
"Configuration": {
diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
index 28593a7f5..02742b512 100644
--- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs
@@ -593,11 +593,12 @@ namespace FlaxEditor.CustomEditors.Editors
panel.Panel.Offsets = new Margin(7, 7, 0, 0);
panel.Panel.BackgroundColor = _background;
var elementType = ElementType;
+ var bindingAttr = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public;
bool single = elementType.IsPrimitive ||
elementType.Equals(new ScriptType(typeof(string))) ||
elementType.IsEnum ||
- (elementType.GetFields().Length == 1 && elementType.GetProperties().Length == 0) ||
- (elementType.GetProperties().Length == 1 && elementType.GetFields().Length == 0) ||
+ (elementType.GetFields(bindingAttr).Length == 1 && elementType.GetProperties(bindingAttr).Length == 0) ||
+ (elementType.GetProperties(bindingAttr).Length == 1 && elementType.GetFields(bindingAttr).Length == 0) ||
elementType.Equals(new ScriptType(typeof(JsonAsset))) ||
elementType.Equals(new ScriptType(typeof(SettingsBase)));
if (_cachedDropPanels == null)
diff --git a/Source/Editor/Gizmo/GizmoBase.cs b/Source/Editor/Gizmo/GizmoBase.cs
index 57cd62d79..80c3e5012 100644
--- a/Source/Editor/Gizmo/GizmoBase.cs
+++ b/Source/Editor/Gizmo/GizmoBase.cs
@@ -13,6 +13,7 @@ namespace FlaxEditor.Gizmo
public abstract class GizmoBase
{
private IGizmoOwner _owner;
+ private bool _visible = true;
///
/// Gets the gizmo owner.
@@ -34,6 +35,11 @@ namespace FlaxEditor.Gizmo
///
public virtual BoundingSphere FocusBounds => BoundingSphere.Empty;
+ ///
+ /// Gets or sets a value indicating whether this gizmo is visible.
+ ///
+ public bool Visible { get { return _visible; } set { _visible = value; } }
+
///
/// Initializes a new instance of the class.
///
diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs
index a759b7247..9b15c0c09 100644
--- a/Source/Editor/Options/InputOptions.cs
+++ b/Source/Editor/Options/InputOptions.cs
@@ -387,6 +387,14 @@ namespace FlaxEditor.Options
[EditorDisplay("Viewport"), EditorOrder(1760)]
public InputBinding ToggleOrthographic = new InputBinding(KeyboardKeys.NumpadDecimal);
+ [DefaultValue(typeof(InputBinding), "G")]
+ [EditorDisplay("Viewport"), EditorOrder(1770)]
+ public InputBinding ToggleGameView = new InputBinding(KeyboardKeys.G);
+
+ [DefaultValue(typeof(InputBinding), "P")]
+ [EditorDisplay("Viewport"), EditorOrder(1770)]
+ public InputBinding ToggleNavMeshVisibility = new InputBinding(KeyboardKeys.P);
+
#endregion
#region Debug Views
diff --git a/Source/Editor/Surface/Archetypes/Parameters.cs b/Source/Editor/Surface/Archetypes/Parameters.cs
index 3d74d2e10..3383e7662 100644
--- a/Source/Editor/Surface/Archetypes/Parameters.cs
+++ b/Source/Editor/Surface/Archetypes/Parameters.cs
@@ -1100,6 +1100,27 @@ namespace FlaxEditor.Surface.Archetypes
NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116)
}
},
+ new NodeArchetype
+ {
+ TypeID = 5,
+ Create = (id, context, arch, groupArch) => new SurfaceNodeParamsSet(id, context, arch, groupArch),
+ Title = "Set Parameter",
+ Description = "Parameter value setter invoked when the animation pose is evaluated (output pose comes from input)",
+ Flags = NodeFlags.AnimGraph,
+ Size = new Float2(140, 40),
+ DefaultValues = new object[]
+ {
+ Guid.Empty,
+ null
+ },
+ Elements = new[]
+ {
+ NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 0),
+ NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 2),
+ NodeElementArchetype.Factory.Input(1, string.Empty, true, ScriptType.Null, 1, 1),
+ NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116)
+ }
+ },
};
}
}
diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs
index cbc041c09..056987e52 100644
--- a/Source/Editor/Surface/VisjectSurface.Input.cs
+++ b/Source/Editor/Surface/VisjectSurface.Input.cs
@@ -724,7 +724,12 @@ namespace FlaxEditor.Surface
if (HasNodesSelection)
{
- var keyMoveRange = 50;
+ var keyMoveDelta = 50;
+ bool altDown = RootWindow.GetKey(KeyboardKeys.Alt);
+ bool shiftDown = RootWindow.GetKey(KeyboardKeys.Shift);
+ if (altDown || shiftDown)
+ keyMoveDelta = shiftDown ? 10 : 25;
+
switch (key)
{
case KeyboardKeys.Backspace:
@@ -752,17 +757,18 @@ namespace FlaxEditor.Surface
Box selectedBox = GetSelectedBox(SelectedNodes);
if (selectedBox != null)
{
- Box toSelect = (key == KeyboardKeys.ArrowUp) ? selectedBox?.ParentNode.GetPreviousBox(selectedBox) : selectedBox?.ParentNode.GetNextBox(selectedBox);
- if (toSelect != null && toSelect.IsOutput == selectedBox.IsOutput)
- {
- Select(toSelect.ParentNode);
- toSelect.ParentNode.SelectBox(toSelect);
- }
+ int delta = key == KeyboardKeys.ArrowDown ? 1 : -1;
+ List boxes = selectedBox.ParentNode.GetBoxes().FindAll(b => b.IsOutput == selectedBox.IsOutput);
+ int selectedIndex = boxes.IndexOf(selectedBox);
+ Box toSelect = boxes[Mathf.Wrap(selectedIndex + delta, 0, boxes.Count - 1)];
+
+ Select(toSelect.ParentNode);
+ toSelect.ParentNode.SelectBox(toSelect);
}
else if (!IsMovingSelection && CanEdit)
{
// Move selected nodes
- var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveRange : keyMoveRange);
+ var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveDelta : keyMoveDelta);
MoveSelectedNodes(delta);
}
return true;
@@ -775,12 +781,8 @@ namespace FlaxEditor.Surface
if (selectedBox != null)
{
Box toSelect = null;
- if ((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput))
+ if (((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput)) && selectedBox.HasAnyConnection)
{
- if (_selectedConnectionIndex < 0 || _selectedConnectionIndex >= selectedBox.Connections.Count)
- {
- _selectedConnectionIndex = 0;
- }
toSelect = selectedBox.Connections[_selectedConnectionIndex];
}
else
@@ -808,7 +810,7 @@ namespace FlaxEditor.Surface
else if (!IsMovingSelection && CanEdit)
{
// Move selected nodes
- var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveRange : keyMoveRange, 0);
+ var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveDelta : keyMoveDelta, 0);
MoveSelectedNodes(delta);
}
return true;
@@ -824,13 +826,9 @@ namespace FlaxEditor.Surface
return true;
if (Root.GetKey(KeyboardKeys.Shift))
- {
_selectedConnectionIndex = ((_selectedConnectionIndex - 1) % connectionCount + connectionCount) % connectionCount;
- }
else
- {
_selectedConnectionIndex = (_selectedConnectionIndex + 1) % connectionCount;
- }
return true;
}
}
diff --git a/Source/Editor/Undo/Actions/ParentActorsAction.cs b/Source/Editor/Undo/Actions/ParentActorsAction.cs
index 312ce578d..aed85cb3c 100644
--- a/Source/Editor/Undo/Actions/ParentActorsAction.cs
+++ b/Source/Editor/Undo/Actions/ParentActorsAction.cs
@@ -134,7 +134,8 @@ namespace FlaxEditor.Actions
var obj = Object.Find(ref item.ID);
if (obj != null)
{
- scenes.Add(obj.Parent.Scene);
+ if (obj.Parent != null)
+ scenes.Add(obj.Parent.Scene);
if (obj is Actor actor)
actor.SetParent(newParent, _worldPositionsStays, true);
else
diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs
index 04563e1cd..82789e7e5 100644
--- a/Source/Editor/Viewport/EditorViewport.cs
+++ b/Source/Editor/Viewport/EditorViewport.cs
@@ -590,7 +590,7 @@ namespace FlaxEditor.Viewport
_cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), _editor.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth)
{
Tag = this,
- TooltipText = "Camera Settings",
+ TooltipText = "Camera Settings.",
Parent = _cameraWidget
};
_cameraWidget.Parent = this;
@@ -599,7 +599,7 @@ namespace FlaxEditor.Viewport
_orthographicModeButton = new ViewportWidgetButton(string.Empty, _editor.Icons.CamSpeed32, null, true)
{
Checked = !_isOrtho,
- TooltipText = "Toggle Orthographic/Perspective Mode",
+ TooltipText = "Toggle Orthographic/Perspective Mode.",
Parent = _cameraWidget
};
_orthographicModeButton.Toggled += OnOrthographicModeToggled;
@@ -838,7 +838,7 @@ namespace FlaxEditor.Viewport
ViewWidgetButtonMenu = new ContextMenu();
var viewModeButton = new ViewportWidgetButton("View", SpriteHandle.Invalid, ViewWidgetButtonMenu)
{
- TooltipText = "View properties",
+ TooltipText = "View properties.",
Parent = viewMode
};
viewMode.Parent = this;
@@ -869,8 +869,10 @@ namespace FlaxEditor.Viewport
{
}
});
- viewLayers.AddButton("Reset layers", () => Task.ViewLayersMask = LayersMask.Default).Icon = _editor.Icons.Rotate32;
- viewLayers.AddButton("Disable layers", () => Task.ViewLayersMask = new LayersMask(0));
+ viewLayers.AddButton("Reset layers", () => Task.ViewLayersMask = LayersMask.Default).Icon = Editor.Instance.Icons.Rotate32;
+ viewLayers.AddSeparator();
+ viewLayers.AddButton("Enable all", () => Task.ViewLayersMask = new LayersMask(-1)).Icon = Editor.Instance.Icons.CheckBoxTick12;
+ viewLayers.AddButton("Disable all", () => Task.ViewLayersMask = new LayersMask(0)).Icon = Editor.Instance.Icons.Cross12;
viewLayers.AddSeparator();
var layers = LayersAndTagsSettings.GetCurrentLayers();
if (layers != null && layers.Length > 0)
@@ -910,8 +912,10 @@ namespace FlaxEditor.Viewport
{
}
});
- viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = _editor.Icons.Rotate32;
- viewFlags.AddButton("Disable flags", () => Task.ViewFlags = ViewFlags.None);
+ viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = Editor.Instance.Icons.Rotate32;
+ viewFlags.AddSeparator();
+ viewFlags.AddButton("Enable all", () => Task.ViewFlags = ViewFlags.All).Icon = Editor.Instance.Icons.CheckBoxTick12;
+ viewFlags.AddButton("Disable all", () => Task.ViewFlags = ViewFlags.None).Icon = Editor.Instance.Icons.Cross12;
viewFlags.AddSeparator();
for (int i = 0; i < ViewFlagsValues.Length; i++)
{
diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
index 743797b68..1b3078051 100644
--- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
@@ -25,6 +25,7 @@ namespace FlaxEditor.Viewport
private readonly Editor _editor;
private readonly ContextMenuButton _showGridButton;
private readonly ContextMenuButton _showNavigationButton;
+ private readonly ContextMenuButton _toggleGameViewButton;
private SelectionOutline _customSelectionOutline;
///
@@ -108,6 +109,13 @@ namespace FlaxEditor.Viewport
private EditorSpritesRenderer _editorSpritesRenderer;
private ViewportRubberBandSelector _rubberBandSelector;
+ private bool _gameViewActive;
+ private ViewFlags _preGameViewFlags;
+ private ViewMode _preGameViewViewMode;
+ private bool _gameViewWasGridShown;
+ private bool _gameViewWasFpsCounterShown;
+ private bool _gameViewWasNagivationShown;
+
///
/// Drag and drop handlers
///
@@ -185,6 +193,7 @@ namespace FlaxEditor.Viewport
: base(Object.New(), editor.Undo, editor.Scene.Root)
{
_editor = editor;
+ var inputOptions = _editor.Options.Options.Input;
DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem);
// Prepare rendering task
@@ -232,9 +241,14 @@ namespace FlaxEditor.Viewport
_showGridButton.CloseMenuOnClick = false;
// Show navigation widget
- _showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", () => ShowNavigation = !ShowNavigation);
+ _showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", inputOptions.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation);
_showNavigationButton.CloseMenuOnClick = false;
+ // Game View
+ ViewWidgetButtonMenu.AddSeparator();
+ _toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView);
+ _toggleGameViewButton.CloseMenuOnClick = false;
+
// Create camera widget
ViewWidgetButtonMenu.AddSeparator();
ViewWidgetButtonMenu.AddButton("Create camera here", CreateCameraAtView);
@@ -259,6 +273,10 @@ namespace FlaxEditor.Viewport
InputActions.Add(options => options.FocusSelection, FocusSelection);
InputActions.Add(options => options.RotateSelection, RotateSelection);
InputActions.Add(options => options.Delete, _editor.SceneEditing.Delete);
+ InputActions.Add(options => options.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation);
+
+ // Game View
+ InputActions.Add(options => options.ToggleGameView, ToggleGameView);
}
///
@@ -373,9 +391,12 @@ namespace FlaxEditor.Viewport
public void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth)
{
// Draw gizmos
- for (int i = 0; i < Gizmos.Count; i++)
+ foreach (var gizmo in Gizmos)
{
- Gizmos[i].Draw(ref renderContext);
+ if (gizmo.Visible)
+ {
+ gizmo.Draw(ref renderContext);
+ }
}
// Draw selected objects debug shapes and visuals
@@ -481,6 +502,36 @@ namespace FlaxEditor.Viewport
TransformGizmo.EndTransforming();
}
+ ///
+ /// Toggles game view view mode on or off.
+ ///
+ public void ToggleGameView()
+ {
+ if (!_gameViewActive)
+ {
+ // Cache flags & values
+ _preGameViewFlags = Task.ViewFlags;
+ _preGameViewViewMode = Task.ViewMode;
+ _gameViewWasGridShown = Grid.Enabled;
+ _gameViewWasFpsCounterShown = ShowFpsCounter;
+ _gameViewWasNagivationShown = ShowNavigation;
+ }
+
+ // Set flags & values
+ Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame;
+ Task.ViewMode = _gameViewActive ? _preGameViewViewMode : ViewMode.Default;
+ ShowFpsCounter = _gameViewActive ? _gameViewWasFpsCounterShown : false;
+ ShowNavigation = _gameViewActive ? _gameViewWasNagivationShown : false;
+ Grid.Enabled = _gameViewActive ? _gameViewWasGridShown : false;
+
+ _gameViewActive = !_gameViewActive;
+
+ TransformGizmo.Visible = !_gameViewActive;
+ SelectionOutline.ShowSelectionOutline = !_gameViewActive;
+
+ _toggleGameViewButton.Icon = _gameViewActive ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
+ }
+
///
public override void OnLostFocus()
{
diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs
index a22b4042f..7f2b1a471 100644
--- a/Source/Editor/Viewport/PrefabWindowViewport.cs
+++ b/Source/Editor/Viewport/PrefabWindowViewport.cs
@@ -720,9 +720,12 @@ namespace FlaxEditor.Viewport
public override void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth)
{
// Draw gizmos
- for (int i = 0; i < Gizmos.Count; i++)
+ foreach (var gizmo in Gizmos)
{
- Gizmos[i].Draw(ref renderContext);
+ if (gizmo.Visible)
+ {
+ gizmo.Draw(ref renderContext);
+ }
}
base.DrawEditorPrimitives(context, ref renderContext, target, targetDepth);
diff --git a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs
index 7e809d968..12142219c 100644
--- a/Source/Editor/Windows/Assets/AnimationGraphWindow.cs
+++ b/Source/Editor/Windows/Assets/AnimationGraphWindow.cs
@@ -99,7 +99,14 @@ namespace FlaxEditor.Windows.Assets
Window = window;
var surfaceParam = window.Surface.GetParameter(BaseModelId);
if (surfaceParam != null)
- BaseModel = FlaxEngine.Content.LoadAsync((Guid)surfaceParam.Value);
+ {
+ if (surfaceParam.Value is Guid asGuid)
+ BaseModel = FlaxEngine.Content.LoadAsync(asGuid);
+ else if (surfaceParam.Value is SkinnedModel asModel)
+ BaseModel = asModel;
+ else
+ BaseModel = null;
+ }
else
BaseModel = window.PreviewActor.GetParameterValue(BaseModelId) as SkinnedModel;
}
diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs
index 41fdfde65..268e18503 100644
--- a/Source/Editor/Windows/Assets/AnimationWindow.cs
+++ b/Source/Editor/Windows/Assets/AnimationWindow.cs
@@ -2,7 +2,6 @@
using System;
using System.Globalization;
-using System.IO;
using System.Reflection;
using System.Xml;
using FlaxEditor.Content;
@@ -25,7 +24,7 @@ namespace FlaxEditor.Windows.Assets
///
///
///
- public sealed class AnimationWindow : AssetEditorWindowBase
+ public sealed class AnimationWindow : ClonedAssetEditorWindowBase
{
private sealed class Preview : AnimationPreview
{
@@ -255,6 +254,7 @@ namespace FlaxEditor.Windows.Assets
private bool _isWaitingForTimelineLoad;
private SkinnedModel _initialPreviewModel, _initialBaseModel;
private float _initialPanel2Splitter = 0.6f;
+ private bool _timelineIsDirty;
///
/// Gets the animation timeline editor.
@@ -295,7 +295,7 @@ namespace FlaxEditor.Windows.Assets
Parent = _panel1.Panel1,
Enabled = false
};
- _timeline.Modified += MarkAsEdited;
+ _timeline.Modified += OnTimelineModified;
_timeline.SetNoTracksText("Loading...");
// Asset properties
@@ -321,11 +321,31 @@ namespace FlaxEditor.Windows.Assets
{
MarkAsEdited();
UpdateToolstrip();
+ _propertiesPresenter.BuildLayout();
+ }
+
+ private void OnTimelineModified()
+ {
+ _timelineIsDirty = true;
+ MarkAsEdited();
+ }
+
+ private bool RefreshTempAsset()
+ {
+ if (_asset == null || _isWaitingForTimelineLoad)
+ return true;
+ if (_timeline.IsModified)
+ {
+ _timeline.Save(_asset);
+ }
+ _propertiesPresenter.BuildLayoutOnUpdate();
+
+ return false;
}
private string GetPreviewModelCacheName()
{
- return _asset.ID + ".PreviewModel";
+ return _item.ID + ".PreviewModel";
}
///
@@ -361,7 +381,11 @@ namespace FlaxEditor.Windows.Assets
if (!IsEdited)
return;
- _timeline.Save(_asset);
+ if (RefreshTempAsset())
+ return;
+ if (SaveToOriginal())
+ return;
+
ClearEditedFlag();
_item.RefreshThumbnail();
}
@@ -414,10 +438,18 @@ namespace FlaxEditor.Windows.Assets
{
base.Update(deltaTime);
+ // Check if temporary asset need to be updated
+ if (_timelineIsDirty)
+ {
+ _timelineIsDirty = false;
+ RefreshTempAsset();
+ }
+
+ // Check if need to load timeline
if (_isWaitingForTimelineLoad && _asset.IsLoaded)
{
_isWaitingForTimelineLoad = false;
- _timeline._id = _asset.ID;
+ _timeline._id = _item.ID;
_timeline.Load(_asset);
_undo.Clear();
_timeline.Enabled = true;
diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
index 392cc896a..75fa87dfe 100644
--- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
+++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
@@ -70,6 +70,13 @@ namespace FlaxEditor.Windows.Assets
return;
var nodes = proxy.Asset.Nodes;
var bones = proxy.Asset.Bones;
+ var blendShapes = proxy.Asset.BlendShapes;
+
+ // Info
+ {
+ var group = layout.Group("Info");
+ group.Label($"Nodes: {nodes.Length}\nBones: {bones.Length}\nBlend Shapes: {blendShapes.Length}").AddCopyContextMenu().Label.Height *= 2.5f;
+ }
// Skeleton Bones
{
@@ -109,7 +116,6 @@ namespace FlaxEditor.Windows.Assets
}
// Blend Shapes
- var blendShapes = proxy.Asset.BlendShapes;
if (blendShapes.Length != 0)
{
var group = layout.Group("Blend Shapes");
diff --git a/Source/Editor/Windows/EditGameWindow.cs b/Source/Editor/Windows/EditGameWindow.cs
index 888dd4250..dbeab28b1 100644
--- a/Source/Editor/Windows/EditGameWindow.cs
+++ b/Source/Editor/Windows/EditGameWindow.cs
@@ -429,6 +429,7 @@ namespace FlaxEditor.Windows
writer.WriteAttributeString("FarPlane", Viewport.FarPlane.ToString());
writer.WriteAttributeString("FieldOfView", Viewport.FieldOfView.ToString());
writer.WriteAttributeString("MovementSpeed", Viewport.MovementSpeed.ToString());
+ writer.WriteAttributeString("ViewportIconsScale", ViewportIconsRenderer.Scale.ToString());
writer.WriteAttributeString("OrthographicScale", Viewport.OrthographicScale.ToString());
writer.WriteAttributeString("UseOrthographicProjection", Viewport.UseOrthographicProjection.ToString());
writer.WriteAttributeString("ViewFlags", ((ulong)Viewport.Task.View.Flags).ToString());
@@ -439,31 +440,24 @@ namespace FlaxEditor.Windows
{
if (bool.TryParse(node.GetAttribute("GridEnabled"), out bool value1))
Viewport.Grid.Enabled = value1;
-
if (bool.TryParse(node.GetAttribute("ShowFpsCounter"), out value1))
Viewport.ShowFpsCounter = value1;
-
if (bool.TryParse(node.GetAttribute("ShowNavigation"), out value1))
Viewport.ShowNavigation = value1;
-
if (float.TryParse(node.GetAttribute("NearPlane"), out float value2))
Viewport.NearPlane = value2;
-
if (float.TryParse(node.GetAttribute("FarPlane"), out value2))
Viewport.FarPlane = value2;
-
if (float.TryParse(node.GetAttribute("FieldOfView"), out value2))
Viewport.FieldOfView = value2;
-
if (float.TryParse(node.GetAttribute("MovementSpeed"), out value2))
Viewport.MovementSpeed = value2;
-
+ if (float.TryParse(node.GetAttribute("ViewportIconsScale"), out value2))
+ ViewportIconsRenderer.Scale = value2;
if (float.TryParse(node.GetAttribute("OrthographicScale"), out value2))
Viewport.OrthographicScale = value2;
-
if (bool.TryParse(node.GetAttribute("UseOrthographicProjection"), out value1))
Viewport.UseOrthographicProjection = value1;
-
if (ulong.TryParse(node.GetAttribute("ViewFlags"), out ulong value3))
Viewport.Task.ViewFlags = (ViewFlags)value3;
diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp
index e99f53b8f..3c2630b51 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.cpp
@@ -336,11 +336,13 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
SkeletonData* animResultSkeleton = &skeleton;
// Retarget animation when using output pose from other skeleton
- AnimGraphImpulse retargetNodes;
if (_graph.BaseModel != data.NodesSkeleton)
{
ANIM_GRAPH_PROFILE_EVENT("Retarget");
auto& targetSkeleton = data.NodesSkeleton->Skeleton;
+ if (context.PoseCacheSize == context.PoseCache.Count())
+ context.PoseCache.AddOne();
+ auto& retargetNodes = context.PoseCache[context.PoseCacheSize++];
retargetNodes = *animResult;
retargetNodes.Nodes.Resize(targetSkeleton.Nodes.Count());
Transform* targetNodes = retargetNodes.Nodes.Get();
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index 1e4444af0..7eb8d32d6 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -109,86 +109,84 @@ namespace
nodes->RootMotion.Orientation.Normalize();
}
}
-
- Matrix ComputeWorldMatrixRecursive(const SkeletonData& skeleton, int32 index, Matrix localMatrix)
- {
- const auto& node = skeleton.Nodes[index];
- index = node.ParentIndex;
- while (index != -1)
- {
- const auto& parent = skeleton.Nodes[index];
- localMatrix *= parent.LocalTransform.GetWorld();
- index = parent.ParentIndex;
- }
- return localMatrix;
- }
-
- Matrix ComputeInverseParentMatrixRecursive(const SkeletonData& skeleton, int32 index)
- {
- Matrix inverseParentMatrix = Matrix::Identity;
- const auto& node = skeleton.Nodes[index];
- if (node.ParentIndex != -1)
- {
- inverseParentMatrix = ComputeWorldMatrixRecursive(skeleton, index, inverseParentMatrix);
- inverseParentMatrix = Matrix::Invert(inverseParentMatrix);
- }
- return inverseParentMatrix;
- }
}
-void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping, Transform& node, int32 targetIndex)
+// Utility for retargeting animation poses between skeletons.
+struct Retargeting
{
- // sourceSkeleton - skeleton of Anim Graph (Base Locomotion pack)
- // targetSkeleton - visual mesh skeleton (City Characters pack)
- // target - anim graph input/output transformation of that node
- const auto& targetNode = targetSkeleton.Nodes[targetIndex];
- const int32 sourceIndex = sourceMapping.NodesMapping[targetIndex];
- if (sourceIndex == -1)
+private:
+ const Matrix* _sourcePosePtr, * _targetPosePtr;
+ const SkeletonData* _sourceSkeleton, *_targetSkeleton;
+ const SkinnedModel::SkeletonMapping* _sourceMapping;
+
+public:
+ void Init(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping)
{
- // Use T-pose
- node = targetNode.LocalTransform;
- return;
+ ASSERT_LOW_LAYER(targetSkeleton.Nodes.Count() == sourceMapping.NodesMapping.Length());
+
+ // Cache world-space poses for source and target skeletons to avoid redundant calculations during retargeting
+ _sourcePosePtr = sourceSkeleton.GetNodesPose().Get();
+ _targetPosePtr = targetSkeleton.GetNodesPose().Get();
+
+ _sourceSkeleton = &sourceSkeleton;
+ _targetSkeleton = &targetSkeleton;
+ _sourceMapping = &sourceMapping;
}
- const auto& sourceNode = sourceSkeleton.Nodes[sourceIndex];
- // [Reference: https://wickedengine.net/2022/09/animation-retargeting/comment-page-1/]
-
- // Calculate T-Pose of source node, target node and target parent node
- Matrix bindMatrix = ComputeWorldMatrixRecursive(sourceSkeleton, sourceIndex, sourceNode.LocalTransform.GetWorld());
- Matrix inverseBindMatrix = Matrix::Invert(bindMatrix);
- Matrix targetMatrix = ComputeWorldMatrixRecursive(targetSkeleton, targetIndex, targetNode.LocalTransform.GetWorld());
- Matrix inverseParentMatrix = ComputeInverseParentMatrixRecursive(targetSkeleton, targetIndex);
-
- // Target node animation is world-space difference of the animated source node inside the target's parent node world-space
- Matrix localMatrix = inverseBindMatrix * ComputeWorldMatrixRecursive(sourceSkeleton, sourceIndex, node.GetWorld());
- localMatrix = targetMatrix * localMatrix * inverseParentMatrix;
-
- // Extract local node transformation
- localMatrix.Decompose(node);
-}
-
-void RetargetSkeletonPose(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, const Transform* sourceNodes, Transform* targetNodes)
-{
- // TODO: cache source and target skeletons world-space poses for faster retargeting (use some pooled memory)
- ASSERT_LOW_LAYER(targetSkeleton.Nodes.Count() == mapping.NodesMapping.Length());
- for (int32 targetIndex = 0; targetIndex < targetSkeleton.Nodes.Count(); targetIndex++)
+ void RetargetNode(const Transform& source, Transform& target, int32 sourceIndex, int32 targetIndex)
{
- auto& targetNode = targetSkeleton.Nodes.Get()[targetIndex];
- const int32 sourceIndex = mapping.NodesMapping.Get()[targetIndex];
- Transform node;
+ // sourceSkeleton - skeleton of Anim Graph
+ // targetSkeleton - visual mesh skeleton
+ // target - anim graph input/output transformation of that node
+ const SkeletonNode& targetNode = _targetSkeleton->Nodes.Get()[targetIndex];
if (sourceIndex == -1)
{
// Use T-pose
- node = targetNode.LocalTransform;
+ target = targetNode.LocalTransform;
}
else
{
- // Retarget
- node = sourceNodes[sourceIndex];
- RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, targetIndex);
+ // [Reference: https://wickedengine.net/2022/09/animation-retargeting/comment-page-1/]
+
+ // Calculate T-Pose of source node, target node and target parent node
+ const Matrix* sourcePosePtr = _sourcePosePtr;
+ const Matrix* targetPosePtr = _targetPosePtr;
+ const Matrix& bindMatrix = sourcePosePtr[sourceIndex];
+ const Matrix& targetMatrix = targetPosePtr[targetIndex];
+ Matrix inverseParentMatrix;
+ if (targetNode.ParentIndex != -1)
+ Matrix::Invert(targetPosePtr[targetNode.ParentIndex], inverseParentMatrix);
+ else
+ inverseParentMatrix = Matrix::Identity;
+
+ // Target node animation is world-space difference of the animated source node inside the target's parent node world-space
+ const SkeletonNode& sourceNode = _sourceSkeleton->Nodes.Get()[sourceIndex];
+ Matrix localMatrix = source.GetWorld();
+ if (sourceNode.ParentIndex != -1)
+ localMatrix = localMatrix * sourcePosePtr[sourceNode.ParentIndex];
+ localMatrix = Matrix::Invert(bindMatrix) * localMatrix;
+ localMatrix = targetMatrix * localMatrix * inverseParentMatrix;
+
+ // Extract local node transformation
+ localMatrix.Decompose(target);
}
- targetNodes[targetIndex] = node;
}
+
+ FORCE_INLINE void RetargetPose(const Transform* sourceNodes, Transform* targetNodes)
+ {
+ for (int32 targetIndex = 0; targetIndex < _targetSkeleton->Nodes.Count(); targetIndex++)
+ {
+ const int32 sourceIndex = _sourceMapping->NodesMapping.Get()[targetIndex];
+ RetargetNode(sourceNodes[sourceIndex], targetNodes[targetIndex], sourceIndex, targetIndex);
+ }
+ }
+};
+
+void RetargetSkeletonPose(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, const Transform* sourceNodes, Transform* targetNodes)
+{
+ Retargeting retargeting;
+ retargeting.Init(sourceSkeleton, targetSkeleton, mapping);
+ retargeting.RetargetPose(sourceNodes, targetNodes);
}
AnimGraphTraceEvent& AnimGraphContext::AddTraceEvent(const AnimGraphNode* node)
@@ -431,9 +429,13 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
const bool weighted = weight < 1.0f;
const bool retarget = mapping.SourceSkeleton && mapping.SourceSkeleton != mapping.TargetSkeleton;
const auto emptyNodes = GetEmptyNodes();
+ Retargeting retargeting;
SkinnedModel::SkeletonMapping sourceMapping;
if (retarget)
+ {
sourceMapping = _graph.BaseModel->GetSkeletonMapping(mapping.SourceSkeleton);
+ retargeting.Init(mapping.SourceSkeleton->Skeleton, mapping.TargetSkeleton->Skeleton, mapping);
+ }
for (int32 nodeIndex = 0; nodeIndex < nodes->Nodes.Count(); nodeIndex++)
{
const int32 nodeToChannel = mapping.NodesMapping[nodeIndex];
@@ -447,7 +449,8 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
// Optionally retarget animation into the skeleton used by the Anim Graph
if (retarget)
{
- RetargetSkeletonNode(mapping.SourceSkeleton->Skeleton, mapping.TargetSkeleton->Skeleton, sourceMapping, srcNode, nodeIndex);
+ const int32 sourceIndex = sourceMapping.NodesMapping[nodeIndex];
+ retargeting.RetargetNode(srcNode, srcNode, sourceIndex, nodeIndex);
}
// Mark node as used
@@ -958,6 +961,21 @@ void AnimGraphExecutor::ProcessGroupParameters(Box* box, Node* node, Value& valu
}
break;
}
+ // Set Parameter
+ case 5:
+ {
+ // Set parameter value
+ int32 paramIndex;
+ const auto param = _graph.GetParameter((Guid)node->Values[0], paramIndex);
+ if (param)
+ {
+ context.Data->Parameters[paramIndex].Value = tryGetValue(node->GetBox(1), 1, Value::Null);
+ }
+
+ // Pass over the pose
+ value = tryGetValue(node->GetBox(2), Value::Null);
+ break;
+ }
default:
break;
}
diff --git a/Source/Engine/Audio/AudioClip.cpp b/Source/Engine/Audio/AudioClip.cpp
index 336ddb7bd..4e258e81a 100644
--- a/Source/Engine/Audio/AudioClip.cpp
+++ b/Source/Engine/Audio/AudioClip.cpp
@@ -132,9 +132,7 @@ int32 AudioClip::GetFirstBufferIndex(float time, float& offset) const
if (_buffersStartTimes[i + 1] > time)
{
offset = time - _buffersStartTimes[i];
-#if BUILD_DEBUG
- ASSERT(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f);
-#endif
+ ASSERT_LOW_LAYER(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f);
return i;
}
}
diff --git a/Source/Engine/Audio/AudioClip.h b/Source/Engine/Audio/AudioClip.h
index d35bd18fc..ac30b4c85 100644
--- a/Source/Engine/Audio/AudioClip.h
+++ b/Source/Engine/Audio/AudioClip.h
@@ -18,6 +18,7 @@ class AudioSource;
API_CLASS(NoSpawn) class FLAXENGINE_API AudioClip : public BinaryAsset, public StreamableResource
{
DECLARE_BINARY_ASSET_HEADER(AudioClip, 2);
+ friend class AudioBackendOAL;
public:
///
diff --git a/Source/Engine/Audio/AudioListener.cpp b/Source/Engine/Audio/AudioListener.cpp
index 9c5898b4a..3151abafd 100644
--- a/Source/Engine/Audio/AudioListener.cpp
+++ b/Source/Engine/Audio/AudioListener.cpp
@@ -37,15 +37,17 @@ void AudioListener::OnEnable()
{
_prevPos = GetPosition();
_velocity = Vector3::Zero;
+
+ ASSERT(!Audio::Listeners.Contains(this));
if (Audio::Listeners.Count() >= AUDIO_MAX_LISTENERS)
{
- LOG(Error, "Unsupported amount of the audio listeners!");
+ if IF_CONSTEXPR (AUDIO_MAX_LISTENERS == 1)
+ LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time.");
+ else
+ LOG(Warning, "Too many Audio Listener active.");
}
else
{
- ASSERT(!Audio::Listeners.Contains(this));
- if (Audio::Listeners.Count() > 0)
- LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time.");
Audio::Listeners.Add(this);
AudioBackend::Listener::Reset();
AudioBackend::Listener::TransformChanged(GetPosition(), GetOrientation());
diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
index 86e828028..715ede74f 100644
--- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
+++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
@@ -14,6 +14,9 @@
#include "Engine/Audio/AudioListener.h"
#include "Engine/Audio/AudioSource.h"
#include "Engine/Audio/AudioSettings.h"
+#include "Engine/Content/Content.h"
+#include "Engine/Level/Level.h"
+#include "Engine/Video/VideoPlayer.h"
// Include OpenAL library
// Source: https://github.com/kcat/openal-soft
@@ -73,6 +76,7 @@ namespace ALC
ALCdevice* Device = nullptr;
ALCcontext* Context = nullptr;
AudioBackend::FeatureFlags Features = AudioBackend::FeatureFlags::None;
+ bool Inited = false;
CriticalSection Locker;
Dictionary SourcesData;
@@ -164,12 +168,9 @@ namespace ALC
float Time;
};
- void RebuildContext(const Array& states)
+ void RebuildContext()
{
- LOG(Info, "Rebuilding audio contexts");
-
ClearContext();
-
if (Device == nullptr)
return;
@@ -182,10 +183,16 @@ namespace ALC
Context = alcCreateContext(Device, attrList);
alcMakeContextCurrent(Context);
-
+ }
+
+ void RebuildListeners()
+ {
for (AudioListener* listener : Audio::Listeners)
Listener::Rebuild(listener);
-
+ }
+
+ void RebuildSources(const Array& states)
+ {
for (int32 i = 0; i < states.Count(); i++)
{
AudioSource* source = Audio::Sources[i];
@@ -205,6 +212,13 @@ namespace ALC
}
}
+ void RebuildContext(const Array& states)
+ {
+ RebuildContext();
+ RebuildListeners();
+ RebuildSources(states);
+ }
+
void RebuildContext(bool isChangingDevice)
{
Array states;
@@ -400,7 +414,7 @@ void AudioBackendOAL::Source_IsLoopingChanged(uint32 sourceID, bool loop)
void AudioBackendOAL::Source_SpatialSetupChanged(uint32 sourceID, bool spatial, float attenuation, float minDistance, float doppler)
{
ALC::Locker.Lock();
- const bool pan = ALC::SourcesData[sourceID].Spatial;
+ const float pan = ALC::SourcesData[sourceID].Pan;
ALC::Locker.Unlock();
if (spatial)
{
@@ -629,6 +643,7 @@ AudioBackend::FeatureFlags AudioBackendOAL::Base_Features()
void AudioBackendOAL::Base_OnActiveDeviceChanged()
{
+ PROFILE_CPU();
PROFILE_MEM(Audio);
// Cleanup
@@ -659,9 +674,53 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged()
LOG(Fatal, "Failed to open OpenAL device ({0}).", String(name));
return;
}
+ if (ALC::Inited)
+ LOG(Info, "Changed audio device to: {}", String(Audio::GetActiveDevice()->Name));
- // Setup
- ALC::RebuildContext(states);
+ // Rebuild context
+ ALC::RebuildContext();
+ if (ALC::Inited)
+ {
+ // Reload all audio clips to recreate their buffers
+ for (AudioClip* audioClip : Content::GetAssets())
+ {
+ audioClip->WaitForLoaded();
+ ScopeLock lock(audioClip->Locker);
+
+ // Clear old buffer IDs
+ for (uint32& bufferID : audioClip->Buffers)
+ bufferID = 0;
+
+ if (audioClip->IsStreamable())
+ {
+ // Let the streaming recreate missing buffers
+ audioClip->RequestStreamingUpdate();
+ }
+ else
+ {
+ // Reload audio clip
+ auto assetLock = audioClip->Storage->Lock();
+ audioClip->LoadChunk(0);
+ audioClip->Buffers[0] = AudioBackend::Buffer::Create();
+ audioClip->WriteBuffer(0);
+
+ }
+ }
+
+ // Reload all videos to recreate their buffers
+ for (VideoPlayer* videoPlayer : Level::GetActors(true))
+ {
+ VideoBackendPlayer& player = videoPlayer->_player;
+
+ // Clear audio state
+ for (uint32& bufferID : player.AudioBuffers)
+ bufferID = 0;
+ player.NextAudioBuffer = 0;
+ player.AudioSource = 0;
+ }
+ }
+ ALC::RebuildListeners();
+ ALC::RebuildSources(states);
}
void AudioBackendOAL::Base_SetDopplerFactor(float value)
@@ -782,6 +841,7 @@ bool AudioBackendOAL::Base_Init()
if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize"))
ALC::Features = EnumAddFlags(ALC::Features, FeatureFlags::SpatialMultiChannel);
#endif
+ ALC::Inited = true;
// Log service info
LOG(Info, "{0} ({1})", String(alGetString(AL_RENDERER)), String(alGetString(AL_VERSION)));
diff --git a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp
index 9600a93fb..6cba1302b 100644
--- a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp
+++ b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp
@@ -672,7 +672,7 @@ bool AudioBackendXAudio2::Base_Init()
HRESULT hr = XAudio2Create(&XAudio2::Instance, 0, XAUDIO2_DEFAULT_PROCESSOR);
if (FAILED(hr))
{
- LOG(Error, "Failed to initalize XAudio2. Error: 0x{0:x}", hr);
+ LOG(Error, "Failed to initialize XAudio2. Error: 0x{0:x}", hr);
return true;
}
XAudio2::Instance->RegisterForCallbacks(&XAudio2::Callback);
@@ -681,7 +681,7 @@ bool AudioBackendXAudio2::Base_Init()
hr = XAudio2::Instance->CreateMasteringVoice(&XAudio2::MasteringVoice);
if (FAILED(hr))
{
- LOG(Error, "Failed to initalize XAudio2 mastering voice. Error: 0x{0:x}", hr);
+ LOG(Error, "Failed to initialize XAudio2 mastering voice. Error: 0x{0:x}", hr);
return true;
}
XAUDIO2_VOICE_DETAILS details;
diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp
index 9fd4cee9c..7a35d3b28 100644
--- a/Source/Engine/Content/Asset.cpp
+++ b/Source/Engine/Content/Asset.cpp
@@ -487,6 +487,8 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const
const auto loadingTask = (ContentLoadTask*)Platform::AtomicRead(&_loadingTask);
if (loadingTask == nullptr)
{
+ if (IsLoaded())
+ return false;
LOG(Warning, "WaitForLoaded asset \'{0}\' failed. No loading task attached and asset is not loaded.", ToString());
return true;
}
diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp
index ed41c4aab..c0355ea0e 100644
--- a/Source/Engine/Content/Assets/SkinnedModel.cpp
+++ b/Source/Engine/Content/Assets/SkinnedModel.cpp
@@ -61,16 +61,24 @@ Array SkinnedModel::GetBlendShapes()
SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source, bool autoRetarget)
{
+ // Fast-path to use cached mapping
SkeletonMapping mapping;
mapping.TargetSkeleton = this;
+ SkeletonMappingData mappingData;
+ if (_skeletonMappingCache.TryGet(source, mappingData))
+ {
+ mapping.SourceSkeleton = mappingData.SourceSkeleton;
+ mapping.NodesMapping = mappingData.NodesMapping;
+ return mapping;
+ }
+ mapping.SourceSkeleton = nullptr;
+
if (WaitForLoaded() || !source || source->WaitForLoaded())
return mapping;
+ PROFILE_CPU();
ScopeLock lock(Locker);
- SkeletonMappingData mappingData;
if (!_skeletonMappingCache.TryGet(source, mappingData))
{
- PROFILE_CPU();
-
// Initialize the mapping
SkeletonRetarget* retarget = nullptr;
const Guid sourceId = source->GetID();
@@ -370,6 +378,7 @@ bool SkinnedModel::SetupSkeleton(const Array& nodes)
model->Skeleton.Bones[i].LocalTransform = node.LocalTransform;
model->Skeleton.Bones[i].NodeIndex = i;
}
+ model->Skeleton.Dirty();
ClearSkeletonMapping();
// Calculate offset matrix (inverse bind pose transform) for every bone manually
@@ -427,6 +436,7 @@ bool SkinnedModel::SetupSkeleton(const Array& nodes, const ArraySkeleton.Nodes = nodes;
model->Skeleton.Bones = bones;
+ model->Skeleton.Dirty();
ClearSkeletonMapping();
// Calculate offset matrix (inverse bind pose transform) for every bone manually
@@ -823,13 +833,13 @@ bool SkinnedModel::SaveMesh(WriteStream& stream, const ModelData& modelData, int
void SkinnedModel::ClearSkeletonMapping()
{
- for (auto& e : _skeletonMappingCache)
+ for (const auto& e : _skeletonMappingCache)
{
e.Key->OnUnloaded.Unbind(this);
#if USE_EDITOR
e.Key->OnReloading.Unbind(this);
#endif
- Allocator::Free(e.Value.NodesMapping.Get());
+ Allocator::Free((void*)e.Value.NodesMapping.Get());
}
_skeletonMappingCache.Clear();
}
@@ -837,8 +847,9 @@ void SkinnedModel::ClearSkeletonMapping()
void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj)
{
ScopeLock lock(Locker);
- auto i = _skeletonMappingCache.Find(obj);
- ASSERT(i != _skeletonMappingCache.End());
+ SkeletonMappingData mappingData;
+ bool found = _skeletonMappingCache.TryGet(obj, mappingData);
+ ASSERT(found);
// Unlink event
obj->OnUnloaded.Unbind(this);
@@ -847,8 +858,8 @@ void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj)
#endif
// Clear cache
- Allocator::Free(i->Value.NodesMapping.Get());
- _skeletonMappingCache.Remove(i);
+ Allocator::Free(mappingData.NodesMapping.Get());
+ _skeletonMappingCache.Remove(obj);
}
uint64 SkinnedModel::GetMemoryUsage() const
diff --git a/Source/Engine/Content/Assets/SkinnedModel.h b/Source/Engine/Content/Assets/SkinnedModel.h
index 894a080c4..111d4d6cb 100644
--- a/Source/Engine/Content/Assets/SkinnedModel.h
+++ b/Source/Engine/Content/Assets/SkinnedModel.h
@@ -3,7 +3,7 @@
#pragma once
#include "ModelBase.h"
-#include "Engine/Core/Collections/Dictionary.h"
+#include "Engine/Threading/ConcurrentDictionary.h"
#include "Engine/Graphics/Models/SkinnedMesh.h"
#include "Engine/Graphics/Models/SkeletonData.h"
@@ -101,9 +101,9 @@ public:
struct FLAXENGINE_API SkeletonMapping
{
// Target skeleton.
- AssetReference TargetSkeleton;
+ SkinnedModel* TargetSkeleton;
// Source skeleton.
- AssetReference SourceSkeleton;
+ SkinnedModel* SourceSkeleton;
// The node-to-node mapping for the fast animation sampling for the skinned model skeleton nodes. Each item is index of the source skeleton node into target skeleton node.
Span NodesMapping;
};
@@ -115,7 +115,7 @@ private:
Span NodesMapping;
};
- Dictionary _skeletonMappingCache;
+ ConcurrentDictionary _skeletonMappingCache;
public:
///
diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp
index 329696dea..a7e132bdc 100644
--- a/Source/Engine/Content/Assets/VisualScript.cpp
+++ b/Source/Engine/Content/Assets/VisualScript.cpp
@@ -1700,6 +1700,8 @@ void VisualScript::CacheScriptingType()
VisualScriptingBinaryModule::VisualScriptingBinaryModule()
: _name("Visual Scripting")
{
+ // Visual Scripts can be unloaded and loaded again even in game
+ CanReload = true;
}
ScriptingObject* VisualScriptingBinaryModule::VisualScriptObjectSpawn(const ScriptingObjectSpawnParams& params)
diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp
index 4a9ea500b..59d2dfffa 100644
--- a/Source/Engine/Content/Content.cpp
+++ b/Source/Engine/Content/Content.cpp
@@ -684,6 +684,19 @@ Array Content::GetAssets()
return assets;
}
+Array Content::GetAssets(const MClass* type)
+{
+ Array assets;
+ AssetsLocker.Lock();
+ for (auto& e : Assets)
+ {
+ if (e.Value->Is(type))
+ assets.Add(e.Value);
+ }
+ AssetsLocker.Unlock();
+ return assets;
+}
+
const Dictionary& Content::GetAssetsRaw()
{
AssetsLocker.Lock();
diff --git a/Source/Engine/Content/Content.h b/Source/Engine/Content/Content.h
index f6dcf59f0..e286e7c7d 100644
--- a/Source/Engine/Content/Content.h
+++ b/Source/Engine/Content/Content.h
@@ -3,6 +3,9 @@
#pragma once
#include "Engine/Scripting/ScriptingType.h"
+#ifndef _MSC_VER
+#include "Engine/Core/Collections/Array.h"
+#endif
#include "AssetInfo.h"
#include "Asset.h"
#include "Config.h"
@@ -122,7 +125,26 @@ public:
/// Gets the assets (loaded or during load).
///
/// The collection of assets.
- static Array GetAssets();
+ API_FUNCTION() static Array GetAssets();
+
+ ///
+ /// Gets the assets (loaded or during load).
+ ///
+ /// Type of the assets to search for. Includes any assets derived from the type.
+ /// Found actors list.
+ API_FUNCTION() static Array GetAssets(API_PARAM(Attributes="TypeReference(typeof(Actor))") const MClass* type);
+
+ ///
+ /// Gets the assets (loaded or during load).
+ ///
+ /// Type of the object.
+ /// Found actors list.
+ template
+ static Array GetAssets()
+ {
+ Array assets = GetAssets(T::GetStaticClass());
+ return *(Array*) & assets;
+ }
///
/// Gets the raw dictionary of assets (loaded or during load).
diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h
index e2f5f0ed6..e18a5b999 100644
--- a/Source/Engine/Core/Collections/Dictionary.h
+++ b/Source/Engine/Core/Collections/Dictionary.h
@@ -4,6 +4,9 @@
#include "HashSetBase.h"
+template
+class ConcurrentDictionary;
+
///
/// Describes single portion of space for the key and value pair in a hash map.
///
@@ -13,6 +16,7 @@ struct DictionaryBucket
friend Memory;
friend HashSetBase;
friend Dictionary;
+ friend ConcurrentDictionary;
/// The key.
KeyType Key;
diff --git a/Source/Engine/Core/Config.h b/Source/Engine/Core/Config.h
index 810217050..95319b885 100644
--- a/Source/Engine/Core/Config.h
+++ b/Source/Engine/Core/Config.h
@@ -57,5 +57,5 @@
#define API_PARAM(...)
#define API_TYPEDEF(...)
#define API_INJECT_CODE(...)
-#define API_AUTO_SERIALIZATION(...) public: void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
+#define API_AUTO_SERIALIZATION(...) public: bool ShouldSerialize(const void* otherObj) const override; void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override;
#define DECLARE_SCRIPTING_TYPE_MINIMAL(type) public: friend class type##Internal; static struct ScriptingTypeInitializer TypeInitializer;
diff --git a/Source/Engine/Core/ISerializable.h b/Source/Engine/Core/ISerializable.h
index c99051fc0..7500eff8b 100644
--- a/Source/Engine/Core/ISerializable.h
+++ b/Source/Engine/Core/ISerializable.h
@@ -36,6 +36,13 @@ public:
///
virtual ~ISerializable() = default;
+ ///
+ /// Compares with other instance to decide whether serialize this instance (eg. any field orp property is modified). Used to skip object serialization if not needed.
+ ///
+ /// The instance of the object (always valid) to compare with to decide whether serialize this instance.
+ /// True if any field or property is modified compared to the other object instance, otherwise false.
+ virtual bool ShouldSerialize(const void* otherObj) const { return true; }
+
///
/// Serializes object to the output stream compared to the values of the other object instance (eg. default class object). If other object is null then serialize all properties.
///
diff --git a/Source/Engine/Core/Math/Color.cs b/Source/Engine/Core/Math/Color.cs
index 2d779dcfa..be4a12789 100644
--- a/Source/Engine/Core/Math/Color.cs
+++ b/Source/Engine/Core/Math/Color.cs
@@ -11,7 +11,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.ColorConverter))]
#endif
- partial struct Color
+ partial struct Color : Json.ICustomValueEquals
{
///
/// The size of the type, in bytes.
@@ -196,6 +196,13 @@ namespace FlaxEngine
A = values[3];
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Color)other;
+ return Equals(ref o);
+ }
+
///
public override bool Equals(object value)
{
diff --git a/Source/Engine/Core/Math/Double2.cs b/Source/Engine/Core/Math/Double2.cs
index 9594b22cb..51fcf32d5 100644
--- a/Source/Engine/Core/Math/Double2.cs
+++ b/Source/Engine/Core/Math/Double2.cs
@@ -65,7 +65,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Double2Converter))]
#endif
- partial struct Double2 : IEquatable, IFormattable
+ partial struct Double2 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2}";
@@ -1574,6 +1574,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Double2)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Double3.cs b/Source/Engine/Core/Math/Double3.cs
index cb26cf071..4dccc1fb3 100644
--- a/Source/Engine/Core/Math/Double3.cs
+++ b/Source/Engine/Core/Math/Double3.cs
@@ -66,7 +66,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Double3Converter))]
#endif
- partial struct Double3 : IEquatable, IFormattable
+ partial struct Double3 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2}";
@@ -1872,6 +1872,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Double3)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Double4.cs b/Source/Engine/Core/Math/Double4.cs
index 70d27cb28..bf176e6ff 100644
--- a/Source/Engine/Core/Math/Double4.cs
+++ b/Source/Engine/Core/Math/Double4.cs
@@ -66,7 +66,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Double4Converter))]
#endif
- partial struct Double4 : IEquatable, IFormattable
+ partial struct Double4 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}";
@@ -1372,6 +1372,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Double4)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Float2.cs b/Source/Engine/Core/Math/Float2.cs
index 1b70dd0a6..5bb81ec3a 100644
--- a/Source/Engine/Core/Math/Float2.cs
+++ b/Source/Engine/Core/Math/Float2.cs
@@ -60,7 +60,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Float2Converter))]
#endif
- partial struct Float2 : IEquatable, IFormattable
+ partial struct Float2 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2}";
@@ -1650,6 +1650,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Float2)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Float3.cs b/Source/Engine/Core/Math/Float3.cs
index 5e8dceed6..50554345b 100644
--- a/Source/Engine/Core/Math/Float3.cs
+++ b/Source/Engine/Core/Math/Float3.cs
@@ -60,7 +60,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Float3Converter))]
#endif
- partial struct Float3 : IEquatable, IFormattable
+ partial struct Float3 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2}";
@@ -1904,6 +1904,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Float3)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Float4.cs b/Source/Engine/Core/Math/Float4.cs
index b6eb6dd9e..26abf4b2e 100644
--- a/Source/Engine/Core/Math/Float4.cs
+++ b/Source/Engine/Core/Math/Float4.cs
@@ -60,7 +60,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Float4Converter))]
#endif
- partial struct Float4 : IEquatable, IFormattable
+ partial struct Float4 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}";
@@ -1412,6 +1412,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Float4)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Half.h b/Source/Engine/Core/Math/Half.h
index 2617649d0..9ec68a6a0 100644
--- a/Source/Engine/Core/Math/Half.h
+++ b/Source/Engine/Core/Math/Half.h
@@ -5,6 +5,7 @@
#include "Math.h"
#include "Vector2.h"
#include "Vector3.h"
+#include "Vector4.h"
///
/// Half-precision 16 bit floating point number consisting of a sign bit, a 5 bit biased exponent, and a 10 bit mantissa
@@ -248,6 +249,19 @@ public:
explicit Half4(const Color& c);
explicit Half4(const Rectangle& rect);
+ operator Float2() const
+ {
+ return ToFloat2();
+ }
+ operator Float3() const
+ {
+ return ToFloat3();
+ }
+ operator Float4() const
+ {
+ return ToFloat4();
+ }
+
public:
Float2 ToFloat2() const;
Float3 ToFloat3() const;
diff --git a/Source/Engine/Core/Math/Int2.cs b/Source/Engine/Core/Math/Int2.cs
index 4a4107252..32a273307 100644
--- a/Source/Engine/Core/Math/Int2.cs
+++ b/Source/Engine/Core/Math/Int2.cs
@@ -14,7 +14,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Int2Converter))]
#endif
- partial struct Int2 : IEquatable, IFormattable
+ partial struct Int2 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0} Y:{1}";
@@ -940,6 +940,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Int2)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Int3.cs b/Source/Engine/Core/Math/Int3.cs
index 78e4600c3..81bb8026e 100644
--- a/Source/Engine/Core/Math/Int3.cs
+++ b/Source/Engine/Core/Math/Int3.cs
@@ -14,7 +14,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Int3Converter))]
#endif
- partial struct Int3 : IEquatable, IFormattable
+ partial struct Int3 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0} Y:{1} Z:{2}";
@@ -1023,6 +1023,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Int3)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Int4.cs b/Source/Engine/Core/Math/Int4.cs
index e180ccb31..bbccadab4 100644
--- a/Source/Engine/Core/Math/Int4.cs
+++ b/Source/Engine/Core/Math/Int4.cs
@@ -14,7 +14,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Int4Converter))]
#endif
- partial struct Int4 : IEquatable, IFormattable
+ partial struct Int4 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0} Y:{1} Z:{2} W:{3}";
@@ -881,6 +881,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Int4)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Packed.cpp b/Source/Engine/Core/Math/Packed.cpp
index 55ebd9ccf..b22e65d3e 100644
--- a/Source/Engine/Core/Math/Packed.cpp
+++ b/Source/Engine/Core/Math/Packed.cpp
@@ -41,16 +41,6 @@ FloatR10G10B10A2::FloatR10G10B10A2(const float* values)
{
}
-FloatR10G10B10A2::operator Float3() const
-{
- return ToFloat3();
-}
-
-FloatR10G10B10A2::operator Float4() const
-{
- return ToFloat4();
-}
-
Float3 FloatR10G10B10A2::ToFloat3() const
{
Float3 vectorOut;
diff --git a/Source/Engine/Core/Math/Packed.h b/Source/Engine/Core/Math/Packed.h
index 8e3aad41f..c5b5e827f 100644
--- a/Source/Engine/Core/Math/Packed.h
+++ b/Source/Engine/Core/Math/Packed.h
@@ -40,9 +40,14 @@ struct FLAXENGINE_API FloatR10G10B10A2
{
return Value;
}
-
- operator Float3() const;
- operator Float4() const;
+ operator Float3() const
+ {
+ return ToFloat3();
+ }
+ operator Float4() const
+ {
+ return ToFloat4();
+ }
FloatR10G10B10A2& operator=(const FloatR10G10B10A2& other)
{
diff --git a/Source/Engine/Core/Math/Quaternion.cs b/Source/Engine/Core/Math/Quaternion.cs
index d89b71488..cf51fac50 100644
--- a/Source/Engine/Core/Math/Quaternion.cs
+++ b/Source/Engine/Core/Math/Quaternion.cs
@@ -60,7 +60,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.QuaternionConverter))]
#endif
- partial struct Quaternion : IEquatable, IFormattable
+ partial struct Quaternion : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}";
@@ -1681,6 +1681,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Quaternion)other;
+ return Equals(ref o);
+ }
+
///
/// Tests whether one quaternion is near another quaternion.
///
diff --git a/Source/Engine/Core/Math/Rectangle.cs b/Source/Engine/Core/Math/Rectangle.cs
index 81c689d48..8e3c2b6c4 100644
--- a/Source/Engine/Core/Math/Rectangle.cs
+++ b/Source/Engine/Core/Math/Rectangle.cs
@@ -6,7 +6,7 @@ using System.Runtime.CompilerServices;
namespace FlaxEngine
{
- partial struct Rectangle : IEquatable
+ partial struct Rectangle : IEquatable, Json.ICustomValueEquals
{
///
/// A which represents an empty space.
@@ -523,6 +523,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Rectangle)other;
+ return Equals(ref o);
+ }
+
///
public override string ToString()
{
diff --git a/Source/Engine/Core/Math/Transform.cs b/Source/Engine/Core/Math/Transform.cs
index fc16a501b..90c0c36c9 100644
--- a/Source/Engine/Core/Math/Transform.cs
+++ b/Source/Engine/Core/Math/Transform.cs
@@ -16,7 +16,7 @@ using System.Runtime.InteropServices;
namespace FlaxEngine
{
[Serializable]
- partial struct Transform : IEquatable, IFormattable
+ partial struct Transform : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "Translation:{0} Orientation:{1} Scale:{2}";
@@ -673,6 +673,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Transform)other;
+ return Equals(ref o);
+ }
+
///
/// Tests whether one transform is near another transform.
///
diff --git a/Source/Engine/Core/Math/Vector2.cs b/Source/Engine/Core/Math/Vector2.cs
index 8e1599513..77c52035d 100644
--- a/Source/Engine/Core/Math/Vector2.cs
+++ b/Source/Engine/Core/Math/Vector2.cs
@@ -73,7 +73,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Vector2Converter))]
#endif
- public unsafe partial struct Vector2 : IEquatable, IFormattable
+ public unsafe partial struct Vector2 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2}";
@@ -954,6 +954,33 @@ namespace FlaxEngine
return result;
}
+ ///
+ /// Performs a spherical linear interpolation between two vectors.
+ ///
+ /// Start vector.
+ /// End vector.
+ /// Value between 0 and 1 indicating the weight of .
+ /// >When the method completes, contains the linear interpolation of the two vectors.
+ public static void Slerp(ref Vector2 start, ref Vector2 end, float amount, out Vector2 result)
+ {
+ var dot = Mathr.Clamp(Dot(start, end), -1.0f, 1.0f);
+ var theta = Mathr.Acos(dot) * amount;
+ Vector2 relativeVector = (end - start * dot).Normalized;
+ result = ((start * Mathr.Cos(theta)) + (relativeVector * Mathr.Sin(theta)));
+ }
+
+ ///
+ /// Performs a spherical linear interpolation between two vectors.
+ ///
+ /// Start vector.
+ /// End vector.
+ /// Value between 0 and 1 indicating the weight of .
+ public static Vector2 Slerp(Vector2 start, Vector2 end, float amount)
+ {
+ Slerp(ref start, ref end, amount, out Vector2 result);
+ return result;
+ }
+
///
/// Performs a gradual change of a vector towards a specified target over time
///
@@ -1774,6 +1801,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Vector2)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h
index 34bd1a59c..cee013840 100644
--- a/Source/Engine/Core/Math/Vector2.h
+++ b/Source/Engine/Core/Math/Vector2.h
@@ -558,6 +558,24 @@ public:
return result;
}
+ // Performs a spherical linear interpolation between two vectors.
+ static void Slerp(const Vector2Base& start, const Vector2Base& end, T amount, Vector2Base& result)
+ {
+ T dot = Math::Clamp(Dot(start, end), -1.0f, 1.0f);
+ T theta = Math::Acos(dot) * amount;
+ Vector2Base relativeVector = end - start * dot;
+ relativeVector.Normalize();
+ result = ((start * Math::Cos(theta)) + (relativeVector * Math::Sin(theta)));
+ }
+
+ // Performs a spherical linear interpolation between two vectors.
+ static Vector2Base Slerp(const Vector2Base& start, const Vector2Base& end, T amount)
+ {
+ Vector2Base result;
+ Slerp(start, end, amount, result);
+ return result;
+ }
+
public:
///
/// Calculates the area of the triangle.
diff --git a/Source/Engine/Core/Math/Vector3.cs b/Source/Engine/Core/Math/Vector3.cs
index 5e01a7a6c..ce7492e55 100644
--- a/Source/Engine/Core/Math/Vector3.cs
+++ b/Source/Engine/Core/Math/Vector3.cs
@@ -73,7 +73,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Vector3Converter))]
#endif
- public unsafe partial struct Vector3 : IEquatable, IFormattable
+ public unsafe partial struct Vector3 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2}";
@@ -1043,6 +1043,33 @@ namespace FlaxEngine
return result;
}
+ ///
+ /// Performs a spherical linear interpolation between two vectors.
+ ///
+ /// Start vector.
+ /// End vector.
+ /// Value between 0 and 1 indicating the weight of .
+ /// When the method completes, contains the linear interpolation of the two vectors.
+ public static void Slerp(ref Vector3 start, ref Vector3 end, float amount, out Vector3 result)
+ {
+ var dot = Mathr.Clamp(Dot(start, end), -1.0f, 1.0f);
+ var theta = Mathr.Acos(dot) * amount;
+ Vector3 relativeVector = (end - start * dot).Normalized;
+ result = ((start * Mathr.Cos(theta)) + (relativeVector * Mathr.Sin(theta)));
+ }
+
+ ///
+ /// Performs a spherical linear interpolation between two vectors.
+ ///
+ /// Start vector.
+ /// End vector.
+ /// Value between 0 and 1 indicating the weight of .
+ public static Vector3 Slerp(Vector3 start, Vector3 end, float amount)
+ {
+ Slerp(ref start, ref end, amount, out var result);
+ return result;
+ }
+
///
/// Performs a gradual change of a vector towards a specified target over time
///
@@ -2133,6 +2160,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Vector3)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Vector3.h b/Source/Engine/Core/Math/Vector3.h
index a19253b00..bf737877b 100644
--- a/Source/Engine/Core/Math/Vector3.h
+++ b/Source/Engine/Core/Math/Vector3.h
@@ -686,6 +686,24 @@ public:
return result;
}
+ // Performs a spherical linear interpolation between two vectors.
+ static void Slerp(const Vector3Base& start, const Vector3Base& end, T amount, Vector3Base& result)
+ {
+ T dot = Math::Clamp(Dot(start, end), -1.0f, 1.0f);
+ T theta = Math::Acos(dot) * amount;
+ Vector3Base relativeVector = end - start * dot;
+ relativeVector.Normalize();
+ result = ((start * Math::Cos(theta)) + (relativeVector * Math::Sin(theta)));
+ }
+
+ // Performs a spherical linear interpolation between two vectors.
+ static Vector3Base Slerp(const Vector3Base& start, const Vector3Base& end, T amount)
+ {
+ Vector3Base result;
+ Slerp(start, end, amount, result);
+ return result;
+ }
+
// Performs a cubic interpolation between two vectors.
static void SmoothStep(const Vector3Base& start, const Vector3Base& end, T amount, Vector3Base& result)
{
diff --git a/Source/Engine/Core/Math/Vector4.cs b/Source/Engine/Core/Math/Vector4.cs
index e50d03ca0..b08a08f50 100644
--- a/Source/Engine/Core/Math/Vector4.cs
+++ b/Source/Engine/Core/Math/Vector4.cs
@@ -72,7 +72,7 @@ namespace FlaxEngine
#if FLAX_EDITOR
[System.ComponentModel.TypeConverter(typeof(TypeConverters.Vector4Converter))]
#endif
- public partial struct Vector4 : IEquatable, IFormattable
+ public partial struct Vector4 : IEquatable, IFormattable, Json.ICustomValueEquals
{
private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}";
@@ -260,6 +260,19 @@ namespace FlaxEngine
///
public bool IsNormalized => Mathr.Abs((X * X + Y * Y + Z * Z + W * W) - 1.0f) < 1e-4f;
+ ///
+ /// Gets the normalized vector. Returned vector has length equal 1.
+ ///
+ public Vector4 Normalized
+ {
+ get
+ {
+ Vector4 vector4 = this;
+ vector4.Normalize();
+ return vector4;
+ }
+ }
+
///
/// Gets a value indicting whether this vector is zero
///
@@ -878,6 +891,33 @@ namespace FlaxEngine
return result;
}
+ ///
+ /// Performs a spherical linear interpolation between two vectors.
+ ///
+ /// Start vector.
+ /// End vector.
+ /// Value between 0 and 1 indicating the weight of .
+ /// When the method completes, contains the linear interpolation of the two vectors.
+ public static void Slerp(ref Vector4 start, ref Vector4 end, Real amount, out Vector4 result)
+ {
+ var dot = Mathr.Clamp(Dot(start, end), -1.0f, 1.0f);
+ var theta = Mathr.Acos(dot) * amount;
+ Vector4 relativeVector = (end - start * dot).Normalized;
+ result = ((start * Mathr.Cos(theta)) + (relativeVector * Mathr.Sin(theta)));
+ }
+
+ ///
+ /// Performs a spherical linear interpolation between two vectors.
+ ///
+ /// Start vector.
+ /// End vector.
+ /// Value between 0 and 1 indicating the weight of .
+ public static Vector4 Slerp(Vector4 start, Vector4 end, Real amount)
+ {
+ Slerp(ref start, ref end, amount, out var result);
+ return result;
+ }
+
///
/// Performs a cubic interpolation between two vectors.
///
@@ -1486,6 +1526,13 @@ namespace FlaxEngine
}
}
+ ///
+ public bool ValueEquals(object other)
+ {
+ var o = (Vector4)other;
+ return Equals(ref o);
+ }
+
///
/// Determines whether the specified is equal to this instance.
///
diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h
index cc7b59730..5f5e436ad 100644
--- a/Source/Engine/Core/Math/Vector4.h
+++ b/Source/Engine/Core/Math/Vector4.h
@@ -129,6 +129,12 @@ public:
FLAXENGINE_API String ToString() const;
public:
+ // Gets a value indicting whether this instance is normalized.
+ bool IsNormalized() const
+ {
+ return Math::Abs((X * X + Y * Y + Z * Z + W * W) - 1.0f) < 1e-4f;
+ }
+
// Gets a value indicting whether this vector is zero.
bool IsZero() const
{
@@ -219,6 +225,45 @@ public:
return Vector4Base(-X, -Y, -Z, -W);
}
+ ///
+ /// Calculates a normalized vector that has length equal to 1.
+ ///
+ Vector4Base GetNormalized() const
+ {
+ Vector4Base result(X, Y, Z, W);
+ result.Normalize();
+ return result;
+ }
+
+public:
+ ///
+ /// Performs vector normalization (scales vector up to unit length).
+ ///
+ void Normalize()
+ {
+ const T length = Math::Sqrt(X * X + Y * Y + Z * Z + W * W);
+ if (length >= ZeroTolerance)
+ {
+ const T inv = (T)1.0f / length;
+ X *= inv;
+ Y *= inv;
+ Z *= inv;
+ W *= inv;
+ }
+ }
+
+ ///
+ /// Performs fast vector normalization (scales vector up to unit length).
+ ///
+ void NormalizeFast()
+ {
+ const T inv = 1.0f / Math::Sqrt(X * X + Y * Y + Z * Z + W * W);
+ X *= inv;
+ Y *= inv;
+ Z *= inv;
+ W *= inv;
+ }
+
public:
Vector4Base operator+(const Vector4Base& b) const
{
@@ -469,6 +514,41 @@ public:
result = Vector4Base(Math::Clamp(v.X, min.X, max.X), Math::Clamp(v.Y, min.Y, max.Y), Math::Clamp(v.Z, min.Z, max.Z), Math::Clamp(v.W, min.W, max.W));
}
+ // Performs vector normalization (scales vector up to unit length).
+ static Vector4Base Normalize(const Vector4Base& v)
+ {
+ Vector4Base r = v;
+ const T length = Math::Sqrt(r.X * r.X + r.Y * r.Y + r.Z * r.Z + r.W * r.W);
+ if (length >= ZeroTolerance)
+ {
+ const T inv = (T)1.0f / length;
+ r.X *= inv;
+ r.Y *= inv;
+ r.Z *= inv;
+ r.W *= inv;
+ }
+ return r;
+ }
+
+ // Performs vector normalization (scales vector up to unit length). This is a faster version that does not perform check for length equal 0 (it assumes that input vector is not empty).
+ static Vector4Base NormalizeFast(const Vector4Base& v)
+ {
+ const T inv = 1.0f / v.Length();
+ return Vector4Base(v.X * inv, v.Y * inv, v.Z * inv, v.W * inv);
+ }
+
+ // Performs vector normalization (scales vector up to unit length).
+ static FORCE_INLINE void Normalize(const Vector4Base& input, Vector4Base& result)
+ {
+ result = Normalize(input);
+ }
+
+ // Calculates the dot product of two vectors.
+ FORCE_INLINE static T Dot(const Vector4Base& a, const Vector4Base& b)
+ {
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z + a.W * b.W;
+ }
+
// Performs a linear interpolation between two vectors.
static void Lerp(const Vector4Base& start, const Vector4Base& end, T amount, Vector4Base& result)
{
@@ -486,6 +566,24 @@ public:
return result;
}
+ // Performs a spherical linear interpolation between two vectors.
+ static void Slerp(const Vector4Base& start, const Vector4Base& end, T amount, Vector4Base& result)
+ {
+ T dot = Math::Clamp(Dot(start, end), -1.0f, 1.0f);
+ T theta = Math::Acos(dot) * amount;
+ Vector4Base relativeVector = end - start * dot;
+ relativeVector.Normalize();
+ result = ((start * Math::Cos(theta)) + (relativeVector * Math::Sin(theta)));
+ }
+
+ // Performs a spherical linear interpolation between two vectors.
+ static Vector4Base Slerp(const Vector4Base& start, const Vector4Base& end, T amount)
+ {
+ Vector4Base result;
+ Slerp(start, end, amount, result);
+ return result;
+ }
+
FLAXENGINE_API static Vector4Base Transform(const Vector4Base& v, const Matrix& m);
};
diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp
index 4ab8552d3..dcabe8e48 100644
--- a/Source/Engine/Core/Types/Variant.cpp
+++ b/Source/Engine/Core/Types/Variant.cpp
@@ -18,8 +18,10 @@
#include "Engine/Core/Math/Ray.h"
#include "Engine/Core/Math/Rectangle.h"
#include "Engine/Core/Math/Transform.h"
+#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/ScriptingObject.h"
+#include "Engine/Scripting/ManagedCLR/MAssembly.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MCore.h"
#include "Engine/Scripting/ManagedCLR/MUtils.h"
@@ -88,6 +90,7 @@ static_assert((int32)VariantType::Types::MAX == ARRAY_COUNT(InBuiltTypesTypeName
VariantType::VariantType(Types type, const StringView& typeName)
{
Type = type;
+ StaticName = 0;
TypeName = nullptr;
const int32 length = typeName.Length();
if (length)
@@ -98,32 +101,41 @@ VariantType::VariantType(Types type, const StringView& typeName)
}
}
-VariantType::VariantType(Types type, const StringAnsiView& typeName)
+VariantType::VariantType(Types type, const StringAnsiView& typeName, bool staticName)
{
Type = type;
- TypeName = nullptr;
- int32 length = typeName.Length();
- if (length)
+ StaticName = staticName && (typeName.HasChars() && typeName[typeName.Length()] == 0); // Require string to be null-terminated (not fully safe check)
+ if (staticName)
{
- TypeName = static_cast(Allocator::Allocate(length + 1));
- Platform::MemoryCopy(TypeName, typeName.Get(), length);
- TypeName[length] = 0;
+ TypeName = (char*)typeName.Get();
}
+ else
+ {
+ TypeName = nullptr;
+ int32 length = typeName.Length();
+ if (length)
+ {
+ TypeName = static_cast(Allocator::Allocate(length + 1));
+ Platform::MemoryCopy(TypeName, typeName.Get(), length);
+ TypeName[length] = 0;
+ }
+ }
+}
+
+VariantType::VariantType(Types type, const ScriptingType& sType)
+ : VariantType(type)
+{
+ SetTypeName(sType);
}
VariantType::VariantType(Types type, const MClass* klass)
{
Type = type;
+ StaticName = false;
TypeName = nullptr;
#if USE_CSHARP
if (klass)
- {
- const StringAnsiView typeName = klass->GetFullName();
- const int32 length = typeName.Length();
- TypeName = static_cast(Allocator::Allocate(length + 1));
- Platform::MemoryCopy(TypeName, typeName.Get(), length);
- TypeName[length] = 0;
- }
+ SetTypeName(*klass);
#endif
}
@@ -190,9 +202,9 @@ VariantType::VariantType(const StringAnsiView& typeName)
if (const auto mclass = Scripting::FindClass(typeName))
{
if (mclass->IsEnum())
- new(this) VariantType(Enum, typeName);
+ new(this) VariantType(Enum, mclass);
else
- new(this) VariantType(ManagedObject, typeName);
+ new(this) VariantType(ManagedObject, mclass);
return;
}
#endif
@@ -204,36 +216,48 @@ VariantType::VariantType(const StringAnsiView& typeName)
VariantType::VariantType(const VariantType& other)
{
Type = other.Type;
- TypeName = nullptr;
- const int32 length = StringUtils::Length(other.TypeName);
- if (length)
+ StaticName = other.StaticName;
+ if (StaticName)
{
- TypeName = static_cast(Allocator::Allocate(length + 1));
- Platform::MemoryCopy(TypeName, other.TypeName, length);
- TypeName[length] = 0;
+ TypeName = other.TypeName;
+ }
+ else
+ {
+ TypeName = nullptr;
+ const int32 length = StringUtils::Length(other.TypeName);
+ if (length)
+ {
+ TypeName = static_cast(Allocator::Allocate(length + 1));
+ Platform::MemoryCopy(TypeName, other.TypeName, length);
+ TypeName[length] = 0;
+ }
}
}
VariantType::VariantType(VariantType&& other) noexcept
{
Type = other.Type;
+ StaticName = other.StaticName;
TypeName = other.TypeName;
other.Type = Null;
other.TypeName = nullptr;
+ other.StaticName = 0;
}
VariantType& VariantType::operator=(const Types& type)
{
Type = type;
- Allocator::Free(TypeName);
+ if (StaticName)
+ Allocator::Free(TypeName);
TypeName = nullptr;
+ StaticName = 0;
return *this;
}
VariantType& VariantType::operator=(VariantType&& other)
{
ASSERT(this != &other);
- Swap(Type, other.Type);
+ Swap(Packed, other.Packed);
Swap(TypeName, other.TypeName);
return *this;
}
@@ -242,14 +266,23 @@ VariantType& VariantType::operator=(const VariantType& other)
{
ASSERT(this != &other);
Type = other.Type;
- Allocator::Free(TypeName);
- TypeName = nullptr;
- const int32 length = StringUtils::Length(other.TypeName);
- if (length)
+ if (StaticName)
+ Allocator::Free(TypeName);
+ StaticName = other.StaticName;
+ if (StaticName)
{
- TypeName = static_cast(Allocator::Allocate(length + 1));
- Platform::MemoryCopy(TypeName, other.TypeName, length);
- TypeName[length] = 0;
+ TypeName = other.TypeName;
+ }
+ else
+ {
+ TypeName = nullptr;
+ const int32 length = StringUtils::Length(other.TypeName);
+ if (length)
+ {
+ TypeName = static_cast(Allocator::Allocate(length + 1));
+ Platform::MemoryCopy(TypeName, other.TypeName, length);
+ TypeName[length] = 0;
+ }
}
return *this;
}
@@ -283,24 +316,45 @@ void VariantType::SetTypeName(const StringView& typeName)
{
if (StringUtils::Length(TypeName) != typeName.Length())
{
- Allocator::Free(TypeName);
+ if (StaticName)
+ Allocator::Free(TypeName);
+ StaticName = 0;
TypeName = static_cast(Allocator::Allocate(typeName.Length() + 1));
TypeName[typeName.Length()] = 0;
}
StringUtils::ConvertUTF162ANSI(typeName.Get(), TypeName, typeName.Length());
}
-void VariantType::SetTypeName(const StringAnsiView& typeName)
+void VariantType::SetTypeName(const StringAnsiView& typeName, bool staticName)
{
- if (StringUtils::Length(TypeName) != typeName.Length())
+ if (StringUtils::Length(TypeName) != typeName.Length() || StaticName != staticName)
{
- Allocator::Free(TypeName);
+ if (StaticName)
+ Allocator::Free(TypeName);
+ StaticName = staticName;
+ if (staticName)
+ {
+ TypeName = (char*)typeName.Get();
+ return;
+ }
TypeName = static_cast(Allocator::Allocate(typeName.Length() + 1));
TypeName[typeName.Length()] = 0;
}
Platform::MemoryCopy(TypeName, typeName.Get(), typeName.Length());
}
+void VariantType::SetTypeName(const ScriptingType& type)
+{
+ SetTypeName(type.Fullname, type.Module->CanReload);
+}
+
+void VariantType::SetTypeName(const MClass& klass)
+{
+#if USE_CSHARP
+ SetTypeName(klass.GetFullName(), klass.GetAssembly()->CanReload());
+#endif
+}
+
const char* VariantType::GetTypeName() const
{
if (TypeName)
@@ -322,6 +376,29 @@ VariantType VariantType::GetElementType() const
return VariantType();
}
+void VariantType::Inline()
+{
+ // Check if the typename comes from static assembly which can be used to inline name instead of dynamic memory allocation
+ StringAnsiView typeName(TypeName);
+ auto& modules = BinaryModule::GetModules();
+ for (auto module : modules)
+ {
+ int32 typeIndex;
+ if (!module->CanReload && module->FindScriptingType(typeName, typeIndex))
+ {
+ ScriptingTypeHandle typeHandle(module, typeIndex);
+ SetTypeName(typeHandle.GetType().Fullname, true);
+ return;
+ }
+ }
+
+#if USE_CSHARP
+ // Try with C#-only types
+ if (const auto mclass = Scripting::FindClass(TypeName))
+ SetTypeName(*mclass);
+#endif
+}
+
::String VariantType::ToString() const
{
::String result;
@@ -632,8 +709,7 @@ Variant::Variant(ScriptingObject* v)
AsObject = v;
if (v)
{
- // TODO: optimize VariantType to support statically linked typename of ScriptingType (via 1 bit flag within Types enum, only in game as editor might hot-reload types)
- Type.SetTypeName(v->GetType().Fullname);
+ Type.SetTypeName(v->GetType());
v->Deleted.Bind(this);
}
}
@@ -644,9 +720,8 @@ Variant::Variant(Asset* v)
AsAsset = v;
if (v)
{
- // TODO: optimize VariantType to support statically linked typename of ScriptingType (via 1 bit flag within Types enum, only in game as editor might hot-reload types)
- Type.SetTypeName(v->GetType().Fullname);
v->AddReference();
+ Type.SetTypeName(v->GetType());
v->OnUnloaded.Bind(this);
}
}
@@ -3007,16 +3082,16 @@ Variant Variant::NewValue(const StringAnsiView& typeName)
switch (type.Type)
{
case ScriptingTypes::Script:
- v.SetType(VariantType(VariantType::Object, typeName));
+ v.SetType(VariantType(VariantType::Object, type));
v.AsObject = type.Script.Spawn(ScriptingObjectSpawnParams(Guid::New(), typeHandle));
if (v.AsObject)
v.AsObject->Deleted.Bind(&v);
break;
case ScriptingTypes::Structure:
- v.SetType(VariantType(VariantType::Structure, typeName));
+ v.SetType(VariantType(VariantType::Structure, type));
break;
case ScriptingTypes::Enum:
- v.SetType(VariantType(VariantType::Enum, typeName));
+ v.SetType(VariantType(VariantType::Enum, type));
v.AsEnum = 0;
break;
default:
@@ -3030,16 +3105,16 @@ Variant Variant::NewValue(const StringAnsiView& typeName)
// Fallback to C#-only types
if (mclass->IsEnum())
{
- v.SetType(VariantType(VariantType::Enum, typeName));
+ v.SetType(VariantType(VariantType::Enum, mclass));
v.AsEnum = 0;
}
else if (mclass->IsValueType())
{
- v.SetType(VariantType(VariantType::Structure, typeName));
+ v.SetType(VariantType(VariantType::Structure, mclass));
}
else
{
- v.SetType(VariantType(VariantType::ManagedObject, typeName));
+ v.SetType(VariantType(VariantType::ManagedObject, mclass));
MObject* instance = mclass->CreateInstance();
if (instance)
{
diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h
index 4fd6ab2eb..5c057bc65 100644
--- a/Source/Engine/Core/Types/Variant.h
+++ b/Source/Engine/Core/Types/Variant.h
@@ -17,7 +17,7 @@ struct ScriptingTypeHandle;
///
API_STRUCT(InBuild) struct FLAXENGINE_API VariantType
{
- enum Types
+ enum Types : uint8
{
Null = 0,
Void,
@@ -80,10 +80,22 @@ API_STRUCT(InBuild) struct FLAXENGINE_API VariantType
};
public:
- ///
- /// The type of the variant.
- ///
- Types Type;
+ union
+ {
+ struct
+ {
+ ///
+ /// The type of the variant.
+ ///
+ Types Type;
+
+ ///
+ /// Internal flag used to indicate that pointer to TypeName has been linked from a static/external memory that is stable (eg. ScriptingType or MClass). Allows avoiding dynamic memory allocation.
+ ///
+ uint8 StaticName : 1;
+ };
+ uint16 Packed;
+ };
///
/// The optional additional full name of the scripting type. Used for Asset, Object, Enum, Structure types to describe type precisely.
@@ -94,17 +106,20 @@ public:
FORCE_INLINE VariantType()
{
Type = Null;
+ StaticName = 0;
TypeName = nullptr;
}
FORCE_INLINE explicit VariantType(Types type)
{
Type = type;
+ StaticName = 0;
TypeName = nullptr;
}
explicit VariantType(Types type, const StringView& typeName);
- explicit VariantType(Types type, const StringAnsiView& typeName);
+ explicit VariantType(Types type, const StringAnsiView& typeName, bool staticName = false);
+ explicit VariantType(Types type, const ScriptingType& sType);
explicit VariantType(Types type, const MClass* klass);
explicit VariantType(const StringAnsiView& typeName);
VariantType(const VariantType& other);
@@ -112,7 +127,8 @@ public:
FORCE_INLINE ~VariantType()
{
- Allocator::Free(TypeName);
+ if (!StaticName)
+ Allocator::Free(TypeName);
}
public:
@@ -130,9 +146,13 @@ public:
public:
void SetTypeName(const StringView& typeName);
- void SetTypeName(const StringAnsiView& typeName);
+ void SetTypeName(const StringAnsiView& typeName, bool staticName = false);
+ void SetTypeName(const ScriptingType& type);
+ void SetTypeName(const MClass& klass);
const char* GetTypeName() const;
VariantType GetElementType() const;
+ // Drops custom type name into the name allocated by the scripting module to reduce memory allocations when referencing types.
+ void Inline();
::String ToString() const;
};
diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp
index f8b9c7b0f..116866848 100644
--- a/Source/Engine/Foliage/Foliage.cpp
+++ b/Source/Engine/Foliage/Foliage.cpp
@@ -7,17 +7,17 @@
#include "Engine/Core/Random.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Graphics/RenderTask.h"
+#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Content/Deprecated.h"
#if !FOLIAGE_USE_SINGLE_QUAD_TREE
#include "Engine/Threading/JobSystem.h"
#if FOLIAGE_USE_DRAW_CALLS_BATCHING
#include "Engine/Graphics/RenderTools.h"
-#include "Engine/Graphics/GPUDevice.h"
-#include "Engine/Renderer/RenderList.h"
#endif
#endif
#include "Engine/Level/SceneQuery.h"
#include "Engine/Profiler/ProfilerCPU.h"
+#include "Engine/Renderer/RenderList.h"
#include "Engine/Renderer/GlobalSignDistanceFieldPass.h"
#include "Engine/Renderer/GI/GlobalSurfaceAtlasPass.h"
#include "Engine/Serialization/Serialization.h"
@@ -41,8 +41,7 @@ Foliage::Foliage(const SpawnParams& params)
void Foliage::AddToCluster(ChunkedArray& clusters, FoliageCluster* cluster, FoliageInstance& instance)
{
- ASSERT(instance.Bounds.Radius > ZeroTolerance);
- ASSERT(cluster->Bounds.Intersects(instance.Bounds));
+ ASSERT_LOW_LAYER(instance.Bounds.Radius > ZeroTolerance);
// Minor clusters don't use bounds intersection but try to find the first free cluster instead
if (cluster->IsMinor)
@@ -63,6 +62,7 @@ void Foliage::AddToCluster(ChunkedArrayBounds.Intersects(instance.Bounds));
while (cluster->Children[0])
{
#define CHECK_CHILD(idx) \
@@ -193,6 +193,8 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster,
// Draw visible instances
const auto frame = Engine::FrameCount;
const auto model = type.Model.Get();
+ const auto transitionLOD = renderContext.View.Pass != DrawPass::Depth; // Let the main view pass update LOD transitions
+ // TODO: move DrawState to be stored per-view (so shadows can fade objects on their own)
for (int32 i = 0; i < cluster->Instances.Count(); i++)
{
auto& instance = *cluster->Instances.Get()[i];
@@ -210,20 +212,29 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster,
// Handling model fade-out transition
if (modelFrame == frame && instance.DrawState.PrevLOD != -1)
{
- // Check if start transition
- if (instance.DrawState.LODTransition == 255)
+ if (transitionLOD)
{
- instance.DrawState.LODTransition = 0;
- }
+ // Check if start transition
+ if (instance.DrawState.LODTransition == 255)
+ {
+ instance.DrawState.LODTransition = 0;
+ }
- RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition);
+ RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition);
- // Check if end transition
- if (instance.DrawState.LODTransition == 255)
- {
- instance.DrawState.PrevLOD = lodIndex;
+ // Check if end transition
+ if (instance.DrawState.LODTransition == 255)
+ {
+ instance.DrawState.PrevLOD = lodIndex;
+ }
+ else
+ {
+ const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD);
+ const float normalizedProgress = static_cast(instance.DrawState.LODTransition) * (1.0f / 255.0f);
+ DrawInstance(renderContext, instance, type, model, prevLOD, normalizedProgress, drawCallsLists, result);
+ }
}
- else
+ else if (instance.DrawState.LODTransition < 255)
{
const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD);
const float normalizedProgress = static_cast(instance.DrawState.LODTransition) * (1.0f / 255.0f);
@@ -236,29 +247,32 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster,
lodIndex += renderContext.View.ModelLODBias;
lodIndex = model->ClampLODIndex(lodIndex);
- // Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports)
- if (modelFrame == frame)
+ if (transitionLOD)
{
- // Check if start transition
- if (instance.DrawState.PrevLOD != lodIndex && instance.DrawState.LODTransition == 255)
+ // Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports)
+ if (modelFrame == frame)
{
+ // Check if start transition
+ if (instance.DrawState.PrevLOD != lodIndex && instance.DrawState.LODTransition == 255)
+ {
+ instance.DrawState.LODTransition = 0;
+ }
+
+ RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition);
+
+ // Check if end transition
+ if (instance.DrawState.LODTransition == 255)
+ {
+ instance.DrawState.PrevLOD = lodIndex;
+ }
+ }
+ // Check if there was a gap between frames in drawing this model instance
+ else if (modelFrame < frame || instance.DrawState.PrevLOD == -1)
+ {
+ // Reset state
+ instance.DrawState.PrevLOD = lodIndex;
instance.DrawState.LODTransition = 0;
}
-
- RenderTools::UpdateModelLODTransition(instance.DrawState.LODTransition);
-
- // Check if end transition
- if (instance.DrawState.LODTransition == 255)
- {
- instance.DrawState.PrevLOD = lodIndex;
- }
- }
- // Check if there was a gap between frames in drawing this model instance
- else if (modelFrame < frame || instance.DrawState.PrevLOD == -1)
- {
- // Reset state
- instance.DrawState.PrevLOD = lodIndex;
- instance.DrawState.LODTransition = 255;
}
// Draw
@@ -281,7 +295,8 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster,
//DebugDraw::DrawSphere(instance.Bounds, Color::YellowGreen);
- instance.DrawState.PrevFrame = frame;
+ if (transitionLOD)
+ instance.DrawState.PrevFrame = frame;
}
}
}
@@ -350,7 +365,7 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster,
draw.DrawState = &instance.DrawState;
draw.Bounds = sphere;
draw.PerInstanceRandom = instance.Random;
- draw.DrawModes = type._drawModes;
+ draw.DrawModes = type.DrawModes;
draw.SetStencilValue(_layer);
type.Model->Draw(renderContext, draw);
diff --git a/Source/Engine/Foliage/FoliageCluster.cpp b/Source/Engine/Foliage/FoliageCluster.cpp
index fd4c0f753..107bf265a 100644
--- a/Source/Engine/Foliage/FoliageCluster.cpp
+++ b/Source/Engine/Foliage/FoliageCluster.cpp
@@ -21,26 +21,7 @@ void FoliageCluster::Init(const BoundingBox& bounds)
void FoliageCluster::UpdateTotalBoundsAndCullDistance()
{
- if (Children[0])
- {
- ASSERT(Instances.IsEmpty());
-
- Children[0]->UpdateTotalBoundsAndCullDistance();
- Children[1]->UpdateTotalBoundsAndCullDistance();
- Children[2]->UpdateTotalBoundsAndCullDistance();
- Children[3]->UpdateTotalBoundsAndCullDistance();
-
- TotalBounds = Children[0]->TotalBounds;
- BoundingBox::Merge(TotalBounds, Children[1]->TotalBounds, TotalBounds);
- BoundingBox::Merge(TotalBounds, Children[2]->TotalBounds, TotalBounds);
- BoundingBox::Merge(TotalBounds, Children[3]->TotalBounds, TotalBounds);
-
- MaxCullDistance = Children[0]->MaxCullDistance;
- MaxCullDistance = Math::Max(MaxCullDistance, Children[1]->MaxCullDistance);
- MaxCullDistance = Math::Max(MaxCullDistance, Children[2]->MaxCullDistance);
- MaxCullDistance = Math::Max(MaxCullDistance, Children[3]->MaxCullDistance);
- }
- else if (Instances.HasItems())
+ if (Instances.HasItems())
{
BoundingBox box;
BoundingBox::FromSphere(Instances[0]->Bounds, TotalBounds);
@@ -58,6 +39,30 @@ void FoliageCluster::UpdateTotalBoundsAndCullDistance()
MaxCullDistance = 0;
}
+ if (Children[0])
+ {
+ Children[0]->UpdateTotalBoundsAndCullDistance();
+ Children[1]->UpdateTotalBoundsAndCullDistance();
+ Children[2]->UpdateTotalBoundsAndCullDistance();
+ Children[3]->UpdateTotalBoundsAndCullDistance();
+
+ if (Instances.HasItems())
+ BoundingBox::Merge(TotalBounds, Children[0]->TotalBounds, TotalBounds);
+ else
+ TotalBounds = Children[0]->TotalBounds;
+ BoundingBox::Merge(TotalBounds, Children[1]->TotalBounds, TotalBounds);
+ BoundingBox::Merge(TotalBounds, Children[2]->TotalBounds, TotalBounds);
+ BoundingBox::Merge(TotalBounds, Children[3]->TotalBounds, TotalBounds);
+
+ if (Instances.HasItems())
+ MaxCullDistance = Math::Max(MaxCullDistance, Children[0]->MaxCullDistance);
+ else
+ MaxCullDistance = Children[0]->MaxCullDistance;
+ MaxCullDistance = Math::Max(MaxCullDistance, Children[1]->MaxCullDistance);
+ MaxCullDistance = Math::Max(MaxCullDistance, Children[2]->MaxCullDistance);
+ MaxCullDistance = Math::Max(MaxCullDistance, Children[3]->MaxCullDistance);
+ }
+
BoundingSphere::FromBox(TotalBounds, TotalBoundsSphere);
}
diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h
index 107fe3533..64e49e18c 100644
--- a/Source/Engine/Graphics/Enums.h
+++ b/Source/Engine/Graphics/Enums.h
@@ -1094,6 +1094,11 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64
/// Default flags for materials/models previews generating.
///
DefaultAssetPreview = Reflections | Decals | DirectionalLights | PointLights | SpotLights | SkyLights | SpecularLight | AntiAliasing | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | Sky | Particles,
+
+ ///
+ /// All flags enabled.
+ ///
+ All = None | DebugDraw | EditorSprites | Reflections | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | Decals | DepthOfField | PhysicsDebug | Fog | MotionBlur | ContactShadows | GlobalSDF | Sky | LightsDebug | Particles,
};
DECLARE_ENUM_OPERATORS(ViewFlags);
diff --git a/Source/Engine/Graphics/Models/CollisionProxy.h b/Source/Engine/Graphics/Models/CollisionProxy.h
index 5dc021867..2ecdce756 100644
--- a/Source/Engine/Graphics/Models/CollisionProxy.h
+++ b/Source/Engine/Graphics/Models/CollisionProxy.h
@@ -6,7 +6,9 @@
#include "Engine/Core/Math/Transform.h"
#include "Engine/Core/Math/Ray.h"
#include "Engine/Core/Math/CollisionsHelper.h"
+#include "Engine/Core/Math/Half.h"
#include "Engine/Core/Collections/Array.h"
+#include "Engine/Graphics/PixelFormat.h"
///
/// Helper container used for detailed triangle mesh intersections tests.
@@ -31,23 +33,38 @@ public:
}
template
- void Init(uint32 vertices, uint32 triangles, const Float3* positions, const IndexType* indices, uint32 positionsStride = sizeof(Float3))
+ void Init(uint32 vertices, uint32 triangles, const Float3* positions, const IndexType* indices, uint32 positionsStride = sizeof(Float3), PixelFormat positionsFormat = PixelFormat::R32G32B32_Float)
{
Triangles.Clear();
Triangles.EnsureCapacity(triangles, false);
const IndexType* it = indices;
- for (uint32 i = 0; i < triangles; i++)
+#define LOOP_BEGIN() \
+ for (uint32 i = 0; i < triangles; i++) \
+ { \
+ const IndexType i0 = *(it++); \
+ const IndexType i1 = *(it++); \
+ const IndexType i2 = *(it++); \
+ if (i0 < vertices && i1 < vertices && i2 < vertices) \
{
- const IndexType i0 = *(it++);
- const IndexType i1 = *(it++);
- const IndexType i2 = *(it++);
- if (i0 < vertices && i1 < vertices && i2 < vertices)
- {
+#define LOOP_END() } }
+ if (positionsFormat == PixelFormat::R32G32B32_Float)
+ {
+ LOOP_BEGIN()
#define GET_POS(idx) *(const Float3*)((const byte*)positions + positionsStride * idx)
Triangles.Add({ GET_POS(i0), GET_POS(i1), GET_POS(i2) });
#undef GET_POS
- }
+ LOOP_END()
}
+ else if (positionsFormat == PixelFormat::R16G16B16A16_Float)
+ {
+ LOOP_BEGIN()
+#define GET_POS(idx) ((const Half4*)((const byte*)positions + positionsStride * idx))->ToFloat3()
+ Triangles.Add({ GET_POS(i0), GET_POS(i1), GET_POS(i2) });
+#undef GET_POS
+ LOOP_END()
+ }
+#undef LOOP_BEGIN
+#undef LOOP_END
}
void Clear()
diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs
index 228e43a8c..29aa86c18 100644
--- a/Source/Engine/Graphics/Models/MeshAccessor.cs
+++ b/Source/Engine/Graphics/Models/MeshAccessor.cs
@@ -265,6 +265,39 @@ namespace FlaxEngine
}
}
+ ///
+ /// Copies the contents of the input into the elements of this stream.
+ ///
+ /// The source .
+ public void Set(Span src)
+ {
+ if (IsLinear(PixelFormat.R32_UInt))
+ {
+ src.CopyTo(MemoryMarshal.Cast(_data));
+ }
+ else if (IsLinear(PixelFormat.R16_UInt))
+ {
+ var count = Count;
+ fixed (byte* data = _data)
+ {
+ for (int i = 0; i < count; i++)
+ ((ushort*)data)[i] = (ushort)src[i];
+ }
+ }
+ else
+ {
+ var count = Count;
+ fixed (byte* data = _data)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ var v = new Float4(src[i]);
+ _sampler.Write(data + i * _stride, ref v);
+ }
+ }
+ }
+ }
+
///
/// Copies the contents of this stream into a destination .
///
@@ -281,9 +314,7 @@ namespace FlaxEngine
fixed (byte* data = _data)
{
for (int i = 0; i < count; i++)
- {
dst[i] = new Float2(_sampler.Read(data + i * _stride));
- }
}
}
}
@@ -304,9 +335,7 @@ namespace FlaxEngine
fixed (byte* data = _data)
{
for (int i = 0; i < count; i++)
- {
dst[i] = new Float3(_sampler.Read(data + i * _stride));
- }
}
}
}
@@ -327,9 +356,37 @@ namespace FlaxEngine
fixed (byte* data = _data)
{
for (int i = 0; i < count; i++)
- {
dst[i] = (Color)_sampler.Read(data + i * _stride);
- }
+ }
+ }
+ }
+
+ ///
+ /// Copies the contents of this stream into a destination .
+ ///
+ /// The destination .
+ public void CopyTo(Span dst)
+ {
+ if (IsLinear(PixelFormat.R32_UInt))
+ {
+ _data.CopyTo(MemoryMarshal.Cast(dst));
+ }
+ else if (IsLinear(PixelFormat.R16_UInt))
+ {
+ var count = Count;
+ fixed (byte* data = _data)
+ {
+ for (int i = 0; i < count; i++)
+ dst[i] = ((ushort*)data)[i];
+ }
+ }
+ else
+ {
+ var count = Count;
+ fixed (byte* data = _data)
+ {
+ for (int i = 0; i < count; i++)
+ dst[i] = (uint)_sampler.Read(data + i * _stride).X;
}
}
}
@@ -619,6 +676,16 @@ namespace FlaxEngine
return Attribute((VertexElement.Types)((byte)VertexElement.Types.TexCoord0 + channel));
}
+ ///
+ /// Gets or sets the index buffer with triangle indices.
+ ///
+ /// Uses stream to read or write data to the index buffer.
+ public uint[] Triangles
+ {
+ get => GetStreamUInt(Index());
+ set => SetStreamUInt(Index(), value);
+ }
+
///
/// Gets or sets the vertex positions. Null if does not exist in vertex buffers of the mesh.
///
@@ -659,6 +726,25 @@ namespace FlaxEngine
set => SetStreamFloat2(VertexElement.Types.TexCoord, value);
}
+ private uint[] GetStreamUInt(Stream stream)
+ {
+ uint[] result = null;
+ if (stream.IsValid)
+ {
+ result = new uint[stream.Count];
+ stream.CopyTo(result);
+ }
+ return result;
+ }
+
+ private void SetStreamUInt(Stream stream, uint[] value)
+ {
+ if (stream.IsValid)
+ {
+ stream.Set(value);
+ }
+ }
+
private delegate void TransformDelegate3(ref Float3 value);
private Float3[] GetStreamFloat3(VertexElement.Types attribute, TransformDelegate3 transform = null)
diff --git a/Source/Engine/Graphics/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp
index 3434cd91a..eadbfcba9 100644
--- a/Source/Engine/Graphics/Models/MeshBase.cpp
+++ b/Source/Engine/Graphics/Models/MeshBase.cpp
@@ -441,6 +441,9 @@ bool MeshBase::Init(uint32 vertices, uint32 triangles, const ArrayFindElement(VertexElement::Types::Position);
if (use16BitIndexBuffer)
- _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint16*)ibData);
+ _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint16*)ibData, vertexBuffer0->GetStride(), positionsElement.Format);
else
- _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint32*)ibData);
+ _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint32*)ibData, vertexBuffer0->GetStride(), positionsElement.Format);
#endif
// Free old buffers
diff --git a/Source/Engine/Graphics/Models/SkeletonData.h b/Source/Engine/Graphics/Models/SkeletonData.h
index 0b6c7d4d7..79e0be512 100644
--- a/Source/Engine/Graphics/Models/SkeletonData.h
+++ b/Source/Engine/Graphics/Models/SkeletonData.h
@@ -73,6 +73,10 @@ struct TIsPODType
///
class FLAXENGINE_API SkeletonData
{
+private:
+ mutable volatile int64 _dirty = 1;
+ mutable Array _cachedPose;
+
public:
///
/// The nodes in this hierarchy. The root node is always at the index 0.
@@ -114,6 +118,11 @@ public:
int32 FindNode(const StringView& name) const;
int32 FindBone(int32 nodeIndex) const;
+ // Gets the skeleton nodes transforms in mesh space (pose). Calculated from the local node transforms and hierarchy. Cached internally and updated when data is dirty.
+ const Array& GetNodesPose() const;
+
+ // Marks data as dirty (modified) to update internal state and recalculate cached data if needed (eg. skeleton pose).
+ void Dirty();
uint64 GetMemoryUsage() const;
///
diff --git a/Source/Engine/Graphics/Models/SkinnedMesh.cpp b/Source/Engine/Graphics/Models/SkinnedMesh.cpp
index 66b3e5701..0377003be 100644
--- a/Source/Engine/Graphics/Models/SkinnedMesh.cpp
+++ b/Source/Engine/Graphics/Models/SkinnedMesh.cpp
@@ -154,6 +154,8 @@ void SkeletonData::Swap(SkeletonData& other)
{
Nodes.Swap(other.Nodes);
Bones.Swap(other.Bones);
+ Dirty();
+ other.Dirty();
}
Transform SkeletonData::GetNodeTransform(int32 nodeIndex) const
@@ -171,6 +173,7 @@ Transform SkeletonData::GetNodeTransform(int32 nodeIndex) const
void SkeletonData::SetNodeTransform(int32 nodeIndex, const Transform& value)
{
CHECK(Nodes.IsValidIndex(nodeIndex));
+ Dirty();
const int32 parentIndex = Nodes[nodeIndex].ParentIndex;
if (parentIndex == -1)
{
@@ -201,6 +204,39 @@ int32 SkeletonData::FindBone(int32 nodeIndex) const
return -1;
}
+const Array& SkeletonData::GetNodesPose() const
+{
+ // Guard with a simple atomic flag to avoid locking if the pose is up to date
+ if (Platform::AtomicRead(&_dirty))
+ {
+ ScopeLock lock(RenderContext::GPULocker);
+ if (Platform::AtomicRead(&_dirty))
+ {
+ const SkeletonNode* nodes = Nodes.Get();
+ const int32 nodesCount = Nodes.Count();
+ _cachedPose.Resize(nodesCount);
+ Matrix* posePtr = _cachedPose.Get();
+ for (int32 nodeIndex = 0; nodeIndex < nodesCount; nodeIndex++)
+ {
+ const SkeletonNode& node = nodes[nodeIndex];
+ Matrix local;
+ Matrix::Transformation(node.LocalTransform.Scale, node.LocalTransform.Orientation, node.LocalTransform.Translation, local);
+ if (node.ParentIndex != -1)
+ Matrix::Multiply(local, posePtr[node.ParentIndex], posePtr[nodeIndex]);
+ else
+ posePtr[nodeIndex] = local;
+ }
+ Platform::AtomicStore(&_dirty, 0);
+ }
+ }
+ return _cachedPose;
+}
+
+void SkeletonData::Dirty()
+{
+ Platform::AtomicStore(&_dirty, 1);
+}
+
uint64 SkeletonData::GetMemoryUsage() const
{
uint64 result = Nodes.Capacity() * sizeof(SkeletonNode) + Bones.Capacity() * sizeof(SkeletonBone);
diff --git a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp
index 8470facac..eb2ea0145 100644
--- a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp
+++ b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.cpp
@@ -5,11 +5,6 @@
#include "Engine/Animations/Config.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Math/Matrix.h"
-#include "Engine/Core/Math/Matrix3x4.h"
-
-SkinnedMeshDrawData::SkinnedMeshDrawData()
-{
-}
SkinnedMeshDrawData::~SkinnedMeshDrawData()
{
@@ -33,7 +28,7 @@ void SkinnedMeshDrawData::Setup(int32 bonesCount)
BonesCount = bonesCount;
_hasValidData = false;
- _isDirty = false;
+ _isDirty = true;
Data.Resize(BoneMatrices->GetSize());
SAFE_DELETE_GPU_RESOURCE(PrevBoneMatrices);
}
diff --git a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.h b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.h
index 24d5ca230..dc780a26d 100644
--- a/Source/Engine/Graphics/Models/SkinnedMeshDrawData.h
+++ b/Source/Engine/Graphics/Models/SkinnedMeshDrawData.h
@@ -36,11 +36,6 @@ public:
Array Data;
public:
- ///
- /// Initializes a new instance of the class.
- ///
- SkinnedMeshDrawData();
-
///
/// Finalizes an instance of the class.
///
@@ -76,7 +71,7 @@ public:
void OnDataChanged(bool dropHistory);
///
- /// After bones Data has been send to the GPU buffer.
+ /// After bones Data has been sent to the GPU buffer.
///
void OnFlush()
{
diff --git a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp
index 05c6d605a..e458ff1c1 100644
--- a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp
+++ b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp
@@ -8,6 +8,7 @@
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/GPUBuffer.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
+#include "Engine/Threading/ConcurrentDictionary.h"
#if GPU_ENABLE_RESOURCE_NAMING
#include "Engine/Scripting/Enums.h"
#endif
@@ -40,27 +41,37 @@ uint32 GetHash(const VertexBufferLayouts& key)
namespace
{
- CriticalSection CacheLocker;
- Dictionary LayoutCache;
- Dictionary VertexBufferCache;
+ ConcurrentDictionary LayoutCache;
+ ConcurrentDictionary VertexBufferCache;
- GPUVertexLayout* AddCache(const VertexBufferLayouts& key, int32 count)
+ GPUVertexLayout* GetCache(const VertexBufferLayouts& key, int32 count)
{
- GPUVertexLayout::Elements elements;
- bool anyValid = false;
- for (int32 slot = 0; slot < count; slot++)
+ GPUVertexLayout* result;
+ if (!VertexBufferCache.TryGet(key, result))
{
- if (key.Layouts[slot])
+ GPUVertexLayout::Elements elements;
+ bool anyValid = false;
+ for (int32 slot = 0; slot < count; slot++)
{
- anyValid = true;
- int32 start = elements.Count();
- elements.Add(key.Layouts[slot]->GetElements());
- for (int32 j = start; j < elements.Count(); j++)
- elements.Get()[j].Slot = (byte)slot;
+ if (key.Layouts[slot])
+ {
+ anyValid = true;
+ int32 start = elements.Count();
+ elements.Add(key.Layouts[slot]->GetElements());
+ for (int32 j = start; j < elements.Count(); j++)
+ elements.Get()[j].Slot = (byte)slot;
+ }
}
+ result = anyValid ? GPUVertexLayout::Get(elements, true) : nullptr;
+ if (!VertexBufferCache.Add(key, result))
+ {
+ // Other thread added the value
+ Delete(result);
+ bool found = VertexBufferCache.TryGet(key, result);
+ ASSERT(found);
+ }
+
}
- GPUVertexLayout* result = anyValid ? GPUVertexLayout::Get(elements, true) : nullptr;
- VertexBufferCache.Add(key, result);
return result;
}
}
@@ -148,7 +159,6 @@ GPUVertexLayout* GPUVertexLayout::Get(const Elements& elements, bool explicitOff
}
// Lookup existing cache
- CacheLocker.Lock();
GPUVertexLayout* result;
if (!LayoutCache.TryGet(hash, result))
{
@@ -160,12 +170,16 @@ GPUVertexLayout* GPUVertexLayout::Get(const Elements& elements, bool explicitOff
LOG(Error, " {}", e.ToString());
#endif
LOG(Error, "Failed to create vertex layout");
- CacheLocker.Unlock();
return nullptr;
}
- LayoutCache.Add(hash, result);
+ if (!LayoutCache.Add(hash, result))
+ {
+ // Other thread added the value
+ Delete(result);
+ bool found = LayoutCache.TryGet(hash, result);
+ ASSERT(found);
+ }
}
- CacheLocker.Unlock();
return result;
}
@@ -185,13 +199,7 @@ GPUVertexLayout* GPUVertexLayout::Get(const Span& vertexBuffers)
key.Layouts[i] = nullptr;
// Lookup existing cache
- CacheLocker.Lock();
- GPUVertexLayout* result;
- if (!VertexBufferCache.TryGet(key, result))
- result = AddCache(key, vertexBuffers.Length());
- CacheLocker.Unlock();
-
- return result;
+ return GetCache(key, vertexBuffers.Length());
}
GPUVertexLayout* GPUVertexLayout::Get(const Span& layouts)
@@ -209,13 +217,7 @@ GPUVertexLayout* GPUVertexLayout::Get(const Span& layouts)
key.Layouts[i] = nullptr;
// Lookup existing cache
- CacheLocker.Lock();
- GPUVertexLayout* result;
- if (!VertexBufferCache.TryGet(key, result))
- result = AddCache(key, layouts.Length());
- CacheLocker.Unlock();
-
- return result;
+ return GetCache(key, layouts.Length());
}
GPUVertexLayout* GPUVertexLayout::Merge(GPUVertexLayout* base, GPUVertexLayout* reference, bool removeUnused, bool addMissing, int32 missingSlotOverride, bool referenceOrder)
diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp
index 7d07cd0e7..f52fab600 100644
--- a/Source/Engine/Level/Actor.cpp
+++ b/Source/Engine/Level/Actor.cpp
@@ -1685,7 +1685,7 @@ Quaternion Actor::LookingAt(const Vector3& worldPos) const
{
const Vector3 direction = worldPos - _transform.Translation;
if (direction.LengthSquared() < ZeroTolerance)
- return _parent->GetOrientation();
+ return _parent ? _parent->GetOrientation() : Quaternion::Identity;
const Float3 newForward = Vector3::Normalize(direction);
const Float3 oldForward = _transform.Orientation * Vector3::Forward;
@@ -1712,7 +1712,7 @@ Quaternion Actor::LookingAt(const Vector3& worldPos, const Vector3& worldUp) con
{
const Vector3 direction = worldPos - _transform.Translation;
if (direction.LengthSquared() < ZeroTolerance)
- return _parent->GetOrientation();
+ return _parent ? _parent->GetOrientation() : Quaternion::Identity;
const Float3 forward = Vector3::Normalize(direction);
const Float3 up = Vector3::Normalize(worldUp);
if (Math::IsOne(Float3::Dot(forward, up)))
diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp
index f75174f72..11497e558 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.cpp
+++ b/Source/Engine/Level/Actors/AnimatedModel.cpp
@@ -14,14 +14,84 @@
#include "Engine/Content/Deprecated.h"
#include "Engine/Graphics/GPUContext.h"
#include "Engine/Graphics/GPUDevice.h"
+#include "Engine/Graphics/GPUPass.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/Models/MeshAccessor.h"
#include "Engine/Graphics/Models/MeshDeformation.h"
+#include "Engine/Renderer/RenderList.h"
#include "Engine/Level/Scene/Scene.h"
#include "Engine/Level/SceneObjectsFactory.h"
-#include "Engine/Profiler/ProfilerMemory.h"
+#include "Engine/Profiler/Profiler.h"
#include "Engine/Serialization/Serialization.h"
+// Implements efficient skinning data update within a shared GPUMemoryPass with manual resource transitions batched for all animated models.
+class AnimatedModelRenderListExtension : public RenderList::IExtension
+{
+public:
+ struct Item
+ {
+ GPUBuffer* BoneMatrices;
+ void* Data;
+ int32 Size;
+ };
+
+ RenderListBuffer- Items;
+
+ void PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch) override
+ {
+ Items.Clear();
+ }
+
+ void PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch) override
+ {
+ const int32 count = Items.Count();
+ if (count == 0)
+ return;
+ PROFILE_GPU_CPU_NAMED("Update Bones");
+ GPUMemoryPass pass(context);
+ Item* items = Items.Get();
+
+ // Special case for D3D11 backend that doesn't need transitions
+ if (context->GetDevice()->GetRendererType() <= RendererType::DirectX11)
+ {
+ for (int32 i = 0; i < count; i++)
+ {
+ Item& item = items[i];
+ context->UpdateBuffer(item.BoneMatrices, item.Data, item.Size);
+ }
+ }
+ else
+ {
+ // Batch resource barriers for buffer update
+ for (int32 i = 0; i < count; i++)
+ pass.Transition(items[i].BoneMatrices, GPUResourceAccess::CopyWrite);
+
+ // Update all buffers within Memory Pass (no barriers between)
+ for (int32 i = 0; i < count; i++)
+ {
+ Item& item = items[i];
+ context->UpdateBuffer(item.BoneMatrices, item.Data, item.Size);
+ }
+
+ // Batch resource barriers for reading in Vertex Shader
+ for (int32 i = 0; i < count; i++)
+ pass.Transition(items[i].BoneMatrices, GPUResourceAccess::ShaderReadGraphics);
+ }
+
+#if COMPILE_WITH_PROFILER
+ // Insert amount of kilobytes of data updated into profiler trace
+ uint32 dataSize = 0;
+ for (int32 i = 0; i < count; i++)
+ dataSize += items[i].Size;
+ ZoneValue(dataSize / 1024);
+#endif
+
+ Items.Clear();
+ }
+};
+
+AnimatedModelRenderListExtension RenderListExtension;
+
AnimatedModel::AnimatedModel(const SpawnParams& params)
: ModelInstanceActor(params)
, _actualMode(AnimationUpdateMode::Never)
@@ -1002,7 +1072,7 @@ void AnimatedModel::Draw(RenderContext& renderContext)
if (renderContext.View.Pass == DrawPass::GlobalSDF)
return;
if (renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas)
- return; // No supported
+ return; // Not supported
ACTOR_GET_WORLD_MATRIX(this, view, world);
GEOMETRY_DRAW_STATE_EVENT_BEGIN(_drawState, world);
@@ -1012,9 +1082,8 @@ void AnimatedModel::Draw(RenderContext& renderContext)
// Flush skinning data with GPU
if (_skinningData.IsDirty())
{
- RenderContext::GPULocker.Lock();
- GPUDevice::Instance->GetMainContext()->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count());
- RenderContext::GPULocker.Unlock();
+ RenderListExtension.Items.Add({ _skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count() });
+ _skinningData.OnFlush();
}
SkinnedMesh::DrawInfo draw;
@@ -1056,9 +1125,8 @@ void AnimatedModel::Draw(RenderContextBatch& renderContextBatch)
// Flush skinning data with GPU
if (_skinningData.IsDirty())
{
- RenderContext::GPULocker.Lock();
- GPUDevice::Instance->GetMainContext()->UpdateBuffer(_skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count());
- RenderContext::GPULocker.Unlock();
+ RenderListExtension.Items.Add({ _skinningData.BoneMatrices, _skinningData.Data.Get(), _skinningData.Data.Count() });
+ _skinningData.OnFlush();
}
SkinnedMesh::DrawInfo draw;
diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h
index 110b9ac61..e3d43b7c5 100644
--- a/Source/Engine/Level/Level.h
+++ b/Source/Engine/Level/Level.h
@@ -473,6 +473,19 @@ public:
/// Found actors list.
API_FUNCTION() static Array GetActors(API_PARAM(Attributes="TypeReference(typeof(Actor))") const MClass* type, bool activeOnly = false);
+ ///
+ /// Finds all the actors of the given type in all the loaded scenes.
+ ///
+ /// Type of the object.
+ /// Finds only active actors.
+ /// Found actors list.
+ template
+ static Array GetActors(bool activeOnly = false)
+ {
+ Array actors = GetActors(T::GetStaticClass(), activeOnly);
+ return *(Array*)&actors;
+ }
+
///
/// Finds all the scripts of the given type in an actor or all the loaded scenes.
///
diff --git a/Source/Engine/Level/MeshReference.cs b/Source/Engine/Level/MeshReference.cs
index 14ef0c72b..cb87e9769 100644
--- a/Source/Engine/Level/MeshReference.cs
+++ b/Source/Engine/Level/MeshReference.cs
@@ -13,7 +13,7 @@ namespace FlaxEngine
public bool ValueEquals(object other)
{
var o = (MeshReference)other;
- return JsonSerializer.ValueEquals(Actor, o.Actor) &&
+ return JsonSerializer.SceneObjectEquals(Actor, o.Actor) &&
LODIndex == o.LODIndex &&
MeshIndex == o.MeshIndex;
}
diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
index 0bed02f0b..378b706ed 100644
--- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
+++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp
@@ -227,9 +227,9 @@ public:
void PrefabInstanceData::CollectPrefabInstances(PrefabInstancesData& prefabInstancesData, const Guid& prefabId, Actor* defaultInstance, Actor* targetActor)
{
ScopeLock lock(PrefabManager::PrefabsReferencesLocker);
- if (PrefabManager::PrefabsReferences.ContainsKey(prefabId))
+ if (auto instancesPtr = PrefabManager::PrefabsReferences.TryGet(prefabId))
{
- auto& instances = PrefabManager::PrefabsReferences[prefabId];
+ auto& instances = *instancesPtr;
int32 usedCount = 0;
for (int32 instanceIndex = 0; instanceIndex < instances.Count(); instanceIndex++)
{
diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp
index 951b657b2..7d3703ee0 100644
--- a/Source/Engine/Particles/Particles.cpp
+++ b/Source/Engine/Particles/Particles.cpp
@@ -677,11 +677,10 @@ void CleanupGPUParticlesSorting()
SAFE_DELETE_GPU_RESOURCE(GPUIndirectArgsBuffer);
}
-void DrawEmittersGPU(RenderContextBatch& renderContextBatch)
+void DrawEmittersGPU(GPUContext* context, RenderContextBatch& renderContextBatch)
{
PROFILE_GPU_CPU_NAMED("DrawEmittersGPU");
ScopeReadLock systemScope(Particles::SystemLocker);
- GPUContext* context = GPUDevice::Instance->GetMainContext();
// Count draws and sorting passes needed for resources allocation
uint32 indirectArgsSize = 0;
@@ -1124,9 +1123,9 @@ void DrawEmitterGPU(RenderContextBatch& renderContextBatch, ParticleBuffer* buff
if (GPUEmitterDraws.Count() == 0)
{
// The first emitter schedules the drawing of all batched draws
- renderContextBatch.GetMainContext().List->AddDelayedDraw([](RenderContextBatch& renderContextBatch, int32 contextIndex)
+ renderContextBatch.GetMainContext().List->AddDelayedDraw([](GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex)
{
- DrawEmittersGPU(renderContextBatch);
+ DrawEmittersGPU(context, renderContextBatch);
});
}
GPUEmitterDraws.Add({ buffer, drawCall, drawModes, staticFlags, bounds, renderModulesIndices, indirectArgsSize, sortOrder, sorting });
diff --git a/Source/Engine/Platform/Base/FileSystemBase.cpp b/Source/Engine/Platform/Base/FileSystemBase.cpp
index f414bbd01..13ae3481c 100644
--- a/Source/Engine/Platform/Base/FileSystemBase.cpp
+++ b/Source/Engine/Platform/Base/FileSystemBase.cpp
@@ -12,25 +12,25 @@
bool FileSystemBase::ShowOpenFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array& filenames)
{
- // No supported
+ // Not supported
return true;
}
bool FileSystemBase::ShowSaveFileDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& filter, bool multiSelect, const StringView& title, Array& filenames)
{
- // No supported
+ // Not supported
return true;
}
bool FileSystemBase::ShowBrowseFolderDialog(Window* parentWindow, const StringView& initialDirectory, const StringView& title, String& path)
{
- // No supported
+ // Not supported
return true;
}
bool FileSystemBase::ShowFileExplorer(const StringView& path)
{
- // No supported
+ // Not supported
return true;
}
diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp
index 544438bb5..ac643b4e8 100644
--- a/Source/Engine/Renderer/RenderList.cpp
+++ b/Source/Engine/Renderer/RenderList.cpp
@@ -15,6 +15,7 @@
#include "Engine/Profiler/Profiler.h"
#include "Engine/Content/Assets/CubeTexture.h"
#include "Engine/Core/Log.h"
+#include "Engine/Core/Math/Half.h"
#include "Engine/Graphics/Shaders/GPUVertexLayout.h"
#include "Engine/Level/Scene/Lightmap.h"
#include "Engine/Level/Actors/PostFxVolume.h"
@@ -30,6 +31,13 @@ namespace
Array FreeRenderList;
Array> MemPool;
CriticalSection MemPoolLocker;
+
+ typedef Array> ExtensionsList;
+ ExtensionsList& GetExtensions()
+ {
+ static ExtensionsList list;
+ return list;
+ }
}
void ShaderObjectData::Store(const Matrix& worldMatrix, const Matrix& prevWorldMatrix, const Rectangle& lightmapUVsArea, const Float3& geometrySize, float perInstanceRandom, float worldDeterminantSign, float lodDitherFactor)
@@ -235,6 +243,16 @@ void RenderList::CleanupCache()
MemPoolLocker.Unlock();
}
+RenderList::IExtension::IExtension()
+{
+ GetExtensions().Add(this);
+}
+
+RenderList::IExtension::~IExtension()
+{
+ GetExtensions().Remove(this);
+}
+
bool RenderList::BlendableSettings::operator<(const BlendableSettings& other) const
{
// Sort by higher priority
@@ -257,18 +275,31 @@ void RenderList::AddSettingsBlend(IPostFxSettingsProvider* provider, float weigh
void RenderList::AddDelayedDraw(DelayedDraw&& func)
{
- MemPoolLocker.Lock(); // TODO: convert _delayedDraws into RenderListBuffer with usage of arena Memory for fast alloc
_delayedDraws.Add(MoveTemp(func));
- MemPoolLocker.Unlock();
}
-void RenderList::DrainDelayedDraws(RenderContextBatch& renderContextBatch, int32 contextIndex)
+void RenderList::DrainDelayedDraws(GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex)
{
- if (_delayedDraws.IsEmpty())
+ if (_delayedDraws.Count() == 0)
return;
+ PROFILE_CPU();
for (DelayedDraw& e : _delayedDraws)
- e(renderContextBatch, contextIndex);
- _delayedDraws.SetCapacity(0);
+ e(context, renderContextBatch, renderContextIndex);
+ _delayedDraws.Clear();
+}
+
+#define LOOP_EXTENSIONS() const auto& extensions = GetExtensions(); for (auto* e : extensions)
+
+void RenderList::PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch)
+{
+ LOOP_EXTENSIONS()
+ e->PreDraw(context, renderContextBatch);
+}
+
+void RenderList::PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch)
+{
+ LOOP_EXTENSIONS()
+ e->PostDraw(context, renderContextBatch);
}
void RenderList::BlendSettings()
@@ -494,7 +525,6 @@ RenderList::RenderList(const SpawnParams& params)
, ObjectBuffer(0, PixelFormat::R32G32B32A32_Float, false, TEXT("Object Buffer"))
, TempObjectBuffer(0, PixelFormat::R32G32B32A32_Float, false, TEXT("Object Buffer"))
, _instanceBuffer(0, sizeof(ShaderObjectDrawInstanceData), TEXT("Instance Buffer"), GPUVertexLayout::Get({ { VertexElement::Types::Attribute0, 3, 0, 1, PixelFormat::R32_UInt } }))
- , _delayedDraws(&Memory)
{
}
@@ -826,6 +856,13 @@ FORCE_INLINE bool DrawsEqual(const DrawCall* a, const DrawCall* b)
Platform::MemoryCompare(a->Geometry.VertexBuffers, b->Geometry.VertexBuffers, sizeof(a->Geometry.VertexBuffers) + sizeof(a->Geometry.VertexBuffersOffsets)) == 0;
}
+FORCE_INLINE Span GetVB(GPUBuffer* const* ptr, int32 maxSize)
+{
+ while (ptr[maxSize - 1] == nullptr && maxSize > 1)
+ maxSize--;
+ return ToSpan(ptr, maxSize);
+}
+
void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsList& list, RenderList* drawCallsList, GPUTextureView* input)
{
if (list.IsEmpty())
@@ -954,7 +991,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL
Platform::MemoryCopy(vb, activeDraw->Geometry.VertexBuffers, sizeof(DrawCall::Geometry.VertexBuffers));
Platform::MemoryCopy(vbOffsets, activeDraw->Geometry.VertexBuffersOffsets, sizeof(DrawCall::Geometry.VertexBuffersOffsets));
context->BindIB(activeDraw->Geometry.IndexBuffer);
- context->BindVB(ToSpan(vb, ARRAY_COUNT(vb)), vbOffsets);
+ context->BindVB(GetVB(vb, ARRAY_COUNT(vb)), vbOffsets);
context->DrawIndexedInstanced(activeDraw->Draw.IndicesCount, activeCount, instanceBufferOffset, 0, activeDraw->Draw.StartIndex);
instanceBufferOffset += activeCount;
@@ -971,7 +1008,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL
// Single-draw call batch
context->BindIB(drawCall.Geometry.IndexBuffer);
- context->BindVB(ToSpan(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets);
+ context->BindVB(GetVB(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets);
if (drawCall.InstanceCount == 0)
{
context->DrawIndexedInstancedIndirect(drawCall.Draw.IndirectArgsBuffer, drawCall.Draw.IndirectArgsOffset);
@@ -994,7 +1031,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL
Platform::MemoryCopy(vb, drawCall.Geometry.VertexBuffers, sizeof(DrawCall::Geometry.VertexBuffers));
Platform::MemoryCopy(vbOffsets, drawCall.Geometry.VertexBuffersOffsets, sizeof(DrawCall::Geometry.VertexBuffersOffsets));
context->BindIB(drawCall.Geometry.IndexBuffer);
- context->BindVB(ToSpan(vb, vbMax + 1), vbOffsets);
+ context->BindVB(GetVB(vb, vbMax + 1), vbOffsets);
if (drawCall.InstanceCount == 0)
{
@@ -1024,7 +1061,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL
const DrawCall& drawCall = drawCallsData[perDraw.DrawObjectIndex];
context->BindIB(drawCall.Geometry.IndexBuffer);
- context->BindVB(ToSpan(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets);
+ context->BindVB(GetVB(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets);
if (drawCall.InstanceCount == 0)
{
@@ -1045,7 +1082,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL
bindParams.DrawCall->Material->Bind(bindParams);
context->BindIB(drawCall.Geometry.IndexBuffer);
- context->BindVB(ToSpan(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets);
+ context->BindVB(GetVB(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets);
for (int32 j = 0; j < batch.Instances.Count(); j++)
{
@@ -1069,7 +1106,7 @@ void RenderList::ExecuteDrawCalls(const RenderContext& renderContext, DrawCallsL
drawCall.Material->Bind(bindParams);
context->BindIB(drawCall.Geometry.IndexBuffer);
- context->BindVB(ToSpan(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets);
+ context->BindVB(GetVB(drawCall.Geometry.VertexBuffers, vbMax), drawCall.Geometry.VertexBuffersOffsets);
if (drawCall.InstanceCount == 0)
{
diff --git a/Source/Engine/Renderer/RenderList.h b/Source/Engine/Renderer/RenderList.h
index 8eb3540e0..b4b7121de 100644
--- a/Source/Engine/Renderer/RenderList.h
+++ b/Source/Engine/Renderer/RenderList.h
@@ -4,7 +4,6 @@
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Memory/ArenaAllocation.h"
-#include "Engine/Core/Math/Half.h"
#include "Engine/Graphics/PostProcessSettings.h"
#include "Engine/Graphics/DynamicBuffer.h"
#include "Engine/Scripting/ScriptingObject.h"
@@ -327,6 +326,21 @@ API_CLASS(Sealed) class FLAXENGINE_API RenderList : public ScriptingObject
///
static void CleanupCache();
+ ///
+ /// The rendering extension interface for custom drawing/effects linked to RenderList. Can be used during async scene drawing and further drawing/processing for more optimized rendering.
+ ///
+ class FLAXENGINE_API IExtension
+ {
+ public:
+ IExtension();
+ virtual ~IExtension();
+
+ // Event called before collecting draw calls. Can be used for initialization.
+ virtual void PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch) {}
+ // Event called after collecting draw calls. Can be used for cleanup or to perform additional drawing using collected draw calls data such as batched data processing.
+ virtual void PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch) {}
+ };
+
public:
///
/// Memory storage with all draw-related data that lives during a single frame rendering time. Thread-safe to allocate memory during rendering jobs.
@@ -460,13 +474,14 @@ public:
///
DynamicTypedBuffer TempObjectBuffer;
- typedef Function DelayedDraw;
+ typedef Function DelayedDraw;
void AddDelayedDraw(DelayedDraw&& func);
- void DrainDelayedDraws(RenderContextBatch& renderContextBatch, int32 contextIndex);
+ void DrainDelayedDraws(GPUContext* context, RenderContextBatch& renderContextBatch, int32 renderContextIndex);
///
/// Adds custom callback (eg. lambda) to invoke after scene draw calls are collected on a main thread (some async draw tasks might be active). Allows for safe usage of GPUContext for draw preparations or to perform GPU-driven drawing.
///
+ /// Can be called in async during scene rendering (thread-safe internally). Lambda is allocated by concurrent arena allocator owned by the RenderList.
template
FORCE_INLINE void AddDelayedDraw(const T& lambda)
{
@@ -475,9 +490,13 @@ public:
AddDelayedDraw(MoveTemp(func));
}
+ // IExtension implementation
+ void PreDraw(GPUContext* context, RenderContextBatch& renderContextBatch);
+ void PostDraw(GPUContext* context, RenderContextBatch& renderContextBatch);
+
private:
DynamicVertexBuffer _instanceBuffer;
- Array _delayedDraws;
+ RenderListBuffer _delayedDraws;
public:
///
diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp
index fd7d43c8b..96253934e 100644
--- a/Source/Engine/Renderer/Renderer.cpp
+++ b/Source/Engine/Renderer/Renderer.cpp
@@ -423,6 +423,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
if (setup.UseMotionVectors)
view.Pass |= DrawPass::MotionVectors;
renderContextBatch.GetMainContext() = renderContext; // Sync render context in batch with the current value
+ renderContext.List->PreDraw(context, renderContextBatch);
bool drawShadows = !isGBufferDebug && EnumHasAnyFlags(view.Flags, ViewFlags::Shadows) && ShadowsPass::Instance()->IsReady();
switch (renderContext.View.Mode)
@@ -461,7 +462,8 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
// Perform custom post-scene drawing (eg. GPU dispatches used by VFX)
for (int32 i = 0; i < renderContextBatch.Contexts.Count(); i++)
- renderContextBatch.Contexts[i].List->DrainDelayedDraws(renderContextBatch, i);
+ renderContextBatch.Contexts[i].List->DrainDelayedDraws(context, renderContextBatch, i);
+ renderContext.List->PostDraw(context, renderContextBatch);
#if USE_EDITOR
GBufferPass::Instance()->OverrideDrawCalls(renderContext);
diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp
index 4d26e678b..bbcd7de57 100644
--- a/Source/Engine/Scripting/BinaryModule.cpp
+++ b/Source/Engine/Scripting/BinaryModule.cpp
@@ -683,6 +683,8 @@ BinaryModule* BinaryModule::GetModule(const StringAnsiView& name)
BinaryModule::BinaryModule()
{
+ CanReload = USE_EDITOR;
+
// Register
GetModules().Add(this);
}
diff --git a/Source/Engine/Scripting/BinaryModule.h b/Source/Engine/Scripting/BinaryModule.h
index 70aa60fff..1da35401b 100644
--- a/Source/Engine/Scripting/BinaryModule.h
+++ b/Source/Engine/Scripting/BinaryModule.h
@@ -91,6 +91,11 @@ public:
///
Dictionary TypeNameToTypeIndex;
+ ///
+ /// Determinates whether module can be hot-reloaded at runtime. For example, in Editor after scripts recompilation. Some modules such as engine and class library modules are static.
+ ///
+ bool CanReload;
+
public:
///
diff --git a/Source/Engine/Scripting/ManagedCLR/MAssembly.h b/Source/Engine/Scripting/ManagedCLR/MAssembly.h
index 6c0aa9579..0a785c06a 100644
--- a/Source/Engine/Scripting/ManagedCLR/MAssembly.h
+++ b/Source/Engine/Scripting/ManagedCLR/MAssembly.h
@@ -34,6 +34,7 @@ private:
int32 _isLoaded : 1;
int32 _isLoading : 1;
+ int32 _canReload : 1;
mutable int32 _hasCachedClasses : 1;
mutable ClassesDictionary _classes;
@@ -125,6 +126,14 @@ public:
return _isLoaded != 0;
}
+ ///
+ /// Returns true if assembly can be hot-reloaded at runtime. For example, in Editor after scripts recompilation. Some assemblies such as engine and class library modules are static.
+ ///
+ FORCE_INLINE bool CanReload() const
+ {
+ return USE_EDITOR && _canReload;
+ }
+
///
/// Gets the assembly name.
///
diff --git a/Source/Engine/Scripting/ManagedCLR/MCore.cpp b/Source/Engine/Scripting/ManagedCLR/MCore.cpp
index 350cc39d2..6fa499002 100644
--- a/Source/Engine/Scripting/ManagedCLR/MCore.cpp
+++ b/Source/Engine/Scripting/ManagedCLR/MCore.cpp
@@ -45,6 +45,7 @@ MAssembly::MAssembly(MDomain* domain, const StringAnsiView& name)
: _domain(domain)
, _isLoaded(false)
, _isLoading(false)
+ , _canReload(true)
, _hasCachedClasses(false)
, _reloadCount(0)
, _name(name)
@@ -59,6 +60,7 @@ MAssembly::MAssembly(MDomain* domain, const StringAnsiView& name, const StringAn
, _domain(domain)
, _isLoaded(false)
, _isLoading(false)
+ , _canReload(true)
, _hasCachedClasses(false)
, _reloadCount(0)
, _name(name)
diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp
index 1c8c2bcdd..4be0ce1a1 100644
--- a/Source/Engine/Scripting/Runtime/DotNet.cpp
+++ b/Source/Engine/Scripting/Runtime/DotNet.cpp
@@ -874,6 +874,7 @@ bool MAssembly::LoadCorlib()
return true;
}
_hasCachedClasses = false;
+ _canReload = false;
CachedAssemblyHandles.Add(_handle, this);
// End
diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp
index 4e17bb80a..aa7e26674 100644
--- a/Source/Engine/Scripting/Scripting.cpp
+++ b/Source/Engine/Scripting/Scripting.cpp
@@ -502,6 +502,7 @@ bool Scripting::LoadBinaryModules(const String& path, const String& projectFolde
// C#
if (managedPath.HasChars() && !((ManagedBinaryModule*)module)->Assembly->IsLoaded())
{
+ (((ManagedBinaryModule*)module)->Assembly)->_canReload = module->CanReload;
if (((ManagedBinaryModule*)module)->Assembly->Load(managedPath, nativePath))
{
LOG(Error, "Failed to load C# assembly '{0}' for binary module {1}.", managedPath, name);
@@ -528,6 +529,7 @@ bool Scripting::Load()
#if USE_CSHARP
// Load C# core assembly
ManagedBinaryModule* corlib = GetBinaryModuleCorlib();
+ corlib->CanReload = false;
if (corlib->Assembly->LoadCorlib())
{
LOG(Error, "Failed to load corlib C# assembly.");
@@ -581,6 +583,8 @@ bool Scripting::Load()
LOG(Error, "Failed to load FlaxEngine C# assembly.");
return true;
}
+ flaxEngineModule->CanReload = false;
+ flaxEngineModule->Assembly->_canReload = false;
onEngineLoaded(flaxEngineModule->Assembly);
// Insert type aliases for vector types that don't exist in C++ but are just typedef (properly redirect them to actual types)
diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs
index 4321ffa36..c8d00567a 100644
--- a/Source/Engine/Serialization/JsonSerializer.cs
+++ b/Source/Engine/Serialization/JsonSerializer.cs
@@ -270,8 +270,8 @@ namespace FlaxEngine.Json
// Special case when saving reference to prefab object and the objects are different but the point to the same prefab object
// In that case, skip saving reference as it's defined in prefab (will be populated via IdsMapping during deserialization)
- if (objA is SceneObject sceneA && objB is SceneObject sceneB && sceneA && sceneB && sceneA.HasPrefabLink && sceneB.HasPrefabLink)
- return sceneA.PrefabObjectID == sceneB.PrefabObjectID;
+ if (objA is SceneObject sceneObjA && objB is SceneObject sceneObjB && sceneObjA && sceneObjB && sceneObjA.HasPrefabLink && sceneObjB.HasPrefabLink)
+ return sceneObjA.PrefabObjectID == sceneObjB.PrefabObjectID;
// Comparing an Int32 and Int64 both of the same value returns false, make types the same then compare
if (objA.GetType() != objB.GetType())
@@ -286,7 +286,6 @@ namespace FlaxEngine.Json
type == typeof(Int32) ||
type == typeof(UInt32) ||
type == typeof(Int64) ||
- type == typeof(SByte) ||
type == typeof(UInt64);
}
if (IsInteger(objA) && IsInteger(objB))
@@ -301,6 +300,12 @@ namespace FlaxEngine.Json
{
if (aList.Count != bList.Count)
return false;
+ for (int i = 0; i < aList.Count; i++)
+ {
+ if (!ValueEquals(aList[i], bList[i]))
+ return false;
+ }
+ return true;
}
if (objA is IEnumerable aEnumerable && objB is IEnumerable bEnumerable)
{
@@ -316,8 +321,30 @@ namespace FlaxEngine.Json
return !bEnumerator.MoveNext();
}
- if (objA is ICustomValueEquals customValueEquals && objA.GetType() == objB.GetType())
+ // Custom comparer
+ if (objA is ICustomValueEquals customValueEquals)
return customValueEquals.ValueEquals(objB);
+
+ // If type contains SceneObject references then it needs to use custom comparision that handles prefab links (see SceneObjectEquals)
+ var typeA = objA.GetType();
+ if (typeA.IsValueType && !typeA.IsEnum && !typeA.IsPrimitive)
+ {
+ var contract = Settings.ContractResolver.ResolveContract(typeA);
+ if (contract is JsonObjectContract objContract)
+ {
+ foreach (var property in objContract.Properties)
+ {
+ var valueProvider = property.ValueProvider;
+ var propA = valueProvider.GetValue(objA);
+ var propB = valueProvider.GetValue(objB);
+ if (!ValueEquals(propA, propB))
+ return false;
+ }
+ return true;
+ }
+ }
+
+ // Generic fallback
return objA.Equals(objB);
#endif
}
diff --git a/Source/Engine/Serialization/Serialization.cpp b/Source/Engine/Serialization/Serialization.cpp
index a3dfc6ffa..1eb6b0181 100644
--- a/Source/Engine/Serialization/Serialization.cpp
+++ b/Source/Engine/Serialization/Serialization.cpp
@@ -78,7 +78,10 @@ void Serialization::Deserialize(ISerializable::DeserializeStream& stream, Varian
v.Type = VariantType::Null;
const auto mTypeName = SERIALIZE_FIND_MEMBER(stream, "TypeName");
if (mTypeName != stream.MemberEnd() && mTypeName->value.IsString())
+ {
v.SetTypeName(StringAnsiView(mTypeName->value.GetStringAnsiView()));
+ v.Inline();
+ }
}
else
{
diff --git a/Source/Engine/Serialization/Serialization.h b/Source/Engine/Serialization/Serialization.h
index d3205ad83..9af6d7be1 100644
--- a/Source/Engine/Serialization/Serialization.h
+++ b/Source/Engine/Serialization/Serialization.h
@@ -415,7 +415,7 @@ namespace Serialization
inline bool ShouldSerialize(const ISerializable& v, const void* otherObj)
{
- return true;
+ return !otherObj || v.ShouldSerialize(otherObj);
}
inline void Serialize(ISerializable::SerializeStream& stream, const ISerializable& v, const void* otherObj)
{
@@ -431,7 +431,7 @@ namespace Serialization
template
inline typename TEnableIf::Value, bool>::Type ShouldSerialize(const ISerializable& v, const void* otherObj)
{
- return true;
+ return !otherObj || v.ShouldSerialize(otherObj);
}
template
inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, const ISerializable& v, const void* otherObj)
diff --git a/Source/Engine/Serialization/Stream.cpp b/Source/Engine/Serialization/Stream.cpp
index f95e9ef9b..4c9b94042 100644
--- a/Source/Engine/Serialization/Stream.cpp
+++ b/Source/Engine/Serialization/Stream.cpp
@@ -255,6 +255,7 @@ void ReadStream::Read(VariantType& data)
ptr++;
}
*ptr = 0;
+ data.Inline();
}
else if (typeNameLength > 0)
{
diff --git a/Source/Engine/Threading/ConcurrentDictionary.h b/Source/Engine/Threading/ConcurrentDictionary.h
new file mode 100644
index 000000000..1b78a735e
--- /dev/null
+++ b/Source/Engine/Threading/ConcurrentDictionary.h
@@ -0,0 +1,318 @@
+// Copyright (c) Wojciech Figat. All rights reserved.
+
+#pragma once
+
+#include "Engine/Core/Collections/Dictionary.h"
+#include "Engine/Platform/CriticalSection.h"
+
+///
+/// Template for unordered dictionary with mapped key with value pairs that supports asynchronous data reading and writing.
+/// Implemented via reader-writer lock pattern, so multiple threads can read data at the same time, but only one thread can write data and it blocks all other threads (including readers) until the write operation is finished.
+/// Optimized for frequent reads (no lock operation).
+///
+/// The type of the keys in the dictionary.
+/// The type of the values in the dictionary.
+/// The type of memory allocator.
+template
+class ConcurrentDictionary : Dictionary
+{
+ friend ConcurrentDictionary;
+public:
+ typedef Dictionary Base;
+ typedef DictionaryBucket Bucket;
+ using AllocationData = typename AllocationType::template Data;
+ using AllocationTag = typename AllocationType::Tag;
+
+private:
+ mutable volatile int64 _threadsReading = 0;
+ volatile int64 _threadsWriting = 0;
+ CriticalSection _locker;
+
+public:
+ ///
+ /// Initializes an empty without reserving any space.
+ ///
+ ConcurrentDictionary()
+ {
+ }
+
+ ///
+ /// Initializes an empty without reserving any space.
+ ///
+ /// The custom allocation tag.
+ ConcurrentDictionary(AllocationTag tag)
+ : Base(tag)
+ {
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~ConcurrentDictionary()
+ {
+ Clear();
+ }
+
+public:
+ ///
+ /// Gets the amount of the elements in the collection.
+ ///
+ int32 Count() const
+ {
+ Reader reader(this);
+ return Base::_elementsCount;
+ }
+
+ ///
+ /// Gets the amount of the elements that can be contained by the collection.
+ ///
+ int32 Capacity() const
+ {
+ Reader reader(this);
+ return Base::_size;
+ }
+
+ ///
+ /// Tries to get element with given key.
+ ///
+ /// The key of the element.
+ /// The result value.
+ /// True if element of given key has been found, otherwise false.
+ template
+ bool TryGet(const KeyComparableType& key, ValueType& result) const
+ {
+ Reader reader(this);
+ typename Base::FindPositionResult pos;
+ Base::FindPosition(key, pos);
+ if (pos.ObjectIndex != -1)
+ result = Base::_allocation.Get()[pos.ObjectIndex].Value;
+ return pos.ObjectIndex != -1;
+ }
+
+public:
+ ///
+ /// Adds a pair of key and value to the collection.
+ ///
+ /// The key.
+ /// The value.
+ /// True if added element, otherwise false if it already exists (or other thread added it).
+ template
+ bool Add(const KeyComparableType& key, const ValueType& value)
+ {
+ Writer writer(this);
+ Bucket* bucket = Base::OnAdd(key, false, true);
+ if (bucket)
+ bucket->Occupy(key, value);
+ return bucket != nullptr;
+ }
+
+ ///
+ /// Removes element with a specified key.
+ ///
+ /// The element key to remove.
+ /// True if item was removed from collection, otherwise false.
+ template
+ bool Remove(const KeyComparableType& key)
+ {
+ Writer writer(this);
+ return Base::Remove(key);
+ }
+
+public:
+ ///
+ /// Removes all elements from the collection.
+ ///
+ void Clear()
+ {
+ Writer writer(this);
+ Base::Clear();
+ }
+
+public:
+ ///
+ /// The read-only dictionary collection iterator.
+ ///
+ struct ConstIterator : Base::IteratorBase
+ {
+ friend ConcurrentDictionary;
+ public:
+ ConstIterator(const ConcurrentDictionary* collection, const int32 index)
+ : Base::IteratorBase(collection, index)
+ {
+ if (collection)
+ collection->BeginRead();
+ }
+
+ ConstIterator(const ConstIterator& i)
+ : Base::IteratorBase(i._collection, i._index)
+ {
+ if (i.collection)
+ i.collection->BeginRead();
+ }
+
+ ConstIterator(ConstIterator&& i) noexcept
+ : Base::IteratorBase(i._collection, i._index)
+ {
+ i._collection = nullptr;
+ }
+
+ ~ConstIterator()
+ {
+ if (this->_collection)
+ ((ConcurrentDictionary*)this->_collection)->EndRead();
+ }
+
+ public:
+ FORCE_INLINE bool operator!() const
+ {
+ return !(bool)*this;
+ }
+
+ FORCE_INLINE bool operator==(const ConstIterator& v) const
+ {
+ return this->_index == v._index && this->_collection == v._collection;
+ }
+
+ FORCE_INLINE bool operator!=(const ConstIterator& v) const
+ {
+ return this->_index != v._index || this->_collection != v._collection;
+ }
+
+ ConstIterator& operator=(const ConstIterator& v)
+ {
+ this->_collection = v._collection;
+ this->_index = v._index;
+ return *this;
+ }
+
+ ConstIterator& operator=(ConstIterator&& v) noexcept
+ {
+ this->_collection = v._collection;
+ this->_index = v._index;
+ v._collection = nullptr;
+ return *this;
+ }
+
+ ConstIterator& operator++()
+ {
+ this->Next();
+ return *this;
+ }
+
+ ConstIterator operator++(int) const
+ {
+ ConstIterator i = *this;
+ i.Next();
+ return i;
+ }
+
+ ConstIterator& operator--()
+ {
+ this->Prev();
+ return *this;
+ }
+
+ ConstIterator operator--(int) const
+ {
+ ConstIterator i = *this;
+ i.Prev();
+ return i;
+ }
+ };
+
+ ConstIterator begin() const
+ {
+ ConstIterator i(this, -1);
+ ++i;
+ return i;
+ }
+
+ FORCE_INLINE ConstIterator end() const
+ {
+ return ConstIterator(this, Base::_size);
+ }
+
+private:
+ void BeginWrite()
+ {
+ Platform::InterlockedIncrement(&_threadsWriting);
+
+ // Wait for all reads to end
+ RETRY:
+ while (Platform::AtomicRead(&_threadsReading))
+ Platform::Yield();
+
+ // Thread-safe writing
+ _locker.Lock();
+ if (Platform::AtomicRead(&_threadsReading))
+ {
+ // Other reader entered during mutex locking so give them a chance to transition into active-waiting
+ _locker.Unlock();
+ goto RETRY;
+ }
+ }
+
+ void EndWrite()
+ {
+ _locker.Unlock();
+ Platform::InterlockedDecrement(&_threadsWriting);
+ }
+
+ void BeginRead() const
+ {
+ RETRY:
+ Platform::InterlockedIncrement(&_threadsReading);
+
+ // Check if any thread is writing (or is about to write)
+ if (Platform::AtomicRead(&_threadsWriting) != 0)
+ {
+ // Wait for all writes to end
+ Platform::InterlockedDecrement(&_threadsReading);
+ while (Platform::AtomicRead(&_threadsWriting))
+ Platform::Yield();
+
+ // Try again
+ goto RETRY;
+ }
+ }
+
+ void EndRead() const
+ {
+ Platform::InterlockedDecrement(&_threadsReading);
+ }
+
+private:
+ // Utility for methods that read-write state.
+ struct Writer
+ {
+ ConcurrentDictionary* _collection;
+
+ Writer(ConcurrentDictionary* collection)
+ : _collection(collection)
+ {
+ _collection->BeginWrite();
+ }
+
+ ~Writer()
+ {
+ _collection->EndWrite();
+ }
+ };
+
+ // Utility for methods that read-only state.
+ struct Reader
+ {
+ const ConcurrentDictionary* _collection;
+
+ Reader(const ConcurrentDictionary* collection)
+ : _collection(collection)
+ {
+ _collection->BeginRead();
+ }
+
+ ~Reader()
+ {
+ _collection->EndRead();
+ }
+ };
+};
diff --git a/Source/Engine/Threading/JobSystem.cpp b/Source/Engine/Threading/JobSystem.cpp
index 692a088b7..8d62aa8e3 100644
--- a/Source/Engine/Threading/JobSystem.cpp
+++ b/Source/Engine/Threading/JobSystem.cpp
@@ -8,7 +8,6 @@
#include "Engine/Core/Types/Span.h"
#include "Engine/Core/Types/Pair.h"
#include "Engine/Core/Memory/SimpleHeapAllocation.h"
-#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Core/Collections/RingBuffer.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Profiler/ProfilerCPU.h"
@@ -22,14 +21,6 @@
#if JOB_SYSTEM_ENABLED
-// Local allocator for job system memory that uses internal pooling and assumes that JobsLocker is taken (write access owned by the calling thread).
-class JobSystemAllocation : public SimpleHeapAllocation
-{
-public:
- static void* Allocate(uintptr size);
- static void Free(void* ptr, uintptr size);
-};
-
class JobSystemService : public EngineService
{
public:
@@ -43,30 +34,25 @@ public:
void Dispose() override;
};
-struct JobData
+// Holds a single job dispatch data
+struct alignas(int64) JobContext
{
- int32 Index;
- int64 JobKey;
-};
-
-template<>
-struct TIsPODType
-{
- enum { Value = true };
-};
-
-struct JobContext
-{
- volatile int64 JobsLeft;
- int32 DependenciesLeft;
+ // The next index of the job to process updated when picking a job by the thread.
+ volatile int64 JobIndex = 0;
+ // The number of jobs left to process updated after job completion by the thread.
+ volatile int64 JobsLeft = 0;
+ // The unique label of this job used to identify it. Set to -1 when job is done.
+ volatile int64 JobLabel = 0;
+ // Utility atomic counter used to indicate that any job is waiting for this one to finish. Then Dependants can be accessed within thread-safe JobsLocker.
+ volatile int64 DependantsCount = 0;
+ // The number of dependency jobs left to be finished before starting this job.
+ volatile int64 DependenciesLeft = 0;
+ // The total number of jobs to process (in this context).
+ int32 JobsCount = 0;
+ // The job function to execute.
Function Job;
- Array Dependants;
-};
-
-template<>
-struct TIsPODType
-{
- enum { Value = false };
+ // List of dependant jobs to signal when this job is done.
+ Array Dependants;
};
class JobSystemThread : public IRunnable
@@ -92,50 +78,36 @@ public:
namespace
{
JobSystemService JobSystemInstance;
- Array> MemPool;
Thread* Threads[PLATFORM_THREADS_LIMIT / 2] = {};
int32 ThreadsCount = 0;
bool JobStartingOnDispatch = true;
volatile int64 ExitFlag = 0;
volatile int64 JobLabel = 0;
- Dictionary JobContexts;
+ volatile int64 JobEndLabel = 0;
+ volatile int64 JobStartLabel = 0;
+ volatile int64 JobContextsCount = 0;
+ uint32 JobContextsSize = 0;
+ uint32 JobContextsMask = 0;
+ JobContext* JobContexts = nullptr;
ConditionVariable JobsSignal;
CriticalSection JobsMutex;
ConditionVariable WaitSignal;
CriticalSection WaitMutex;
CriticalSection JobsLocker;
- RingBuffer Jobs;
-}
-
-void* JobSystemAllocation::Allocate(uintptr size)
-{
- void* result = nullptr;
- for (int32 i = 0; i < MemPool.Count(); i++)
- {
- if (MemPool.Get()[i].Second == size)
- {
- result = MemPool.Get()[i].First;
- MemPool.RemoveAt(i);
- break;
- }
- }
- if (!result)
- {
- PROFILE_MEM(EngineThreading);
- result = Platform::Allocate(size, 16);
- }
- return result;
-}
-
-void JobSystemAllocation::Free(void* ptr, uintptr size)
-{
- PROFILE_MEM(EngineThreading);
- MemPool.Add({ ptr, size });
+#define GET_CONTEXT_INDEX(label) (uint32)((label) & (int64)JobContextsMask)
}
bool JobSystemService::Init()
{
PROFILE_MEM(EngineThreading);
+
+ // Initialize job context storage (fixed-size ring buffer for active jobs tracking)
+ JobContextsSize = 256;
+ JobContextsMask = JobContextsSize - 1;
+ JobContexts = (JobContext*)Platform::Allocate(JobContextsSize * sizeof(JobContext), alignof(JobContext));
+ Memory::ConstructItems(JobContexts, (int32)JobContextsSize);
+
+ // Spawn threads
ThreadsCount = Math::Min(Platform::GetCPUInfo().LogicalProcessorCount, ARRAY_COUNT(Threads));
for (int32 i = 0; i < ThreadsCount; i++)
{
@@ -146,6 +118,7 @@ bool JobSystemService::Init()
return true;
Threads[i] = thread;
}
+
return false;
}
@@ -171,35 +144,67 @@ void JobSystemService::Dispose()
}
}
- JobContexts.SetCapacity(0);
- Jobs.Release();
- for (auto& e : MemPool)
- Platform::Free(e.First);
- MemPool.Clear();
+ Memory::DestructItems(JobContexts, (int32)JobContextsSize);
+ Platform::Free(JobContexts);
+ JobContexts = nullptr;
}
int32 JobSystemThread::Run()
{
+ // Pin thread to the physical core
Platform::SetThreadAffinityMask(1ull << Index);
- JobData data;
- Function job;
bool attachCSharpThread = true;
MONO_THREAD_INFO_TYPE* monoThreadInfo = nullptr;
while (Platform::AtomicRead(&ExitFlag) == 0)
{
// Try to get a job
- JobsLocker.Lock();
- if (Jobs.Count() != 0)
+ int32 jobIndex;
+ JobContext* jobContext = nullptr;
{
- data = Jobs.PeekFront();
- Jobs.PopFront();
- const JobContext& context = ((const Dictionary&)JobContexts).At(data.JobKey);
- job = context.Job;
- }
- JobsLocker.Unlock();
+ int64 jobOffset = 0;
+ RETRY:
+ int64 jobStartLabel = Platform::AtomicRead(&JobStartLabel) + jobOffset;
+ int64 jobEndLabel = Platform::AtomicRead(&JobEndLabel);
+ if (jobStartLabel <= jobEndLabel && jobEndLabel > 0)
+ {
+ jobContext = &JobContexts[GET_CONTEXT_INDEX(jobStartLabel)];
+ if (Platform::AtomicRead(&jobContext->DependenciesLeft) > 0)
+ {
+ // This job still waits for dependency so skip it for now and try the next one
+ jobOffset++;
+ jobContext = nullptr;
+ goto RETRY;
+ }
- if (job.IsBinded())
+ // Move forward with index for a job
+ jobIndex = (int32)(Platform::InterlockedIncrement(&jobContext->JobIndex) - 1);
+ if (jobIndex < jobContext->JobsCount)
+ {
+ // Index is valid
+ }
+ else if (jobStartLabel < jobEndLabel && jobOffset == 0)
+ {
+ // No more jobs inside this context, move to the next one
+ Platform::InterlockedCompareExchange(&JobStartLabel, jobStartLabel + 1, jobStartLabel);
+ jobContext = nullptr;
+ goto RETRY;
+ }
+ else
+ {
+ // No more jobs
+ jobContext = nullptr;
+ if (jobStartLabel < jobEndLabel)
+ {
+ // Try with a different one before going to sleep
+ jobOffset++;
+ goto RETRY;
+ }
+ }
+ }
+ }
+
+ if (jobContext)
{
#if USE_CSHARP
// Ensure to have C# thread attached to this thead (late init due to MCore being initialized after Job System)
@@ -212,37 +217,39 @@ int32 JobSystemThread::Run()
#endif
// Run job
- job(data.Index);
+ jobContext->Job(jobIndex);
// Move forward with the job queue
- bool notifyWaiting = false;
- JobsLocker.Lock();
- JobContext& context = JobContexts.At(data.JobKey);
- if (Platform::InterlockedDecrement(&context.JobsLeft) <= 0)
+ if (Platform::InterlockedDecrement(&jobContext->JobsLeft) <= 0)
{
- // Update any dependant jobs
- for (int64 dependant : context.Dependants)
+ // Mark job as done before processing dependants
+ Platform::AtomicStore(&jobContext->JobLabel, -1);
+
+ // Check if any other job waits on this one
+ if (Platform::AtomicRead(&jobContext->DependantsCount) != 0)
{
- JobContext& dependantContext = JobContexts.At(dependant);
- if (--dependantContext.DependenciesLeft <= 0)
+ // Update dependant jobs
+ JobsLocker.Lock();
+ for (int64 dependant : jobContext->Dependants)
{
- // Dispatch dependency when it's ready
- JobData dependantData;
- dependantData.JobKey = dependant;
- for (dependantData.Index = 0; dependantData.Index < dependantContext.JobsLeft; dependantData.Index++)
- Jobs.PushBack(dependantData);
+ JobContext& dependantContext = JobContexts[GET_CONTEXT_INDEX(dependant)];
+ if (dependantContext.JobLabel == dependant)
+ Platform::InterlockedDecrement(&dependantContext.DependenciesLeft);
}
+ JobsLocker.Unlock();
}
- // Remove completed context
- JobContexts.Remove(data.JobKey);
- notifyWaiting = true;
- }
- JobsLocker.Unlock();
- if (notifyWaiting)
- WaitSignal.NotifyAll();
+ // Cleanup completed context
+ jobContext->Job.Unbind();
+ jobContext->Dependants.Clear();
+ Platform::AtomicStore(&jobContext->DependantsCount, 0);
+ Platform::AtomicStore(&jobContext->DependenciesLeft, -999); // Mark to indicate deleted context
+ Platform::AtomicStore(&jobContext->JobLabel, -1);
+ Platform::InterlockedDecrement(&JobContextsCount);
- job.Unbind();
+ // Wakeup any thread waiting for the jobs to complete
+ WaitSignal.NotifyAll();
+ }
}
else
{
@@ -266,8 +273,8 @@ void JobSystem::Execute(const Function& job, int32 jobCount)
if (jobCount > 1)
{
// Async
- const int64 jobWaitHandle = Dispatch(job, jobCount);
- Wait(jobWaitHandle);
+ const int64 label = Dispatch(job, jobCount);
+ Wait(label);
}
else
#endif
@@ -284,21 +291,31 @@ int64 JobSystem::Dispatch(const Function& job, int32 jobCount)
return 0;
PROFILE_CPU();
#if JOB_SYSTEM_ENABLED
- const auto label = Platform::InterlockedAdd(&JobLabel, (int64)jobCount) + jobCount;
+ while (Platform::InterlockedIncrement(&JobContextsCount) >= JobContextsSize)
+ {
+ // Too many jobs in flight, wait for some to complete to free up contexts
+ PROFILE_CPU_NAMED("JOB SYSTEM OVERFLOW");
+ ZoneColor(TracyWaitZoneColor);
+ Platform::InterlockedDecrement(&JobContextsCount);
+ Platform::Sleep(1);
+ }
- JobData data;
- data.JobKey = label;
+ // Get a new label
+ const int64 label = Platform::InterlockedIncrement(&JobLabel);
- JobContext context;
+ // Build job
+ JobContext& context = JobContexts[GET_CONTEXT_INDEX(label)];
context.Job = job;
+ context.JobIndex = 0;
context.JobsLeft = jobCount;
+ context.JobLabel = label;
+ context.DependantsCount = 0;
context.DependenciesLeft = 0;
+ context.JobsCount = jobCount;
+ context.Dependants.Clear();
- JobsLocker.Lock();
- JobContexts.Add(label, MoveTemp(context));
- for (data.Index = 0; data.Index < jobCount; data.Index++)
- Jobs.PushBack(data);
- JobsLocker.Unlock();
+ // Move the job queue forward
+ Platform::InterlockedIncrement(&JobEndLabel);
if (JobStartingOnDispatch)
{
@@ -321,34 +338,47 @@ int64 JobSystem::Dispatch(const Function& job, Span dependen
if (jobCount <= 0)
return 0;
PROFILE_CPU();
+ PROFILE_MEM(EngineThreading);
#if JOB_SYSTEM_ENABLED
- const auto label = Platform::InterlockedAdd(&JobLabel, (int64)jobCount) + jobCount;
+ while (Platform::InterlockedIncrement(&JobContextsCount) >= JobContextsSize)
+ {
+ // Too many jobs in flight, wait for some to complete to free up contexts
+ PROFILE_CPU_NAMED("JOB SYSTEM OVERFLOW");
+ ZoneColor(TracyWaitZoneColor);
+ Platform::InterlockedDecrement(&JobContextsCount);
+ Platform::Sleep(1);
+ }
- JobData data;
- data.JobKey = label;
+ // Get a new label
+ const int64 label = Platform::InterlockedIncrement(&JobLabel);
- JobContext context;
+ // Build job
+ JobContext& context = JobContexts[GET_CONTEXT_INDEX(label)];
context.Job = job;
+ context.JobIndex = 0;
context.JobsLeft = jobCount;
+ context.JobLabel = label;
+ context.DependantsCount = 0;
context.DependenciesLeft = 0;
-
- JobsLocker.Lock();
- for (int64 dependency : dependencies)
+ context.JobsCount = jobCount;
+ context.Dependants.Clear();
{
- if (JobContext* dependencyContext = JobContexts.TryGet(dependency))
+ JobsLocker.Lock();
+ for (int64 dependency : dependencies)
{
- context.DependenciesLeft++;
- dependencyContext->Dependants.Add(label);
+ JobContext& dependencyContext = JobContexts[GET_CONTEXT_INDEX(dependency)];
+ if (Platform::AtomicRead(&dependencyContext.JobLabel) == dependency)
+ {
+ Platform::InterlockedIncrement(&dependencyContext.DependantsCount);
+ dependencyContext.Dependants.Add(label);
+ context.DependenciesLeft++;
+ }
}
+ JobsLocker.Unlock();
}
- JobContexts.Add(label, MoveTemp(context));
- if (context.DependenciesLeft == 0)
- {
- // No dependencies left to complete so dispatch now
- for (data.Index = 0; data.Index < jobCount; data.Index++)
- Jobs.PushBack(data);
- }
- JobsLocker.Unlock();
+
+ // Move the job queue forward
+ Platform::InterlockedIncrement(&JobEndLabel);
if (context.DependenciesLeft == 0 && JobStartingOnDispatch)
{
@@ -369,19 +399,17 @@ int64 JobSystem::Dispatch(const Function& job, Span dependen
void JobSystem::Wait()
{
#if JOB_SYSTEM_ENABLED
- JobsLocker.Lock();
- int32 numJobs = JobContexts.Count();
- JobsLocker.Unlock();
+ PROFILE_CPU();
+ ZoneColor(TracyWaitZoneColor);
+ int64 numJobs = Platform::AtomicRead(&JobContextsCount);
while (numJobs > 0)
{
WaitMutex.Lock();
WaitSignal.Wait(WaitMutex, 1);
WaitMutex.Unlock();
- JobsLocker.Lock();
- numJobs = JobContexts.Count();
- JobsLocker.Unlock();
+ numJobs = Platform::AtomicRead(&JobContextsCount);
}
#endif
}
@@ -394,12 +422,11 @@ void JobSystem::Wait(int64 label)
while (Platform::AtomicRead(&ExitFlag) == 0)
{
- JobsLocker.Lock();
- const JobContext* context = JobContexts.TryGet(label);
- JobsLocker.Unlock();
+ const JobContext& context = JobContexts[GET_CONTEXT_INDEX(label)];
+ const bool finished = Platform::AtomicRead(&context.JobLabel) != label || Platform::AtomicRead(&context.JobsLeft) <= 0;
// Skip if context has been already executed (last job removes it)
- if (!context)
+ if (finished)
break;
// Wait on signal until input label is not yet done
@@ -417,15 +444,10 @@ void JobSystem::SetJobStartingOnDispatch(bool value)
{
#if JOB_SYSTEM_ENABLED
JobStartingOnDispatch = value;
- if (value)
+ if (value && (Platform::AtomicRead(&JobEndLabel) - Platform::AtomicRead(&JobStartLabel)) > 0)
{
- JobsLocker.Lock();
- const int32 count = Jobs.Count();
- JobsLocker.Unlock();
- if (count == 1)
- JobsSignal.NotifyOne();
- else if (count != 0)
- JobsSignal.NotifyAll();
+ // Wake up threads to start processing jobs that may be already in the queue
+ JobsSignal.NotifyAll();
}
#endif
}
diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp
index 09ac78e6b..f3d830382 100644
--- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp
+++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp
@@ -384,7 +384,7 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value)
// Apply hardness, use 0.991 as max since any value above will result in harsh aliasing
auto x2 = writeLocal(ValueType::Float, String::Format(TEXT("saturate((1 - {0}) * (1 / (1 - clamp({1}, 0, 0.991f))))"), x1.Value, hardness.Value), node);
- value = writeLocal(ValueType::Float, String::Format(TEXT("{0} ? (1 - {1}) : {1}"), invert.Value, x2.Value), node);
+ value = writeLocal(ValueType::Float, String::Format(TEXT("select({0}, (1 - {1}), {1})"), invert.Value, x2.Value), node);
break;
}
// Tiling & Offset
@@ -459,7 +459,7 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value)
auto x = writeLocal(ValueType::Float, String::Format(TEXT("56100000.0f * pow({0}, -1) + 148.0f"), temperature.Value), node);
// Value Y
- auto y = writeLocal(ValueType::Float, String::Format(TEXT("{0} > 6500.0f ? 35200000.0f * pow({0}, -1) + 184.0f : 100.04f * log({0}) - 623.6f"), temperature.Value), node);
+ auto y = writeLocal(ValueType::Float, String::Format(TEXT("select({0} > 6500.0f, 35200000.0f * pow({0}, -1) + 184.0f, 100.04f * log({0}) - 623.6f)"), temperature.Value), node);
// Value Z
auto z = writeLocal(ValueType::Float, String::Format(TEXT("194.18f * log({0}) - 1448.6f"), temperature.Value), node);
@@ -467,7 +467,7 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value)
// Final color
auto color = writeLocal(ValueType::Float3, String::Format(TEXT("float3({0}, {1}, {2})"), x.Value, y.Value, z.Value), node);
color = writeLocal(ValueType::Float3, String::Format(TEXT("clamp({0}, 0.0f, 255.0f) / 255.0f"), color.Value), node);
- value = writeLocal(ValueType::Float3, String::Format(TEXT("{1} < 1000.0f ? {0} * {1}/1000.0f : {0}"), color.Value, temperature.Value), node);
+ value = writeLocal(ValueType::Float3, String::Format(TEXT("select({1} < 1000.0f, {0} * {1}/1000.0f, {0})"), color.Value, temperature.Value), node);
break;
}
// HSVToRGB
@@ -490,8 +490,8 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value)
const auto rgb = tryGetValue(node->GetBox(0), node->Values[0]).AsFloat3();
const auto epsilon = writeLocal(ValueType::Float, TEXT("1e-10"), node);
- auto p = writeLocal(ValueType::Float4, String::Format(TEXT("({0}.g < {0}.b) ? float4({0}.bg, -1.0f, 2.0f/3.0f) : float4({0}.gb, 0.0f, -1.0f/3.0f)"), rgb.Value), node);
- auto q = writeLocal(ValueType::Float4, String::Format(TEXT("({0}.r < {1}.x) ? float4({1}.xyw, {0}.r) : float4({0}.r, {1}.yzx)"), rgb.Value, p.Value), node);
+ auto p = writeLocal(ValueType::Float4, String::Format(TEXT("select(({0}.g < {0}.b), float4({0}.bg, -1.0f, 2.0f/3.0f), float4({0}.gb, 0.0f, -1.0f/3.0f))"), rgb.Value), node);
+ auto q = writeLocal(ValueType::Float4, String::Format(TEXT("select(({0}.r < {1}.x), float4({1}.xyw, {0}.r), float4({0}.r, {1}.yzx))"), rgb.Value, p.Value), node);
auto c = writeLocal(ValueType::Float, String::Format(TEXT("{0}.x - min({0}.w, {0}.y)"), q.Value), node);
auto h = writeLocal(ValueType::Float, String::Format(TEXT("abs(({0}.w - {0}.y) / (6 * {1} + {2}) + {0}.z)"), q.Value, c.Value, epsilon.Value), node);
@@ -721,13 +721,13 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value)
blendFormula = TEXT("1.0 - (1.0 - base) * (1.0 - blend)");
break;
case 5: // Overlay
- blendFormula = TEXT("base <= 0.5 ? 2.0 * base * blend : 1.0 - 2.0 * (1.0 - base) * (1.0 - blend)");
+ blendFormula = TEXT("select(base <= 0.5, 2.0 * base * blend, 1.0 - 2.0 * (1.0 - base) * (1.0 - blend))");
break;
case 6: // Linear Burn
blendFormula = TEXT("base + blend - 1.0");
break;
case 7: // Linear Light
- blendFormula = TEXT("blend < 0.5 ? max(base + (2.0 * blend) - 1.0, 0.0) : min(base + 2.0 * (blend - 0.5), 1.0)");
+ blendFormula = TEXT("select(blend < 0.5, max(base + (2.0 * blend) - 1.0, 0.0), min(base + 2.0 * (blend - 0.5), 1.0))");
break;
case 8: // Darken
blendFormula = TEXT("min(base, blend)");
@@ -745,10 +745,10 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value)
blendFormula = TEXT("base / (blend + 0.000001)");
break;
case 13: // Hard Light
- blendFormula = TEXT("blend <= 0.5 ? 2.0 * base * blend : 1.0 - 2.0 * (1.0 - base) * (1.0 - blend)");
+ blendFormula = TEXT("select(blend <= 0.5, 2.0 * base * blend, 1.0 - 2.0 * (1.0 - base) * (1.0 - blend))");
break;
case 14: // Pin Light
- blendFormula = TEXT("blend <= 0.5 ? min(base, 2.0 * blend) : max(base, 2.0 * (blend - 0.5))");
+ blendFormula = TEXT("select(blend <= 0.5, min(base, 2.0 * blend), max(base, 2.0 * (blend - 0.5)))");
break;
case 15: // Hard Mix
blendFormula = TEXT("step(1.0 - base, blend)");
diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
index 7978b4f9e..13b835185 100644
--- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
+++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp
@@ -585,7 +585,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value)
const auto offset = useOffset ? eatBox(offsetBox->GetParent(), offsetBox->FirstConnection()) : Value::Zero;
const Char* samplerName;
const int32 samplerIndex = node->Values[0].AsInt;
- if (samplerIndex == TextureGroup)
+ if (samplerIndex == TextureGroup && node->Values.Count() > 2)
{
auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[2].AsInt);
samplerName = *textureGroupSampler.ShaderName;
diff --git a/Source/Engine/UI/GUI/ContainerControl.cs b/Source/Engine/UI/GUI/ContainerControl.cs
index ada93ff1e..55fcecf80 100644
--- a/Source/Engine/UI/GUI/ContainerControl.cs
+++ b/Source/Engine/UI/GUI/ContainerControl.cs
@@ -901,6 +901,15 @@ namespace FlaxEngine.GUI
internal bool RayCastChildren(ref Float2 location, out Control hit)
{
+ if (_clipChildren)
+ {
+ GetDesireClientArea(out var clientArea);
+ if (!clientArea.Contains(ref location))
+ {
+ hit = null;
+ return false;
+ }
+ }
for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--)
{
var child = _children[i];
diff --git a/Source/Engine/Video/MF/VideoBackendMF.cpp b/Source/Engine/Video/MF/VideoBackendMF.cpp
index df24f5eed..3e738d09f 100644
--- a/Source/Engine/Video/MF/VideoBackendMF.cpp
+++ b/Source/Engine/Video/MF/VideoBackendMF.cpp
@@ -47,6 +47,8 @@ struct VideoPlayerMF
namespace MF
{
Array Players;
+ TimeSpan UpdateDeltaTime;
+ double UpdateTime;
bool Configure(VideoBackendPlayer& player, VideoPlayerMF& playerMF, DWORD streamIndex)
{
@@ -395,13 +397,6 @@ namespace MF
if (!playerMF.Playing && !playerMF.Seek)
return;
- bool useTimeScale = true;
-#if USE_EDITOR
- if (!Editor::IsPlayMode)
- useTimeScale = false;
-#endif
- TimeSpan dt = useTimeScale ? Time::Update.DeltaTime : Time::Update.UnscaledDeltaTime;
-
// Update playback time
if (playerMF.FirstFrame)
{
@@ -410,9 +405,9 @@ namespace MF
}
else if (playerMF.Playing)
{
- playerMF.Time += dt;
+ playerMF.Time += UpdateDeltaTime;
}
- if (playerMF.Time > player.Duration)
+ if (playerMF.Time > player.Duration && player.Duration.Ticks != 0)
{
if (playerMF.Loop)
{
@@ -452,7 +447,7 @@ namespace MF
}
// Update streams
- if (ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_VIDEO_STREAM, dt))
+ if (ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_VIDEO_STREAM, UpdateDeltaTime))
{
// Failed to pick a valid sample so try again with seeking
playerMF.Seek = 1;
@@ -464,7 +459,7 @@ namespace MF
}
}
if (player.AudioInfo.BitDepth != 0)
- ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_AUDIO_STREAM, dt);
+ ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_AUDIO_STREAM, UpdateDeltaTime);
player.Tick();
}
@@ -610,12 +605,17 @@ bool VideoBackendMF::Base_Init()
VIDEO_API_MF_ERROR(MFStartup, hr);
return true;
}
+ MF::UpdateTime = Platform::GetTimeSeconds();
return false;
}
void VideoBackendMF::Base_Update(TaskGraph* graph)
{
+ double time = Platform::GetTimeSeconds();
+ MF::UpdateDeltaTime = TimeSpan::FromSeconds(time - MF::UpdateTime);
+ MF::UpdateTime = time;
+
// Schedule work to update all videos in async
Function job;
job.Bind(MF::UpdatePlayer);
diff --git a/Source/Engine/Video/VideoPlayer.cpp b/Source/Engine/Video/VideoPlayer.cpp
index 59f8d2438..363779038 100644
--- a/Source/Engine/Video/VideoPlayer.cpp
+++ b/Source/Engine/Video/VideoPlayer.cpp
@@ -133,7 +133,9 @@ void VideoPlayer::SetTime(float time)
if (_state == States::Stopped || _player.Backend == nullptr)
return;
TimeSpan timeSpan = TimeSpan::FromSeconds(time);
- timeSpan.Ticks = Math::Clamp(timeSpan.Ticks, 0, _player.Duration.Ticks);
+ timeSpan.Ticks = Math::Max(timeSpan.Ticks, 0);
+ if (_player.Duration.Ticks > 0)
+ timeSpan.Ticks = Math::Min(timeSpan.Ticks, _player.Duration.Ticks);
_player.Backend->Player_Seek(_player, timeSpan);
}
diff --git a/Source/Engine/Video/VideoPlayer.h b/Source/Engine/Video/VideoPlayer.h
index e1b7e877d..4447337ea 100644
--- a/Source/Engine/Video/VideoPlayer.h
+++ b/Source/Engine/Video/VideoPlayer.h
@@ -15,6 +15,7 @@ class FLAXENGINE_API VideoPlayer : public Actor
{
DECLARE_SCENE_OBJECT(VideoPlayer);
API_AUTO_SERIALIZATION();
+ friend class AudioBackendOAL;
public:
///
diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
index 46f0ec245..3bdb16815 100644
--- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
+++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs
@@ -1892,13 +1892,13 @@ namespace Flax.Build.Bindings
CppAutoSerializeProperties.Clear();
CppIncludeFiles.Add("Engine/Serialization/Serialization.h");
+ // Serialize
contents.AppendLine();
contents.Append($"void {typeNameNative}::Serialize(SerializeStream& stream, const void* otherObj)").AppendLine();
contents.Append('{').AppendLine();
if (baseType != null)
contents.Append($" {baseType.FullNameNative}::Serialize(stream, otherObj);").AppendLine();
contents.Append($" SERIALIZE_GET_OTHER_OBJ({typeNameNative});").AppendLine();
-
if (classInfo != null)
{
foreach (var fieldInfo in classInfo.Fields)
@@ -1910,7 +1910,6 @@ namespace Flax.Build.Bindings
contents.Append($" SERIALIZE{typeHint}({fieldInfo.Name});").AppendLine();
CppAutoSerializeFields.Add(fieldInfo);
}
-
foreach (var propertyInfo in classInfo.Properties)
{
if (propertyInfo.Getter == null || propertyInfo.Setter == null)
@@ -1952,21 +1951,19 @@ namespace Flax.Build.Bindings
CppAutoSerializeFields.Add(fieldInfo);
}
}
-
contents.Append('}').AppendLine();
+ // Deserialize
contents.AppendLine();
contents.Append($"void {typeNameNative}::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)").AppendLine();
contents.Append('{').AppendLine();
if (baseType != null)
contents.Append($" {baseType.FullNameNative}::Deserialize(stream, modifier);").AppendLine();
-
foreach (var fieldInfo in CppAutoSerializeFields)
{
var typeHint = GenerateCppAutoSerializationDefineType(buildData, contents, moduleInfo, typeInfo, fieldInfo.Type, fieldInfo);
contents.Append($" DESERIALIZE{typeHint}({fieldInfo.Name});").AppendLine();
}
-
foreach (var propertyInfo in CppAutoSerializeProperties)
{
contents.AppendLine(" {");
@@ -1978,7 +1975,43 @@ namespace Flax.Build.Bindings
contents.AppendLine(" }");
contents.AppendLine(" }");
}
+ contents.Append('}').AppendLine();
+ // ShouldSerialize
+ contents.AppendLine();
+ contents.Append($"bool {typeNameNative}::ShouldSerialize(const void* otherObj) const").AppendLine();
+ contents.Append('{').AppendLine();
+ if (!typeInfo.IsScriptingObject)
+ {
+ contents.Append($" SERIALIZE_GET_OTHER_OBJ({typeNameNative});").AppendLine();
+ contents.AppendLine(" bool result = false;");
+ if (baseType != null)
+ contents.Append($" result |= {baseType.FullNameNative}::ShouldSerialize(otherObj);").AppendLine();
+ foreach (var fieldInfo in CppAutoSerializeFields)
+ {
+ contents.Append($" result |= Serialization::ShouldSerialize({fieldInfo.Name}, &other->{fieldInfo.Name});").AppendLine();
+ }
+ foreach (var propertyInfo in CppAutoSerializeProperties)
+ {
+ contents.Append(" {");
+ contents.Append(" const auto");
+ if (propertyInfo.Getter.ReturnType.IsConstRef)
+ contents.Append('&');
+ contents.Append($" value = {propertyInfo.Getter.Name}();");
+ contents.Append(" const auto");
+ if (propertyInfo.Getter.ReturnType.IsConstRef)
+ contents.Append('&');
+ contents.Append($" otherValue = other->{propertyInfo.Getter.Name}();");
+ contents.Append(" result |= Serialization::ShouldSerialize(value, &otherValue);").AppendLine();
+ contents.Append('}').AppendLine();
+ }
+ contents.AppendLine(" return result;");
+ }
+ else
+ {
+ // Not needed to generate
+ contents.AppendLine(" return true;");
+ }
contents.Append('}').AppendLine();
}