Add CreateFromGameScreen

This commit is contained in:
Soreepeong 2024-03-03 20:08:52 +09:00
parent c0938bd3a9
commit c04ce36b9c
19 changed files with 1403 additions and 581 deletions

View file

@ -1,5 +1,7 @@
using Dalamud.Storage.Assets;
using TerraFX.Interop.DirectX;
namespace Dalamud;
/// <summary>
@ -19,9 +21,16 @@ public enum DalamudAsset
/// <see cref="DalamudAssetPurpose.TextureFromRaw"/>: The fallback empty texture.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 })]
[DalamudAssetRawTexture(4, 8, 4, SharpDX.DXGI.Format.BC1_UNorm)]
[DalamudAssetRawTexture(4, 4, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, 8)]
Empty4X4 = 1000,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromRaw"/>: The fallback empty texture.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0 })]
[DalamudAssetRawTexture(4, 4, DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, 8)]
White4X4 = 1014,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The Dalamud logo.
/// </summary>

View file

@ -70,7 +70,8 @@ internal class InterfaceManager : IDisposable, IServiceType
[ServiceManager.ServiceDependency]
private readonly DalamudIme dalamudIme = Service<DalamudIme>.Get();
private readonly ConcurrentQueue<Action> runBeforePresent = new();
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
private readonly SwapChainVtableResolver address = new();
private readonly Hook<SetCursorDelegate> setCursorHook;
@ -285,13 +286,13 @@ internal class InterfaceManager : IDisposable, IServiceType
this.deferredDisposeDisposables.Add(locked);
}
/// <summary>Queues an action to be run before Present call.</summary>
/// <summary>Queues an action to be run before <see cref="ImGui.Render"/> call.</summary>
/// <param name="action">The action.</param>
/// <returns>A <see cref="Task"/> that resolves once <paramref name="action"/> is run.</returns>
public Task RunBeforePresent(Action action)
public Task RunBeforeImGuiRender(Action action)
{
var tcs = new TaskCompletionSource();
this.runBeforePresent.Enqueue(
this.runBeforeImGuiRender.Enqueue(
() =>
{
try
@ -307,14 +308,58 @@ internal class InterfaceManager : IDisposable, IServiceType
return tcs.Task;
}
/// <summary>Queues a function to be run before Present call.</summary>
/// <summary>Queues a function to be run before <see cref="ImGui.Render"/> call.</summary>
/// <typeparam name="T">The type of the return value.</typeparam>
/// <param name="func">The function.</param>
/// <returns>A <see cref="Task"/> that resolves once <paramref name="func"/> is run.</returns>
public Task<T> RunBeforePresent<T>(Func<T> func)
public Task<T> RunBeforeImGuiRender<T>(Func<T> func)
{
var tcs = new TaskCompletionSource<T>();
this.runBeforePresent.Enqueue(
this.runBeforeImGuiRender.Enqueue(
() =>
{
try
{
tcs.SetResult(func());
}
catch (Exception e)
{
tcs.SetException(e);
}
});
return tcs.Task;
}
/// <summary>Queues an action to be run after <see cref="ImGui.Render"/> call.</summary>
/// <param name="action">The action.</param>
/// <returns>A <see cref="Task"/> that resolves once <paramref name="action"/> is run.</returns>
public Task RunAfterImGuiRender(Action action)
{
var tcs = new TaskCompletionSource();
this.runAfterImGuiRender.Enqueue(
() =>
{
try
{
action();
tcs.SetResult();
}
catch (Exception e)
{
tcs.SetException(e);
}
});
return tcs.Task;
}
/// <summary>Queues a function to be run after <see cref="ImGui.Render"/> call.</summary>
/// <typeparam name="T">The type of the return value.</typeparam>
/// <param name="func">The function.</param>
/// <returns>A <see cref="Task"/> that resolves once <paramref name="func"/> is run.</returns>
public Task<T> RunAfterImGuiRender<T>(Func<T> func)
{
var tcs = new TaskCompletionSource<T>();
this.runAfterImGuiRender.Enqueue(
() =>
{
try
@ -566,7 +611,7 @@ internal class InterfaceManager : IDisposable, IServiceType
if (!this.dalamudAtlas!.HasBuiltAtlas)
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
while (this.runBeforePresent.TryDequeue(out var action))
while (this.runBeforeImGuiRender.TryDequeue(out var action))
action.InvokeSafely();
if (this.address.IsReshade)
@ -574,19 +619,22 @@ internal class InterfaceManager : IDisposable, IServiceType
var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags);
RenderImGui(this.scene!);
this.CleanupPostImGuiRender();
this.PostImGuiRender();
return pRes;
}
RenderImGui(this.scene!);
this.CleanupPostImGuiRender();
this.PostImGuiRender();
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
}
private void CleanupPostImGuiRender()
private void PostImGuiRender()
{
while (this.runAfterImGuiRender.TryDequeue(out var action))
action.InvokeSafely();
if (!this.deferredDisposeTextures.IsEmpty)
{
var count = 0;

View file

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Utility;
@ -21,6 +22,7 @@ using ImGuiNET;
using Serilog;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager;
@ -49,6 +51,7 @@ internal class TexWidget : IDataWindowWidget
private Vector2 inputTexScale = Vector2.Zero;
private TextureManager textureManager = null!;
private FileDialogManager fileDialogManager = null!;
private ExistingTextureModificationArgs existingTextureModificationArgs;
private string[]? supportedRenderTargetFormatNames;
private DXGI_FORMAT[]? supportedRenderTargetFormats;
@ -82,6 +85,13 @@ internal class TexWidget : IDataWindowWidget
this.supportedRenderTargetFormats = null;
this.supportedRenderTargetFormatNames = null;
this.fileDialogManager = new();
this.existingTextureModificationArgs = new()
{
Uv0 = new(0.25f),
Uv1 = new(0.75f),
NewWidth = 320,
NewHeight = 240,
};
this.Ready = true;
}
@ -123,6 +133,31 @@ internal class TexWidget : IDataWindowWidget
ImGui.PopID();
}
ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing()));
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Capture: ");
if (ImGui.Button("Game"))
this.addedTextures.Add(new() { Api10 = this.textureManager.CreateFromGameScreen() });
ImGui.SameLine();
if (ImGui.Button("Game (Auto)"))
this.addedTextures.Add(new() { Api10 = this.textureManager.CreateFromGameScreen(true) });
ImGui.SameLine();
if (ImGui.Button("Main Viewport"))
{
this.addedTextures.Add(
new() { Api10 = this.textureManager.CreateFromImGuiViewport(ImGui.GetMainViewport().ID) });
}
ImGui.SameLine();
if (ImGui.Button("Main Viewport (Auto)"))
{
this.addedTextures.Add(
new() { Api10 = this.textureManager.CreateFromImGuiViewport(ImGui.GetMainViewport().ID, true) });
}
if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon), ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.PushID(nameof(this.DrawGetFromGameIcon));
@ -158,24 +193,14 @@ internal class TexWidget : IDataWindowWidget
ImGui.PopID();
}
ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing()));
if (this.supportedRenderTargetFormats is null)
if (ImGui.CollapsingHeader($"CropCopy##{this.DrawExistingTextureModificationArgs}"))
{
this.supportedRenderTargetFormatNames = null;
this.supportedRenderTargetFormats =
Enum.GetValues<DXGI_FORMAT>()
.Where(this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync)
.ToArray();
this.renderTargetChoiceInt = 0;
ImGui.PushID(nameof(this.DrawExistingTextureModificationArgs));
this.DrawExistingTextureModificationArgs();
ImGui.PopID();
}
this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray();
ImGui.Combo(
"CropCopy Format",
ref this.renderTargetChoiceInt,
this.supportedRenderTargetFormatNames,
this.supportedRenderTargetFormatNames.Length);
ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing()));
Action? runLater = null;
foreach (var t in this.addedTextures)
@ -217,13 +242,14 @@ internal class TexWidget : IDataWindowWidget
return;
var texTask = this.textureManager.CreateFromExistingTextureAsync(
source.CreateWrapSharingLowLevelResource(),
new(0.25f),
new(0.75f),
supportedFormats[this.renderTargetChoiceInt]);
this.existingTextureModificationArgs with
{
Format = supportedFormats[this.renderTargetChoiceInt],
});
this.addedTextures.Add(new() { Api10 = texTask });
};
}
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
unsafe
@ -233,7 +259,7 @@ internal class TexWidget : IDataWindowWidget
var psrv = (ID3D11ShaderResourceView*)source.ImGuiHandle;
var rcsrv = psrv->AddRef() - 1;
psrv->Release();
var pres = default(ID3D11Resource*);
psrv->GetResource(&pres);
var rcres = pres->AddRef() - 1;
@ -558,6 +584,49 @@ internal class TexWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(10);
}
private void DrawExistingTextureModificationArgs()
{
var vec2 = this.existingTextureModificationArgs.Uv0;
if (ImGui.InputFloat2("UV0", ref vec2))
this.existingTextureModificationArgs.Uv0 = vec2;
vec2 = this.existingTextureModificationArgs.Uv1;
if (ImGui.InputFloat2("UV1", ref vec2))
this.existingTextureModificationArgs.Uv1 = vec2;
Span<int> wh = stackalloc int[2];
wh[0] = this.existingTextureModificationArgs.NewWidth;
wh[1] = this.existingTextureModificationArgs.NewHeight;
if (ImGui.InputInt2("New Size", ref wh[0]))
{
this.existingTextureModificationArgs.NewWidth = wh[0];
this.existingTextureModificationArgs.NewHeight = wh[1];
}
var b = this.existingTextureModificationArgs.MakeOpaque;
if (ImGui.Checkbox("Make Opaque", ref b))
this.existingTextureModificationArgs.MakeOpaque = b;
if (this.supportedRenderTargetFormats is null)
{
this.supportedRenderTargetFormatNames = null;
this.supportedRenderTargetFormats =
Enum.GetValues<DXGI_FORMAT>()
.Where(this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync)
.ToArray();
this.renderTargetChoiceInt = 0;
}
this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray();
ImGui.Combo(
"Format",
ref this.renderTargetChoiceInt,
this.supportedRenderTargetFormatNames,
this.supportedRenderTargetFormatNames.Length);
ImGuiHelpers.ScaledDummy(10);
}
private async void SaveTextureAsync(string name, Func<Task<IDalamudTextureWrap>> textureGetter)
{
try
@ -627,15 +696,18 @@ internal class TexWidget : IDataWindowWidget
}
using var textureWrap = await textureGetter.Invoke();
var props = new Dictionary<string, object>();
if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff)
props["CompressionQuality"] = 1.0f;
else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif ||
encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp)
props["ImageQuality"] = 1.0f;
await this.textureManager.SaveToFileAsync(
textureWrap,
encoder.ContainerGuid,
path,
props: new Dictionary<string, object>
{
["CompressionQuality"] = 1.0f,
["ImageQuality"] = 1.0f,
});
props: props);
Service<NotificationManager>.Get().AddNotification(
$"File saved to: {path}",

View file

@ -638,8 +638,8 @@ internal sealed partial class FontAtlasFactory
new(
width,
height,
width * bpp,
(int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)),
(int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm),
width * bpp),
buf);
this.data.AddExistingTexture(wrap);
texture.TexID = wrap.ImGuiHandle;

View file

