From 65b35a4b8adebe264618976d7ec5dc9df99b12d5 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 18 Mar 2026 21:40:22 +0100 Subject: [PATCH 01/51] auto scroll to new track when adding track to timeline --- Source/Editor/GUI/Timeline/Timeline.cs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 15e7eb953..0cd0cdc04 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -229,6 +229,7 @@ namespace FlaxEditor.GUI.Timeline private List _mediaMoveStartTracks; private byte[][] _mediaMoveStartData; private float _zoom = 1.0f; + private float _tracksVScrollTarget = 0f; private bool _isMovingPositionHandle; private bool _canPlayPause = true, _canStop = true; private List _batchedUndoActions; @@ -898,6 +899,8 @@ namespace FlaxEditor.GUI.Timeline }; UpdatePositionHandle(); PlaybackState = PlaybackStates.Disabled; + + _tracksVScrollTarget = _tracksPanelArea.VScrollBar.TargetValue; } private void UpdatePositionHandle() @@ -1305,6 +1308,10 @@ namespace FlaxEditor.GUI.Timeline MarkAsEdited(); if (withUndo) Undo?.AddAction(new AddRemoveTrackAction(this, track, true)); + + // Scroll to track + _tracksPanelArea.ScrollViewTo(track); + _tracksVScrollTarget = _tracksPanelArea.VScrollBar.TargetValue; } /// @@ -2033,12 +2040,18 @@ namespace FlaxEditor.GUI.Timeline base.Update(deltaTime); // Synchronize scroll vertical bars for tracks and media panels to keep the view in sync - var scroll1 = _tracksPanelArea.VScrollBar; - var scroll2 = _backgroundArea.VScrollBar; - if (scroll1.IsThumbClicked || _tracksPanelArea.IsMouseOver) - scroll2.TargetValue = scroll1.Value; + var tracksVScroll = _tracksPanelArea.VScrollBar; + var backgroundVScroll = _backgroundArea.VScrollBar; + bool forceBackgroundToTracksScroll = !Mathf.WithinEpsilon(_tracksVScrollTarget - tracksVScroll.Value, 0f, 5f); + if (tracksVScroll.IsThumbClicked || _tracksPanelArea.IsMouseOver || forceBackgroundToTracksScroll) + { + backgroundVScroll.TargetValue = tracksVScroll.Value; + } else - scroll1.TargetValue = scroll2.Value; + { + _tracksVScrollTarget = 0f; + tracksVScroll.TargetValue = backgroundVScroll.Value; + } // Batch undo actions if (_batchedUndoActions != null && _batchedUndoActions.Count != 0) From 503a0e6763a735ccc00cdafdbeced545c0d5ece7 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 17:33:45 +0100 Subject: [PATCH 02/51] add slider to adjust skeleton name font size --- .../Viewport/Previews/AnimatedModelPreview.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs index a678e868a..5bb33ef63 100644 --- a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs +++ b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs @@ -2,6 +2,7 @@ using System; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Input; using FlaxEngine; using Object = FlaxEngine.Object; @@ -14,7 +15,7 @@ namespace FlaxEditor.Viewport.Previews public class AnimatedModelPreview : AssetPreview { private AnimatedModel _previewModel; - private ContextMenuButton _showNodesButton, _showBoundsButton, _showFloorButton, _showNodesNamesButton; + private ContextMenuButton _showNodesButton, _showBoundsButton, _showFloorButton, _showNodesNamesButton, _nodeNameSizeButton; private bool _showNodes, _showBounds, _showFloor, _showNodesNames; private StaticModel _floorModel; private bool _playAnimation, _playAnimationOnce; @@ -110,9 +111,16 @@ namespace FlaxEditor.Viewport.Previews ShowDebugDraw = true; if (_showNodesNamesButton != null) _showNodesNamesButton.Checked = value; + if (_nodeNameSizeButton != null) + _nodeNameSizeButton.Enabled = value; } } + /// + /// The font size used in the node name debug draw. + /// + public int NodeNamesSize = 10; + /// /// Gets or sets a value indicating whether show animated model bounding box debug view. /// @@ -210,6 +218,15 @@ namespace FlaxEditor.Viewport.Previews _showFloorButton.CloseMenuOnClick = false; } + _nodeNameSizeButton = ViewWidgetButtonMenu.AddButton("Skeleton Names Size"); + _nodeNameSizeButton.CloseMenuOnClick = false; + var nodeNameSizeValue = new IntValueBox(NodeNamesSize, 118, 2, 70.0f, 1, 32) + { + Parent = _nodeNameSizeButton + }; + _nodeNameSizeButton.Enabled = ShowNodesNames; + nodeNameSizeValue.ValueChanged += () => NodeNamesSize = nodeNameSizeValue.Value; + // Enable shadows PreviewLight.ShadowsMode = ShadowsCastingMode.All; PreviewLight.CascadeCount = 3; @@ -371,7 +388,7 @@ namespace FlaxEditor.Viewport.Previews if (nodesMask != null && !nodesMask[nodeIndex]) continue; //var t = new Transform(pose[nodeIndex].TranslationVector, Quaternion.Identity, new Float3(0.1f)); - DebugDraw.DrawText(nodes[nodeIndex].Name, pose[nodeIndex].TranslationVector, Color.White, 20, 0.0f, 0.1f); + DebugDraw.DrawText(nodes[nodeIndex].Name, pose[nodeIndex].TranslationVector, Color.White, NodeNamesSize, 0.0f, 0.25f); } } } From 9505be310fbb01193ee2e9b077f910b5d304545c Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 17:34:01 +0100 Subject: [PATCH 03/51] fix spelling in ShowBitDepth --- Source/Editor/Content/Import/AudioImportSettings.cs | 2 +- Source/Engine/Tools/AudioTool/AudioTool.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Content/Import/AudioImportSettings.cs b/Source/Editor/Content/Import/AudioImportSettings.cs index b645c1509..a0af8c154 100644 --- a/Source/Editor/Content/Import/AudioImportSettings.cs +++ b/Source/Editor/Content/Import/AudioImportSettings.cs @@ -12,7 +12,7 @@ namespace FlaxEngine.Tools { partial struct Options { - private bool ShowBtiDepth => Format != AudioFormat.Vorbis; + private bool ShowBitDepth => Format != AudioFormat.Vorbis; } } } diff --git a/Source/Engine/Tools/AudioTool/AudioTool.h b/Source/Engine/Tools/AudioTool/AudioTool.h index b0c9c4c76..289b61c72 100644 --- a/Source/Engine/Tools/AudioTool/AudioTool.h +++ b/Source/Engine/Tools/AudioTool/AudioTool.h @@ -70,7 +70,7 @@ public: /// /// The size of a single sample in bits. The clip will be converted to this bit depth on import. /// - API_FIELD(Attributes="EditorOrder(50), VisibleIf(nameof(ShowBtiDepth))") + API_FIELD(Attributes="EditorOrder(50), VisibleIf(nameof(ShowBitDepth))") BitDepth BitDepth = BitDepth::_16; String ToString() const; From 699fb1260426ee1068b0ad7dae2d4b4d176e8d3f Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 17:35:57 +0100 Subject: [PATCH 04/51] improve Properties Panel layout for SkinnedModel --- Source/Engine/Level/Actors/AnimatedModel.h | 42 +++++++++++++--------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index a520d6723..b62b165c6 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -98,99 +98,99 @@ public: /// /// The skinned model asset used for rendering. /// - API_FIELD(Attributes="EditorOrder(10), DefaultValue(null), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(10), DefaultValue(null), EditorDisplay(\"Skeleton\")") AssetReference SkinnedModel; /// /// The animation graph asset used for the skinned mesh skeleton bones evaluation (controls the animation). /// - API_FIELD(Attributes="EditorOrder(15), DefaultValue(null), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(15), DefaultValue(null), EditorDisplay(\"Skeleton\")") AssetReference AnimationGraph; /// /// If true, use per-bone motion blur on this skeletal model. It requires additional rendering, can be disabled to save performance. /// - API_FIELD(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Rendering\")") bool PerBoneMotionBlur = true; /// /// If true, animation speed will be affected by the global timescale parameter. /// - API_FIELD(Attributes="EditorOrder(30), DefaultValue(true), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(30), DefaultValue(true), EditorDisplay(\"Update\")") bool UseTimeScale = true; /// /// If true, the animation will be updated even when an actor cannot be seen by any camera. Otherwise, the animations themselves will also stop running when the actor is off-screen. /// - API_FIELD(Attributes="EditorOrder(40), DefaultValue(false), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(40), DefaultValue(false), EditorDisplay(\"Update\")") bool UpdateWhenOffscreen = false; /// /// The animation update delta timescale. Can be used to speed up animation playback or create slow motion effect. /// - API_FIELD(Attributes="EditorOrder(45), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(45), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Update\")") float UpdateSpeed = 1.0f; /// /// The animation update mode. Can be used to optimize the performance. /// - API_FIELD(Attributes="EditorOrder(50), DefaultValue(AnimationUpdateMode.Auto), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(50), DefaultValue(AnimationUpdateMode.Auto), EditorDisplay(\"Update\")") AnimationUpdateMode UpdateMode = AnimationUpdateMode::Auto; /// /// The master scale parameter for the actor bounding box. Helps to reduce mesh flickering effect on screen edges. /// - API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Rendering\")") float BoundsScale = 1.5f; /// /// The custom bounds(in actor local space). If set to empty bounds then source skinned model bind pose bounds will be used. /// - API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Rendering\")") BoundingBox CustomBounds = BoundingBox::Zero; /// /// The model Level Of Detail bias value. Allows to increase or decrease rendered model quality. /// - API_FIELD(Attributes="EditorOrder(80), DefaultValue(0), Limit(-100, 100, 0.1f), EditorDisplay(\"Skinned Model\", \"LOD Bias\")") + API_FIELD(Attributes="EditorOrder(80), DefaultValue(0), Limit(-100, 100, 0.1f), EditorDisplay(\"Rendering\", \"LOD Bias\")") int32 LODBias = 0; /// /// Gets the model forced Level Of Detail index. Allows to bind the given model LOD to show. Value -1 disables this feature. /// - API_FIELD(Attributes="EditorOrder(90), DefaultValue(-1), Limit(-1, 100, 0.1f), EditorDisplay(\"Skinned Model\", \"Forced LOD\")") + API_FIELD(Attributes="EditorOrder(90), DefaultValue(-1), Limit(-1, 100, 0.1f), EditorDisplay(\"Rendering\", \"Forced LOD\")") int32 ForcedLOD = -1; /// /// The draw passes to use for rendering this object. /// - API_FIELD(Attributes="EditorOrder(100), DefaultValue(DrawPass.Default), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(100), DefaultValue(DrawPass.Default), EditorDisplay(\"Rendering\")") DrawPass DrawModes = DrawPass::Default; /// /// The object sort order key used when sorting drawable objects during rendering. Use lower values to draw object before others, higher values are rendered later (on top). Can be used to control transparency drawing. /// - API_FIELD(Attributes="EditorDisplay(\"Skinned Model\"), EditorOrder(110), DefaultValue(0)") + API_FIELD(Attributes="EditorOrder(110), DefaultValue(0), EditorDisplay(\"Rendering\")") int8 SortOrder = 0; /// /// The shadows casting mode. /// [Deprecated on 26.10.2022, expires on 26.10.2024] /// - API_FIELD(Attributes="EditorOrder(110), DefaultValue(ShadowsCastingMode.All), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(110), DefaultValue(ShadowsCastingMode.All), EditorDisplay(\"Rendering\")") DEPRECATED() ShadowsCastingMode ShadowsMode = ShadowsCastingMode::All; /// /// The animation root motion apply target. If not specified the animated model will apply it itself. /// - API_FIELD(Attributes="EditorOrder(120), DefaultValue(null), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(120), DefaultValue(null), EditorDisplay(\"Skeleton\")") ScriptingObjectReference RootMotionTarget; #if USE_EDITOR /// - /// If checked, the skeleton pose will be shawn during debug shapes drawing. + /// If checked, the skeleton pose will be shown during debug shapes drawing. /// - API_FIELD(Attributes="EditorOrder(200), EditorDisplay(\"Skinned Model\")") bool ShowDebugDrawSkeleton = false; + API_FIELD(Attributes="EditorOrder(200), EditorDisplay(\"Debug\"), VisibleIf(nameof(ShowDebugDrawOptions))") bool ShowDebugDrawSkeleton = false; #endif public: @@ -440,6 +440,14 @@ public: API_FUNCTION() bool IsPlayingSlotAnimation(const StringView& slotName, Animation* anim = nullptr); private: + /// + /// Used to hide options if when the skinned model or animation graph is null. + /// + API_PROPERTY(Attributes = "HideInEditor, NoSerialize") bool GetShowDebugDrawOptions() const + { + return SkinnedModel != nullptr && AnimationGraph != nullptr; + } + void ApplyRootMotion(const Transform& rootMotionDelta); void SyncParameters(); void RunBlendShapeDeformer(const MeshBase* mesh, struct MeshDeformationData& deformation); From d77e1e9a532575b5a1a1e4ab84adfc334d8764c5 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 18:25:00 +0100 Subject: [PATCH 05/51] wrap GetShowDebugDrawOptions() with USE_EDITOR preprocessor --- Source/Engine/Level/Actors/AnimatedModel.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index b62b165c6..46ed275c0 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -440,6 +440,7 @@ public: API_FUNCTION() bool IsPlayingSlotAnimation(const StringView& slotName, Animation* anim = nullptr); private: +#if USE_EDITOR /// /// Used to hide options if when the skinned model or animation graph is null. /// @@ -447,6 +448,7 @@ private: { return SkinnedModel != nullptr && AnimationGraph != nullptr; } +#endif void ApplyRootMotion(const Transform& rootMotionDelta); void SyncParameters(); From a831e15bf7ba6310e124a00592bd7192013631a8 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 20:47:34 +0100 Subject: [PATCH 06/51] fix rare bug where tracks pannel wouldn't scroll all the way through --- Source/Editor/GUI/Timeline/Timeline.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 0cd0cdc04..516c30544 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -229,7 +229,7 @@ namespace FlaxEditor.GUI.Timeline private List _mediaMoveStartTracks; private byte[][] _mediaMoveStartData; private float _zoom = 1.0f; - private float _tracksVScrollTarget = 0f; + private float _tracksVScrollTarget; private bool _isMovingPositionHandle; private bool _canPlayPause = true, _canStop = true; private List _batchedUndoActions; @@ -899,8 +899,6 @@ namespace FlaxEditor.GUI.Timeline }; UpdatePositionHandle(); PlaybackState = PlaybackStates.Disabled; - - _tracksVScrollTarget = _tracksPanelArea.VScrollBar.TargetValue; } private void UpdatePositionHandle() @@ -2042,14 +2040,20 @@ namespace FlaxEditor.GUI.Timeline // Synchronize scroll vertical bars for tracks and media panels to keep the view in sync var tracksVScroll = _tracksPanelArea.VScrollBar; var backgroundVScroll = _backgroundArea.VScrollBar; - bool forceBackgroundToTracksScroll = !Mathf.WithinEpsilon(_tracksVScrollTarget - tracksVScroll.Value, 0f, 5f); - if (tracksVScroll.IsThumbClicked || _tracksPanelArea.IsMouseOver || forceBackgroundToTracksScroll) + bool forceBackgroundToTracksScroll = _tracksVScrollTarget > 0; + if (forceBackgroundToTracksScroll) + { + backgroundVScroll.TargetValue = tracksVScroll.Value; + + if (Mathf.Abs(tracksVScroll.Value - _tracksVScrollTarget) < 0.5f) + _tracksVScrollTarget = 0f; + } + else if (tracksVScroll.IsThumbClicked || _tracksPanelArea.IsMouseOver) { backgroundVScroll.TargetValue = tracksVScroll.Value; } else { - _tracksVScrollTarget = 0f; tracksVScroll.TargetValue = backgroundVScroll.Value; } From 811bf0d630058302dedf3c9fe4981ce1611f5956 Mon Sep 17 00:00:00 2001 From: Jake Young Date: Thu, 7 May 2026 01:56:09 -0400 Subject: [PATCH 07/51] Change the default for showing debug draw to be on by default. --- Source/Editor/Windows/GameWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index e95fb61b2..7b7370c26 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -123,7 +123,7 @@ namespace FlaxEditor.Windows private readonly ScaledRenderOutputControl _viewport; private readonly GameRoot _guiRoot; private bool _showGUI = true, _editGUI = true; - private bool _showDebugDraw = false; + private bool _showDebugDraw = true; private bool _audioMuted = false; private float _audioVolume = 1; private bool _isMaximized = false, _isUnlockingMouse = false; From 96d670efb7702dd1f5cf25bc718914005626966b Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 19 May 2026 22:49:58 +0300 Subject: [PATCH 08/51] Add support for Windows 11 SDK version 28000 --- .../Flax.Build/Platforms/Windows/WindowsPlatformBase.cs | 7 +++++++ .../Flax.Build/Platforms/Windows/WindowsToolchainBase.cs | 1 + 2 files changed, 8 insertions(+) diff --git a/Source/Tools/Flax.Build/Platforms/Windows/WindowsPlatformBase.cs b/Source/Tools/Flax.Build/Platforms/Windows/WindowsPlatformBase.cs index 26afcd894..f91be996f 100644 --- a/Source/Tools/Flax.Build/Platforms/Windows/WindowsPlatformBase.cs +++ b/Source/Tools/Flax.Build/Platforms/Windows/WindowsPlatformBase.cs @@ -140,6 +140,11 @@ namespace Flax.Build.Platforms /// Windows 11 SDK (10.0.26100.0) 24H2 /// v10_0_26100_0, + + /// + /// Windows 11 SDK (10.0.28000.0) + /// + v10_0_28000_0, } /// @@ -333,6 +338,7 @@ namespace Flax.Build.Platforms case WindowsPlatformSDK.v10_0_22000_0: return new Version(10, 0, 22000, 0); case WindowsPlatformSDK.v10_0_22621_0: return new Version(10, 0, 22621, 0); case WindowsPlatformSDK.v10_0_26100_0: return new Version(10, 0, 26100, 0); + case WindowsPlatformSDK.v10_0_28000_0: return new Version(10, 0, 28000, 0); default: throw new ArgumentOutOfRangeException(nameof(sdk), sdk, null); } } @@ -388,6 +394,7 @@ namespace Flax.Build.Platforms WindowsPlatformSDK.v10_0_22000_0, WindowsPlatformSDK.v10_0_22621_0, WindowsPlatformSDK.v10_0_26100_0, + WindowsPlatformSDK.v10_0_28000_0, }; foreach (var sdk10 in sdk10Roots) { diff --git a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs index 750a16aab..5ce13dd18 100644 --- a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs +++ b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs @@ -314,6 +314,7 @@ namespace Flax.Build.Platforms case WindowsPlatformSDK.v10_0_22000_0: case WindowsPlatformSDK.v10_0_22621_0: case WindowsPlatformSDK.v10_0_26100_0: + case WindowsPlatformSDK.v10_0_28000_0: { var sdkVersionName = WindowsPlatformBase.GetSDKVersion(SDK).ToString(); string includeRootDir = Path.Combine(windowsSdkDir, "include", sdkVersionName); From 1176dc515d8b8755e70d2b6b5533924cb0a0c706 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 19 May 2026 23:44:46 +0300 Subject: [PATCH 09/51] Fix window decoration buttons not clickable with DPI scale --- Source/Editor/GUI/WindowDecorations.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/GUI/WindowDecorations.cs b/Source/Editor/GUI/WindowDecorations.cs index 74dc38273..7d7bc5dd5 100644 --- a/Source/Editor/GUI/WindowDecorations.cs +++ b/Source/Editor/GUI/WindowDecorations.cs @@ -181,7 +181,7 @@ public class WindowDecorations : ContainerControl return WindowHitCodes.NoWhere; var dpiScale = _window.DpiScale; - var pos = _window.ScreenToClient(mouse * dpiScale); // pos is not DPI adjusted + var pos = _window.ScreenToClient(mouse * dpiScale); // pos is DPI adjusted in window space if (!_window.IsMaximized) { var winSize = _window.Size; @@ -214,6 +214,7 @@ public class WindowDecorations : ContainerControl return WindowHitCodes.Bottom; } + pos /= dpiScale; // The position should not be DPI adjusted in control space var controlUnderMouse = GetChildAt(pos, control => control != _title); if (_title.Bounds.Contains(pos) && controlUnderMouse == null) return WindowHitCodes.Caption; From 4d4d1a589f0315684ff74fc8ac00fc7ec1a00838 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 19 May 2026 23:46:32 +0300 Subject: [PATCH 10/51] Fix mouse position in events reported from caption area on Windows --- Source/Engine/Platform/Windows/WindowsInput.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Platform/Windows/WindowsInput.cpp b/Source/Engine/Platform/Windows/WindowsInput.cpp index 64299b6a3..96151e894 100644 --- a/Source/Engine/Platform/Windows/WindowsInput.cpp +++ b/Source/Engine/Platform/Windows/WindowsInput.cpp @@ -189,6 +189,7 @@ bool WindowsMouse::WndProc(Window* window, const UINT msg, WPARAM wParam, LPARAM POINT p; p.x = static_cast(WINDOWS_GET_X_LPARAM(lParam)); p.y = static_cast(WINDOWS_GET_Y_LPARAM(lParam)); + const Float2 mousePosScreen(static_cast(p.x), static_cast(p.y)); ::ClientToScreen(window->GetHWND(), &p); const Float2 mousePos(static_cast(p.x), static_cast(p.y)); @@ -203,7 +204,8 @@ bool WindowsMouse::WndProc(Window* window, const UINT msg, WPARAM wParam, LPARAM } case WM_NCMOUSEMOVE: { - OnMouseMove(mousePos, window); + // The position in the message is already reported in screen-space + OnMouseMove(mousePosScreen, window); result = true; break; } From 5010597bd2f8505e0544b83f8cf72157067313c5 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 21 May 2026 00:20:52 +0200 Subject: [PATCH 11/51] add being able to rotate view with direction gizmo --- Source/Editor/Gizmo/DirectionGizmo.cs | 48 +++++++++++++++++-- Source/Editor/Options/ViewportOptions.cs | 2 +- .../Viewport/MainEditorGizmoViewport.cs | 4 +- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs index aedfe73d0..ecdd19e66 100644 --- a/Source/Editor/Gizmo/DirectionGizmo.cs +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -36,8 +36,10 @@ internal class DirectionGizmo : ContainerControl private List _axisData = new List(); private int _hoveredAxisIndex = -1; + private bool _mouseDown; + private Float2 _mouseDownLocation; + private SpriteHandle _posHandle; - private SpriteHandle _negHandle; private FontReference _fontReference; @@ -110,7 +112,6 @@ internal class DirectionGizmo : ContainerControl var editor = Editor.Instance; _posHandle = editor.Icons.VisjectBoxClosed32; - _negHandle = editor.Icons.VisjectBoxOpen32; _fontReference = new FontReference(Style.Current.FontSmall); @@ -142,7 +143,25 @@ internal class DirectionGizmo : ContainerControl public override void OnMouseMove(Float2 location) { _hoveredAxisIndex = -1; + + if (_mouseDown) + { + StartMouseCapture(); + Cursor = CursorType.Hidden; + const float sensitivity = 0.125f; + Float2 delta = Input.MousePositionDelta; + delta *= Mathf.DegreesToRadians; + delta *= sensitivity; + + const float orbitRadius = 500f; + Quaternion newOrientation = _viewport.ViewOrientation * Quaternion.RotationYawPitchRoll(delta.X , delta.Y, 0f); + Vector3 orbitCenter = _viewport.ViewPosition + _viewport.ViewDirection * orbitRadius; + _viewport.ViewportCamera.SetArcBallView(newOrientation, orbitCenter, orbitRadius); + + return; + } + // Check which axis is being hovered - check from closest to farthest for proper layering for (int i = _spritePositions.Count - 1; i >= 0; i--) { @@ -156,9 +175,27 @@ internal class DirectionGizmo : ContainerControl base.OnMouseMove(location); } + public override bool OnMouseDown(Float2 location, MouseButton button) + { + _mouseDown = true; + _mouseDownLocation = location; + return true; + } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { + if (_mouseDown && _mouseDownLocation != location) + { + _mouseDown = false; + EndMouseCapture(); + Root.MousePosition = PointToParent(Root, _mouseDownLocation); + Cursor = CursorType.Default; + return true; + } + + _mouseDown = false; + if (base.OnMouseUp(location, button)) return true; @@ -269,7 +306,12 @@ internal class DirectionGizmo : ContainerControl // Rebuild sprite positions list for hover detection _spritePositions.Clear(); - Render2D.DrawSprite(_posHandle, new Rectangle(0, 0, Size), Color.Black.AlphaMultiplied(_backgroundOpacity)); + if (IsMouseOver) + { + Rectangle backgroundRect = new Rectangle(0, 0, Size); + Color backgroundColor = Color.DarkGray.AlphaMultiplied(_backgroundOpacity); + Render2D.DrawSprite(_posHandle, backgroundRect, backgroundColor); + } // Draw in order from farthest to closest for (int i = 0; i < _axisData.Count; i++) diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index c7f6ba544..2dd08b6d1 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -187,7 +187,7 @@ namespace FlaxEditor.Options public float DirectionGizmoScale { get; set; } = 1f; /// - /// Gets or sets a value for the opacity of the main viewports background. + /// Gets or sets a value for the opacity of the main viewports background. Background will only show when the gizmo is hovered. /// [DefaultValue(0.1f), Limit(0.0f, 1.0f)] [EditorDisplay("Direction Gizmo"), EditorOrder(502), Tooltip("The background opacity of the of the direction gizmo in the main viewport.")] diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index f171840b5..51b240202 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -706,7 +706,7 @@ namespace FlaxEditor.Viewport { base.OnLeftMouseButtonDown(); - if (!IsAltKeyDown) + if (!IsAltKeyDown && !_directionGizmo.IsMouseOver) _rubberBandSelector.TryStartingRubberBandSelection(_viewMousePos); } @@ -714,7 +714,7 @@ namespace FlaxEditor.Viewport protected override void OnLeftMouseButtonUp() { // Skip if was controlling mouse or mouse is not over the area - if (_prevInput.IsControllingMouse || !Bounds.Contains(ref _viewMousePos)) + if (_prevInput.IsControllingMouse || !Bounds.Contains(ref _viewMousePos) || _directionGizmo.IsMouseOver) return; // Select rubberbanded rect actor nodes or pick with gizmo From 637f3dc1761b66404dd34b4f7b5bb9bca0186f1c Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 21 May 2026 00:26:45 +0200 Subject: [PATCH 12/51] make code a bit nicer --- Source/Editor/Gizmo/DirectionGizmo.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs index ecdd19e66..b8e90f36e 100644 --- a/Source/Editor/Gizmo/DirectionGizmo.cs +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -185,17 +185,20 @@ internal class DirectionGizmo : ContainerControl /// public override bool OnMouseUp(Float2 location, MouseButton button) { - if (_mouseDown && _mouseDownLocation != location) + if (_mouseDown) { _mouseDown = false; - EndMouseCapture(); - Root.MousePosition = PointToParent(Root, _mouseDownLocation); - Cursor = CursorType.Default; - return true; + + if (_mouseDownLocation != location) + { + _mouseDown = false; + EndMouseCapture(); + Root.MousePosition = PointToParent(Root, _mouseDownLocation); + Cursor = CursorType.Default; + return true; + } } - _mouseDown = false; - if (base.OnMouseUp(location, button)) return true; From 96a081bf93d54c4260607436eae42bd88a57d483 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 21 May 2026 21:43:26 +0200 Subject: [PATCH 13/51] use mouse wrapping and rename mouse wrapping function parameter --- Source/Editor/Gizmo/DirectionGizmo.cs | 2 +- Source/Engine/UI/GUI/Control.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs index b8e90f36e..0eee9f916 100644 --- a/Source/Editor/Gizmo/DirectionGizmo.cs +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -146,7 +146,7 @@ internal class DirectionGizmo : ContainerControl if (_mouseDown) { - StartMouseCapture(); + StartMouseCapture(true); Cursor = CursorType.Hidden; const float sensitivity = 0.125f; diff --git a/Source/Engine/UI/GUI/Control.cs b/Source/Engine/UI/GUI/Control.cs index f3eae8a24..8ae3b3616 100644 --- a/Source/Engine/UI/GUI/Control.cs +++ b/Source/Engine/UI/GUI/Control.cs @@ -572,12 +572,12 @@ namespace FlaxEngine.GUI /// /// Starts the mouse tracking. Used by the scrollbars, splitters, etc. /// - /// If set to true will use mouse screen offset. + /// If set to true will wrap when it hits a screen border. [NoAnimate] - public void StartMouseCapture(bool useMouseScreenOffset = false) + public void StartMouseCapture(bool screenSpaceMouseWrap = false) { var parent = Root; - parent?.StartTrackingMouse(this, useMouseScreenOffset); + parent?.StartTrackingMouse(this, screenSpaceMouseWrap); } /// From 571821bf3d1b1637e8e7165cd27a7c3bf5e8951f Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Sat, 23 May 2026 21:09:37 +0300 Subject: [PATCH 14/51] Fix Rider detection on macOS when installed in the user Applications folder --- .../Scripting/CodeEditors/RiderCodeEditor.cpp | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp index b63815dce..de6001efd 100644 --- a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp @@ -14,6 +14,9 @@ #if PLATFORM_WINDOWS #include "Engine/Platform/Win32/IncludeWindowsHeaders.h" +#elif PLATFORM_MAC +#include "Engine/Platform/Apple/AppleUtils.h" +#include #endif namespace @@ -68,10 +71,14 @@ namespace if (!launcherPath.HasChars() || !FileSystem::FileExists(exePath)) return; - if (launchOverridePath != String::Empty) - installations->Add(New(launchOverridePath, versionMember->value.GetText())); - else - installations->Add(New(exePath, versionMember->value.GetText())); + String installPath = launchOverridePath != String::Empty ? launchOverridePath : exePath; + StringUtils::PathRemoveRelativeParts(installPath); + for (RiderInstallation* installation : *installations) + { + if (installation->path == installPath) + return; + } + installations->Add(New(installPath, versionMember->value.GetText())); } #if PLATFORM_WINDOWS @@ -221,17 +228,29 @@ void RiderCodeEditor::FindEditors(Array* output) String applicationSupportFolder; FileSystem::GetSpecialFolderPath(SpecialFolder::ProgramData, applicationSupportFolder); + NSURL* appURL = [[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:@"com.jetbrains.rider"]; + if (appURL != nullptr) + { + const String appPath = AppleUtils::ToString((CFStringRef)[appURL path]); + SearchDirectory(&installations, appPath / TEXT("Contents/Resources"), appPath); + } + Array subMacDirectories; FileSystem::GetChildDirectories(subMacDirectories, applicationSupportFolder / TEXT("JetBrains/Toolbox/apps/Rider/ch-0/")); FileSystem::GetChildDirectories(subMacDirectories, applicationSupportFolder / TEXT("JetBrains/Toolbox/apps/Rider/ch-1/")); for (const String& directory : subMacDirectories) { - String riderAppDirectory = directory / TEXT("Rider.app/Contents/Resources"); - SearchDirectory(&installations, riderAppDirectory); + String riderAppPath = directory / TEXT("Rider.app"); + SearchDirectory(&installations, riderAppPath / TEXT("Contents/Resources"), riderAppPath); } // Check the local installer version - SearchDirectory(&installations, TEXT("/Applications/Rider.app/Contents/Resources")); + SearchDirectory(&installations, TEXT("/Applications/Rider.app/Contents/Resources"), TEXT("/Applications/Rider.app")); + + String userFolder; + FileSystem::GetSpecialFolderPath(SpecialFolder::Documents, userFolder); + String riderAppPath = userFolder / TEXT("../Applications/Rider.app"); + SearchDirectory(&installations, riderAppPath / TEXT("Contents/Resources"), riderAppPath); #endif for (const String& directory : subDirectories) From 285762bfdbd075b57f30f3dbceba281ffc398992 Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Sun, 24 May 2026 11:13:41 +0300 Subject: [PATCH 15/51] Fix Vulkan Tracy timestamp queries on MoltenVK Disable Vulkan Tracy GPU profiling when Vulkan timer queries are disabled. Apple platforms already set VULKAN_USE_TIMER_QUERIES to 0 --- Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.cpp | 6 +++--- Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.h | 2 ++ Source/Engine/GraphicsDevice/Vulkan/Config.h | 4 ++++ Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp | 8 ++++---- .../GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp | 4 ++-- Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp | 2 +- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.cpp index 48daecc83..3721abbb2 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.cpp @@ -57,7 +57,7 @@ void CmdBufferVulkan::End() if (vkCmdEndDebugUtilsLabelEXT) vkCmdEndDebugUtilsLabelEXT(GetHandle()); #endif -#if GPU_ENABLE_TRACY +#if VULKAN_USE_TRACY_GPU tracy::EndVkZoneScope(_tracyZones.Last().Data); _tracyZones.RemoveLast(); #endif @@ -101,7 +101,7 @@ void CmdBufferVulkan::BeginEvent(const Char* name, void* tracyContext) char buffer[60]; int32 bufferSize = StringUtils::Copy(buffer, name, sizeof(buffer)); -#if GPU_ENABLE_TRACY +#if VULKAN_USE_TRACY_GPU auto& zone = _tracyZones.AddOne(); tracy::BeginVkZoneScope(zone.Data, tracyContext, GetHandle(), buffer, bufferSize); #endif @@ -128,7 +128,7 @@ void CmdBufferVulkan::EndEvent() vkCmdEndDebugUtilsLabelEXT(GetHandle()); #endif -#if GPU_ENABLE_TRACY +#if VULKAN_USE_TRACY_GPU tracy::EndVkZoneScope(_tracyZones.Last().Data); _tracyZones.RemoveLast(); #endif diff --git a/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.h b/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.h index 925f7a40f..a67964e08 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.h +++ b/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.h @@ -43,8 +43,10 @@ private: FenceVulkan* _fence; #if GPU_ALLOW_PROFILE_EVENTS int32 _eventsBegin = 0; +#if VULKAN_USE_TRACY_GPU struct TracyZone { byte Data[TracyVulkanZoneSize]; }; Array> _tracyZones; +#endif #endif // The latest value when command buffer was submitted. diff --git a/Source/Engine/GraphicsDevice/Vulkan/Config.h b/Source/Engine/GraphicsDevice/Vulkan/Config.h index fd5880400..e7db3fccd 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/Config.h +++ b/Source/Engine/GraphicsDevice/Vulkan/Config.h @@ -49,6 +49,10 @@ #define VULKAN_USE_TIMER_QUERIES 1 #endif +#ifndef VULKAN_USE_TRACY_GPU +#define VULKAN_USE_TRACY_GPU (GPU_ENABLE_TRACY && VULKAN_USE_TIMER_QUERIES) +#endif + // Fence wait operation timeout in seconds #ifndef VULKAN_WAIT_TIMEOUT #define VULKAN_WAIT_TIMEOUT 5.0f diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp index 3bdccd2e1..6a5d11e04 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUContextVulkan.cpp @@ -111,7 +111,7 @@ GPUContextVulkan::GPUContextVulkan(GPUDeviceVulkan* device, QueueVulkan* queue) _handlesSizes[(int32)SpirvShaderResourceBindingType::UAV] = GPU_MAX_UA_BINDED; #endif -#if GPU_ENABLE_TRACY +#if VULKAN_USE_TRACY_GPU #if VK_EXT_calibrated_timestamps && VK_EXT_host_query_reset && !PLATFORM_SWITCH // Use calibrated timestamps extension if (vkResetQueryPoolEXT && vkGetCalibratedTimestampsEXT && _device->PhysicalDeviceFeatures12.hostQueryReset) @@ -138,7 +138,7 @@ GPUContextVulkan::GPUContextVulkan(GPUDeviceVulkan* device, QueueVulkan* queue) GPUContextVulkan::~GPUContextVulkan() { -#if GPU_ENABLE_TRACY +#if VULKAN_USE_TRACY_GPU tracy::DestroyVkContext(_tracyContext); #endif for (int32 i = 0; i < _descriptorPools.Count(); i++) @@ -799,7 +799,7 @@ void GPUContextVulkan::FrameEnd() // Execute any queued layout transitions that weren't already handled by the render pass FlushBarriers(); -#if GPU_ENABLE_TRACY +#if VULKAN_USE_TRACY_GPU if (cmdBuffer) tracy::CollectVkContext(_tracyContext, cmdBuffer->GetHandle()); #endif @@ -813,7 +813,7 @@ void GPUContextVulkan::FrameEnd() void GPUContextVulkan::EventBegin(const Char* name) { const auto cmdBuffer = _cmdBufferManager->GetCmdBuffer(); -#if COMPILE_WITH_PROFILER +#if VULKAN_USE_TRACY_GPU void* tracyContext = _tracyContext; #else void* tracyContext = nullptr; diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp index 5ac20b613..b67ee2f24 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp @@ -45,7 +45,7 @@ static const char* GInstanceExtensions[] = #if defined(VK_KHR_display) && 0 VK_KHR_DISPLAY_EXTENSION_NAME, #endif -#if GPU_ENABLE_TRACY && VK_EXT_calibrated_timestamps && VK_EXT_host_query_reset +#if VULKAN_USE_TRACY_GPU && VK_EXT_calibrated_timestamps && VK_EXT_host_query_reset VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, // Required by VK_EXT_host_query_reset (unless using Vulkan 1.1 or newer) #endif nullptr @@ -66,7 +66,7 @@ static const char* GDeviceExtensions[] = #if VK_KHR_sampler_mirror_clamp_to_edge VK_KHR_SAMPLER_MIRROR_CLAMP_TO_EDGE_EXTENSION_NAME, #endif -#if GPU_ENABLE_TRACY && VK_EXT_calibrated_timestamps && VK_EXT_host_query_reset +#if VULKAN_USE_TRACY_GPU && VK_EXT_calibrated_timestamps && VK_EXT_host_query_reset VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME, VK_EXT_HOST_QUERY_RESET_EXTENSION_NAME, #endif diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp index f63d2182f..5917290c6 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp @@ -1677,7 +1677,7 @@ bool GPUDeviceVulkan::Init() VulkanPlatform::RestrictEnabledPhysicalDeviceFeatures(PhysicalDeviceFeatures, enabledFeatures); deviceInfo.pEnabledFeatures = &enabledFeatures; -#if GPU_ENABLE_TRACY && VK_EXT_calibrated_timestamps && VK_EXT_host_query_reset +#if VULKAN_USE_TRACY_GPU && VK_EXT_calibrated_timestamps && VK_EXT_host_query_reset VkPhysicalDeviceHostQueryResetFeatures resetFeatures; if (PhysicalDeviceFeatures12.hostQueryReset) { From 3140c711a4557bcba80448d143f7dc9d9c446a0f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 27 May 2026 14:39:45 +0200 Subject: [PATCH 16/51] Simplify apple platform include defines --- Source/Engine/Platform/Apple/AppleFileSystem.cpp | 2 +- Source/Engine/Platform/Apple/AppleFileSystem.h | 2 +- Source/Engine/Platform/Apple/ApplePlatform.cpp | 2 +- Source/Engine/Platform/Apple/ApplePlatform.h | 2 +- Source/Engine/Platform/Apple/AppleThread.h | 2 +- Source/Engine/Platform/Apple/AppleUtils.h | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.cpp b/Source/Engine/Platform/Apple/AppleFileSystem.cpp index a38269d69..7d6ece861 100644 --- a/Source/Engine/Platform/Apple/AppleFileSystem.cpp +++ b/Source/Engine/Platform/Apple/AppleFileSystem.cpp @@ -1,6 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. -#if PLATFORM_MAC || PLATFORM_IOS +#if PLATFORM_APPLE_FAMILY #include "AppleFileSystem.h" #include "AppleUtils.h" diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.h b/Source/Engine/Platform/Apple/AppleFileSystem.h index 45d9e4df6..1a1cf5f4b 100644 --- a/Source/Engine/Platform/Apple/AppleFileSystem.h +++ b/Source/Engine/Platform/Apple/AppleFileSystem.h @@ -2,7 +2,7 @@ #pragma once -#if PLATFORM_MAC || PLATFORM_IOS +#if PLATFORM_APPLE_FAMILY #include "Engine/Platform/Unix/UnixFileSystem.h" diff --git a/Source/Engine/Platform/Apple/ApplePlatform.cpp b/Source/Engine/Platform/Apple/ApplePlatform.cpp index 49b484ac6..d0d615c58 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.cpp +++ b/Source/Engine/Platform/Apple/ApplePlatform.cpp @@ -1,6 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. -#if PLATFORM_MAC || PLATFORM_IOS +#if PLATFORM_APPLE_FAMILY #if PLATFORM_MAC #define PLATFORM_MAC_CACHED 1 diff --git a/Source/Engine/Platform/Apple/ApplePlatform.h b/Source/Engine/Platform/Apple/ApplePlatform.h index ca4e2614d..f8d73d1e0 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.h +++ b/Source/Engine/Platform/Apple/ApplePlatform.h @@ -2,7 +2,7 @@ #pragma once -#if PLATFORM_MAC || PLATFORM_IOS +#if PLATFORM_APPLE_FAMILY #include "../Unix/UnixPlatform.h" diff --git a/Source/Engine/Platform/Apple/AppleThread.h b/Source/Engine/Platform/Apple/AppleThread.h index cf90657a1..6442cc373 100644 --- a/Source/Engine/Platform/Apple/AppleThread.h +++ b/Source/Engine/Platform/Apple/AppleThread.h @@ -2,7 +2,7 @@ #pragma once -#if PLATFORM_MAC || PLATFORM_IOS +#if PLATFORM_APPLE_FAMILY #include "../Unix/UnixThread.h" #include diff --git a/Source/Engine/Platform/Apple/AppleUtils.h b/Source/Engine/Platform/Apple/AppleUtils.h index 07d7fcb1f..e083d5fb4 100644 --- a/Source/Engine/Platform/Apple/AppleUtils.h +++ b/Source/Engine/Platform/Apple/AppleUtils.h @@ -2,7 +2,7 @@ #pragma once -#if PLATFORM_MAC || PLATFORM_IOS +#if PLATFORM_APPLE_FAMILY #include "Engine/Core/Types/String.h" #include From 067c8ae5b81f8352c82d56c9e83f058e07e3808b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 27 May 2026 14:40:41 +0200 Subject: [PATCH 17/51] Bump up build number --- Flax.flaxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flax.flaxproj b/Flax.flaxproj index ebe0f807c..04a161cf2 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 12, "Revision": 0, - "Build": 6912 + "Build": 6913 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.", From 7c1df5c98036bd25268e4696a6b7be9fedf8ab62 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 27 May 2026 17:22:22 +0200 Subject: [PATCH 18/51] Fix crash when loading invalid `VisjectMeta` #4114 --- Source/Engine/Visject/VisjectMeta.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Visject/VisjectMeta.cpp b/Source/Engine/Visject/VisjectMeta.cpp index 00f3ca3b4..ecef4c26c 100644 --- a/Source/Engine/Visject/VisjectMeta.cpp +++ b/Source/Engine/Visject/VisjectMeta.cpp @@ -9,13 +9,15 @@ bool VisjectMeta::Load(ReadStream* stream, bool loadData) { Release(); - int32 entries; + int32 entries = -1; stream->ReadInt32(&entries); + if (entries < 0 || entries > MAX_uint16) + return true; Entries.Resize(entries); for (int32 i = 0; i < entries; i++) { - Entry& e = Entries[i]; + Entry& e = Entries.Get()[i]; stream->ReadInt32(&e.TypeID); DateTime creationTime; From ab6b5927f8d5f462859d046050fe2149ace3dfe5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 27 May 2026 17:23:22 +0200 Subject: [PATCH 19/51] Fix missing `Int2`/`Int3`/`Int4` in `Variant` support implementation parts #4114 --- .../Animations/Graph/AnimGroup.Animation.cpp | 60 +++----- Source/Engine/Core/Math/Vector2.cpp | 2 + Source/Engine/Core/Math/Vector3.cpp | 2 + Source/Engine/Core/Math/Vector4.cpp | 2 + Source/Engine/Core/Types/Variant.cpp | 130 ++++++++++++++++-- .../Graphics/Materials/MaterialParams.cpp | 12 ++ .../ParticleEmitterGraph.CPU.Particles.cpp | 60 +++----- Source/Engine/Scripting/ManagedCLR/MUtils.cpp | 21 +++ Source/Engine/Serialization/Stream.cpp | 6 + Source/Engine/Visject/GraphUtilities.cpp | 90 ++++++++++++ 10 files changed, 293 insertions(+), 92 deletions(-) diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index b07b5a132..afebe60aa 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -873,65 +873,41 @@ void AnimGraphExecutor::ProcessGroupParameters(Box* box, Node* node, Value& valu switch (param->Type.Type) { case VariantType::Float2: - switch (box->ID) - { - case 1: - case 2: + if (box->ID >= 1 && box->ID <= 2) value = value.AsFloat2().Raw[box->ID - 1]; - break; - } break; case VariantType::Float3: - switch (box->ID) - { - case 1: - case 2: - case 3: + if (box->ID >= 1 && box->ID <= 3) value = value.AsFloat3().Raw[box->ID - 1]; - break; - } break; case VariantType::Float4: case VariantType::Color: - switch (box->ID) - { - case 1: - case 2: - case 3: - case 4: + if (box->ID >= 1 && box->ID <= 4) value = value.AsFloat4().Raw[box->ID - 1]; - break; - } break; case VariantType::Double2: - switch (box->ID) - { - case 1: - case 2: + if (box->ID >= 1 && box->ID <= 2) value = value.AsDouble2().Raw[box->ID - 1]; - break; - } break; case VariantType::Double3: - switch (box->ID) - { - case 1: - case 2: - case 3: + if (box->ID >= 1 && box->ID <= 3) value = value.AsDouble3().Raw[box->ID - 1]; - break; - } break; case VariantType::Double4: - switch (box->ID) - { - case 1: - case 2: - case 3: - case 4: + if (box->ID >= 1 && box->ID <= 4) value = value.AsDouble4().Raw[box->ID - 1]; - break; - } + break; + case VariantType::Int2: + if (box->ID >= 1 && box->ID <= 2) + value = value.AsInt2().Raw[box->ID - 1]; + break; + case VariantType::Int3: + if (box->ID >= 1 && box->ID <= 3) + value = value.AsInt3().Raw[box->ID - 1]; + break; + case VariantType::Int4: + if (box->ID >= 1 && box->ID <= 4) + value = value.AsInt4().Raw[box->ID - 1]; break; case VariantType::Matrix: { diff --git a/Source/Engine/Core/Math/Vector2.cpp b/Source/Engine/Core/Math/Vector2.cpp index c6abd7589..790a27bcc 100644 --- a/Source/Engine/Core/Math/Vector2.cpp +++ b/Source/Engine/Core/Math/Vector2.cpp @@ -9,6 +9,8 @@ // Float static_assert(sizeof(Float2) == 8, "Invalid Float2 type size."); +static_assert(sizeof(Int2) == 8, "Invalid Int2 type size."); +static_assert(sizeof(Double2) == 16, "Invalid Double2 type size."); template<> const Float2 Float2::Zero(0.0f); diff --git a/Source/Engine/Core/Math/Vector3.cpp b/Source/Engine/Core/Math/Vector3.cpp index 6c0c3f1f8..f96954d97 100644 --- a/Source/Engine/Core/Math/Vector3.cpp +++ b/Source/Engine/Core/Math/Vector3.cpp @@ -13,6 +13,8 @@ // Float static_assert(sizeof(Float3) == 12, "Invalid Float3 type size."); +static_assert(sizeof(Int3) == 12, "Invalid Int3 type size."); +static_assert(sizeof(Double3) == 24, "Invalid Double3 type size."); template<> const Float3 Float3::Zero(0.0f); diff --git a/Source/Engine/Core/Math/Vector4.cpp b/Source/Engine/Core/Math/Vector4.cpp index 699d2d550..afae743c6 100644 --- a/Source/Engine/Core/Math/Vector4.cpp +++ b/Source/Engine/Core/Math/Vector4.cpp @@ -11,6 +11,8 @@ // Float static_assert(sizeof(Float4) == 16, "Invalid Float4 type size."); +static_assert(sizeof(Int4) == 16, "Invalid Int4 type size."); +static_assert(sizeof(Double4) == 32, "Invalid Double4 type size."); template<> const Float4 Float4::Zero(0.0f); diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index aecc292dc..7f805f9e3 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -4,6 +4,7 @@ #include "Engine/Core/Collections/HashFunctions.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Content/Asset.h" +#include "Engine/Content/Deprecated.h" #include "Engine/Core/Log.h" #include "Engine/Core/Math/Mathd.h" #include "Engine/Core/Math/BoundingBox.h" @@ -3193,6 +3194,9 @@ bool Variant::CanCast(const Variant& v, const VariantType& to) case VariantType::Double2: case VariantType::Double3: case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: case VariantType::Enum: return true; default: @@ -3216,6 +3220,9 @@ bool Variant::CanCast(const Variant& v, const VariantType& to) case VariantType::Double2: case VariantType::Double3: case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: case VariantType::Enum: return true; default: @@ -3239,6 +3246,9 @@ bool Variant::CanCast(const Variant& v, const VariantType& to) case VariantType::Double2: case VariantType::Double3: case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: case VariantType::Enum: return true; default: @@ -3262,6 +3272,9 @@ bool Variant::CanCast(const Variant& v, const VariantType& to) case VariantType::Double2: case VariantType::Double3: case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: case VariantType::Enum: return true; default: @@ -3285,6 +3298,9 @@ bool Variant::CanCast(const Variant& v, const VariantType& to) case VariantType::Double2: case VariantType::Double3: case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: case VariantType::Enum: return true; default: @@ -3308,6 +3324,9 @@ bool Variant::CanCast(const Variant& v, const VariantType& to) case VariantType::Double2: case VariantType::Double3: case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: case VariantType::Enum: return true; default: @@ -3331,6 +3350,9 @@ bool Variant::CanCast(const Variant& v, const VariantType& to) case VariantType::Double2: case VariantType::Double3: case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: case VariantType::Enum: return true; default: @@ -3354,6 +3376,9 @@ bool Variant::CanCast(const Variant& v, const VariantType& to) case VariantType::Double2: case VariantType::Double3: case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: case VariantType::Enum: return true; default: @@ -3377,6 +3402,9 @@ bool Variant::CanCast(const Variant& v, const VariantType& to) case VariantType::Double2: case VariantType::Double3: case VariantType::Double4: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: case VariantType::Enum: return true; default: @@ -3520,6 +3548,12 @@ Variant Variant::Cast(const Variant& v, const VariantType& to) return Variant(Double3(v.AsBool ? 1.0 : 0.0)); case VariantType::Double4: return Variant(Double4(v.AsBool ? 1.0 : 0.0)); + case VariantType::Int2: + return Variant(Int2(v.AsBool ? 1 : 0)); + case VariantType::Int3: + return Variant(Int3(v.AsBool ? 1 : 0)); + case VariantType::Int4: + return Variant(Int4(v.AsBool ? 1 : 0)); case VariantType::Enum: return Enum(to, v.AsBool ? 1 : 0); default: ; @@ -3558,6 +3592,12 @@ Variant Variant::Cast(const Variant& v, const VariantType& to) return Variant(Double3((double)v.AsInt16)); case VariantType::Double4: return Variant(Double4((double)v.AsInt16)); + case VariantType::Int2: + return Variant(Int2(v.AsInt16)); + case VariantType::Int3: + return Variant(Int3(v.AsInt16)); + case VariantType::Int4: + return Variant(Int4(v.AsInt16)); case VariantType::Enum: return Enum(to, (int64)v.AsInt16); default: ; @@ -3588,6 +3628,18 @@ Variant Variant::Cast(const Variant& v, const VariantType& to) return Variant(Float3((float)v.AsInt)); case VariantType::Float4: return Variant(Float4((float)v.AsInt)); + case VariantType::Double2: + return Variant(Double2((double)v.AsInt)); + case VariantType::Double3: + return Variant(Double3((double)v.AsInt)); + case VariantType::Double4: + return Variant(Double4((double)v.AsInt)); + case VariantType::Int2: + return Variant(Int2(v.AsInt)); + case VariantType::Int3: + return Variant(Int3(v.AsInt)); + case VariantType::Int4: + return Variant(Int4(v.AsInt)); case VariantType::Color: return Variant(Color((float)v.AsInt)); case VariantType::Enum: @@ -3628,6 +3680,12 @@ Variant Variant::Cast(const Variant& v, const VariantType& to) return Variant(Double3((double)v.AsUint16)); case VariantType::Double4: return Variant(Double4((double)v.AsUint16)); + case VariantType::Int2: + return Variant(Int2(v.AsUint16)); + case VariantType::Int3: + return Variant(Int3(v.AsUint16)); + case VariantType::Int4: + return Variant(Int4(v.AsUint16)); case VariantType::Enum: return Enum(to, (int64)v.AsUint16); default: ; @@ -3666,6 +3724,12 @@ Variant Variant::Cast(const Variant& v, const VariantType& to) return Variant(Double3((double)v.AsUint)); case VariantType::Double4: return Variant(Double4((double)v.AsUint)); + case VariantType::Int2: + return Variant(Int2((int32)v.AsUint)); + case VariantType::Int3: + return Variant(Int3((int32)v.AsUint)); + case VariantType::Int4: + return Variant(Int4((int32)v.AsUint)); case VariantType::Enum: return Enum(to, (int64)v.AsUint); default: ; @@ -3704,6 +3768,12 @@ Variant Variant::Cast(const Variant& v, const VariantType& to) return Variant(Double3((double)v.AsInt64)); case VariantType::Double4: return Variant(Double4((double)v.AsInt64)); + case VariantType::Int2: + return Variant(Int2((int32)v.AsInt64)); + case VariantType::Int3: + return Variant(Int3((int32)v.AsInt64)); + case VariantType::Int4: + return Variant(Int4((int32)v.AsInt64)); case VariantType::Enum: return Enum(to, (int64)v.AsInt64); default: ; @@ -3723,25 +3793,31 @@ Variant Variant::Cast(const Variant& v, const VariantType& to) case VariantType::Uint16: return Variant((uint16)v.AsUint16); case VariantType::Uint: - return Variant((uint32)v.AsUint); + return Variant((uint32)v.AsUint64); case VariantType::Float: return Variant((float)v.AsUint64); case VariantType::Double: return Variant((double)v.AsUint64); case VariantType::Float2: - return Variant(Float2((float)v.AsInt)); + return Variant(Float2((float)v.AsUint64)); case VariantType::Float3: - return Variant(Float3((float)v.AsInt)); + return Variant(Float3((float)v.AsUint64)); case VariantType::Float4: - return Variant(Float4((float)v.AsInt)); + return Variant(Float4((float)v.AsUint64)); case VariantType::Color: - return Variant(Color((float)v.AsInt)); + return Variant(Color((float)v.AsUint64)); case VariantType::Double2: - return Variant(Double2((double)v.AsInt)); + return Variant(Double2((double)v.AsUint64)); case VariantType::Double3: - return Variant(Double3((double)v.AsInt)); + return Variant(Double3((double)v.AsUint64)); case VariantType::Double4: - return Variant(Double4((double)v.AsInt)); + return Variant(Double4((double)v.AsUint64)); + case VariantType::Int2: + return Variant(Int2((int32)v.AsUint64)); + case VariantType::Int3: + return Variant(Int3((int32)v.AsUint64)); + case VariantType::Int4: + return Variant(Int4((int32)v.AsUint64)); case VariantType::Enum: return Enum(to, (int64)v.AsInt); default: ; @@ -3780,6 +3856,12 @@ Variant Variant::Cast(const Variant& v, const VariantType& to) return Variant(Double3(v.AsFloat)); case VariantType::Double4: return Variant(Double4(v.AsFloat)); + case VariantType::Int2: + return Variant(Int2((int32)v.AsFloat)); + case VariantType::Int3: + return Variant(Int3((int32)v.AsFloat)); + case VariantType::Int4: + return Variant(Int4((int32)v.AsFloat)); case VariantType::Enum: return Enum(to, (int64)v.AsFloat); default: ; @@ -3818,6 +3900,12 @@ Variant Variant::Cast(const Variant& v, const VariantType& to) return Variant(Double3(v.AsDouble)); case VariantType::Double4: return Variant(Double4(v.AsDouble)); + case VariantType::Int2: + return Variant(Int2((int32)v.AsFloat)); + case VariantType::Int3: + return Variant(Int3((int32)v.AsFloat)); + case VariantType::Int4: + return Variant(Int4((int32)v.AsFloat)); case VariantType::Enum: return Enum(to, (int64)v.AsDouble); default: ; @@ -4246,6 +4334,7 @@ void Variant::AllocStructure() AsBlob.Length = 2; AsBlob.Data = Allocator::Allocate(AsBlob.Length); *((int16*)AsBlob.Data) = 0; + MARK_CONTENT_DEPRECATED(); } #if USE_CSHARP else if (const auto mclass = Scripting::FindClass(typeName)) @@ -4371,6 +4460,13 @@ uint32 GetHash(const Variant& key) return GetHash((void*)key.AsObject); case VariantType::Structure: case VariantType::Blob: + case VariantType::Transform: + case VariantType::Matrix: +#if USE_LARGE_WORLDS + case VariantType::BoundingSphere: + case VariantType::BoundingBox: + case VariantType::Ray: +#endif return Crc::MemCrc32(key.AsBlob.Data, key.AsBlob.Length); case VariantType::Asset: return GetHash((void*)key.AsAsset); @@ -4380,6 +4476,24 @@ uint32 GetHash(const Variant& key) return GetHash(*(Guid*)key.AsData); case VariantType::Typename: return GetHash((const char*)key.AsBlob.Data); + case VariantType::Float2: + return GetHash(*(const Float2*)key.AsData); + case VariantType::Float3: + return GetHash(*(const Float3*)key.AsData); + case VariantType::Float4: + return GetHash(*(const Float4*)key.AsData); + case VariantType::Int2: + return GetHash(*(const Int2*)key.AsData); + case VariantType::Int3: + return GetHash(*(const Int3*)key.AsData); + case VariantType::Int4: + return GetHash(*(const Int4*)key.AsData); + case VariantType::Double2: + return GetHash(*(const Double2*)key.AsData); + case VariantType::Double3: + return GetHash(*(const Double3*)key.AsData); + case VariantType::Double4: + return GetHash(*(const Double4*)key.AsBlob.Data); case VariantType::ManagedObject: #if USE_CSHARP return key.MANAGED_GC_HANDLE ? (uint32)MCore::Object::GetHashCode(MCore::GCHandle::GetTarget(key.MANAGED_GC_HANDLE)) : 0; diff --git a/Source/Engine/Graphics/Materials/MaterialParams.cpp b/Source/Engine/Graphics/Materials/MaterialParams.cpp index 082ac497b..3157f3552 100644 --- a/Source/Engine/Graphics/Materials/MaterialParams.cpp +++ b/Source/Engine/Graphics/Materials/MaterialParams.cpp @@ -460,6 +460,18 @@ void MaterialParameter::Bind(BindMeta& meta) const ASSERT_LOW_LAYER(meta.Constants.Get() && meta.Constants.Length() >= (int32)(_offset + sizeof(Float4))); *((Float4*)(meta.Constants.Get() + _offset)) = (Float4)e->Value.AsDouble4(); break; + case VariantType::Int2: + ASSERT_LOW_LAYER(meta.Constants.Get() && meta.Constants.Length() >= (int32)(_offset + sizeof(Int2))); + *((Int2*)(meta.Constants.Get() + _offset)) = (Int2)e->Value.AsInt2(); + break; + case VariantType::Int3: + ASSERT_LOW_LAYER(meta.Constants.Get() && meta.Constants.Length() >= (int32)(_offset + sizeof(Int3))); + *((Int3*)(meta.Constants.Get() + _offset)) = (Int3)e->Value.AsInt3(); + break; + case VariantType::Int4: + ASSERT_LOW_LAYER(meta.Constants.Get() && meta.Constants.Length() >= (int32)(_offset + sizeof(Int4))); + *((Int4*)(meta.Constants.Get() + _offset)) = (Int4)e->Value.AsInt4(); + break; default: ; } } diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp index 1a7ba37d1..e12b781b0 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp @@ -27,65 +27,41 @@ void ParticleEmitterGraphCPUExecutor::ProcessGroupParameters(Box* box, Node* nod switch (param->Type.Type) { case VariantType::Float2: - switch (box->ID) - { - case 1: - case 2: + if (box->ID >= 1 && box->ID <= 2) value = value.AsFloat2().Raw[box->ID - 1]; - break; - } break; case VariantType::Float3: - switch (box->ID) - { - case 1: - case 2: - case 3: + if (box->ID >= 1 && box->ID <= 3) value = value.AsFloat3().Raw[box->ID - 1]; - break; - } break; case VariantType::Float4: case VariantType::Color: - switch (box->ID) - { - case 1: - case 2: - case 3: - case 4: + if (box->ID >= 1 && box->ID <= 4) value = value.AsFloat4().Raw[box->ID - 1]; - break; - } break; case VariantType::Double2: - switch (box->ID) - { - case 1: - case 2: + if (box->ID >= 1 && box->ID <= 2) value = value.AsDouble2().Raw[box->ID - 1]; - break; - } break; case VariantType::Double3: - switch (box->ID) - { - case 1: - case 2: - case 3: + if (box->ID >= 1 && box->ID <= 3) value = value.AsDouble3().Raw[box->ID - 1]; - break; - } break; case VariantType::Double4: - switch (box->ID) - { - case 1: - case 2: - case 3: - case 4: + if (box->ID >= 1 && box->ID <= 4) value = value.AsDouble4().Raw[box->ID - 1]; - break; - } + break; + case VariantType::Int2: + if (box->ID >= 1 && box->ID <= 2) + value = value.AsInt2().Raw[box->ID - 1]; + break; + case VariantType::Int3: + if (box->ID >= 1 && box->ID <= 3) + value = value.AsInt3().Raw[box->ID - 1]; + break; + case VariantType::Int4: + if (box->ID >= 1 && box->ID <= 4) + value = value.AsInt4().Raw[box->ID - 1]; break; case VariantType::Matrix: { diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp index 0d7617e0d..5c0ad1676 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp @@ -609,6 +609,12 @@ MObject* MUtils::BoxVariant(const Variant& value) return MCore::Object::Box((void*)&value.AsData, Double3::TypeInitializer.GetClass()); case VariantType::Double4: return MCore::Object::Box((void*)&value.AsData, Double4::TypeInitializer.GetClass()); + case VariantType::Int2: + return MCore::Object::Box((void*)&value.AsData, Int2::TypeInitializer.GetClass()); + case VariantType::Int3: + return MCore::Object::Box((void*)&value.AsData, Int3::TypeInitializer.GetClass()); + case VariantType::Int4: + return MCore::Object::Box((void*)&value.AsData, Int4::TypeInitializer.GetClass()); case VariantType::Color: return MCore::Object::Box((void*)&value.AsData, stdTypes.ColorClass); case VariantType::Guid: @@ -889,6 +895,12 @@ MClass* MUtils::GetClass(const VariantType& value) return Double3::TypeInitializer.GetClass(); case VariantType::Double4: return Double4::TypeInitializer.GetClass(); + case VariantType::Int2: + return Int2::TypeInitializer.GetClass(); + case VariantType::Int3: + return Int3::TypeInitializer.GetClass(); + case VariantType::Int4: + return Int4::TypeInitializer.GetClass(); case VariantType::Color: return Color::TypeInitializer.GetClass(); case VariantType::Guid: @@ -979,6 +991,12 @@ MClass* MUtils::GetClass(const Variant& value) return Double3::TypeInitializer.GetClass(); case VariantType::Double4: return Double4::TypeInitializer.GetClass(); + case VariantType::Int2: + return Int2::TypeInitializer.GetClass(); + case VariantType::Int3: + return Int3::TypeInitializer.GetClass(); + case VariantType::Int4: + return Int4::TypeInitializer.GetClass(); case VariantType::Color: return stdTypes.ColorClass; case VariantType::Guid: @@ -1148,6 +1166,9 @@ void* MUtils::VariantToManagedArgPtr(Variant& value, MType* type, bool& failed) CASE_IN_BUILD_TYPE(Double2, AsData); CASE_IN_BUILD_TYPE(Double3, AsData); CASE_IN_BUILD_TYPE(Double4, AsBlob.Data); + CASE_IN_BUILD_TYPE(Int2, AsData); + CASE_IN_BUILD_TYPE(Int3, AsData); + CASE_IN_BUILD_TYPE(Int4, AsData); #undef CASE_IN_BUILD_TYPE if (klass->IsValueType()) { diff --git a/Source/Engine/Serialization/Stream.cpp b/Source/Engine/Serialization/Stream.cpp index 5b109fd55..0ba3ced8b 100644 --- a/Source/Engine/Serialization/Stream.cpp +++ b/Source/Engine/Serialization/Stream.cpp @@ -288,12 +288,15 @@ void ReadStream::Read(Variant& data) break; } case VariantType::Float2: + case VariantType::Int2: ReadBytes(&data.AsData, sizeof(Float2)); break; case VariantType::Float3: + case VariantType::Int3: ReadBytes(&data.AsData, sizeof(Float3)); break; case VariantType::Float4: + case VariantType::Int4: ReadBytes(&data.AsData, sizeof(Float4)); break; case VariantType::Double2: @@ -687,12 +690,15 @@ void WriteStream::Write(const Variant& data) Write(id); break; case VariantType::Float2: + case VariantType::Int2: WriteBytes(data.AsData, sizeof(Float2)); break; case VariantType::Float3: + case VariantType::Int3: WriteBytes(data.AsData, sizeof(Float3)); break; case VariantType::Float4: + case VariantType::Int4: WriteBytes(data.AsData, sizeof(Float4)); break; case VariantType::Double2: diff --git a/Source/Engine/Visject/GraphUtilities.cpp b/Source/Engine/Visject/GraphUtilities.cpp index 36bfe297f..13f43fc1a 100644 --- a/Source/Engine/Visject/GraphUtilities.cpp +++ b/Source/Engine/Visject/GraphUtilities.cpp @@ -256,6 +256,33 @@ void GraphUtilities::ApplySomeMathHere(Variant& v, Variant& a, MathOp1 op) vv.W = (double)op((float)aa.W); break; } + case VariantType::Int2: + { + Int2& vv = *(Int2*)v.AsData; + const Int2& aa = *(const Int2*)a.AsData; + vv.X = (int32)op((float)aa.X); + vv.Y = (int32)op((float)aa.Y); + break; + } + case VariantType::Int3: + { + Int3& vv = *(Int3*)v.AsData; + const Int3& aa = *(const Int3*)a.AsData; + vv.X = (int32)op((float)aa.X); + vv.Y = (int32)op((float)aa.Y); + vv.Z = (int32)op((float)aa.Z); + break; + } + case VariantType::Int4: + { + Int4& vv = *(Int4*)v.AsData; + const Int4& aa = *(const Int4*)a.AsData; + vv.X = (int32)op((float)aa.X); + vv.Y = (int32)op((float)aa.Y); + vv.Z = (int32)op((float)aa.Z); + vv.W = (int32)op((float)aa.W); + break; + } case VariantType::Quaternion: { Quaternion& vv = *(Quaternion*)v.AsData; @@ -381,6 +408,36 @@ void GraphUtilities::ApplySomeMathHere(Variant& v, Variant& a, Variant& b, MathO vv.W = (double)op((float)aa.W, (float)bb.W); break; } + case VariantType::Int2: + { + Int2& vv = *(Int2*)v.AsData; + const Int2& aa = *(const Int2*)a.AsData; + const Int2& bb = *(const Int2*)b.AsData; + vv.X = (int32)op((float)aa.X, (float)bb.X); + vv.Y = (int32)op((float)aa.Y, (float)bb.Y); + break; + } + case VariantType::Int3: + { + Int3& vv = *(Int3*)v.AsData; + const Int3& aa = *(const Int3*)a.AsData; + const Int3& bb = *(const Int3*)b.AsData; + vv.X = (int32)op((float)aa.X, (float)bb.X); + vv.Y = (int32)op((float)aa.Y, (float)bb.Y); + vv.Z = (int32)op((float)aa.Z, (float)bb.Z); + break; + } + case VariantType::Int4: + { + Int4& vv = *(Int4*)v.AsData; + const Int4& aa = *(const Int4*)a.AsData; + const Int4& bb = *(const Int4*)b.AsData; + vv.X = (int32)op((float)aa.X, (float)bb.X); + vv.Y = (int32)op((float)aa.Y, (float)bb.Y); + vv.Z = (int32)op((float)aa.Z, (float)bb.Z); + vv.W = (int32)op((float)aa.W, (float)bb.W); + break; + } case VariantType::Quaternion: { Quaternion& vv = *(Quaternion*)v.AsData; @@ -499,6 +556,39 @@ void GraphUtilities::ApplySomeMathHere(Variant& v, Variant& a, Variant& b, Varia vv.W = (double)op((float)aa.W, (float)bb.W, (float)cc.W); break; } + case VariantType::Int2: + { + Int2& vv = *(Int2*)v.AsData; + const Int3& aa = *(const Int2*)a.AsData; + const Int3& bb = *(const Int2*)b.AsData; + const Int3& cc = *(const Int2*)c.AsData; + vv.X = (int32)op((float)aa.X, (float)bb.X, (float)cc.X); + vv.Y = (int32)op((float)aa.Y, (float)bb.Y, (float)cc.Y); + break; + } + case VariantType::Int3: + { + Int3& vv = *(Int3*)v.AsData; + const Int3& aa = *(const Int3*)a.AsData; + const Int3& bb = *(const Int3*)b.AsData; + const Int3& cc = *(const Int3*)c.AsData; + vv.X = (int32)op((float)aa.X, (float)bb.X, (float)cc.X); + vv.Y = (int32)op((float)aa.Y, (float)bb.Y, (float)cc.Y); + vv.Z = (int32)op((float)aa.Z, (float)bb.Z, (float)cc.Z); + break; + } + case VariantType::Int4: + { + Int4& vv = *(Int4*)v.AsData; + const Int4& aa = *(const Int4*)a.AsData; + const Int4& bb = *(const Int4*)b.AsData; + const Int4& cc = *(const Int4*)c.AsData; + vv.X = (int32)op((float)aa.X, (float)bb.X, (float)cc.X); + vv.Y = (int32)op((float)aa.Y, (float)bb.Y, (float)cc.Y); + vv.Z = (int32)op((float)aa.Z, (float)bb.Z, (float)cc.Z); + vv.W = (int32)op((float)aa.W, (float)bb.W, (float)cc.W); + break; + } case VariantType::Quaternion: { Quaternion& vv = *(Quaternion*)v.AsData; From 791fb785cfbfa9d81575664dafd6ed9d4098aa8d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 27 May 2026 17:54:06 +0200 Subject: [PATCH 20/51] Revert "Simplify apple platform include defines" This reverts commit 3140c711a4557bcba80448d143f7dc9d9c446a0f. --- Source/Engine/Platform/Apple/AppleFileSystem.cpp | 2 +- Source/Engine/Platform/Apple/AppleFileSystem.h | 2 +- Source/Engine/Platform/Apple/ApplePlatform.cpp | 2 +- Source/Engine/Platform/Apple/ApplePlatform.h | 2 +- Source/Engine/Platform/Apple/AppleThread.h | 2 +- Source/Engine/Platform/Apple/AppleUtils.h | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.cpp b/Source/Engine/Platform/Apple/AppleFileSystem.cpp index 7d6ece861..a38269d69 100644 --- a/Source/Engine/Platform/Apple/AppleFileSystem.cpp +++ b/Source/Engine/Platform/Apple/AppleFileSystem.cpp @@ -1,6 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. -#if PLATFORM_APPLE_FAMILY +#if PLATFORM_MAC || PLATFORM_IOS #include "AppleFileSystem.h" #include "AppleUtils.h" diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.h b/Source/Engine/Platform/Apple/AppleFileSystem.h index 1a1cf5f4b..45d9e4df6 100644 --- a/Source/Engine/Platform/Apple/AppleFileSystem.h +++ b/Source/Engine/Platform/Apple/AppleFileSystem.h @@ -2,7 +2,7 @@ #pragma once -#if PLATFORM_APPLE_FAMILY +#if PLATFORM_MAC || PLATFORM_IOS #include "Engine/Platform/Unix/UnixFileSystem.h" diff --git a/Source/Engine/Platform/Apple/ApplePlatform.cpp b/Source/Engine/Platform/Apple/ApplePlatform.cpp index d0d615c58..49b484ac6 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.cpp +++ b/Source/Engine/Platform/Apple/ApplePlatform.cpp @@ -1,6 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. -#if PLATFORM_APPLE_FAMILY +#if PLATFORM_MAC || PLATFORM_IOS #if PLATFORM_MAC #define PLATFORM_MAC_CACHED 1 diff --git a/Source/Engine/Platform/Apple/ApplePlatform.h b/Source/Engine/Platform/Apple/ApplePlatform.h index f8d73d1e0..ca4e2614d 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.h +++ b/Source/Engine/Platform/Apple/ApplePlatform.h @@ -2,7 +2,7 @@ #pragma once -#if PLATFORM_APPLE_FAMILY +#if PLATFORM_MAC || PLATFORM_IOS #include "../Unix/UnixPlatform.h" diff --git a/Source/Engine/Platform/Apple/AppleThread.h b/Source/Engine/Platform/Apple/AppleThread.h index 6442cc373..cf90657a1 100644 --- a/Source/Engine/Platform/Apple/AppleThread.h +++ b/Source/Engine/Platform/Apple/AppleThread.h @@ -2,7 +2,7 @@ #pragma once -#if PLATFORM_APPLE_FAMILY +#if PLATFORM_MAC || PLATFORM_IOS #include "../Unix/UnixThread.h" #include diff --git a/Source/Engine/Platform/Apple/AppleUtils.h b/Source/Engine/Platform/Apple/AppleUtils.h index e083d5fb4..07d7fcb1f 100644 --- a/Source/Engine/Platform/Apple/AppleUtils.h +++ b/Source/Engine/Platform/Apple/AppleUtils.h @@ -2,7 +2,7 @@ #pragma once -#if PLATFORM_APPLE_FAMILY +#if PLATFORM_MAC || PLATFORM_IOS #include "Engine/Core/Types/String.h" #include From 320d37d9a2569a8699f47c80fb7e1c92338a25c5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 09:54:18 +0200 Subject: [PATCH 21/51] Fix `MeshAccessor` triangle count calculation bug #4121 --- Source/Engine/Graphics/Models/MeshAccessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index cd216cdf5..17c1e71d7 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -621,7 +621,7 @@ namespace FlaxEngine { ibData = dataPtr[IB]; use16BitIndexBuffer = _formats[IB] == PixelFormat.R16_UInt; - triangles = (uint)(_data[IB].Length / PixelFormatExtensions.SizeInBytes(_formats[IB])); + triangles = (uint)(_data[IB].Length / (PixelFormatExtensions.SizeInBytes(_formats[IB]) * 3)); } if (mesh.Init(vertices, triangles, vbData, ibData, use16BitIndexBuffer, vbLayout)) From e7c5f257e9e8ac819c21f1a18556531077d866e5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 09:54:35 +0200 Subject: [PATCH 22/51] Minor fixes and improvements --- Source/Editor/GUI/ItemsListContextMenu.cs | 2 ++ Source/Engine/Graphics/Async/GPUTasksContext.cpp | 3 ++- Source/Engine/Graphics/Models/MeshAccessor.cs | 13 +++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Source/Editor/GUI/ItemsListContextMenu.cs b/Source/Editor/GUI/ItemsListContextMenu.cs index 50c08e9ba..5f09342c7 100644 --- a/Source/Editor/GUI/ItemsListContextMenu.cs +++ b/Source/Editor/GUI/ItemsListContextMenu.cs @@ -589,6 +589,8 @@ namespace FlaxEditor.GUI // Get the next item bool controlDown = Root.GetKey(KeyboardKeys.Control); var items = GetVisibleItems(!controlDown); + if (items.Count == 0) + return true; var focusedIndex = items.IndexOf(focusedItem); int delta = key == KeyboardKeys.ArrowDown ? -1 : 1; diff --git a/Source/Engine/Graphics/Async/GPUTasksContext.cpp b/Source/Engine/Graphics/Async/GPUTasksContext.cpp index fe2198865..4fbe88510 100644 --- a/Source/Engine/Graphics/Async/GPUTasksContext.cpp +++ b/Source/Engine/Graphics/Async/GPUTasksContext.cpp @@ -76,9 +76,10 @@ void GPUTasksContext::OnFrameBegin() { auto task = _tasksSyncing[i]; auto state = task->GetState(); + if (EnumHasAllFlags(task->Flags, ObjectFlags::WasMarkedToDelete)) + state = TaskState::Finished; if (task->GetSyncPoint() <= _currentSyncPoint && state != TaskState::Finished) { - // TODO: add stats counter and count performed jobs, print to log on exit. task->Sync(); } if (state == TaskState::Failed || state == TaskState::Canceled) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index 17c1e71d7..fccb4e518 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -643,11 +643,16 @@ namespace FlaxEngine else { Float3 min = Float3.Maximum, max = Float3.Minimum; - for (int i = 0; i < vertices; i++) + PixelFormatSampler.Get(positionStream.Format, out var positionSampler); + int positionStride = positionStream.Stride; + fixed (byte* data = positionStream.Data) { - Float3 pos = positionStream.GetFloat3(i); - Float3.Min(ref min, ref pos, out min); - Float3.Max(ref max, ref pos, out max); + for (int i = 0; i < vertices; i++) + { + Float3 pos = new Float3(positionSampler.Read(data + i * positionStride)); + Float3.Min(ref min, ref pos, out min); + Float3.Max(ref max, ref pos, out max); + } } bounds = new BoundingBox(min, max); } From 3e670f5e80d9a98b095b48ab2ecd25ee74dfd0e9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 10:07:16 +0200 Subject: [PATCH 23/51] Remove not needed line #4013 --- Source/Editor/GUI/Timeline/Timeline.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 3ca83f903..861bd7e44 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -1302,7 +1302,6 @@ namespace FlaxEditor.GUI.Timeline if (track.ParentTrack != null) OnTracksOrderChanged(); track.OnSpawned(); - _tracksPanelArea.ScrollViewTo(track); MarkAsEdited(); if (withUndo) Undo?.AddAction(new AddRemoveTrackAction(this, track, true)); From b004e90606c124e001054c59f1286d818770c210 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 10:32:16 +0200 Subject: [PATCH 24/51] Adjust category names #4014 --- Source/Engine/Level/Actors/AnimatedModel.h | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index 8d0959147..b6d922744 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -110,74 +110,74 @@ public: /// /// If true, use per-bone motion blur on this skeletal model. It requires additional rendering, can be disabled to save performance. /// - API_FIELD(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Drawing\")") bool PerBoneMotionBlur = true; /// /// If true, animation speed will be affected by the global timescale parameter. /// - API_FIELD(Attributes="EditorOrder(30), DefaultValue(true), EditorDisplay(\"Update\")") + API_FIELD(Attributes="EditorOrder(30), DefaultValue(true), EditorDisplay(\"Updating\")") bool UseTimeScale = true; /// /// If true, the animation will be updated even when an actor cannot be seen by any camera. Otherwise, the animations themselves will also stop running when the actor is off-screen. /// - API_FIELD(Attributes="EditorOrder(40), DefaultValue(false), EditorDisplay(\"Update\")") + API_FIELD(Attributes="EditorOrder(40), DefaultValue(false), EditorDisplay(\"Updating\")") bool UpdateWhenOffscreen = false; /// /// The animation update delta timescale. Can be used to speed up animation playback or create slow motion effect. /// - API_FIELD(Attributes="EditorOrder(45), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Update\")") + API_FIELD(Attributes="EditorOrder(45), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Updating\")") float UpdateSpeed = 1.0f; /// /// The animation update mode. Can be used to optimize the performance. /// - API_FIELD(Attributes="EditorOrder(50), DefaultValue(AnimationUpdateMode.Auto), EditorDisplay(\"Update\")") + API_FIELD(Attributes="EditorOrder(50), DefaultValue(AnimationUpdateMode.Auto), EditorDisplay(\"Updating\")") AnimationUpdateMode UpdateMode = AnimationUpdateMode::Auto; /// /// The master scale parameter for the actor bounding box. Helps to reduce mesh flickering effect on screen edges. /// - API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Drawing\")") float BoundsScale = 1.5f; /// /// The custom bounds(in actor local space). If set to empty bounds then source skinned model bind pose bounds will be used. /// - API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Drawing\")") BoundingBox CustomBounds = BoundingBox::Zero; /// /// The model Level Of Detail bias value. Allows to increase or decrease rendered model quality. /// - API_FIELD(Attributes="EditorOrder(80), DefaultValue(0), Limit(-100, 100, 0.1f), EditorDisplay(\"Rendering\", \"LOD Bias\")") + API_FIELD(Attributes="EditorOrder(80), DefaultValue(0), Limit(-100, 100, 0.1f), EditorDisplay(\"Drawing\", \"LOD Bias\")") int32 LODBias = 0; /// /// Gets the model forced Level Of Detail index. Allows to bind the given model LOD to show. Value -1 disables this feature. /// - API_FIELD(Attributes="EditorOrder(90), DefaultValue(-1), Limit(-1, 100, 0.1f), EditorDisplay(\"Rendering\", \"Forced LOD\")") + API_FIELD(Attributes="EditorOrder(90), DefaultValue(-1), Limit(-1, 100, 0.1f), EditorDisplay(\"Drawing\", \"Forced LOD\")") int32 ForcedLOD = -1; /// /// The draw passes to use for rendering this object. /// - API_FIELD(Attributes="EditorOrder(100), DefaultValue(DrawPass.Default), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(100), DefaultValue(DrawPass.Default), EditorDisplay(\"Drawing\")") DrawPass DrawModes = DrawPass::Default; /// /// The object sort order key used when sorting drawable objects during rendering. Use lower values to draw object before others, higher values are rendered later (on top). Can be used to control transparency drawing. /// - API_FIELD(Attributes="EditorOrder(110), DefaultValue(0), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(110), DefaultValue(0), EditorDisplay(\"Drawing\")") int8 SortOrder = 0; /// /// The shadows casting mode. /// [Deprecated on 26.10.2022, expires on 26.10.2024] /// - API_FIELD(Attributes="EditorOrder(110), DefaultValue(ShadowsCastingMode.All), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(110), DefaultValue(ShadowsCastingMode.All), EditorDisplay(\"Drawing\")") DEPRECATED() ShadowsCastingMode ShadowsMode = ShadowsCastingMode::All; /// @@ -190,7 +190,7 @@ public: /// /// If checked, the skeleton pose will be shown during debug shapes drawing. /// - API_FIELD(Attributes="EditorOrder(200), EditorDisplay(\"Debug\"), VisibleIf(nameof(ShowDebugDrawOptions))") bool ShowDebugDrawSkeleton = false; + API_FIELD(Attributes="EditorOrder(200), EditorDisplay(\"Skeleton\"), VisibleIf(nameof(ShowDebugDrawOptions))") bool ShowDebugDrawSkeleton = false; #endif public: @@ -442,9 +442,9 @@ public: private: #if USE_EDITOR /// - /// Used to hide options if when the skinned model or animation graph is null. + /// Used to hide options if when the skinned model or animation graph is null. /// - API_PROPERTY(Attributes = "HideInEditor, NoSerialize") bool GetShowDebugDrawOptions() const + API_PROPERTY(Attributes="HideInEditor, NoSerialize") bool GetShowDebugDrawOptions() const { return SkinnedModel != nullptr && AnimationGraph != nullptr; } From 6fa38b75d593c8e8805427a7b55e5673c728a4cf Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 10:40:51 +0200 Subject: [PATCH 25/51] Add material instance creation out of other instance #4063 --- .../Editor/Content/Proxy/MaterialBaseProxy.cs | 109 ++++++++++++++++++ .../Content/Proxy/MaterialInstanceProxy.cs | 52 +-------- Source/Editor/Content/Proxy/MaterialProxy.cs | 96 +-------------- 3 files changed, 111 insertions(+), 146 deletions(-) create mode 100644 Source/Editor/Content/Proxy/MaterialBaseProxy.cs diff --git a/Source/Editor/Content/Proxy/MaterialBaseProxy.cs b/Source/Editor/Content/Proxy/MaterialBaseProxy.cs new file mode 100644 index 000000000..83c98ee6e --- /dev/null +++ b/Source/Editor/Content/Proxy/MaterialBaseProxy.cs @@ -0,0 +1,109 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEditor.Content.Thumbnails; +using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Viewport.Previews; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Content +{ + /// + /// A base class for asset proxy object. + /// + /// + public abstract class MaterialBaseProxy : BinaryAssetProxy + { + /// + /// The material preview drawer. + /// + protected MaterialPreview _preview; + + /// + public override bool CanCreate(ContentFolder targetLocation) + { + return targetLocation.CanHaveAssets; + } + + /// + public override void OnContentWindowContextMenu(ContextMenu menu, ContentItem item) + { + base.OnContentWindowContextMenu(menu, item); + + if (item is BinaryAssetItem binaryAssetItem) + { + var button = menu.AddButton("Create Material Instance", CreateMaterialInstanceClicked); + button.Tag = binaryAssetItem; + } + } + + private void CreateMaterialInstanceClicked(ContextMenuButton button) + { + var binaryAssetItem = (BinaryAssetItem)button.Tag; + CreateMaterialInstance(binaryAssetItem); + } + + /// + /// Creates the material instance from the given material. + /// + /// The material item to use as a base material. + public static void CreateMaterialInstance(BinaryAssetItem materialItem) + { + var materialInstanceName = materialItem.ShortName + " Instance"; + var materialInstanceProxy = Editor.Instance.ContentDatabase.GetProxy(); + Editor.Instance.Windows.ContentWin.NewItem(materialInstanceProxy, null, item => OnMaterialInstanceCreated(item, materialItem), materialInstanceName); + } + + private static void OnMaterialInstanceCreated(ContentItem item, BinaryAssetItem materialItem) + { + var assetItem = (AssetItem)item; + var materialInstance = FlaxEngine.Content.LoadAsync(assetItem.ID); + if (materialInstance == null || materialInstance.WaitForLoaded()) + { + Editor.LogError("Failed to load created material instance."); + return; + } + materialInstance.BaseMaterial = FlaxEngine.Content.LoadAsync(materialItem.ID); + materialInstance.Save(); + } + + /// + public override void OnThumbnailDrawPrepare(ThumbnailRequest request) + { + if (_preview == null) + { + _preview = new MaterialPreview(false); + InitAssetPreview(_preview); + } + } + + /// + public override void OnThumbnailDrawBegin(ThumbnailRequest request, ContainerControl guiRoot, GPUContext context) + { + _preview.Material = (MaterialBase)request.Asset; + _preview.Parent = guiRoot; + _preview.SyncBackbufferSize(); + + _preview.Task.OnDraw(); + } + + /// + public override void OnThumbnailDrawEnd(ThumbnailRequest request, ContainerControl guiRoot) + { + _preview.Material = null; + _preview.Parent = null; + } + + /// + public override void Dispose() + { + if (_preview != null) + { + _preview.Dispose(); + _preview = null; + } + + base.Dispose(); + } + } +} diff --git a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs index fc4fcdbc1..212417e9f 100644 --- a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs @@ -2,23 +2,18 @@ using System; using FlaxEditor.Content.Thumbnails; -using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows; using FlaxEditor.Windows.Assets; using FlaxEngine; -using FlaxEngine.GUI; namespace FlaxEditor.Content { /// /// A asset proxy object. /// - /// [ContentContextMenu("New/Material/Material Instance")] - public class MaterialInstanceProxy : BinaryAssetProxy + public class MaterialInstanceProxy : MaterialBaseProxy { - private MaterialPreview _preview; - /// public override string Name => "Material Instance"; @@ -34,12 +29,6 @@ namespace FlaxEditor.Content /// public override Type AssetType => typeof(MaterialInstance); - /// - public override bool CanCreate(ContentFolder targetLocation) - { - return targetLocation.CanHaveAssets; - } - /// public override void Create(string outputPath, object arg) { @@ -47,49 +36,10 @@ namespace FlaxEditor.Content throw new Exception("Failed to create new asset."); } - /// - public override void OnThumbnailDrawPrepare(ThumbnailRequest request) - { - if (_preview == null) - { - _preview = new MaterialPreview(false); - InitAssetPreview(_preview); - } - } - /// public override bool CanDrawThumbnail(ThumbnailRequest request) { return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((MaterialInstance)request.Asset); } - - /// - public override void OnThumbnailDrawBegin(ThumbnailRequest request, ContainerControl guiRoot, GPUContext context) - { - _preview.Material = (MaterialInstance)request.Asset; - _preview.Parent = guiRoot; - _preview.SyncBackbufferSize(); - - _preview.Task.OnDraw(); - } - - /// - public override void OnThumbnailDrawEnd(ThumbnailRequest request, ContainerControl guiRoot) - { - _preview.Material = null; - _preview.Parent = null; - } - - /// - public override void Dispose() - { - if (_preview != null) - { - _preview.Dispose(); - _preview = null; - } - - base.Dispose(); - } } } diff --git a/Source/Editor/Content/Proxy/MaterialProxy.cs b/Source/Editor/Content/Proxy/MaterialProxy.cs index 4769ca548..58d34299c 100644 --- a/Source/Editor/Content/Proxy/MaterialProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialProxy.cs @@ -2,24 +2,18 @@ using System; using FlaxEditor.Content.Thumbnails; -using FlaxEditor.GUI.ContextMenu; -using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows; using FlaxEditor.Windows.Assets; using FlaxEngine; -using FlaxEngine.GUI; namespace FlaxEditor.Content { /// /// A asset proxy object. /// - /// [ContentContextMenu("New/Material/Material")] - public class MaterialProxy : BinaryAssetProxy + public class MaterialProxy : MaterialBaseProxy { - private MaterialPreview _preview; - /// public override string Name => "Material"; @@ -35,12 +29,6 @@ namespace FlaxEditor.Content /// public override Type AssetType => typeof(Material); - /// - public override bool CanCreate(ContentFolder targetLocation) - { - return targetLocation.CanHaveAssets; - } - /// public override void Create(string outputPath, object arg) { @@ -48,92 +36,10 @@ namespace FlaxEditor.Content throw new Exception("Failed to create new asset."); } - /// - public override void OnContentWindowContextMenu(ContextMenu menu, ContentItem item) - { - base.OnContentWindowContextMenu(menu, item); - - if (item is BinaryAssetItem binaryAssetItem) - { - var button = menu.AddButton("Create Material Instance", CreateMaterialInstanceClicked); - button.Tag = binaryAssetItem; - } - } - - private void CreateMaterialInstanceClicked(ContextMenuButton obj) - { - var binaryAssetItem = (BinaryAssetItem)obj.Tag; - CreateMaterialInstance(binaryAssetItem); - } - - /// - /// Creates the material instance from the given material. - /// - /// The material item to use as a base material. - public static void CreateMaterialInstance(BinaryAssetItem materialItem) - { - var materialInstanceName = materialItem.ShortName + " Instance"; - var materialInstanceProxy = Editor.Instance.ContentDatabase.GetProxy(); - Editor.Instance.Windows.ContentWin.NewItem(materialInstanceProxy, null, item => OnMaterialInstanceCreated(item, materialItem), materialInstanceName); - } - - private static void OnMaterialInstanceCreated(ContentItem item, BinaryAssetItem materialItem) - { - var assetItem = (AssetItem)item; - var materialInstance = FlaxEngine.Content.LoadAsync(assetItem.ID); - if (materialInstance == null || materialInstance.WaitForLoaded()) - { - Editor.LogError("Failed to load created material instance."); - return; - } - - materialInstance.BaseMaterial = FlaxEngine.Content.LoadAsync(materialItem.ID); - materialInstance.Save(); - } - - /// - public override void OnThumbnailDrawPrepare(ThumbnailRequest request) - { - if (_preview == null) - { - _preview = new MaterialPreview(false); - InitAssetPreview(_preview); - } - } - /// public override bool CanDrawThumbnail(ThumbnailRequest request) { return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((Material)request.Asset); } - - /// - public override void OnThumbnailDrawBegin(ThumbnailRequest request, ContainerControl guiRoot, GPUContext context) - { - _preview.Material = (Material)request.Asset; - _preview.Parent = guiRoot; - _preview.SyncBackbufferSize(); - - _preview.Task.OnDraw(); - } - - /// - public override void OnThumbnailDrawEnd(ThumbnailRequest request, ContainerControl guiRoot) - { - _preview.Material = null; - _preview.Parent = null; - } - - /// - public override void Dispose() - { - if (_preview != null) - { - _preview.Dispose(); - _preview = null; - } - - base.Dispose(); - } } } From b7a59447a324eb0320fa61a1e27f073d48339bc3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 11:32:24 +0200 Subject: [PATCH 26/51] Fix Web build files location to properly exist in final package https://forum.flaxengine.com/t/web-build-issue-with-c-script/2590 --- .../Web/Binaries/{Data => }/check_browser_version.js | 0 Source/Platforms/Web/Binaries/{Data => }/check_jspi.js | 0 Source/Platforms/Web/Binaries/{Data => }/shell.html | 0 Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs | 6 +++--- 4 files changed, 3 insertions(+), 3 deletions(-) rename Source/Platforms/Web/Binaries/{Data => }/check_browser_version.js (100%) rename Source/Platforms/Web/Binaries/{Data => }/check_jspi.js (100%) rename Source/Platforms/Web/Binaries/{Data => }/shell.html (100%) diff --git a/Source/Platforms/Web/Binaries/Data/check_browser_version.js b/Source/Platforms/Web/Binaries/check_browser_version.js similarity index 100% rename from Source/Platforms/Web/Binaries/Data/check_browser_version.js rename to Source/Platforms/Web/Binaries/check_browser_version.js diff --git a/Source/Platforms/Web/Binaries/Data/check_jspi.js b/Source/Platforms/Web/Binaries/check_jspi.js similarity index 100% rename from Source/Platforms/Web/Binaries/Data/check_jspi.js rename to Source/Platforms/Web/Binaries/check_jspi.js diff --git a/Source/Platforms/Web/Binaries/Data/shell.html b/Source/Platforms/Web/Binaries/shell.html similarity index 100% rename from Source/Platforms/Web/Binaries/Data/shell.html rename to Source/Platforms/Web/Binaries/shell.html diff --git a/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs b/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs index a465ccac0..6551e3625 100644 --- a/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs @@ -367,7 +367,7 @@ namespace Flax.Build.Platforms if (args.All(arg => !arg.Contains("-sASSERTIONS"))) { // minimum_runtime_check.js from Emscripten checks min browser versions only with ASSERTIONS enabled so use custom check - var checkBrowserVersion = File.ReadAllText(Path.Combine(Globals.EngineRoot, "Source/Platforms/Web/Binaries/Data/check_browser_version.js")); + var checkBrowserVersion = File.ReadAllText(Path.Combine(Globals.EngineRoot, "Source/Platforms/Web/Binaries/check_browser_version.js")); checkBrowserVersion = checkBrowserVersion.Replace("TARGET_NOT_SUPPORTED", "0x7fffffff"); checkBrowserVersion = checkBrowserVersion.Replace("MIN_CHROME_VERSION", minChrome.ToString()); checkBrowserVersion = checkBrowserVersion.Replace("MIN_FIREFOX_VERSION", minFirefox.ToString()); @@ -377,11 +377,11 @@ namespace Flax.Build.Platforms args.Add($"--pre-js \"{path}\""); } if (addJSPI) - args.Add($"--pre-js \"{Globals.EngineRoot}/Source/Platforms/Web/Binaries/Data/check_jspi.js\""); + args.Add($"--pre-js \"{Globals.EngineRoot}/Source/Platforms/Web/Binaries/check_jspi.js\""); // Customize output HTML shell if (options.LinkEnv.Output == LinkerOutput.Executable) - args.Add($"--shell-file \"{Globals.EngineRoot}/Source/Platforms/Web/Binaries/Data/shell.html\""); + args.Add($"--shell-file \"{Globals.EngineRoot}/Source/Platforms/Web/Binaries/shell.html\""); } args.Add("-Wl,--start-group"); From c36c39df376c6ec2dce5e47f70124c70b0b4e1ec Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 11:38:34 +0200 Subject: [PATCH 27/51] Add auto focus on model when changing Base Model in Anim Graph --- Source/Editor/Viewport/Previews/AnimationPreview.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Viewport/Previews/AnimationPreview.cs b/Source/Editor/Viewport/Previews/AnimationPreview.cs index 8431821d6..b8942f2bf 100644 --- a/Source/Editor/Viewport/Previews/AnimationPreview.cs +++ b/Source/Editor/Viewport/Previews/AnimationPreview.cs @@ -1,6 +1,5 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; using FlaxEngine; using FlaxEditor.Viewport.Widgets; @@ -14,6 +13,7 @@ namespace FlaxEditor.Viewport.Previews /// public class AnimationPreview : AnimatedModelPreview { + private bool _baseModelMissing; private ViewportWidgetButton _playPauseButton; /// @@ -94,14 +94,23 @@ namespace FlaxEditor.Viewport.Previews var style = Style.Current; var skinnedModel = SkinnedModel; + var baseModelMissing = false; if (skinnedModel == null) { Render2D.DrawText(style.FontLarge, "Missing Base Model", new Rectangle(Float2.Zero, Size), Color.Red, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapWords); + baseModelMissing = true; } else if (!skinnedModel.IsLoaded) { Render2D.DrawText(style.FontLarge, skinnedModel.LastLoadFailed ? "Failed to load" : "Loading...", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center); + baseModelMissing = true; } + if (_baseModelMissing && !baseModelMissing) + { + // Focus model when base model appears + ResetCamera(); + } + _baseModelMissing = baseModelMissing; } /// From 6daec81db1e19dc08c6c9fce9d72e9ec1c8c6ad4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 13:30:40 +0200 Subject: [PATCH 28/51] Fix crash when using curves inside Anim Graph https://forum.flaxengine.com/t/bug-report-v1-12-using-curves-in-the-animation-graph-causes-a-crash/2594 --- .../Animations/Graph/AnimGraph.Base.cpp | 4 +- Source/Engine/Content/Assets/VisualScript.cpp | 10 ++-- Source/Engine/Content/Assets/VisualScript.h | 40 +++++++++++++- Source/Engine/Visject/ShaderGraph.h | 9 ++++ Source/Engine/Visject/VisjectGraph.cpp | 2 +- Source/Engine/Visject/VisjectGraph.h | 53 ++++--------------- 6 files changed, 66 insertions(+), 52 deletions(-) diff --git a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp index 1cc9c66a4..8f1d494ec 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp @@ -61,14 +61,14 @@ void AnimGraphBase::Clear() StateTransitions.Resize(0); // Base - GraphType::Clear(); + VisjectGraph::Clear(); } #if USE_EDITOR void AnimGraphBase::GetReferences(Array& output) const { - GraphType::GetReferences(output); + VisjectGraph::GetReferences(output); // Collect references from nested graph (assets used in state machines) for (const auto* subGraph : SubGraphs) diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp index a7e132bdc..62d901b6f 100644 --- a/Source/Engine/Content/Assets/VisualScript.cpp +++ b/Source/Engine/Content/Assets/VisualScript.cpp @@ -163,7 +163,7 @@ VisjectExecutor::Value VisualScriptExecutor::eatBox(Node* caller, Box* box) // Add to the calling stack VisualScripting::StackFrame frame = *stack.Stack; - frame.Node = parentNode; + frame.Node = (VisualScriptGraphNode*)parentNode; frame.Box = box; frame.PreviousFrame = stack.Stack; stack.Stack = &frame; @@ -189,7 +189,7 @@ VisjectExecutor::Value VisualScriptExecutor::eatBox(Node* caller, Box* box) VisjectExecutor::Graph* VisualScriptExecutor::GetCurrentGraph() const { auto& stack = ThreadStacks.Get(); - return stack.Stack && stack.Stack->Script ? &stack.Stack->Script->Graph : nullptr; + return stack.Stack && stack.Stack->Script ? (Graph*)&stack.Stack->Script->Graph : nullptr; } void VisualScriptExecutor::ProcessGroupParameters(Box* box, Node* node, Value& value) @@ -432,7 +432,7 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& // Call Impulse or Pure Method if (boxBase->ID == 0 || (bool)node->Values[3]) { - auto& cache = node->Data.InvokeMethod; + auto& cache = ((VisualScriptGraphNode*)node)->Data.InvokeMethod; if (!cache.Method) { // Load method signature @@ -667,7 +667,7 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& // Get Field case 7: { - auto& cache = node->Data.GetSetField; + auto& cache = ((VisualScriptGraphNode*)node)->Data.GetSetField; if (!cache.Field) { const auto typeName = (StringView)node->Values[0]; @@ -753,7 +753,7 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& // Get Field case 8: { - auto& cache = node->Data.GetSetField; + auto& cache = ((VisualScriptGraphNode*)node)->Data.GetSetField; if (!cache.Field) { const auto typeName = (StringView)node->Values[0]; diff --git a/Source/Engine/Content/Assets/VisualScript.h b/Source/Engine/Content/Assets/VisualScript.h index 9e7af97cd..91b9506fa 100644 --- a/Source/Engine/Content/Assets/VisualScript.h +++ b/Source/Engine/Content/Assets/VisualScript.h @@ -12,11 +12,47 @@ #define VISUAL_SCRIPT_GRAPH_MAX_CALL_STACK 250 #define VISUAL_SCRIPT_DEBUGGING USE_EDITOR -#define VisualScriptGraphNode VisjectGraphNode<> - class VisualScripting; class VisualScriptingBinaryModule; +/// +/// Visual Script graph node. +/// +class VisualScriptGraphNode : public VisjectGraphNode<> +{ +public: + struct InvokeMethodData + { + void* Method; + BinaryModule* Module; + int32 ParamsCount; + uint32 OutParamsMask; + bool IsStatic; + }; + + struct GetSetFieldData + { + void* Field; + BinaryModule* Module; + bool IsStatic; + }; + + /// + /// Custom cached data per node type. Compact to use as small amount of memory as possible. + /// + struct AdditionalData + { + union + { + InvokeMethodData InvokeMethod; + GetSetFieldData GetSetField; + }; + }; + + // The custom per-node data. Used to cache data for faster usage at runtime. + AdditionalData Data; +}; + /// /// The Visual Script graph data. /// diff --git a/Source/Engine/Visject/ShaderGraph.h b/Source/Engine/Visject/ShaderGraph.h index 5b17604d0..d22260a87 100644 --- a/Source/Engine/Visject/ShaderGraph.h +++ b/Source/Engine/Visject/ShaderGraph.h @@ -167,6 +167,15 @@ public: // Base return Base::onNodeLoaded(n); } + void Clear() override + { + FloatCurves.Clear(); + Float2Curves.Clear(); + Float3Curves.Clear(); + Float4Curves.Clear(); + + Base::Clear(); + } }; /// diff --git a/Source/Engine/Visject/VisjectGraph.cpp b/Source/Engine/Visject/VisjectGraph.cpp index ec4f51364..8b8c27010 100644 --- a/Source/Engine/Visject/VisjectGraph.cpp +++ b/Source/Engine/Visject/VisjectGraph.cpp @@ -952,7 +952,7 @@ void VisjectExecutor::ProcessGroupTools(Box* box, Node* node, Value& value) #define SAMPLE_CURVE(id, curves, type, graphType) \ case id: \ { \ - const auto& curve = GetCurrentGraph()->curves[node->Data.Curve.CurveIndex]; \ + const auto& curve = GetCurrentGraph()->curves[node->CurveIndex]; \ const float time = (float)tryGetValue(node->GetBox(0), Value::Zero); \ value.Type = VariantType(VariantType::graphType); \ curve.Evaluate(*(type*)value.AsData, time, false); \ diff --git a/Source/Engine/Visject/VisjectGraph.h b/Source/Engine/Visject/VisjectGraph.h index 1f0de1ce0..0eceaf8cb 100644 --- a/Source/Engine/Visject/VisjectGraph.h +++ b/Source/Engine/Visject/VisjectGraph.h @@ -42,42 +42,6 @@ public: template class VisjectGraphNode : public GraphNode { -public: - struct CurveData - { - /// - /// The curve index. - /// - int32 CurveIndex; - }; - - /// - /// Custom cached data per node type. Compact to use as small amount of memory as possible. - /// - struct AdditionalData - { - union - { - CurveData Curve; - - struct - { - void* Method; - BinaryModule* Module; - int32 ParamsCount; - uint32 OutParamsMask; - bool IsStatic; - } InvokeMethod; - - struct - { - void* Field; - BinaryModule* Module; - bool IsStatic; - } GetSetField; - }; - }; - public: VisjectGraphNode() : GraphNode() @@ -85,10 +49,7 @@ public: } public: - /// - /// The custom data (depends on node type). Used to cache data for faster usage at runtime. - /// - AdditionalData Data; + int32 CurveIndex = MAX_uint16; /// /// The asset references. Linked resources such as Animation assets are referenced in graph data as ID. We need to keep valid refs to them at runtime to keep data in memory. @@ -148,7 +109,7 @@ public: #define SETUP_CURVE(id, curves, access) \ case id: \ { \ - n->Data.Curve.CurveIndex = curves.Count(); \ + n->CurveIndex = curves.Count(); \ auto& curve = curves.AddOne(); \ const int32 keyframesCount = n->Values[0].AsInt; \ auto& keyframes = curve.GetKeyframes(); \ @@ -177,9 +138,17 @@ public: } } - // Base return Base::onNodeLoaded(n); } + void Clear() override + { + FloatCurves.Clear(); + Float2Curves.Clear(); + Float3Curves.Clear(); + Float4Curves.Clear(); + + Base::Clear(); + } }; /// From 018c7cf33df262c3ac0c7b4ff699457562a8a6b2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 1 Jun 2026 19:19:50 +0200 Subject: [PATCH 29/51] Update editor icons atlas with new folder color and add original `.psd` file --- Content/Editor/IconsAtlas.flax | 4 ++-- Content/Editor/IconsAtlas.psd | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 Content/Editor/IconsAtlas.psd diff --git a/Content/Editor/IconsAtlas.flax b/Content/Editor/IconsAtlas.flax index cd1c745a2..01baadfed 100644 --- a/Content/Editor/IconsAtlas.flax +++ b/Content/Editor/IconsAtlas.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2248c3069f16a3b1eb62aa4660c81427fd6effa364f8f0694ba751be8e60114c -size 5612622 +oid sha256:c2a7d0c6969a180d59a32fbc908fe432bddb393437e9c5b64ddb25737e4aab94 +size 5609840 diff --git a/Content/Editor/IconsAtlas.psd b/Content/Editor/IconsAtlas.psd new file mode 100644 index 000000000..b062fc0fa --- /dev/null +++ b/Content/Editor/IconsAtlas.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ee4e46c2b39cf6def6be079a898204e283253bf5841ccf3985fa9d49834b9a0 +size 2766306 From 1badeda31cb0ab2158373a11df425e3635a15a26 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 1 Jun 2026 19:20:00 +0200 Subject: [PATCH 30/51] Fix error when drawing animated model thumbnail --- .../Viewport/Previews/AnimatedModelPreview.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs index 58df5f836..a5ff3758b 100644 --- a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs +++ b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs @@ -216,16 +216,17 @@ namespace FlaxEditor.Viewport.Previews _showFloorButton = ViewWidgetShowMenu.AddButton("Floor", button => ShowFloor = !ShowFloor); _showFloorButton.IndexInParent = 1; _showFloorButton.CloseMenuOnClick = false; - } - _nodeNameSizeButton = ViewWidgetButtonMenu.AddButton("Skeleton Names Size"); - _nodeNameSizeButton.CloseMenuOnClick = false; - var nodeNameSizeValue = new IntValueBox(NodeNamesSize, 118, 2, 70.0f, 1, 32) - { - Parent = _nodeNameSizeButton - }; - _nodeNameSizeButton.Enabled = ShowNodesNames; - nodeNameSizeValue.ValueChanged += () => NodeNamesSize = nodeNameSizeValue.Value; + // Skeleton Names Size + _nodeNameSizeButton = ViewWidgetButtonMenu.AddButton("Skeleton Names Size"); + _nodeNameSizeButton.CloseMenuOnClick = false; + var nodeNameSizeValue = new IntValueBox(NodeNamesSize, 118, 2, 70.0f, 1, 32) + { + Parent = _nodeNameSizeButton + }; + _nodeNameSizeButton.Enabled = ShowNodesNames; + nodeNameSizeValue.ValueChanged += () => NodeNamesSize = nodeNameSizeValue.Value; + } // Enable shadows PreviewLight.ShadowsMode = ShadowsCastingMode.All; From ff526ecafb496b7185d9774b246fd146f33f0160 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Jun 2026 13:23:21 +0200 Subject: [PATCH 31/51] Fix nested prefab stack overflow when adding new object to nested prefabs hierarchy https://github.com/LOOPDISK/FlaxEngine/pull/44 --- Source/Engine/Level/Actors/AnimatedModel.cpp | 3 +- Source/Engine/Level/Prefabs/Prefab.Apply.cpp | 60 +++++---- Source/Engine/Level/Prefabs/Prefab.h | 2 +- Source/Engine/Level/SceneObjectsFactory.cpp | 2 +- Source/Engine/Tests/TestPrefabs.cpp | 121 +++++++++++++++++++ 5 files changed, 159 insertions(+), 29 deletions(-) diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index ce106dc83..bc3c5b8fb 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -820,8 +820,7 @@ void AnimatedModel::RunBlendShapeDeformer(const MeshBase* mesh, MeshDeformationD void AnimatedModel::BeginPlay(SceneBeginData* data) { - if (SkinnedModel && SkinnedModel->IsLoaded()) - PreInitSkinningData(); + PreInitSkinningData(); // Base ModelInstanceActor::BeginPlay(data); diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index cb6c82b5d..d972ee520 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -24,6 +24,7 @@ #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Threading/MainThreadTask.h" #include "Editor/Editor.h" +#include "FlaxEngine.Gen.h" // Apply flow: // - collect all prefabs using this prefab (load and create default instances) @@ -772,7 +773,13 @@ bool Prefab::ApplyAll(Actor* targetActor) if (ApplyAllInternal(targetActor, true, thisPrefabInstancesData)) return true; - SyncNestedPrefabs(allPrefabs, allPrefabsInstancesData); + // Sync nested prefabs + if (allPrefabs.HasItems()) + { + LOG(Info, "Updating referencing prefabs"); + HashSet synced; + SyncNestedPrefabs(allPrefabs, allPrefabsInstancesData, synced); + } const auto endTime = DateTime::NowUTC(); LOG(Info, "Prefab updated! {0} ms", (int32)(endTime - startTime).GetTotalMilliseconds()); @@ -1027,8 +1034,14 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr rapidjson_flax::Document targetDataDocument; if (NestedPrefabs.HasItems()) { + // Use initial data buffer (unstripped) but reorder objects to match the sequence (eg. when new object was added to the nested prefab) targetDataDocument.Parse(dataBuffer.GetString(), dataBuffer.GetSize()); - SceneObjectsFactory::PrefabSyncData prefabSyncData(*sceneObjects.Value, targetDataDocument, modifier.Value); + Array reorderedObjects = *sceneObjects.Value; + newPrefabInstanceIdToDataIndexCounter = 0; + for (auto i = newPrefabInstanceIdToDataIndex.Begin(); i.IsNotEnd(); ++i) + reorderedObjects.Insert(i->Value, sceneObjects->At(newPrefabInstanceIdToDataIndexStart + newPrefabInstanceIdToDataIndexCounter++)); + reorderedObjects.Resize(sceneObjects.Value->Count()); // reorderedObjects matches order in targetDataDocument + SceneObjectsFactory::PrefabSyncData prefabSyncData(reorderedObjects, targetDataDocument, modifier.Value); SceneObjectsFactory::SetupPrefabInstances(context, prefabSyncData); if (context.Instances.HasItems()) @@ -1236,7 +1249,7 @@ bool Prefab::UpdateInternal(const Array& defaultInstanceObjects, r { return Init(TypeName, StringAnsiView(tmpBuffer.GetString(), (int32)tmpBuffer.GetSize())); } -#if 1 // Set to 0 to use memory-only reload that does not modifies the source file - useful for testing and debugging prefabs apply +#if 1 // Set to 0 to use memory-only reload that does not modify the source file - useful for testing and debugging prefabs apply #if COMPILE_WITH_ASSETS_IMPORTER Locker.Unlock(); @@ -1295,7 +1308,7 @@ bool Prefab::UpdateInternal(const Array& defaultInstanceObjects, r _defaultInstance->DeleteObject(); _defaultInstance = nullptr; } - _isLoaded = false; + _loadState = 0; // Update prefab data manually (to prevent updating source asset file - just for testing) Document.Parse(buffer.GetString(), buffer.GetSize()); @@ -1348,7 +1361,7 @@ bool Prefab::UpdateInternal(const Array& defaultInstanceObjects, r NestedPrefabs.Add(prefabId); } } - _isLoaded = true; + _loadState = 1; } #endif @@ -1395,34 +1408,31 @@ bool Prefab::SyncChangesInternal(PrefabInstancesData& prefabInstancesData) return ApplyAllInternal(targetActor, false, prefabInstancesData); } -void Prefab::SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array& allPrefabsInstancesData) const +void Prefab::SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array& allPrefabsInstancesData, HashSet& synced) const { PROFILE_CPU(); - LOG(Info, "Updating referencing prefabs"); - - // TODO: this may not work well for very complex prefab nesting -> loop order matters, maybe build a graph of dependencies? // Call recursive for all referencing prefab assets to refresh nested prefabs for (int32 i = 0; i < allPrefabs.Count(); i++) { - auto nestedPrefab = allPrefabs[i].Get(); - if (nestedPrefab) + Prefab* nestedPrefab = allPrefabs[i].Get(); + if (!nestedPrefab || synced.Contains(nestedPrefab->GetID())) + continue; + if (nestedPrefab->WaitForLoaded()) { - if (nestedPrefab->WaitForLoaded()) - { - LOG(Warning, "Waiting for prefab asset load failed."); - continue; - } + LOG(Warning, "Waiting for '{}' load failed.", nestedPrefab->ToString()); + continue; + } - // Sync only if prefab is used by this prefab (directly) and it has been captured before - const int32 nestedPrefabIndex = nestedPrefab->NestedPrefabs.Find(GetID()); - if (nestedPrefabIndex != -1) - { - if (nestedPrefab->SyncChangesInternal(allPrefabsInstancesData[i])) - continue; - nestedPrefab->SyncNestedPrefabs(allPrefabs, allPrefabsInstancesData); - ObjectsRemovalService::Flush(); - } + // Sync only if prefab is used by this prefab (directly) and it has been captured before + const int32 nestedPrefabIndex = nestedPrefab->NestedPrefabs.Find(GetID()); + if (nestedPrefabIndex != -1) + { + synced.Add(nestedPrefab->GetID()); + if (nestedPrefab->SyncChangesInternal(allPrefabsInstancesData[i])) + continue; + nestedPrefab->SyncNestedPrefabs(allPrefabs, allPrefabsInstancesData, synced); + ObjectsRemovalService::Flush(); } } } diff --git a/Source/Engine/Level/Prefabs/Prefab.h b/Source/Engine/Level/Prefabs/Prefab.h index cde99a2cb..b1200ca29 100644 --- a/Source/Engine/Level/Prefabs/Prefab.h +++ b/Source/Engine/Level/Prefabs/Prefab.h @@ -104,7 +104,7 @@ private: bool ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPrefab, PrefabInstancesData& prefabInstancesData); bool UpdateInternal(const Array& defaultInstanceObjects, rapidjson_flax::StringBuffer& tmpBuffer); bool SyncChangesInternal(PrefabInstancesData& prefabInstancesData); - void SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array& allPrefabsInstancesData) const; + void SyncNestedPrefabs(const NestedPrefabsList& allPrefabs, Array& allPrefabsInstancesData, HashSet& synced) const; #endif void DeleteDefaultInstance(); diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index 59d23a994..e9931eb20 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -752,7 +752,7 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn obj->SetOrderInParent(order); } - // Setup hierarchy for the prefab instances (ensure any new objects are connected) + // Setup hierarchy for the prefab instances (after adding new objects to ensure they are connected, eg. when reparenting existing prefab into a new root) for (const auto& instance : context.Instances) { const auto& prefabStartData = data.Data[instance.StatIndex]; diff --git a/Source/Engine/Tests/TestPrefabs.cpp b/Source/Engine/Tests/TestPrefabs.cpp index 91bce81cc..fb34a7284 100644 --- a/Source/Engine/Tests/TestPrefabs.cpp +++ b/Source/Engine/Tests/TestPrefabs.cpp @@ -8,6 +8,7 @@ #include "Engine/Level/Actors/EmptyActor.h" #include "Engine/Level/Actors/DirectionalLight.h" #include "Engine/Level/Actors/ExponentialHeightFog.h" +#include "Engine/Level/Actors/AnimatedModel.h" #include "Engine/Level/Prefabs/Prefab.h" #include "Engine/Level/Prefabs/PrefabManager.h" #include "Engine/Scripting/ScriptingObjectReference.h" @@ -905,4 +906,124 @@ TEST_CASE("Prefabs") instance1->DeleteObject(); instance2->DeleteObject(); } + SECTION("Test Adding Object To Base Prefab") + { + // https://github.com/LOOPDISK/FlaxEngine/pull/44 + + // Create inner prefab with 3 objects in hierarchy + AssetReference prefabInner = Content::CreateVirtualAsset(); + REQUIRE(prefabInner); + Guid id; + Guid::Parse("15dbe4b0416be0777a6ce59e8788b10f", id); + prefabInner->ChangeID(id); + auto prefabInnerInit = prefabInner->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"3de462104f56f681c14650a0171f88fb\"," + "\"TypeName\" : \"FlaxEngine.SpotLight\"," + "\"Name\" : \"Inner.Root\"" + "}," + "{" + "\"ID\": \"19b181f846b6911635ffacb902c93c6a\"," + "\"TypeName\" : \"FlaxEngine.StaticModel\"," + "\"ParentID\" : \"3de462104f56f681c14650a0171f88fb\"," + "\"Name\" : \"Inner.Cube\"" + "}," + "{" + "\"ID\": \"8950889f4a2e752d55165fbf10eaf184\"," + "\"TypeName\" : \"FlaxEngine.AnimatedModel\"," + "\"ParentID\" : \"19b181f846b6911635ffacb902c93c6a\"," + "\"Name\" : \"Inner.Model\"" + "}" + "]"); + REQUIRE(!prefabInnerInit); + + // Create outer prefab with 2 instances of inner prefab + AssetReference prefabOuter = Content::CreateVirtualAsset(); + REQUIRE(prefabOuter); + SCOPE_EXIT{ Content::DeleteAsset(prefabOuter); }; + Guid::Parse("2ab744714f746e31855f41815612d14b", id); + prefabOuter->ChangeID(id); + auto prefabOuterInit = prefabOuter->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"dba7f4bb4acfd62608b9a8bf550f31a5\"," + "\"TypeName\": \"FlaxEngine.EmptyActor\"," + "\"Name\": \"Outer.Root\"" + "}," + "{" + "\"ID\": \"a3b705284432bed9f043829c04a2bc8f\"," + "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"3de462104f56f681c14650a0171f88fb\"," + "\"ParentID\": \"dba7f4bb4acfd62608b9a8bf550f31a5\"," + "\"Name\": \"Instance 1\"" + "}," + "{" + "\"ID\": \"06a8c15a41b822dd27f3ac9d79b142d3\"," + "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"19b181f846b6911635ffacb902c93c6a\"," + "\"ParentID\": \"a3b705284432bed9f043829c04a2bc8f\"" + "}," + "{" + "\"ID\": \"4759fb9e4c4dda3b61ab5ab43949e42f\"," + "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"8950889f4a2e752d55165fbf10eaf184\"," + "\"ParentID\": \"06a8c15a41b822dd27f3ac9d79b142d3\"" + "}," + "{" + "\"ID\": \"1225be664c0c081e714bbf93e09b99e4\"," + "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"3de462104f56f681c14650a0171f88fb\"," + "\"ParentID\": \"dba7f4bb4acfd62608b9a8bf550f31a5\"," + "\"Name\": \"Instance 2\"" + "}," + "{" + "\"ID\": \"b397243540322182b806ad8339b7b617\"," + "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"19b181f846b6911635ffacb902c93c6a\"," + "\"ParentID\": \"1225be664c0c081e714bbf93e09b99e4\"" + "}," + "{" + "\"ID\": \"2c3b8e824daf038a58df528a238ca2de\"," + "\"PrefabID\": \"15dbe4b0416be0777a6ce59e8788b10f\"," + "\"PrefabObjectID\": \"8950889f4a2e752d55165fbf10eaf184\"," + "\"ParentID\": \"b397243540322182b806ad8339b7b617\"" + "}" + "]"); + REQUIRE(!prefabOuterInit); + + // Spawn test instances of both prefabs + ScriptingObjectReference instanceInner = PrefabManager::SpawnPrefab(prefabInner); + ScriptingObjectReference instanceOuter = PrefabManager::SpawnPrefab(prefabOuter); + + // Add new object to the inner prefab + instanceInner->Children[0]->GetOrAddChild(); + + // Apply changes + bool applyResult = PrefabManager::ApplyAll(instanceInner); + REQUIRE(!applyResult); + + // Check state of outer instance to properly reflect hierarchy + REQUIRE(instanceOuter); + REQUIRE(instanceOuter->Children.Count() == 2); + REQUIRE(instanceOuter->Children[0] != nullptr); + REQUIRE(instanceOuter->Children[0]->Children.Count() == 1); + REQUIRE(instanceOuter->Children[0]->Children[0]); + REQUIRE(instanceOuter->Children[0]->Children[0]->Children.Count() == 2); + REQUIRE(instanceOuter->Children[0]->Children[0]->Children[0]->Is()); + REQUIRE(instanceOuter->Children[0]->Children[0]->Children[1]->Is()); + REQUIRE(instanceOuter->Children[1] != nullptr); + REQUIRE(instanceOuter->Children[1]->Children.Count() == 1); + REQUIRE(instanceOuter->Children[1]->Children[0]); + REQUIRE(instanceOuter->Children[1]->Children[0]->Children.Count() == 2); + REQUIRE(instanceOuter->Children[1]->Children[0]->Children[0]->Is()); + REQUIRE(instanceOuter->Children[0]->Children[0]->Children[1]->Is()); + REQUIRE(instanceOuter->Children[0]->Children[0] != instanceOuter->Children[1]->Children[0]); + REQUIRE(instanceOuter->Children[0]->Children[0]->Children[0] != instanceOuter->Children[1]->Children[0]->Children[0]); + REQUIRE(instanceOuter->Children[0]->Children[0]->Children[1] != instanceOuter->Children[1]->Children[0]->Children[1]); + + // Cleanup + instanceInner->DeleteObject(); + instanceOuter->DeleteObject(); + } } From a12c5e2203ca7eeccc45ae2fd65c87090918de5f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Jun 2026 13:24:10 +0200 Subject: [PATCH 32/51] Add more profiler events to assets code --- Source/Engine/Content/Asset.cpp | 1 + Source/Engine/Level/Prefabs/Prefab.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index da6b7329a..5560c20b9 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -272,6 +272,7 @@ String Asset::ToString() const void Asset::OnDeleteObject() { + PROFILE_CPU_NAMED("Asset.Unload"); ASSERT(IsInMainThread()); // Send event to the gameplay so it can release handle to this asset diff --git a/Source/Engine/Level/Prefabs/Prefab.cpp b/Source/Engine/Level/Prefabs/Prefab.cpp index d309bf7bb..80120ea87 100644 --- a/Source/Engine/Level/Prefabs/Prefab.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.cpp @@ -7,6 +7,7 @@ #include "Engine/Core/Log.h" #include "Engine/Level/Prefabs/PrefabManager.h" #include "Engine/Level/Actor.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Threading/Threading.h" #include "Engine/Scripting/Scripting.h" @@ -22,6 +23,7 @@ Prefab::Prefab(const SpawnParams& params, const AssetInfo* info) Guid Prefab::GetRootObjectId() const { + PROFILE_CPU(); ASSERT(IsLoaded()); ScopeLock lock(Locker); @@ -57,6 +59,7 @@ Actor* Prefab::GetDefaultInstance() // Skip if already created (reuse cached result) if (_defaultInstance) return _defaultInstance; + PROFILE_CPU(); // Skip if not loaded if (!IsLoaded()) From 9ce602619269f8fe9bcacd746ce191fcd83bb8c9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Jun 2026 14:07:28 +0200 Subject: [PATCH 33/51] Optimize model actors entries serialization --- .../Graphics/Models/ModelInstanceEntry.cpp | 18 +++++++++++++----- .../Graphics/Models/ModelInstanceEntry.h | 1 + Source/Engine/Level/Actors/AnimatedModel.cpp | 7 ++----- Source/Engine/Level/Actors/SplineModel.cpp | 7 ++----- Source/Engine/Level/Actors/StaticModel.cpp | 6 ++---- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp b/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp index 1c9440b39..6f8ace8d7 100644 --- a/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp +++ b/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp @@ -21,6 +21,13 @@ bool ModelInstanceEntries::HasContentLoaded() const return result; } +bool ModelInstanceEntries::ShouldSerialize(const void* otherObj) const +{ + if (!otherObj) + return true; + return !(*this == *(const ModelInstanceEntries*)otherObj); +} + void ModelInstanceEntries::Serialize(SerializeStream& stream, const void* otherObj) { SERIALIZE_GET_OTHER_OBJ(ModelInstanceEntries); @@ -43,12 +50,13 @@ void ModelInstanceEntries::Serialize(SerializeStream& stream, const void* otherO void ModelInstanceEntries::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) { PROFILE_MEM(Graphics); - const DeserializeStream& entries = stream["Entries"]; - ASSERT(entries.IsArray()); - Resize(entries.Size()); - for (rapidjson::SizeType i = 0; i < entries.Size(); i++) + const DeserializeStream& entriesData = stream[DeserializeStream::GenericValue(rapidjson::StringRef("Entries", 7))]; + CHECK(entriesData.IsArray()); + Resize(entriesData.Size()); + ModelInstanceEntry* entries = Get(); + for (int32 i = 0; i < Count(); i++) { - At(i).Deserialize((DeserializeStream&)entries[i], modifier); + entries[i].Deserialize((DeserializeStream&)entriesData[i], modifier); } } diff --git a/Source/Engine/Graphics/Models/ModelInstanceEntry.h b/Source/Engine/Graphics/Models/ModelInstanceEntry.h index 8f8a2ca3c..79403cce1 100644 --- a/Source/Engine/Graphics/Models/ModelInstanceEntry.h +++ b/Source/Engine/Graphics/Models/ModelInstanceEntry.h @@ -115,6 +115,7 @@ public: public: // [ISerializable] + bool ShouldSerialize(const void* otherObj) const override; void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; }; diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index bc3c5b8fb..ba3a2025d 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -1262,9 +1262,7 @@ void AnimatedModel::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE(ShadowsMode); PRAGMA_ENABLE_DEPRECATION_WARNINGS SERIALIZE(RootMotionTarget); - - stream.JKEY("Buffer"); - stream.Object(&Entries, other ? &other->Entries : nullptr); + SERIALIZE_MEMBER(Buffer, Entries); } void AnimatedModel::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) @@ -1289,8 +1287,7 @@ void AnimatedModel::Deserialize(DeserializeStream& stream, ISerializeModifier* m DESERIALIZE(ShadowsMode); PRAGMA_ENABLE_DEPRECATION_WARNINGS DESERIALIZE(RootMotionTarget); - - Entries.DeserializeIfExists(stream, "Buffer", modifier); + DESERIALIZE_MEMBER(Buffer, Entries); // [Deprecated on 07.02.2022, expires on 07.02.2024] if (modifier->EngineBuild <= 6330) diff --git a/Source/Engine/Level/Actors/SplineModel.cpp b/Source/Engine/Level/Actors/SplineModel.cpp index 0da4a3d70..fdda6941e 100644 --- a/Source/Engine/Level/Actors/SplineModel.cpp +++ b/Source/Engine/Level/Actors/SplineModel.cpp @@ -497,9 +497,7 @@ void SplineModel::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE_MEMBER(PreTransform, _preTransform) SERIALIZE(Model); SERIALIZE(DrawModes); - - stream.JKEY("Buffer"); - stream.Object(&Entries, other ? &other->Entries : nullptr); + SERIALIZE_MEMBER(Buffer, Entries); } void SplineModel::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) @@ -514,8 +512,7 @@ void SplineModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod DESERIALIZE_MEMBER(PreTransform, _preTransform); DESERIALIZE(Model); DESERIALIZE(DrawModes); - - Entries.DeserializeIfExists(stream, "Buffer", modifier); + DESERIALIZE_MEMBER(Buffer, Entries); // [Deprecated on 07.02.2022, expires on 07.02.2024] if (modifier->EngineBuild <= 6330) diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp index 1fba1ba2e..82976b54c 100644 --- a/Source/Engine/Level/Actors/StaticModel.cpp +++ b/Source/Engine/Level/Actors/StaticModel.cpp @@ -464,8 +464,7 @@ void StaticModel::Serialize(SerializeStream& stream, const void* otherObj) stream.Rectangle(Lightmap.UVsArea); } - stream.JKEY("Buffer"); - stream.Object(&Entries, other ? &other->Entries : nullptr); + SERIALIZE_MEMBER(Buffer, Entries); if (_vertexColorsCount) { @@ -504,8 +503,7 @@ void StaticModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod DESERIALIZE_MEMBER(DrawModes, _drawModes); DESERIALIZE_MEMBER(LightmapIndex, Lightmap.TextureIndex); DESERIALIZE_MEMBER(LightmapArea, Lightmap.UVsArea); - - Entries.DeserializeIfExists(stream, "Buffer", modifier); + DESERIALIZE_MEMBER(Buffer, Entries); { const auto member = stream.FindMember("VertexColors"); From 2531a4b9182569a35decd0ce9c4ebd72c0b52361 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Jun 2026 15:51:46 +0200 Subject: [PATCH 34/51] Fix asset 'Reload' option to be available when asset failed to load for manual load --- Source/Editor/Content/Items/AssetItem.cs | 4 ++-- Source/Editor/Windows/ContentWindow.ContextMenu.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Content/Items/AssetItem.cs b/Source/Editor/Content/Items/AssetItem.cs index eaa93561a..78519b400 100644 --- a/Source/Editor/Content/Items/AssetItem.cs +++ b/Source/Editor/Content/Items/AssetItem.cs @@ -98,12 +98,12 @@ namespace FlaxEditor.Content } /// - /// Reloads the asset (if it's loaded). + /// Reloads the asset (if it's loaded or failed to load). /// public void Reload() { var asset = FlaxEngine.Content.GetAsset(ID); - if (asset != null && asset.IsLoaded) + if (asset != null && (asset.IsLoaded || asset.LastLoadFailed)) { asset.Reload(); } diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index f4985eb7a..22ef54bad 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -132,7 +132,8 @@ namespace FlaxEditor.Windows if (item is AssetItem assetItem) { - if (assetItem.IsLoaded) + var asset = FlaxEngine.Content.GetAsset(assetItem.ID); + if (asset != null && (asset.IsLoaded || asset.LastLoadFailed)) cm.AddButton("Reload", assetItem.Reload); cm.AddButton("Copy asset ID", () => Clipboard.Text = JsonSerializer.GetStringID(assetItem.ID)); cm.AddButton("Select actors using this asset", () => Editor.SceneEditing.SelectActorsUsingAsset(assetItem.ID)); From 44117084c887f474f9dd82de8fa2af6f838d3ffa Mon Sep 17 00:00:00 2001 From: Murry Lancashire Date: Tue, 2 Jun 2026 15:53:35 +0200 Subject: [PATCH 35/51] Fix asset cache eviction for locked files to retain cached info https://github.com/LOOPDISK/FlaxEngine/pull/45 --- Source/Engine/Content/Cache/AssetsCache.cpp | 126 ++++++++++++-------- Source/Engine/Content/Cache/AssetsCache.h | 30 ++++- 2 files changed, 105 insertions(+), 51 deletions(-) diff --git a/Source/Engine/Content/Cache/AssetsCache.cpp b/Source/Engine/Content/Cache/AssetsCache.cpp index 96083509e..e36a0d76c 100644 --- a/Source/Engine/Content/Cache/AssetsCache.cpp +++ b/Source/Engine/Content/Cache/AssetsCache.cpp @@ -113,7 +113,7 @@ void AssetsCache::Init() } // Use only valid entries - if (IsEntryValid(e)) + if (IsEntryValid(e) != EntryValidation::Invalid) _registry.Add(e.Info.ID, e); else rejectedCount++; @@ -295,14 +295,23 @@ bool AssetsCache::FindAsset(const StringView& path, AssetInfo& info) auto& e = i->Value; if (e.Info.Path == path) { - if (!IsEntryValid(e)) + const auto validation = IsEntryValid(e); + if (validation == EntryValidation::Invalid) { LOG(Warning, "Missing file from registry: \'{0}\':{1}:{2}", e.Info.Path, e.Info.ID, e.Info.TypeName); _registry.Remove(i); } else { - // Found +#if ENABLE_ASSETS_DISCOVERY + if (validation == EntryValidation::Inaccessible && !e.WarnedInaccessible) + { + e.WarnedInaccessible = true; + LOG(Warning, "Asset file locked, keeping cached entry: \'{0}\':{1}:{2}", e.Info.Path, e.Info.ID, e.Info.TypeName); + } +#endif + + // Found valid or inaccessible but return cached info either way result = true; info = e.Info; } @@ -322,13 +331,22 @@ bool AssetsCache::FindAsset(const Guid& id, AssetInfo& info) auto e = _registry.TryGet(id); if (e != nullptr) { - if (!IsEntryValid(*e)) + const auto validation = IsEntryValid(*e); + if (validation == EntryValidation::Invalid) { LOG(Warning, "Missing file from registry: \'{0}\':{1}:{2}", e->Info.Path, e->Info.ID, e->Info.TypeName); _registry.Remove(id); } else { +#if ENABLE_ASSETS_DISCOVERY + if (validation == EntryValidation::Inaccessible && !e->WarnedInaccessible) + { + e->WarnedInaccessible = true; + LOG(Warning, "Asset file locked, keeping cached entry: \'{0}\':{1}:{2}", e->Info.Path, e->Info.ID, e->Info.TypeName); + } +#endif + // Found result = true; info = e->Info; @@ -567,60 +585,70 @@ bool AssetsCache::RenameAsset(const StringView& oldPath, const StringView& newPa #endif -bool AssetsCache::IsEntryValid(Entry& e) +AssetsCache::EntryValidation AssetsCache::IsEntryValid(Entry& e) { #if ENABLE_ASSETS_DISCOVERY // Check if file exists - if (FileSystem::FileExists(e.Info.Path)) + if (!FileSystem::FileExists(e.Info.Path)) + return EntryValidation::Invalid; + + // Check if file hasn't been modified + const auto fileModified = FileSystem::GetFileLastEditTime(e.Info.Path); + if (fileModified == e.FileModified) { - // Check if file hasn't been modified - const auto fileModified = FileSystem::GetFileLastEditTime(e.Info.Path); - if (fileModified == e.FileModified) - return true; - - const auto extension = FileSystem::GetExtension(e.Info.Path).ToLower(); - - // Check if it's a binary asset - if (ContentStorageManager::IsFlaxStorageExtension(extension)) - { - // Validate ID within storage container - const auto storage = ContentStorageManager::GetStorage(e.Info.Path); - if (storage) - { - // Check if storage at given location contains that asset - const bool isValid = storage->HasAsset(e.Info); - - // Update entry and mark cache as dirty - e.FileModified = fileModified; - _isDirty = true; - - return isValid; - } - } - // Check for json resource - else if (JsonStorageProxy::IsValidExtension(extension)) - { - // Check Json storage layer - Guid jsonId; - String jsonTypeName; - if (JsonStorageProxy::GetAssetInfo(e.Info.Path, jsonId, jsonTypeName)) - { - const bool isValid = e.Info.ID == jsonId && e.Info.TypeName == jsonTypeName; - - // Update entry and mark cache as dirty - e.FileModified = fileModified; - _isDirty = true; - - return isValid; - } - } + e.WarnedInaccessible = false; + return EntryValidation::Valid; } - return false; + const auto extension = FileSystem::GetExtension(e.Info.Path).ToLower(); + // Check if it's a binary asset + if (ContentStorageManager::IsFlaxStorageExtension(extension)) + { + // Validate ID within storage container + const auto storage = ContentStorageManager::GetStorage(e.Info.Path); + if (storage) + { + // Check if storage at given location contains that asset + const bool isValid = storage->HasAsset(e.Info); + + // Update entry and mark cache as dirty + e.FileModified = fileModified; + e.WarnedInaccessible = false; + _isDirty = true; + + return isValid ? EntryValidation::Valid : EntryValidation::Invalid; + } + } + // Check for json resource + else if (JsonStorageProxy::IsValidExtension(extension)) + { + // Check Json storage layer + Guid jsonId; + String jsonTypeName; + if (JsonStorageProxy::GetAssetInfo(e.Info.Path, jsonId, jsonTypeName)) + { + const bool isValid = e.Info.ID == jsonId && e.Info.TypeName == jsonTypeName; + + // Update entry and mark cache as dirty + e.FileModified = fileModified; + e.WarnedInaccessible = false; + _isDirty = true; + + return isValid ? EntryValidation::Valid : EntryValidation::Invalid; + } + } + else + { + // Unknown file type + return EntryValidation::Invalid; + } + + // File exists but cannot be read (likely locked by git or another process) + return EntryValidation::Inaccessible; #else // In game we don't care about it because all cached asset entries are valid (precached) // Skip only entries with missing file - return e.Info.Path.HasChars(); + return e.Info.Path.HasChars() ? EntryValidation::Valid : EntryValidation::Invalid; #endif } diff --git a/Source/Engine/Content/Cache/AssetsCache.h b/Source/Engine/Content/Cache/AssetsCache.h index 42f05a2aa..28380b69d 100644 --- a/Source/Engine/Content/Cache/AssetsCache.h +++ b/Source/Engine/Content/Cache/AssetsCache.h @@ -58,6 +58,11 @@ public: /// The file modified date. /// DateTime FileModified; + + /// + /// True if a warning about this entry being inaccessible has already been logged (prevents log spam). Runtime-only, not serialized. + /// + bool WarnedInaccessible = false; #endif Entry() @@ -73,6 +78,27 @@ public: } }; + /// + /// Result of validating an asset cache entry. + /// + enum class EntryValidation + { + /// + /// File verified, contains this asset. + /// + Valid, + + /// + /// File missing or contains a different asset. + /// + Invalid, + + /// + /// File exists but cannot be opened (locked by another process). + /// + Inaccessible, + }; + typedef Dictionary Registry; typedef Dictionary PathsMapping; @@ -232,6 +258,6 @@ public: /// Determines whether cached asset entry is valid. /// /// The asset entry. - /// True if is valid, otherwise false. - bool IsEntryValid(Entry& e); + /// The validation result. + EntryValidation IsEntryValid(Entry& e); }; From 24654e5b02abb0cca7ca09babd9b4e5f841f6860 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Jun 2026 15:54:00 +0200 Subject: [PATCH 36/51] Add `CHECK_NO_RETURN` for checks in code that should continue function execution --- Source/Engine/Content/Storage/FlaxStorage.cpp | 6 +++--- Source/Engine/Platform/Platform.h | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index 8dee911e4..4eaea48f8 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -243,9 +243,9 @@ FlaxStorage::~FlaxStorage() { // Validate if has been disposed ASSERT(IsDisposed()); - CHECK(_chunksLock == 0); - CHECK(_refCount == 0); - CHECK(_isUnloadingData == 0); + CHECK_NO_RETURN(_chunksLock == 0); + CHECK_NO_RETURN(_refCount == 0); + CHECK_NO_RETURN(_isUnloadingData == 0); ASSERT(_chunks.IsEmpty()); #if USE_EDITOR diff --git a/Source/Engine/Platform/Platform.h b/Source/Engine/Platform/Platform.h index e2c6808c9..438cb6602 100644 --- a/Source/Engine/Platform/Platform.h +++ b/Source/Engine/Platform/Platform.h @@ -75,6 +75,12 @@ Platform::CheckFailed(#expression, __FILE__, __LINE__); \ return returnValue; \ } +// Performs a soft check of the expression. Logs the expression failure and continues execution. +#define CHECK_NO_RETURN(expression) \ + if (!(expression)) \ + { \ + Platform::CheckFailed(#expression, __FILE__, __LINE__); \ + } #if ENABLE_ASSERTION // Performs a soft check of the expression. Logs the expression failure and returns from the function call. From 777602fee6ae6f6d5d41b2b188811677c5bc29b5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Jun 2026 15:54:36 +0200 Subject: [PATCH 37/51] Fix asset storage handling when file is locked https://github.com/LOOPDISK/FlaxEngine/pull/45 --- Source/Engine/Content/BinaryAsset.cpp | 5 +---- Source/Engine/Content/Cache/AssetsCache.cpp | 4 ++-- Source/Engine/Content/Storage/ContentStorageManager.cpp | 4 ++++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Content/BinaryAsset.cpp b/Source/Engine/Content/BinaryAsset.cpp index 04fb3c3cf..c79e011b2 100644 --- a/Source/Engine/Content/BinaryAsset.cpp +++ b/Source/Engine/Content/BinaryAsset.cpp @@ -564,10 +564,7 @@ ContentLoadTask* BinaryAsset::createLoadingTask() loadTask = preLoadChunksTask; } - // Before asset loading we have to initialize storage - // TODO: maybe in build game we could do it in place? - // This step is only for opening asset files in background and upgrading them - // In build game we have only a few packages which are ready to use + // Before asset loading we have to initialize storage and pull the asset header auto initTask = New(this); initTask->ContinueWith(loadTask); loadTask = initTask; diff --git a/Source/Engine/Content/Cache/AssetsCache.cpp b/Source/Engine/Content/Cache/AssetsCache.cpp index e36a0d76c..96da061f1 100644 --- a/Source/Engine/Content/Cache/AssetsCache.cpp +++ b/Source/Engine/Content/Cache/AssetsCache.cpp @@ -378,13 +378,13 @@ void AssetsCache::GetAllByTypeName(const StringView& typeName, Array& resu void AssetsCache::RegisterAssets(FlaxStorage* storage) { PROFILE_CPU(); - ASSERT(storage); // Get all entries Array entries; storage->GetEntries(entries); - ASSERT(entries.HasItems()); + if (entries.IsEmpty()) + return; ASSETS_CACHE_LOCK(); auto storagePath = storage->GetPath(); diff --git a/Source/Engine/Content/Storage/ContentStorageManager.cpp b/Source/Engine/Content/Storage/ContentStorageManager.cpp index 8aa36c7be..33a16e031 100644 --- a/Source/Engine/Content/Storage/ContentStorageManager.cpp +++ b/Source/Engine/Content/Storage/ContentStorageManager.cpp @@ -54,6 +54,7 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b Locker.Lock(); // Try fast lookup + bool wasCached = true; FlaxStorage* storage; if (!StorageMap.TryGet(path, storage)) { @@ -74,6 +75,7 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b // Register storage container StorageMap.Add(path, storage); + wasCached = false; } // Build reference (before releasing the lock so ContentStorageSystem::Job won't delete it when running from async thread) @@ -90,6 +92,8 @@ FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, b if (loadFailed) { LOG(Error, "Failed to load {0}.", path); + if (wasCached) + return result; Locker.Lock(); StorageMap.Remove(path); if (storage->IsPackage()) From 84ccb9df0c72a569d77609a6e9973271743f7079 Mon Sep 17 00:00:00 2001 From: Murry Lancashire Date: Tue, 2 Jun 2026 16:42:30 +0200 Subject: [PATCH 38/51] Add more LOD Generation options to model import settings (eg. borders lock, preserve UVs) https://github.com/LOOPDISK/FlaxEngine/commit/1cfd4634727055c2a7249dff447528877332beab --- .../Editor/Content/Import/ModelImportEntry.cs | 1 + Source/Engine/Tools/ModelTool/ModelTool.cpp | 55 +++++++++++++++++-- Source/Engine/Tools/ModelTool/ModelTool.h | 22 ++++++-- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs index 5e17d7ecf..eed8db6ba 100644 --- a/Source/Editor/Content/Import/ModelImportEntry.cs +++ b/Source/Editor/Content/Import/ModelImportEntry.cs @@ -19,6 +19,7 @@ namespace FlaxEngine.Tools private bool ShowRootMotion => ShowAnimation && RootMotion != RootMotionMode.None; private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals; private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents; + private bool ShowGenerateLODs => ShowGeometry && GenerateLODs; private bool ShowFramesRange => ShowAnimation && Duration == AnimationDuration.Custom; private bool ShowSplitting => Type != ModelType.Prefab; } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 119fa9555..5fe834ad0 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -588,6 +588,10 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(TriangleReduction); SERIALIZE(SloppyOptimization); SERIALIZE(LODTargetError); + SERIALIZE(LODTargetErrorAbsolute); + SERIALIZE(LODLockBorder); + SERIALIZE(LODPreserveUVs); + SERIALIZE(LODPreserveUVsWeight); SERIALIZE(ImportMaterials); SERIALIZE(CreateEmptyMaterialSlots); SERIALIZE(ImportMaterialsAsInstances); @@ -645,6 +649,10 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(TriangleReduction); DESERIALIZE(SloppyOptimization); DESERIALIZE(LODTargetError); + DESERIALIZE(LODTargetErrorAbsolute); + DESERIALIZE(LODLockBorder); + DESERIALIZE(LODPreserveUVs); + DESERIALIZE(LODPreserveUVsWeight); DESERIALIZE(ImportMaterials); DESERIALIZE(CreateEmptyMaterialSlots); DESERIALIZE(ImportMaterialsAsInstances); @@ -1954,6 +1962,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option // Automatic LOD generation if (options.GenerateLODs && options.LODCount > 1 && data.LODs.HasItems() && options.TriangleReduction < 1.0f - ZeroTolerance) { + PROFILE_CPU_NAMED("GenerateLODs"); auto lodStartTime = DateTime::NowUTC(); meshopt_setAllocator(MeshOptAllocate, MeshOptDeallocate); float triangleReduction = Math::Saturate(options.TriangleReduction); @@ -1992,13 +2001,51 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option continue; indices.Clear(); indices.Resize(srcMeshIndexCount); - int32 dstMeshIndexCount = {}; + int32 dstMeshIndexCount = 0; if (options.SloppyOptimization) + { + PROFILE_CPU_NAMED("meshopt_simplifySloppy"); dstMeshIndexCount = (int32)meshopt_simplifySloppy(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget, options.LODTargetError); + } else - dstMeshIndexCount = (int32)meshopt_simplify(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget, options.LODTargetError); - if (dstMeshIndexCount <= 0 || dstMeshIndexCount > indices.Count()) - continue; + { + // Build simplification flags + unsigned int simplifyOptions = 0; + if (options.LODLockBorder) + simplifyOptions |= meshopt_SimplifyLockBorder; + if (options.LODTargetErrorAbsolute) + simplifyOptions |= meshopt_SimplifyErrorAbsolute; + if (options.LODPreserveUVs && srcMesh->UVs.HasItems()) + { + // Pack UV channels as attributes for meshopt_simplifyWithAttributes + int32 uvChannelCount = srcMesh->UVs.Count(); + int32 attributeCount = uvChannelCount * 2; // 2 floats (U, V) per channel + Array attributes; + attributes.Resize(srcMeshVertexCount * attributeCount); + Array attributeWeights; + attributeWeights.Resize(attributeCount); + for (int32 ch = 0; ch < uvChannelCount; ch++) + { + for (int32 v = 0; v < srcMeshVertexCount; v++) + { + Float2 uv = srcMesh->UVs[ch][v]; + attributes[v * attributeCount + ch * 2 + 0] = uv.X; + attributes[v * attributeCount + ch * 2 + 1] = uv.Y; + } + attributeWeights[ch * 2 + 0] = options.LODPreserveUVsWeight; + attributeWeights[ch * 2 + 1] = options.LODPreserveUVsWeight; + } + PROFILE_CPU_NAMED("meshopt_simplifyWithAttributes"); + dstMeshIndexCount = (int32)meshopt_simplifyWithAttributes(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), attributes.Get(), sizeof(float) * attributeCount, attributeWeights.Get(), attributeCount, nullptr, dstMeshIndexCountTarget, options.LODTargetError, simplifyOptions, nullptr); + } + else + { + PROFILE_CPU_NAMED("meshopt_simplify"); + dstMeshIndexCount = (int32)meshopt_simplify(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget, options.LODTargetError, simplifyOptions, nullptr); + } + } + if (dstMeshIndexCount <= 0 || dstMeshIndexCount >= indices.Count()) + continue; // Skip if failed to generate LOD or it doesn't have less vertices than source indices.Resize(dstMeshIndexCount); // Generate simplified vertex buffer remapping table (use only vertices from LOD index buffer) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index ff47e635d..f104bd83f 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -296,20 +296,32 @@ public: API_FIELD(Attributes="EditorOrder(1100), EditorDisplay(\"Level Of Detail\", \"Generate LODs\"), VisibleIf(nameof(ShowGeometry))") bool GenerateLODs = false; // The index of the LOD from the source model data to use as a reference for following LODs generation. - API_FIELD(Attributes="EditorOrder(1110), EditorDisplay(\"Level Of Detail\", \"Base LOD\"), VisibleIf(nameof(ShowGeometry)), Limit(0, 5, 0.065f)") + API_FIELD(Attributes="EditorOrder(1110), EditorDisplay(\"Level Of Detail\", \"Base LOD\"), VisibleIf(nameof(ShowGenerateLODs)), Limit(0, 5, 0.065f)") int32 BaseLOD = 0; // The amount of LODs to include in the model (all remaining ones starting from Base LOD will be generated). - API_FIELD(Attributes="EditorOrder(1120), EditorDisplay(\"Level Of Detail\", \"LOD Count\"), VisibleIf(nameof(ShowGeometry)), Limit(1, 6, 0.065f)") + API_FIELD(Attributes="EditorOrder(1120), EditorDisplay(\"Level Of Detail\", \"LOD Count\"), VisibleIf(nameof(ShowGenerateLODs)), Limit(1, 6, 0.065f)") int32 LODCount = 4; // The target amount of triangles for the generated LOD (based on the higher LOD). Normalized to range 0-1. For instance 0.4 cuts the triangle count to 40%. - API_FIELD(Attributes="EditorOrder(1130), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGeometry)), Limit(0, 1, 0.001f)") + API_FIELD(Attributes="EditorOrder(1130), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGenerateLODs)), Limit(0, 1, 0.001f)") float TriangleReduction = 0.5f; // Whether to do a sloppy mesh optimization. This is faster but does not follow the topology of the original mesh. - API_FIELD(Attributes="EditorOrder(1140), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGeometry))") + API_FIELD(Attributes="EditorOrder(1140), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGenerateLODs))") bool SloppyOptimization = false; // Target error is an approximate measure of the deviation from the original mesh using distance normalized to [0,1] range (e.g. 0.01 means that simplifier will try to maintain the error to be below 1% of the mesh extents). Only used if Sloppy is unchecked. - API_FIELD(Attributes="EditorOrder(1150), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGeometry)), Limit(0.01f, 1, 0.001f)") + API_FIELD(Attributes="EditorOrder(1150), EditorDisplay(\"Level Of Detail\", \"LOD Target Error\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs)), Limit(0.01f, 1, 0.001f)") float LODTargetError = 0.05f; + // If checked, vertices on topological borders (edges without a paired triangle) will not be moved during simplification. Useful for meshes that tile or share edges with other meshes. + API_FIELD(Attributes="EditorOrder(1170), EditorDisplay(\"Level Of Detail\", \"Lock Border\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs))") + bool LODLockBorder = false; + // If checked, the target error will be treated as absolute rather than relative to the mesh extents. In that mode, error is defined in absolute units which can be universal across similar mesh types no matter their size. + API_FIELD(Attributes="EditorOrder(1160), EditorDisplay(\"Level Of Detail\", \"LOD Target Error Absolute\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs))") + bool LODTargetErrorAbsolute = false; + // If checked, UV channels will be included in the simplification error metric to preserve UV layout. Essential for trimsheets and atlased textures. + API_FIELD(Attributes="EditorOrder(1180), EditorDisplay(\"Level Of Detail\", \"Preserve UVs\"), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs))") + bool LODPreserveUVs = false; + // The weight of UV attributes in the simplification error metric. Higher values preserve UVs more aggressively at the cost of geometric quality. Only used when Preserve UVs is enabled. + API_FIELD(Attributes="EditorOrder(1190), EditorDisplay(\"Level Of Detail\", \"Preserve UVs Weight\"), VisibleIf(nameof(LODPreserveUVs)), VisibleIf(nameof(SloppyOptimization), true), VisibleIf(nameof(ShowGenerateLODs)), Limit(0.001f, 1, 0.001f)") + float LODPreserveUVsWeight = 0.01f; public: // Materials From fca6ed43ccacb9b746b72bd4bf2787d02ccb67d7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 2 Jun 2026 19:17:40 +0200 Subject: [PATCH 39/51] Fix compilation regression --- Source/Engine/Graphics/Models/ModelInstanceEntry.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp b/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp index 6f8ace8d7..55448e1a3 100644 --- a/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp +++ b/Source/Engine/Graphics/Models/ModelInstanceEntry.cpp @@ -50,7 +50,7 @@ void ModelInstanceEntries::Serialize(SerializeStream& stream, const void* otherO void ModelInstanceEntries::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) { PROFILE_MEM(Graphics); - const DeserializeStream& entriesData = stream[DeserializeStream::GenericValue(rapidjson::StringRef("Entries", 7))]; + const DeserializeStream& entriesData = stream[rapidjson_flax::Value(rapidjson::StringRef("Entries", 7))]; CHECK(entriesData.IsArray()); Resize(entriesData.Size()); ModelInstanceEntry* entries = Get(); From c2ec3fe2cbbb9dd6030fd0c85209c8f4a6a00f1a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 05:03:22 +0200 Subject: [PATCH 40/51] Simplify async render flushing code --- Source/Engine/Graphics/RenderTask.cpp | 7 +++++++ Source/Engine/Graphics/RenderTask.h | 5 +++++ Source/Engine/Renderer/Renderer.cpp | 8 ++------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Graphics/RenderTask.cpp b/Source/Engine/Graphics/RenderTask.cpp index ad8bbbdc0..94fdb11ac 100644 --- a/Source/Engine/Graphics/RenderTask.cpp +++ b/Source/Engine/Graphics/RenderTask.cpp @@ -507,3 +507,10 @@ RenderContextBatch::RenderContextBatch(const RenderContext& context) Contexts.Add(context); EnableAsync = JobSystem::GetThreadsCount() > 1; } + +void RenderContextBatch::FlushWaitLabels() +{ + for (const int64 label : WaitLabels) + JobSystem::Wait(label); + WaitLabels.Clear(); +} diff --git a/Source/Engine/Graphics/RenderTask.h b/Source/Engine/Graphics/RenderTask.h index 8cba1006e..db1332bba 100644 --- a/Source/Engine/Graphics/RenderTask.h +++ b/Source/Engine/Graphics/RenderTask.h @@ -533,4 +533,9 @@ API_STRUCT(NoDefault) struct FLAXENGINE_API RenderContextBatch { return Contexts.Get()[0]; } + + /// + /// Waits for all scheduled async jobs to complete and clears WaitLabels. + /// + void FlushWaitLabels(); }; diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp index a1f531ac1..1098692cc 100644 --- a/Source/Engine/Renderer/Renderer.cpp +++ b/Source/Engine/Renderer/Renderer.cpp @@ -356,9 +356,7 @@ void Renderer::DrawActors(RenderContext& renderContext, const Array& cus Level::DrawActors(renderContextBatch, SceneRendering::DrawCategory::SceneDraw); Level::DrawActors(renderContextBatch, SceneRendering::DrawCategory::SceneDrawAsync); JobSystem::SetJobStartingOnDispatch(true); - for (const int64 label : renderContextBatch.WaitLabels) - JobSystem::Wait(label); - renderContextBatch.WaitLabels.Clear(); + renderContextBatch.FlushWaitLabels(); } } @@ -483,9 +481,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont // Wait for async jobs to finish JobSystem::SetJobStartingOnDispatch(true); - for (const int64 label : renderContextBatch.WaitLabels) - JobSystem::Wait(label); - renderContextBatch.WaitLabels.Clear(); + renderContextBatch.FlushWaitLabels(); // Perform custom post-scene drawing (eg. GPU dispatches used by VFX) for (int32 i = 0; i < renderContextBatch.Contexts.Count(); i++) From f4be035f04a19a9f34ca68288ec32df91c042a71 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 05:03:32 +0200 Subject: [PATCH 41/51] Add `Physics::DeleteScene` --- Source/Engine/Physics/Physics.cpp | 9 +++++++++ Source/Engine/Physics/Physics.h | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/Source/Engine/Physics/Physics.cpp b/Source/Engine/Physics/Physics.cpp index ecd7c1093..24bb4c1a6 100644 --- a/Source/Engine/Physics/Physics.cpp +++ b/Source/Engine/Physics/Physics.cpp @@ -184,6 +184,15 @@ PhysicsScene* Physics::FindScene(const StringView& name) return nullptr; } +void Physics::DeleteScene(PhysicsScene* scene) +{ + if (scene == nullptr || scene == DefaultScene) + return; + scene->CollectResults(); + Scenes.RemoveKeepOrder(scene); + Delete(scene); +} + bool Physics::GetAutoSimulation() { return !DefaultScene || DefaultScene->GetAutoSimulation(); diff --git a/Source/Engine/Physics/Physics.h b/Source/Engine/Physics/Physics.h index 85cd5e77b..11fd3fb80 100644 --- a/Source/Engine/Physics/Physics.h +++ b/Source/Engine/Physics/Physics.h @@ -32,6 +32,11 @@ API_CLASS(Static) class FLAXENGINE_API Physics /// API_FUNCTION() static PhysicsScene* FindScene(const StringView& name); + /// + /// Delete an existing scene (excluding the default one). + /// + API_FUNCTION() static void DeleteScene(PhysicsScene* scene); + public: /// /// The automatic simulation feature. True if perform physics simulation after on fixed update by auto, otherwise user should do it. From 89a1f00c57e7ef986df77c7d2e46ab2a5a6eadd5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Apr 2026 00:24:57 +0200 Subject: [PATCH 42/51] Fix `Guid` diff serialization and loading invalid values --- Source/Engine/Serialization/Serialization.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Serialization/Serialization.cpp b/Source/Engine/Serialization/Serialization.cpp index ec02fd65f..78fbf7ec5 100644 --- a/Source/Engine/Serialization/Serialization.cpp +++ b/Source/Engine/Serialization/Serialization.cpp @@ -408,7 +408,7 @@ void Serialization::Deserialize(ISerializable::DeserializeStream& stream, Varian bool Serialization::ShouldSerialize(const Guid& v, const void* otherObj) { - return v.IsValid(); + return !otherObj || v != *(Guid*)otherObj; } void Serialization::Serialize(ISerializable::SerializeStream& stream, const Guid& v, const void* otherObj) @@ -427,10 +427,12 @@ void Serialization::Deserialize(ISerializable::DeserializeStream& stream, Guid& const char* b = a + 8; const char* c = b + 8; const char* d = c + 8; - StringUtils::ParseHex(a, 8, &v.A); - StringUtils::ParseHex(b, 8, &v.B); - StringUtils::ParseHex(c, 8, &v.C); - StringUtils::ParseHex(d, 8, &v.D); + bool failed = StringUtils::ParseHex(a, 8, &v.A); + failed |= StringUtils::ParseHex(b, 8, &v.B); + failed |= StringUtils::ParseHex(c, 8, &v.C); + failed |= StringUtils::ParseHex(d, 8, &v.D); + if (failed) + v = Guid::Empty; } bool Serialization::ShouldSerialize(const DateTime& v, const void* otherObj) From 422300adbd69e2ff1c7ba169536783a05662ab44 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 10:57:51 +0200 Subject: [PATCH 43/51] Add `VariantType::GetScriptingType` for easier type information access --- Source/Engine/AI/BehaviorKnowledge.cpp | 13 +++++-------- Source/Engine/Content/Assets/VisualScript.cpp | 2 +- Source/Engine/Core/Types/Variant.cpp | 5 +++++ Source/Engine/Core/Types/Variant.h | 1 + Source/Engine/Debug/DebugCommands.cpp | 2 +- Source/Engine/Level/Prefabs/Prefab.cpp | 2 +- Source/Engine/Visject/VisjectGraph.cpp | 2 +- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Source/Engine/AI/BehaviorKnowledge.cpp b/Source/Engine/AI/BehaviorKnowledge.cpp index 4d8e2eed9..872a12834 100644 --- a/Source/Engine/AI/BehaviorKnowledge.cpp +++ b/Source/Engine/AI/BehaviorKnowledge.cpp @@ -205,10 +205,9 @@ bool BehaviorKnowledge::Set(const StringAnsiView& path, const Variant& value) bool BehaviorKnowledge::HasGoal(ScriptingTypeHandle type) const { - for (int32 i = 0; i < Goals.Count(); i++) + for (const Variant& goal : Goals) { - const ScriptingTypeHandle goalType = Scripting::FindScriptingType(Goals[i].Type.GetTypeName()); - if (goalType == type) + if (goal.Type.GetScriptingType() == type) return true; } return false; @@ -218,8 +217,7 @@ const Variant& BehaviorKnowledge::GetGoal(ScriptingTypeHandle type) const { for (const Variant& goal : Goals) { - const ScriptingTypeHandle goalType = Scripting::FindScriptingType(goal.Type.GetTypeName()); - if (goalType == type) + if (goal.Type.GetScriptingType() == type) return goal; } return Variant::Null; @@ -242,10 +240,9 @@ void BehaviorKnowledge::RemoveGoal(ScriptingTypeHandle type) { for (int32 i = 0; i < Goals.Count(); i++) { - const ScriptingTypeHandle goalType = Scripting::FindScriptingType(Goals[i].Type.GetTypeName()); - if (goalType == type) + if (Goals[i].Type.GetScriptingType() == type) { - Goals.RemoveAt(i); + Goals.RemoveAtKeepOrder(i); break; } } diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp index 62d901b6f..26b896b02 100644 --- a/Source/Engine/Content/Assets/VisualScript.cpp +++ b/Source/Engine/Content/Assets/VisualScript.cpp @@ -339,7 +339,7 @@ void VisualScriptExecutor::ProcessGroupTools(Box* box, Node* node, Value& value) obj = Value::Null; #else const ScriptingTypeHandle type = Scripting::FindScriptingType(StringAnsiView(typeNameAnsi.Get(), typeName.Length())); - const ScriptingTypeHandle objType = Scripting::FindScriptingType(obj.Type.GetTypeName()); + const ScriptingTypeHandle objType = obj.Type.GetScriptingType(); if (!type || !objType || !objType.IsSubclassOf(type)) obj = Value::Null; #endif diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index 7f805f9e3..78b506b58 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -362,6 +362,11 @@ const char* VariantType::GetTypeName() const return InBuiltTypesTypeNames[Type]; } +ScriptingTypeHandle VariantType::GetScriptingType() const +{ + return Scripting::FindScriptingType(GetTypeName()); +} + VariantType VariantType::GetElementType() const { if (Type == Array) diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 13b928171..530a4da76 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -151,6 +151,7 @@ public: void SetTypeName(const ScriptingType& type); void SetTypeName(const MClass& klass); const char* GetTypeName() const; + ScriptingTypeHandle GetScriptingType() 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(); diff --git a/Source/Engine/Debug/DebugCommands.cpp b/Source/Engine/Debug/DebugCommands.cpp index feb985cd4..ac6e55de7 100644 --- a/Source/Engine/Debug/DebugCommands.cpp +++ b/Source/Engine/Debug/DebugCommands.cpp @@ -85,7 +85,7 @@ struct CommandData else if (value.Type.Type == VariantType::Structure) { // Prettify structure printing - ScriptingTypeHandle resultType = Scripting::FindScriptingType(value.Type.GetTypeName()); + ScriptingTypeHandle resultType = value.Type.GetScriptingType(); if (resultType) { Array fields; diff --git a/Source/Engine/Level/Prefabs/Prefab.cpp b/Source/Engine/Level/Prefabs/Prefab.cpp index 80120ea87..a14eae500 100644 --- a/Source/Engine/Level/Prefabs/Prefab.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.cpp @@ -64,7 +64,7 @@ Actor* Prefab::GetDefaultInstance() // Skip if not loaded if (!IsLoaded()) { - LOG(Warning, "Cannot instantiate object from not loaded prefab asset."); + LOG(Warning, "Cannot instantiate object from not loaded prefab asset ({}, {})", GetPath(), GetID()); return nullptr; } diff --git a/Source/Engine/Visject/VisjectGraph.cpp b/Source/Engine/Visject/VisjectGraph.cpp index 8b8c27010..2b18a7d71 100644 --- a/Source/Engine/Visject/VisjectGraph.cpp +++ b/Source/Engine/Visject/VisjectGraph.cpp @@ -766,7 +766,7 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value) structureValue = Variant::Cast(structureValue, typeVariantType); } structureValue.InvertInline(); // Extract any Float3/Int32 into Structure type from inlined format - const ScriptingTypeHandle structureValueTypeHandle = Scripting::FindScriptingType(structureValue.Type.GetTypeName()); + const ScriptingTypeHandle structureValueTypeHandle = structureValue.Type.GetScriptingType(); if (structureValue.Type.Type != VariantType::Structure || typeHandle != structureValueTypeHandle) { OnError(node, box, String::Format(TEXT("Cannot unpack value of type {0} to structure of type {1}"), structureValue.Type, typeName)); From f6f7bbb3d01dd5ca44c05ae7e00ca9f349e63569 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 10:58:17 +0200 Subject: [PATCH 44/51] Fix Variant static typenames caching bug in Editor --- Source/Engine/Core/Types/Variant.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index 78b506b58..d4fd3fdce 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -125,7 +125,7 @@ VariantType::VariantType(Types type, const StringAnsiView& typeName, bool static VariantType::VariantType(Types type, const ScriptingType& sType) : VariantType(type) { - SetTypeName(sType.Fullname, sType.Module->CanReload); + SetTypeName(sType.Fullname, !sType.Module->CanReload); } VariantType::VariantType(Types type, const MClass* klass) @@ -345,13 +345,13 @@ void VariantType::SetTypeName(const StringAnsiView& typeName, bool staticName) void VariantType::SetTypeName(const ScriptingType& type) { - SetTypeName(type.Fullname, type.Module->CanReload); + SetTypeName(type.Fullname, !type.Module->CanReload); } void VariantType::SetTypeName(const MClass& klass) { #if USE_CSHARP - SetTypeName(klass.GetFullName(), klass.GetAssembly()->CanReload()); + SetTypeName(klass.GetFullName(), !klass.GetAssembly()->CanReload()); #endif } From 0f8653709919bf1262b2add4df69eb4ef088a6ca Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 11:01:14 +0200 Subject: [PATCH 45/51] Add simpler `Variant::Enum` that auto-setups variant type from enum scripting info --- Source/Engine/Core/Types/Variant.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 530a4da76..9532d4ec4 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -421,6 +421,15 @@ public: return MoveTemp(v); } + template + static typename TEnableIf::Value, Variant>::Type Enum(const T value) + { + Variant v; + v.SetType(VariantType(VariantType::Enum, StaticType().GetType())); + v.AsUint64 = (uint64)value; + return MoveTemp(v); + } + template static typename TEnableIf::Value && !TIsPointer::Value, Variant>::Type Structure(VariantType&& type, const T& value) { From bdeb89538cf129b71051444eefbf0a223994ceec Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 11:05:17 +0200 Subject: [PATCH 46/51] Optimize auto generated Variant Types in bindings to reduce dynamic memory allocs in game builds --- .../Flax.Build/Bindings/BindingsGenerator.Cpp.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 6dd2aba6f..e67f66cde 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -203,11 +203,11 @@ namespace Flax.Build.Bindings var fullname = apiType.FullNameManaged; if (apiType.IsEnum) - return $"Variant::Enum(VariantType(VariantType::Enum, StringAnsiView(\"{fullname}\", {fullname.Length})), {value})"; + return $"Variant::Enum(VariantType(VariantType::Enum, StringAnsiView(\"{fullname}\", {fullname.Length}), !USE_EDITOR), {value})"; if (apiType.IsStruct && !CppInBuildVariantStructures.Contains(apiType.Name)) { if (apiType.IsInBuild) - return $"Variant::Structure(VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length})), {(typeInfo.IsPtr ? "*" + value : value)})"; + return $"Variant::Structure(VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length}), !USE_EDITOR), {(typeInfo.IsPtr ? "*" + value : value)})"; return $"Variant::Structure(VariantType(VariantType::Structure, {apiType.FullNameNative}::TypeInitializer.GetType()), {(typeInfo.IsPtr ? "*" + value : value)})"; } } @@ -269,7 +269,7 @@ namespace Flax.Build.Bindings { var elementType = FindApiTypeInfo(buildData, typeInfo.GenericArgs[0], caller); var elementName = $"{(elementType != null ? elementType.FullNameManaged : typeInfo.GenericArgs[0].Type)}[]"; - return $"VariantType(VariantType::Array, StringAnsiView(\"{elementName}\", {elementName.Length}))"; + return $"VariantType(VariantType::Array, StringAnsiView(\"{elementName}\", {elementName.Length}), !USE_EDITOR)"; } if (typeInfo.Type == "Dictionary" && typeInfo.GenericArgs != null) return "VariantType(VariantType::Dictionary)"; @@ -280,11 +280,11 @@ namespace Flax.Build.Bindings { var fullname = apiType.FullNameManaged; if (apiType.IsEnum) - return $"VariantType(VariantType::Enum, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + return $"VariantType(VariantType::Enum, StringAnsiView(\"{fullname}\", {fullname.Length}), !USE_EDITOR)"; if (apiType.IsStruct) { if (apiType.IsInBuild) - return $"VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length}))"; + return $"VariantType(VariantType::Structure, StringAnsiView(\"{fullname}\", {fullname.Length}), !USE_EDITOR)"; return $"VariantType(VariantType::Structure, {apiType.FullNameNative}::TypeInitializer.GetType())"; } if (apiType.IsClass) @@ -3103,7 +3103,7 @@ namespace Flax.Build.Bindings header.Append(" Variant result;").AppendLine(); var apiType = FindApiTypeInfo(buildData, valueType, moduleInfo); var elementName = $"{(apiType != null ? apiType.FullNameManaged : valueType.Type)}[]"; - header.Append($" result.SetType(VariantType(VariantType::Array, StringAnsiView(\"{elementName}\", {elementName.Length})));").AppendLine(); + header.Append($" result.SetType(VariantType(VariantType::Array, StringAnsiView(\"{elementName}\", {elementName.Length}), !USE_EDITOR));").AppendLine(); header.Append(" auto* array = reinterpret_cast*>(result.AsData);").AppendLine(); header.Append(" array->Resize(length);").AppendLine(); header.Append(" for (int32 i = 0; i < length; i++)").AppendLine(); From e0f234c66767cafc3284911c6dbc47886dc7a338 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 13:00:43 +0200 Subject: [PATCH 47/51] Add enum serialization as string via `EnumString` attribute --- .../Attributes/EnumStringAttribute.cs | 15 +++++ Source/Engine/Scripting/BinaryModule.cpp | 63 ++++++++----------- Source/Engine/Scripting/ScriptingType.h | 6 +- .../ExtendedDefaultContractResolver.cs | 18 ++++-- Source/Engine/Serialization/Serialization.cpp | 56 ++++++++++++++++- Source/Engine/Serialization/Serialization.h | 8 ++- .../Bindings/BindingsGenerator.Cpp.cs | 3 +- 7 files changed, 120 insertions(+), 49 deletions(-) create mode 100644 Source/Engine/Scripting/Attributes/EnumStringAttribute.cs diff --git a/Source/Engine/Scripting/Attributes/EnumStringAttribute.cs b/Source/Engine/Scripting/Attributes/EnumStringAttribute.cs new file mode 100644 index 000000000..aa9222c8b --- /dev/null +++ b/Source/Engine/Scripting/Attributes/EnumStringAttribute.cs @@ -0,0 +1,15 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System; + +namespace FlaxEngine +{ + /// + /// Changes enum serialization to use string names instead of integer values. This makes saved data resilient to enum reordering or changes in values (but not to renaming enums). Deserialization accepts both string names and integer values for backward compatibility. + /// + /// + [AttributeUsage(AttributeTargets.Enum)] + public sealed class EnumStringAttribute : Attribute + { + } +} diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index 02358fac0..029a245c3 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -204,7 +204,7 @@ ScriptingType::ScriptingType(const StringAnsiView& fullname, BinaryModule* modul Struct.SetField = setField; } -ScriptingType::ScriptingType(const StringAnsiView& fullname, BinaryModule* module, int32 size, EnumItem* items) +ScriptingType::ScriptingType(const StringAnsiView& fullname, BinaryModule* module, int32 size, EnumItem* items, bool stringSerialization) : ManagedClass(nullptr) , Module(module) , InitRuntime(DefaultInitRuntime) @@ -215,6 +215,7 @@ ScriptingType::ScriptingType(const StringAnsiView& fullname, BinaryModule* modul , Size(size) { Enum.Items = items; + Enum.StringSerialization = stringSerialization; } ScriptingType::ScriptingType(const StringAnsiView& fullname, BinaryModule* module, InitRuntimeHandler initRuntime, SetupScriptVTableHandler setupScriptVTable, SetupScriptObjectVTableHandler setupScriptObjectVTable, GetInterfaceWrapper getInterfaceWrapper) @@ -270,6 +271,7 @@ ScriptingType::ScriptingType(const ScriptingType& other) break; case ScriptingTypes::Enum: Enum.Items = other.Enum.Items; + Enum.StringSerialization = other.Enum.StringSerialization; break; case ScriptingTypes::Interface: Interface.SetupScriptVTable = other.Interface.SetupScriptVTable; @@ -323,6 +325,7 @@ ScriptingType::ScriptingType(ScriptingType&& other) break; case ScriptingTypes::Enum: Enum.Items = other.Enum.Items; + Enum.StringSerialization = other.Enum.StringSerialization; break; case ScriptingTypes::Interface: Interface.SetupScriptVTable = other.Interface.SetupScriptVTable; @@ -604,71 +607,57 @@ StringAnsiView ScriptingType::GetName() const return Fullname; } +#if BUILD_DEBUG || USE_EDITOR +#define INIT_TYPE(...) \ + module->Types.AddUninitialized(); \ + new(module->Types.Get() + TypeIndex)ScriptingType(fullname, module, ##__VA_ARGS__); \ + if (module->TypeNameToTypeIndex.ContainsKey(fullname)) \ + LOG(Error, "Duplicated native typename {0} from module {1}.", String(fullname), String(module->GetName())); \ + module->TypeNameToTypeIndex[fullname] = TypeIndex; +#else +#define INIT_TYPE(...) \ + module->Types.AddUninitialized(); \ + new(module->Types.Get() + TypeIndex)ScriptingType(fullname, module, ##__VA_ARGS__); \ + module->TypeNameToTypeIndex[fullname] = TypeIndex; +#endif + ScriptingTypeInitializer::ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::InitRuntimeHandler initRuntime, ScriptingType::SpawnHandler spawn, ScriptingTypeInitializer* baseType, ScriptingType::SetupScriptVTableHandler setupScriptVTable, ScriptingType::SetupScriptObjectVTableHandler setupScriptObjectVTable, const ScriptingType::InterfaceImplementation* interfaces) : ScriptingTypeHandle(module, module->Types.Count()) { // Script - module->Types.AddUninitialized(); - new(module->Types.Get() + TypeIndex)ScriptingType(fullname, module, size, initRuntime, spawn, baseType, setupScriptVTable, setupScriptObjectVTable, interfaces); -#if BUILD_DEBUG - if (module->TypeNameToTypeIndex.ContainsKey(fullname)) - LOG(Error, "Duplicated native typename {0} from module {1}.", String(fullname), String(module->GetName())); -#endif - module->TypeNameToTypeIndex[fullname] = TypeIndex; + INIT_TYPE(size, initRuntime, spawn, baseType, setupScriptVTable, setupScriptObjectVTable, interfaces); } ScriptingTypeInitializer::ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::InitRuntimeHandler initRuntime, ScriptingType::Ctor ctor, ScriptingType::Dtor dtor, ScriptingTypeInitializer* baseType, const ScriptingType::InterfaceImplementation* interfaces) : ScriptingTypeHandle(module, module->Types.Count()) { // Class - module->Types.AddUninitialized(); - new(module->Types.Get() + TypeIndex)ScriptingType(fullname, module, size, initRuntime, ctor, dtor, baseType, interfaces); -#if BUILD_DEBUG - if (module->TypeNameToTypeIndex.ContainsKey(fullname)) - LOG(Error, "Duplicated native typename {0} from module {1}.", String(fullname), String(module->GetName())); -#endif - module->TypeNameToTypeIndex[fullname] = TypeIndex; + INIT_TYPE(size, initRuntime, ctor, dtor, baseType, interfaces); } ScriptingTypeInitializer::ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::InitRuntimeHandler initRuntime, ScriptingType::Ctor ctor, ScriptingType::Dtor dtor, ScriptingType::Copy copy, ScriptingType::Box box, ScriptingType::Unbox unbox, ScriptingType::GetField getField, ScriptingType::SetField setField, ScriptingTypeInitializer* baseType, const ScriptingType::InterfaceImplementation* interfaces) : ScriptingTypeHandle(module, module->Types.Count()) { // Structure - module->Types.AddUninitialized(); - new(module->Types.Get() + TypeIndex)ScriptingType(fullname, module, size, initRuntime, ctor, dtor, copy, box, unbox, getField, setField, baseType, interfaces); -#if BUILD_DEBUG - if (module->TypeNameToTypeIndex.ContainsKey(fullname)) - LOG(Error, "Duplicated native typename {0} from module {1}.", String(fullname), String(module->GetName())); -#endif - module->TypeNameToTypeIndex[fullname] = TypeIndex; + INIT_TYPE(size, initRuntime, ctor, dtor, copy, box, unbox, getField, setField, baseType, interfaces); } -ScriptingTypeInitializer::ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::EnumItem* items) +ScriptingTypeInitializer::ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::EnumItem* items, bool stringSerialization) : ScriptingTypeHandle(module, module->Types.Count()) { // Enum - module->Types.AddUninitialized(); - new(module->Types.Get() + TypeIndex)ScriptingType(fullname, module, size, items); -#if BUILD_DEBUG - if (module->TypeNameToTypeIndex.ContainsKey(fullname)) - LOG(Error, "Duplicated native typename {0} from module {1}.", String(fullname), String(module->GetName())); -#endif - module->TypeNameToTypeIndex[fullname] = TypeIndex; + INIT_TYPE(size, items, stringSerialization); } ScriptingTypeInitializer::ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, ScriptingType::InitRuntimeHandler initRuntime, ScriptingType::SetupScriptVTableHandler setupScriptVTable, ScriptingType::SetupScriptObjectVTableHandler setupScriptObjectVTable, ScriptingType::GetInterfaceWrapper getInterfaceWrapper) : ScriptingTypeHandle(module, module->Types.Count()) { // Interface - module->Types.AddUninitialized(); - new(module->Types.Get() + TypeIndex)ScriptingType(fullname, module, initRuntime, setupScriptVTable, setupScriptObjectVTable, getInterfaceWrapper); -#if BUILD_DEBUG - if (module->TypeNameToTypeIndex.ContainsKey(fullname)) - LOG(Error, "Duplicated native typename {0} from module {1}.", String(fullname), String(module->GetName())); -#endif - module->TypeNameToTypeIndex[fullname] = TypeIndex; + INIT_TYPE(initRuntime, setupScriptVTable, setupScriptObjectVTable, getInterfaceWrapper); } +#undef INIT_TYPE + CriticalSection BinaryModule::Locker; BinaryModule::BinaryModulesList& BinaryModule::GetModules() diff --git a/Source/Engine/Scripting/ScriptingType.h b/Source/Engine/Scripting/ScriptingType.h index e1fb3dc04..8a15dd336 100644 --- a/Source/Engine/Scripting/ScriptingType.h +++ b/Source/Engine/Scripting/ScriptingType.h @@ -266,6 +266,8 @@ struct FLAXENGINE_API ScriptingType { // Enum items table (the last item name is null) EnumItem* Items; + // Enum uses string names serialization instead of integer values. + bool StringSerialization; } Enum; struct @@ -290,7 +292,7 @@ struct FLAXENGINE_API ScriptingType ScriptingType(const StringAnsiView& fullname, BinaryModule* module, int32 size, InitRuntimeHandler initRuntime = DefaultInitRuntime, SpawnHandler spawn = DefaultSpawn, ScriptingTypeInitializer* baseType = nullptr, SetupScriptVTableHandler setupScriptVTable = nullptr, SetupScriptObjectVTableHandler setupScriptObjectVTable = nullptr, const InterfaceImplementation* interfaces = nullptr); ScriptingType(const StringAnsiView& fullname, BinaryModule* module, int32 size, InitRuntimeHandler initRuntime, Ctor ctor, Dtor dtor, ScriptingTypeInitializer* baseType, const InterfaceImplementation* interfaces = nullptr); ScriptingType(const StringAnsiView& fullname, BinaryModule* module, int32 size, InitRuntimeHandler initRuntime, Ctor ctor, Dtor dtor, Copy copy, Box box, Unbox unbox, GetField getField, SetField setField, ScriptingTypeInitializer* baseType, const InterfaceImplementation* interfaces = nullptr); - ScriptingType(const StringAnsiView& fullname, BinaryModule* module, int32 size, EnumItem* items); + ScriptingType(const StringAnsiView& fullname, BinaryModule* module, int32 size, EnumItem* items, bool stringSerialization); ScriptingType(const StringAnsiView& fullname, BinaryModule* module, InitRuntimeHandler initRuntime, SetupScriptVTableHandler setupScriptVTable, SetupScriptObjectVTableHandler setupScriptObjectVTable, GetInterfaceWrapper getInterfaceWrapper); ScriptingType(const ScriptingType& other); ScriptingType(ScriptingType&& other); @@ -339,7 +341,7 @@ struct FLAXENGINE_API ScriptingTypeInitializer : ScriptingTypeHandle ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::InitRuntimeHandler initRuntime = ScriptingType::DefaultInitRuntime, ScriptingType::SpawnHandler spawn = ScriptingType::DefaultSpawn, ScriptingTypeInitializer* baseType = nullptr, ScriptingType::SetupScriptVTableHandler setupScriptVTable = nullptr, ScriptingType::SetupScriptObjectVTableHandler setupScriptObjectVTable = nullptr, const ScriptingType::InterfaceImplementation* interfaces = nullptr); ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::InitRuntimeHandler initRuntime, ScriptingType::Ctor ctor, ScriptingType::Dtor dtor, ScriptingTypeInitializer* baseType = nullptr, const ScriptingType::InterfaceImplementation* interfaces = nullptr); ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::InitRuntimeHandler initRuntime, ScriptingType::Ctor ctor, ScriptingType::Dtor dtor, ScriptingType::Copy copy, ScriptingType::Box box, ScriptingType::Unbox unbox, ScriptingType::GetField getField, ScriptingType::SetField setField, ScriptingTypeInitializer* baseType = nullptr, const ScriptingType::InterfaceImplementation* interfaces = nullptr); - ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::EnumItem* items); + ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, int32 size, ScriptingType::EnumItem* items, bool stringSerialization); ScriptingTypeInitializer(BinaryModule* module, const StringAnsiView& fullname, ScriptingType::InitRuntimeHandler initRuntime, ScriptingType::SetupScriptVTableHandler setupScriptVTable, ScriptingType::SetupScriptObjectVTableHandler setupScriptObjectVTable, ScriptingType::GetInterfaceWrapper getInterfaceWrapper); }; diff --git a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedDefaultContractResolver.cs b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedDefaultContractResolver.cs index b8e07e448..4f2690863 100644 --- a/Source/Engine/Serialization/JsonCustomSerializers/ExtendedDefaultContractResolver.cs +++ b/Source/Engine/Serialization/JsonCustomSerializers/ExtendedDefaultContractResolver.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; namespace FlaxEngine.Json.JsonCustomSerializers @@ -44,6 +45,13 @@ namespace FlaxEngine.Json.JsonCustomSerializers ((JsonObjectContract)contract).ItemReferenceLoopHandling = ReferenceLoopHandling.Serialize; } + // Check if use enum serialization as string + var type = Nullable.GetUnderlyingType(objectType) ?? objectType; + if (type.IsEnum && type.GetCustomAttribute() != null) + { + contract.Converter = new StringEnumConverter(); + } + return contract; } @@ -53,19 +61,19 @@ namespace FlaxEngine.Json.JsonCustomSerializers var contract = base.CreateDictionaryContract(objectType); // Override contract to save enums keys as integer - if (contract.DictionaryKeyType?.IsEnum ?? false) + var keyType = contract.DictionaryKeyType; + if ((keyType?.IsEnum ?? false) && keyType.GetCustomAttribute() == null) { - var enumType = contract.DictionaryKeyType; contract.DictionaryKeyResolver = name => { try { - var e = Enum.Parse(enumType, name); + var e = Enum.Parse(keyType, name); name = Convert.ToInt32(e).ToString(); } - catch + catch (Exception ex) { - // Ignore errors + Debug.Logger.LogHandler.LogWrite(LogType.Warning, $"Failed to parse enum '{name}' as {keyType.Name}: {ex.Message}"); } return name; }; diff --git a/Source/Engine/Serialization/Serialization.cpp b/Source/Engine/Serialization/Serialization.cpp index 78fbf7ec5..bf3098140 100644 --- a/Source/Engine/Serialization/Serialization.cpp +++ b/Source/Engine/Serialization/Serialization.cpp @@ -49,6 +49,54 @@ void ISerializable::DeserializeIfExists(DeserializeStream& stream, const char* m var = defaultValue;\ } +void Serialization::SerializeEnum(ISerializable::SerializeStream& stream, uint32 v, ScriptingTypeHandle typeHandle) +{ + if (typeHandle) + { + // Check if serialize enum as string + const ScriptingType& type = typeHandle.GetType(); + if (type.Type == ScriptingTypes::Enum && type.Enum.StringSerialization) + { + const auto items = type.Enum.Items; + for (int32 i = 0; items[i].Name; i++) + { + if (items[i].Value == v) + { + stream.String(items[i].Name); + return; + } + } + } + } + stream.Uint(v); +} + +int32 Serialization::DeserializeEnum(ISerializable::DeserializeStream& stream, ScriptingTypeHandle typeHandle) +{ + if (stream.IsString() && typeHandle) + { + // Deserialize enum from string + const ScriptingType& type = typeHandle.GetType(); + if (type.Type == ScriptingTypes::Enum) + { + const auto str = stream.GetStringAnsiView(); + const auto items = type.Enum.Items; + for (int32 i = 0; items[i].Name; i++) + { + if (str == items[i].Name) + { + return (int32)items[i].Value; + } + } + int32 result; + if (!StringUtils::Parse(stream.GetString(), &result)) + return result; + LOG(Warning, "Failed to parse enum '{}' as {}", str.ToString(), type.Fullname.ToString()); + } + } + return DeserializeInt(stream); +} + bool Serialization::ShouldSerialize(const VariantType& v, const void* otherObj) { return !otherObj || v != *(VariantType*)otherObj; @@ -129,7 +177,6 @@ void Serialization::Serialize(ISerializable::SerializeStream& stream, const Vari stream.Int64(v.AsInt64); break; case VariantType::Uint64: - case VariantType::Enum: stream.Uint64(v.AsUint64); break; case VariantType::Float: @@ -222,6 +269,9 @@ void Serialization::Serialize(ISerializable::SerializeStream& stream, const Vari else stream.String("", 0); break; + case VariantType::Enum: + SerializeEnum(stream, (int32)v.AsUint64, v.Type.GetScriptingType()); + break; case VariantType::ManagedObject: case VariantType::Structure: { @@ -276,7 +326,6 @@ void Serialization::Deserialize(ISerializable::DeserializeStream& stream, Varian v.AsInt64 = value.GetInt64(); break; case VariantType::Uint64: - case VariantType::Enum: v.AsUint64 = value.GetUint64(); break; case VariantType::Float: @@ -371,6 +420,9 @@ void Serialization::Deserialize(ISerializable::DeserializeStream& stream, Varian CHECK(value.IsString()); v.SetTypename(value.GetStringAnsiView()); break; + case VariantType::Enum: + v.AsInt64 = DeserializeEnum(value, v.Type.GetScriptingType()); + break; case VariantType::ManagedObject: case VariantType::Structure: { diff --git a/Source/Engine/Serialization/Serialization.h b/Source/Engine/Serialization/Serialization.h index 9af6d7be1..41ae4898a 100644 --- a/Source/Engine/Serialization/Serialization.h +++ b/Source/Engine/Serialization/Serialization.h @@ -38,12 +38,16 @@ namespace Serialization int32 result = 0; if (stream.IsInt()) result = stream.GetInt(); + else if (stream.IsInt64()) + result = (int32)stream.GetInt64(); else if (stream.IsFloat()) result = (int32)stream.GetFloat(); else if (stream.IsString()) StringUtils::Parse(stream.GetString(), &result); return result; } + FLAXENGINE_API void SerializeEnum(ISerializable::SerializeStream& stream, uint32 v, ScriptingTypeHandle typeHandle); + FLAXENGINE_API int32 DeserializeEnum(ISerializable::DeserializeStream& stream, ScriptingTypeHandle typeHandle); // In-build types @@ -226,12 +230,12 @@ namespace Serialization template inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, const T& v, const void* otherObj) { - stream.Uint((uint32)v); + SerializeEnum(stream, (uint32)v, StaticType()); } template inline typename TEnableIf::Value>::Type Deserialize(ISerializable::DeserializeStream& stream, T& v, ISerializeModifier* modifier) { - v = (T)DeserializeInt(stream); + v = (T)DeserializeEnum(stream, StaticType()); } // Common types diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index e67f66cde..c3d2efbd9 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -2761,7 +2761,8 @@ namespace Flax.Build.Bindings contents.Append($"ScriptingTypeInitializer {enumTypeNameInternal}_TypeInitializer((BinaryModule*)GetBinaryModule{moduleInfo.Name}(), "); contents.Append($"StringAnsiView(\"{enumTypeNameManaged}\", {enumTypeNameManaged.Length}), "); contents.Append($"sizeof({enumTypeNameNative}), "); - contents.Append($"{enumTypeNameInternal}Internal::Items);").AppendLine(); + var stringSerialization = enumInfo.Attributes != null && enumInfo.Attributes.Contains("EnumString") ? "true" : "false"; + contents.Append($"{enumTypeNameInternal}Internal::Items, {stringSerialization});").AppendLine(); contents.AppendLine($"template<> {moduleInfo.Name.ToUpperInvariant()}_API ScriptingTypeHandle StaticType<{enumTypeNameNative}>() {{ return {enumTypeNameInternal}_TypeInitializer; }}"); } From fd8ae9bc2b65a2931944aed2c28fc6fe10d947ea Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 13:01:11 +0200 Subject: [PATCH 48/51] Rename `SceneRenderTask::RenderingPercentage` to `RenderScale` --- .../Editor/Windows/GraphicsQualityWindow.cs | 8 +++--- Source/Engine/Graphics/RenderTask.cpp | 25 +++++++++++-------- Source/Engine/Graphics/RenderTask.h | 8 +++++- Source/Engine/Renderer/Renderer.cpp | 4 ++- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Source/Editor/Windows/GraphicsQualityWindow.cs b/Source/Editor/Windows/GraphicsQualityWindow.cs index 27b131404..02e2da9bf 100644 --- a/Source/Editor/Windows/GraphicsQualityWindow.cs +++ b/Source/Editor/Windows/GraphicsQualityWindow.cs @@ -98,10 +98,10 @@ namespace FlaxEditor.Windows [NoSerialize, DefaultValue(1.0f), Limit(0.05f, 5, 0)] [EditorOrder(1400), EditorDisplay("Quality")] [Tooltip("The scale of the rendering resolution relative to the output dimensions. If lower than 1 the scene and postprocessing will be rendered at a lower resolution and upscaled to the output backbuffer.")] - public float RenderingPercentage + public float RenderScale { - get => MainRenderTask.Instance.RenderingPercentage; - set => MainRenderTask.Instance.RenderingPercentage = value; + get => MainRenderTask.Instance.RenderScale; + set => MainRenderTask.Instance.RenderScale = value; } [NoSerialize, DefaultValue(RenderingUpscaleLocation.AfterAntiAliasingPass), VisibleIf(nameof(UpscaleLocation_Visible))] @@ -113,7 +113,7 @@ namespace FlaxEditor.Windows set => MainRenderTask.Instance.UpscaleLocation = value; } - private bool UpscaleLocation_Visible => MainRenderTask.Instance.RenderingPercentage < 1.0f; + private bool UpscaleLocation_Visible => MainRenderTask.Instance.RenderScale < 1.0f; [NoSerialize, DefaultValue(1.0f), Limit(0, 1)] [EditorOrder(1500), EditorDisplay("Quality"), Tooltip("The global density scale for all foliage instances. The default value is 1. Use values from range 0-1. Lower values decrease amount of foliage instances in-game. Use it to tweak game performance for slower devices.")] diff --git a/Source/Engine/Graphics/RenderTask.cpp b/Source/Engine/Graphics/RenderTask.cpp index 94fdb11ac..31639c39d 100644 --- a/Source/Engine/Graphics/RenderTask.cpp +++ b/Source/Engine/Graphics/RenderTask.cpp @@ -353,8 +353,11 @@ Viewport SceneRenderTask::GetViewport() const viewport = Buffers->GetViewport(); else viewport = Viewport(0, 0, 1280, 720); - viewport.Width *= RenderingPercentage; - viewport.Height *= RenderingPercentage; +PRAGMA_DISABLE_DEPRECATION_WARNINGS + float renderScale = RenderingPercentage * RenderScale; +PRAGMA_ENABLE_DEPRECATION_WARNINGS + viewport.Width *= renderScale; + viewport.Height *= renderScale; return viewport; } @@ -394,13 +397,16 @@ void SceneRenderTask::OnBegin(GPUContext* context) } // Setup render buffers for the output rendering resolution +PRAGMA_DISABLE_DEPRECATION_WARNINGS + float renderScale = RenderingPercentage * RenderScale; +PRAGMA_ENABLE_DEPRECATION_WARNINGS if (Output) { - Buffers->Init((int32)((float)Output->Width() * RenderingPercentage), (int32)((float)Output->Height() * RenderingPercentage)); + Buffers->Init((int32)((float)Output->Width() * renderScale), (int32)((float)Output->Height() * renderScale)); } else if (SwapChain) { - Buffers->Init((int32)((float)SwapChain->GetWidth() * RenderingPercentage), (int32)((float)SwapChain->GetHeight() * RenderingPercentage)); + Buffers->Init((int32)((float)SwapChain->GetWidth() * renderScale), (int32)((float)SwapChain->GetHeight() * renderScale)); } } @@ -434,7 +440,10 @@ bool SceneRenderTask::Resize(int32 width, int32 height) PROFILE_MEM(Graphics); if (Output && Output->Resize(width, height)) return true; - if (Buffers && Buffers->Init((int32)((float)width * RenderingPercentage), (int32)((float)height * RenderingPercentage))) +PRAGMA_DISABLE_DEPRECATION_WARNINGS + float renderScale = RenderingPercentage * RenderScale; +PRAGMA_ENABLE_DEPRECATION_WARNINGS + if (Buffers && Buffers->Init((int32)((float)width * renderScale), (int32)((float)height * renderScale))) return true; return false; } @@ -477,12 +486,6 @@ void MainRenderTask::OnBegin(GPUContext* context) // Use the main camera for the game (can be later overriden in Begin event by external code) Camera = Camera::GetMainCamera(); -#if !USE_EDITOR - // Sync render buffers size with the backbuffer - const auto size = Screen::GetSize(); - Buffers->Init((int32)(size.X * RenderingPercentage), (int32)(size.Y * RenderingPercentage)); -#endif - SceneRenderTask::OnBegin(context); } diff --git a/Source/Engine/Graphics/RenderTask.h b/Source/Engine/Graphics/RenderTask.h index db1332bba..a4dbb5151 100644 --- a/Source/Engine/Graphics/RenderTask.h +++ b/Source/Engine/Graphics/RenderTask.h @@ -268,8 +268,14 @@ public: /// /// The scale of the rendering resolution relative to the output dimensions. If lower than 1 the scene and postprocessing will be rendered at a lower resolution and upscaled to the output backbuffer. + /// [Deprecated in v1.13] /// - API_FIELD() float RenderingPercentage = 1.0f; + API_FIELD() DEPRECATED("Use RenderScale instead.") float RenderingPercentage = 1.0f; + + /// + /// The scale of the rendering resolution relative to the output dimensions. If lower than 1 the scene and postprocessing will be rendered at a lower resolution and upscaled to the output backbuffer. + /// + API_FIELD() float RenderScale = 1.0f; /// /// The image resolution upscale location within rendering pipeline. Unused if RenderingPercentage is 1. diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp index 1098692cc..6173a1f5a 100644 --- a/Source/Engine/Renderer/Renderer.cpp +++ b/Source/Engine/Renderer/Renderer.cpp @@ -733,7 +733,9 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont } // Upscaling after scene rendering but before post processing - bool useUpscaling = task->RenderingPercentage < 1.0f; +PRAGMA_DISABLE_DEPRECATION_WARNINGS + bool useUpscaling = task->RenderingPercentage * task->RenderScale < 1.0f; +PRAGMA_ENABLE_DEPRECATION_WARNINGS const Viewport outputViewport = task->GetOutputViewport(); if (useUpscaling && setup.UpscaleLocation == RenderingUpscaleLocation::BeforePostProcessingPass) { From 27ee42b0a13810fce6b53f4e2adf6526e8d3d149 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 13:01:26 +0200 Subject: [PATCH 49/51] Bump up build number --- Flax.flaxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flax.flaxproj b/Flax.flaxproj index 04a161cf2..ed79458d2 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 12, "Revision": 0, - "Build": 6913 + "Build": 6914 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.", From eed227aa794c53586b98b8428d7b6b1339189a36 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 14:15:40 +0200 Subject: [PATCH 50/51] Add distance-scale to vertex paint vertices Add vertex paint brush size changing with shift+scroll Fix vertex paint brush size to match the highlight sphere --- Source/Editor/Tools/VertexPainting.cs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Source/Editor/Tools/VertexPainting.cs b/Source/Editor/Tools/VertexPainting.cs index 643441c46..dea5247f4 100644 --- a/Source/Editor/Tools/VertexPainting.cs +++ b/Source/Editor/Tools/VertexPainting.cs @@ -8,6 +8,7 @@ using FlaxEditor.Gizmo; using FlaxEditor.GUI.Tabs; using FlaxEditor.Modules; using FlaxEditor.SceneGraph; +using FlaxEditor.Utilities; using FlaxEditor.Viewport.Modes; using FlaxEngine; using FlaxEngine.GUI; @@ -307,7 +308,7 @@ namespace FlaxEditor.Tools public VertexPaintingGizmo Gizmo; public VertexColorsPreviewMode PreviewMode = VertexColorsPreviewMode.RGB; - public float PreviewVertexSize = 6.0f; + public float PreviewVertexSize = 4.0f; public float BrushSize = 100.0f; public float BrushStrength = 1.0f; public float BrushFalloff = 1.0f; @@ -402,7 +403,7 @@ namespace FlaxEditor.Tools if (meshDatas == null) throw new Exception("Missing mesh data of the model to paint."); var instanceTransform = _selectedModel.Transform; - var brushSphere = new BoundingSphere(_hitLocation, _gizmoMode.BrushSize); + var brushSphere = new BoundingSphere(_hitLocation, _gizmoMode.BrushSize * 0.5f); if (_paintUpdateCount == 0 && !_selectedModel.HasVertexColors) { // Initialize the instance vertex colors with originals from the asset @@ -509,6 +510,13 @@ namespace FlaxEditor.Tools return; } + // Increase or decrease brush size with scroll + if (Input.GetKey(KeyboardKeys.Shift) && !Input.GetMouseButton(MouseButton.Right)) + { + _gizmoMode.BrushSize += dt * _gizmoMode.BrushSize * Input.Mouse.ScrollDelta * 5f; + _gizmoMode.BrushSize = Mathf.Clamp(_gizmoMode.BrushSize, 0.0001f, 100000.0f); + } + // Perform detailed tracing to find cursor location for the brush var ray = Owner.MouseRay; var view = new Ray(Owner.ViewPosition, Owner.ViewDirection); @@ -570,7 +578,7 @@ namespace FlaxEditor.Tools } if (_brushModel && _brushMaterial) { - _brushMaterial.SetParameterValue("Color", new Color(1.0f, 0.85f, 0.0f)); // TODO: expose to editor options + _brushMaterial.SetParameterValue("Color", new Color(1.0f, 0.85f, 0.0f)); _brushMaterial.SetParameterValue("DepthBuffer", Owner.RenderTask.Buffers.DepthBuffer); Quaternion rotation = RootNode.RaycastNormalRotation(ref _hitNormal); Matrix transform = Matrix.Scaling(_gizmoMode.BrushSize * 0.01f) * Matrix.RotationQuaternion(rotation) * Matrix.Translation(_hitLocation - viewOrigin); @@ -586,8 +594,10 @@ namespace FlaxEditor.Tools _verticesPreviewMaterial = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.WiresDebugMaterial); } var instanceTransform = _selectedModel.Transform; - var modelScaleMatrix = Matrix.Scaling(_gizmoMode.PreviewVertexSize * 0.01f); - var brushSphere = new BoundingSphere(_hitLocation, _gizmoMode.BrushSize); + var distanceScale = (float)Vector3.Distance(instanceTransform.Translation, renderContext.View.Position) / (10.0f * Units.Meters2Units); + var vertexScale = Mathf.Lerp(0.005f, 0.01f, Mathf.Saturate(distanceScale)); + var modelScaleMatrix = Matrix.Scaling(_gizmoMode.PreviewVertexSize * vertexScale); + var brushSphere = new BoundingSphere(_hitLocation, _gizmoMode.BrushSize * 0.5f); var lodIndex = _gizmoMode.ModelLOD == -1 ? RenderTools.ComputeModelLOD(_selectedModel.Model, ref renderContext.View.Position, (float)_selectedModel.Sphere.Radius, ref renderContext) : _gizmoMode.ModelLOD; lodIndex = Mathf.Clamp(lodIndex, 0, meshDatas.Length - 1); var lodData = meshDatas[lodIndex]; From 1a8827ba7635ac62823eb53a14e2aca63eee2db4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 3 Jun 2026 14:21:38 +0200 Subject: [PATCH 51/51] Fix Web build when python is installed in folder with whitespaces in path --- Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp index a2c35f38f..e38a7310c 100644 --- a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp @@ -191,7 +191,7 @@ bool WebPlatformTools::OnPostProcess(CookingData& data) FileSystem::GetChildDirectories(pythons, emscriptenSdk / TEXT("/python")); if (pythons.HasItems()) { - procSettings.Arguments = procSettings.FileName + TEXT(".py ") + procSettings.Arguments; + procSettings.Arguments = String::Format(TEXT("\"{}.py\" {}"), procSettings.FileName, procSettings.Arguments); #if PLATFORM_WINDOWS procSettings.FileName = pythons[0] / TEXT("/python.exe"); #else