chore: simplify refcounting logic, more concurrency fixes

This commit is contained in:
goat 2023-08-09 21:22:29 +02:00
parent 8a300cc98e
commit 24ad2d4c8b
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B

View file

@ -233,13 +233,12 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
/// Get texture info. /// Get texture info.
/// </summary> /// </summary>
/// <param name="path">Path to the texture.</param> /// <param name="path">Path to the texture.</param>
/// <param name="refresh">Whether or not the texture should be reloaded if it was unloaded.</param>
/// <param name="rethrow"> /// <param name="rethrow">
/// If true, exceptions caused by texture load will not be caught. /// If true, exceptions caused by texture load will not be caught.
/// If false, exceptions will be caught and a dummy texture will be returned to prevent plugins from using invalid texture handles. /// If false, exceptions will be caught and a dummy texture will be returned to prevent plugins from using invalid texture handles.
/// </param> /// </param>
/// <returns>Info object storing texture metadata.</returns> /// <returns>Info object storing texture metadata.</returns>
internal TextureInfo? GetInfo(string path, bool refresh = true, bool rethrow = false) internal TextureInfo GetInfo(string path, bool rethrow = false)
{ {
TextureInfo? info; TextureInfo? info;
lock (this.activeTextures) lock (this.activeTextures)
@ -248,106 +247,94 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
{ {
Debug.Assert(rethrow, "This should never run when getting outside of creator"); Debug.Assert(rethrow, "This should never run when getting outside of creator");
if (!refresh)
return null;
info = new TextureInfo(); info = new TextureInfo();
this.activeTextures.Add(path, info); this.activeTextures.Add(path, info);
} }
if (info == null) if (info == null)
throw new Exception("null info in activeTextures"); throw new Exception("null info in activeTextures");
// NOTE: We need to increase the refcount here while locking the collection!
// Otherwise, if this is loaded from a task, cleanup might already try to delete it
// before it can be increased.
info.RefCount++;
} }
if (refresh && info.KeepAliveCount == 0) if (info.KeepAliveCount == 0)
info.LastAccess = DateTime.UtcNow; info.LastAccess = DateTime.UtcNow;
if (info is { Wrap: not null }) if (info is { Wrap: not null })
return info; return info;
if (refresh) if (!this.im.IsReady)
{
if (!this.im.IsReady)
throw new InvalidOperationException("Cannot create textures before scene is ready"); throw new InvalidOperationException("Cannot create textures before scene is ready");
string? interceptPath = null; string? interceptPath = null;
this.InterceptTexDataLoad?.Invoke(path, ref interceptPath); this.InterceptTexDataLoad?.Invoke(path, ref interceptPath);
if (interceptPath != null) if (interceptPath != null)
{ {
Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath); Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath);
path = interceptPath; path = interceptPath;
} }
TextureWrap? wrap; TextureWrap? wrap;
try try
{
// We want to load this from the disk, probably, if the path has a root
// Not sure if this can cause issues with e.g. network drives, might have to rethink
// and add a flag instead if it does.
if (Path.IsPathRooted(path))
{ {
// We want to load this from the disk, probably, if the path has a root if (Path.GetExtension(path) == ".tex")
// Not sure if this can cause issues with e.g. network drives, might have to rethink
// and add a flag instead if it does.
if (Path.IsPathRooted(path))
{ {
if (Path.GetExtension(path) == ".tex") // Attempt to load via Lumina
{ var file = this.dataManager.GameData.GetFileFromDisk<TexFile>(path);
// Attempt to load via Lumina wrap = this.GetTexture(file);
var file = this.dataManager.GameData.GetFileFromDisk<TexFile>(path); Log.Verbose("Texture {Path} loaded FS via Lumina", path);
wrap = this.GetTexture(file);
Log.Verbose("Texture {Path} loaded FS via Lumina", path);
}
else
{
// Attempt to load image
wrap = this.im.LoadImage(path);
Log.Verbose("Texture {Path} loaded FS via LoadImage", path);
}
} }
else else
{ {
// Load regularly from dats // Attempt to load image
var file = this.dataManager.GetFile<TexFile>(path); wrap = this.im.LoadImage(path);
if (file == null) Log.Verbose("Texture {Path} loaded FS via LoadImage", path);
throw new Exception("Could not load TexFile from dat.");
wrap = this.GetTexture(file);
Log.Verbose("Texture {Path} loaded from SqPack", path);
} }
if (wrap == null)
throw new Exception("Could not create texture");
// TODO: We could support this, but I don't think it's worth it at the moment.
var extents = new Vector2(wrap.Width, wrap.Height);
if (info.Extents != Vector2.Zero && info.Extents != extents)
Log.Warning("Texture at {Path} changed size between reloads, this is currently not supported.", path);
info.Extents = extents;
} }
catch (Exception e) else
{ {
Log.Error(e, "Could not load texture from {Path}", path); // Load regularly from dats
var file = this.dataManager.GetFile<TexFile>(path);
// When creating the texture initially, we want to be able to pass errors back to the plugin if (file == null)
if (rethrow) throw new Exception("Could not load TexFile from dat.");
throw;
wrap = this.GetTexture(file);
// This means that the load failed due to circumstances outside of our control, Log.Verbose("Texture {Path} loaded from SqPack", path);
// and we can't do anything about it. Return a dummy texture so that the plugin still
// has something to draw.
wrap = this.fallbackTextureWrap;
// Prevent divide-by-zero
if (info.Extents == Vector2.Zero)
info.Extents = Vector2.One;
} }
if (wrap == null)
throw new Exception("Could not create texture");
info.Wrap = wrap; // TODO: We could support this, but I don't think it's worth it at the moment.
var extents = new Vector2(wrap.Width, wrap.Height);
if (info.Extents != Vector2.Zero && info.Extents != extents)
Log.Warning("Texture at {Path} changed size between reloads, this is currently not supported.", path);
info.Extents = extents;
}
catch (Exception e)
{
Log.Error(e, "Could not load texture from {Path}", path);
// When creating the texture initially, we want to be able to pass errors back to the plugin
if (rethrow)
throw;
// This means that the load failed due to circumstances outside of our control,
// and we can't do anything about it. Return a dummy texture so that the plugin still
// has something to draw.
wrap = this.fallbackTextureWrap;
// Prevent divide-by-zero
if (info.Extents == Vector2.Zero)
info.Extents = Vector2.One;
} }
info.Wrap = wrap;
return info; return info;
} }
@ -359,23 +346,23 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
/// <param name="keepAlive">Whether or not this handle was created in keep-alive mode.</param> /// <param name="keepAlive">Whether or not this handle was created in keep-alive mode.</param>
internal void NotifyTextureDisposed(string path, bool keepAlive) internal void NotifyTextureDisposed(string path, bool keepAlive)
{ {
var info = this.GetInfo(path, false); lock (this.activeTextures)
// This texture was already disposed
if (info == null)
{ {
Log.Warning("Disposing unknown texture {Path}", path); if (!this.activeTextures.TryGetValue(path, out var info))
return; {
Log.Warning("Disposing texture that didn't exist: {Path}", path);
return;
}
info.RefCount--;
if (keepAlive)
info.KeepAliveCount--;
// Clean it up by the next update. If it's re-requested in-between, we don't reload it.
if (info.RefCount <= 0)
info.LastAccess = default;
} }
info.RefCount--;
if (keepAlive)
info.KeepAliveCount--;
// Clean it up by the next update. If it's re-requested in-between, we don't reload it.
if (info.RefCount <= 0)
info.LastAccess = default;
} }
private static string FormatIconPath(uint iconId, string? type, bool highResolution) private static string FormatIconPath(uint iconId, string? type, bool highResolution)
@ -391,14 +378,22 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
private TextureManagerTextureWrap? CreateWrap(string path, bool keepAlive) private TextureManagerTextureWrap? CreateWrap(string path, bool keepAlive)
{ {
// This will create the texture. lock (this.activeTextures)
// That's fine, it's probably used immediately and this will let the plugin catch load errors. {
var info = this.GetInfo(path, rethrow: true)!; // This will create the texture.
// That's fine, it's probably used immediately and this will let the plugin catch load errors.
var info = this.GetInfo(path, rethrow: true);
if (keepAlive) // We need to increase the refcounts here while locking the collection!
info.KeepAliveCount++; // Otherwise, if this is loaded from a task, cleanup might already try to delete it
// before it can be increased.
info.RefCount++;
return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this); if (keepAlive)
info.KeepAliveCount++;
return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this);
}
} }
private void FrameworkOnUpdate(Framework fw) private void FrameworkOnUpdate(Framework fw)
@ -589,7 +584,7 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr ImGuiHandle => !this.IsDisposed ? public IntPtr ImGuiHandle => !this.IsDisposed ?
this.manager.GetInfo(this.path)!.Wrap!.ImGuiHandle : this.manager.GetInfo(this.path).Wrap!.ImGuiHandle :
throw new InvalidOperationException("Texture already disposed. You may not render it."); throw new InvalidOperationException("Texture already disposed. You may not render it.");
/// <inheritdoc/> /// <inheritdoc/>