@ -360,10 +360,10 @@ internal sealed partial class FontAtlasFactory
new(
texFile.Header.Width,
texFile.Header.Height,
texFile.Header.Width * bpp,
(int)(targetIsB4G4R4A4
? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM
: DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM)),
: DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM),
texFile.Header.Width * bpp),
buffer));
}
finally

View file

@ -0,0 +1,95 @@
using System.Numerics;
using Dalamud.Plugin.Services;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.Textures;
/// <summary>Describes how to modify an existing texture.</summary>
public record struct ExistingTextureModificationArgs()
{
/// <summary>Gets or sets the left top coordinates relative to the size of the source texture.</summary>
/// <para>Coordinates should be in range between 0 and 1.</para>
public Vector2 Uv0 { get; set; } = Vector2.Zero;
/// <summary>Gets or sets the right bottom coordinates relative to the size of the source texture.</summary>
/// <para>Coordinates should be in range between 0 and 1.</para>
/// <remarks>If set to <see cref="Vector2.Zero"/>, then it will be interpreted as <see cref="Vector2.One"/>,
/// to accommodate the use of default value of this record struct.</remarks>
public Vector2 Uv1 { get; set; } = Vector2.One;
/// <summary>Gets or sets the new width.</summary>
/// <remarks>Set to 0 to automatically calculate according to the original texture size, <see cref="Uv0"/>, and
/// <see cref="Uv1"/>.</remarks>
public int NewWidth { get; set; }
/// <summary>Gets or sets the new height.</summary>
/// <remarks>Set to 0 to automatically calculate according to the original texture size, <see cref="Uv0"/>, and
/// <see cref="Uv1"/>.</remarks>
public int NewHeight { get; set; }
/// <summary>Gets or sets a value indicating whether to make the texture opaque.</summary>
/// <remarks>Alpha channel values will be filled with 1.0.</remarks>
public bool MakeOpaque { get; set; } = false;
/// <summary>Gets or sets the new DXGI format.</summary>
/// <remarks>
/// <para>Set to 0 (<see cref="DXGI_FORMAT.DXGI_FORMAT_UNKNOWN"/>) to use the source pixel format.</para>
/// <para>Supported values can be queried with
/// <see cref="ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync"/>. This may not necessarily
/// match <see cref="ITextureProvider.IsDxgiFormatSupported"/>.
/// </para></remarks>
public int DxgiFormat { get; set; } = (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM;
/// <summary>Gets or sets the format (typed).</summary>
internal DXGI_FORMAT Format
{
get => (DXGI_FORMAT)this.DxgiFormat;
set => this.DxgiFormat = (int)value;
}
/// <summary>Gets the effective value of <see cref="Uv1"/>.</summary>
internal Vector2 Uv1Effective => this.Uv1 == Vector2.Zero ? Vector2.One : this.Uv1;
/// <summary>Test if this instance of <see cref="ExistingTextureModificationArgs"/> does not instruct to change the
/// underlying data of a texture.</summary>
/// <param name="desc">The texture description to test against.</param>
/// <returns><c>true</c> if this instance of <see cref="ExistingTextureModificationArgs"/> does not instruct to
/// change the underlying data of a texture.</returns>
internal bool IsCompleteSourceCopy(in D3D11_TEXTURE2D_DESC desc) =>
this.Uv0 == Vector2.Zero
&& this.Uv1 == Vector2.One
&& (this.NewWidth == 0 || this.NewWidth == desc.Width)
&& (this.NewHeight == 0 || this.NewHeight == desc.Height)
&& !this.MakeOpaque
&& (this.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || this.Format == desc.Format);
/// <summary>Checks the properties and throws an exception if values are invalid.</summary>
internal void ThrowOnInvalidValues()
{
if (this.Uv0.X is < 0 or > 1 or float.NaN)
throw new ArgumentException($"{nameof(this.Uv0)}.X is out of range.");
if (this.Uv0.Y is < 0 or > 1 or float.NaN)
throw new ArgumentException($"{nameof(this.Uv0)}.Y is out of range.");
if (this.Uv1Effective.X is < 0 or > 1 or float.NaN)
throw new ArgumentException($"{nameof(this.Uv1)}.X is out of range.");
if (this.Uv1Effective.Y is < 0 or > 1 or float.NaN)
throw new ArgumentException($"{nameof(this.Uv1)}.Y is out of range.");
if (this.Uv0.X >= this.Uv1Effective.X || this.Uv0.Y >= this.Uv1Effective.Y)
{
throw new ArgumentException(
$"{nameof(this.Uv0)} must be strictly less than {nameof(this.Uv1)} in a componentwise way.");
}
if (this.NewWidth < 0)
throw new ArgumentException($"{nameof(this.NewWidth)} cannot be a negative number.");
if (this.NewHeight < 0)
throw new ArgumentException($"{nameof(this.NewHeight)} cannot be a negative number.");
}
}

View file

@ -28,7 +28,8 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture
public static SharedImmediateTexture CreatePlaceholder(string path) => new GamePathSharedImmediateTexture(path);
/// <inheritdoc/>
public override string ToString() => $"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})";
public override string ToString() =>
$"{nameof(GamePathSharedImmediateTexture)}#{this.InstanceIdForDebug}({this.path})";
/// <inheritdoc/>
protected override void ReleaseResources()

View file

@ -0,0 +1,406 @@
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Dalamud.Interface.Internal;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
private SimpleDrawerImpl? simpleDrawer;
[ServiceManager.CallWhenServicesReady("Need device")]
private unsafe void ContinueConstructionFromExistingTextures(InterfaceManager.InterfaceManagerWithScene withScene)
{
this.simpleDrawer = new();
this.simpleDrawer.Setup(this.Device.Get());
}
/// <summary>A class for drawing simple stuff.</summary>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
internal sealed unsafe class SimpleDrawerImpl : IDisposable
{
private ComPtr<ID3D11SamplerState> sampler;
private ComPtr<ID3D11VertexShader> vertexShader;
private ComPtr<ID3D11PixelShader> pixelShader;
private ComPtr<ID3D11InputLayout> inputLayout;
private ComPtr<ID3D11Buffer> vertexConstantBuffer;
private ComPtr<ID3D11BlendState> blendState;
private ComPtr<ID3D11BlendState> blendStateForStrippingAlpha;
private ComPtr<ID3D11RasterizerState> rasterizerState;
private ComPtr<ID3D11Buffer> vertexBufferFill;
private ComPtr<ID3D11Buffer> vertexBufferMutable;
private ComPtr<ID3D11Buffer> indexBuffer;
/// <summary>Finalizes an instance of the <see cref="SimpleDrawerImpl"/> class.</summary>
~SimpleDrawerImpl() => this.Dispose();
/// <inheritdoc/>
public void Dispose()
{
this.sampler.Reset();
this.vertexShader.Reset();
this.pixelShader.Reset();
this.inputLayout.Reset();
this.vertexConstantBuffer.Reset();
this.blendState.Reset();
this.blendStateForStrippingAlpha.Reset();
this.rasterizerState.Reset();
this.vertexBufferFill.Reset();
this.vertexBufferMutable.Reset();
this.indexBuffer.Reset();
GC.SuppressFinalize(this);
}
/// <summary>Sets up this instance of <see cref="SimpleDrawerImpl"/>.</summary>
/// <param name="device">The device.</param>
public void Setup(ID3D11Device* device)
{
var assembly = typeof(ImGuiScene.ImGui_Impl_DX11).Assembly;
// Create the vertex shader
if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty())
{
this.vertexShader.Reset();
this.inputLayout.Reset();
using var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!;
var array = ArrayPool<byte>.Shared.Rent((int)stream.Length);
stream.ReadExactly(array, 0, (int)stream.Length);
fixed (byte* pArray = array)
fixed (ID3D11VertexShader** ppShader = &this.vertexShader.GetPinnableReference())
fixed (ID3D11InputLayout** ppInputLayout = &this.inputLayout.GetPinnableReference())
fixed (void* pszPosition = "POSITION"u8)
fixed (void* pszTexCoord = "TEXCOORD"u8)
fixed (void* pszColor = "COLOR"u8)
{
device->CreateVertexShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError();
var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[]
{
new()
{
SemanticName = (sbyte*)pszPosition,
Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT,
AlignedByteOffset = uint.MaxValue,
},
new()
{
SemanticName = (sbyte*)pszTexCoord,
Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT,
AlignedByteOffset = uint.MaxValue,
},
new()
{
SemanticName = (sbyte*)pszColor,
Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
AlignedByteOffset = uint.MaxValue,
},
};
device->CreateInputLayout(ied, 3, pArray, (nuint)stream.Length, ppInputLayout).ThrowOnError();
}
ArrayPool<byte>.Shared.Return(array);
}
// Create the constant buffer
if (this.vertexConstantBuffer.IsEmpty())
{
var bufferDesc = new D3D11_BUFFER_DESC(
(uint)sizeof(Matrix4x4),
(uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER,
D3D11_USAGE.D3D11_USAGE_IMMUTABLE);
var data = Matrix4x4.Identity;
var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = &data };
fixed (ID3D11Buffer** ppBuffer = &this.vertexConstantBuffer.GetPinnableReference())
device->CreateBuffer(&bufferDesc, &subr, ppBuffer).ThrowOnError();
}
// Create the pixel shader
if (this.pixelShader.IsEmpty())
{
using var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!;
var array = ArrayPool<byte>.Shared.Rent((int)stream.Length);
stream.ReadExactly(array, 0, (int)stream.Length);
fixed (byte* pArray = array)
fixed (ID3D11PixelShader** ppShader = &this.pixelShader.GetPinnableReference())
device->CreatePixelShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError();
ArrayPool<byte>.Shared.Return(array);
}
// Create the blending setup
if (this.blendState.IsEmpty())
{
var blendStateDesc = new D3D11_BLEND_DESC
{
RenderTarget =
{
e0 =
{
BlendEnable = true,
SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA,
DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA,
BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA,
DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE,
BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL,
},
},
};
fixed (ID3D11BlendState** ppBlendState = &this.blendState.GetPinnableReference())
device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError();
}
if (this.blendStateForStrippingAlpha.IsEmpty())
{
var blendStateDesc = new D3D11_BLEND_DESC
{
RenderTarget =
{
e0 =
{
BlendEnable = true,
SrcBlend = D3D11_BLEND.D3D11_BLEND_ZERO,
DestBlend = D3D11_BLEND.D3D11_BLEND_ONE,
BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE,
DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ZERO,
BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALPHA,
},
},
};
fixed (ID3D11BlendState** ppBlendState = &this.blendStateForStrippingAlpha.GetPinnableReference())
device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError();
}
// Create the rasterizer state
if (this.rasterizerState.IsEmpty())
{
var rasterizerDesc = new D3D11_RASTERIZER_DESC
{
FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID,
CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE,
};
fixed (ID3D11RasterizerState** ppRasterizerState = &this.rasterizerState.GetPinnableReference())
device->CreateRasterizerState(&rasterizerDesc, ppRasterizerState).ThrowOnError();
}
// Create the font sampler
if (this.sampler.IsEmpty())
{
var samplerDesc = new D3D11_SAMPLER_DESC(
D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR,
D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
0f,
0,
D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS,
null,
0,
0);
fixed (ID3D11SamplerState** ppSampler = &this.sampler.GetPinnableReference())
device->CreateSamplerState(&samplerDesc, ppSampler).ThrowOnError();
}
if (this.vertexBufferFill.IsEmpty())
{
var data = stackalloc ImDrawVert[]
{
new() { col = uint.MaxValue, pos = new(-1, 1), uv = new(0, 0) },
new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(0, 1) },
new() { col = uint.MaxValue, pos = new(1, 1), uv = new(1, 0) },
new() { col = uint.MaxValue, pos = new(1, -1), uv = new(1, 1) },
};
var desc = new D3D11_BUFFER_DESC(
(uint)(sizeof(ImDrawVert) * 4),
(uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER,
D3D11_USAGE.D3D11_USAGE_IMMUTABLE);
var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data };
var buffer = default(ID3D11Buffer*);
device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError();
this.vertexBufferFill.Attach(buffer);
}
if (this.vertexBufferMutable.IsEmpty())
{
var desc = new D3D11_BUFFER_DESC(
(uint)(sizeof(ImDrawVert) * 4),
(uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER,
D3D11_USAGE.D3D11_USAGE_DYNAMIC,
(uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE);
var buffer = default(ID3D11Buffer*);
device->CreateBuffer(&desc, null, &buffer).ThrowOnError();
this.vertexBufferMutable.Attach(buffer);
}
if (this.indexBuffer.IsEmpty())
{
var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 };
var desc = new D3D11_BUFFER_DESC(
sizeof(ushort) * 6,
(uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER,
D3D11_USAGE.D3D11_USAGE_IMMUTABLE);
var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data };
var buffer = default(ID3D11Buffer*);
device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError();
this.indexBuffer.Attach(buffer);
}
}
/// <summary>Draws the given shader resource view to the current render target.</summary>
/// <param name="ctx">An instance of <see cref="ID3D11DeviceContext"/>.</param>
/// <param name="srv">The shader resource view.</param>
/// <param name="uv0">The left top coordinates relative to the size of the source texture.</param>
/// <param name="uv1">The right bottom coordinates relative to the size of the source texture.</param>
/// <remarks>This function does not throw.</remarks>
public void Draw(
ID3D11DeviceContext* ctx,
ID3D11ShaderResourceView* srv,
Vector2 uv0,
Vector2 uv1)
{
using var rtv = default(ComPtr<ID3D11RenderTargetView>);
ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null);
if (rtv.IsEmpty())
return;
using var rtvRes = default(ComPtr<ID3D11Resource>);
rtv.Get()->GetResource(rtvRes.GetAddressOf());
using var rtvTex = default(ComPtr<ID3D11Texture2D>);
if (rtvRes.As(&rtvTex).FAILED)
return;
D3D11_TEXTURE2D_DESC texDesc;
rtvTex.Get()->GetDesc(&texDesc);
ID3D11Buffer* buffer;
if (uv0 == Vector2.Zero && uv1 == Vector2.One)
{
buffer = this.vertexBufferFill.Get();
}
else
{
buffer = this.vertexBufferMutable.Get();
var mapped = default(D3D11_MAPPED_SUBRESOURCE);
if (ctx->Map((ID3D11Resource*)buffer, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0u, &mapped).FAILED)
return;
_ = new Span<ImDrawVert>(mapped.pData, 4)
{
[0] = new() { col = uint.MaxValue, pos = new(-1, 1), uv = uv0 },
[1] = new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(uv0.X, uv1.Y) },
[2] = new() { col = uint.MaxValue, pos = new(1, 1), uv = new(uv1.X, uv0.Y) },
[3] = new() { col = uint.MaxValue, pos = new(1, -1), uv = uv1 },
};
ctx->Unmap((ID3D11Resource*)buffer, 0u);
}
var stride = (uint)sizeof(ImDrawVert);
var offset = 0u;
ctx->IASetInputLayout(this.inputLayout);
ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset);
ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0);
ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height);
ctx->RSSetState(this.rasterizerState);
ctx->RSSetViewports(1, &viewport);
var blendColor = default(Vector4);
ctx->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff);
ctx->OMSetDepthStencilState(null, 0);
ctx->VSSetShader(this.vertexShader.Get(), null, 0);
buffer = this.vertexConstantBuffer.Get();
ctx->VSSetConstantBuffers(0, 1, &buffer);
ctx->PSSetShader(this.pixelShader, null, 0);
var simp = this.sampler.Get();
ctx->PSSetSamplers(0, 1, &simp);
ctx->PSSetShaderResources(0, 1, &srv);
ctx->GSSetShader(null, null, 0);
ctx->HSSetShader(null, null, 0);
ctx->DSSetShader(null, null, 0);
ctx->CSSetShader(null, null, 0);
ctx->DrawIndexed(6, 0, 0);
var ppn = default(ID3D11ShaderResourceView*);
ctx->PSSetShaderResources(0, 1, &ppn);
}
/// <summary>Fills alpha channel to 1.0 from the current render target.</summary>
/// <param name="ctx">An instance of <see cref="ID3D11DeviceContext"/>.</param>
/// <remarks>This function does not throw.</remarks>
public void StripAlpha(ID3D11DeviceContext* ctx)
{
using var rtv = default(ComPtr<ID3D11RenderTargetView>);
ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null);
if (rtv.IsEmpty())
return;
using var rtvRes = default(ComPtr<ID3D11Resource>);
rtv.Get()->GetResource(rtvRes.GetAddressOf());
using var rtvTex = default(ComPtr<ID3D11Texture2D>);
if (rtvRes.As(&rtvTex).FAILED)
return;
D3D11_TEXTURE2D_DESC texDesc;
rtvTex.Get()->GetDesc(&texDesc);
var buffer = this.vertexBufferFill.Get();
var stride = (uint)sizeof(ImDrawVert);
var offset = 0u;
ctx->IASetInputLayout(this.inputLayout);
ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset);
ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0);
ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height);
ctx->RSSetState(this.rasterizerState);
ctx->RSSetViewports(1, &viewport);
var blendColor = default(Vector4);
ctx->OMSetBlendState(this.blendStateForStrippingAlpha, (float*)&blendColor, 0xffffffff);
ctx->OMSetDepthStencilState(null, 0);
ctx->VSSetShader(this.vertexShader.Get(), null, 0);
buffer = this.vertexConstantBuffer.Get();
ctx->VSSetConstantBuffers(0, 1, &buffer);
ctx->PSSetShader(this.pixelShader, null, 0);
var simp = this.sampler.Get();
ctx->PSSetSamplers(0, 1, &simp);
var ppn = (ID3D11ShaderResourceView*)Service<DalamudAssetManager>.Get().White4X4.ImGuiHandle;
ctx->PSSetShaderResources(0, 1, &ppn);
ctx->GSSetShader(null, null, 0);
ctx->HSSetShader(null, null, 0);
ctx->DSSetShader(null, null, 0);
ctx->CSSetShader(null, null, 0);
ctx->DrawIndexed(6, 0, 0);
ppn = default;
ctx->PSSetShaderResources(0, 1, &ppn);
}
}
}

