mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-15 22:22:41 +00:00
[shader-decoder] Fix address calculation for SW linear textures
This commit is contained in:
@@ -142,6 +142,7 @@ public static class AgcExports
|
||||
private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28;
|
||||
private const uint CbSetShRegisterRangeMarker = 0x6875000D;
|
||||
private static readonly object _submitTraceGate = new();
|
||||
private static readonly object _textureHashTraceGate = new();
|
||||
private static readonly HashSet<uint> _tracedDcbSizes = new();
|
||||
private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new();
|
||||
private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new();
|
||||
@@ -149,6 +150,7 @@ public static class AgcExports
|
||||
private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new();
|
||||
private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new();
|
||||
private static readonly HashSet<ulong> _tracedComputeShaders = new();
|
||||
private static readonly Dictionary<(ulong Address, uint Width, uint Height), ulong> _tracedTextureHashes = [];
|
||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||
private static readonly Dictionary<
|
||||
@@ -168,6 +170,10 @@ public static class AgcExports
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static readonly bool _traceTextureHashes = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_HASHES"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static long _dcbWriteDataTraceCount;
|
||||
private static long _dcbWaitRegMemTraceCount;
|
||||
private static long _createShaderTraceCount;
|
||||
@@ -3747,7 +3753,6 @@ public static class AgcExports
|
||||
var sourceWidth = descriptor.TileMode == 0
|
||||
? GetLinearTexturePitch(
|
||||
Math.Max(descriptor.Width, descriptor.Pitch),
|
||||
descriptor.Height,
|
||||
descriptor.Format)
|
||||
: descriptor.Width;
|
||||
var sourceByteCount = GetTextureByteCount(
|
||||
@@ -3764,7 +3769,7 @@ public static class AgcExports
|
||||
|
||||
if (!isStorage &&
|
||||
descriptor.Address != 0 &&
|
||||
VulkanVideoPresenter.IsGuestImageAvailable(
|
||||
VulkanVideoPresenter.IsGpuGuestImageAvailable(
|
||||
descriptor.Address,
|
||||
descriptor.Format,
|
||||
descriptor.NumberType))
|
||||
@@ -3825,6 +3830,8 @@ public static class AgcExports
|
||||
return true;
|
||||
}
|
||||
|
||||
TraceTextureHash(descriptor, source);
|
||||
|
||||
var nonZero = 0;
|
||||
for (var i = 0; i < source.Length; i++)
|
||||
{
|
||||
@@ -3884,6 +3891,34 @@ public static class AgcExports
|
||||
MipLevel: 0);
|
||||
}
|
||||
|
||||
private static void TraceTextureHash(TextureDescriptor descriptor, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (!_traceTextureHashes ||
|
||||
descriptor.Address == 0 ||
|
||||
descriptor.Width > 256 ||
|
||||
descriptor.Height > 256)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hash = ComputeFingerprint(source);
|
||||
var key = (descriptor.Address, descriptor.Width, descriptor.Height);
|
||||
lock (_textureHashTraceGate)
|
||||
{
|
||||
if (_tracedTextureHashes.TryGetValue(key, out var previousHash) &&
|
||||
previousHash == hash)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_tracedTextureHashes[key] = hash;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] agc.texture_hash addr=0x{descriptor.Address:X16} " +
|
||||
$"size={descriptor.Width}x{descriptor.Height} bytes={source.Length} hash=0x{hash:X16}");
|
||||
}
|
||||
|
||||
private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList<uint> descriptor) =>
|
||||
descriptor.Count >= 4
|
||||
? new VulkanGuestSampler(
|
||||
@@ -4527,23 +4562,17 @@ public static class AgcExports
|
||||
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
|
||||
}
|
||||
|
||||
private static uint GetLinearTexturePitch(uint pitch, uint height, uint format)
|
||||
private static uint GetLinearTexturePitch(uint pitch, uint format)
|
||||
{
|
||||
var bytesPerTexel = GetTextureBytesPerTexel(format);
|
||||
if (bytesPerTexel == 0 || height == 0)
|
||||
if (bytesPerTexel == 0)
|
||||
{
|
||||
return pitch;
|
||||
}
|
||||
|
||||
var pitchAlignment = Math.Max(8UL, 64UL / bytesPerTexel);
|
||||
var alignedPitch = AlignUp(pitch, pitchAlignment);
|
||||
var sliceAlignment = Math.Max(64UL, 256UL / bytesPerTexel);
|
||||
while ((alignedPitch * height) % sliceAlignment != 0)
|
||||
{
|
||||
alignedPitch += pitchAlignment;
|
||||
}
|
||||
|
||||
return checked((uint)alignedPitch);
|
||||
// GFX10 ADDR_SW_LINEAR aligns each row to 256 bytes.
|
||||
var pitchAlignment = Math.Max(1UL, 256UL / bytesPerTexel);
|
||||
return checked((uint)AlignUp(pitch, pitchAlignment));
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment) =>
|
||||
@@ -4833,9 +4862,13 @@ public static class AgcExports
|
||||
var type = (fields[3] >> 28) & 0xFu;
|
||||
var baseLevel = (fields[3] >> 12) & 0xFu;
|
||||
var lastLevel = (fields[3] >> 16) & 0xFu;
|
||||
var pitch = fields.Count >= 5
|
||||
? ((fields[4] >> 13) & 0x3FFFu) + 1
|
||||
: width;
|
||||
// The 128-bit RDNA2 2D resource derives pitch[12:0] from width;
|
||||
// the optional extension word only supplies pitch[13].
|
||||
var pitch = width;
|
||||
if (fields.Count >= 5)
|
||||
{
|
||||
pitch = ((((width - 1) & 0x1FFFu) | (((fields[4] >> 13) & 1u) << 13)) + 1);
|
||||
}
|
||||
var dstSelect = fields[3] & 0xFFFu;
|
||||
if (address == 0 || width == 0 || height == 0)
|
||||
{
|
||||
|
||||
@@ -168,6 +168,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private static readonly object _gate = new();
|
||||
private static readonly Queue<object> _pendingGuestWork = new();
|
||||
private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
|
||||
private static readonly Dictionary<ulong, uint> _gpuGuestImages = new();
|
||||
private static readonly HashSet<(ulong Address, uint Width, uint Height)>
|
||||
_tracedGuestImageSubmissions = [];
|
||||
private static Thread? _thread;
|
||||
@@ -600,7 +601,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsGuestImageAvailable(
|
||||
internal static bool IsGpuGuestImageAvailable(
|
||||
ulong address,
|
||||
uint format,
|
||||
uint numberType)
|
||||
@@ -613,7 +614,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
return _availableGuestImages.TryGetValue(address, out var availableFormat) &&
|
||||
return _gpuGuestImages.TryGetValue(address, out var availableFormat) &&
|
||||
availableFormat == guestFormat;
|
||||
}
|
||||
}
|
||||
@@ -971,6 +972,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public VulkanGuestSampler SamplerState;
|
||||
public Sampler Sampler;
|
||||
public GuestImageResource? GuestImage;
|
||||
public ulong CpuContentFingerprint;
|
||||
public bool UpdatesCpuContent;
|
||||
}
|
||||
|
||||
private sealed class GlobalBufferResource
|
||||
@@ -1009,6 +1012,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public Framebuffer Framebuffer;
|
||||
public bool Initialized;
|
||||
public bool InitialUploadPending;
|
||||
public bool IsCpuBacked;
|
||||
public ulong CpuContentFingerprint;
|
||||
}
|
||||
|
||||
private sealed record PendingGuestSubmission(
|
||||
@@ -2838,6 +2843,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"tile={texture.TileMode} format={vkFormat}");
|
||||
}
|
||||
|
||||
if (TryCreateCpuTextureRefreshResource(texture, guestImage, view, out var refresh))
|
||||
{
|
||||
return refresh;
|
||||
}
|
||||
|
||||
return new TextureResource
|
||||
{
|
||||
Address = texture.Address,
|
||||
@@ -2855,6 +2865,71 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return CreateTextureResource(texture);
|
||||
}
|
||||
|
||||
private bool TryCreateCpuTextureRefreshResource(
|
||||
VulkanGuestDrawTexture texture,
|
||||
GuestImageResource guestImage,
|
||||
ImageView view,
|
||||
out TextureResource resource)
|
||||
{
|
||||
resource = default!;
|
||||
if (!guestImage.IsCpuBacked ||
|
||||
guestImage.Width != texture.Width ||
|
||||
guestImage.Height != texture.Height ||
|
||||
guestImage.MipLevels != 1 ||
|
||||
texture.RgbaPixels.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var rowLength = texture.TileMode == 0
|
||||
? Math.Max(texture.Pitch, texture.Width)
|
||||
: texture.Width;
|
||||
var expectedSize = GetTextureByteCount(texture.Format, rowLength, texture.Height);
|
||||
if (expectedSize == 0 || expectedSize > int.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var pixels = texture.RgbaPixels.Length == (int)expectedSize
|
||||
? texture.RgbaPixels
|
||||
: CreateFallbackTexturePixels(texture.Format, rowLength, texture.Height, expectedSize);
|
||||
var fingerprint = ComputeTextureContentFingerprint(pixels);
|
||||
if ((guestImage.Initialized || guestImage.InitialUploadPending) &&
|
||||
guestImage.CpuContentFingerprint == fingerprint)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var uploadPixels = texture.Format == 13
|
||||
? ExpandRgb32Pixels(pixels)
|
||||
: pixels;
|
||||
var debugName = TextureDebugName(texture, guestImage.Format);
|
||||
var (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
|
||||
uploadPixels,
|
||||
$"{debugName} refresh staging");
|
||||
TraceVulkanShader(
|
||||
$"vk.texture_refresh addr=0x{texture.Address:X16} " +
|
||||
$"size={texture.Width}x{texture.Height} bytes={uploadPixels.Length}");
|
||||
resource = new TextureResource
|
||||
{
|
||||
Address = texture.Address,
|
||||
StagingBuffer = stagingBuffer,
|
||||
StagingMemory = stagingMemory,
|
||||
Image = guestImage.Image,
|
||||
View = view,
|
||||
Width = guestImage.Width,
|
||||
Height = guestImage.Height,
|
||||
RowLength = rowLength,
|
||||
DstSelect = texture.DstSelect,
|
||||
NeedsUpload = true,
|
||||
SamplerState = texture.Sampler,
|
||||
GuestImage = guestImage,
|
||||
CpuContentFingerprint = fingerprint,
|
||||
UpdatesCpuContent = true,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsCompatibleGuestImageAlias(
|
||||
VulkanGuestDrawTexture texture,
|
||||
GuestImageResource guestImage)
|
||||
@@ -3096,24 +3171,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
var uploadPixels = texture.Format == 13
|
||||
? ExpandRgb32Pixels(pixels)
|
||||
: pixels;
|
||||
var uploadSize = (ulong)uploadPixels.Length;
|
||||
var contentFingerprint = ComputeTextureContentFingerprint(pixels);
|
||||
|
||||
var stagingBuffer = CreateBuffer(
|
||||
uploadSize,
|
||||
BufferUsageFlags.TransferSrcBit,
|
||||
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
|
||||
out var stagingMemory);
|
||||
void* mapped;
|
||||
Check(_vk.MapMemory(_device, stagingMemory, 0, uploadSize, 0, &mapped), "vkMapMemory(texture)");
|
||||
fixed (byte* source = uploadPixels)
|
||||
{
|
||||
System.Buffer.MemoryCopy(
|
||||
source,
|
||||
mapped,
|
||||
uploadPixels.Length,
|
||||
uploadPixels.Length);
|
||||
}
|
||||
_vk.UnmapMemory(_device, stagingMemory);
|
||||
var (stagingBuffer, stagingMemory) = CreateTextureStagingBuffer(
|
||||
uploadPixels,
|
||||
$"{TextureDebugName(texture, vkFormat)} staging");
|
||||
|
||||
var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat);
|
||||
var imageInfo = new ImageCreateInfo
|
||||
@@ -3163,7 +3225,6 @@ internal static unsafe class VulkanVideoPresenter
|
||||
};
|
||||
Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)");
|
||||
var debugName = TextureDebugName(texture, vkFormat);
|
||||
SetDebugName(ObjectType.Buffer, stagingBuffer.Handle, $"{debugName} staging");
|
||||
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
|
||||
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
|
||||
var resource = new TextureResource
|
||||
@@ -3181,6 +3242,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
NeedsUpload = true,
|
||||
OwnsStorage = true,
|
||||
SamplerState = texture.Sampler,
|
||||
CpuContentFingerprint = contentFingerprint,
|
||||
UpdatesCpuContent = texture.Address != 0,
|
||||
};
|
||||
|
||||
if (texture.Address != 0 &&
|
||||
@@ -3197,6 +3260,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
Memory = imageMemory,
|
||||
View = view,
|
||||
InitialUploadPending = true,
|
||||
IsCpuBacked = true,
|
||||
CpuContentFingerprint = contentFingerprint,
|
||||
};
|
||||
_guestImages.Add(texture.Address, guestImage);
|
||||
resource.OwnsStorage = false;
|
||||
@@ -3209,6 +3274,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (guestFormat != 0)
|
||||
{
|
||||
_availableGuestImages[texture.Address] = guestFormat;
|
||||
_gpuGuestImages.Remove(texture.Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3216,6 +3282,41 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return resource;
|
||||
}
|
||||
|
||||
private (VkBuffer Buffer, DeviceMemory Memory) CreateTextureStagingBuffer(
|
||||
byte[] pixels,
|
||||
string debugName)
|
||||
{
|
||||
var size = (ulong)pixels.Length;
|
||||
var buffer = CreateBuffer(
|
||||
size,
|
||||
BufferUsageFlags.TransferSrcBit,
|
||||
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
|
||||
out var memory);
|
||||
void* mapped;
|
||||
Check(_vk.MapMemory(_device, memory, 0, size, 0, &mapped), "vkMapMemory(texture)");
|
||||
fixed (byte* source = pixels)
|
||||
{
|
||||
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
|
||||
}
|
||||
_vk.UnmapMemory(_device, memory);
|
||||
SetDebugName(ObjectType.Buffer, buffer.Handle, debugName);
|
||||
return (buffer, memory);
|
||||
}
|
||||
|
||||
private static ulong ComputeTextureContentFingerprint(ReadOnlySpan<byte> pixels)
|
||||
{
|
||||
const ulong offsetBasis = 14695981039346656037;
|
||||
const ulong prime = 1099511628211;
|
||||
var hash = offsetBasis;
|
||||
foreach (var value in pixels)
|
||||
{
|
||||
hash ^= value;
|
||||
hash *= prime;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
private void DumpTextureUpload(
|
||||
VulkanGuestDrawTexture texture,
|
||||
byte[] pixels,
|
||||
@@ -4267,12 +4368,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
|
||||
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
|
||||
|
||||
var targetHasPriorContents = target.Initialized || target.InitialUploadPending;
|
||||
var toColorAttachment = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = target.Initialized ? AccessFlags.ShaderReadBit : 0,
|
||||
SrcAccessMask = targetHasPriorContents ? AccessFlags.ShaderReadBit : 0,
|
||||
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
|
||||
OldLayout = target.Initialized
|
||||
OldLayout = targetHasPriorContents
|
||||
? ImageLayout.ShaderReadOnlyOptimal
|
||||
: ImageLayout.Undefined,
|
||||
NewLayout = ImageLayout.ColorAttachmentOptimal,
|
||||
@@ -4283,8 +4385,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
_commandBuffer,
|
||||
target.Initialized
|
||||
? PipelineStageFlags.FragmentShaderBit
|
||||
targetHasPriorContents
|
||||
? PipelineStageFlags.AllCommandsBit
|
||||
: PipelineStageFlags.TopOfPipeBit,
|
||||
PipelineStageFlags.ColorAttachmentOutputBit,
|
||||
0,
|
||||
@@ -4337,12 +4439,15 @@ internal static unsafe class VulkanVideoPresenter
|
||||
MarkSampledImagesInitialized(resources);
|
||||
MarkStorageImagesInitialized(resources, traceContents: false);
|
||||
|
||||
var guestTextureFormat = GetGuestTextureFormat(target.Format);
|
||||
var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
|
||||
work.Target.Format,
|
||||
work.Target.NumberType);
|
||||
if (work.PublishTarget && guestTextureFormat != 0)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages[target.Address] = guestTextureFormat;
|
||||
_gpuGuestImages[target.Address] = guestTextureFormat;
|
||||
}
|
||||
}
|
||||
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
|
||||
@@ -4384,6 +4489,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages.Remove(work.Target.Address);
|
||||
_gpuGuestImages.Remove(work.Target.Address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4423,6 +4529,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
existing.MipLevels == mipLevels &&
|
||||
existing.Format == format)
|
||||
{
|
||||
existing.IsCpuBacked = false;
|
||||
existing.CpuContentFingerprint = 0;
|
||||
if (existing.RenderPass.Handle == 0)
|
||||
{
|
||||
var attachmentView = existing.MipViews.Length > 0
|
||||
@@ -4448,6 +4556,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages.Remove(target.Address);
|
||||
_gpuGuestImages.Remove(target.Address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5386,11 +5495,18 @@ internal static unsafe class VulkanVideoPresenter
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasPriorContents = texture.GuestImage is { } guestImage &&
|
||||
(guestImage.Initialized || guestImage.InitialUploadPending);
|
||||
var toTransfer = new ImageMemoryBarrier
|
||||
{
|
||||
SType = StructureType.ImageMemoryBarrier,
|
||||
SrcAccessMask = hasPriorContents
|
||||
? AccessFlags.ShaderReadBit
|
||||
: 0,
|
||||
DstAccessMask = AccessFlags.TransferWriteBit,
|
||||
OldLayout = ImageLayout.Undefined,
|
||||
OldLayout = hasPriorContents
|
||||
? ImageLayout.ShaderReadOnlyOptimal
|
||||
: ImageLayout.Undefined,
|
||||
NewLayout = ImageLayout.TransferDstOptimal,
|
||||
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||
@@ -5399,7 +5515,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
};
|
||||
_vk.CmdPipelineBarrier(
|
||||
_commandBuffer,
|
||||
PipelineStageFlags.TopOfPipeBit,
|
||||
hasPriorContents
|
||||
? PipelineStageFlags.AllCommandsBit
|
||||
: PipelineStageFlags.TopOfPipeBit,
|
||||
PipelineStageFlags.TransferBit,
|
||||
0,
|
||||
0,
|
||||
@@ -5569,6 +5687,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
if (format != 0)
|
||||
{
|
||||
_availableGuestImages[texture.Address] = format;
|
||||
_gpuGuestImages[texture.Address] = format;
|
||||
}
|
||||
|
||||
if (traceContents &&
|
||||
@@ -5608,6 +5727,10 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
guestImage.Initialized = true;
|
||||
guestImage.InitialUploadPending = false;
|
||||
if (texture.UpdatesCpuContent)
|
||||
{
|
||||
guestImage.CpuContentFingerprint = texture.CpuContentFingerprint;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6379,6 +6502,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
lock (_gate)
|
||||
{
|
||||
_availableGuestImages.Clear();
|
||||
_gpuGuestImages.Clear();
|
||||
}
|
||||
DestroySwapchainResources();
|
||||
if (_device.Handle != 0)
|
||||
|
||||
Reference in New Issue
Block a user