Skip to content

Commit 930092f

Browse files
Merge pull request #278 from xZekro51/build-fixes-and-separate-thread
Moved build to separate thread, improvement to ALC by better sweeping and loose-ends clearing
2 parents 688ebca + e6f1311 commit 930092f

40 files changed

Lines changed: 789 additions & 110 deletions

Prowl.Editor/AssetsDatabase/EditorAssetDatabase.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,41 @@ public string[] GetAllAssetPaths()
695695
/// <summary>Get an already-loaded asset from memory without triggering import. Returns null if not loaded.</summary>
696696
public EngineObject? GetLoadedAsset(Guid guid) => _loadedAssets.GetValueOrDefault(guid);
697697

698+
/// <summary>
699+
/// Clear the cache for <see cref="_loadedAssets"/> on assembly reload so that scenes/prefabs that might hold
700+
/// user-defined scripts won't stop the ALC from reloading
701+
/// </summary>
702+
[OnAssemblyUnload]
703+
internal static void ClearScenesAndPrefabForReload()
704+
{
705+
var db = Instance;
706+
if (db == null) return;
707+
708+
foreach (var kv in db._loadedAssets.ToArray())
709+
{
710+
EngineObject? asset = kv.Value;
711+
if (asset is null) continue;
712+
713+
bool sensitive = asset is Runtime.Resources.Scene
714+
|| asset is Runtime.Resources.PrefabAsset
715+
|| asset.GetType().Assembly.IsCollectible;
716+
717+
if (!sensitive) continue;
718+
719+
if (db._loadedAssets.TryRemove(kv.Key, out var removed))
720+
{
721+
try
722+
{
723+
removed?.Dispose();
724+
}
725+
catch(Exception e)
726+
{
727+
Debug.LogException(e);
728+
}
729+
}
730+
}
731+
}
732+
698733
/// <summary>Load a cached thumbnail for an asset. Returns (width, height, pixels) or null.</summary>
699734
public (int width, int height, byte[] pixels)? LoadThumbnail(Guid guid) => ThumbnailGenerator.LoadThumbnail(guid, _project.ThumbnailsPath);
700735

Prowl.Editor/AssetsDatabase/Importers/ImporterRegistry.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,18 @@ public static class ImporterRegistry
1414
private static readonly Dictionary<string, Type> _nameToImporter = new(StringComparer.OrdinalIgnoreCase);
1515
private static bool _initialized;
1616

17+
[Runtime.OnAssemblyLoad]
1718
public static void Reinitialize() { _initialized = false; Initialize(); }
1819