View file

@ -1,6 +1,3 @@
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
@ -18,8 +15,6 @@ namespace Dalamud.Interface.Textures.Internal;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
private DrawsOneSquare? drawsOneSquare;
/// <inheritdoc/>
bool ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) =>
this.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat);
@ -50,27 +45,9 @@ internal sealed partial class TextureManager
}
/// <inheritdoc/>
Task<IDalamudTextureWrap> ITextureProvider.CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,
int dxgiFormat,
bool leaveWrapOpen,
CancellationToken cancellationToken) =>
this.CreateFromExistingTextureAsync(
wrap,
uv0,
uv1,
(DXGI_FORMAT)dxgiFormat,
leaveWrapOpen,
cancellationToken);
/// <inheritdoc cref="ITextureProvider.CreateFromExistingTextureAsync"/>
public Task<IDalamudTextureWrap> CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,
DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN,
ExistingTextureModificationArgs args = default,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default)
{
@ -82,7 +59,7 @@ internal sealed partial class TextureManager
async Task<IDalamudTextureWrap> ImmediateLoadFunction(CancellationToken ct)
{
using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrap, uv0, uv1, format);
using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrap, args);
unsafe
{
@ -108,27 +85,52 @@ internal sealed partial class TextureManager
}
/// <inheritdoc/>
Task<(RawImageSpecification Specification, byte[] RawData)> ITextureProvider.GetRawDataFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,
int dxgiFormat,
bool leaveWrapOpen,
CancellationToken cancellationToken) =>
this.GetRawDataFromExistingTextureAsync(
wrap,
uv0,
uv1,
(DXGI_FORMAT)dxgiFormat,
leaveWrapOpen,
cancellationToken);
public Task<IDalamudTextureWrap> CreateFromGameScreen(
bool autoUpdate = false,
CancellationToken cancellationToken = default) =>
this.interfaceManager.RunBeforeImGuiRender(
() =>
{
cancellationToken.ThrowIfCancellationRequested();
var t = new ViewportTextureWrap(ImGui.GetMainViewport().ID, true, autoUpdate, cancellationToken);
t.Update();
try
{
return t.FirstUpdateTask.Result;
}
catch
{
t.Dispose();
throw;
}
});
/// <inheritdoc cref="ITextureProvider.GetRawDataFromExistingTextureAsync"/>
/// <inheritdoc/>
public Task<IDalamudTextureWrap> CreateFromImGuiViewport(
uint viewportId,
bool autoUpdate = false,
CancellationToken cancellationToken = default) =>
this.interfaceManager.RunBeforeImGuiRender(
() =>
{
cancellationToken.ThrowIfCancellationRequested();
var t = new ViewportTextureWrap(viewportId, false, autoUpdate, cancellationToken);
t.Update();
try
{
return t.FirstUpdateTask.Result;
}
catch
{
t.Dispose();
throw;
}
});
/// <inheritdoc/>
public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,
DXGI_FORMAT dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN,
ExistingTextureModificationArgs args = default,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default)
{
@ -157,9 +159,9 @@ internal sealed partial class TextureManager
tex2D.Get()->GetDesc(&texDesc);
}
if (texDesc.Format != dxgiFormat && dxgiFormat != DXGI_FORMAT.DXGI_FORMAT_UNKNOWN)
if (!args.IsCompleteSourceCopy(texDesc))
{
using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrap, uv0, uv1, dxgiFormat);
using var tmp = await this.NoThrottleCreateFromExistingTextureAsync(wrap, args);
unsafe
{
tex2D.Swap(&tmp);
@ -167,7 +169,7 @@ internal sealed partial class TextureManager
}
cancellationToken.ThrowIfCancellationRequested();
return await this.interfaceManager.RunBeforePresent(
return await this.interfaceManager.RunBeforeImGuiRender(
() => ExtractMappedResource(this.Device, context, tex2D, cancellationToken));
static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource(
@ -214,8 +216,8 @@ internal sealed partial class TextureManager
var specs = new RawImageSpecification(
(int)desc.Width,
(int)desc.Height,
(int)mapped.RowPitch,
(int)desc.Format);
(int)desc.Format,
(int)mapped.RowPitch);
var bytes = new Span<byte>(mapped.pData, checked((int)mapped.DepthPitch)).ToArray();
return (specs, bytes);
}
@ -229,10 +231,10 @@ internal sealed partial class TextureManager
private async Task<ComPtr<ID3D11Texture2D>> NoThrottleCreateFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,
DXGI_FORMAT format)
ExistingTextureModificationArgs args)
{
args.ThrowOnInvalidValues();
using var texSrv = default(ComPtr<ID3D11ShaderResourceView>);
using var context = default(ComPtr<ID3D11DeviceContext>);
using var tex2D = default(ComPtr<ID3D11Texture2D>);
@ -254,33 +256,34 @@ internal sealed partial class TextureManager
tex2D.Get()->GetDesc(&texDesc);
}
if (format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN)
format = texDesc.Format;
var newWidth = checked((uint)MathF.Round((uv1.X - uv0.X) * texDesc.Width));
var newHeight = checked((uint)MathF.Round((uv1.Y - uv0.Y) * texDesc.Height));
if (args.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN)
args = args with { Format = texDesc.Format };
if (args.NewWidth == 0)
args = args with { NewWidth = (int)MathF.Round((args.Uv1Effective.X - args.Uv0.X) * texDesc.Width) };
if (args.NewHeight == 0)
args = args with { NewHeight = (int)MathF.Round((args.Uv1Effective.Y - args.Uv0.Y) * texDesc.Height) };
using var tex2DCopyTemp = default(ComPtr<ID3D11Texture2D>);
unsafe
{
var tex2DCopyTempDesc = new D3D11_TEXTURE2D_DESC
{
Width = newWidth,
Height = newHeight,
Width = (uint)args.NewWidth,
Height = (uint)args.NewHeight,
MipLevels = 1,
ArraySize = 1,
Format = format,
Format = args.Format,
SampleDesc = new(1, 0),
Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT,
BindFlags =
(uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET),
BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE |
D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET),
CPUAccessFlags = 0u,
MiscFlags = 0u,
};
this.Device.Get()->CreateTexture2D(&tex2DCopyTempDesc, null, tex2DCopyTemp.GetAddressOf()).ThrowOnError();
}
await this.interfaceManager.RunBeforePresent(
await this.interfaceManager.RunBeforeImGuiRender(
() =>
{
unsafe
@ -294,292 +297,20 @@ internal sealed partial class TextureManager
&rtvCopyTempDesc,
rtvCopyTemp.GetAddressOf()).ThrowOnError();
this.drawsOneSquare ??= new();
this.drawsOneSquare.Setup(this.Device.Get());
context.Get()->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null);
this.drawsOneSquare.Draw(
this.SimpleDrawer.Draw(
context.Get(),
texSrv.Get(),
(int)newWidth,
(int)newHeight,
uv0,
uv1);
context.Get()->OMSetRenderTargets(0, null, null);
args.Uv0,
args.Uv1Effective);
if (args.MakeOpaque)
this.SimpleDrawer.StripAlpha(context.Get());
var dummy = default(ID3D11RenderTargetView*);
context.Get()->OMSetRenderTargets(1u, &dummy, null);
}
});
return new(tex2DCopyTemp);
}
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
private sealed unsafe class DrawsOneSquare : IDisposable
{
private ComPtr<ID3D11SamplerState> sampler;
private ComPtr<ID3D11VertexShader> vertexShader;
private ComPtr<ID3D11PixelShader> pixelShader;
private ComPtr<ID3D11InputLayout> inputLayout;
private ComPtr<ID3D11Buffer> vertexConstantBuffer;
private ComPtr<ID3D11BlendState> blendState;
private ComPtr<ID3D11RasterizerState> rasterizerState;
private ComPtr<ID3D11Buffer> vertexBufferFill;
private ComPtr<ID3D11Buffer> vertexBufferMutable;
private ComPtr<ID3D11Buffer> indexBuffer;
~DrawsOneSquare() => this.Dispose();
public void Dispose()
{
this.sampler.Reset();
this.vertexShader.Reset();
this.pixelShader.Reset();
this.inputLayout.Reset();
this.vertexConstantBuffer.Reset();
this.blendState.Reset();
this.rasterizerState.Reset();
this.vertexBufferFill.Reset();
this.vertexBufferMutable.Reset();
this.indexBuffer.Reset();
}
public void Setup<T>(T* device) where T : unmanaged, ID3D11Device.Interface
{
var assembly = typeof(ImGuiScene.ImGui_Impl_DX11).Assembly;
// Create the vertex shader
if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty())
{
this.vertexShader.Reset();
this.inputLayout.Reset();
using var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!;
var array = ArrayPool<byte>.Shared.Rent((int)stream.Length);
stream.ReadExactly(array, 0, (int)stream.Length);
fixed (byte* pArray = array)
fixed (ID3D11VertexShader** ppShader = &this.vertexShader.GetPinnableReference())
fixed (ID3D11InputLayout** ppInputLayout = &this.inputLayout.GetPinnableReference())
fixed (void* pszPosition = "POSITION"u8)
fixed (void* pszTexCoord = "TEXCOORD"u8)
fixed (void* pszColor = "COLOR"u8)
{
device->CreateVertexShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError();
var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[]
{
new()
{
SemanticName = (sbyte*)pszPosition,
Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT,
AlignedByteOffset = uint.MaxValue,
},
new()
{
SemanticName = (sbyte*)pszTexCoord,
Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT,
AlignedByteOffset = uint.MaxValue,
},
new()
{
SemanticName = (sbyte*)pszColor,
Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
AlignedByteOffset = uint.MaxValue,
},
};
device->CreateInputLayout(ied, 3, pArray, (nuint)stream.Length, ppInputLayout).ThrowOnError();
}
ArrayPool<byte>.Shared.Return(array);
}
// Create the constant buffer
if (this.vertexConstantBuffer.IsEmpty())
{
var bufferDesc = new D3D11_BUFFER_DESC(
(uint)sizeof(Matrix4x4),
(uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER,
D3D11_USAGE.D3D11_USAGE_IMMUTABLE);
var data = Matrix4x4.Identity;
var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = &data };
fixed (ID3D11Buffer** ppBuffer = &this.vertexConstantBuffer.GetPinnableReference())
device->CreateBuffer(&bufferDesc, &subr, ppBuffer).ThrowOnError();
}
// Create the pixel shader
if (this.pixelShader.IsEmpty())
{
using var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!;
var array = ArrayPool<byte>.Shared.Rent((int)stream.Length);
stream.ReadExactly(array, 0, (int)stream.Length);
fixed (byte* pArray = array)
fixed (ID3D11PixelShader** ppShader = &this.pixelShader.GetPinnableReference())
device->CreatePixelShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError();
ArrayPool<byte>.Shared.Return(array);
}
// Create the blending setup
if (this.blendState.IsEmpty())
{
var blendStateDesc = new D3D11_BLEND_DESC
{
RenderTarget =
{
e0 =
{
BlendEnable = true,
SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA,
DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA,
BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA,
DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE,
BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD,
RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL,
},
},
};
fixed (ID3D11BlendState** ppBlendState = &this.blendState.GetPinnableReference())
device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError();
}
// Create the rasterizer state
if (this.rasterizerState.IsEmpty())
{
var rasterizerDesc = new D3D11_RASTERIZER_DESC
{
FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID,
CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE,
};
fixed (ID3D11RasterizerState** ppRasterizerState = &this.rasterizerState.GetPinnableReference())
device->CreateRasterizerState(&rasterizerDesc, ppRasterizerState).ThrowOnError();
}
// Create the font sampler
if (this.sampler.IsEmpty())
{
var samplerDesc = new D3D11_SAMPLER_DESC(
D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR,
D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP,
0f,
0,
D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS,
null,
0,
0);
fixed (ID3D11SamplerState** ppSampler = &this.sampler.GetPinnableReference())
device->CreateSamplerState(&samplerDesc, ppSampler).ThrowOnError();
}
if (this.vertexBufferFill.IsEmpty())
{
var data = stackalloc ImDrawVert[]
{
new() { col = uint.MaxValue, pos = new(-1, 1), uv = new(0, 0) },
new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(0, 1) },
new() { col = uint.MaxValue, pos = new(1, 1), uv = new(1, 0) },
new() { col = uint.MaxValue, pos = new(1, -1), uv = new(1, 1) },
};
var desc = new D3D11_BUFFER_DESC(
(uint)(sizeof(ImDrawVert) * 4),
(uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER,
D3D11_USAGE.D3D11_USAGE_IMMUTABLE);
var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data };
var buffer = default(ID3D11Buffer*);
device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError();
this.vertexBufferFill.Attach(buffer);
}
if (this.vertexBufferMutable.IsEmpty())
{
var desc = new D3D11_BUFFER_DESC(
(uint)(sizeof(ImDrawVert) * 4),
(uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER,
D3D11_USAGE.D3D11_USAGE_DYNAMIC,
(uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE);
var buffer = default(ID3D11Buffer*);
device->CreateBuffer(&desc, null, &buffer).ThrowOnError();
this.vertexBufferMutable.Attach(buffer);
}
if (this.indexBuffer.IsEmpty())
{
var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 };
var desc = new D3D11_BUFFER_DESC(
sizeof(ushort) * 6,
(uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER,
D3D11_USAGE.D3D11_USAGE_IMMUTABLE);
var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data };
var buffer = default(ID3D11Buffer*);
device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError();
this.indexBuffer.Attach(buffer);
}
}
public void Draw(
ID3D11DeviceContext* ctx,
ID3D11ShaderResourceView* srv,
int width,
int height,
Vector2 uv0,
Vector2 uv1)
{
ID3D11Buffer* buffer;
if (uv0 == Vector2.Zero && uv1 == Vector2.One)
{
buffer = this.vertexBufferFill.Get();
}
else
{
buffer = this.vertexBufferMutable.Get();
var mapped = default(D3D11_MAPPED_SUBRESOURCE);
ctx->Map((ID3D11Resource*)buffer, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0u, &mapped).ThrowOnError();
_ = new Span<ImDrawVert>(mapped.pData, 4)
{
[0] = new() { col = uint.MaxValue, pos = new(-1, 1), uv = uv0 },
[1] = new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(uv0.X, uv1.Y) },
[2] = new() { col = uint.MaxValue, pos = new(1, 1), uv = new(uv1.X, uv0.Y) },
[3] = new() { col = uint.MaxValue, pos = new(1, -1), uv = uv1 },
};
ctx->Unmap((ID3D11Resource*)buffer, 0u);
}
var stride = (uint)sizeof(ImDrawVert);
var offset = 0u;
ctx->IASetInputLayout(this.inputLayout);
ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset);
ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0);
ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
var viewport = new D3D11_VIEWPORT(0, 0, width, height);
ctx->RSSetState(this.rasterizerState);
ctx->RSSetViewports(1, &viewport);
var blendColor = default(Vector4);
ctx->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff);
ctx->OMSetDepthStencilState(null, 0);
ctx->VSSetShader(this.vertexShader.Get(), null, 0);
buffer = this.vertexConstantBuffer.Get();
ctx->VSSetConstantBuffers(0, 1, &buffer);
ctx->PSSetShader(this.pixelShader, null, 0);
var simp = this.sampler.Get();
ctx->PSSetSamplers(0, 1, &simp);
ctx->PSSetShaderResources(0, 1, &srv);
ctx->GSSetShader(null, null, 0);
ctx->HSSetShader(null, null, 0);
ctx->DSSetShader(null, null, 0);
ctx->CSSetShader(null, null, 0);
ctx->DrawIndexed(6, 0, 0);
var ppn = default(ID3D11ShaderResourceView*);
ctx->PSSetShaderResources(0, 1, &ppn);
}
}
}

