mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 23:19:44 +08:00
feat(gpu): GPU compute detile for guest tiled textures (Vulkan + Metal) (#592)
* feat(gpu): GPU compute detile for guest tiled textures (Vulkan + Metal) Move RDNA2 exact-XOR deswizzle (swizzle modes 5/9/24/27, 4bpp) off the CPU onto a GPU compute pass. GnmTiling.GetDetileParams resolves the shared addressing into DetileParams; the CPU fallback and both GPU kernels consume the same params so they never disagree. Vulkan (verified bit-exact on NVIDIA): SpirvFixedShaders.CreateDetileCompute hand-emits the SPIR-V kernel; VulkanDetilePass.RecordDetile records the dispatch into the async batch command buffer (never a blocking submit on the render thread) with transients retired via fence; VulkanDetileSelfTest (SHARPEMU_DETILE_SELFTEST=1) checks both entry points against the CPU detile. Metal (Mac-untested): detile_compute.msl (detile_cs) + MetalDetilePass mirror the Vulkan pass. The active Metal path CPU-detiles via the new GnmTiling.DetileWithParams when a texture arrives packaged (empty RgbaPixels + TiledSource/Detile), keeping Metal correct under default-on with no regression; wiring MetalDetilePass live is the remaining on-device step. Flags: GPU detile is default-on (SHARPEMU_GPU_DETILE=0 disables); [GPU-DETILE] diagnostics gated behind SHARPEMU_LOG_GPU_DETILE=1. Tests: 17 detile unit tests pass, incl. DetileWithParams and GetDetileParams each matching TryDetile bit-for-bit across all supported modes/bpp, plus a SPIR-V structural-validity test. * feat(gpu): GPU compute detile for guest tiled textures (Vulkan + Metal) Move RDNA2 exact-XOR deswizzle (swizzle modes 5/9/24/27, 4bpp) off the CPU onto a GPU compute pass. GnmTiling.GetDetileParams resolves the shared addressing into DetileParams that the CPU fallback and both GPU kernels consume, so they never disagree; everything else keeps the CPU path. Vulkan (verified bit-exact on NVIDIA): SpirvFixedShaders.CreateDetileCompute hand-emits the kernel; VulkanDetilePass.RecordDetile records into the async batch command buffer (never a blocking submit on the render thread) with transients retired via fence, falling back to CPU detile on failure. VulkanDetileSelfTest (SHARPEMU_DETILE_SELFTEST=1) checks both entry points. Metal (Mac-untested): detile_compute.msl + MetalDetilePass mirror the Vulkan pass; the active Metal path CPU-detiles via GnmTiling.DetileWithParams so it stays correct under default-on. Wiring MetalDetilePass live is a follow-up. Flags: default-on (SHARPEMU_GPU_DETILE=0 disables); diagnostics behind SHARPEMU_LOG_GPU_DETILE=1. Adds 17 passing detile unit tests. * Fix: added support layered texture support for the GPU-Detiling. * Fix: Added support for BlockTable (1 / 4 / 8 (Morton/Z-order)) * feat: added support for 8 and 16 bpp (bytes per element) * Fixed a build failure specific to this branch ---------
This commit is contained in:
@@ -0,0 +1,742 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Numerics;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Silk.NET.Vulkan;
|
||||
using VkBuffer = Silk.NET.Vulkan.Buffer;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
/// <summary>
|
||||
/// Self-contained GPU deswizzle pass: runs the ExactXor detile equation from
|
||||
/// <see cref="GnmTiling.GetDetileParams"/> as a Vulkan compute shader
|
||||
/// (<see cref="SpirvFixedShaders.CreateDetileCompute"/>), writing a linear buffer
|
||||
/// and copying it into a sampled image — the GPU equivalent of the CPU
|
||||
/// <c>GnmTiling.TryDetile</c> + staging upload.
|
||||
///
|
||||
/// Two entry points share the same (verified) recording:
|
||||
/// <see cref="DetileIntoImage"/> is a self-contained one-shot (submit + wait) used
|
||||
/// by the isolation self-test; <see cref="RecordDetile"/> records into a caller's
|
||||
/// command buffer and hands back its transient buffers + descriptor pool for the
|
||||
/// caller to retire with that command buffer's fence — the render-path variant,
|
||||
/// which must never block the render thread.
|
||||
///
|
||||
/// Only ExactXor 4-bytes/element surfaces are handled; <see cref="Supports"/> lets
|
||||
/// the caller fall back to the CPU path for everything else.
|
||||
/// </summary>
|
||||
internal sealed unsafe class VulkanDetilePass : IDisposable
|
||||
{
|
||||
private const uint LocalSize = 8;
|
||||
private const uint PushConstantBytes = 11 * sizeof(uint);
|
||||
|
||||
private readonly Vk _vk;
|
||||
private readonly Device _device;
|
||||
private readonly Queue _queue;
|
||||
private readonly PhysicalDevice _physicalDevice;
|
||||
private readonly uint _queueFamilyIndex;
|
||||
|
||||
private ShaderModule _shaderModule;
|
||||
private DescriptorSetLayout _descriptorSetLayout;
|
||||
private PipelineLayout _pipelineLayout;
|
||||
private Pipeline _pipeline;
|
||||
private CommandPool _commandPool;
|
||||
private bool _initialized;
|
||||
private bool _disposed;
|
||||
|
||||
public VulkanDetilePass(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
PhysicalDevice physicalDevice,
|
||||
uint queueFamilyIndex)
|
||||
{
|
||||
_vk = vk;
|
||||
_device = device;
|
||||
_queue = queue;
|
||||
_physicalDevice = physicalDevice;
|
||||
_queueFamilyIndex = queueFamilyIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The kernel handles the exact-XOR and block-table modes at 4/8/16
|
||||
/// bytes-per-element (one, two, or four 32-bit words per element). 1/2 bpp are
|
||||
/// sub-word and stay on the CPU.
|
||||
/// </summary>
|
||||
public static bool Supports(in DetileParams parameters) =>
|
||||
(parameters.Equation == DetileEquation.ExactXor ||
|
||||
parameters.Equation == DetileEquation.BlockTable) &&
|
||||
parameters.BytesPerElement is 4 or 8 or 16;
|
||||
|
||||
/// <summary>Transient per-detile resources the caller must retire once the
|
||||
/// command buffer they were recorded into has completed.</summary>
|
||||
public readonly record struct Transients(
|
||||
(VkBuffer Buffer, DeviceMemory Memory)[] Buffers,
|
||||
DescriptorPool DescriptorPool);
|
||||
|
||||
private struct DetileResources
|
||||
{
|
||||
public VkBuffer Tiled;
|
||||
public DeviceMemory TiledMemory;
|
||||
public VkBuffer XTerm;
|
||||
public DeviceMemory XMemory;
|
||||
public VkBuffer YTerm;
|
||||
public DeviceMemory YMemory;
|
||||
public VkBuffer Output;
|
||||
public DeviceMemory OutputMemory;
|
||||
public DescriptorPool Pool;
|
||||
public DescriptorSet Set;
|
||||
public ulong OutputBytes;
|
||||
public uint SrcSliceElements;
|
||||
public uint EquationValue;
|
||||
public uint UintsPerElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the deswizzle of <paramref name="tiled"/> into <paramref name="image"/>
|
||||
/// (<paramref name="texelWidth"/> x <paramref name="texelHeight"/> texels x
|
||||
/// <paramref name="layers"/> array slices, currently in
|
||||
/// <paramref name="currentLayout"/>) onto <paramref name="commandBuffer"/>,
|
||||
/// leaving the image <see cref="ImageLayout.ShaderReadOnlyOptimal"/>. The kernel
|
||||
/// iterates the element grid from <paramref name="parameters"/> (for
|
||||
/// block-compressed formats a 4x4 block is one element, so the element grid is
|
||||
/// smaller than the texel grid). The tiled buffer holds the array slices packed
|
||||
/// contiguously (each an independently tiled 2D surface). Does not submit; the
|
||||
/// caller retires <paramref name="transients"/> with the command buffer's fence.
|
||||
/// Returns false (with empty transients) when unsupported.
|
||||
/// </summary>
|
||||
public bool RecordDetile(
|
||||
CommandBuffer commandBuffer,
|
||||
Image image,
|
||||
ImageLayout currentLayout,
|
||||
uint texelWidth,
|
||||
uint texelHeight,
|
||||
uint layers,
|
||||
ReadOnlySpan<byte> tiled,
|
||||
in DetileParams parameters,
|
||||
out Transients transients)
|
||||
{
|
||||
transients = new Transients([], default);
|
||||
if (_disposed || !Supports(parameters) || texelWidth == 0 || texelHeight == 0 || layers == 0 ||
|
||||
tiled.IsEmpty || tiled.Length % (int)(layers * (uint)parameters.BytesPerElement) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsurePipeline();
|
||||
|
||||
var resources = default(DetileResources);
|
||||
try
|
||||
{
|
||||
PrepareResources(tiled, parameters, layers, ref resources);
|
||||
RecordCommands(commandBuffer, in resources, image, currentLayout, texelWidth, texelHeight, layers, in parameters);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DestroyResources(in resources);
|
||||
throw;
|
||||
}
|
||||
|
||||
transients = new Transients(
|
||||
[
|
||||
(resources.Tiled, resources.TiledMemory),
|
||||
(resources.XTerm, resources.XMemory),
|
||||
(resources.YTerm, resources.YMemory),
|
||||
(resources.Output, resources.OutputMemory),
|
||||
],
|
||||
resources.Pool);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-shot variant used by the isolation self-test: records the detile onto a
|
||||
/// private command buffer, submits, waits, and frees every transient. Never
|
||||
/// call this on the render thread — its blocking wait would deadlock the
|
||||
/// present pipeline; use <see cref="RecordDetile"/> there.
|
||||
/// </summary>
|
||||
public bool DetileIntoImage(
|
||||
Image image,
|
||||
ImageLayout currentLayout,
|
||||
uint texelWidth,
|
||||
uint texelHeight,
|
||||
uint layers,
|
||||
ReadOnlySpan<byte> tiled,
|
||||
in DetileParams parameters)
|
||||
{
|
||||
if (_disposed || !Supports(parameters) || texelWidth == 0 || texelHeight == 0 || layers == 0 ||
|
||||
tiled.IsEmpty || tiled.Length % (int)(layers * (uint)parameters.BytesPerElement) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
EnsurePipeline();
|
||||
|
||||
var resources = default(DetileResources);
|
||||
CommandBuffer commandBuffer = default;
|
||||
Fence fence = default;
|
||||
try
|
||||
{
|
||||
PrepareResources(tiled, parameters, layers, ref resources);
|
||||
|
||||
commandBuffer = AllocateCommandBuffer();
|
||||
BeginCommandBuffer(commandBuffer);
|
||||
RecordCommands(commandBuffer, in resources, image, currentLayout, texelWidth, texelHeight, layers, in parameters);
|
||||
Check(_vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer(detile)");
|
||||
|
||||
fence = CreateFence();
|
||||
var submitInfo = new SubmitInfo
|
||||
{
|
||||
SType = StructureType.SubmitInfo,
|
||||
CommandBufferCount = 1,
|
||||
PCommandBuffers = &commandBuffer,
|
||||
};
|
||||
Check(_vk.QueueSubmit(_queue, 1, &submitInfo, fence), "vkQueueSubmit(detile)");
|
||||
Check(_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue), "vkWaitForFences(detile)");
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (fence.Handle != 0)
|
||||
{
|
||||
_vk.DestroyFence(_device, fence, null);
|
||||
}
|
||||
|
||||
if (commandBuffer.Handle != 0)
|
||||
{
|
||||
_vk.FreeCommandBuffers(_device, _commandPool, 1, &commandBuffer);
|
||||
}
|
||||
|
||||
DestroyResources(in resources);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareResources(ReadOnlySpan<byte> tiled, in DetileParams parameters, uint layers, ref DetileResources resources)
|
||||
{
|
||||
// Binding 1 carries the within-block offset table, binding 2 the Y terms.
|
||||
// ExactXor: xTerm/yTerm are byte offsets; the kernel indexes a uint[], so it
|
||||
// wants element offsets — for a power-of-two element size the low
|
||||
// log2(bpp) bits of every term are 0, so the right shift is exact.
|
||||
// BlockTable: GetDetileParams' block table is already element offsets; it
|
||||
// goes in binding 1 and binding 2 is an unused placeholder.
|
||||
uint[] xTerm;
|
||||
uint[] yTerm;
|
||||
if (parameters.Equation == DetileEquation.BlockTable)
|
||||
{
|
||||
xTerm = new uint[parameters.BlockTable.Length];
|
||||
for (var index = 0; index < xTerm.Length; index++)
|
||||
{
|
||||
xTerm[index] = (uint)parameters.BlockTable[index];
|
||||
}
|
||||
|
||||
yTerm = [0];
|
||||
resources.EquationValue = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var shift = BitOperations.TrailingZeroCount((uint)parameters.BytesPerElement);
|
||||
xTerm = ToElementTerms(parameters.XByteTerm, shift);
|
||||
yTerm = ToElementTerms(parameters.YByteTerm, shift);
|
||||
resources.EquationValue = 0;
|
||||
}
|
||||
|
||||
// The array slices are packed contiguously in the tiled buffer, so each
|
||||
// slice's element stride is the whole tiled buffer split evenly by layer.
|
||||
// Element sizes are in bytes-per-element; the kernel moves bpp/4 words each.
|
||||
var bytesPerElement = (uint)parameters.BytesPerElement;
|
||||
resources.UintsPerElement = bytesPerElement / sizeof(uint);
|
||||
resources.SrcSliceElements = (uint)((ulong)tiled.Length / bytesPerElement / layers);
|
||||
resources.OutputBytes =
|
||||
(ulong)parameters.ElementsWide * (ulong)parameters.ElementsHigh * bytesPerElement * layers;
|
||||
|
||||
resources.Tiled = CreateHostBuffer((ulong)tiled.Length, BufferUsageFlags.StorageBufferBit, out resources.TiledMemory);
|
||||
UploadBytes(resources.TiledMemory, tiled);
|
||||
resources.XTerm = CreateHostBuffer((ulong)xTerm.Length * sizeof(uint), BufferUsageFlags.StorageBufferBit, out resources.XMemory);
|
||||
UploadUInts(resources.XMemory, xTerm);
|
||||
resources.YTerm = CreateHostBuffer((ulong)yTerm.Length * sizeof(uint), BufferUsageFlags.StorageBufferBit, out resources.YMemory);
|
||||
UploadUInts(resources.YMemory, yTerm);
|
||||
resources.Output = CreateHostBuffer(
|
||||
resources.OutputBytes,
|
||||
BufferUsageFlags.StorageBufferBit | BufferUsageFlags.TransferSrcBit,
|
||||
out resources.OutputMemory);
|
||||
|
||||
resources.Pool = CreateDescriptorPool();
|
||||
resources.Set = AllocateDescriptorSet(resources.Pool);
|
||||
WriteDescriptors(
|
||||
resources.Set,
|
||||
(resources.Tiled, (ulong)tiled.Length),
|
||||
(resources.XTerm, (ulong)xTerm.Length * sizeof(uint)),
|
||||
(resources.YTerm, (ulong)yTerm.Length * sizeof(uint)),
|
||||
(resources.Output, resources.OutputBytes));
|
||||
}
|
||||
|
||||
private void RecordCommands(
|
||||
CommandBuffer commandBuffer,
|
||||
in DetileResources resources,
|
||||
Image image,
|
||||
ImageLayout currentLayout,
|
||||
uint texelWidth,
|
||||
uint texelHeight,
|
||||
uint layers,
|
||||
in DetileParams parameters)
|
||||
{
|
||||
// The kernel iterates the element grid (smaller than the texel grid for
|
||||
// block-compressed formats); the image copy below uses the texel grid.
|
||||
var elementsWide = (uint)parameters.ElementsWide;
|
||||
var elementsHigh = (uint)parameters.ElementsHigh;
|
||||
|
||||
var descriptorSet = resources.Set;
|
||||
_vk.CmdBindPipeline(commandBuffer, PipelineBindPoint.Compute, _pipeline);
|
||||
_vk.CmdBindDescriptorSets(
|
||||
commandBuffer, PipelineBindPoint.Compute, _pipelineLayout, 0, 1, &descriptorSet, 0, null);
|
||||
|
||||
Span<uint> push =
|
||||
[
|
||||
elementsWide,
|
||||
elementsHigh,
|
||||
(uint)parameters.BlockWidth,
|
||||
(uint)parameters.BlockHeight,
|
||||
(uint)parameters.BlockElements,
|
||||
(uint)parameters.BlocksPerRow,
|
||||
(uint)parameters.XMask,
|
||||
(uint)parameters.YMask,
|
||||
resources.SrcSliceElements,
|
||||
resources.EquationValue,
|
||||
resources.UintsPerElement,
|
||||
];
|
||||
fixed (uint* pushPointer = push)
|
||||
{
|
||||
_vk.CmdPushConstants(
|
||||
commandBuffer, _pipelineLayout, ShaderStageFlags.ComputeBit, 0, PushConstantBytes, pushPointer);
|
||||
}
|
||||
|
||||
// X is widened by uintsPerElement (each thread copies one word); one
|
||||
// dispatch-Z layer per array slice.
|
||||
_vk.CmdDispatch(
|
||||
commandBuffer,
|
||||
(elementsWide * resources.UintsPerElement + LocalSize - 1) / LocalSize,
|
||||
(elementsHigh + LocalSize - 1) / LocalSize,
|
||||
layers);
|
||||
|
||||
// Compute store -> transfer read on the linear output buffer.
|
||||
var outputBarrier = new BufferMemoryBarrier
|
||||
{
|
||||
SType = StructureType.BufferMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.ShaderWriteBit,
|
||||
DstAccessMask = AccessFlags.TransferReadBit,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Buffer = resources.Output,
|
||||
Offset = 0,
|
||||
Size = resources.OutputBytes,
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
commandBuffer,
|
||||
PipelineStageFlags.ComputeShaderBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&outputBarrier,
|
||||
0,
|
||||
null);
|
||||
|
||||
var initialized = currentLayout == ImageLayout.ShaderReadOnlyOptimal;
|
||||
TransitionImage(
|
||||
commandBuffer,
|
||||
image,
|
||||
currentLayout,
|
||||
ImageLayout.TransferDstOptimal,
|
||||
initialized ? AccessFlags.ShaderReadBit : 0,
|
||||
AccessFlags.TransferWriteBit,
|
||||
initialized ? PipelineStageFlags.FragmentShaderBit : PipelineStageFlags.TopOfPipeBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
layers);
|
||||
|
||||
// The output buffer is layer-major, tightly packed (BufferRowLength 0 =>
|
||||
// one element-row per texel-row, which for compressed formats is the block
|
||||
// row), so a single copy fills every array layer. Extent is in texels.
|
||||
var copyRegion = new BufferImageCopy
|
||||
{
|
||||
BufferOffset = 0,
|
||||
BufferRowLength = 0,
|
||||
BufferImageHeight = 0,
|
||||
ImageSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, layers),
|
||||
ImageOffset = default,
|
||||
ImageExtent = new Extent3D(texelWidth, texelHeight, 1),
|
||||
};
|
||||
_vk.CmdCopyBufferToImage(
|
||||
commandBuffer, resources.Output, image, ImageLayout.TransferDstOptimal, 1, ©Region);
|
||||
|
||||
TransitionImage(
|
||||
commandBuffer,
|
||||
image,
|
||||
ImageLayout.TransferDstOptimal,
|
||||
ImageLayout.ShaderReadOnlyOptimal,
|
||||
AccessFlags.TransferWriteBit,
|
||||
AccessFlags.ShaderReadBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
PipelineStageFlags.FragmentShaderBit,
|
||||
layers);
|
||||
}
|
||||
|
||||
private void DestroyResources(in DetileResources resources)
|
||||
{
|
||||
if (resources.Pool.Handle != 0)
|
||||
{
|
||||
_vk.DestroyDescriptorPool(_device, resources.Pool, null);
|
||||
}
|
||||
|
||||
DestroyBuffer(resources.Output, resources.OutputMemory);
|
||||
DestroyBuffer(resources.YTerm, resources.YMemory);
|
||||
DestroyBuffer(resources.XTerm, resources.XMemory);
|
||||
DestroyBuffer(resources.Tiled, resources.TiledMemory);
|
||||
}
|
||||
|
||||
private void EnsurePipeline()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var spirv = SpirvFixedShaders.CreateDetileCompute();
|
||||
fixed (byte* code = spirv)
|
||||
{
|
||||
var moduleInfo = new ShaderModuleCreateInfo
|
||||
{
|
||||
SType = StructureType.ShaderModuleCreateInfo,
|
||||
CodeSize = (nuint)spirv.Length,
|
||||
PCode = (uint*)code,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateShaderModule(_device, &moduleInfo, null, out _shaderModule),
|
||||
"vkCreateShaderModule(detile)");
|
||||
}
|
||||
|
||||
var bindings = stackalloc DescriptorSetLayoutBinding[4];
|
||||
for (uint index = 0; index < 4; index++)
|
||||
{
|
||||
bindings[index] = new DescriptorSetLayoutBinding
|
||||
{
|
||||
Binding = index,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
DescriptorCount = 1,
|
||||
StageFlags = ShaderStageFlags.ComputeBit,
|
||||
};
|
||||
}
|
||||
|
||||
var layoutInfo = new DescriptorSetLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetLayoutCreateInfo,
|
||||
BindingCount = 4,
|
||||
PBindings = bindings,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateDescriptorSetLayout(_device, &layoutInfo, null, out _descriptorSetLayout),
|
||||
"vkCreateDescriptorSetLayout(detile)");
|
||||
|
||||
var pushRange = new PushConstantRange
|
||||
{
|
||||
StageFlags = ShaderStageFlags.ComputeBit,
|
||||
Offset = 0,
|
||||
Size = PushConstantBytes,
|
||||
};
|
||||
var setLayout = _descriptorSetLayout;
|
||||
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineLayoutCreateInfo,
|
||||
SetLayoutCount = 1,
|
||||
PSetLayouts = &setLayout,
|
||||
PushConstantRangeCount = 1,
|
||||
PPushConstantRanges = &pushRange,
|
||||
};
|
||||
Check(
|
||||
_vk.CreatePipelineLayout(_device, &pipelineLayoutInfo, null, out _pipelineLayout),
|
||||
"vkCreatePipelineLayout(detile)");
|
||||
|
||||
ReadOnlySpan<byte> entryPoint = "main\0"u8;
|
||||
fixed (byte* entry = entryPoint)
|
||||
{
|
||||
var pipelineInfo = new ComputePipelineCreateInfo
|
||||
{
|
||||
SType = StructureType.ComputePipelineCreateInfo,
|
||||
Layout = _pipelineLayout,
|
||||
Stage = new PipelineShaderStageCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||
Stage = ShaderStageFlags.ComputeBit,
|
||||
Module = _shaderModule,
|
||||
PName = entry,
|
||||
},
|
||||
};
|
||||
Check(
|
||||
_vk.CreateComputePipelines(_device, default, 1, &pipelineInfo, null, out _pipeline),
|
||||
"vkCreateComputePipelines(detile)");
|
||||
}
|
||||
|
||||
var poolInfo = new CommandPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.CommandPoolCreateInfo,
|
||||
QueueFamilyIndex = _queueFamilyIndex,
|
||||
Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateCommandPool(_device, &poolInfo, null, out _commandPool),
|
||||
"vkCreateCommandPool(detile)");
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private static uint[] ToElementTerms(int[] byteTerms, int shift)
|
||||
{
|
||||
var terms = new uint[byteTerms.Length];
|
||||
for (var index = 0; index < byteTerms.Length; index++)
|
||||
{
|
||||
terms[index] = (uint)byteTerms[index] >> shift;
|
||||
}
|
||||
|
||||
return terms;
|
||||
}
|
||||
|
||||
private VkBuffer CreateHostBuffer(ulong size, BufferUsageFlags usage, out DeviceMemory memory)
|
||||
{
|
||||
var bufferInfo = new BufferCreateInfo
|
||||
{
|
||||
SType = StructureType.BufferCreateInfo,
|
||||
Size = size,
|
||||
Usage = usage,
|
||||
SharingMode = SharingMode.Exclusive,
|
||||
};
|
||||
Check(_vk.CreateBuffer(_device, &bufferInfo, null, out var buffer), "vkCreateBuffer(detile)");
|
||||
|
||||
_vk.GetBufferMemoryRequirements(_device, buffer, out var requirements);
|
||||
var allocateInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
AllocationSize = requirements.Size,
|
||||
MemoryTypeIndex = FindMemoryType(
|
||||
requirements.MemoryTypeBits,
|
||||
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit),
|
||||
};
|
||||
Check(_vk.AllocateMemory(_device, &allocateInfo, null, out memory), "vkAllocateMemory(detile)");
|
||||
Check(_vk.BindBufferMemory(_device, buffer, memory, 0), "vkBindBufferMemory(detile)");
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private uint FindMemoryType(uint typeBits, MemoryPropertyFlags requiredFlags)
|
||||
{
|
||||
_vk.GetPhysicalDeviceMemoryProperties(_physicalDevice, out var properties);
|
||||
var memoryTypes = &properties.MemoryTypes.Element0;
|
||||
for (uint index = 0; index < properties.MemoryTypeCount; index++)
|
||||
{
|
||||
if ((typeBits & (1u << (int)index)) != 0 &&
|
||||
(memoryTypes[index].PropertyFlags & requiredFlags) == requiredFlags)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No compatible Vulkan host-visible memory type for detile.");
|
||||
}
|
||||
|
||||
private void UploadBytes(DeviceMemory memory, ReadOnlySpan<byte> data)
|
||||
{
|
||||
void* mapped;
|
||||
Check(_vk.MapMemory(_device, memory, 0, (ulong)data.Length, 0, &mapped), "vkMapMemory(detile)");
|
||||
data.CopyTo(new Span<byte>(mapped, data.Length));
|
||||
_vk.UnmapMemory(_device, memory);
|
||||
}
|
||||
|
||||
private void UploadUInts(DeviceMemory memory, uint[] data)
|
||||
{
|
||||
void* mapped;
|
||||
var byteCount = (ulong)data.Length * sizeof(uint);
|
||||
Check(_vk.MapMemory(_device, memory, 0, byteCount, 0, &mapped), "vkMapMemory(detile terms)");
|
||||
data.AsSpan().CopyTo(new Span<uint>(mapped, data.Length));
|
||||
_vk.UnmapMemory(_device, memory);
|
||||
}
|
||||
|
||||
private DescriptorPool CreateDescriptorPool()
|
||||
{
|
||||
var poolSize = new DescriptorPoolSize
|
||||
{
|
||||
Type = DescriptorType.StorageBuffer,
|
||||
DescriptorCount = 4,
|
||||
};
|
||||
var poolInfo = new DescriptorPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorPoolCreateInfo,
|
||||
MaxSets = 1,
|
||||
PoolSizeCount = 1,
|
||||
PPoolSizes = &poolSize,
|
||||
};
|
||||
Check(
|
||||
_vk.CreateDescriptorPool(_device, &poolInfo, null, out var pool),
|
||||
"vkCreateDescriptorPool(detile)");
|
||||
return pool;
|
||||
}
|
||||
|
||||
private DescriptorSet AllocateDescriptorSet(DescriptorPool pool)
|
||||
{
|
||||
var setLayout = _descriptorSetLayout;
|
||||
var allocateInfo = new DescriptorSetAllocateInfo
|
||||
{
|
||||
SType = StructureType.DescriptorSetAllocateInfo,
|
||||
DescriptorPool = pool,
|
||||
DescriptorSetCount = 1,
|
||||
PSetLayouts = &setLayout,
|
||||
};
|
||||
Check(
|
||||
_vk.AllocateDescriptorSets(_device, &allocateInfo, out var descriptorSet),
|
||||
"vkAllocateDescriptorSets(detile)");
|
||||
return descriptorSet;
|
||||
}
|
||||
|
||||
private void WriteDescriptors(
|
||||
DescriptorSet descriptorSet,
|
||||
(VkBuffer Buffer, ulong Size) binding0,
|
||||
(VkBuffer Buffer, ulong Size) binding1,
|
||||
(VkBuffer Buffer, ulong Size) binding2,
|
||||
(VkBuffer Buffer, ulong Size) binding3)
|
||||
{
|
||||
var buffers = stackalloc DescriptorBufferInfo[4]
|
||||
{
|
||||
new DescriptorBufferInfo { Buffer = binding0.Buffer, Offset = 0, Range = binding0.Size },
|
||||
new DescriptorBufferInfo { Buffer = binding1.Buffer, Offset = 0, Range = binding1.Size },
|
||||
new DescriptorBufferInfo { Buffer = binding2.Buffer, Offset = 0, Range = binding2.Size },
|
||||
new DescriptorBufferInfo { Buffer = binding3.Buffer, Offset = 0, Range = binding3.Size },
|
||||
};
|
||||
|
||||
var writes = stackalloc WriteDescriptorSet[4];
|
||||
for (uint index = 0; index < 4; index++)
|
||||
{
|
||||
writes[index] = new WriteDescriptorSet
|
||||
{
|
||||
SType = StructureType.WriteDescriptorSet,
|
||||
DstSet = descriptorSet,
|
||||
DstBinding = index,
|
||||
DstArrayElement = 0,
|
||||
DescriptorCount = 1,
|
||||
DescriptorType = DescriptorType.StorageBuffer,
|
||||
PBufferInfo = &buffers[index],
|
||||
};
|
||||
}
|
||||
|
||||
_vk.UpdateDescriptorSets(_device, 4, writes, 0, null);
|
||||
}
|
||||
|
||||
private CommandBuffer AllocateCommandBuffer()
|
||||
{
|
||||
var allocateInfo = new CommandBufferAllocateInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferAllocateInfo,
|
||||
CommandPool = _commandPool,
|
||||
Level = CommandBufferLevel.Primary,
|
||||
CommandBufferCount = 1,
|
||||
};
|
||||
Check(
|
||||
_vk.AllocateCommandBuffers(_device, &allocateInfo, out var commandBuffer),
|
||||
"vkAllocateCommandBuffers(detile)");
|
||||
return commandBuffer;
|
||||
}
|
||||
|
||||
private void BeginCommandBuffer(CommandBuffer commandBuffer)
|
||||
{
|
||||
var beginInfo = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
Check(_vk.BeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(detile)");
|
||||
}
|
||||
|
||||
private void TransitionImage(
|
||||
CommandBuffer commandBuffer,
|
||||
Image image,
|
||||
ImageLayout oldLayout,
|
||||
ImageLayout newLayout,
|
||||
AccessFlags srcAccess,
|
||||
AccessFlags dstAccess,
|
||||
PipelineStageFlags srcStage,
|
||||
PipelineStageFlags dstStage,
|
||||
uint layers)
|
||||
{
|
||||
var barrier = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = srcAccess,
|
||||
DstAccessMask = dstAccess,
|
||||
OldLayout = oldLayout,
|
||||
NewLayout = newLayout,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = image,
|
||||
SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, layers),
|
||||
};
|
||||
_vk.CmdPipelineBarrier(commandBuffer, srcStage, dstStage, 0, 0, null, 0, null, 1, &barrier);
|
||||
}
|
||||
|
||||
private Fence CreateFence()
|
||||
{
|
||||
var fenceInfo = new FenceCreateInfo { SType = StructureType.FenceCreateInfo };
|
||||
Check(_vk.CreateFence(_device, &fenceInfo, null, out var fence), "vkCreateFence(detile)");
|
||||
return fence;
|
||||
}
|
||||
|
||||
private void DestroyBuffer(VkBuffer buffer, DeviceMemory memory)
|
||||
{
|
||||
if (buffer.Handle != 0)
|
||||
{
|
||||
_vk.DestroyBuffer(_device, buffer, null);
|
||||
}
|
||||
|
||||
if (memory.Handle != 0)
|
||||
{
|
||||
_vk.FreeMemory(_device, memory, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void Check(Result result, string operation)
|
||||
{
|
||||
if (result != Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"{operation} failed: {result}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
if (_pipeline.Handle != 0)
|
||||
{
|
||||
_vk.DestroyPipeline(_device, _pipeline, null);
|
||||
}
|
||||
|
||||
if (_pipelineLayout.Handle != 0)
|
||||
{
|
||||
_vk.DestroyPipelineLayout(_device, _pipelineLayout, null);
|
||||
}
|
||||
|
||||
if (_descriptorSetLayout.Handle != 0)
|
||||
{
|
||||
_vk.DestroyDescriptorSetLayout(_device, _descriptorSetLayout, null);
|
||||
}
|
||||
|
||||
if (_shaderModule.Handle != 0)
|
||||
{
|
||||
_vk.DestroyShaderModule(_device, _shaderModule, null);
|
||||
}
|
||||
|
||||
if (_commandPool.Handle != 0)
|
||||
{
|
||||
_vk.DestroyCommandPool(_device, _commandPool, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Silk.NET.Vulkan;
|
||||
using VkBuffer = Silk.NET.Vulkan.Buffer;
|
||||
|
||||
namespace SharpEmu.Libs.VideoOut;
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in GPU equivalence check for <see cref="VulkanDetilePass"/>. When
|
||||
/// SHARPEMU_DETILE_SELFTEST=1 it builds a known tiled surface, deswizzles it on
|
||||
/// the GPU into a real image, reads the image back, and compares against the CPU
|
||||
/// <see cref="GnmTiling.TryDetile"/> — the same equivalence the unit test proves
|
||||
/// for the params, now end-to-end through the actual Vulkan pass. It logs
|
||||
/// [DETILE-SELFTEST] PASS/FAIL and never throws into startup (any failure is
|
||||
/// caught and logged), so it is safe to leave wired.
|
||||
/// </summary>
|
||||
internal static unsafe class VulkanDetileSelfTest
|
||||
{
|
||||
private const uint Width = 256;
|
||||
private const uint Height = 256;
|
||||
|
||||
// (swizzleMode, bytesPerElement, image format). Mode 27 is exact-XOR, mode 8
|
||||
// (64 KiB Z) is block-table — both branches. bpp 4/8/16 exercises the
|
||||
// one/two/four-words-per-element copy. These formats are non-block-compressed
|
||||
// (element grid == texel grid), so Width/Height are both element and texel dims.
|
||||
private static readonly (uint Mode, int Bpp, Format Format)[] Cases =
|
||||
[
|
||||
(27, 4, Format.R8G8B8A8Unorm),
|
||||
(8, 4, Format.R8G8B8A8Unorm),
|
||||
(27, 8, Format.R32G32Uint),
|
||||
(27, 16, Format.R32G32B32A32Uint),
|
||||
];
|
||||
|
||||
public static void RunIfRequested(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
PhysicalDevice physicalDevice,
|
||||
uint queueFamilyIndex)
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("SHARPEMU_DETILE_SELFTEST") != "1")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Run(vk, device, queue, physicalDevice, queueFamilyIndex);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"[DETILE-SELFTEST] FAIL (exception): {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void Run(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
PhysicalDevice physicalDevice,
|
||||
uint queueFamilyIndex)
|
||||
{
|
||||
using var pass = new VulkanDetilePass(vk, device, queue, physicalDevice, queueFamilyIndex);
|
||||
var commandPool = CreateCommandPool(vk, device, queueFamilyIndex);
|
||||
try
|
||||
{
|
||||
foreach (var (mode, bpp, format) in Cases)
|
||||
{
|
||||
// A plain 2D texture (1 layer) and an array texture (2 layers) — the
|
||||
// arrayed case exercises the kernel's dispatch-Z slice addressing.
|
||||
RunCase(vk, device, physicalDevice, queue, commandPool, pass, mode, bpp, format, layers: 1);
|
||||
RunCase(vk, device, physicalDevice, queue, commandPool, pass, mode, bpp, format, layers: 2);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (commandPool.Handle != 0)
|
||||
{
|
||||
vk.DestroyCommandPool(device, commandPool, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunCase(
|
||||
Vk vk,
|
||||
Device device,
|
||||
PhysicalDevice physicalDevice,
|
||||
Queue queue,
|
||||
CommandPool commandPool,
|
||||
VulkanDetilePass pass,
|
||||
uint swizzleMode,
|
||||
int bytesPerElement,
|
||||
Format format,
|
||||
uint layers)
|
||||
{
|
||||
var parameters = GnmTiling.GetDetileParams(swizzleMode, bytesPerElement, (int)Width, (int)Height);
|
||||
if (!parameters.IsSupported || !VulkanDetilePass.Supports(parameters))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[DETILE-SELFTEST] FAIL: mode {swizzleMode} bpp {bytesPerElement} not supported by the GPU pass.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Whole-block tiled source with a per-layer-distinct deterministic pattern
|
||||
// (so a slice mix-up is caught), the array slices packed contiguously.
|
||||
var blocksHigh = ((int)Height + parameters.BlockHeight - 1) / parameters.BlockHeight;
|
||||
var sliceTiledBytes = (int)((long)parameters.BlocksPerRow * blocksHigh * parameters.BlockBytes);
|
||||
var sliceLinearBytes = (int)(Width * Height * bytesPerElement);
|
||||
var tiled = new byte[sliceTiledBytes * layers];
|
||||
var expected = new byte[sliceLinearBytes * layers];
|
||||
for (var layer = 0; layer < layers; layer++)
|
||||
{
|
||||
for (var index = 0; index < sliceTiledBytes; index++)
|
||||
{
|
||||
tiled[layer * sliceTiledBytes + index] = (byte)((index * 31 + 7 + layer * 101) & 0xFF);
|
||||
}
|
||||
|
||||
if (!GnmTiling.TryDetile(
|
||||
tiled.AsSpan(layer * sliceTiledBytes, sliceTiledBytes),
|
||||
expected.AsSpan(layer * sliceLinearBytes, sliceLinearBytes),
|
||||
swizzleMode, (int)Width, (int)Height, bytesPerElement))
|
||||
{
|
||||
Console.Error.WriteLine("[DETILE-SELFTEST] FAIL: CPU TryDetile declined.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var tiledBytes = tiled;
|
||||
var label = $"mode{swizzleMode} {bytesPerElement}bpp x{layers}";
|
||||
|
||||
// Phase 1: the one-shot DetileIntoImage (submit + wait in place).
|
||||
VerifyPhase(
|
||||
vk, device, physicalDevice, queue, commandPool, expected, layers, format, $"DetileIntoImage {label}",
|
||||
image => pass.DetileIntoImage(image, ImageLayout.Undefined, Width, Height, layers, tiledBytes, parameters));
|
||||
|
||||
// Phase 2: RecordDetile — the exact code path the render loop uses
|
||||
// (record into a command buffer, submit, retire the transients).
|
||||
VerifyPhase(
|
||||
vk, device, physicalDevice, queue, commandPool, expected, layers, format, $"RecordDetile {label}",
|
||||
image => RecordDetileAndSubmit(vk, device, queue, commandPool, pass, image, layers, tiledBytes, parameters));
|
||||
}
|
||||
|
||||
private static void VerifyPhase(
|
||||
Vk vk,
|
||||
Device device,
|
||||
PhysicalDevice physicalDevice,
|
||||
Queue queue,
|
||||
CommandPool commandPool,
|
||||
byte[] expected,
|
||||
uint layers,
|
||||
Format format,
|
||||
string label,
|
||||
Func<Image, bool> detile)
|
||||
{
|
||||
var image = CreateImage(vk, device, physicalDevice, format, layers, out var imageMemory);
|
||||
var readback = CreateHostBuffer(
|
||||
vk, device, physicalDevice, (ulong)expected.Length, BufferUsageFlags.TransferDstBit, out var readbackMemory);
|
||||
try
|
||||
{
|
||||
if (!detile(image))
|
||||
{
|
||||
Console.Error.WriteLine($"[DETILE-SELFTEST] {label} FAIL: declined.");
|
||||
return;
|
||||
}
|
||||
|
||||
CopyImageToBuffer(vk, device, queue, commandPool, image, readback, layers);
|
||||
|
||||
void* mapped;
|
||||
Check(
|
||||
vk.MapMemory(device, readbackMemory, 0, (ulong)expected.Length, 0, &mapped),
|
||||
$"vkMapMemory(selftest {label})");
|
||||
var actual = new Span<byte>(mapped, expected.Length);
|
||||
var firstMismatch = -1;
|
||||
for (var index = 0; index < expected.Length; index++)
|
||||
{
|
||||
if (actual[index] != expected[index])
|
||||
{
|
||||
firstMismatch = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
vk.UnmapMemory(device, readbackMemory);
|
||||
|
||||
Console.Error.WriteLine(firstMismatch < 0
|
||||
? $"[DETILE-SELFTEST] {label} PASS: {Width}x{Height}x{layers} matches CPU detile ({expected.Length} bytes)."
|
||||
: $"[DETILE-SELFTEST] {label} FAIL: first mismatch at byte {firstMismatch}.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (readback.Handle != 0)
|
||||
{
|
||||
vk.DestroyBuffer(device, readback, null);
|
||||
}
|
||||
|
||||
if (readbackMemory.Handle != 0)
|
||||
{
|
||||
vk.FreeMemory(device, readbackMemory, null);
|
||||
}
|
||||
|
||||
if (image.Handle != 0)
|
||||
{
|
||||
vk.DestroyImage(device, image, null);
|
||||
}
|
||||
|
||||
if (imageMemory.Handle != 0)
|
||||
{
|
||||
vk.FreeMemory(device, imageMemory, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Records the detile into a fresh command buffer, submits, waits, and retires
|
||||
// the transients exactly as the presenter's batch does — verifying the render
|
||||
// path's code (RecordDetile) without needing a game to trigger it.
|
||||
private static bool RecordDetileAndSubmit(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
CommandPool commandPool,
|
||||
VulkanDetilePass pass,
|
||||
Image image,
|
||||
uint layers,
|
||||
ReadOnlySpan<byte> tiled,
|
||||
in DetileParams parameters)
|
||||
{
|
||||
var commandBuffer = AllocateCommandBuffer(vk, device, commandPool);
|
||||
var beginInfo = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
Check(vk.BeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(selftest record)");
|
||||
|
||||
if (!pass.RecordDetile(
|
||||
commandBuffer, image, ImageLayout.Undefined, Width, Height, layers, tiled, parameters, out var transients))
|
||||
{
|
||||
_ = vk.EndCommandBuffer(commandBuffer);
|
||||
vk.FreeCommandBuffers(device, commandPool, 1, &commandBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
Check(vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer(selftest record)");
|
||||
|
||||
var fenceInfo = new FenceCreateInfo { SType = StructureType.FenceCreateInfo };
|
||||
Check(vk.CreateFence(device, &fenceInfo, null, out var fence), "vkCreateFence(selftest record)");
|
||||
try
|
||||
{
|
||||
var submitInfo = new SubmitInfo
|
||||
{
|
||||
SType = StructureType.SubmitInfo,
|
||||
CommandBufferCount = 1,
|
||||
PCommandBuffers = &commandBuffer,
|
||||
};
|
||||
Check(vk.QueueSubmit(queue, 1, &submitInfo, fence), "vkQueueSubmit(selftest record)");
|
||||
Check(vk.WaitForFences(device, 1, &fence, true, ulong.MaxValue), "vkWaitForFences(selftest record)");
|
||||
}
|
||||
finally
|
||||
{
|
||||
vk.DestroyFence(device, fence, null);
|
||||
vk.FreeCommandBuffers(device, commandPool, 1, &commandBuffer);
|
||||
foreach (var (buffer, memory) in transients.Buffers)
|
||||
{
|
||||
if (buffer.Handle != 0)
|
||||
{
|
||||
vk.DestroyBuffer(device, buffer, null);
|
||||
}
|
||||
|
||||
if (memory.Handle != 0)
|
||||
{
|
||||
vk.FreeMemory(device, memory, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (transients.DescriptorPool.Handle != 0)
|
||||
{
|
||||
vk.DestroyDescriptorPool(device, transients.DescriptorPool, null);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Image CreateImage(
|
||||
Vk vk,
|
||||
Device device,
|
||||
PhysicalDevice physicalDevice,
|
||||
Format format,
|
||||
uint layers,
|
||||
out DeviceMemory memory)
|
||||
{
|
||||
var imageInfo = new ImageCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageCreateInfo,
|
||||
ImageType = ImageType.Type2D,
|
||||
Format = format,
|
||||
Extent = new Extent3D(Width, Height, 1),
|
||||
MipLevels = 1,
|
||||
ArrayLayers = layers,
|
||||
Samples = SampleCountFlags.Count1Bit,
|
||||
Tiling = ImageTiling.Optimal,
|
||||
Usage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.TransferSrcBit,
|
||||
SharingMode = SharingMode.Exclusive,
|
||||
InitialLayout = ImageLayout.Undefined,
|
||||
};
|
||||
Check(vk.CreateImage(device, &imageInfo, null, out var image), "vkCreateImage(selftest)");
|
||||
|
||||
vk.GetImageMemoryRequirements(device, image, out var requirements);
|
||||
var allocateInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
AllocationSize = requirements.Size,
|
||||
MemoryTypeIndex = FindMemoryType(
|
||||
vk,
|
||||
physicalDevice,
|
||||
requirements.MemoryTypeBits,
|
||||
MemoryPropertyFlags.DeviceLocalBit),
|
||||
};
|
||||
Check(vk.AllocateMemory(device, &allocateInfo, null, out memory), "vkAllocateMemory(selftest image)");
|
||||
Check(vk.BindImageMemory(device, image, memory, 0), "vkBindImageMemory(selftest)");
|
||||
return image;
|
||||
}
|
||||
|
||||
private static void CopyImageToBuffer(
|
||||
Vk vk,
|
||||
Device device,
|
||||
Queue queue,
|
||||
CommandPool commandPool,
|
||||
Image image,
|
||||
VkBuffer destination,
|
||||
uint layers)
|
||||
{
|
||||
var commandBuffer = AllocateCommandBuffer(vk, device, commandPool);
|
||||
var beginInfo = new CommandBufferBeginInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferBeginInfo,
|
||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||
};
|
||||
Check(vk.BeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(selftest readback)");
|
||||
|
||||
// DetileIntoImage left the image ShaderReadOnly; move it to TransferSrc.
|
||||
var toTransferSrc = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = AccessFlags.ShaderReadBit,
|
||||
DstAccessMask = AccessFlags.TransferReadBit,
|
||||
OldLayout = ImageLayout.ShaderReadOnlyOptimal,
|
||||
NewLayout = ImageLayout.TransferSrcOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
Image = image,
|
||||
SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, layers),
|
||||
};
|
||||
vk.CmdPipelineBarrier(
|
||||
commandBuffer,
|
||||
PipelineStageFlags.FragmentShaderBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
&toTransferSrc);
|
||||
|
||||
// Layer-major readback: one copy pulls every array slice back into the
|
||||
// buffer contiguously, matching the packed `expected` layout.
|
||||
var region = new BufferImageCopy
|
||||
{
|
||||
BufferOffset = 0,
|
||||
BufferRowLength = 0,
|
||||
BufferImageHeight = 0,
|
||||
ImageSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, layers),
|
||||
ImageOffset = default,
|
||||
ImageExtent = new Extent3D(Width, Height, 1),
|
||||
};
|
||||
vk.CmdCopyImageToBuffer(
|
||||
commandBuffer,
|
||||
image,
|
||||
ImageLayout.TransferSrcOptimal,
|
||||
destination,
|
||||
1,
|
||||
®ion);
|
||||
|
||||
Check(vk.EndCommandBuffer(commandBuffer), "vkEndCommandBuffer(selftest readback)");
|
||||
|
||||
var fenceInfo = new FenceCreateInfo { SType = StructureType.FenceCreateInfo };
|
||||
Check(vk.CreateFence(device, &fenceInfo, null, out var fence), "vkCreateFence(selftest)");
|
||||
try
|
||||
{
|
||||
var submitInfo = new SubmitInfo
|
||||
{
|
||||
SType = StructureType.SubmitInfo,
|
||||
CommandBufferCount = 1,
|
||||
PCommandBuffers = &commandBuffer,
|
||||
};
|
||||
Check(vk.QueueSubmit(queue, 1, &submitInfo, fence), "vkQueueSubmit(selftest readback)");
|
||||
Check(
|
||||
vk.WaitForFences(device, 1, &fence, true, ulong.MaxValue),
|
||||
"vkWaitForFences(selftest readback)");
|
||||
}
|
||||
finally
|
||||
{
|
||||
vk.DestroyFence(device, fence, null);
|
||||
vk.FreeCommandBuffers(device, commandPool, 1, &commandBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static CommandPool CreateCommandPool(Vk vk, Device device, uint queueFamilyIndex)
|
||||
{
|
||||
var poolInfo = new CommandPoolCreateInfo
|
||||
{
|
||||
SType = StructureType.CommandPoolCreateInfo,
|
||||
QueueFamilyIndex = queueFamilyIndex,
|
||||
Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
|
||||
};
|
||||
Check(vk.CreateCommandPool(device, &poolInfo, null, out var pool), "vkCreateCommandPool(selftest)");
|
||||
return pool;
|
||||
}
|
||||
|
||||
private static CommandBuffer AllocateCommandBuffer(Vk vk, Device device, CommandPool commandPool)
|
||||
{
|
||||
var allocateInfo = new CommandBufferAllocateInfo
|
||||
{
|
||||
SType = StructureType.CommandBufferAllocateInfo,
|
||||
CommandPool = commandPool,
|
||||
Level = CommandBufferLevel.Primary,
|
||||
CommandBufferCount = 1,
|
||||
};
|
||||
Check(
|
||||
vk.AllocateCommandBuffers(device, &allocateInfo, out var commandBuffer),
|
||||
"vkAllocateCommandBuffers(selftest)");
|
||||
return commandBuffer;
|
||||
}
|
||||
|
||||
private static VkBuffer CreateHostBuffer(
|
||||
Vk vk,
|
||||
Device device,
|
||||
PhysicalDevice physicalDevice,
|
||||
ulong size,
|
||||
BufferUsageFlags usage,
|
||||
out DeviceMemory memory)
|
||||
{
|
||||
var bufferInfo = new BufferCreateInfo
|
||||
{
|
||||
SType = StructureType.BufferCreateInfo,
|
||||
Size = size,
|
||||
Usage = usage,
|
||||
SharingMode = SharingMode.Exclusive,
|
||||
};
|
||||
Check(vk.CreateBuffer(device, &bufferInfo, null, out var buffer), "vkCreateBuffer(selftest)");
|
||||
|
||||
vk.GetBufferMemoryRequirements(device, buffer, out var requirements);
|
||||
var allocateInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
AllocationSize = requirements.Size,
|
||||
MemoryTypeIndex = FindMemoryType(
|
||||
vk,
|
||||
physicalDevice,
|
||||
requirements.MemoryTypeBits,
|
||||
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit),
|
||||
};
|
||||
Check(vk.AllocateMemory(device, &allocateInfo, null, out memory), "vkAllocateMemory(selftest buffer)");
|
||||
Check(vk.BindBufferMemory(device, buffer, memory, 0), "vkBindBufferMemory(selftest)");
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static uint FindMemoryType(
|
||||
Vk vk,
|
||||
PhysicalDevice physicalDevice,
|
||||
uint typeBits,
|
||||
MemoryPropertyFlags requiredFlags)
|
||||
{
|
||||
vk.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties);
|
||||
var memoryTypes = &properties.MemoryTypes.Element0;
|
||||
for (uint index = 0; index < properties.MemoryTypeCount; index++)
|
||||
{
|
||||
if ((typeBits & (1u << (int)index)) != 0 &&
|
||||
(memoryTypes[index].PropertyFlags & requiredFlags) == requiredFlags)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No compatible Vulkan memory type for the detile self-test.");
|
||||
}
|
||||
|
||||
private static void Check(Result result, string operation)
|
||||
{
|
||||
if (result != Result.Success)
|
||||
{
|
||||
throw new InvalidOperationException($"{operation} failed: {result}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2860,6 +2860,14 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private long _lastPipelineCacheSaveTick;
|
||||
private Queue _queue;
|
||||
private uint _queueFamilyIndex;
|
||||
// GPU deswizzle (default on; SHARPEMU_GPU_DETILE=0 forces the CPU path).
|
||||
// Lazily built on the first tiled texture; disposed with the presenter.
|
||||
private static readonly bool _gpuDetileEnabled = !string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GPU_DETILE"), "0", StringComparison.Ordinal);
|
||||
private static readonly bool _gpuDetileLog = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_GPU_DETILE"), "1", StringComparison.Ordinal);
|
||||
private VulkanDetilePass? _detilePass;
|
||||
private long _gpuDetileCount;
|
||||
private SwapchainKHR _swapchain;
|
||||
private Image[] _swapchainImages = [];
|
||||
private ImageView[] _swapchainImageViews = [];
|
||||
@@ -2906,6 +2914,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private readonly Stack<Fence> _recycledGuestFences = new();
|
||||
private readonly Stack<CommandBuffer> _recycledGuestCommandBuffers = new();
|
||||
private readonly List<(VkBuffer Buffer, DeviceMemory Memory)> _batchRetireBuffers = new();
|
||||
// Descriptor pools from GPU-detile passes recorded into the batch; retired
|
||||
// with the batch's fence, alongside _batchRetireBuffers.
|
||||
private readonly List<DescriptorPool> _batchRetireDescriptorPools = new();
|
||||
private const int MaxRecycledGuestFences = 32;
|
||||
private const int MaxRecycledGuestCommandBuffers = 32;
|
||||
private VkBuffer _stagingBuffer;
|
||||
@@ -3277,6 +3288,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
IReadOnlyList<TranslatedDrawResources> Resources,
|
||||
IReadOnlyList<GuestImageResource> TraceImages,
|
||||
IReadOnlyList<(VkBuffer Buffer, DeviceMemory Memory)> RetireBuffers,
|
||||
IReadOnlyList<DescriptorPool> RetirePools,
|
||||
ulong Timeline,
|
||||
string DebugName,
|
||||
VulkanGuestQueueIdentity Queue,
|
||||
@@ -4241,6 +4253,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
|
||||
LoadDebugUtilsCommands();
|
||||
VulkanDetileSelfTest.RunIfRequested(_vk, _device, _queue, _physicalDevice, _queueFamilyIndex);
|
||||
if (!_vk.TryGetDeviceExtension(_instance, _device, out _swapchainApi))
|
||||
{
|
||||
throw new InvalidOperationException("VK_KHR_swapchain is unavailable.");
|
||||
@@ -4949,7 +4962,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_batchCommandBuffer,
|
||||
_batchResources.ToArray(),
|
||||
_batchTraceImages.ToArray(),
|
||||
_batchRetireBuffers.Count > 0 ? _batchRetireBuffers.ToArray() : []);
|
||||
_batchRetireBuffers.Count > 0 ? _batchRetireBuffers.ToArray() : [],
|
||||
retirePools: _batchRetireDescriptorPools.Count > 0
|
||||
? _batchRetireDescriptorPools.ToArray()
|
||||
: []);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -4967,6 +4983,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_vk.FreeMemory(_device, memory, null);
|
||||
}
|
||||
|
||||
foreach (var pool in _batchRetireDescriptorPools)
|
||||
{
|
||||
_vk.DestroyDescriptorPool(_device, pool, null);
|
||||
}
|
||||
|
||||
ReleaseGuestCommandBuffer(_batchCommandBuffer);
|
||||
throw;
|
||||
}
|
||||
@@ -4975,6 +4996,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_batchResources.Clear();
|
||||
_batchTraceImages.Clear();
|
||||
_batchRetireBuffers.Clear();
|
||||
_batchRetireDescriptorPools.Clear();
|
||||
_batchCommandBuffer = default;
|
||||
}
|
||||
}
|
||||
@@ -4984,7 +5006,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
IReadOnlyList<TranslatedDrawResources> resources,
|
||||
IReadOnlyList<GuestImageResource> traceImages,
|
||||
IReadOnlyList<(VkBuffer Buffer, DeviceMemory Memory)>? retireBuffers = null,
|
||||
IReadOnlyList<TranslatedDrawResources>? referencedResources = null)
|
||||
IReadOnlyList<TranslatedDrawResources>? referencedResources = null,
|
||||
IReadOnlyList<DescriptorPool>? retirePools = null)
|
||||
{
|
||||
var fence = AcquireGuestFence();
|
||||
try
|
||||
@@ -5042,6 +5065,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
resources,
|
||||
traceImages,
|
||||
retireBuffers ?? [],
|
||||
retirePools ?? [],
|
||||
_submitTimeline,
|
||||
resources.Count > 0 ? resources[0].DebugName : "batch",
|
||||
_activeGuestQueue,
|
||||
@@ -5213,6 +5237,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_vk.FreeMemory(_device, memory, null);
|
||||
}
|
||||
|
||||
foreach (var pool in submission.RetirePools)
|
||||
{
|
||||
_vk.DestroyDescriptorPool(_device, pool, null);
|
||||
}
|
||||
|
||||
ReleaseGuestCommandBuffer(submission.CommandBuffer);
|
||||
ReleaseGuestFence(submission.Fence, needsReset: true);
|
||||
if (submission.Timeline > _completedTimeline)
|
||||
@@ -7845,7 +7874,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
MarkTextureContentCached(key);
|
||||
SharpEmu.HLE.GuestImageWriteTracker.Track(
|
||||
texture.Address,
|
||||
(ulong)texture.RgbaPixels.Length,
|
||||
(ulong)(texture.TiledSource?.Length ?? texture.RgbaPixels.Length),
|
||||
CurrentGuestWorkSequenceForDiagnostics,
|
||||
"vulkan.texture-cache");
|
||||
}
|
||||
@@ -8226,6 +8255,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return (uint)selectedMipLevel;
|
||||
}
|
||||
|
||||
private VulkanDetilePass EnsureDetilePass() =>
|
||||
_detilePass ??= new VulkanDetilePass(
|
||||
_vk, _device, _queue, _physicalDevice, _queueFamilyIndex);
|
||||
|
||||
private TextureResource CreateTextureResource(GuestDrawTexture texture)
|
||||
{
|
||||
var width = Math.Max(texture.Width, 1);
|
||||
@@ -8255,31 +8288,91 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"dst=0x{texture.DstSelect:X3} " +
|
||||
$"bytes={texture.RgbaPixels.Length} expected={expectedSize}");
|
||||
}
|
||||
var pixels = texture.RgbaPixels.Length == (int)(expectedSize * layers)
|
||||
? texture.RgbaPixels
|
||||
: CreateFallbackTexturePixels(texture.Format, rowLength, height, expectedSize);
|
||||
if (!ReferenceEquals(pixels, texture.RgbaPixels))
|
||||
// The GPU detile pass deswizzles plain 2D and array textures (one
|
||||
// dispatch-Z layer per slice) at 4/8/16 bpp, including block-compressed
|
||||
// formats (element grid = ceil(texels/4), smaller than the texel grid).
|
||||
// Validate against the element grid + bpp from the resolved params, and
|
||||
// require the tiled source to cover every layer's linear extent (tiled
|
||||
// slices are >= the linear size due to whole-block padding).
|
||||
DetileParams? gpuDetileParams = null;
|
||||
byte[]? gpuTiledSource = null;
|
||||
if (_gpuDetileEnabled &&
|
||||
texture.Detile is { } detileCandidate &&
|
||||
texture.TiledSource is { Length: > 0 } tiledCandidate &&
|
||||
VulkanDetilePass.Supports(detileCandidate) &&
|
||||
detileCandidate.ElementsWide > 0 &&
|
||||
detileCandidate.ElementsHigh > 0 &&
|
||||
(long)tiledCandidate.Length >=
|
||||
(long)detileCandidate.ElementsWide * detileCandidate.ElementsHigh *
|
||||
detileCandidate.BytesPerElement * layers &&
|
||||
tiledCandidate.Length % (int)(layers * (uint)detileCandidate.BytesPerElement) == 0)
|
||||
{
|
||||
layers = 1;
|
||||
gpuDetileParams = detileCandidate;
|
||||
gpuTiledSource = tiledCandidate;
|
||||
}
|
||||
if (AddressListContains("SHARPEMU_FORCE_WHITE_TEXTURE_TARGETS", texture.Address))
|
||||
{
|
||||
pixels = pixels.ToArray();
|
||||
pixels.AsSpan().Fill(0xFF);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] vk.texture_force_white addr=0x{texture.Address:X16} " +
|
||||
$"size={width}x{height} bytes={pixels.Length}");
|
||||
}
|
||||
DumpTextureUpload(texture, pixels, rowLength, width, height);
|
||||
TraceTextureUploadContents(texture, pixels, rowLength, width, height, vkFormat);
|
||||
var uploadPixels = texture.Format == 13
|
||||
? ExpandRgb32Pixels(pixels)
|
||||
: pixels;
|
||||
var contentFingerprint = ComputeTextureContentFingerprint(pixels);
|
||||
|
||||
var (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
|
||||
uploadPixels,
|
||||
$"{TextureDebugName(texture, vkFormat)} staging");
|
||||
VkBuffer stagingBuffer = default;
|
||||
DeviceMemory stagingMemory = default;
|
||||
ulong contentFingerprint;
|
||||
if (gpuTiledSource is { } gpuSource)
|
||||
{
|
||||
// GPU detile: no CPU staging; the compute pass writes the image directly.
|
||||
contentFingerprint = ComputeTextureContentFingerprint(gpuSource);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Safety net: the AGC gate can package a texture as a GPU-detile
|
||||
// candidate (empty RgbaPixels + TiledSource) that this path did
|
||||
// not accept for the GPU compute pass (see the gpuTiledSource
|
||||
// guard above). Detile the raw tiled bytes on the CPU here rather
|
||||
// than letting empty RgbaPixels fall through to a blank fallback
|
||||
// image — otherwise such textures render empty (missing text).
|
||||
var cpuDetiled = texture.RgbaPixels;
|
||||
if (cpuDetiled.Length == 0 &&
|
||||
layers == 1 &&
|
||||
texture.TiledSource is { Length: > 0 } fallbackTiled &&
|
||||
texture.Detile is { } fallbackParams &&
|
||||
expectedSize > 0 &&
|
||||
expectedSize <= int.MaxValue)
|
||||
{
|
||||
var linear = new byte[expectedSize];
|
||||
if (GnmTiling.TryDetile(
|
||||
fallbackTiled,
|
||||
linear,
|
||||
texture.TileMode,
|
||||
fallbackParams.ElementsWide,
|
||||
fallbackParams.ElementsHigh,
|
||||
fallbackParams.BytesPerElement))
|
||||
{
|
||||
cpuDetiled = linear;
|
||||
}
|
||||
}
|
||||
|
||||
var pixels = cpuDetiled.Length == (int)(expectedSize * layers)
|
||||
? cpuDetiled
|
||||
: CreateFallbackTexturePixels(texture.Format, rowLength, height, expectedSize);
|
||||
if (!ReferenceEquals(pixels, texture.RgbaPixels))
|
||||
{
|
||||
layers = 1;
|
||||
}
|
||||
if (AddressListContains("SHARPEMU_FORCE_WHITE_TEXTURE_TARGETS", texture.Address))
|
||||
{
|
||||
pixels = pixels.ToArray();
|
||||
pixels.AsSpan().Fill(0xFF);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] vk.texture_force_white addr=0x{texture.Address:X16} " +
|
||||
$"size={width}x{height} bytes={pixels.Length}");
|
||||
}
|
||||
DumpTextureUpload(texture, pixels, rowLength, width, height);
|
||||
TraceTextureUploadContents(texture, pixels, rowLength, width, height, vkFormat);
|
||||
var uploadPixels = texture.Format == 13
|
||||
? ExpandRgb32Pixels(pixels)
|
||||
: pixels;
|
||||
contentFingerprint = ComputeTextureContentFingerprint(pixels);
|
||||
(stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
|
||||
uploadPixels,
|
||||
$"{TextureDebugName(texture, vkFormat)} staging");
|
||||
}
|
||||
|
||||
var supportsMutableUsage = !IsBlockCompressedFormat(vkFormat);
|
||||
var supportsAttachmentUsage =
|
||||
@@ -8341,6 +8434,83 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var debugName = TextureDebugName(texture, vkFormat);
|
||||
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
|
||||
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
|
||||
|
||||
// GPU detile: record the deswizzle into the shared batch command buffer
|
||||
// (async — never a blocking submit on the render thread), leaving the
|
||||
// image ShaderReadOnly before the draw that samples it. The transient
|
||||
// buffers + descriptor pool retire with the batch fence. On any failure
|
||||
// fall back to a CPU detile + normal staged upload.
|
||||
var gpuDetiled = false;
|
||||
if (gpuTiledSource is { } detileSource && gpuDetileParams is { } detileParameters)
|
||||
{
|
||||
try
|
||||
{
|
||||
var detileCommandBuffer = BeginBatchedGuestCommands();
|
||||
CloseOpenTranslatedRenderPass();
|
||||
if (EnsureDetilePass().RecordDetile(
|
||||
detileCommandBuffer,
|
||||
image,
|
||||
ImageLayout.Undefined,
|
||||
width,
|
||||
height,
|
||||
layers,
|
||||
detileSource,
|
||||
detileParameters,
|
||||
out var detileTransients))
|
||||
{
|
||||
_batchRetireBuffers.AddRange(detileTransients.Buffers);
|
||||
_batchRetireDescriptorPools.Add(detileTransients.DescriptorPool);
|
||||
gpuDetiled = true;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] GPU detile failed for addr=0x{texture.Address:X16}, " +
|
||||
$"falling back to CPU: {exception.Message}");
|
||||
gpuDetiled = false;
|
||||
}
|
||||
|
||||
if (!gpuDetiled)
|
||||
{
|
||||
// CPU fallback: the tiled source packs the array slices
|
||||
// contiguously, so detile each slice into its layer-major
|
||||
// linear region (single layer degrades to one iteration).
|
||||
var totalLinear = checked((int)(expectedSize * layers));
|
||||
var linear = new byte[totalLinear];
|
||||
var sliceTiledBytes = detileSource.Length / (int)layers;
|
||||
var sliceLinearBytes = (int)expectedSize;
|
||||
var detiledAll = true;
|
||||
for (var layer = 0; layer < layers; layer++)
|
||||
{
|
||||
// TryDetile iterates the element grid (for BC, ceil(texels/4)).
|
||||
if (!GnmTiling.TryDetile(
|
||||
detileSource.AsSpan(layer * sliceTiledBytes, sliceTiledBytes),
|
||||
linear.AsSpan(layer * sliceLinearBytes, sliceLinearBytes),
|
||||
texture.TileMode,
|
||||
detileParameters.ElementsWide,
|
||||
detileParameters.ElementsHigh,
|
||||
detileParameters.BytesPerElement))
|
||||
{
|
||||
detiledAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (detiledAll)
|
||||
{
|
||||
(stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
|
||||
linear, $"{TextureDebugName(texture, vkFormat)} staging(cpu-fallback)");
|
||||
}
|
||||
}
|
||||
else if (_gpuDetileLog && Interlocked.Increment(ref _gpuDetileCount) is 1 or 100 or 1000 or 10000)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GPU-DETILE] active: {_gpuDetileCount} texture(s) detiled on GPU " +
|
||||
$"(latest {width}x{height} mode {texture.TileMode}).");
|
||||
}
|
||||
}
|
||||
|
||||
var resource = new TextureResource
|
||||
{
|
||||
Address = texture.Address,
|
||||
@@ -8356,7 +8526,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
RowLength = rowLength,
|
||||
DstSelect = texture.DstSelect,
|
||||
Layers = layers,
|
||||
NeedsUpload = true,
|
||||
NeedsUpload = !gpuDetiled,
|
||||
OwnsStorage = true,
|
||||
SamplerState = texture.Sampler,
|
||||
CpuContentFingerprint = contentFingerprint,
|
||||
@@ -8367,6 +8537,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (texture.Address != 0 &&
|
||||
!texture.ArrayedView &&
|
||||
layers == 1 &&
|
||||
!gpuDetiled &&
|
||||
!_guestImages.ContainsKey(texture.Address))
|
||||
{
|
||||
var guestFormat = GetGuestTextureFormat(texture.Format, texture.NumberType);
|
||||
@@ -16643,6 +16814,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_lastOrderedGuestFlipVersions.Clear();
|
||||
}
|
||||
DestroySwapchainResources();
|
||||
_detilePass?.Dispose();
|
||||
_detilePass = null;
|
||||
if (_device.Handle != 0)
|
||||
{
|
||||
if (_pipelineCache.Handle != 0)
|
||||
|
||||
Reference in New Issue
Block a user