diff --git a/Penumbra/Interop/Services/TextureArraySlicer.cs b/Penumbra/Interop/Services/TextureArraySlicer.cs
new file mode 100644
index 00000000..c934ac2b
--- /dev/null
+++ b/Penumbra/Interop/Services/TextureArraySlicer.cs
@@ -0,0 +1,119 @@
+using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
+using OtterGui.Services;
+using SharpDX.Direct3D;
+using SharpDX.Direct3D11;
+
+namespace Penumbra.Interop.Services;
+
+///
+/// Creates ImGui handles over slices of array textures, and manages their lifetime.
+///
+public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
+{
+ private const uint InitialTimeToLive = 2;
+
+ private readonly Dictionary<(nint XivTexture, byte SliceIndex), SliceState> _activeSlices = [];
+ private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = [];
+
+ /// Caching this across frames will cause a crash to desktop.
+ public nint GetImGuiHandle(Texture* texture, byte sliceIndex)
+ {
+ if (texture == null)
+ throw new ArgumentNullException(nameof(texture));
+ if (sliceIndex >= texture->ArraySize)
+ throw new ArgumentOutOfRangeException(nameof(sliceIndex), $"Slice index ({sliceIndex}) is greater than or equal to the texture array size ({texture->ArraySize})");
+ if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state))
+ {
+ state.Refresh();
+ return (nint)state.ShaderResourceView;
+ }
+ var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView;
+ var description = srv.Description;
+ switch (description.Dimension)
+ {
+ case ShaderResourceViewDimension.Texture1D:
+ case ShaderResourceViewDimension.Texture2D:
+ case ShaderResourceViewDimension.Texture2DMultisampled:
+ case ShaderResourceViewDimension.Texture3D:
+ case ShaderResourceViewDimension.TextureCube:
+ // This function treats these as single-slice arrays.
+ // As per the range check above, the only valid slice (i. e. 0) has been requested, therefore there is nothing to do.
+ break;
+ case ShaderResourceViewDimension.Texture1DArray:
+ description.Texture1DArray.FirstArraySlice = sliceIndex;
+ description.Texture2DArray.ArraySize = 1;
+ break;
+ case ShaderResourceViewDimension.Texture2DArray:
+ description.Texture2DArray.FirstArraySlice = sliceIndex;
+ description.Texture2DArray.ArraySize = 1;
+ break;
+ case ShaderResourceViewDimension.Texture2DMultisampledArray:
+ description.Texture2DMSArray.FirstArraySlice = sliceIndex;
+ description.Texture2DMSArray.ArraySize = 1;
+ break;
+ case ShaderResourceViewDimension.TextureCubeArray:
+ description.TextureCubeArray.First2DArrayFace = sliceIndex * 6;
+ description.TextureCubeArray.CubeCount = 1;
+ break;
+ default:
+ throw new NotSupportedException($"{nameof(TextureArraySlicer)} does not support dimension {description.Dimension}");
+ }
+ state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description));
+ _activeSlices.Add(((nint)texture, sliceIndex), state);
+ return (nint)state.ShaderResourceView;
+ }
+
+ public void Tick()
+ {
+ try
+ {
+ foreach (var (key, slice) in _activeSlices)
+ {
+ if (!slice.Tick())
+ _expiredKeys.Add(key);
+ }
+ foreach (var key in _expiredKeys)
+ {
+ _activeSlices.Remove(key);
+ }
+ }
+ finally
+ {
+ _expiredKeys.Clear();
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var slice in _activeSlices.Values)
+ {
+ slice.Dispose();
+ }
+ }
+
+ private sealed class SliceState(ShaderResourceView shaderResourceView) : IDisposable
+ {
+ public readonly ShaderResourceView ShaderResourceView = shaderResourceView;
+
+ private uint _timeToLive = InitialTimeToLive;
+
+ public void Refresh()
+ {
+ _timeToLive = InitialTimeToLive;
+ }
+
+ public bool Tick()
+ {
+ if (unchecked(_timeToLive--) > 0)
+ return true;
+
+ ShaderResourceView.Dispose();
+ return false;
+ }
+
+ public void Dispose()
+ {
+ ShaderResourceView.Dispose();
+ }
+ }
+}
diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj
index 24ffe469..8e143e3c 100644
--- a/Penumbra/Penumbra.csproj
+++ b/Penumbra/Penumbra.csproj
@@ -72,6 +72,14 @@
$(DalamudLibPath)Iced.dll
False
+
+ $(DalamudLibPath)SharpDX.dll
+ False
+
+
+ $(DalamudLibPath)SharpDX.Direct3D11.dll
+ False
+
lib\OtterTex.dll
diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs
index 6d382ad4..575a381f 100644
--- a/Penumbra/UI/WindowSystem.cs
+++ b/Penumbra/UI/WindowSystem.cs
@@ -2,6 +2,7 @@ using Dalamud.Interface;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using OtterGui.Services;
+using Penumbra.Interop.Services;
using Penumbra.UI.AdvancedWindow;
using Penumbra.UI.Knowledge;
using Penumbra.UI.Tabs.Debug;
@@ -10,23 +11,25 @@ namespace Penumbra.UI;
public class PenumbraWindowSystem : IDisposable, IUiService
{
- private readonly IUiBuilder _uiBuilder;
- private readonly WindowSystem _windowSystem;
- private readonly FileDialogService _fileDialog;
- public readonly ConfigWindow Window;
- public readonly PenumbraChangelog Changelog;
- public readonly KnowledgeWindow KnowledgeWindow;
+ private readonly IUiBuilder _uiBuilder;
+ private readonly WindowSystem _windowSystem;
+ private readonly FileDialogService _fileDialog;
+ private readonly TextureArraySlicer _textureArraySlicer;
+ public readonly ConfigWindow Window;
+ public readonly PenumbraChangelog Changelog;
+ public readonly KnowledgeWindow KnowledgeWindow;
public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window,
LaunchButton _, ModEditWindow editWindow, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab,
- KnowledgeWindow knowledgeWindow)
+ KnowledgeWindow knowledgeWindow, TextureArraySlicer textureArraySlicer)
{
- _uiBuilder = pi.UiBuilder;
- _fileDialog = fileDialog;
- KnowledgeWindow = knowledgeWindow;
- Changelog = changelog;
- Window = window;
- _windowSystem = new WindowSystem("Penumbra");
+ _uiBuilder = pi.UiBuilder;
+ _fileDialog = fileDialog;
+ _textureArraySlicer = textureArraySlicer;
+ KnowledgeWindow = knowledgeWindow;
+ Changelog = changelog;
+ Window = window;
+ _windowSystem = new WindowSystem("Penumbra");
_windowSystem.AddWindow(changelog.Changelog);
_windowSystem.AddWindow(window);
_windowSystem.AddWindow(editWindow);
@@ -37,6 +40,7 @@ public class PenumbraWindowSystem : IDisposable, IUiService
_uiBuilder.OpenConfigUi += Window.OpenSettings;
_uiBuilder.Draw += _windowSystem.Draw;
_uiBuilder.Draw += _fileDialog.Draw;
+ _uiBuilder.Draw += _textureArraySlicer.Tick;
_uiBuilder.DisableGposeUiHide = !config.HideUiInGPose;
_uiBuilder.DisableCutsceneUiHide = !config.HideUiInCutscenes;
_uiBuilder.DisableUserUiHide = !config.HideUiWhenUiHidden;
@@ -51,5 +55,6 @@ public class PenumbraWindowSystem : IDisposable, IUiService
_uiBuilder.OpenConfigUi -= Window.OpenSettings;
_uiBuilder.Draw -= _windowSystem.Draw;
_uiBuilder.Draw -= _fileDialog.Draw;
+ _uiBuilder.Draw -= _textureArraySlicer.Tick;
}
}