View file

@ -50,7 +50,7 @@ internal sealed partial class TextureManager
this.textureManager = textureManager;
this.textureManager.framework.Update += this.FrameworkOnUpdate;
}
/// <summary>Gets all the loaded textures from game resources.</summary>
public ICollection<SharedImmediateTexture> ForDebugGamePathTextures => this.gameDict.Values;

View file

@ -3,8 +3,6 @@ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -43,13 +41,42 @@ internal sealed partial class TextureManager
CancellationToken cancellationToken = default)
{
using var wrapDispose = leaveWrapOpen ? null : wrap;
var texDesc = default(D3D11_TEXTURE2D_DESC);
unsafe
{
using var texSrv = default(ComPtr<ID3D11ShaderResourceView>);
using var context = default(ComPtr<ID3D11DeviceContext>);
using var tex2D = default(ComPtr<ID3D11Texture2D>);
fixed (Guid* piid = &IID.IID_ID3D11ShaderResourceView)
((IUnknown*)wrap.ImGuiHandle)->QueryInterface(piid, (void**)texSrv.GetAddressOf()).ThrowOnError();
this.Device.Get()->GetImmediateContext(context.GetAddressOf());
using (var texRes = default(ComPtr<ID3D11Resource>))
{
texSrv.Get()->GetResource(texRes.GetAddressOf());
using var tex2DTemp = default(ComPtr<ID3D11Texture2D>);
texRes.As(&tex2DTemp).ThrowOnError();
tex2D.Swap(&tex2DTemp);
}
tex2D.Get()->GetDesc(&texDesc);
}
var dxgiFormat = texDesc.Format;
if (!WicManager.GetCorrespondingWicPixelFormat(dxgiFormat, out _, out _))
dxgiFormat = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM;
using var istream = ManagedIStream.Create(stream, leaveStreamOpen);
var (specs, bytes) = await this.GetRawDataFromExistingTextureAsync(
wrap,
Vector2.Zero,
Vector2.One,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM,
new()
{
Format = dxgiFormat,
},
true,
cancellationToken).ConfigureAwait(false);
@ -80,9 +107,9 @@ internal sealed partial class TextureManager
containerGuid,
File.Create(pathTemp),
props,
true,
false,
cancellationToken);
leaveWrapOpen: true,
leaveStreamOpen: false,
cancellationToken: cancellationToken);
}
catch (Exception e)
{
@ -206,6 +233,7 @@ internal sealed partial class TextureManager
{
private readonly TextureManager textureManager;
private ComPtr<IWICImagingFactory> wicFactory;
private ComPtr<IWICImagingFactory2> wicFactory2;
/// <summary>Initializes a new instance of the <see cref="WicManager"/> class.</summary>
/// <param name="textureManager">An instance of <see cref="Interface.Textures.Internal.TextureManager"/>.</param>
@ -216,13 +244,27 @@ internal sealed partial class TextureManager
{
fixed (Guid* pclsidWicImagingFactory = &CLSID.CLSID_WICImagingFactory)
fixed (Guid* piidWicImagingFactory = &IID.IID_IWICImagingFactory)
fixed (Guid* pclsidWicImagingFactory2 = &CLSID.CLSID_WICImagingFactory2)
fixed (Guid* piidWicImagingFactory2 = &IID.IID_IWICImagingFactory2)
{
CoCreateInstance(
pclsidWicImagingFactory,
null,
(uint)CLSCTX.CLSCTX_INPROC_SERVER,
piidWicImagingFactory,
(void**)this.wicFactory.GetAddressOf()).ThrowOnError();
if (CoCreateInstance(
pclsidWicImagingFactory2,
null,
(uint)CLSCTX.CLSCTX_INPROC_SERVER,
piidWicImagingFactory2,
(void**)this.wicFactory2.GetAddressOf()).SUCCEEDED)
{
this.wicFactory2.As(ref this.wicFactory).ThrowOnError();
}
else
{
CoCreateInstance(
pclsidWicImagingFactory,
null,
(uint)CLSCTX.CLSCTX_INPROC_SERVER,
piidWicImagingFactory,
(void**)this.wicFactory.GetAddressOf()).ThrowOnError();
}
}
}
}
@ -232,6 +274,76 @@ internal sealed partial class TextureManager
/// </summary>
~WicManager() => this.ReleaseUnmanagedResource();
/// <summary>
/// Gets the corresponding <see cref="DXGI_FORMAT"/> from a <see cref="Guid"/> containing a WIC pixel format.
/// </summary>
/// <param name="fmt">The WIC pixel format.</param>
/// <returns>The corresponding <see cref="DXGI_FORMAT"/>, or <see cref="DXGI_FORMAT.DXGI_FORMAT_UNKNOWN"/> if
/// unavailable.</returns>
public static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch
{
// See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile
_ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT
.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM,
_ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN,
};
/// <summary>
/// Gets the corresponding <see cref="Guid"/> containing a WIC pixel format from a <see cref="DXGI_FORMAT"/>.
/// </summary>
/// <param name="dxgiPixelFormat">The DXGI pixel format.</param>
/// <param name="wicPixelFormat">The corresponding <see cref="Guid"/>.</param>
/// <param name="srgb">Whether the image is in SRGB.</param>
/// <returns><c>true</c> if a corresponding pixel format exists.</returns>
public static bool GetCorrespondingWicPixelFormat(
DXGI_FORMAT dxgiPixelFormat,
out Guid wicPixelFormat,
out bool srgb)
{
wicPixelFormat = dxgiPixelFormat switch
{
// See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile
DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat,
DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat64bppRGBAHalf,
DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppRGBA,
DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102XR,
DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102,
DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551,
DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565,
DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat32bppGrayFloat,
DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat16bppGrayHalf,
DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat16bppGray,
DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray,
DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppAlpha,
DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM => GUID.GUID_WICPixelFormat32bppRGBA,
DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => GUID.GUID_WICPixelFormat32bppRGBA,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => GUID.GUID_WICPixelFormat32bppBGRA,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => GUID.GUID_WICPixelFormat32bppBGRA,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => GUID.GUID_WICPixelFormat32bppBGR,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB => GUID.GUID_WICPixelFormat32bppBGR,
_ => Guid.Empty,
};
srgb = dxgiPixelFormat
is DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB
or DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB
or DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
return wicPixelFormat != Guid.Empty;
}
/// <inheritdoc/>
public void Dispose()
{
@ -349,7 +461,7 @@ internal sealed partial class TextureManager
bitmapLock.Get()->GetDataPointer(&cbBufferSize, &pbData).ThrowOnError();
bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError();
return this.textureManager.NoThrottleCreateFromRaw(
new(rcLock.Width, rcLock.Height, (int)stride, (int)dxgiFormat),
new(rcLock.Width, rcLock.Height, (int)dxgiFormat, (int)stride),
new(pbData, (int)cbBufferSize));
}
@ -382,15 +494,13 @@ internal sealed partial class TextureManager
/// <param name="cancellationToken">The cancellation token.</param>
public unsafe void SaveToStreamUsingWic(
RawImageSpecification specs,
byte[] bytes,
ReadOnlySpan<byte> bytes,
Guid containerFormat,
ComPtr<IStream> stream,
IReadOnlyDictionary<string, object>? props = null,
CancellationToken cancellationToken = default)
{
var outPixelFormat = GUID.GUID_WICPixelFormat32bppBGRA;
var inPixelFormat = GetCorrespondingWicPixelFormat((DXGI_FORMAT)specs.DxgiFormat);
if (inPixelFormat == Guid.Empty)
if (!GetCorrespondingWicPixelFormat((DXGI_FORMAT)specs.DxgiFormat, out var inPixelFormat, out var srgb))
throw new NotSupportedException("DXGI_FORMAT from specs is not supported by WIC.");
using var encoder = default(ComPtr<IWICBitmapEncoder>);
@ -398,6 +508,24 @@ internal sealed partial class TextureManager
this.wicFactory.Get()->CreateEncoder(&containerFormat, null, encoder.GetAddressOf()).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
// See: DirectXTK/Src/ScreenGrab.cpp
var outPixelFormat = (DXGI_FORMAT)specs.DxgiFormat switch
{
DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat,
DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT when !this.wicFactory2.IsEmpty() =>
GUID.GUID_WICPixelFormat128bppRGBAFloat,
DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat32bppBGRA,
DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppBGRA,
DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551,
DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565,
DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat8bppGray,
DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat8bppGray,
DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat8bppGray,
DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray,
DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppGray,
_ => GUID.GUID_WICPixelFormat24bppBGR,
};
encoder.Get()->Initialize(stream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache)
.ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
@ -406,35 +534,14 @@ internal sealed partial class TextureManager
encoder.Get()->CreateNewFrame(encoderFrame.GetAddressOf(), propertyBag.GetAddressOf()).ThrowOnError();
cancellationToken.ThrowIfCancellationRequested();
// Opt-in to the WIC2 support for writing 32-bit Windows BMP files with an alpha channel
if (containerFormat == GUID.GUID_ContainerFormatBmp && !this.wicFactory2.IsEmpty())
propertyBag.Get()->Write("EnableV5Header32bppBGRA", true).ThrowOnError();
if (props is not null)
{
var nprop = 0u;
propertyBag.Get()->CountProperties(&nprop).ThrowOnError();
for (var i = 0u; i < nprop; i++)
{
var pbag2 = default(PROPBAG2);
var npropread = 0u;
propertyBag.Get()->GetPropertyInfo(i, 1, &pbag2, &npropread).ThrowOnError();
if (npropread == 0)
continue;
try
{
var propName = new string((char*)pbag2.pstrName);
if (props.TryGetValue(propName, out var untypedValue))
{
VARIANT val;
// Marshal calls VariantInit.
Marshal.GetNativeVariantForObject(untypedValue, (nint)(&val));
VariantChangeType(&val, &val, 0, pbag2.vt).ThrowOnError();
propertyBag.Get()->Write(1, &pbag2, &val).ThrowOnError();
VariantClear(&val);
}
}
finally
{
CoTaskMemFree(pbag2.pstrName);
}
}
foreach (var (name, untypedValue) in props)
propertyBag.Get()->Write(name, untypedValue).ThrowOnError();
}
encoderFrame.Get()->Initialize(propertyBag).ThrowOnError();
@ -442,6 +549,34 @@ internal sealed partial class TextureManager
encoderFrame.Get()->SetPixelFormat(&outPixelFormat).ThrowOnError();
encoderFrame.Get()->SetSize(checked((uint)specs.Width), checked((uint)specs.Height)).ThrowOnError();
using (var metaWriter = default(ComPtr<IWICMetadataQueryWriter>))
{
if (encoderFrame.Get()->GetMetadataQueryWriter(metaWriter.GetAddressOf()).SUCCEEDED)
{
if (containerFormat == GUID.GUID_ContainerFormatPng)
{
// Set sRGB chunk
if (srgb)
{
_ = metaWriter.Get()->SetMetadataByName("/sRGB/RenderingIntent", (byte)0);
}
else
{
// add gAMA chunk with gamma 1.0
// gama value * 100,000 -- i.e. gamma 1.0
_ = metaWriter.Get()->SetMetadataByName("/sRGB/RenderingIntent", 100000U);
// remove sRGB chunk which is added by default.
_ = metaWriter.Get()->RemoveMetadataByName("/sRGB/RenderingIntent");
}
}
else
{
// Set EXIF Colorspace of sRGB
_ = metaWriter.Get()->SetMetadataByName("System.Image.ColorSpace", (ushort)0);
}
}
}
using var tempBitmap = default(ComPtr<IWICBitmap>);
fixed (byte* pBytes = bytes)
@ -480,61 +615,11 @@ internal sealed partial class TextureManager
encoder.Get()->Commit().ThrowOnError();
}
/// <summary>
/// Gets the corresponding <see cref="DXGI_FORMAT"/> from a <see cref="Guid"/> containing a WIC pixel format.
/// </summary>
/// <param name="fmt">The WIC pixel format.</param>
/// <returns>The corresponding <see cref="DXGI_FORMAT"/>, or <see cref="DXGI_FORMAT.DXGI_FORMAT_UNKNOWN"/> if
/// unavailable.</returns>
private static DXGI_FORMAT GetCorrespondingDxgiFormat(Guid fmt) => 0 switch
private void ReleaseUnmanagedResource()
{
// See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile
_ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT
.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT,
_ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM,
_ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM,
_ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN,
};
/// <summary>
/// Gets the corresponding <see cref="Guid"/> containing a WIC pixel format from a <see cref="DXGI_FORMAT"/>.
/// </summary>
/// <param name="fmt">The DXGI pixel format.</param>
/// <returns>The corresponding <see cref="Guid"/>, or <see cref="Guid.Empty"/> if unavailable.</returns>
private static Guid GetCorrespondingWicPixelFormat(DXGI_FORMAT fmt) => fmt switch
{
// See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile
DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT => GUID.GUID_WICPixelFormat128bppRGBAFloat,
DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT => GUID.GUID_WICPixelFormat64bppRGBAHalf,
DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM => GUID.GUID_WICPixelFormat64bppRGBA,
DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102XR,
DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM => GUID.GUID_WICPixelFormat32bppRGBA1010102,
DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM => GUID.GUID_WICPixelFormat16bppBGRA5551,
DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM => GUID.GUID_WICPixelFormat16bppBGR565,
DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT => GUID.GUID_WICPixelFormat32bppGrayFloat,
DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT => GUID.GUID_WICPixelFormat16bppGrayHalf,
DXGI_FORMAT.DXGI_FORMAT_R16_UNORM => GUID.GUID_WICPixelFormat16bppGray,
DXGI_FORMAT.DXGI_FORMAT_R8_UNORM => GUID.GUID_WICPixelFormat8bppGray,
DXGI_FORMAT.DXGI_FORMAT_A8_UNORM => GUID.GUID_WICPixelFormat8bppAlpha,
DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM => GUID.GUID_WICPixelFormat32bppRGBA,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM => GUID.GUID_WICPixelFormat32bppBGRA,
DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM => GUID.GUID_WICPixelFormat32bppBGR,
_ => Guid.Empty,
};
private void ReleaseUnmanagedResource() => this.wicFactory.Reset();
this.wicFactory.Reset();
this.wicFactory2.Reset();
}
private readonly struct ComponentEnumerable<T> : IEnumerable<ComPtr<T>>
where T : unmanaged, IWICComponentInfo.Interface

