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.
/// </summary>
/// <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">
/// 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.
/// </param>
/// <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;
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");
if (!refresh)
return null;
info = new TextureInfo();
this.activeTextures.Add(path, info);
}
if (info == null)
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;
if (info is { Wrap: not null })
return info;
if (refresh)
{
if (!this.im.IsReady)
if (!this.im.IsReady)
throw new InvalidOperationException("Cannot create textures before scene is ready");
string? interceptPath = null;
this.InterceptTexDataLoad?.Invoke(path, ref interceptPath);
string? interceptPath = null;
this.InterceptTexDataLoad?.Invoke(path, ref interceptPath);
if (interceptPath != null)
{
Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath);
path = interceptPath;
}
if (interceptPath != null)
{
Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath);
path = interceptPath;
}
TextureWrap? wrap;
try
TextureWrap? wrap;
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
// 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")
{
if (Path.GetExtension(path) == ".tex")
{
// Attempt to load via Lumina
var file = this.dataManager.GameData.GetFileFromDisk<TexFile>(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);
}
// Attempt to load via Lumina
var file = this.dataManager.GameData.GetFileFromDisk<TexFile>(path);
wrap = this.GetTexture(file);
Log.Verbose("Texture {Path} loaded FS via Lumina", path);
}
else
{
// Load regularly from dats
var file = this.dataManager.GetFile<TexFile>(path);
if (file == null)
throw new Exception("Could not load TexFile from dat.");
wrap = this.GetTexture(file);
Log.Verbose("Texture {Path} loaded from SqPack", path);
// Attempt to load image
wrap = this.im.LoadImage(path);
Log.Verbose("Texture {Path} loaded FS via LoadImage", 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);
// 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;
// Load regularly from dats
var file = this.dataManager.GetFile<TexFile>(path);
if (file == null)
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");
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;
}
@ -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>
internal void NotifyTextureDisposed(string path, bool keepAlive)
{
var info = this.GetInfo(path, false);
// This texture was already disposed
if (info == null)
lock (this.activeTextures)
{
Log.Warning("Disposing unknown texture {Path}", path);
return;
if (!this.activeTextures.TryGetValue(path, out var info))
{
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)
@ -391,14 +378,22 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
private TextureManagerTextureWrap? CreateWrap(string path, bool keepAlive)
{
// 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)!;
lock (this.activeTextures)
{
// 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)
info.KeepAliveCount++;
// We need to increase the refcounts 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++;
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)
@ -589,7 +584,7 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap
/// <inheritdoc/>
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.");
/// <inheritdoc/>