Skip to content

Commit 4e8adc5

Browse files
Refactored Asset Thumbnails, Fixed issue with some thumbnails not updating in places
1 parent 348aa6f commit 4e8adc5

6 files changed

Lines changed: 73 additions & 95 deletions

File tree

Prowl.Editor/AssetsDatabase/EditorAssetDatabase.cs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using Prowl.Editor.Thumbnails;
1010
using Prowl.Editor.Importers;
1111
using Prowl.Runtime;
12-
using Prowl.Editor.GUI.Panels;
1312
using Prowl.Editor.Projects.Scripting;
1413
using Prowl.Editor.Projects;
1514

@@ -30,6 +29,9 @@ public class EditorAssetDatabase : IAssetDatabase
3029
private readonly Dictionary<string, Guid> _pathToGuid = new(StringComparer.OrdinalIgnoreCase);
3130
private readonly ConcurrentDictionary<Guid, EngineObject> _loadedAssets = new();
3231
private readonly ConcurrentDictionary<Guid, (Guid parentGuid, int index)> _subAssetIndex = new();
32+
// GPU-uploaded thumbnail cache. Main-thread-only (texture creation isn't thread-safe), same
33+
// as _pathToGuid every UI that shows asset thumbnails shares this instead of keeping its own.
34+
private readonly Dictionary<Guid, Runtime.Resources.Texture2D?> _thumbnailTextures = new();
3335
// Per-thread re-entrancy guard that breaks dependency/sub-asset cycles during deserialize.
3436
[ThreadStatic] private static HashSet<Guid>? _loadingStack;
3537
// Serializes the deserialize body so concurrent loads of the same asset collapse onto one.
@@ -628,6 +630,7 @@ private bool RunImport(AssetEntry entry)
628630
_subAssetIndex.TryRemove(oldGuid, out _);
629631
if (_loadedAssets.TryRemove(oldGuid, out var oldObj)) oldObj?.Dispose();
630632
ThumbnailGenerator.DeleteThumbnail(oldGuid, _project.ThumbnailsPath);
633+
InvalidateThumbnailTexture(oldGuid);
631634
string oldCache = GetCachePath(oldGuid);
632635
if (File.Exists(oldCache)) try { File.Delete(oldCache); } catch { }
633636
}
@@ -643,10 +646,12 @@ private bool RunImport(AssetEntry entry)
643646
// Update dependency graph
644647
_dependencies.SetDependencies(entry.Guid, ctx.Dependencies);
645648

646-
// Queue thumbnail generation (lazy, one per frame)
649+
// Queue thumbnail generation (lazy, one per frame) and drop any cached GPU texture for
650+
// the old content so the next access rebuilds it from the freshly generated thumbnail.
647651
{
648652
string? sourceFile = ctx.MainAsset is Runtime.Resources.Texture2D ? absolutePath : null;
649653
ThumbnailGenerator.Enqueue(entry.Guid, ctx.MainAsset, sourceFile);
654+
InvalidateThumbnailTexture(entry.Guid);
650655
}
651656

652657
return true;
@@ -810,6 +815,57 @@ internal static void ClearScenesAndPrefabForReload()
810815
/// <summary>Load a cached thumbnail for an asset. Returns (width, height, pixels) or null.</summary>
811816
public (int width, int height, byte[] pixels)? LoadThumbnail(Guid guid) => ThumbnailGenerator.LoadThumbnail(guid, _project.ThumbnailsPath);
812817