View file

@ -63,7 +63,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu
this.wicManager = new(this);
}
/// <summary>Gets the D3D11 Device used to create textures.</summary>
/// <summary>Gets the D3D11 Device used to create textures. Ownership is not transferred.</summary>
public unsafe ComPtr<ID3D11Device> Device
{
get
@ -80,15 +80,19 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu
}
}
/// <summary>Gets a simpler drawer.</summary>
public SimpleDrawerImpl SimpleDrawer =>
this.simpleDrawer ?? throw new ObjectDisposedException(nameof(TextureManager));
/// <summary>Gets the shared texture manager.</summary>
public SharedTextureManager Shared =>
this.sharedTextureManager ??
throw new ObjectDisposedException(nameof(this.sharedTextureManager));
throw new ObjectDisposedException(nameof(TextureManager));
/// <summary>Gets the WIC manager.</summary>
public WicManager Wic =>
this.wicManager ??
throw new ObjectDisposedException(nameof(this.sharedTextureManager));
throw new ObjectDisposedException(nameof(TextureManager));
/// <inheritdoc/>
public void Dispose()
@ -98,9 +102,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu
this.disposing = true;
this.drawsOneSquare?.Dispose();
this.drawsOneSquare = null;
Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose();
Interlocked.Exchange(ref this.sharedTextureManager, null)?.Dispose();
Interlocked.Exchange(ref this.wicManager, null)?.Dispose();
}
@ -201,7 +203,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu
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;
}
@ -232,7 +234,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu
var subrdata = new D3D11_SUBRESOURCE_DATA { pSysMem = dataPtr, SysMemPitch = (uint)specs.Pitch };
device.Get()->CreateTexture2D(&texd, &subrdata, texture.GetAddressOf()).ThrowOnError();
}
var viewDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC
{
Format = texd.Format,
@ -264,7 +266,7 @@ internal sealed partial class TextureManager : IServiceType, IDisposable, ITextu
}
return this.NoThrottleCreateFromRaw(
RawImageSpecification.From(buffer.Width, buffer.Height, dxgiFormat),
new(buffer.Width, buffer.Height, dxgiFormat),
buffer.RawData);
}

