using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Textures.TextureWraps.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using Dalamud.Utility.TerraFxCom;
using Lumina.Data;
using Lumina.Data.Files;
using Lumina.Data.Parsing.Tex.Buffers;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Textures.Internal;
/// Service responsible for loading and disposing ImGui texture wraps.
[ServiceManager.EarlyLoadedService]
internal sealed partial class TextureManager
: IInternalDisposableService,
ITextureProvider,
ITextureSubstitutionProvider,
ITextureReadbackProvider
{
private static readonly ModuleLog Log = ModuleLog.Create();
[ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration dalamudConfiguration = Service.Get();
[ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service.Get();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service.Get();
[ServiceManager.ServiceDependency]
private readonly InterfaceManager interfaceManager = Service.Get();
private readonly CancellationTokenSource disposeCts = new();
private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader;
private SharedTextureManager? sharedTextureManager;
private WicManager? wicManager;
private ComPtr device;
[ServiceManager.ServiceConstructor]
private unsafe TextureManager(InterfaceManager.InterfaceManagerWithScene withScene)
{
using var failsafe = new DisposeSafety.ScopedFinalizer();
failsafe.Add(this.device = new((ID3D11Device*)withScene.Manager.Backend!.DeviceHandle));
failsafe.Add(this.dynamicPriorityTextureLoader = new(Math.Max(1, Environment.ProcessorCount - 1)));
failsafe.Add(this.sharedTextureManager = new(this));
failsafe.Add(this.wicManager = new(this));
failsafe.Add(this.simpleDrawer = new());
this.framework.Update += this.BlameTrackerUpdate;
failsafe.Add(() => this.framework.Update -= this.BlameTrackerUpdate);
this.simpleDrawer.Setup(this.device.Get());
failsafe.Cancel();
}
/// Finalizes an instance of the class.
~TextureManager() => this.ReleaseUnmanagedResources();
/// Gets the dynamic-priority queue texture loader.
public DynamicPriorityQueueLoader DynamicPriorityTextureLoader
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.dynamicPriorityTextureLoader ?? throw new ObjectDisposedException(nameof(TextureManager));
}
/// Gets a simpler drawer.
public SimpleDrawerImpl SimpleDrawer
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.simpleDrawer ?? throw new ObjectDisposedException(nameof(TextureManager));
}
/// Gets the shared texture manager.
public SharedTextureManager Shared
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.sharedTextureManager ?? throw new ObjectDisposedException(nameof(TextureManager));
}
/// Gets the WIC manager.
public WicManager Wic
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.wicManager ?? throw new ObjectDisposedException(nameof(TextureManager));
}
///
void IInternalDisposableService.DisposeService()
{
if (this.disposeCts.IsCancellationRequested)
return;
this.disposeCts.Cancel();
Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose();
Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose();
Interlocked.Exchange(ref this.sharedTextureManager, null)?.Dispose();
Interlocked.Exchange(ref this.wicManager, null)?.Dispose();
this.ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
///
public Task CreateFromImageAsync(
ReadOnlyMemory bytes,
string? debugName = null,
CancellationToken cancellationToken = default) =>
this.DynamicPriorityTextureLoader.LoadAsync(
null,
ct => Task.Run(
() =>
this.BlameSetName(
this.NoThrottleCreateFromImage(bytes.ToArray(), ct),
debugName ??
$"{nameof(this.CreateFromImageAsync)}({bytes.Length:n0}b)"),
ct),
cancellationToken);
///
public Task CreateFromImageAsync(
Stream stream,
bool leaveOpen = false,
string? debugName = null,
CancellationToken cancellationToken = default) =>
this.DynamicPriorityTextureLoader.LoadAsync(
null,
async ct =>
{
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms, ct).ConfigureAwait(false);
return this.BlameSetName(
this.NoThrottleCreateFromImage(ms.GetBuffer(), ct),
debugName ??
$"{nameof(this.CreateFromImageAsync)}(stream)");
},
cancellationToken,
leaveOpen ? null : stream);
///
// It probably doesn't make sense to throttle this, as it copies the passed bytes to GPU without any transformation.
public IDalamudTextureWrap CreateFromRaw(
RawImageSpecification specs,
ReadOnlySpan bytes,
string? debugName = null) =>
this.BlameSetName(
this.NoThrottleCreateFromRaw(specs, bytes),
debugName ?? $"{nameof(this.CreateFromRaw)}({specs}, {bytes.Length:n0})");
///
public Task CreateFromRawAsync(
RawImageSpecification specs,
ReadOnlyMemory bytes,
string? debugName = null,
CancellationToken cancellationToken = default) =>
this.DynamicPriorityTextureLoader.LoadAsync(
null,
_ => Task.FromResult(
this.BlameSetName(
this.NoThrottleCreateFromRaw(specs, bytes.Span),
debugName ??
$"{nameof(this.CreateFromRawAsync)}({specs}, {bytes.Length:n0})")),
cancellationToken);
///
public Task CreateFromRawAsync(
RawImageSpecification specs,
Stream stream,
bool leaveOpen = false,
string? debugName = null,
CancellationToken cancellationToken = default) =>
this.DynamicPriorityTextureLoader.LoadAsync(
null,
async ct =>
{
await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new();
await stream.CopyToAsync(ms, ct).ConfigureAwait(false);
return this.BlameSetName(
this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)),
debugName ??
$"{nameof(this.CreateFromRawAsync)}({specs}, stream)");
},
cancellationToken,
leaveOpen ? null : stream);
///
public IDalamudTextureWrap CreateFromTexFile(TexFile file) =>
this.BlameSetName(
this.CreateFromTexFileAsync(file).Result,
$"{nameof(this.CreateFromTexFile)}({nameof(file)})");
///
public Task CreateFromTexFileAsync(
TexFile file,
string? debugName = null,
CancellationToken cancellationToken = default)
{
return this.DynamicPriorityTextureLoader.LoadAsync(
null,
_ => Task.FromResult(
this.BlameSetName(
this.NoThrottleCreateFromTexFile(file.Header, file.TextureBuffer),
debugName ?? $"{nameof(this.CreateFromTexFile)}({ForceNullable(file.FilePath)?.Path})")),
cancellationToken);
static T? ForceNullable(T s) => s;
}
///
public unsafe IDalamudTextureWrap CreateEmpty(
RawImageSpecification specs,
bool cpuRead,
bool cpuWrite,
string? debugName = null)
{
if (cpuRead && cpuWrite)
throw new ArgumentException("cpuRead and cpuWrite cannot be set at the same time.");
var cpuaf = default(D3D11_CPU_ACCESS_FLAG);
if (cpuRead)
cpuaf |= D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ;
if (cpuWrite)
cpuaf |= D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE;
D3D11_USAGE usage;
if (cpuRead)
usage = D3D11_USAGE.D3D11_USAGE_STAGING;
else if (cpuWrite)
usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC;
else
usage = D3D11_USAGE.D3D11_USAGE_DEFAULT;
using var texture = this.device.CreateTexture2D(
new()
{
Width = (uint)specs.Width,
Height = (uint)specs.Height,
MipLevels = 1,
ArraySize = 1,
Format = specs.Format,
SampleDesc = new(1, 0),
Usage = usage,
BindFlags = (uint)D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE,
CPUAccessFlags = (uint)cpuaf,
MiscFlags = 0,
});
using var view = this.device.CreateShaderResourceView(
texture,
new(texture, D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D));
var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true);
this.BlameSetName(wrap, debugName ?? $"{nameof(this.CreateEmpty)}({specs})");
return wrap;
}
///
public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) => this.CreateDrawListTexture(null, debugName);
///
/// Plugin that created the draw list.
///
///
public IDrawListTextureWrap CreateDrawListTexture(LocalPlugin? plugin, string? debugName = null) =>
new DrawListTextureWrap(
new(this.device),
this,
Service.Get().Empty4X4,
plugin,
debugName ?? $"{nameof(this.CreateDrawListTexture)}");
///
bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) =>
this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat);
///
public unsafe bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat)
{
D3D11_FORMAT_SUPPORT supported;
if (this.device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED)
return false;
const D3D11_FORMAT_SUPPORT required = D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D;
return (supported & required) == required;
}
///
internal unsafe IDalamudTextureWrap NoThrottleCreateFromRaw(
RawImageSpecification specs,
ReadOnlySpan bytes)
{
var texd = new D3D11_TEXTURE2D_DESC
{
Width = (uint)specs.Width,
Height = (uint)specs.Height,
MipLevels = 1,
ArraySize = 1,
Format = specs.Format,
SampleDesc = new(1, 0),
Usage = D3D11_USAGE.D3D11_USAGE_IMMUTABLE,
BindFlags = (uint)D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE,
CPUAccessFlags = 0,
MiscFlags = 0,
};
using var texture = default(ComPtr);
fixed (void* dataPtr = bytes)
{
var subrdata = new D3D11_SUBRESOURCE_DATA { pSysMem = dataPtr, SysMemPitch = (uint)specs.Pitch };
this.device.Get()->CreateTexture2D(&texd, &subrdata, texture.GetAddressOf()).ThrowOnError();
}
var viewDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC
{
Format = texd.Format,
ViewDimension = D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D,
Texture2D = new() { MipLevels = texd.MipLevels },
};
using var view = default(ComPtr);
this.device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &viewDesc, view.GetAddressOf())
.ThrowOnError();
var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true);
this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromRaw)}({specs}, {bytes.Length:n0})");
return wrap;
}
/// Creates a texture from the given . Skips the load throttler; intended to be used
/// from implementation of s.
/// Header of a .tex file.
/// Texture buffer.
/// The loaded texture.
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile.TexHeader header, TextureBuffer buffer)
{
ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(header.Format, false);
if (conversion != TexFile.DxgiFormatConversion.NoConversion ||
!this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat))
{
dxgiFormat = (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM;
buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8);
}
var wrap = this.NoThrottleCreateFromRaw(new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData);
this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({header.Width} x {header.Height})");
return wrap;
}
/// Creates a texture from the given , trying to interpret it as a
/// .
/// The file bytes.
/// The loaded texture.
internal unsafe IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes)
{
ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes))
throw new InvalidDataException("The file is not a TexFile.");
TexFile.TexHeader header;
TextureBuffer buffer;
fixed (byte* p = fileBytes)
{
var lbr = new LuminaBinaryReader(new UnmanagedMemoryStream(p, fileBytes.Length));
header = lbr.ReadStructure();
buffer = TextureBuffer.FromStream(header, lbr);
}
var wrap = this.NoThrottleCreateFromTexFile(header, buffer);
this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({fileBytes.Length:n0})");
return wrap;
}
private void ReleaseUnmanagedResources() => this.device.Reset();
/// Runs the given action in IDXGISwapChain.Present immediately or waiting as needed.
/// The action to run.
// Not sure why this and the below can't be unconditional RunOnFrameworkThread
private async Task RunDuringPresent(Action action)
{
if (this.interfaceManager.IsMainThreadInPresent && ThreadSafety.IsMainThread)
action();
else
await this.interfaceManager.RunBeforeImGuiRender(action);
}
/// Runs the given function in IDXGISwapChain.Present immediately or waiting as needed.
/// The type of the return value.
/// The function to run.
/// The return value from the function.
private async Task RunDuringPresent(Func func)
{
if (this.interfaceManager.IsMainThreadInPresent && ThreadSafety.IsMainThread)
return func();
return await this.interfaceManager.RunBeforeImGuiRender(func);
}
}