818+
/// <summary>
819+
/// Resolve an asset GUID to its cached GPU thumbnail texture, building it from the on-disk
820+
/// pixel cache (<see cref="LoadThumbnail"/>) on first use. Returns null when no thumbnail
821+
/// exists yet. Shared by every UI that shows asset thumbnails (Project panel, asset pickers,
822+
/// Terrain layer tiles, etc.) so there's one GPU copy per asset, invalidated automatically
823+
/// whenever this database reimports or deletes that asset.
824+
/// </summary>
825+
public Runtime.Resources.Texture2D? GetThumbnailTexture(Guid guid)
826+
{
827+
if (guid == Guid.Empty) return null;
828+
829+
if (_thumbnailTextures.TryGetValue(guid, out var cached))
830+
return cached;
831+
832+
var thumb = LoadThumbnail(guid);
833+
if (thumb == null) return null;
834+
835+
try
836+
{
837+
var (w, h, pixels) = thumb.Value;
838+
var tex = new Runtime.Resources.Texture2D((uint)w, (uint)h, false, TextureImageFormat.Color4b);
839+
tex.SetData<byte>(pixels);
840+
tex.SetTextureFilters(TextureMin.Linear, TextureMag.Linear);
841+
_thumbnailTextures[guid] = tex;
842+
return tex;
843+
}
844+
catch
845+
{
846+
_thumbnailTextures[guid] = null;
847+
return null;
848+
}
849+
}
850+
851+
/// <summary>Dispose and drop a single cached thumbnail texture so it's rebuilt from disk on next access.</summary>
852+
public void InvalidateThumbnailTexture(Guid guid)
853+
{
854+
if (_thumbnailTextures.TryGetValue(guid, out var tex))
855+
{
856+
tex?.Dispose();
857+
_thumbnailTextures.Remove(guid);
858+
}
859+
}
860+
861+
/// <summary>Dispose and clear every cached thumbnail texture (e.g. after the thumbnail size setting changes).</summary>
862+
public void ClearThumbnailTextureCache()
863+
{
864+
foreach (var tex in _thumbnailTextures.Values)
865+
tex?.Dispose();
866+
_thumbnailTextures.Clear();
867+
}
868+
813869
// ================================================================
814870
// Asset CRUD
815871
// ================================================================
@@ -962,12 +1018,14 @@ public void DeleteAsset(string relativePath)
9621018
var entry = _guidToEntry.TryGetValue(guid, out var e) ? e : null;
9631019
DisposeAndRemove(guid);
9641020
ThumbnailGenerator.DeleteThumbnail(guid, _project.ThumbnailsPath);
1021+
InvalidateThumbnailTexture(guid);
9651022
if (entry?.SubAssets != null)
9661023
foreach (var sub in entry.SubAssets)
9671024
{
9681025
DisposeAndRemove(sub.Guid);
9691026
_subAssetIndex.TryRemove(sub.Guid, out _);
9701027
ThumbnailGenerator.DeleteThumbnail(sub.Guid, _project.ThumbnailsPath);
1028+
InvalidateThumbnailTexture(sub.Guid);
9711029

9721030
// Clean sub-asset cache file
9731031
string subCachePath = GetCachePath(sub.Guid);
@@ -1192,14 +1250,14 @@ public void Reimport(Guid guid)
11921250
foreach (var sub in entry.SubAssets)
11931251
DisposeAndRemove(sub.Guid);
11941252

1195-
// Clear old thumbnails and invalidate UI cache
1253+
// Clear old thumbnails and invalidate the cached GPU texture
11961254
ThumbnailGenerator.DeleteThumbnail(guid, _project.ThumbnailsPath);
1197-
ProjectPanel.InvalidateThumbnail(guid);
1255+
InvalidateThumbnailTexture(guid);
11981256
if (entry.SubAssets != null)
11991257
foreach (var sub in entry.SubAssets)
12001258
{
12011259
ThumbnailGenerator.DeleteThumbnail(sub.Guid, _project.ThumbnailsPath);
1202-
ProjectPanel.InvalidateThumbnail(sub.Guid);
1260+
InvalidateThumbnailTexture(sub.Guid);
12031261
}
12041262

12051263
entry.NeedsReimport = true;
@@ -1451,6 +1509,8 @@ public void Dispose()
14511509
_watcher?.Dispose();
14521510
_watcher = null;
14531511

1512+
ClearThumbnailTextureCache();
1513+
14541514
// Clear the global registrations if they still point at this instance, so a torn-down
14551515
// database (e.g. between tests) doesn't leave dangling statics behind.
14561516
if (Instance == this) Instance = null;

Prowl.Editor/Core/EditorApplication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1680,7 +1680,7 @@ public void SaveEditorWindowState()
16801680
public void ClearEditorCache()
16811681
{
16821682
try { Thumbnails.ThumbnailGenerator.DeleteAll(); } catch { }
1683-
GUI.Panels.ProjectPanel.ClearThumbnailCache();
1683+
EditorAssetDatabase.Instance?.ClearThumbnailTextureCache();
16841684
try
16851685
{
16861686
if (Project.Current != null && System.IO.File.Exists(Project.Current.EditorStatePath))

Prowl.Editor/GUI/CustomEditors/TerrainEditor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ private void DrawPaint(Paper paper, string id, Prowl.Scribe.FontFile font, Terra
246246
paper.Box($"{id}_l{i}_bar").Width(2).Height(UnitValue.StretchOne)
247247
.Margin(0, 0, UnitValue.Stretch(), UnitValue.Stretch())
248248
.BackgroundColor(EditorTheme.Accent).IsNotInteractable();
249-
var albThumb = Prowl.Editor.GUI.Panels.ProjectPanel.GetThumbnailTexture(data.Layers[i].Albedo.AssetID);
249+
var albThumb = EditorAssetDatabase.Instance?.GetThumbnailTexture(data.Layers[i].Albedo.AssetID);
250250
var swBox = paper.Box($"{id}_l{i}_sw").Width(16).Height(16).Rounded(4)
251251
.Margin(0, 0, UnitValue.Stretch(), UnitValue.Stretch())
252252
.BorderColor(EditorTheme.WithAlpha(SColor.White, 38)).BorderWidth(1).IsNotInteractable();
@@ -581,7 +581,7 @@ private void DrawProtoGrid(Paper paper, string id, Prowl.Scribe.FontFile font, i
581581
.BorderColor(sel ? EditorTheme.WithAlpha(EditorTheme.Accent, 102) : SColor.Transparent).BorderWidth(1)
582582
.OnClick(_ => onSelect(capture)).Enter())
583583
{
584-
var thumb = Prowl.Editor.GUI.Panels.ProjectPanel.GetThumbnailTexture(getThumbGuid(idx));
584+
var thumb = EditorAssetDatabase.Instance?.GetThumbnailTexture(getThumbGuid(idx));
585585
var thumbBox = paper.Box($"{id}_t{idx}_th").Width(46).Height(46).Rounded(9)
586586
.Margin(UnitValue.Stretch(), UnitValue.Stretch(), 0, 0)
587587
.BackgroundColor(EditorTheme.WithAlpha(tint, 34))

Prowl.Editor/GUI/Panels/PreferencesPanel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ private void DrawGeneral(Paper paper, EditorSettings s)
130130
s.ThumbnailSize = v switch { 1 => 64, 2 => 128, _ => 32 };
131131
s.Save();
132132
ThumbnailGenerator.DeleteAll();
133-
ProjectPanel.ClearThumbnailCache();
133+
EditorAssetDatabase.Instance?.ClearThumbnailTextureCache();
134134
}, thumbOptions).Show());
135135

136136
EditorGUI.SectionHeader(paper, "pref_gen_maint", Loc.Get("pref.maintenance"));

Prowl.Editor/GUI/Panels/ProjectPanel.cs

Lines changed: 3 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ public override void OnHeaderContent(Paper paper, float width, float height)
7373
private bool _contentBgHovered;
7474
// Rename state is managed by RenameOverlay
7575
private static readonly HashSet<Guid> _expandedAssets = new(); // files with sub-assets expanded
76-
private static readonly Dictionary<Guid, Runtime.Resources.Texture2D?> _thumbnailCache = new();
7776
private static Guid _pendingPingNavigate; // Navigate to pinged asset's folder on next frame
7877
private static Guid _lastPingedGuid; // Track when a new ping starts
7978
private const float MinThumbSize = 20f; // Below this = list mode
@@ -479,7 +478,7 @@ private void PerformAssetMove(AssetDragPayload payload, string destRelFolder)
479478

480479
if (moved > 0)
481480
{
482-
_thumbnailCache.Clear(); // paths changed thumbnail lookup may be stale
481+
// Thumbnails are keyed by GUID, not path, so a move/rename doesn't invalidate them.
483482
Runtime.Debug.Log($"Moved {moved} item(s) to '{(string.IsNullOrEmpty(destRelFolder) ? "Assets" : destRelFolder)}'.");
484483
}
485484
}
@@ -1230,7 +1229,7 @@ private void DrawSubThumb(Paper paper, Scribe.FontFile font, ContentItem sub, in
12301229
.Tooltip(sub.Name)
12311230
.Enter())
12321231
{
1233-
var thumbTex = GetThumbnailTexture(sub.Guid);
1232+
var thumbTex = EditorAssetDatabase.Instance?.GetThumbnailTexture(sub.Guid);
12341233
if (thumbTex != null)
12351234
{
12361235
paper.Box($"proj_subth_{sub.Guid}").Width(42).Height(42).Margin(ST, ST, 0, 0)
@@ -1350,7 +1349,7 @@ private void DrawGridItem(Paper paper, Scribe.FontFile font, string id, ContentI
13501349
}
13511350

13521351
// Thumbnail area
1353-
var thumbTex = GetThumbnailTexture(item.Guid);
1352+
var thumbTex = EditorAssetDatabase.Instance?.GetThumbnailTexture(item.Guid);
13541353
if (thumbTex != null)
13551354
{
13561355
// Rounded image tile (texture-brushed rounded rect) + a matching rounded border.
@@ -1566,56 +1565,6 @@ private static string FormatSize(long bytes)
15661565

15671566
private static string GetFileIcon(string ext) => FileIconRegistry.GetIconForExtension(ext);
15681567

1569-
/// <summary>Resolve an asset GUID to its cached thumbnail texture (loads from disk on first use).
1570-
/// Returns null when no thumbnail exists yet. Shared with other editors (e.g. TerrainEditor tiles).</summary>
1571-
public static Runtime.Resources.Texture2D? GetThumbnailTexture(Guid guid)
1572-
{
1573-
if (guid == Guid.Empty) return null;
1574-
1575-
if (_thumbnailCache.TryGetValue(guid, out var cached))
1576-
return cached;
1577-
1578-
// Try loading from disk don't cache null so we retry when thumbnail is generated
1579-
var db = EditorAssetDatabase.Instance;
1580-
if (db == null) return null;
1581-
1582-
var thumb = db.LoadThumbnail(guid);
1583-
if (thumb == null) return null;
1584-
1585-
try
1586-
{
1587-
var (w, h, pixels) = thumb.Value;
1588-
var tex = new Runtime.Resources.Texture2D((uint)w, (uint)h, false, TextureImageFormat.Color4b);
1589-
tex.SetData<byte>(pixels);
1590-
tex.SetTextureFilters(TextureMin.Linear, TextureMag.Linear);
1591-
_thumbnailCache[guid] = tex;
1592-
return tex;
1593-
}
1594-
catch
1595-
{
1596-
_thumbnailCache[guid] = null;
1597-
return null;
1598-
}
1599-
}
1600-
1601-
/// <summary>Clear the thumbnail cache (e.g. after reimport).</summary>
1602-
public static void ClearThumbnailCache()
1603-
{
1604-
foreach (var tex in _thumbnailCache.Values)
1605-
tex?.Dispose();
1606-
_thumbnailCache.Clear();
1607-
}
1608-
1609-
/// <summary>Invalidate a single thumbnail so it reloads from disk on next access.</summary>
1610-
public static void InvalidateThumbnail(Guid guid)
1611-
{
1612-
if (_thumbnailCache.TryGetValue(guid, out var tex))
1613-
{
1614-
tex?.Dispose();
1615-
_thumbnailCache.Remove(guid);
1616-
}
1617-
}
1618-
16191568
private static string GetSubAssetIcon(Type? type)
16201569
{
16211570
if (type == null) return EditorIcons.File;

Prowl.Editor/GUI/Popups/SelectorModal.cs

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ public static class SelectorModal
4848
private static Action<object?>? _callback;
4949
private static string _searchText = "";
5050

51-
// Thumbnail cache (shared with ProjectPanel would be ideal, but keep it simple)
52-
private static readonly Dictionary<Guid, Texture2D?> _thumbCache = new();
53-
5451
public static bool IsOpen => _open;
5552

5653
/// <summary>
@@ -384,7 +381,7 @@ private static void DrawAssetsTab(Paper paper, Prowl.Scribe.FontFile font, float
384381
private static void DrawAssetGridItem(Paper paper, Prowl.Scribe.FontFile font,
385382
string id, Guid guid, string name, float cellSize, float labelH, float totalCellH)
386383
{
387-
var thumbTex = GetThumbnailTexture(guid);
384+
var thumbTex = EditorAssetDatabase.Instance?.GetThumbnailTexture(guid);
388385

389386
using (paper.Column(id)
390387
.Width(cellSize).Height(totalCellH)
@@ -456,32 +453,4 @@ private static string GetHierarchyPath(GameObject go)
456453
parts.Reverse();
457454
return string.Join("/", parts);
458455
}
459-
460-
private static Texture2D? GetThumbnailTexture(Guid guid)
461-
{
462-
if (guid == Guid.Empty) return null;
463-
464-
if (_thumbCache.TryGetValue(guid, out var cached))
465-
return cached;
466-
467-
var db = EditorAssetDatabase.Instance;
468-
if (db == null) return null;
469-
470-
var thumb = db.LoadThumbnail(guid);
471-
if (thumb == null) return null;
472-
473-
try
474-
{
475-
var (w, h, pixels) = thumb.Value;
476-
var tex = new Texture2D((uint)w, (uint)h, false, TextureImageFormat.Color4b);
477-
tex.SetData<byte>(pixels);
478-
tex.SetTextureFilters(TextureMin.Linear, TextureMag.Linear);
479-
_thumbCache[guid] = tex;
480-
return tex;
481-
}
482-
catch
483-
{
484-
return null;
485-
}
486-
}
487456
}

0 commit comments

Comments
 (0)