20+
/// <summary>Drop cached importer type maps so the script AssemblyLoadContext can be collected.</summary>
21+
[Runtime.OnAssemblyUnload]
22+
public static void ClearCache()
23+
{
24+
_initialized = false;
25+
_extensionToImporter.Clear();
26+
_nameToImporter.Clear();
27+
}
28+
1929
public static void Initialize()
2030
{
2131
if (_initialized) return;

Prowl.Editor/AssetsDatabase/Thumbnails/ThumbnailGeneratorRegistry.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,17 @@ public static class ThumbnailGeneratorRegistry
4141
private static readonly Dictionary<Type, IThumbnailGenerator> _generators = new();
4242
private static bool _initialized;
4343

44+
[Runtime.OnAssemblyLoad]
4445
public static void Reinitialize() { _initialized = false; Initialize(); }
4546

47+
/// <summary>Drop cached generators (keyed by user <see cref="Type"/>) so the script AssemblyLoadContext can be collected.</summary>
48+
[Runtime.OnAssemblyUnload]
49+
public static void ClearCache()
50+
{
51+
_initialized = false;
52+
_generators.Clear();
53+
}
54+
4655
public static void Initialize()
4756
{
4857
if (_initialized) return;

Prowl.Editor/Core/EditorApplication.cs

Lines changed: 213 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -427,10 +427,9 @@ public override void BeginGui(Paper paper)
427427

428428
// Load user script assemblies and re-register all types
429429
ScriptAssemblyManager.LoadAssemblies(Project.Current);
430-
ReinitializeRegistries();
431430

432-
// Load project settings
433-
ProjectSettingsRegistry.OnProjectOpened();
431+
// ReinitializeRegistries() runs the [OnAssemblyLoad] hooks, which include the project-settings reload.
432+
ReinitializeRegistries();
434433

435434
// Restore layout from project (or use default)
436435
var savedLayout = LoadDockLayout();
@@ -1116,6 +1115,30 @@ private void ScanAndRegisterPanels()
11161115
return FindInNode(node.ChildA, panelType) ?? FindInNode(node.ChildB, panelType);
11171116
}
11181117

1118+
/// <summary>Enumerate every open panel across the docked tree and all floating windows.</summary>
1119+
private IEnumerable<DockPanel> EnumerateAllPanels()
1120+
{
1121+
foreach (var p in EnumerateNodePanels(_dockSpace.Root))
1122+
yield return p;
1123+
foreach (var fw in _dockSpace.FloatingWindows)
1124+
foreach (var p in EnumerateNodePanels(fw.Node))
1125+
yield return p;
1126+
}
1127+
1128+
private static IEnumerable<DockPanel> EnumerateNodePanels(DockNode? node)
1129+
{
1130+
if (node == null) yield break;
1131+
if (node.IsLeaf)
1132+
{
1133+
if (node.Tabs != null)
1134+
foreach (var tab in node.Tabs)
1135+
yield return tab;
1136+
yield break;
1137+
}
1138+
foreach (var p in EnumerateNodePanels(node.ChildA)) yield return p;
1139+
foreach (var p in EnumerateNodePanels(node.ChildB)) yield return p;
1140+
}
1141+
11191142
/// <summary>
11201143
/// Open a panel. If it's already open, focus it. Otherwise create a new instance as a floating window.
11211144
/// </summary>
@@ -1267,42 +1290,199 @@ private void RegisterMenus()
12671290
// Script Compilation
12681291
// ================================================================
12691292

1270-
/// <summary>Called by ScriptAssemblyManager after hot-reload to re-scan all registries.</summary>
1271-
public void ReinitializeAfterReload() => ReinitializeRegistries();
1293+
/// <summary>
1294+
/// Called by <see cref="ScriptAssemblyManager"/> right after the new script assemblies are
1295+
/// loaded. Re-scans every registry against the fresh assemblies and reloads project settings.
1296+
/// </summary>
1297+
public void ReinitializeAfterReload()
1298+
{
1299+
ReinitializeRegistries();
1300+
}
12721301

1273-
private void ReinitializeRegistries()
1302+
/// <summary>
1303+
/// Drops every strong reference the editor holds into the script <see cref="System.Runtime.Loader.AssemblyLoadContext"/>
1304+
/// so it can actually be collected when unloaded. This is the counterpart to
1305+
/// <see cref="ReinitializeAfterReload"/>: tear everything down here, rebuild it there.
1306+
///
1307+
/// Anything that survives this call and transitively reaches a user type (a live instance, a
1308+
/// <see cref="Type"/> handle, a delegate bound to user code, a <see cref="FieldInfo"/>) pins
1309+
/// the old context and forces a full editor restart instead of a hot-reload.
1310+
/// </summary>
1311+
public void ReleaseScriptReferences()
12741312
{
1313+
CaptureSelectionForReload();
1314+
1315+
// 1. Live object graph: the scene's GameObjects hold user MonoBehaviour instances.
1316+
// The scene was already serialized to disk by SaveSceneForRestart().
1317+
Selection.Clear();
1318+
Undo.Clear();
1319+
Runtime.Resources.Scene.Unload();
1320+
1321+
// 1b. Long-lived editor panels cache scene objects (e.g. the Inspector's last target,
1322+
// the Hierarchy's drag target). Let each drop its references before the unload.
1323+
if (_dockSpace != null)
1324+
foreach (var panel in EnumerateAllPanels())
1325+
if (panel is IScriptReloadCleanup cleanup)
1326+
try { cleanup.OnScriptReloadCleanup(); } catch { }
1327+
1328+
// Release Paper callbacks as they might otherwise pin ALC types across a reload.
1329+
ReleasePaperRetainedCallbacks();
1330+
1331+
// 2. Play-mode leftovers (normally empty outside play mode; cleared defensively).
1332+
_savedEditorScene = null;
1333+
_savedEditorTime = null;
1334+
MenuRegistry.Clear();
12751335
_registeredPanels.Clear();
1276-
ScanAndRegisterPanels();
1277-
InitializeOnLoadRegistry.Reinitialize();
1278-
PropertyEditorRegistry.Reinitialize();
1279-
CustomEditorRegistry.Reinitialize();
1280-
GraphTools.NodeRendererRegistry.Reinitialize();
1281-
GraphTools.NodePreviewRegistry.Reinitialize();
1282-
Runtime.GraphTools.GraphValidatorRegistry.Reinitialize();
1283-
Inspector.AssetImporterEditorRegistry.Reinitialize();
1284-
GUI.Popups.AddComponentPopup.Reinitialize();
1285-
Importers.ImporterRegistry.Reinitialize();
1286-
ProjectSettingsRegistry.Reinitialize();
1287-
CreateAssetMenuRegistry.Reinitialize();
1288-
ShaderTypeCreateMenu.Register();
1289-
ThumbnailGeneratorRegistry.Reinitialize();
1290-
SceneDropHandlerRegistry.Reinitialize();
1291-
CreateGameObjectMenuRegistry.Reinitialize();
1292-
FileIconRegistry.Reinitialize();
1293-
AssetDoubleClickRegistry.Reinitialize();
1294-
ScriptTemplateRegistry.Reinitialize();
1295-
1296-
// Re-register Window menu items for any new panels from user assemblies
1297-
foreach (var (type, path) in _registeredPanels)
1336+
1337+
// 3. The Echo serializer cache lives in an external package so we can't call OnAssemblyUnload there.
1338+
Echo.Serializer.ClearCache();
1339+
1340+
// 4. Everything tagged [OnAssemblyUnload]
1341+
ScriptReloadCallbacks.InvokeAssemblyUnload();
1342+
}
1343+
1344+
private void ReleasePaperRetainedCallbacks()
1345+
{
1346+
try
12981347
{
1299-
var capturedType = type;
1300-
MenuRegistry.Register($"Window/{path}", () => OpenPanel(capturedType),
1301-
isChecked: () => IsPanelOpen(capturedType));
1348+
var paper = PaperInstance;
1349+
if (paper == null) return;
1350+
1351+
Type t = paper.GetType();
1352+
const BindingFlags BF = BindingFlags.NonPublic | BindingFlags.Instance;
1353+
1354+
if (t.GetField("_elements", BF)?.GetValue(paper) is not Array elements) return;
1355+
1356+
int count = t.GetField("_elementCount", BF)?.GetValue(paper) is int c ? c : 0;
1357+
count = Math.Clamp(count, 0, elements.Length);
1358+
if (count < elements.Length)
1359+
Array.Clear(elements, count, elements.Length - count);
1360+
}
1361+
catch (Exception ex)
1362+
{
1363+
Runtime.Debug.LogWarning($"[EditorApplication] Could not reset PaperUI retained callbacks: {ex.Message}");
13021364
}
1365+
}
13031366

1304-
// Re-register GameObject menu items for any new creators from user assemblies
1305-
CreateGameObjectMenuRegistry.RegisterMenuBarItems();
1367+
// ================================================================
1368+
// Selection preserve/restore across a hot-reload
1369+
// ================================================================
1370+
1371+
private List<SelectionToken>? _reloadSelection;
1372+
private SelectionToken _reloadActive;
1373+
private bool _hasReloadActive;
1374+
1375+
/// <summary>
1376+
/// Snapshot the current selection as identifier tokens (called before the selection is cleared).
1377+
/// </summary>
1378+
private void CaptureSelectionForReload()
1379+
{
1380+
_reloadSelection = new List<SelectionToken>();
1381+
_hasReloadActive = false;
1382+
1383+
foreach (var obj in Selection.Selected)
1384+
{
1385+
if (!TryMakeSelectionToken(obj, out var token))
1386+
continue;
1387+
1388+
_reloadSelection.Add(token);
1389+
if (ReferenceEquals(obj, Selection.ActiveObject))
1390+
{
1391+
_reloadActive = token;
1392+
_hasReloadActive = true;
1393+
}
1394+
}
1395+
}
1396+
1397+
/// <summary>
1398+
/// Tries to create a selection token to then restore the selection after script reload.
1399+
/// </summary>
1400+
private static bool TryMakeSelectionToken(object obj, out SelectionToken token)
1401+
{
1402+
switch (obj)
1403+
{
1404+
// Scene GameObject - restore by stable scene identifier.
1405+
case GameObject go:
1406+
token = new SelectionToken(SelKind.GameObject, go.Identifier, Guid.Empty, "", "", false);
1407+
return true;
1408+
// Scene component - restore by owning GameObject + component identifier.
1409+
case MonoBehaviour mb when mb.GameObject.IsValid():
1410+
token = new SelectionToken(SelKind.Component, mb.GameObject.Identifier, mb.Identifier, "", "", false);
1411+
return true;
1412+
// Project asset - restore by AssetID via the asset database.
1413+
case EngineObject eo when eo.AssetID != Guid.Empty:
1414+
token = new SelectionToken(SelKind.Asset, eo.AssetID, Guid.Empty, "", "", false);
1415+
return true;
1416+
// Project browser item - identifier-only, rebuilt from its path/guid.
1417+
case ContentItem ci:
1418+
token = new SelectionToken(SelKind.Content, ci.Guid, Guid.Empty, ci.RelativePath, ci.Name, ci.IsFolder);
1419+
return true;
1420+
default:
1421+
token = default;
1422+
return false;
1423+
}
1424+
}
1425+
1426+
/// <summary>
1427+
/// Re-resolve the captured selection tokens against the freshly reloaded scene/assets and re-select them.
1428+
/// </summary>
1429+
public void RestoreSelectionAfterReload()
1430+
{
1431+
if (_reloadSelection == null)
1432+
return;
1433+
1434+
var tokens = _reloadSelection;
1435+
_reloadSelection = null;
1436+
1437+
Selection.Clear();
1438+
object? active = null;
1439+
1440+
foreach (var token in tokens)
1441+
{
1442+
object? resolved = ResolveSelectionToken(token);
1443+
if (resolved == null)
1444+
continue;
1445+
1446+
Selection.AddToSelection(resolved);
1447+
if (_hasReloadActive && token.Equals(_reloadActive))
1448+
active = resolved;
1449+
}
1450+
1451+
if (active != null)
1452+
Selection.ActiveObject = active;
1453+
1454+
_hasReloadActive = false;
1455+
}
1456+
1457+
private static object? ResolveSelectionToken(SelectionToken token)
1458+
{
1459+
switch (token.Kind)
1460+
{
1461+
case SelKind.GameObject:
1462+
return Runtime.Resources.Scene.Current?.FindObjectByIdentifier<GameObject>(token.Id);
1463+
case SelKind.Component:
1464+
return Runtime.Resources.Scene.Current?.FindObjectByIdentifier<GameObject>(token.Id)?.GetComponentByIdentifier(token.CompId);
1465+
case SelKind.Asset:
1466+
return Runtime.AssetDatabase.Get(token.Id);
1467+
case SelKind.Content:
1468+
// ContentItem compares by Guid + RelativePath, so a rebuilt instance re-selects the same item.
1469+
return new ContentItem { Guid = token.Id, RelativePath = token.Path, Name = token.Name, IsFolder = token.IsFolder };
1470+
default:
1471+
return null;
1472+
}
1473+
}
1474+
1475+
private void ReinitializeRegistries()
1476+
{
1477+
// Panel scan is an editor-instance step (needed before the menu rebuild reads the panel list).
1478+
_registeredPanels.Clear();
1479+
ScanAndRegisterPanels();
1480+
1481+
// Run every [OnAssemblyLoad] hook
1482+
ScriptReloadCallbacks.InvokeAssemblyLoad();
1483+
1484+
MenuRegistry.Clear();
1485+
RegisterMenus();
13061486
}
13071487

13081488
public void RestoreAutoSavedScene(string path)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// This file is part of the Prowl Game Engine
2+
// Licensed under the MIT License. See the LICENSE file in the project root for details.
3+
4+
namespace Prowl.Editor.Core;
5+
6+
/// <summary>
7+
/// Implemented by long-lived editor objects (typically dock panels) that cache references to
8+
/// scene objects or user-script types. Hot-reload tears the script
9+
/// <see cref="System.Runtime.Loader.AssemblyLoadContext"/> down, and any surviving reference
10+
/// into it pins the old context and blocks the unload.
11+
///
12+
/// <see cref="EditorApplication.ReleaseScriptReferences"/> calls <see cref="OnScriptReloadCleanup"/>
13+
/// on every open panel right before the unload. Implementations must drop their cached
14+
/// scene/user references (set fields to null, clear collections). They do NOT need to dispose
15+
/// themselves — the panel instance lives on; only its references into the dying context go away.
16+
/// </summary>
17+
public interface IScriptReloadCleanup
18+
{
19+
void OnScriptReloadCleanup();
20+
}

0 commit comments

Comments
 (0)