View file

@ -0,0 +1,239 @@
using System.Diagnostics;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
using ImGuiNET;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using NotSupportedException = System.NotSupportedException;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>A texture wrap that takes its buffer from the frame buffer (of swap chain).</summary>
internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable
{
private readonly uint viewportId;
private readonly bool beforeImGuiRender;
private readonly CancellationToken cancellationToken;
private readonly TaskCompletionSource<IDalamudTextureWrap> firstUpdateTaskCompletionSource = new();
private D3D11_TEXTURE2D_DESC desc;
private ComPtr<ID3D11Texture2D> tex;
private ComPtr<ID3D11ShaderResourceView> srv;
private ComPtr<ID3D11RenderTargetView> rtv;
private bool autoUpdate;
private bool disposed;
/// <summary>Initializes a new instance of the <see cref="ViewportTextureWrap"/> class.</summary>
/// <param name="viewportId">The source viewport ID.</param>
/// <param name="beforeImGuiRender">Capture before calling <see cref="ImGui.Render"/>.</param>
/// <param name="autoUpdate">If <c>true</c>, automatically update the underlying texture.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public ViewportTextureWrap(
uint viewportId,
bool beforeImGuiRender,
bool autoUpdate,
CancellationToken cancellationToken)
{
this.viewportId = viewportId;
this.beforeImGuiRender = beforeImGuiRender;
this.autoUpdate = autoUpdate;
this.cancellationToken = cancellationToken;
}
/// <summary>Finalizes an instance of the <see cref="ViewportTextureWrap"/> class.</summary>
~ViewportTextureWrap() => this.Dispose(false);
/// <inheritdoc/>
public unsafe nint ImGuiHandle => (nint)this.srv.Get();
/// <inheritdoc/>
public int Width => (int)this.desc.Width;
/// <inheritdoc/>
public int Height => (int)this.desc.Height;
/// <summary>Gets the task representing the first <see cref="Update"/> call.</summary>
public Task<IDalamudTextureWrap> FirstUpdateTask => this.firstUpdateTaskCompletionSource.Task;
/// <summary>Updates the texture from the source viewport.</summary>
public unsafe void Update()
{
if (this.cancellationToken.IsCancellationRequested || this.disposed)
{
this.firstUpdateTaskCompletionSource.TrySetCanceled();
return;
}
try
{
ThreadSafety.AssertMainThread();
using var backBuffer = GetImGuiViewportBackBuffer(this.viewportId);
D3D11_TEXTURE2D_DESC newDesc;
backBuffer.Get()->GetDesc(&newDesc);
if (newDesc.SampleDesc.Count > 1)
throw new NotSupportedException("Multisampling is not expected");
using var device = default(ComPtr<ID3D11Device>);
backBuffer.Get()->GetDevice(device.GetAddressOf());
using var context = default(ComPtr<ID3D11DeviceContext>);
device.Get()->GetImmediateContext(context.GetAddressOf());
if (this.desc.Width != newDesc.Width
|| this.desc.Height != newDesc.Height
|| this.desc.Format != newDesc.Format)
{
var texDesc = new D3D11_TEXTURE2D_DESC
{
Width = newDesc.Width,
Height = newDesc.Height,
MipLevels = 1,
ArraySize = 1,
Format = newDesc.Format,
SampleDesc = new(1, 0),
Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT,
BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE |
D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET),
CPUAccessFlags = 0u,
MiscFlags = 0u,
};
using var texTemp = default(ComPtr<ID3D11Texture2D>);
device.Get()->CreateTexture2D(&texDesc, null, texTemp.GetAddressOf()).ThrowOnError();
var rtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC(
texTemp,
D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D);
using var rtvTemp = default(ComPtr<ID3D11RenderTargetView>);
device.Get()->CreateRenderTargetView(
(ID3D11Resource*)texTemp.Get(),
&rtvDesc,
rtvTemp.GetAddressOf()).ThrowOnError();
var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC(
texTemp,
D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D);
using var srvTemp = default(ComPtr<ID3D11ShaderResourceView>);
device.Get()->CreateShaderResourceView(
(ID3D11Resource*)texTemp.Get(),
&srvDesc,
srvTemp.GetAddressOf())
.ThrowOnError();
this.desc = newDesc;
srvTemp.Swap(ref this.srv);
rtvTemp.Swap(ref this.rtv);
texTemp.Swap(ref this.tex);
}
context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get());
var rtvLocal = this.rtv.Get();
context.Get()->OMSetRenderTargets(1u, &rtvLocal, null);
Service<TextureManager>.Get().SimpleDrawer.StripAlpha(context.Get());
var dummy = default(ID3D11RenderTargetView*);
context.Get()->OMSetRenderTargets(1u, &dummy, null);
this.firstUpdateTaskCompletionSource.TrySetResult(this);
}
catch (Exception e)
{
this.firstUpdateTaskCompletionSource.TrySetException(e);
}
if (this.autoUpdate)
{
Service<Framework>.Get().RunOnTick(
() =>
{
if (this.beforeImGuiRender)
Service<InterfaceManager>.Get().RunBeforeImGuiRender(this.Update);
else
Service<InterfaceManager>.Get().RunAfterImGuiRender(this.Update);
},
cancellationToken: this.cancellationToken);
}
}
/// <summary>Queue the texture to be disposed once the frame ends. </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>Actually dispose the wrapped texture.</summary>
void IDeferredDisposable.RealDispose()
{
_ = this.FirstUpdateTask.Exception;
this.tex.Reset();
this.srv.Reset();
}
private static unsafe ComPtr<ID3D11Texture2D> GetImGuiViewportBackBuffer(uint viewportId)
{
ThreadSafety.AssertMainThread();
var viewports = ImGui.GetPlatformIO().Viewports;
var viewportIndex = 0;
for (; viewportIndex < viewports.Size; viewportIndex++)
{
if (viewports[viewportIndex].ID == viewportId)
break;
}
if (viewportIndex >= viewports.Size)
{
throw new ArgumentOutOfRangeException(
nameof(viewportId),
viewportId,
"Could not find a viewport with the given ID.");
}
var texture = default(ComPtr<ID3D11Texture2D>);
Debug.Assert(viewports[0].ID == ImGui.GetMainViewport().ID, "ImGui has changed");
if (viewportId == viewports[0].ID)
{
var device = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Device.Instance();
fixed (Guid* piid = &IID.IID_ID3D11Texture2D)
{
((IDXGISwapChain*)device->SwapChain->DXGISwapChain)->GetBuffer(0, piid, (void**)texture.GetAddressOf())
.ThrowOnError();
}
}
else
{
// See: ImGui_Impl_DX11.ImGuiViewportDataDx11
var rud = (nint*)viewports[viewportIndex].RendererUserData;
if (rud == null || rud[0] == nint.Zero || rud[1] == nint.Zero)
throw new InvalidOperationException();
using var resource = default(ComPtr<ID3D11Resource>);
((ID3D11RenderTargetView*)rud[viewportIndex])->GetResource(resource.GetAddressOf());
resource.As(&texture).ThrowOnError();
}
return texture;
}
private void Dispose(bool disposing)
{
this.disposed = true;
this.autoUpdate = false;
if (disposing)
Service<InterfaceManager>.GetNullable()?.EnqueueDeferredDispose(this);
else
((IDeferredDisposable)this).RealDispose();
}
}

