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); } }