From 17f70d40909045251d453ad6aabe2ebc5f0bc539 Mon Sep 17 00:00:00 2001 From: Olly Rybak Date: Fri, 14 Aug 2026 00:07:56 +1000 Subject: [PATCH 01/34] Quick fix to make the infinite grid infinite. --- Source/Shaders/Editor/Grid.shader | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Shaders/Editor/Grid.shader b/Source/Shaders/Editor/Grid.shader index 45bb1ed90..527cb1551 100644 --- a/Source/Shaders/Editor/Grid.shader +++ b/Source/Shaders/Editor/Grid.shader @@ -44,6 +44,7 @@ META_VS_IN_ELEMENT(POSITION, 0, R32G32B32_FLOAT, 0, ALIGN, PER_VERTEX, 0, true) VertexOutput VS_Grid(ModelInput input) { VertexOutput output; + input.Position.xyz += float3(ViewPos.x, 0, ViewPos.z); output.WorldPosition = input.Position.xyz + ViewOrigin; float3 geoPosition = input.Position.xyz - float3(0, ViewOrigin.y, 0); output.Position = mul(float4(geoPosition, 1), ViewProjectionMatrix); From b5ced0732656cb54a82226a652e4e89868469911 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 16 Aug 2026 14:33:18 +0200 Subject: [PATCH 02/34] Fix terrain patch destruction crash regression --- Source/Engine/Terrain/TerrainPatch.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp index d51745e55..a0d742d56 100644 --- a/Source/Engine/Terrain/TerrainPatch.cpp +++ b/Source/Engine/Terrain/TerrainPatch.cpp @@ -2227,9 +2227,9 @@ void TerrainPatch::DestroyCollision() ScopeLock lock(_collisionLocker); ASSERT(HasCollision()); - void* scene = _terrain->GetPhysicsScene()->GetPhysicsScene(); + void* scene = _terrain->GetPhysicsScene() ? _terrain->GetPhysicsScene()->GetPhysicsScene() : nullptr; PhysicsBackend::RemoveCollider(_terrain); - if (_terrain->IsDuringPlay() && _terrain->IsActiveInHierarchy()) + if (scene && _terrain->IsDuringPlay() && _terrain->IsActiveInHierarchy()) PhysicsBackend::RemoveSceneActor(scene, _physicsActor); PhysicsBackend::DestroyActor(_physicsActor); PhysicsBackend::DestroyShape(_physicsShape); From cef6838b6821f6691623f9f1ae5207c39a6fc6ff Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 16 Aug 2026 14:39:47 +0200 Subject: [PATCH 03/34] Fix mesh index buffer when using 32-bit indices #4217 --- Source/Engine/Graphics/Models/Mesh.cpp | 2 +- Source/Engine/Graphics/Models/SkinnedMesh.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Graphics/Models/Mesh.cpp b/Source/Engine/Graphics/Models/Mesh.cpp index b793cfe38..a5975e05f 100644 --- a/Source/Engine/Graphics/Models/Mesh.cpp +++ b/Source/Engine/Graphics/Models/Mesh.cpp @@ -197,7 +197,7 @@ bool Mesh::UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Float3* ve bool Mesh::UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Float3* vertices, const uint32* triangles, const Float3* normals, const Float3* tangents, const Float2* uvs, const Color32* colors) { - return ::UpdateMesh(this, vertexCount, triangleCount, PixelFormat::R16_UInt, vertices, triangles, normals, tangents, uvs, colors); + return ::UpdateMesh(this, vertexCount, triangleCount, PixelFormat::R32_UInt, vertices, triangles, normals, tangents, uvs, colors); } bool Mesh::Load(uint32 vertices, uint32 triangles, const void* vb0, const void* vb1, const void* vb2, const void* ib, bool use16BitIndexBuffer) diff --git a/Source/Engine/Graphics/Models/SkinnedMesh.cpp b/Source/Engine/Graphics/Models/SkinnedMesh.cpp index 97aa6f6fb..67133c802 100644 --- a/Source/Engine/Graphics/Models/SkinnedMesh.cpp +++ b/Source/Engine/Graphics/Models/SkinnedMesh.cpp @@ -305,7 +305,7 @@ bool SkinnedMesh::UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Flo bool SkinnedMesh::UpdateMesh(uint32 vertexCount, uint32 triangleCount, const Float3* vertices, const uint32* triangles, const Int4* blendIndices, const Float4* blendWeights, const Float3* normals, const Float3* tangents, const Float2* uvs, const Color32* colors) { - return ::UpdateMesh(this, vertexCount, triangleCount, PixelFormat::R16_UInt, vertices, triangles, blendIndices, blendWeights, normals, tangents, uvs, colors); + return ::UpdateMesh(this, vertexCount, triangleCount, PixelFormat::R32_UInt, vertices, triangles, blendIndices, blendWeights, normals, tangents, uvs, colors); } void SkinnedMesh::Draw(const RenderContext& renderContext, const DrawInfo& info, float lodDitherFactor) const From 6b5cd3d56d8fb925f4d6812e64ebf865909a3ce4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 16 Aug 2026 14:41:15 +0200 Subject: [PATCH 04/34] Add missing shader file --- Content/Shaders/Editor/Grid.flax | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Content/Shaders/Editor/Grid.flax b/Content/Shaders/Editor/Grid.flax index 09ecd00e6..36149ed28 100644 --- a/Content/Shaders/Editor/Grid.flax +++ b/Content/Shaders/Editor/Grid.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5671b8b77b460a17d0a3c14174994a05cf1b3d8869d10b350de4a8053419836 -size 4647 +oid sha256:0aacf1fb2ab1e1cbbf6ebcb4003e0596e6a42ca1fa7761dbf5d8c0fd3b237e4e +size 4705 From e25349a54dc5ad6b7e465602567a1c2d4677eff5 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 16 Aug 2026 20:58:01 +0300 Subject: [PATCH 05/34] Fix errors breaking workspace events during content database rebuild Fixes one case where restorable asset windows does not get restored after recompiling scripts. --- .../Editor/Modules/ContentDatabaseModule.cs | 81 +++++++++++-------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index 0559e18db..237c95792 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -826,23 +826,29 @@ namespace FlaxEditor.Modules var startTime = Platform.TimeSeconds; _rebuildFlag = false; _rebuildInitFlag = false; - _enableEvents = false; - // Load all folders - // TODO: we should create async task for gathering content and whole workspace contents if it takes too long - // TODO: create progress bar in content window and after end we should enable events and update it + _enableEvents = false; _isDuringFastSetup = true; var startItems = _itemsCreated; - foreach (var project in Projects) + try { - if (project.Content != null) - LoadFolder(project.Content, true); - if (project.Source != null) - LoadFolder(project.Source, true); + // Load all folders + // TODO: we should create async task for gathering content and whole workspace contents if it takes too long + // TODO: create progress bar in content window and after end we should enable events and update it + foreach (var project in Projects) + { + if (project.Content != null) + LoadFolder(project.Content, true); + if (project.Source != null) + LoadFolder(project.Source, true); + } } - _isDuringFastSetup = false; - - _enableEvents = enableEvents; + finally + { + _isDuringFastSetup = false; + _enableEvents = enableEvents; + } + var endTime = Platform.TimeSeconds; Editor.Log(string.Format("Project database created in {0} ms. Items count: {1}", (int)((endTime - startTime) * 1000.0), _itemsCreated - startItems)); Profiler.EndEvent(); @@ -1333,39 +1339,44 @@ namespace FlaxEditor.Modules _enableEvents = false; _isDuringFastSetup = true; var startItems = _itemsCreated; - foreach (var project in Projects) + try { - if (project.Content != null) + foreach (var project in Projects) { - //Dispose(project.Content.Folder); - for (int i = 0; i < project.Content.Folder.Children.Count; i++) + if (project.Content != null) { - Dispose(project.Content.Folder.Children[i]); - i--; + //Dispose(project.Content.Folder); + for (int i = 0; i < project.Content.Folder.Children.Count; i++) + { + Dispose(project.Content.Folder.Children[i]); + i--; + } + } + if (project.Source != null) + { + //Dispose(project.Source.Folder); + for (int i = 0; i < project.Source.Folder.Children.Count; i++) + { + Dispose(project.Source.Folder.Children[i]); + i--; + } } } - if (project.Source != null) + + List removeProxies = new List(); + foreach (var proxy in Editor.Instance.ContentDatabase.Proxy) { - //Dispose(project.Source.Folder); - for (int i = 0; i < project.Source.Folder.Children.Count; i++) - { - Dispose(project.Source.Folder.Children[i]); - i--; - } + if (proxy.GetType().IsCollectible) + removeProxies.Add(proxy); } + foreach (var proxy in removeProxies) + RemoveProxy(proxy, false); } - - List removeProxies = new List(); - foreach (var proxy in Editor.Instance.ContentDatabase.Proxy) + finally { - if (proxy.GetType().IsCollectible) - removeProxies.Add(proxy); + _isDuringFastSetup = false; + _enableEvents = enabledEvents; } - foreach (var proxy in removeProxies) - RemoveProxy(proxy, false); - - _isDuringFastSetup = false; - _enableEvents = enabledEvents; } private void OnScriptsReloadEnd() From 4ee8169fc95bde9c86288290476ea0c145559dc9 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 16 Aug 2026 20:59:25 +0300 Subject: [PATCH 06/34] Prevent Flax tests project overriding the last opened project location --- Source/Editor/Editor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Editor/Editor.cpp b/Source/Editor/Editor.cpp index 18bf43a3a..f3065c1a1 100644 --- a/Source/Editor/Editor.cpp +++ b/Source/Editor/Editor.cpp @@ -642,9 +642,11 @@ int32 Editor::LoadProduct() } } +#if !FLAX_TESTS // Update the last opened project path if (lastProjectPath.Compare(Project->ProjectFolderPath) != 0) File::WriteAllText(lastProjectSettingPath, Project->ProjectFolderPath, Encoding::UTF8); +#endif return 0; } From 373ce6b212b459f27c0ea0943ed9027bf4fb396e Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 18 Aug 2026 00:49:05 +0300 Subject: [PATCH 07/34] Keep asset windows alive during scripts reload Recreated asset editor windows are now reattached to the native window after scripts reload to keep the current state and Z-order of the windows untouched on platforms where window state can't be changed without user interaction (Wayland). --- Source/Editor/CustomEditorWindow.cs | 18 +- Source/Editor/GUI/Docking/DockWindow.cs | 48 ++++ .../GUI/Docking/FloatWindowDockPanel.cs | 12 +- Source/Editor/Modules/WindowsModule.cs | 214 +++++++++--------- .../Windows/Assets/AssetEditorWindow.cs | 2 +- .../Windows/Assets/BehaviorTreeWindow.cs | 2 +- .../Editor/Windows/Assets/JsonAssetWindow.cs | 2 +- Source/Editor/Windows/Assets/PrefabWindow.cs | 2 +- Source/Engine/Platform/Base/Enums.h | 5 + 9 files changed, 183 insertions(+), 122 deletions(-) diff --git a/Source/Editor/CustomEditorWindow.cs b/Source/Editor/CustomEditorWindow.cs index 3df453ef8..4a94e0a34 100644 --- a/Source/Editor/CustomEditorWindow.cs +++ b/Source/Editor/CustomEditorWindow.cs @@ -1,8 +1,10 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using FlaxEditor.CustomEditors; using FlaxEditor.GUI.Docking; using FlaxEditor.Windows; +using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor @@ -89,8 +91,20 @@ namespace FlaxEditor { Editor.Instance.Windows.AddToRestore(this); } - Window.Close(); - Window.Dispose(); + Window.Close(ClosingReason.ScriptsReload); + } + + /// + /// Reattaches the window control to existing floating window. + /// + /// The window handle. + /// Initial window state. + /// The panel to dock to, if any. + /// Only used if is set. If true the window will be selected after docking it. + /// The splitter value to use if toDock is not null. If not specified, a default value will be used. + public void Restore(IntPtr windowHandle, DockState state = DockState.Float, DockPanel toDock = null, bool autoSelect = true, float? splitterValue = null) + { + _win.Restore(windowHandle, state, toDock, autoSelect, splitterValue); } /// diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs index 8cde79ce8..20ce85b0a 100644 --- a/Source/Editor/GUI/Docking/DockWindow.cs +++ b/Source/Editor/GUI/Docking/DockWindow.cs @@ -1,7 +1,9 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using System.Xml; using System.Globalization; +using System.Linq; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; @@ -232,6 +234,46 @@ namespace FlaxEditor.GUI.Docking } } + /// + /// Reattaches the window control to existing floating window. + /// + /// The window handle. + /// Initial window state. + /// Panel to dock to it. + /// Only used if is set. If true the window will be selected after docking it. + /// Only used if is set. The splitter value to use. If not specified, a default value will be used. + public void Restore(IntPtr windowHandle, DockState state = DockState.Float, DockPanel toDock = null, bool autoSelect = true, float? splitterValue = null) + { + if (state != DockState.Float) + { + Show(state, toDock, autoSelect, splitterValue); + return; + } + + Undock(); + + // Find the existing window and remove all controls from it + var window = Editor.GetWindows().First(x => x.NativePtr == windowHandle); + var windowGUI = window.GUI; + while (windowGUI.Children.Count > 0) + windowGUI.Children[^1].Dispose(); + windowGUI.EndTrackingMouse(); + + // Create dock panel for the window + var dockPanel = new FloatWindowDockPanel(_masterPanel, windowGUI); + dockPanel.DockWindowInternal(DockState.DockFill, this); + + // Perform layout + Visible = true; + windowGUI.UnlockChildrenRecursive(); + windowGUI.PerformLayout(); + + OnShow(); + + // Perform layout again + windowGUI.PerformLayout(); + } + /// /// Shows the window. /// @@ -332,6 +374,12 @@ namespace FlaxEditor.GUI.Docking } else { + if (reason == ClosingReason.ScriptsReload && _dockedTo is FloatWindowDockPanel floatPanel) + { + // Unlink the window to keep it alive during scripts reload + floatPanel.UnlinkWindow(); + } + // Undock Undock(); diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index b47d76bdd..c7481d338 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -89,6 +89,13 @@ namespace FlaxEditor.GUI.Docking } } + internal void UnlinkWindow() + { + _window?.Window.Closing -= OnClosing; + _window?.Window.LeftButtonHit -= OnLeftButtonHit; + _window = null; + } + /// protected override void PerformLayoutBeforeChildren() { @@ -191,9 +198,7 @@ namespace FlaxEditor.GUI.Docking } // Unlink - _window.Window.Closing -= OnClosing; - _window.Window.LeftButtonHit = null; - _window = null; + UnlinkWindow(); // Remove object FlaxEngine.Assertions.Assert.IsTrue(TabsCount == 0 && ChildPanelsCount == 0); @@ -244,6 +249,7 @@ namespace FlaxEditor.GUI.Docking { _masterPanel?.FloatingPanels.Remove(this); + UnlinkWindow(); base.OnDestroy(); } } diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 1a03c7c57..8074ab814 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Text; @@ -33,11 +34,12 @@ namespace FlaxEditor.Modules private float _projectIconScreenshotTimeout = -1; private string _windowsLayoutPath; - private struct WindowRestoreData + private class WindowRestoreData { public string AssemblyName; public string TypeName; + public IntPtr WindowHandle; public DockState DockState; public DockPanel DockedTo; public int DockedTabIndex; @@ -45,20 +47,10 @@ namespace FlaxEditor.Modules public bool SelectOnShow = false; - public bool Maximize; - public bool Minimize; - public Float2 FloatSize; - public Float2 FloatPosition; - public Guid AssetItemID; - - // Constructor, to allow for default values - public WindowRestoreData() - { - } } - private readonly List _restoreWindows = new List(); + private readonly Dictionary _restoreWindows = new(); /// /// The main editor window. @@ -822,10 +814,14 @@ namespace FlaxEditor.Modules internal void AddToRestore(AssetEditorWindow win) { - AddToRestore(win, win.GetType(), new WindowRestoreData + var assetItemId = win.Item.ID; + if (!_restoreWindows.TryGetValue(assetItemId, out var winData)) { - AssetItemID = win.Item.ID, - }); + winData = new WindowRestoreData(); + _restoreWindows.Add(assetItemId, winData); + } + winData.AssetItemID = assetItemId; + AddToRestore(win, win.GetType(), winData); } internal void AddToRestore(CustomEditorWindow win) @@ -836,62 +832,65 @@ namespace FlaxEditor.Modules if (constructor == null || type.IsGenericType) return; - AddToRestore(win.Window, type, new WindowRestoreData()); + // TODO: Restore data for custom editors + var assetItemId = Guid.NewGuid(); + if (!_restoreWindows.TryGetValue(assetItemId, out var winData)) + { + winData = new WindowRestoreData(); + _restoreWindows.Add(assetItemId, winData); + } + winData.AssetItemID = assetItemId; + AddToRestore(win.Window, type, winData); } private void AddToRestore(EditorWindow win, Type type, WindowRestoreData winData) { - // Ensure that this window is only selected following recompilation - // if it was the active tab in its dock panel. Otherwise, there is a - // risk of interrupting the user's workflow by potentially selecting - // background tabs. var window = win.RootWindow?.Window; var panel = win.ParentDockPanel; - winData.SelectOnShow = panel.SelectedTab == win; - winData.DockedTabIndex = 0; - if (panel is FloatWindowDockPanel && window != null && panel.TabsCount == 1) + winData.AssemblyName = type.Assembly.GetName().Name; + winData.TypeName = type.FullName; + if (panel is FloatWindowDockPanel) { - winData.DockState = DockState.Float; - winData.FloatPosition = window.Position; - winData.FloatSize = window.ClientSize; - winData.Maximize = window.IsMaximized; - winData.Minimize = window.IsMinimized; - winData.DockedTo = panel; + // Populate data for other tabs now, the tab index may change after tab is destroyed + if (winData.DockedTo == null) + { + for (int i = 0; i < panel.Tabs.Count; i++) + { + if (panel.Tabs[i] is AssetEditorWindow assetEditorWindow) + { + window ??= assetEditorWindow.RootWindow?.Window; // The window handle is sometimes missing in some tabs + var assetItemId = assetEditorWindow.Item.ID; + if (!_restoreWindows.TryGetValue(assetItemId, out var tabWinData)) + { + tabWinData = new WindowRestoreData(); + _restoreWindows.Add(assetItemId, tabWinData); + } + + tabWinData.DockedTabIndex = i; + tabWinData.SelectOnShow = panel.SelectedTab == assetEditorWindow; + tabWinData.DockState = DockState.DockFill; + tabWinData.DockedTo = panel; + } + } + winData.DockState = DockState.Float; + winData.WindowHandle = window?.NativePtr ?? IntPtr.Zero; + } } else { - for (int i = 0; i < panel.Tabs.Count; i++) - { - if (panel.Tabs[i] == win) - { - winData.DockedTabIndex = i; - break; - } - } - if (panel.TabsCount > 1) - { - winData.DockState = DockState.DockFill; - winData.DockedTo = panel; - } - else - { - winData.DockState = panel.TryGetDockState(out var splitterValue); - winData.DockedTo = panel.ParentDockPanel; - winData.SplitterValue = splitterValue; - } + winData.DockState = panel.TryGetDockState(out var splitterValue); + winData.DockedTo = panel.ParentDockPanel; + winData.SplitterValue = splitterValue; } - winData.AssemblyName = type.Assembly.GetName().Name; - winData.TypeName = type.FullName; - _restoreWindows.Add(winData); } private void OnWorkspaceRebuilt() { - // Go in reverse order to create floating Prefab windows first before docked windows - for (int i = _restoreWindows.Count - 1; i >= 0; i--) + var assetEditorWindows = new Dictionary(); + + // Create float windows first, then add docked windows in order of the tabs + foreach (var (_, winData) in _restoreWindows) { - var winData = _restoreWindows[i]; - try { var assembly = Utils.GetAssemblyByName(winData.AssemblyName); @@ -908,75 +907,24 @@ namespace FlaxEditor.Modules var assetType = assetItem.GetType(); var ctor = type.GetConstructor(new Type[] { typeof(Editor), assetType }); var win = (AssetEditorWindow)ctor.Invoke(new object[] { Editor.Instance, assetItem }); - - win.Show(winData.DockState, winData.DockState != DockState.Float ? winData.DockedTo : null, winData.SelectOnShow, winData.SplitterValue); + var previouslyDockedTo = winData.DockedTo; + win.Restore(winData.WindowHandle, winData.DockState, winData.DockState != DockState.Float ? winData.DockedTo : null, winData.SelectOnShow, winData.SplitterValue); if (winData.DockState == DockState.Float) { - var window = win.RootWindow.Window; - window.Position = winData.FloatPosition; - if (winData.Maximize) - { - window.Maximize(); - } - else if (winData.Minimize) - { - window.Minimize(); - } - else - { - window.ClientSize = winData.FloatSize; - } - // Update panel reference in other windows docked to this panel - foreach (ref var otherData in CollectionsMarshal.AsSpan(_restoreWindows)) + foreach (var key in _restoreWindows.Keys) { - if (otherData.DockedTo == winData.DockedTo) + ref var otherData = ref CollectionsMarshal.GetValueRefOrNullRef(_restoreWindows, key); + if (otherData.DockedTo == previouslyDockedTo) otherData.DockedTo = win.ParentDockPanel; } } - var panel = win.ParentDockPanel; - int currentTabIndex = 0; - for (int pi = 0; pi < panel.TabsCount; pi++) - { - if (panel.Tabs[pi] == win) - { - currentTabIndex = pi; - break; - } - } - while (currentTabIndex > winData.DockedTabIndex) - { - win.ParentDockPanel.MoveTabLeft(currentTabIndex); - currentTabIndex--; - } - while (currentTabIndex < winData.DockedTabIndex) - { - win.ParentDockPanel.MoveTabRight(currentTabIndex); - currentTabIndex++; - } - panel.PerformLayout(true); + assetEditorWindows.Add(winData.AssetItemID, win); } else { var win = (CustomEditorWindow)Activator.CreateInstance(type); - win.Show(winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue); - if (winData.DockState == DockState.Float) - { - var window = win.Window.RootWindow.Window; - window.Position = winData.FloatPosition; - if (winData.Maximize) - { - window.Maximize(); - } - else if (winData.Minimize) - { - window.Minimize(); - } - else - { - window.ClientSize = winData.FloatSize; - } - } + win.Restore(winData.WindowHandle, winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue); } } catch (Exception ex) @@ -985,6 +933,46 @@ namespace FlaxEditor.Modules Editor.LogWarning(string.Format("Failed to restore window {0} (assembly: {1})", winData.TypeName, winData.AssemblyName)); } } + + // Reorder tabs to previous order + foreach (var (_, winData) in _restoreWindows) + { + var win = assetEditorWindows.GetValueOrDefault(winData.AssetItemID); + if (win == null) + continue; + + var panel = win.ParentDockPanel; + int currentTabIndex = 0; + for (int pi = 0; pi < panel.TabsCount; pi++) + { + if (panel.Tabs[pi] == win) + { + currentTabIndex = pi; + break; + } + } + while (currentTabIndex > winData.DockedTabIndex) + { + win.ParentDockPanel.MoveTabLeft(currentTabIndex); + currentTabIndex--; + } + while (currentTabIndex < winData.DockedTabIndex) + { + win.ParentDockPanel.MoveTabRight(currentTabIndex); + currentTabIndex++; + } + } + + // Restore last selected tab + foreach (var (_, winData) in _restoreWindows) + { + var win = assetEditorWindows.GetValueOrDefault(winData.AssetItemID); + if (win != null && winData.SelectOnShow) + { + win.ParentDockPanel.SelectTab(win, false); + win.ParentDockPanel.PerformLayout(true); + } + } // Restored windows stole the focus from Editor if (_restoreWindows.Count > 0) diff --git a/Source/Editor/Windows/Assets/AssetEditorWindow.cs b/Source/Editor/Windows/Assets/AssetEditorWindow.cs index 93d6c850c..802686b76 100644 --- a/Source/Editor/Windows/Assets/AssetEditorWindow.cs +++ b/Source/Editor/Windows/Assets/AssetEditorWindow.cs @@ -177,7 +177,7 @@ namespace FlaxEditor.Windows.Assets Save(); } Editor.Instance.Windows.AddToRestore(this); - Close(); + Close(ClosingReason.ScriptsReload); } } diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 7772d59b2..461668783 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -274,7 +274,7 @@ namespace FlaxEditor.Windows.Assets base.OnScriptsReloadBegin(); // TODO: impl hot-reload for BT to nicely refresh state (save asset, clear undo/properties, reload surface) - Close(); + Close(ClosingReason.ScriptsReload); } private void UpdateKnowledge() diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs index 4d9c04942..696f63b25 100644 --- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs +++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs @@ -148,7 +148,7 @@ namespace FlaxEditor.Windows.Assets protected override void OnScriptsReloadBegin() { base.OnScriptsReloadBegin(); - Close(); + Close(ClosingReason.ScriptsReload); } /// diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index 06e007a17..da41b88c5 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -366,7 +366,7 @@ namespace FlaxEditor.Windows.Assets _viewport.Prefab = null; _undo?.Clear(); // TODO: maybe don't clear undo? - Close(); + Close(ClosingReason.ScriptsReload); } private void OnUndoEvent(IUndoAction action) diff --git a/Source/Engine/Platform/Base/Enums.h b/Source/Engine/Platform/Base/Enums.h index 4a903ad19..4af4df785 100644 --- a/Source/Engine/Platform/Base/Enums.h +++ b/Source/Engine/Platform/Base/Enums.h @@ -28,6 +28,11 @@ API_ENUM() enum class ClosingReason /// The close event. /// CloseEvent, + + /// + /// The scripts reload event. + /// + ScriptsReload, }; /// From b81e52481e025fe8d61af504a1a2a63b2b054cb8 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 18 Aug 2026 02:21:27 +0300 Subject: [PATCH 08/34] Fix mouse capture not ending after connecting Visject boxes --- Source/Editor/Surface/VisjectSurface.Connecting.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/Surface/VisjectSurface.Connecting.cs b/Source/Editor/Surface/VisjectSurface.Connecting.cs index 22616f6ad..621f6105f 100644 --- a/Source/Editor/Surface/VisjectSurface.Connecting.cs +++ b/Source/Editor/Surface/VisjectSurface.Connecting.cs @@ -287,6 +287,7 @@ namespace FlaxEditor.Surface // Reset instigator list _connectionInstigators.Clear(); + EndMouseCapture(); } } } From 7e624da0a1cba2c8bc4de7a7ffdcbc2157de5149 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 18 Aug 2026 02:42:02 +0300 Subject: [PATCH 09/34] Delete Visject surface node when press started over the close button --- Source/Editor/Surface/SurfaceNode.cs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Source/Editor/Surface/SurfaceNode.cs b/Source/Editor/Surface/SurfaceNode.cs index d28d04e1e..b050d5ae2 100644 --- a/Source/Editor/Surface/SurfaceNode.cs +++ b/Source/Editor/Surface/SurfaceNode.cs @@ -76,6 +76,11 @@ namespace FlaxEditor.Surface /// The footer rectangle (local space). /// protected Rectangle _footerRect; + + /// + /// The last mouse down position. + /// + protected Float2 _mouseDownMousePosition; /// /// The node archetype. @@ -163,8 +168,6 @@ namespace FlaxEditor.Surface /// protected virtual Color ArchetypeColor => GroupArchetype.Color; - private Float2 mouseDownMousePosition; - /// /// Calculates the size of the node including header, footer, and margins. /// @@ -1166,10 +1169,9 @@ namespace FlaxEditor.Surface if (base.OnMouseDown(location, button)) return true; + _mouseDownMousePosition = location; if (button == MouseButton.Left && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && _closeButtonRect.Contains(ref location)) return true; - if (button == MouseButton.Right) - mouseDownMousePosition = Input.Mouse.Position; return false; } @@ -1180,18 +1182,22 @@ namespace FlaxEditor.Surface if (base.OnMouseUp(location, button)) return true; - // Close/ delete - bool canDelete = !Surface.IsConnecting && !Surface.WasSelecting && !Surface.WasMovingSelection; - if (button == MouseButton.Left && canDelete && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && _closeButtonRect.Contains(ref location)) + if (button == MouseButton.Left) { - Surface.Delete(this); - return true; + // Close/delete + bool canDelete = !Surface.IsConnecting && !Surface.WasSelecting && !Surface.WasMovingSelection; + if (canDelete && (Archetype.Flags & NodeFlags.NoCloseButton) == 0 && + _closeButtonRect.Contains(ref location) && _closeButtonRect.Contains(ref _mouseDownMousePosition)) + { + Surface.Delete(this); + return true; + } } // Secondary Context Menu if (button == MouseButton.Right) { - float distance = Float2.Distance(mouseDownMousePosition, Input.Mouse.Position); + float distance = Float2.Distance(_mouseDownMousePosition, location); if (distance > 2.5f) return true; From b6f12be917effaad6e35d955e8edf2d67ac64fe5 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Fri, 21 Aug 2026 13:32:12 +0300 Subject: [PATCH 10/34] Fix scripts/content files getting deleted after file modifications Some editors like Visual Studio saves the modified file to a temporary file, deletes the original file and renames the temporary file back to original filename, which rarely causes the editor to detect it as a file deletion action by user. --- .../Editor/Modules/ContentDatabaseModule.cs | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index 237c95792..76c1180e6 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -748,21 +748,27 @@ namespace FlaxEditor.Modules if (item.Path.Contains(".Build.cs", StringComparison.Ordinal) && item.ItemType == ContentItemType.Script) Editor.Instance.CodeEditing.RemoveModule(item.Path); - // Check if it's an asset - if (item.IsAsset) - { - // Delete asset by using content pool - FlaxEngine.Content.DeleteAsset(path); - } - else if (item is ScriptItem) - { - FlaxEngine.Content.DeleteScript(path); - } - else if (deletedByUser) - { - // Delete file - if (File.Exists(path)) - File.Delete(path); + // Delete asset file only if it was explicitly deleted by the user. + // Some applications might modify the file by deleting the original and replacing + // it with a new file which sometimes is caught in the middle of these operations. + if (deletedByUser) + { + // Check if it's an asset + if (item.IsAsset) + { + // Delete asset by using content pool + FlaxEngine.Content.DeleteAsset(path); + } + else if (item is ScriptItem) + { + FlaxEngine.Content.DeleteScript(path); + } + else + { + // Delete file + if (File.Exists(path)) + File.Delete(path); + } } // Unlink from the parent From b1607e864d02641ff6a9ad2cb5ad060c41053a7b Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Fri, 21 Aug 2026 13:32:43 +0300 Subject: [PATCH 11/34] Generate project files automatically when Editor has focus --- Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs index 4b5f07664..41d0dc04e 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs @@ -505,7 +505,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing base.OnUpdate(); // Automatic project files generation after workspace modifications - if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty && !ScriptsBuilder.IsCompiling) + if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty && !ScriptsBuilder.IsCompiling && Engine.HasFocus) { // Try to delay generation when a lot of files are added at once if (ScriptsBuilder.IsSourceDirtyFor(TimeSpan.FromMilliseconds(150))) From de7f47c8e2e079b61e0eff993b40b78a60340df6 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Fri, 21 Aug 2026 13:33:23 +0300 Subject: [PATCH 12/34] Detect content changes only when Editor has focus --- Source/Editor/Modules/ContentDatabaseModule.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index 76c1180e6..292f98058 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -1393,17 +1393,21 @@ namespace FlaxEditor.Modules /// public override void OnUpdate() { - // Update all dirty content tree nodes - lock (_dirtyNodes) + // Check for updates only when the editor is focused to only check once for all changes done in background. + if (FlaxEngine.Engine.HasFocus) { - foreach (var node in _dirtyNodes) + // Update all dirty content tree nodes + lock (_dirtyNodes) { - LoadFolder(node, true); + foreach (var node in _dirtyNodes) + { + LoadFolder(node, true); - if (_enableEvents) - WorkspaceModified?.Invoke(); + if (_enableEvents) + WorkspaceModified?.Invoke(); + } + _dirtyNodes.Clear(); } - _dirtyNodes.Clear(); } // Lazy-rebuilds From 5d8cb4acaf5365e1c394c0dc93745c70bf91ad2f Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Fri, 21 Aug 2026 16:44:31 +0300 Subject: [PATCH 13/34] Try to improve scripting assembly file loading when file is locked --- Source/Engine/Engine/NativeInterop.Unmanaged.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index cb8688dcf..d9f5729f9 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -901,20 +901,21 @@ namespace FlaxEngine.Interop Assembly assembly; #if FLAX_EDITOR - // Load assembly from loaded bytes to prevent file locking in Editor - var assemblyBytes = File.ReadAllBytes(assemblyPath); - using MemoryStream stream = new MemoryStream(assemblyBytes); + // Load assembly with stream to prevent runtime from locking the assembly file + using FileStream stream = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read); var pdbPath = Path.ChangeExtension(assemblyPath, "pdb"); if (File.Exists(pdbPath)) { // Load including debug symbols - using FileStream pdbStream = new FileStream(Path.ChangeExtension(assemblyPath, "pdb"), FileMode.Open); + using FileStream pdbStream = new FileStream(Path.ChangeExtension(assemblyPath, "pdb"), FileMode.Open, FileAccess.Read); assembly = scriptingAssemblyLoadContext.LoadFromStream(stream, pdbStream); } else { assembly = scriptingAssemblyLoadContext.LoadFromStream(stream); } + + // TODO: Use new .NET 11 AssemblyLoadContext.SetAssemblyLocationOverride to specify correct Assembly.Location #else // Load assembly from file assembly = scriptingAssemblyLoadContext.LoadFromAssemblyPath(assemblyPath); From a2b730e5ebfd378610e8f179ce89a9a7077828c2 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 25 Aug 2026 22:30:00 +0300 Subject: [PATCH 14/34] Fix inconsistent `Time.GameTime` when entering play mode GameWindow entering effect does not reset properly sometimes when play mode is entered due to game time getting reset after the `OnPlayBegin` event. --- Source/Editor/States/PlayingState.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/States/PlayingState.cs b/Source/Editor/States/PlayingState.cs index 51dfd3677..a81487e43 100644 --- a/Source/Editor/States/PlayingState.cs +++ b/Source/Editor/States/PlayingState.cs @@ -159,11 +159,11 @@ namespace FlaxEditor.States SceneDuplicated?.Invoke(); RestoreSelection(); + Time.Synchronize(true); + Editor.OnPlayBegin(); IsPlayModeStarting = false; Profiler.EndEvent(); - - Time.Synchronize(true); } private void SetupEditorEnvOptions() From a56903d3ade19924d5ff407a1883213f88dfd321 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 25 Aug 2026 22:33:39 +0300 Subject: [PATCH 15/34] Fix client-side decorations in borderless editor game window on launch --- .../GUI/Docking/FloatWindowDockPanel.cs | 42 +++++++++++++------ Source/Editor/Windows/GameWindow.cs | 7 ++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index b47d76bdd..788631daa 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -65,6 +65,31 @@ namespace FlaxEditor.GUI.Docking /// public bool IsDragging { get; internal set; } + /// + /// Shows client-side window decorations around this panel. + /// + public bool ShowDecorations + { + get; + set + { + if (value == field) + return; + field = value; + if (value) + { + var decorations = Parent.AddChild(new FloatWindowDecorations(this)); + decorations.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false); + } + else + { + var decorations = Parent.GetChild(); + if (decorations != null) + decorations.Dispose(); + } + } + } = false; + /// /// Initializes a new instance of the class. /// @@ -82,11 +107,7 @@ namespace FlaxEditor.GUI.Docking _window.Window.Closing += OnClosing; _window.Window.LeftButtonHit += OnLeftButtonHit; - if (Utilities.Utils.UseCustomWindowDecorations()) - { - var decorations = Parent.AddChild(new FloatWindowDecorations(this)); - decorations.SetAnchorPreset(AnchorPresets.HorizontalStretchTop, false); - } + ShowDecorations = Utilities.Utils.UseCustomWindowDecorations(); } /// @@ -94,13 +115,10 @@ namespace FlaxEditor.GUI.Docking { base.PerformLayoutBeforeChildren(); - var decorations = Parent.GetChild(); - if (decorations != null) - { - // Apply offset for the title bar - foreach (var child in Children) - child.Bounds = child.Bounds with { Y = decorations.Height, Height = Parent.Height - decorations.Height }; - } + // Apply offset for the title bar + var decorationsHeight = Parent.GetChild()?.Height ?? 0; + foreach (var child in Children) + child.Bounds = child.Bounds with { Y = decorationsHeight, Height = Parent.Height - decorationsHeight }; } /// diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index 8c3793627..9419fbb51 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Xml; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Docking; using FlaxEditor.GUI.Input; using FlaxEditor.Modules; using FlaxEditor.Options; @@ -331,6 +332,9 @@ namespace FlaxEditor.Windows { IsFloating = true; var rootWindow = RootWindow; + var floatWin = rootWindow.GetChild(); + if (floatWin != null && floatWin.ShowDecorations) + floatWin.ShowDecorations = false; var monitorBounds = Platform.GetMonitorBounds(rootWindow.RootWindow.Window.ClientPosition); rootWindow.Window.Position = monitorBounds.Location; rootWindow.Window.SetBorderless(true); @@ -338,6 +342,9 @@ namespace FlaxEditor.Windows } else { + var floatWin = RootWindow.GetChild(); + if (floatWin != null && !floatWin.ShowDecorations && Utilities.Utils.UseCustomWindowDecorations()) + floatWin.ShowDecorations = true; IsFloating = false; } } From 101c15c677ef964a40a4285515036a8b5f6659b8 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 25 Aug 2026 22:35:29 +0300 Subject: [PATCH 16/34] Prevent refloating editor game window when window was floating before --- Source/Editor/GUI/Docking/DockWindow.cs | 2 +- Source/Editor/Windows/GameWindow.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs index 8cde79ce8..d6b86c833 100644 --- a/Source/Editor/GUI/Docking/DockWindow.cs +++ b/Source/Editor/GUI/Docking/DockWindow.cs @@ -488,7 +488,7 @@ namespace FlaxEditor.GUI.Docking base.Focus(); SelectTab(false); - BringToFront(); + _dockedTo?.RootWindow?.Focus(); } /// diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index 9419fbb51..f4c95cd17 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -310,9 +310,11 @@ namespace FlaxEditor.Windows // Restore if (rootWindow != null) rootWindow.Restore(); - if (_maximizeRestoreDockTo != null && _maximizeRestoreDockTo.IsDisposing) - _maximizeRestoreDockTo = null; - Show(_maximizeRestoreDockState, _maximizeRestoreDockTo); + var dockTo = _maximizeRestoreDockTo; + if (dockTo != null && dockTo.IsDisposing) + dockTo = null; + if (_dockedTo != dockTo) + Show(_maximizeRestoreDockState, dockTo); } } } From d21edc12cbce9ad22cbcdf55969bf1849537c509 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 25 Aug 2026 23:39:03 +0300 Subject: [PATCH 17/34] Fix restoring editor game window from maximized state to split panels --- Source/Editor/Windows/GameWindow.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index f4c95cd17..df9743534 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -133,6 +133,8 @@ namespace FlaxEditor.Windows private float _gameStartTime; private GUI.Docking.DockState _maximizeRestoreDockState; private GUI.Docking.DockPanel _maximizeRestoreDockTo; + private GUI.Docking.DockPanel _maximizeRestoreDockToParent; + private float _maximizeRestoreSplitterValue; private CursorLockMode _cursorLockMode = CursorLockMode.None; // Viewport scaling variables @@ -296,7 +298,8 @@ namespace FlaxEditor.Windows if (value) { _maximizeRestoreDockTo = _dockedTo; - _maximizeRestoreDockState = _dockedTo.TryGetDockState(out _); + _maximizeRestoreDockToParent = _dockedTo.ParentDockPanel; + _maximizeRestoreDockState = _dockedTo.TryGetDockState(out _maximizeRestoreSplitterValue); if (_maximizeRestoreDockState != GUI.Docking.DockState.Float) { var monitorBounds = Platform.GetMonitorBounds(PointToScreen(Size * 0.5f)); @@ -312,9 +315,9 @@ namespace FlaxEditor.Windows rootWindow.Restore(); var dockTo = _maximizeRestoreDockTo; if (dockTo != null && dockTo.IsDisposing) - dockTo = null; + dockTo = _maximizeRestoreDockToParent; if (_dockedTo != dockTo) - Show(_maximizeRestoreDockState, dockTo); + Show(_maximizeRestoreDockState, dockTo, splitterValue: _maximizeRestoreSplitterValue); } } } From 17df377737a2065ce2963db958bcfcb94d10a7a3 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Tue, 25 Aug 2026 23:43:14 +0300 Subject: [PATCH 18/34] Fix restoring editor game window from maximized state to dock panels --- Source/Editor/Windows/GameWindow.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index df9743534..e0c56b18b 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -300,6 +300,8 @@ namespace FlaxEditor.Windows _maximizeRestoreDockTo = _dockedTo; _maximizeRestoreDockToParent = _dockedTo.ParentDockPanel; _maximizeRestoreDockState = _dockedTo.TryGetDockState(out _maximizeRestoreSplitterValue); + if (_dockedTo.Tabs.Count > 1) + _maximizeRestoreDockState = DockState.DockFill; if (_maximizeRestoreDockState != GUI.Docking.DockState.Float) { var monitorBounds = Platform.GetMonitorBounds(PointToScreen(Size * 0.5f)); From 28d5a6d0de1af884683297071522833ae3e75f37 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Wed, 26 Aug 2026 00:33:49 +0300 Subject: [PATCH 19/34] Fix `DockWindow.IsDocked` to ignore single tab floating window as docked --- Source/Editor/GUI/Docking/DockWindow.cs | 2 +- Source/Editor/Windows/GameWindow.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs index d6b86c833..a6c98a1f6 100644 --- a/Source/Editor/GUI/Docking/DockWindow.cs +++ b/Source/Editor/GUI/Docking/DockWindow.cs @@ -50,7 +50,7 @@ namespace FlaxEditor.GUI.Docking /// /// Gets a value indicating whether this window is docked. /// - public bool IsDocked => _dockedTo != null; + public bool IsDocked => _dockedTo != null && _dockedTo.TabsCount > 1; /// /// Gets a value indicating whether this window is selected. diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index e0c56b18b..d351147cb 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -913,7 +913,7 @@ namespace FlaxEditor.Windows /// public void FocusGameViewport() { - if (!IsDocked) + if (ParentDockPanel == null) { ShowFloating(); } @@ -1018,7 +1018,7 @@ namespace FlaxEditor.Windows Screen.CursorVisible = true; Screen.CursorLock = CursorLockMode.None; - if (Editor.IsPlayMode && IsDocked && IsSelected && RootWindow.FocusedControl == null) + if (Editor.IsPlayMode && ParentDockPanel != null && IsSelected && RootWindow.FocusedControl == null) { // Game UI cleared focus so regain it to maintain UI navigation just like game window does FlaxEngine.Scripting.InvokeOnUpdate(Focus); From 582af19cb20b48b2b975048753da93bbb881cd0a Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Wed, 26 Aug 2026 20:11:03 +0300 Subject: [PATCH 20/34] Fix compilation --- Source/Editor/GUI/Docking/FloatWindowDockPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index 788631daa..4c7fb19b7 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -70,7 +70,7 @@ namespace FlaxEditor.GUI.Docking /// public bool ShowDecorations { - get; + get => field; set { if (value == field) @@ -88,7 +88,7 @@ namespace FlaxEditor.GUI.Docking decorations.Dispose(); } } - } = false; + } /// /// Initializes a new instance of the class. From dc0556015a55a53cc7237b9033b159785c294c94 Mon Sep 17 00:00:00 2001 From: Vaibhav Srivastava Date: Sat, 29 Aug 2026 17:06:09 +0530 Subject: [PATCH 21/34] docs: fix typo relase -> release Signed-off-by: Vaibhav Srivastava --- Development/Documentation/mono.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/Documentation/mono.md b/Development/Documentation/mono.md index 4a880b7a0..74def3fcb 100644 --- a/Development/Documentation/mono.md +++ b/Development/Documentation/mono.md @@ -14,4 +14,4 @@ Some useful notes and tips for devs: * `MONO_GC_DEBUG=nursery-canaries` - it might catch some buffer overflows in case of problems in code. * `MONO_GC_DEBUG=:` - will print GC debug to the log file (eg. `4:sgen-gc`). * Methods `mono_custom_attrs_from_property` and `mono_custom_attrs_get_attr` are internally cached -* If C++ mono call a method in c# that will throw an error, error will be handled but, not completly. Calling relase domain will return random `Access memory violation`. First search for error in c# code. No workaround yet. +* If C++ mono call a method in c# that will throw an error, error will be handled but, not completly. Calling release domain will return random `Access memory violation`. First search for error in c# code. No workaround yet. From d6b74c0bff8fbe12bc96cbfab91a3f2106efaaad Mon Sep 17 00:00:00 2001 From: Vaibhav Srivastava Date: Sat, 29 Aug 2026 17:06:26 +0530 Subject: [PATCH 22/34] docs: fix typo completly -> completely Signed-off-by: Vaibhav Srivastava --- Development/Documentation/mono.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/Documentation/mono.md b/Development/Documentation/mono.md index 74def3fcb..ff9dd1691 100644 --- a/Development/Documentation/mono.md +++ b/Development/Documentation/mono.md @@ -14,4 +14,4 @@ Some useful notes and tips for devs: * `MONO_GC_DEBUG=nursery-canaries` - it might catch some buffer overflows in case of problems in code. * `MONO_GC_DEBUG=:` - will print GC debug to the log file (eg. `4:sgen-gc`). * Methods `mono_custom_attrs_from_property` and `mono_custom_attrs_get_attr` are internally cached -* If C++ mono call a method in c# that will throw an error, error will be handled but, not completly. Calling release domain will return random `Access memory violation`. First search for error in c# code. No workaround yet. +* If C++ mono call a method in c# that will throw an error, error will be handled but, not completely. Calling release domain will return random `Access memory violation`. First search for error in c# code. No workaround yet. From b7b357014f43079ff80d36b5d736c72207500b36 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 29 Aug 2026 23:14:24 +0200 Subject: [PATCH 23/34] Comply with .net 8 for now --- Source/Editor/GUI/Docking/FloatWindowDockPanel.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index 4c7fb19b7..1ba732d84 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -49,6 +49,7 @@ namespace FlaxEditor.GUI.Docking private MasterDockPanel _masterPanel; private WindowRootControl _window; + private bool _showDecorations; /// /// Gets the master panel. @@ -70,12 +71,12 @@ namespace FlaxEditor.GUI.Docking /// public bool ShowDecorations { - get => field; + get => _showDecorations; set { - if (value == field) + if (value == _showDecorations) return; - field = value; + _showDecorations = value; if (value) { var decorations = Parent.AddChild(new FloatWindowDecorations(this)); From d99631ddbf1a2ced518e2cff98ba80f483e36261 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 29 Aug 2026 23:18:08 +0200 Subject: [PATCH 24/34] Comply with .net 8 for now --- Source/Editor/GUI/Docking/FloatWindowDockPanel.cs | 9 ++++++--- Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index f6875c4a9..f8eb26ded 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -113,9 +113,12 @@ namespace FlaxEditor.GUI.Docking internal void UnlinkWindow() { - _window?.Window.Closing -= OnClosing; - _window?.Window.LeftButtonHit -= OnLeftButtonHit; - _window = null; + if (_window != null) + { + _window.Window.Closing -= OnClosing; + _window.Window.LeftButtonHit -= OnLeftButtonHit; + _window = null; + } } /// diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs index de0fa755c..54dbcc250 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs @@ -136,7 +136,7 @@ namespace Flax.Build /// /// The maximum SDK version. /// - public static Version MaximumVersion => new Version(10, 0); + public static Version MaximumVersion => new Version(8, 0); /// public override TargetPlatform[] Platforms From ab7936c9ff93a8fd989558c15e0bd29532ae3f15 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 29 Aug 2026 23:46:54 +0200 Subject: [PATCH 25/34] Add middle and right mouse button actions to asset/object pickers to clear value or show popup quickly --- .../CustomEditors/Editors/FlaxObjectRefEditor.cs | 16 ++++++++++++++++ Source/Editor/GUI/AssetPicker.cs | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs b/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs index d7ab6d12b..a6a02ae1f 100644 --- a/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs +++ b/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs @@ -322,7 +322,11 @@ namespace FlaxEditor.CustomEditors.Editors // Deselect if (_value != null && button1Rect.Contains(ref location)) + { + Focus(); Value = null; + return true; + } // Picker dropdown menu if (_supportsPickDropDown && (isSelected ? button2Rect : button1Rect).Contains(ref location)) @@ -372,6 +376,18 @@ namespace FlaxEditor.CustomEditors.Editors if (_hasValidDragOver) _hasValidDragOver = false; } + if (button == MouseButton.Middle) + { + // Clear value + Focus(); + Value = null; + return true; + } + if (button == MouseButton.Right && _supportsPickDropDown) + { + // Show picker + ShowDropDownMenu(); + } return base.OnMouseUp(location, button); } diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 56c358c2f..98e9a0730 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -299,6 +299,7 @@ namespace FlaxEditor.GUI } else if (Button1Rect.Contains(location)) { + // Show picker Focus(); OnSubmit(); } @@ -318,6 +319,18 @@ namespace FlaxEditor.GUI } } } + if (button == MouseButton.Middle && IconRect.Contains(ref location)) + { + // Clear value + Focus(); + Validator.SelectedItem = null; + } + if (button == MouseButton.Right && IconRect.Contains(ref location)) + { + // Show picker + Focus(); + OnSubmit(); + } // Handled return true; From d9f14c0a86b3b48328f5009a1df42e04aa320aa8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 30 Aug 2026 00:13:03 +0200 Subject: [PATCH 26/34] Update macOS actions to to .NET 10 --- .github/workflows/build_ios.yml | 4 ++-- .github/workflows/cooking.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_ios.yml b/.github/workflows/build_ios.yml index 4e4291167..a83cd2233 100644 --- a/.github/workflows/build_ios.yml +++ b/.github/workflows/build_ios.yml @@ -19,7 +19,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Setup .NET Workload run: | dotnet workload install ios @@ -33,4 +33,4 @@ jobs: git lfs pull - name: Build run: | - ./Development/Scripts/Mac/CallBuildTool.sh -build -log -dotnet=9 -arch=ARM64 -platform=iOS -configuration=Release -buildtargets=FlaxGame + ./Development/Scripts/Mac/CallBuildTool.sh -build -log -dotnet=10 -arch=ARM64 -platform=iOS -configuration=Release -buildtargets=FlaxGame diff --git a/.github/workflows/cooking.yml b/.github/workflows/cooking.yml index 8c957bda1..5c6e1af6f 100644 --- a/.github/workflows/cooking.yml +++ b/.github/workflows/cooking.yml @@ -18,7 +18,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Setup .NET Workload run: | dotnet workload install ios @@ -42,7 +42,7 @@ jobs: cp ".github/data/Build Settings.json" "FlaxSamples/MaterialsFeaturesTour/Content/Settings" - name: Build Editor run: | - ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -dotnet=8 -arch=ARM64 -platform=Mac -configuration=Development -buildtargets=FlaxEditor + ./Development/Scripts/Mac/CallBuildTool.sh -build -log -printSDKs -dotnet=10 -arch=ARM64 -platform=Mac -configuration=Development -buildtargets=FlaxEditor - name: Cook Game (iOS) run: | ./Binaries/Editor/Mac/Development/FlaxEditor -std -headless -mute -null -project "FlaxSamples/MaterialsFeaturesTour" -build "Development.iOS" From ec1f4a9a4ba6884f5ec0a11bdc32fb47af5e7e1e Mon Sep 17 00:00:00 2001 From: Withaust Date: Sun, 30 Aug 2026 10:48:35 +0300 Subject: [PATCH 27/34] Fix headless editor & Roslyn analyzer include resolution --- Source/Editor/Windows/GameWindow.cs | 7 ++++--- .../Projects/VisualStudio/CSSDKProjectGenerator.cs | 4 +--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index d351147cb..4032e0aff 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -672,9 +672,10 @@ namespace FlaxEditor.Windows IsBorderless = false; Cursor = CursorType.Default; Screen.CursorLock = CursorLockMode.None; - if (Screen.MainWindow.IsMouseTracking) - Screen.MainWindow.EndTrackingMouse(); - RootControl.GameRoot.EndMouseCapture(); + var mainWindow = Screen.MainWindow; + if (mainWindow != null && mainWindow.IsMouseTracking) + mainWindow.EndTrackingMouse(); + RootControl.GameRoot?.EndMouseCapture(); } /// diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs index ffc95e047..e99b5b328 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs @@ -330,9 +330,7 @@ namespace Flax.Build.Projects.VisualStudio foreach (var analyzer in configuration.TargetBuildOptions.ScriptingAPI.Analyzers) { csProjectFileContent.AppendLine(string.Format(" ", configuration.Name)); - csProjectFileContent.AppendLine(string.Format(" ", Path.GetFileNameWithoutExtension(analyzer))); - csProjectFileContent.AppendLine(string.Format(" {0}", Utilities.MakePathRelativeTo(analyzer, projectDirectory).Replace('/', '\\'))); - csProjectFileContent.AppendLine(" "); + csProjectFileContent.AppendLine(string.Format(" ", Utilities.MakePathRelativeTo(analyzer, projectDirectory).Replace('/', '\\'))); csProjectFileContent.AppendLine(" "); } From b375b4073639f3be701c130fb478da62eafaffb6 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 30 Aug 2026 20:13:06 +0200 Subject: [PATCH 28/34] Minor fixes to nuget libs usage --- .../Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs index 9cca0dfa6..f158078f5 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs @@ -110,6 +110,11 @@ namespace Flax.Build.NativeCpp return libFolder; // Try to find nearest framework folder + if (Framework.StartsWith("netcoreapp")) + { + var version = System.Version.Parse(Framework.Substring(10)); + return string.Empty; + } if (Framework.StartsWith("net")) { var baseVersion = int.Parse(Framework.Substring(3, Framework.IndexOf('.') - 3)); @@ -144,7 +149,10 @@ namespace Flax.Build.NativeCpp var dlls = Directory.Exists(libFolder) ? Directory.GetFiles(libFolder, "*.dll", SearchOption.TopDirectoryOnly) : []; if (dlls.Length == 0) { - Log.Error($"Missing NuGet package \"{Name}, {Version}, {Framework}\" binaries (folder: {libFolder})"); + if (!File.Exists(Path.Combine(libFolder, "_._"))) // Skip error for packages without a binary (eg. Microsoft.NET.Test.Sdk) + { + Log.Error($"Missing NuGet package \"{Name}, {Version}, {Framework}\" binaries (folder: {libFolder})"); + } return string.Empty; } return dlls[0]; From 3fcad576a1620882309b50d0d19cffde3a7f1c36 Mon Sep 17 00:00:00 2001 From: David Svez Date: Sun, 23 Aug 2026 17:03:49 -0500 Subject: [PATCH 29/34] Fix GUI traversal during child removal --- Source/Engine/Tests/TestContainerControl.cs | 42 +++++++++++++++++++++ Source/Engine/UI/GUI/ContainerControl.cs | 28 +++++++------- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/Source/Engine/Tests/TestContainerControl.cs b/Source/Engine/Tests/TestContainerControl.cs index 3b3c4856a..ce2b4503e 100644 --- a/Source/Engine/Tests/TestContainerControl.cs +++ b/Source/Engine/Tests/TestContainerControl.cs @@ -26,6 +26,29 @@ namespace FlaxEngine.Tests } } + private sealed class RemovingControl : MyControl + { + private readonly Control[] _controlsToRemove; + + public RemovingControl(float x, float y, float width, float height, params Control[] controlsToRemove) + : base(x, y, width, height) + { + _controlsToRemove = controlsToRemove; + } + + public override void OnMouseEnter(Float2 location) + { + for (int i = 0; i < _controlsToRemove.Length; i++) + { + var control = _controlsToRemove[i]; + if (control.Parent == Parent) + control.Parent = null; + } + + base.OnMouseEnter(location); + } + } + [Test] public void TestChildren() { @@ -53,6 +76,25 @@ namespace FlaxEngine.Tests Assert.AreEqual(cc1.GetChildAt(new Vector2(15, 5)), cc2); Assert.AreEqual(cc1.GetChildAtRecursive(new Vector2(35, 25)), c3); } + + [Test] + public void TestMouseMoveAllowsChildrenRemoval() + { + var container = new MyContainerControl(0, 0, 100, 100); + var first = new MyControl(0, 0, 100, 100); + var second = new MyControl(0, 0, 100, 100); + var removing = new RemovingControl(0, 0, 100, 100, first, second); + container.AddChild(first); + container.AddChild(second); + container.AddChild(removing); + + // The top-most child removes multiple siblings during input dispatch. + // Traversal must not use the now-stale next index. + container.OnMouseMove(new Float2(50, 50)); + + Assert.AreEqual(1, container.ChildrenCount); + Assert.AreEqual(removing, container.GetChild(0)); + } } } #endif diff --git a/Source/Engine/UI/GUI/ContainerControl.cs b/Source/Engine/UI/GUI/ContainerControl.cs index 55fcecf80..aa0078c19 100644 --- a/Source/Engine/UI/GUI/ContainerControl.cs +++ b/Source/Engine/UI/GUI/ContainerControl.cs @@ -910,7 +910,7 @@ namespace FlaxEngine.GUI return false; } } - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible) @@ -928,7 +928,7 @@ namespace FlaxEngine.GUI public override void OnMouseEnter(Float2 location) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -948,7 +948,7 @@ namespace FlaxEngine.GUI public override void OnMouseMove(Float2 location) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -998,7 +998,7 @@ namespace FlaxEngine.GUI public override bool OnMouseWheel(Float2 location, float delta) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1019,7 +1019,7 @@ namespace FlaxEngine.GUI public override bool OnMouseDown(Float2 location, MouseButton button) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1040,7 +1040,7 @@ namespace FlaxEngine.GUI public override bool OnMouseUp(Float2 location, MouseButton button) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1061,7 +1061,7 @@ namespace FlaxEngine.GUI public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1096,7 +1096,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchEnter(Float2 location, int pointerId) { - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled && !child.IsTouchPointerOver(pointerId)) @@ -1114,7 +1114,7 @@ namespace FlaxEngine.GUI /// public override bool OnTouchDown(Float2 location, int pointerId) { - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1139,7 +1139,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchMove(Float2 location, int pointerId) { - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1168,7 +1168,7 @@ namespace FlaxEngine.GUI /// public override bool OnTouchUp(Float2 location, int pointerId) { - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled && child.IsTouchPointerOver(pointerId)) @@ -1250,7 +1250,7 @@ namespace FlaxEngine.GUI var result = base.OnDragEnter(ref location, data); // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1275,7 +1275,7 @@ namespace FlaxEngine.GUI var result = base.OnDragMove(ref location, data); // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) @@ -1333,7 +1333,7 @@ namespace FlaxEngine.GUI var result = base.OnDragDrop(ref location, data); // Check all children collisions with mouse and fire events for them - for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) + for (int i = _children.Count - 1; i >= 0 && i < _children.Count; i--) { var child = _children[i]; if (child.Visible && child.Enabled) From c727ffa70d7edfcf61cc87354ec5e90102f748e4 Mon Sep 17 00:00:00 2001 From: Roman Zhu Date: Sun, 30 Aug 2026 22:05:42 +0200 Subject: [PATCH 30/34] get rid of timestamp comparison for shaders, compare actual data # Conflicts: # Source/Engine/Graphics/Materials/MaterialShader.h # Source/Engine/Tests/Tests.Build.cs --- .../ShadersCompilation/ShadersCompilation.cpp | 44 ++++++++++++++++++- .../ShadersCompilation/ShadersCompilation.h | 10 +++++ Source/Engine/Tests/TestShaderSourceSync.cpp | 42 ++++++++++++++++++ Source/Engine/Tests/Tests.Build.cs | 1 + 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 Source/Engine/Tests/TestShaderSourceSync.cpp diff --git a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp index c8f68f007..e471bb47d 100644 --- a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp +++ b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp @@ -26,6 +26,8 @@ #if USE_EDITOR #define COMPILE_WITH_ASSETS_IMPORTER 1 // Hack to use shaders importing in this module #include "Engine/ContentImporters/AssetsImportingManager.h" +#include "Engine/Content/Storage/ContentStorageManager.h" +#include "Engine/Utilities/Encryption.h" #include "Engine/Platform/FileSystemWatcher.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Platform/File.h" @@ -495,6 +497,37 @@ String ShadersCompilation::CompactShaderPath(StringView path) #if USE_EDITOR +bool ShadersCompilation::IsShaderSourceAssetUpToDate(const StringView& sourcePath, const StringView& assetPath) +{ + PROFILE_CPU(); + StringAnsi source; + if (File::ReadAllText(sourcePath, source)) + return false; + if (!source.HasChars() || source[source.Length() - 1] != '\n') + source.Append('\n'); + + const auto storage = ContentStorageManager::GetStorage(assetPath); + AssetInitData data; + if (!storage + || storage->GetEntriesCount() != 1 + || storage->GetEntry(0).TypeName != Shader::TypeName + || storage->LoadAssetHeader(0, data) + || data.SerializedVersion != Shader::SerializedVersion) + return false; + + FlaxChunk* sourceChunk = data.Header.Chunks[SHADER_FILE_CHUNK_SOURCE]; + if (!sourceChunk || storage->LoadAssetChunk(sourceChunk) || !sourceChunk->Data.IsValid()) + return false; + + BytesContainer embeddedSource; + embeddedSource.Copy(sourceChunk->Data); + if (embeddedSource.Length() != source.Length() + 1) + return false; + Encryption::DecryptBytes(embeddedSource.Get(), embeddedSource.Length()); + embeddedSource.Get()[embeddedSource.Length() - 1] = 0; + return Platform::MemoryCompare(embeddedSource.Get(), source.Get(), source.Length()) == 0; +} + namespace { Array ShadersSourcesWatchers; @@ -511,6 +544,13 @@ namespace return result; } + bool ImportShaderIfChanged(const StringView& sourcePath, const StringView& assetPath, Guid& assetId) + { + if (ShadersCompilation::IsShaderSourceAssetUpToDate(sourcePath, assetPath)) + return false; + return AssetsImportingManager::Import(sourcePath, assetPath, assetId); + } + void OnWatcherShadersEvent(const String& path, FileSystemAction action) { if (action == FileSystemAction::Delete || !path.EndsWith(TEXT(".shader"))) @@ -533,7 +573,7 @@ namespace const String name = StringUtils::GetPathWithoutExtension(localPath); const String outputPath = shadersAssetsPath / name + ASSET_FILES_EXTENSION_WITH_DOT; Guid id = GetShaderAssetId(name); - AssetsImportingManager::ImportIfEdited(path, outputPath, id); + ImportShaderIfChanged(path, outputPath, id); } void RegisterShaderWatchers(const ProjectInfo* project, HashSet& projects) @@ -562,7 +602,7 @@ namespace const String name = StringUtils::GetPathWithoutExtension(localPath); const String outputPath = shadersAssetsPath / name + ASSET_FILES_EXTENSION_WITH_DOT; Guid id = GetShaderAssetId(name); - AssetsImportingManager::ImportIfEdited(path, outputPath, id); + ImportShaderIfChanged(path, outputPath, id); } } diff --git a/Source/Engine/ShadersCompilation/ShadersCompilation.h b/Source/Engine/ShadersCompilation/ShadersCompilation.h index b32c9f51a..af0a9ec61 100644 --- a/Source/Engine/ShadersCompilation/ShadersCompilation.h +++ b/Source/Engine/ShadersCompilation/ShadersCompilation.h @@ -47,6 +47,16 @@ public: // Compacts the full shader file path into portable format with project name prefix such as './/ShaderFile.hlsl'. static String CompactShaderPath(StringView path); +#if USE_EDITOR + /// + /// Checks whether a shader asset embeds the current source file contents. + /// + /// The shader source file path. + /// The shader asset file path. + /// True when the embedded source matches, otherwise false. + static bool IsShaderSourceAssetUpToDate(const StringView& sourcePath, const StringView& assetPath); +#endif + private: static ShaderCompiler* RequestCompiler(ShaderProfile profile, PlatformType platform); static void FreeCompiler(ShaderCompiler* compiler); diff --git a/Source/Engine/Tests/TestShaderSourceSync.cpp b/Source/Engine/Tests/TestShaderSourceSync.cpp new file mode 100644 index 000000000..96bad398b --- /dev/null +++ b/Source/Engine/Tests/TestShaderSourceSync.cpp @@ -0,0 +1,42 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#include "Engine/Core/ScopeExit.h" +#include "Engine/Core/Types/DataContainer.h" +#include "Engine/Engine/Globals.h" +#include "Engine/Platform/File.h" +#include "Engine/Platform/FileSystem.h" +#include "Engine/ShadersCompilation/ShadersCompilation.h" +#include + +#if COMPILE_WITH_SHADER_COMPILER && USE_EDITOR + +TEST_CASE("Shader source asset synchronization ignores timestamps") +{ + const String sourcePath = Globals::StartupFolder / TEXT("Source/Shaders/VolumetricFog.shader"); + const String assetPath = Globals::EngineContentFolder / TEXT("Shaders/VolumetricFog.flax"); + REQUIRE(FileSystem::FileExists(sourcePath)); + REQUIRE(FileSystem::FileExists(assetPath)); + CHECK(ShadersCompilation::IsShaderSourceAssetUpToDate(sourcePath, assetPath)); + + const String tempRoot = Globals::TemporaryFolder / (TEXT("ShaderSourceSync-") + Guid::New().ToString(Guid::FormatType::N)); + REQUIRE(!FileSystem::CreateDirectory(tempRoot)); + SCOPE_EXIT + { + FileSystem::DeleteDirectory(tempRoot, true); + }; + + DataContainer assetData; + StringAnsi modifiedSource; + REQUIRE(!File::ReadAllBytes(assetPath, assetData)); + REQUIRE(!File::ReadAllText(sourcePath, modifiedSource)); + modifiedSource.Append("// Deliberately different source\n"); + + const String tempSourcePath = tempRoot / TEXT("VolumetricFog.shader"); + const String tempAssetPath = tempRoot / TEXT("VolumetricFog.flax"); + REQUIRE(!File::WriteAllBytes(tempSourcePath, modifiedSource.Get(), modifiedSource.Length())); + REQUIRE(!File::WriteAllBytes(tempAssetPath, assetData.Get(), assetData.Length())); + CHECK_FALSE(FileSystem::GetFileLastEditTime(tempSourcePath) > FileSystem::GetFileLastEditTime(tempAssetPath)); + CHECK_FALSE(ShadersCompilation::IsShaderSourceAssetUpToDate(tempSourcePath, tempAssetPath)); +} + +#endif diff --git a/Source/Engine/Tests/Tests.Build.cs b/Source/Engine/Tests/Tests.Build.cs index 1056d1002..d55456176 100644 --- a/Source/Engine/Tests/Tests.Build.cs +++ b/Source/Engine/Tests/Tests.Build.cs @@ -21,6 +21,7 @@ public class Tests : EngineModule base.Setup(options); options.PrivateDependencies.Add("ModelTool"); + options.PrivateDependencies.Add("ShadersCompilation"); } /// From 4deb754fa1bb369e6d83f919c9620267db9bb975 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 30 Aug 2026 22:05:56 +0200 Subject: [PATCH 31/34] Update shaders --- Content/Shaders/GBuffer.flax | 4 ++-- Content/Shaders/GlobalSignDistanceField.flax | 4 ++-- Content/Shaders/ProbesFilter.flax | 4 ++-- Content/Shaders/SSR.flax | 4 ++-- Content/Shaders/VolumetricFog.flax | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Content/Shaders/GBuffer.flax b/Content/Shaders/GBuffer.flax index 0d2d9b4a0..08b18514a 100644 --- a/Content/Shaders/GBuffer.flax +++ b/Content/Shaders/GBuffer.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfbd11dc6e19b6fc8837d7f5275524baa333f24184395358b945c9cbf95b2dda -size 3134 +oid sha256:bc44c3fea80b45c14e6aca0d8dbc6623562072a9a9c0984f4cb97ffe88c82623 +size 3131 diff --git a/Content/Shaders/GlobalSignDistanceField.flax b/Content/Shaders/GlobalSignDistanceField.flax index 01dae76b1..9dc013e92 100644 --- a/Content/Shaders/GlobalSignDistanceField.flax +++ b/Content/Shaders/GlobalSignDistanceField.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0506a5485a0107f67714ceb8c1714f18cb5718bacfde36fad91d53ea3cb60de9 -size 14044 +oid sha256:c9c460aa86ae8d4d8fc5abf6ab5c1339d736178f21586e69c774d46bdcc265f1 +size 14041 diff --git a/Content/Shaders/ProbesFilter.flax b/Content/Shaders/ProbesFilter.flax index 679eac27b..e7223f64e 100644 --- a/Content/Shaders/ProbesFilter.flax +++ b/Content/Shaders/ProbesFilter.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bbe90799accc93fabdc900df37bf762132037eeaff17f5731f379b6b3d017d2b -size 2033 +oid sha256:4f2cb35e9fe3990631cf831a874add31964e5570fe2817f099905d1ecde67eab +size 2031 diff --git a/Content/Shaders/SSR.flax b/Content/Shaders/SSR.flax index f286e8f45..eff8fa529 100644 --- a/Content/Shaders/SSR.flax +++ b/Content/Shaders/SSR.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e607712c48bd2484fb5bce5fa3f5aa2a4a05d0c03e0f24483b7eb1463d954263 -size 11592 +oid sha256:95928c48d4140f7c01738b18511a0bf938350d693ccbf8397b999cb1917bb673 +size 11587 diff --git a/Content/Shaders/VolumetricFog.flax b/Content/Shaders/VolumetricFog.flax index 2a2bc2a17..68aa4ab1c 100644 --- a/Content/Shaders/VolumetricFog.flax +++ b/Content/Shaders/VolumetricFog.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8ed390d9a555003ff888c3fce208c6a3d413aa55f230566eb3f6cfe6d5f90df -size 13195 +oid sha256:b198c0acb87e26759e258e82bc93e678b78b4982d0c8326aadce8e8af0fc6d68 +size 13193 From ac4737a4d511cc637787ef4c988a25c26c443796 Mon Sep 17 00:00:00 2001 From: Roman Zhu Date: Fri, 28 Aug 2026 15:01:26 +0200 Subject: [PATCH 32/34] Silence successful content handle releases --- Source/Engine/Content/Storage/ContentStorageManager.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Content/Storage/ContentStorageManager.cpp b/Source/Engine/Content/Storage/ContentStorageManager.cpp index 33a16e031..5656fd647 100644 --- a/Source/Engine/Content/Storage/ContentStorageManager.cpp +++ b/Source/Engine/Content/Storage/ContentStorageManager.cpp @@ -122,11 +122,8 @@ FlaxStorageReference ContentStorageManager::EnsureAccess(const StringView& path) // Note: because we want to create new storage package it may exists. // So let's check if any storage container is referencing that location and try to close it. auto storage = TryGetStorage(path); - if (storage && storage->IsLoaded()) - { - LOG(Info, "File \'{0}\' is in use. Trying to release handle to it.", path); - storage->CloseFileHandles(); - } + if (storage && storage->IsLoaded() && storage->CloseFileHandles()) + LOG(Warning, "Cannot release content storage handle for '{0}'.", path); return storage; } From c393db6fdc5ccc7fdbd8bd336b18fb33b9bf34ec Mon Sep 17 00:00:00 2001 From: Roman Zhu Date: Sun, 30 Aug 2026 22:13:27 +0200 Subject: [PATCH 33/34] Fix asset file race https://github.com/RomanZhu/FlaxEngine/commit/71bad36469bc6cdd31384f429bdc05c251e0eb91 --- Source/Engine/Content/Storage/FlaxStorage.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index 4eaea48f8..b7be69468 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -1438,10 +1438,6 @@ FileReadStream* FlaxStorage::OpenFile() bool FlaxStorage::CloseFileHandles() { - // Guard the whole process so if new thread wants to lock the chunks will need to wait for this to end - Platform::InterlockedIncrement(&_isUnloadingData); - SCOPE_EXIT{ Platform::InterlockedDecrement(&_isUnloadingData); }; - if (Platform::AtomicRead(&_chunksLock) == 0 && Platform::AtomicRead(&_files) == 0) return false; // Early out when no files are opened PROFILE_CPU(); @@ -1469,7 +1465,13 @@ bool FlaxStorage::CloseFileHandles() } } } - waitTime = 100; + + // Guard the whole process so if new thread wants to lock the chunks will need to wait for this to end + Platform::InterlockedIncrement(&_isUnloadingData); + SCOPE_EXIT{ Platform::InterlockedDecrement(&_isUnloadingData); }; + + // Wait for chunks lock again (with larger timeout for longer tasks) + waitTime = 1000; while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0) Platform::Sleep(1); if (Platform::AtomicRead(&_chunksLock) != 0) From a215381d4b816146b31ec80be2ebacc074e3dbd2 Mon Sep 17 00:00:00 2001 From: Roman Zhu Date: Fri, 28 Aug 2026 21:27:37 +0200 Subject: [PATCH 34/34] tsk tsk no crash a simp --- .../Tools/ModelTool/ModelTool.Assimp.cpp | 70 ++++++++++++++----- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index 4509d287d..bdc768704 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -20,26 +20,29 @@ #include #include #include -#include #include #include -class AssimpLogStream : public Assimp::LogStream +class AssimpLogger final : public Assimp::Logger { public: - AssimpLogStream() + AssimpLogger() + : Logger(NORMAL) { - Assimp::DefaultLogger::create(""); - Assimp::DefaultLogger::get()->attachStream(this); } - ~AssimpLogStream() + bool attachStream(Assimp::LogStream*, unsigned int) override { - Assimp::DefaultLogger::get()->detachStream(this); - Assimp::DefaultLogger::kill(); + return false; } - void write(const char* message) override + bool detachStream(Assimp::LogStream*, unsigned int) override + { + return false; + } + +private: + static void Write(const Char* type, const char* message) { String s(message); if (s.Length() <= 0) @@ -52,10 +55,49 @@ public: else if (c >= 255) c = '?'; } - LOG(Info, "[Assimp]: {0}", s); + LOG(Info, "[Assimp]: {0}: {1}", type, s); + } + + void OnDebug(const char* message) override + { + if (m_Severity >= DEBUGGING) + Write(TEXT("Debug"), message); + } + + void OnVerboseDebug(const char* message) override + { + if (m_Severity >= VERBOSE) + Write(TEXT("Debug"), message); + } + + void OnInfo(const char* message) override + { + Write(TEXT("Info"), message); + } + + void OnWarn(const char* message) override + { + Write(TEXT("Warn"), message); + } + + void OnError(const char* message) override + { + Write(TEXT("Error"), message); } }; +Assimp::Logger* GetAssimpLogger() +{ + static Assimp::Logger* logger = []() + { + auto result = new AssimpLogger(); + Assimp::DefaultLogger::set(result); + LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision()); + return result; + }(); + return logger; +} + Float2 ToFloat2(const aiVector2D& v) { return Float2(v.x, v.y); @@ -148,7 +190,6 @@ struct AssimpBone struct AssimpImporterData { Assimp::Importer AssimpImporter; - AssimpLogStream AssimpLogStream; const String Path; const aiScene* Scene = nullptr; const ModelTool::Options& Options; @@ -699,12 +740,7 @@ void ImportAnimation(int32 index, ModelData& data, AssimpImporterData& importerD bool ModelTool::ImportDataAssimp(const String& path, ModelData& data, Options& options, String& errorMsg) { - static bool AssimpInited = false; - if (!AssimpInited) - { - AssimpInited = true; - LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision()); - } + GetAssimpLogger(); bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry); bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations); AssimpImporterData context(path, options);