View file

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Numerics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@ -34,32 +33,47 @@ public partial interface ITextureProvider
/// </summary>
/// <param name="wrap">The source texture wrap. The passed value may be disposed once this function returns,
/// without having to wait for the completion of the returned <see cref="Task{TResult}"/>.</param>
/// <param name="uv0">The left top coordinates relative to the size of the source texture.</param>
/// <param name="uv1">The right bottom coordinates relative to the size of the source texture.</param>
/// <param name="dxgiFormat">The desired target format. Use 0 to use the source format.</param>
/// <param name="args">The texture modification arguments.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the copied texture on success. Dispose after use.</returns>
/// <remarks>
/// <para>Coordinates in <paramref name="uv0"/> and <paramref name="uv1"/> should be in range between 0 and 1.
/// </para>
/// <para>Supported values for <paramref name="dxgiFormat"/> may not necessarily match
/// <see cref="IsDxgiFormatSupported"/>.</para>
/// </remarks>
/// <remarks><para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,
int dxgiFormat = 0,
ExistingTextureModificationArgs args = default,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default);
/// <summary>Creates a texture from the game screen, before rendering Dalamud.</summary>
/// <param name="autoUpdate">If <c>true</c>, automatically update the underlying texture.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the copied texture on success. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromGameScreen(
bool autoUpdate = false,
CancellationToken cancellationToken = default);
/// <summary>Creates a texture from the game screen, before rendering Dalamud.</summary>
/// <param name="viewportId">The viewport ID.</param>
/// <param name="autoUpdate">If <c>true</c>, automatically update the underlying texture.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the copied texture on success. Dispose after use.</returns>
/// <remarks>
/// <para>Use <c>ImGui.GetMainViewport().ID</c> to capture the game screen with Dalamud rendered.</para>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task<IDalamudTextureWrap> CreateFromImGuiViewport(
uint viewportId,
bool autoUpdate = false,
CancellationToken cancellationToken = default);
/// <summary>Gets a texture from the given bytes, trying to interpret it as a .tex file or other well-known image
/// files, such as .png.</summary>
/// <param name="bytes">The bytes to load.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromImageAsync(
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default);
@ -70,8 +84,10 @@ public partial interface ITextureProvider
/// <param name="leaveOpen">Whether to leave the stream open once the task completes, sucessfully or not.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
/// <remarks><paramref name="stream"/> will be closed or not only according to <paramref name="leaveOpen"/>;
/// <paramref name="cancellationToken"/> is irrelevant in closing the stream.</remarks>
/// <remarks>
/// <para><paramref name="stream"/> will be closed or not only according to <paramref name="leaveOpen"/>;
/// <paramref name="cancellationToken"/> is irrelevant in closing the stream.</para>
/// <para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromImageAsync(
Stream stream,
bool leaveOpen = false,
@ -81,6 +97,7 @@ public partial interface ITextureProvider
/// <param name="specs">The specifications for the raw bitmap.</param>
/// <param name="bytes">The bytes to load.</param>
/// <returns>The texture loaded from the supplied raw bitmap. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
IDalamudTextureWrap CreateFromRaw(
RawImageSpecification specs,
ReadOnlySpan<byte> bytes);
@ -90,6 +107,7 @@ public partial interface ITextureProvider
/// <param name="bytes">The bytes to load.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
ReadOnlyMemory<byte> bytes,
@ -101,8 +119,11 @@ public partial interface ITextureProvider
/// <param name="leaveOpen">Whether to leave the stream open once the task completes, sucessfully or not.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the loaded texture on success. Dispose after use.</returns>
/// <remarks><paramref name="stream"/> will be closed or not only according to <paramref name="leaveOpen"/>;
/// <paramref name="cancellationToken"/> is irrelevant in closing the stream.</remarks>
/// <remarks>
/// <para><paramref name="stream"/> will be closed or not only according to <paramref name="leaveOpen"/>;
/// <paramref name="cancellationToken"/> is irrelevant in closing the stream.</para>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task<IDalamudTextureWrap> CreateFromRawAsync(
RawImageSpecification specs,
Stream stream,
@ -115,12 +136,14 @@ public partial interface ITextureProvider
/// </summary>
/// <param name="file">The texture to obtain a handle to.</param>
/// <returns>A texture wrap that can be used to render the texture. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
IDalamudTextureWrap CreateFromTexFile(TexFile file);
/// <summary>Get a texture handle for the specified Lumina <see cref="TexFile"/>.</summary>
/// <param name="file">The texture to obtain a handle to.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A texture wrap that can be used to render the texture. Dispose after use.</returns>
/// <remarks><para>This function may throw an exception.</para></remarks>
Task<IDalamudTextureWrap> CreateFromTexFileAsync(
TexFile file,
CancellationToken cancellationToken = default);
@ -128,13 +151,14 @@ public partial interface ITextureProvider
/// <summary>Gets the supported bitmap decoders.</summary>
/// <returns>The supported bitmap decoders.</returns>
/// <remarks>
/// The following functions support the files of the container types pointed by yielded values.
/// <para>The following functions support the files of the container types pointed by yielded values.</para>
/// <ul>
/// <li><see cref="GetFromFile"/></li>
/// <li><see cref="GetFromManifestResource"/></li>
/// <li><see cref="CreateFromImageAsync(ReadOnlyMemory{byte},CancellationToken)"/></li>
/// <li><see cref="CreateFromImageAsync(Stream,bool,CancellationToken)"/></li>
/// </ul>
/// <para>This function may throw an exception.</para>
/// </remarks>
IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos();
@ -145,32 +169,39 @@ public partial interface ITextureProvider
/// <ul>
/// <li><see cref="SaveToStreamAsync"/></li>
/// </ul>
/// <para>This function may throw an exception.</para>
/// </remarks>
IEnumerable<IBitmapCodecInfo> GetSupportedImageEncoderInfos();
/// <summary>Gets a shared texture corresponding to the given game resource icon specifier.</summary>
/// <param name="lookup">A game icon specifier.</param>
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
/// <remarks>This function is under the effect of <see cref="ITextureSubstitutionProvider.GetSubstitutedPath"/>.
/// <remarks>
/// <para>This function is under the effect of <see cref="ITextureSubstitutionProvider.GetSubstitutedPath"/>.</para>
/// <para>This function does not throw exceptions.</para>
/// </remarks>
ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup);
/// <summary>Gets a shared texture corresponding to the given path to a game resource.</summary>
/// <param name="path">A path to a game resource.</param>
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
/// <remarks>This function is under the effect of <see cref="ITextureSubstitutionProvider.GetSubstitutedPath"/>.
/// <remarks>
/// <para>This function is under the effect of <see cref="ITextureSubstitutionProvider.GetSubstitutedPath"/>.</para>
/// <para>This function does not throw exceptions.</para>
/// </remarks>
ISharedImmediateTexture GetFromGame(string path);
/// <summary>Gets a shared texture corresponding to the given file on the filesystem.</summary>
/// <param name="path">A path to a file on the filesystem.</param>
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
ISharedImmediateTexture GetFromFile(string path);
/// <summary>Gets a shared texture corresponding to the given file of the assembly manifest resources.</summary>
/// <param name="assembly">The assembly containing manifest resources.</param>
/// <param name="name">The case-sensitive name of the manifest resource being requested.</param>
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name);
/// <summary>Get a path for a specific icon's .tex file.</summary>
@ -185,14 +216,12 @@ public partial interface ITextureProvider
/// <param name="lookup">The icon lookup.</param>
/// <param name="path">The resolved path.</param>
/// <returns><c>true</c> if the corresponding file exists and <paramref name="path"/> has been set.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
bool TryGetIconPath(in GameIconLookup lookup, [NotNullWhen(true)] out string? path);
/// <summary>Gets the raw data of a texture wrap.</summary>
/// <param name="wrap">The source texture wrap.</param>
/// <param name="uv0">The left top coordinates relative to the size of the source texture.</param>
/// <param name="uv1">The right bottom coordinates relative to the size of the source texture.</param>
/// <param name="dxgiFormat">The desired target format.
/// If 0 (unknown) is passed, then the format will not be converted.</param>
/// <param name="args">The texture modification arguments.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
@ -200,16 +229,11 @@ public partial interface ITextureProvider
/// <remarks>
/// <para>The length of the returned <c>RawData</c> may not match
/// <see cref="RawImageSpecification.Height"/> * <see cref="RawImageSpecification.Pitch"/>.</para>
/// <para>If <paramref name="uv0"/> is <see cref="Vector2.Zero"/>,
/// <paramref name="uv1"/> is <see cref="Vector2.One"/>, and <paramref name="dxgiFormat"/> is <c>0</c>,
/// then the source data will be returned.</para>
/// <para>This function can fail.</para>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task<(RawImageSpecification Specification, byte[] RawData)> GetRawDataFromExistingTextureAsync(
IDalamudTextureWrap wrap,
Vector2 uv0,
Vector2 uv1,
int dxgiFormat = 0,
ExistingTextureModificationArgs args = default,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default);
@ -217,9 +241,7 @@ public partial interface ITextureProvider
/// <param name="wrap">The texture wrap to save.</param>
/// <param name="containerGuid">The container GUID, obtained from <see cref="GetSupportedImageEncoderInfos"/>.</param>
/// <param name="stream">The stream to save to.</param>
/// <param name="props">Properties to pass to the encoder. See
/// <a href="https://learn.microsoft.com/en-us/windows/win32/wic/-wic-creating-encoder#encoder-options">Microsoft
/// Learn</a> for available parameters.</param>
/// <param name="props">Properties to pass to the encoder. See remarks for valid values.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="leaveStreamOpen">Whether to leave <paramref name="stream"/> open when the returned
@ -228,6 +250,15 @@ public partial interface ITextureProvider
/// <returns>A task representing the save process.</returns>
/// <remarks>
/// <para><paramref name="wrap"/> must not be disposed until the task finishes.</para>
/// <para>See the following webpages for the valid values for <paramref name="props"/> per
/// <paramref name="containerGuid"/>.</para>
/// <ul>
/// <li><a href="https://learn.microsoft.com/en-us/windows/win32/wic/native-wic-codecs">
/// WIC Codecs from Microsoft</a></li>
/// <li><a href="https://learn.microsoft.com/en-us/windows/win32/wic/-wic-creating-encoder#encoder-options">
/// Image Encoding Overview: Encoder options</a></li>
/// </ul>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task SaveToStreamAsync(
IDalamudTextureWrap wrap,
@ -242,15 +273,22 @@ public partial interface ITextureProvider
/// <param name="wrap">The texture wrap to save.</param>
/// <param name="containerGuid">The container GUID, obtained from <see cref="GetSupportedImageEncoderInfos"/>.</param>
/// <param name="path">The target file path. The target file will be overwritten if it exist.</param>
/// <param name="props">Properties to pass to the encoder. See
/// <a href="https://learn.microsoft.com/en-us/windows/win32/wic/-wic-creating-encoder#encoder-options">Microsoft
/// Learn</a> for available parameters.</param>
/// <param name="props">Properties to pass to the encoder. See remarks for valid values.</param>
/// <param name="leaveWrapOpen">Whether to leave <paramref name="wrap"/> non-disposed when the returned
/// <see cref="Task{TResult}"/> completes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the save process.</returns>
/// <remarks>
/// <para><paramref name="wrap"/> must not be disposed until the task finishes.</para>
/// <para>See the following webpages for the valid values for <paramref name="props"/> per
/// <paramref name="containerGuid"/>.</para>
/// <ul>
/// <li><a href="https://learn.microsoft.com/en-us/windows/win32/wic/native-wic-codecs">
/// WIC Codecs from Microsoft</a></li>
/// <li><a href="https://learn.microsoft.com/en-us/windows/win32/wic/-wic-creating-encoder#encoder-options">
/// Image Encoding Overview: Encoder options</a></li>
/// </ul>
/// <para>This function may throw an exception.</para>
/// </remarks>
Task SaveToFileAsync(
IDalamudTextureWrap wrap,
@ -266,11 +304,13 @@ public partial interface ITextureProvider
/// </summary>
/// <param name="dxgiFormat">The DXGI format.</param>
/// <returns><c>true</c> if supported.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
bool IsDxgiFormatSupported(int dxgiFormat);
/// <summary>Determines whether the system supports the given DXGI format for use with
/// <see cref="CreateFromExistingTextureAsync"/>.</summary>
/// <param name="dxgiFormat">The DXGI format.</param>
/// <returns><c>true</c> if supported.</returns>
/// <remarks><para>This function does not throw exceptions.</para></remarks>
bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat);
}

View file

@ -1,25 +1,53 @@
using System.Diagnostics.CodeAnalysis;
using TerraFX.Interop.DirectX;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Describes a raw image.
/// </summary>
/// <param name="Width">The width of the image.</param>
/// <param name="Height">The height of the image.</param>
/// <param name="Pitch">The pitch of the image in bytes. The value may not always exactly match
/// <c><paramref name="Width"/> * bytesPerPixelFromDxgiFormat</c>.</param>
/// <param name="DxgiFormat">The format of the image. See <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">DXGI_FORMAT</a>.</param>
[SuppressMessage(
"StyleCop.CSharp.NamingRules",
"SA1313:Parameter names should begin with lower-case letter",
Justification = "no")]
public readonly record struct RawImageSpecification(int Width, int Height, int Pitch, int DxgiFormat)
/// <summary>Describes a raw image.</summary>
public record struct RawImageSpecification
{
private const string FormatNotSupportedMessage = $"{nameof(DxgiFormat)} is not supported.";
/// <summary>Initializes a new instance of the <see cref="RawImageSpecification"/> class.</summary>
/// <param name="width">The width of the raw image.</param>
/// <param name="height">The height of the raw image.</param>
/// <param name="dxgiFormat">The DXGI format of the raw image.</param>
/// <param name="pitch">The pitch of the raw image in bytes.
/// Specify <c>-1</c> to calculate it from other parameters.</param>
public RawImageSpecification(int width, int height, int dxgiFormat, int pitch = -1)
{
if (pitch < 0)
{
if (!GetFormatInfo((DXGI_FORMAT)dxgiFormat, out var bitsPerPixel, out var isBlockCompression))
throw new NotSupportedException(FormatNotSupportedMessage);
pitch = isBlockCompression
? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel
: ((width * bitsPerPixel) + 7) / 8;
}
this.Width = width;
this.Height = height;
this.Pitch = pitch;
this.DxgiFormat = dxgiFormat;
}
/// <summary>Gets or sets the width of the raw image.</summary>
public int Width { get; set; }
/// <summary>Gets or sets the height of the raw image.</summary>
public int Height { get; set; }
/// <summary>Gets or sets the pitch of the raw image in bytes.</summary>
/// <remarks>The value may not always exactly match
/// <c><see cref="Width"/> * bytesPerPixelFromDxgiFormat</c>.
/// </remarks>
public int Pitch { get; set; }
/// <summary>Gets or sets the format of the raw image.</summary>
/// <remarks>See <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgiformat/ne-dxgiformat-dxgi_format">
/// DXGI_FORMAT</a>.</remarks>
public int DxgiFormat { get; set; }
/// <summary>Gets the number of bits per pixel.</summary>
/// <exception cref="NotSupportedException">Thrown if <see cref="DxgiFormat"/> is not supported.</exception>
public int BitsPerPixel =>
@ -27,27 +55,6 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P
? bitsPerPixel
: throw new NotSupportedException(FormatNotSupportedMessage);
/// <summary>
/// Creates a new instance of <see cref="RawImageSpecification"/> record using the given resolution and pixel
/// format. Pitch will be automatically calculated.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <param name="format">The format.</param>
/// <returns>The new instance.</returns>
/// <exception cref="NotSupportedException">Thrown if <see cref="DxgiFormat"/> is not supported.</exception>
public static RawImageSpecification From(int width, int height, int format)
{
if (!GetFormatInfo((DXGI_FORMAT)format, out var bitsPerPixel, out var isBlockCompression))
throw new NotSupportedException(FormatNotSupportedMessage);
var pitch = isBlockCompression
? Math.Max(1, (width + 3) / 4) * 2 * bitsPerPixel
: ((width * bitsPerPixel) + 7) / 8;
return new(width, height, pitch, format);
}
/// <summary>
/// Creates a new instance of <see cref="RawImageSpecification"/> record using the given resolution,
/// in B8G8R8A8(BGRA32) UNorm pixel format.
@ -56,7 +63,7 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P
/// <param name="height">The height.</param>
/// <returns>The new instance.</returns>
public static RawImageSpecification Bgra32(int width, int height) =>
new(width, height, width * 4, (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM);
new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, width * 4);
/// <summary>
/// Creates a new instance of <see cref="RawImageSpecification"/> record using the given resolution,
@ -66,7 +73,7 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P
/// <param name="height">The height.</param>
/// <returns>The new instance.</returns>
public static RawImageSpecification Rgba32(int width, int height) =>
new(width, height, width * 4, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM);
new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, width * 4);
/// <summary>
/// Creates a new instance of <see cref="RawImageSpecification"/> record using the given resolution,
@ -76,7 +83,7 @@ public readonly record struct RawImageSpecification(int Width, int Height, int P
/// <param name="height">The height.</param>
/// <returns>The new instance.</returns>
public static RawImageSpecification A8(int width, int height) =>
new(width, height, width, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM);
new(width, height, (int)DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, width);
private static bool GetFormatInfo(DXGI_FORMAT format, out int bitsPerPixel, out bool isBlockCompression)
{

View file

@ -83,6 +83,9 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
/// <inheritdoc/>
public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4);
/// <inheritdoc/>
public IDalamudTextureWrap White4X4 => this.GetDalamudTextureWrap(DalamudAsset.White4X4);
/// <inheritdoc/>
public void Dispose()
{

View file

@ -2,25 +2,21 @@
using SharpDX.DXGI;
using TerraFX.Interop.DirectX;
namespace Dalamud.Storage.Assets;
/// <summary>
/// Provide raw texture data directly.
/// </summary>
/// <summary>Provide raw texture data directly. </summary>
[AttributeUsage(AttributeTargets.Field)]
internal class DalamudAssetRawTextureAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudAssetRawTextureAttribute"/> class.
/// </summary>
/// <summary>Initializes a new instance of the <see cref="DalamudAssetRawTextureAttribute"/> class.</summary>
/// <param name="width">The width.</param>
/// <param name="pitch">The pitch.</param>
/// <param name="height">The height.</param>
/// <param name="format">The format.</param>
public DalamudAssetRawTextureAttribute(int width, int pitch, int height, Format format)
{
this.Specification = new(width, height, pitch, (int)format);
}
/// <param name="pitch">The pitch.</param>
public DalamudAssetRawTextureAttribute(int width, int height, DXGI_FORMAT format, int pitch) =>
this.Specification = new(width, height, (int)format, pitch);
/// <summary>
/// Gets the specification.

View file

@ -23,6 +23,11 @@ public interface IDalamudAssetManager
/// </summary>
IDalamudTextureWrap Empty4X4 { get; }
/// <summary>
/// Gets the shared texture wrap for <see cref="DalamudAsset.White4X4"/>.
/// </summary>
IDalamudTextureWrap White4X4 { get; }
/// <summary>
/// Gets whether the stream for the asset is instantly available.
/// </summary>

View file

@ -0,0 +1,83 @@
using System.Runtime.InteropServices;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Utility.TerraFxCom;
/// <summary>Utilities for <see cref="IUnknown"/> and its derivatives.</summary>
internal static unsafe partial class TerraFxComInterfaceExtensions
{
/// <summary>Calls <see cref="IPropertyBag2.Write"/>.</summary>
/// <param name="obj">The property bag.</param>
/// <param name="name">The name of the item to be interpreted as a VARIANT.</param>
/// <param name="value">The new value, to be interpreted as a <see cref="VARIANT"/>.</param>
/// <returns>Return value from <inheritdoc cref="IPropertyBag2.Write"/>.</returns>
public static HRESULT Write(ref this IPropertyBag2 obj, string name, object? value)
{
VARIANT varValue;
// Marshal calls VariantInit.
Marshal.GetNativeVariantForObject(value, (nint)(&varValue));
try
{
fixed (char* pName = name)
{
var option = new PROPBAG2 { pstrName = (ushort*)pName };
return obj.Write(1, &option, &varValue);
}
}
finally
{
VariantClear(&varValue);
}
}
/// <summary>Calls <inheritdoc cref="IWICMetadataQueryWriter.SetMetadataByName"/>.</summary>
/// <param name="obj">The object.</param>
/// <param name="name">The name of the metadata.</param>
/// <param name="value">The new value, to be interpreted as a <see cref="PROPVARIANT"/>.</param>
/// <returns>Return value from <inheritdoc cref="IWICMetadataQueryWriter.SetMetadataByName"/>.</returns>
public static HRESULT SetMetadataByName(ref this IWICMetadataQueryWriter obj, string name, object? value)
{
VARIANT varValue;
// Marshal calls VariantInit.
Marshal.GetNativeVariantForObject(value, (nint)(&varValue));
try
{
PROPVARIANT propVarValue;
var propVarRes = VariantToPropVariant(&varValue, &propVarValue);
if (propVarRes < 0)
return propVarRes;
try
{
fixed (char* pName = name)
return obj.SetMetadataByName((ushort*)pName, &propVarValue);
}
finally
{
_ = PropVariantClear(&propVarValue);
}
}
finally
{
_ = VariantClear(&varValue);
}
}
/// <summary>Calls <inheritdoc cref="IWICMetadataQueryWriter.SetMetadataByName"/>.</summary>
/// <param name="obj">The object.</param>
/// <param name="name">The name of the metadata.</param>
/// <returns>Return value from <inheritdoc cref="IWICMetadataQueryWriter.SetMetadataByName"/>.</returns>
public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name)
{
fixed (char* pName = name)
return obj.RemoveMetadataByName((ushort*)pName);
}
[LibraryImport("propsys.dll")]
private static partial int VariantToPropVariant(
void* pVarIn,
void* pPropVarOut);
}