Compare commits

..

2 Commits

6 changed files with 1023 additions and 96 deletions
+75 -7
View File
@@ -142,7 +142,7 @@ public static class AgcExports
private static readonly HashSet<uint> _tracedDcbSizes = 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, GuestDrawKind Kind)> _tracedShaderTranslations = new();
private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new(); private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new();
private static readonly HashSet<(ulong Es, ulong Ps, ulong Target)> _tracedShaderDraws = new(); private static readonly HashSet<(ulong Es, ulong Ps, ulong Target, ulong Texture, uint VertexCount)> _tracedShaderDraws = new();
private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new(); 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<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new();
private static readonly HashSet<ulong> _tracedComputeShaders = new(); private static readonly HashSet<ulong> _tracedComputeShaders = new();
@@ -2788,7 +2788,7 @@ public static class AgcExports
$"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16}"); $"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16}");
} }
if (vertexCount is not (3 or 4 or 6)) if (vertexCount == 0 || vertexCount > 1_048_576)
{ {
return; return;
} }
@@ -2872,8 +2872,9 @@ public static class AgcExports
lock (_submitTraceGate) lock (_submitTraceGate)
{ {
var firstTextureAddress = translatedDraw.Textures.FirstOrDefault()?.Descriptor.Address ?? 0;
if (_tracedShaderDraws.Add( if (_tracedShaderDraws.Add(
(exportShaderAddress, pixelShaderAddress, firstTarget.Address))) (exportShaderAddress, pixelShaderAddress, firstTarget.Address, firstTextureAddress, vertexCount)))
{ {
TraceTranslatedGuestDraw( TraceTranslatedGuestDraw(
ctx, ctx,
@@ -3033,6 +3034,11 @@ public static class AgcExports
return false; return false;
} }
TraceAgcShader(
$"agc.texture_binding ps=0x{pixelShaderAddress:X16} es=0x{exportShaderAddress:X16} " +
$"pc=0x{binding.Pc:X} op={binding.Opcode} storage={(Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode) ? 1 : 0)} " +
$"decoded={FormatTextureDescriptor(texture)} " +
$"raw={FormatShaderDwords(binding.ResourceDescriptor)} sampler={FormatShaderDwords(binding.SamplerDescriptor)}");
textures.Add( textures.Add(
new TranslatedImageBinding( new TranslatedImageBinding(
texture, texture,
@@ -3477,7 +3483,8 @@ public static class AgcExports
',', ',',
draw.VertexInputs.Select(input => draw.VertexInputs.Select(input =>
$"{input.Location}:pc=0x{input.Pc:X}:0x{input.BaseAddress:X16}" + $"{input.Location}:pc=0x{input.Pc:X}:0x{input.BaseAddress:X16}" +
$":stride{input.Stride}:off{input.OffsetBytes}:c{input.ComponentCount}")); $":stride{input.Stride}:off{input.OffsetBytes}:c{input.ComponentCount}" +
$":fmt{input.DataFormat}/num{input.NumberFormat}"));
var scissor = draw.RenderState.Scissor is { } drawScissor var scissor = draw.RenderState.Scissor is { } drawScissor
? $"{drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height}" ? $"{drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height}"
: "full"; : "full";
@@ -3486,6 +3493,29 @@ public static class AgcExports
$"{drawViewport.Width:0.###}x{drawViewport.Height:0.###}:" + $"{drawViewport.Width:0.###}x{drawViewport.Height:0.###}:" +
$"{drawViewport.MinDepth:0.###}-{drawViewport.MaxDepth:0.###}" $"{drawViewport.MinDepth:0.###}-{drawViewport.MaxDepth:0.###}"
: "full"; : "full";
var rasterRegisters = new (string Name, uint Offset)[]
{
("screen_tl", PaScScreenScissorTl),
("screen_br", PaScScreenScissorBr),
("window_off", PaScWindowOffset),
("window_tl", PaScWindowScissorTl),
("window_br", PaScWindowScissorBr),
("generic_tl", PaScGenericScissorTl),
("generic_br", PaScGenericScissorBr),
("vport_tl", PaScVportScissor0Tl),
("vport_br", PaScVportScissor0Br),
("mode", PaScModeCntl0),
("xscale", PaClVportXScale),
("xoffset", PaClVportXOffset),
("yscale", PaClVportYScale),
("yoffset", PaClVportYOffset),
};
var raster = string.Join(
',',
rasterRegisters.Select(entry =>
state.CxRegisters.TryGetValue(entry.Offset, out var value)
? $"{entry.Name}=0x{value:X8}"
: $"{entry.Name}=missing"));
var blend = draw.RenderState.Blend; var blend = draw.RenderState.Blend;
TraceAgcShader( TraceAgcShader(
$"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " + $"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " +
@@ -3493,6 +3523,7 @@ public static class AgcExports
$"primitive=0x{draw.PrimitiveType:X} " + $"primitive=0x{draw.PrimitiveType:X} " +
$"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " + $"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " +
$"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " + $"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " +
$"raster=[{raster}] " +
$"ps_ena=0x{psInputEna:X8} ps_addr=0x{psInputAddr:X8} " + $"ps_ena=0x{psInputEna:X8} ps_addr=0x{psInputAddr:X8} " +
$"targets=[{targets}] textures=[{textures}] " + $"targets=[{targets}] textures=[{textures}] " +
$"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]"); $"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]");
@@ -3550,6 +3581,8 @@ public static class AgcExports
buffers[index] = new VulkanGuestVertexBuffer( buffers[index] = new VulkanGuestVertexBuffer(
binding.Location, binding.Location,
binding.ComponentCount, binding.ComponentCount,
binding.DataFormat,
binding.NumberFormat,
binding.BaseAddress, binding.BaseAddress,
binding.Stride, binding.Stride,
binding.OffsetBytes, binding.OffsetBytes,
@@ -3579,7 +3612,10 @@ public static class AgcExports
} }
var sourceWidth = descriptor.TileMode == 0 var sourceWidth = descriptor.TileMode == 0
? Math.Max(descriptor.Width, descriptor.Pitch) ? GetLinearTexturePitch(
Math.Max(descriptor.Width, descriptor.Pitch),
descriptor.Height,
descriptor.Format)
: descriptor.Width; : descriptor.Width;
var sourceByteCount = GetTextureByteCount( var sourceByteCount = GetTextureByteCount(
descriptor.Format, descriptor.Format,
@@ -3617,7 +3653,7 @@ public static class AgcExports
IsStorage: true, IsStorage: true,
MipLevels: descriptor.MipLevels, MipLevels: descriptor.MipLevels,
MipLevel: mipLevel, MipLevel: mipLevel,
Pitch: descriptor.Pitch, Pitch: sourceWidth,
TileMode: descriptor.TileMode, TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect, DstSelect: descriptor.DstSelect,
Sampler: ToVulkanSampler(samplerDescriptor)); Sampler: ToVulkanSampler(samplerDescriptor));
@@ -3663,7 +3699,7 @@ public static class AgcExports
IsStorage: isStorage, IsStorage: isStorage,
MipLevels: descriptor.MipLevels, MipLevels: descriptor.MipLevels,
MipLevel: mipLevel, MipLevel: mipLevel,
Pitch: descriptor.Pitch, Pitch: sourceWidth,
TileMode: descriptor.TileMode, TileMode: descriptor.TileMode,
DstSelect: descriptor.DstSelect, DstSelect: descriptor.DstSelect,
Sampler: ToVulkanSampler(samplerDescriptor)); Sampler: ToVulkanSampler(samplerDescriptor));
@@ -4326,6 +4362,28 @@ public static class AgcExports
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes); : checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
} }
private static uint GetLinearTexturePitch(uint pitch, uint height, uint format)
{
var bytesPerTexel = GetTextureBytesPerTexel(format);
if (bytesPerTexel == 0 || height == 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);
}
private static ulong AlignUp(ulong value, ulong alignment) =>
(value + alignment - 1) & ~(alignment - 1);
private static void TraceShaderTranslationMiss( private static void TraceShaderTranslationMiss(
CpuContext ctx, CpuContext ctx,
SubmittedDcbState state, SubmittedDcbState state,
@@ -4594,6 +4652,10 @@ public static class AgcExports
return false; return false;
} }
// GFX10/RDNA2 T# layout: WIDTH is split across word1[31:30] (lo 2 bits)
// and word2[11:0] (hi 12 bits); FORMAT is the combined 9-bit field at
// word1[28:20]. Verified against Kyty's decode of the same game
// descriptors (fmt=56=8_8_8_8_UNORM, extent 1280x720, sw_mode 27).
// GNM T# exposes a 38-bit baseaddr256 field, but RPCSX and the // GNM T# exposes a 38-bit baseaddr256 field, but RPCSX and the
// Demon's Souls descriptors both show that only the low 32 bits are // Demon's Souls descriptors both show that only the low 32 bits are
// part of the guest GPU VA. The upper baseaddr bits carry resource // part of the guest GPU VA. The upper baseaddr bits carry resource
@@ -5367,6 +5429,12 @@ public static class AgcExports
? "none" ? "none"
: string.Join(',', values.Select(static value => $"{value:X8}")); : string.Join(',', values.Select(static value => $"{value:X8}"));
private static string FormatTextureDescriptor(TextureDescriptor descriptor) =>
$"addr=0x{descriptor.Address:X16} {descriptor.Width}x{descriptor.Height} " +
$"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " +
$"type={descriptor.Type} levels={descriptor.BaseLevel}-{descriptor.LastLevel} " +
$"pitch={descriptor.Pitch} dst=0x{descriptor.DstSelect:X3}";
private static void DumpSpirv( private static void DumpSpirv(
string stage, string stage,
ulong shaderAddress, ulong shaderAddress,
+2
View File
@@ -276,6 +276,8 @@ internal sealed record Gen5VertexInputBinding(
uint Pc, uint Pc,
uint Location, uint Location,
uint ComponentCount, uint ComponentCount,
uint DataFormat,
uint NumberFormat,
ulong BaseAddress, ulong BaseAddress,
uint Stride, uint Stride,
uint OffsetBytes, uint OffsetBytes,
@@ -20,7 +20,9 @@ internal static class Gen5ShaderScalarEvaluator
ulong BaseAddress, ulong BaseAddress,
uint Stride, uint Stride,
uint NumRecords, uint NumRecords,
ulong SizeBytes); ulong SizeBytes,
uint NumberFormat,
uint DataFormat);
public static bool TryResolveImageBindings( public static bool TryResolveImageBindings(
CpuContext ctx, CpuContext ctx,
@@ -400,14 +402,21 @@ internal static class Gen5ShaderScalarEvaluator
return false; return false;
} }
var bindingData = data;
var bindingStride = descriptor.Stride;
var bindingOffset = unchecked((uint)control.OffsetBytes + scalarOffset);
var bindingDataFormat = descriptor.DataFormat;
var bindingNumberFormat = descriptor.NumberFormat;
binding = new Gen5VertexInputBinding( binding = new Gen5VertexInputBinding(
instruction.Pc, instruction.Pc,
location, location,
control.DwordCount, control.DwordCount,
bindingDataFormat,
bindingNumberFormat,
descriptor.BaseAddress, descriptor.BaseAddress,
descriptor.Stride, bindingStride,
unchecked((uint)control.OffsetBytes + scalarOffset), bindingOffset,
data); bindingData);
return true; return true;
} }
@@ -803,7 +812,11 @@ internal static class Gen5ShaderScalarEvaluator
"SFF1I32B32" => left == 0 ? uint.MaxValue : (uint)BitOperations.TrailingZeroCount(left), "SFF1I32B32" => left == 0 ? uint.MaxValue : (uint)BitOperations.TrailingZeroCount(left),
_ => registers[destination.Value] | (1u << ((int)left & 31)), _ => registers[destination.Value] | (1u << ((int)left & 31)),
}; };
if (instruction.Opcode != "SBitset1B32")
{
scalarConditionCode = registers[destination.Value] != 0; scalarConditionCode = registers[destination.Value] != 0;
}
return true; return true;
} }
@@ -829,13 +842,15 @@ internal static class Gen5ShaderScalarEvaluator
} }
case "SSubU32": case "SSubU32":
result = left - right; result = left - right;
scalarConditionCode = left >= right; scalarConditionCode = right > left;
break; break;
case "SAddI32": case "SAddI32":
result = unchecked((uint)((int)left + (int)right)); result = unchecked((uint)((int)left + (int)right));
scalarConditionCode = SignedAddOverflow(left, right, result);
break; break;
case "SSubI32": case "SSubI32":
result = unchecked((uint)((int)left - (int)right)); result = unchecked((uint)((int)left - (int)right));
scalarConditionCode = SignedSubOverflow(left, right, result);
break; break;
case "SAddcU32": case "SAddcU32":
{ {
@@ -846,23 +861,27 @@ internal static class Gen5ShaderScalarEvaluator
} }
case "SSubbU32": case "SSubbU32":
{ {
var borrow = scalarConditionCode ? 0UL : 1UL; var borrow = scalarConditionCode ? 1UL : 0UL;
var subtrahend = (ulong)right + borrow; var subtrahend = (ulong)right + borrow;
result = unchecked(left - (uint)subtrahend); result = unchecked(left - (uint)subtrahend);
scalarConditionCode = left >= subtrahend; scalarConditionCode = subtrahend > left;
break; break;
} }
case "SMinI32": case "SMinI32":
result = unchecked((uint)Math.Min((int)left, (int)right)); result = unchecked((uint)Math.Min((int)left, (int)right));
scalarConditionCode = (int)left < (int)right;
break; break;
case "SMinU32": case "SMinU32":
result = Math.Min(left, right); result = Math.Min(left, right);
scalarConditionCode = left < right;
break; break;
case "SMaxI32": case "SMaxI32":
result = unchecked((uint)Math.Max((int)left, (int)right)); result = unchecked((uint)Math.Max((int)left, (int)right));
scalarConditionCode = (int)left > (int)right;
break; break;
case "SMaxU32": case "SMaxU32":
result = Math.Max(left, right); result = Math.Max(left, right);
scalarConditionCode = left > right;
break; break;
case "SCselectB32": case "SCselectB32":
result = scalarConditionCode ? left : right; result = scalarConditionCode ? left : right;
@@ -926,6 +945,7 @@ internal static class Gen5ShaderScalarEvaluator
var offset = (int)right & 31; var offset = (int)right & 31;
var width = Math.Min(((int)right >> 16) & 0x7F, 32 - offset); var width = Math.Min(((int)right >> 16) & 0x7F, 32 - offset);
result = width == 0 ? 0 : left >> offset & (uint.MaxValue >> (32 - width)); result = width == 0 ? 0 : left >> offset & (uint.MaxValue >> (32 - width));
scalarConditionCode = result != 0;
break; break;
} }
case "SBfeI32": case "SBfeI32":
@@ -935,23 +955,41 @@ internal static class Gen5ShaderScalarEvaluator
result = width == 0 result = width == 0
? 0 ? 0
: unchecked((uint)(((int)(left << (32 - width - offset))) >> (32 - width))); : unchecked((uint)(((int)(left << (32 - width - offset))) >> (32 - width)));
scalarConditionCode = result != 0;
break; break;
} }
case "SAbsdiffI32": case "SAbsdiffI32":
result = unchecked((uint)Math.Abs((long)(int)left - (int)right)); result = unchecked((uint)Math.Abs((long)(int)left - (int)right));
scalarConditionCode = result != 0;
break; break;
case "SLshl1AddU32": case "SLshl1AddU32":
result = (left << 1) + right; {
var wide = ((ulong)left << 1) + right;
result = (uint)wide;
scalarConditionCode = wide > uint.MaxValue;
break; break;
}
case "SLshl2AddU32": case "SLshl2AddU32":
result = (left << 2) + right; {
var wide = ((ulong)left << 2) + right;
result = (uint)wide;
scalarConditionCode = wide > uint.MaxValue;
break; break;
}
case "SLshl3AddU32": case "SLshl3AddU32":
result = (left << 3) + right; {
var wide = ((ulong)left << 3) + right;
result = (uint)wide;
scalarConditionCode = wide > uint.MaxValue;
break; break;
}
case "SLshl4AddU32": case "SLshl4AddU32":
result = (left << 4) + right; {
var wide = ((ulong)left << 4) + right;
result = (uint)wide;
scalarConditionCode = wide > uint.MaxValue;
break; break;
}
case "SPackLlB32B16": case "SPackLlB32B16":
result = (left & 0xFFFFu) | (right << 16); result = (left & 0xFFFFu) | (right << 16);
break; break;
@@ -993,7 +1031,8 @@ internal static class Gen5ShaderScalarEvaluator
"SNandSaveexecB64" or "SNandSaveexecB64" or
"SNorSaveexecB64" or "SNorSaveexecB64" or
"SXnorSaveexecB64" or "SXnorSaveexecB64" or
"SAndn1SaveexecB64")) "SAndn1SaveexecB64" or
"SOrn1SaveexecB64"))
{ {
return false; return false;
} }
@@ -1021,11 +1060,12 @@ internal static class Gen5ShaderScalarEvaluator
"SAndSaveexecB64" => oldExec & source, "SAndSaveexecB64" => oldExec & source,
"SOrSaveexecB64" => oldExec | source, "SOrSaveexecB64" => oldExec | source,
"SXorSaveexecB64" => oldExec ^ source, "SXorSaveexecB64" => oldExec ^ source,
"SAndn1SaveexecB64" => ~oldExec & source, "SAndn1SaveexecB64" => ~source & oldExec,
"SAndn2SaveexecB64" => oldExec & ~source, "SAndn2SaveexecB64" => source & ~oldExec,
"SOrn2SaveexecB64" => oldExec | ~source, "SOrn1SaveexecB64" => ~source | oldExec,
"SNandSaveexecB64" => ~(oldExec & source), "SOrn2SaveexecB64" => source | ~oldExec,
"SNorSaveexecB64" => ~(oldExec | source), "SNandSaveexecB64" => ~(source & oldExec),
"SNorSaveexecB64" => ~(source | oldExec),
_ => ~(oldExec ^ source), _ => ~(oldExec ^ source),
}; };
@@ -1095,6 +1135,12 @@ internal static class Gen5ShaderScalarEvaluator
private static ulong MaskWaveValue(ulong value) => value & RdnaWaveMask; private static ulong MaskWaveValue(ulong value) => value & RdnaWaveMask;
private static bool SignedAddOverflow(uint left, uint right, uint result) =>
((left ^ result) & (right ^ result) & 0x80000000u) != 0;
private static bool SignedSubOverflow(uint left, uint right, uint result) =>
((left ^ right) & (left ^ result) & 0x80000000u) != 0;
private static bool TryExecuteScalarCompare( private static bool TryExecuteScalarCompare(
Gen5ShaderInstruction instruction, Gen5ShaderInstruction instruction,
uint[] registers, uint[] registers,
@@ -1416,7 +1462,7 @@ internal static class Gen5ShaderScalarEvaluator
word2 == 0 && word2 == 0 &&
word3 == 0) word3 == 0)
{ {
descriptor = new BufferDescriptor(0, 0, 0, 0); descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0);
return true; return true;
} }
@@ -1428,19 +1474,64 @@ internal static class Gen5ShaderScalarEvaluator
return false; return false;
} }
descriptor = new BufferDescriptor(0, 0, 0, 0); descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0);
return true; return true;
} }
var baseAddress = word0 | ((ulong)(word1 & 0x0FFFu) << 32); var baseAddress = word0 | ((ulong)(word1 & 0xFFFFu) << 32);
var stride = (word1 >> 16) & 0x3FFFu; var stride = (word1 >> 16) & 0x3FFFu;
var unifiedFormat = (word3 >> 12) & 0x7Fu;
var (dataFormat, numberFormat) =
DecodeGfx10BufferFormat(unifiedFormat);
var sizeBytes = stride == 0 var sizeBytes = stride == 0
? word2 ? word2
: (ulong)stride * word2; : (ulong)stride * word2;
descriptor = new BufferDescriptor(baseAddress, stride, word2, sizeBytes); descriptor = new BufferDescriptor(baseAddress, stride, word2, sizeBytes, numberFormat, dataFormat);
return true; return true;
} }
private static (uint DataFormat, uint NumberFormat)
DecodeGfx10BufferFormat(uint format) =>
format switch
{
0 => (0, 0),
>= 1 and <= 6 => (1, format - 1),
>= 7 and <= 13 => (2, DecodeUnifiedNumber(format - 7, 7)),
>= 14 and <= 19 => (3, format - 14),
>= 20 and <= 22 => (4, DecodeIntegerOrFloatNumber(format - 20)),
>= 23 and <= 29 => (5, DecodeUnifiedNumber(format - 23, 7)),
>= 30 and <= 36 => (6, DecodeUnifiedNumber(format - 30, 7)),
>= 37 and <= 43 => (7, DecodeUnifiedNumber(format - 37, 7)),
>= 44 and <= 49 => (8, format - 44),
>= 50 and <= 55 => (9, format - 50),
>= 56 and <= 61 => (10, format - 56),
>= 62 and <= 64 => (11, DecodeIntegerOrFloatNumber(format - 62)),
>= 65 and <= 71 => (12, DecodeUnifiedNumber(format - 65, 7)),
>= 72 and <= 74 => (13, DecodeIntegerOrFloatNumber(format - 72)),
>= 75 and <= 77 => (14, DecodeIntegerOrFloatNumber(format - 75)),
128 => (1, 9),
129 => (3, 9),
130 => (10, 9),
132 => (34, 7),
133 => (16, 0),
134 => (17, 0),
135 => (18, 0),
136 => (19, 0),
140 => (4, 7),
_ => (0, 0),
};
private static uint DecodeUnifiedNumber(uint offset, uint formatCount) =>
offset == formatCount - 1 ? 7u : offset;
private static uint DecodeIntegerOrFloatNumber(uint offset) =>
offset switch
{
0 => 4,
1 => 5,
_ => 7,
};
private static bool TryReadUserDataScalarLoad( private static bool TryReadUserDataScalarLoad(
Gen5ShaderState state, Gen5ShaderState state,
Gen5ShaderInstruction instruction, Gen5ShaderInstruction instruction,
@@ -452,6 +452,7 @@ internal static class Gen5ShaderTranslator
0x2A => "SNorSaveexecB64", 0x2A => "SNorSaveexecB64",
0x2B => "SXnorSaveexecB64", 0x2B => "SXnorSaveexecB64",
0x37 => "SAndn1SaveexecB64", 0x37 => "SAndn1SaveexecB64",
0x38 => "SOrn1SaveexecB64",
_ => string.Empty, _ => string.Empty,
}; };
+266 -24
View File
@@ -212,12 +212,26 @@ internal static partial class Gen5SpirvTranslator
case "VSinF32": case "VSinF32":
result = EmitFloatResult( result = EmitFloatResult(
instruction, instruction,
Ext(13, _floatType, GetFloatSource(instruction, 0))); Ext(
13,
_floatType,
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
GetFloatSource(instruction, 0),
Float(MathF.Tau))));
break; break;
case "VCosF32": case "VCosF32":
result = EmitFloatResult( result = EmitFloatResult(
instruction, instruction,
Ext(14, _floatType, GetFloatSource(instruction, 0))); Ext(
14,
_floatType,
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
GetFloatSource(instruction, 0),
Float(MathF.Tau))));
break; break;
case "VAddF32": case "VAddF32":
result = EmitFloatBinary(instruction, SpirvOp.FAdd); result = EmitFloatBinary(instruction, SpirvOp.FAdd);
@@ -635,13 +649,29 @@ internal static partial class Gen5SpirvTranslator
width); width);
break; break;
} }
case "VBfiB32":
{
var mask = GetRawSource(instruction, 0);
var insert = GetRawSource(instruction, 1);
var source = GetRawSource(instruction, 2);
result = _module.AddInstruction(
SpirvOp.BitwiseOr,
_uintType,
BitwiseAnd(mask, insert),
BitwiseAnd(
_module.AddInstruction(SpirvOp.Not, _uintType, mask),
source));
break;
}
case "VCvtPkrtzF16F32": case "VCvtPkrtzF16F32":
{ {
var first = TruncateFloat32ForPack(GetFloatSource(instruction, 0));
var second = TruncateFloat32ForPack(GetFloatSource(instruction, 1));
var vector = _module.AddInstruction( var vector = _module.AddInstruction(
SpirvOp.CompositeConstruct, SpirvOp.CompositeConstruct,
_vec2Type, _vec2Type,
GetFloatSource(instruction, 0), first,
GetFloatSource(instruction, 1)); second);
result = Ext(58, _uintType, vector); result = Ext(58, _uintType, vector);
break; break;
} }
@@ -945,6 +975,16 @@ internal static partial class Gen5SpirvTranslator
StoreS(destination, result); StoreS(destination, result);
Store(_scc, IsNotZero(result)); Store(_scc, IsNotZero(result));
return true; return true;
case "SBitset1B32":
result = _module.AddInstruction(
SpirvOp.BitFieldInsert,
_uintType,
LoadS(destination),
UInt(1),
BitwiseAnd(left, UInt(31)),
UInt(1));
StoreS(destination, result);
return true;
default: default:
{ {
if (instruction.Sources.Count < 2) if (instruction.Sources.Count < 2)
@@ -971,13 +1011,14 @@ internal static partial class Gen5SpirvTranslator
left, left,
right); right);
Store(_scc, _module.AddInstruction( Store(_scc, _module.AddInstruction(
SpirvOp.UGreaterThanEqual, SpirvOp.UGreaterThan,
_boolType, _boolType,
left, right,
right)); left));
break; break;
case "SAddI32": case "SAddI32":
result = IAdd(left, right); result = IAdd(left, right);
Store(_scc, SignedAddOverflow(left, right, result));
break; break;
case "SSubI32": case "SSubI32":
result = _module.AddInstruction( result = _module.AddInstruction(
@@ -985,6 +1026,7 @@ internal static partial class Gen5SpirvTranslator
_uintType, _uintType,
left, left,
right); right);
Store(_scc, SignedSubOverflow(left, right, result));
break; break;
case "SAddcU32": case "SAddcU32":
{ {
@@ -1021,8 +1063,8 @@ internal static partial class Gen5SpirvTranslator
SpirvOp.Select, SpirvOp.Select,
_uintType, _uintType,
Load(_boolType, _scc), Load(_boolType, _scc),
UInt(0), UInt(1),
UInt(1)); UInt(0));
var partial = _module.AddInstruction( var partial = _module.AddInstruction(
SpirvOp.ISub, SpirvOp.ISub,
_uintType, _uintType,
@@ -1033,23 +1075,31 @@ internal static partial class Gen5SpirvTranslator
_uintType, _uintType,
partial, partial,
borrow); borrow);
var firstNoBorrow = _module.AddInstruction( var firstBorrow = _module.AddInstruction(
SpirvOp.UGreaterThanEqual, SpirvOp.UGreaterThan,
_boolType, _boolType,
left, right,
right); left);
var secondNoBorrow = _module.AddInstruction( var secondBorrow = _module.AddInstruction(
SpirvOp.UGreaterThanEqual, SpirvOp.LogicalAnd,
_boolType, _boolType,
partial, _module.AddInstruction(
borrow); SpirvOp.IEqual,
_boolType,
borrow,
UInt(1)),
_module.AddInstruction(
SpirvOp.IEqual,
_boolType,
right,
left));
Store( Store(
_scc, _scc,
_module.AddInstruction( _module.AddInstruction(
SpirvOp.LogicalAnd, SpirvOp.LogicalOr,
_boolType, _boolType,
firstNoBorrow, firstBorrow,
secondNoBorrow)); secondBorrow));
break; break;
} }
case "SMulI32": case "SMulI32":
@@ -1061,6 +1111,7 @@ internal static partial class Gen5SpirvTranslator
break; break;
case "SAndB32": case "SAndB32":
result = BitwiseAnd(left, right); result = BitwiseAnd(left, right);
Store(_scc, IsNotZero(result));
break; break;
case "SOrB32": case "SOrB32":
result = _module.AddInstruction( result = _module.AddInstruction(
@@ -1068,6 +1119,7 @@ internal static partial class Gen5SpirvTranslator
_uintType, _uintType,
left, left,
right); right);
Store(_scc, IsNotZero(result));
break; break;
case "SXorB32": case "SXorB32":
result = _module.AddInstruction( result = _module.AddInstruction(
@@ -1075,22 +1127,64 @@ internal static partial class Gen5SpirvTranslator
_uintType, _uintType,
left, left,
right); right);
Store(_scc, IsNotZero(result));
break; break;
case "SAndn2B32": case "SAndn2B32":
result = BitwiseAnd( result = BitwiseAnd(
left, left,
_module.AddInstruction(SpirvOp.Not, _uintType, right)); _module.AddInstruction(SpirvOp.Not, _uintType, right));
Store(_scc, IsNotZero(result));
break;
case "SOrn2B32":
result = _module.AddInstruction(
SpirvOp.BitwiseOr,
_uintType,
left,
_module.AddInstruction(SpirvOp.Not, _uintType, right));
Store(_scc, IsNotZero(result));
break;
case "SNandB32":
result = _module.AddInstruction(
SpirvOp.Not,
_uintType,
BitwiseAnd(left, right));
Store(_scc, IsNotZero(result));
break;
case "SNorB32":
result = _module.AddInstruction(
SpirvOp.Not,
_uintType,
_module.AddInstruction(
SpirvOp.BitwiseOr,
_uintType,
left,
right));
Store(_scc, IsNotZero(result));
break;
case "SXnorB32":
result = _module.AddInstruction(
SpirvOp.Not,
_uintType,
_module.AddInstruction(
SpirvOp.BitwiseXor,
_uintType,
left,
right));
Store(_scc, IsNotZero(result));
break; break;
case "SLshlB32": case "SLshlB32":
result = ShiftLeftLogical(left, right); result = ShiftLeftLogical(left, right);
Store(_scc, IsNotZero(result));
break; break;
case "SLshrB32": case "SLshrB32":
result = ShiftRightLogical( result = ShiftRightLogical(
left, left,
BitwiseAnd(right, UInt(31))); BitwiseAnd(right, UInt(31)));
Store(_scc, IsNotZero(result));
break; break;
case "SAshrI32": case "SAshrI32":
result = ShiftRightArithmetic(left, right); result = ShiftRightArithmetic(left, right);
Store(_scc, IsNotZero(result));
break; break;
case "SBfmB32": case "SBfmB32":
result = _module.AddInstruction( result = _module.AddInstruction(
@@ -1133,6 +1227,7 @@ internal static partial class Gen5SpirvTranslator
left, left,
offset, offset,
width); width);
Store(_scc, IsNotZero(result));
break; break;
} }
case "SCselectB32": case "SCselectB32":
@@ -1145,9 +1240,47 @@ internal static partial class Gen5SpirvTranslator
break; break;
case "SMinU32": case "SMinU32":
result = Ext(38, _uintType, left, right); result = Ext(38, _uintType, left, right);
Store(
_scc,
_module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
left,
right));
break;
case "SMinI32":
result = Bitcast(
_uintType,
Ext(39, _intType, Bitcast(_intType, left), Bitcast(_intType, right)));
Store(
_scc,
_module.AddInstruction(
SpirvOp.SLessThan,
_boolType,
Bitcast(_intType, left),
Bitcast(_intType, right)));
break; break;
case "SMaxU32": case "SMaxU32":
result = Ext(41, _uintType, left, right); result = Ext(41, _uintType, left, right);
Store(
_scc,
_module.AddInstruction(
SpirvOp.UGreaterThan,
_boolType,
left,
right));
break;
case "SMaxI32":
result = Bitcast(
_uintType,
Ext(42, _intType, Bitcast(_intType, left), Bitcast(_intType, right)));
Store(
_scc,
_module.AddInstruction(
SpirvOp.SGreaterThan,
_boolType,
Bitcast(_intType, left),
Bitcast(_intType, right)));
break; break;
case "SLshl1AddU32": case "SLshl1AddU32":
case "SLshl2AddU32": case "SLshl2AddU32":
@@ -1292,17 +1425,55 @@ internal static partial class Gen5SpirvTranslator
"SXorSaveexecB64" => _module.AddInstruction( "SXorSaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseXor, _ulongType, oldExec, left), SpirvOp.BitwiseXor, _ulongType, oldExec, left),
"SAndn2SaveexecB64" => _module.AddInstruction( "SAndn2SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseAnd, _ulongType, oldExec, notLeft),
"SAndn1SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseAnd, SpirvOp.BitwiseAnd,
_ulongType, _ulongType,
left,
_module.AddInstruction( _module.AddInstruction(
SpirvOp.Not, SpirvOp.Not,
_ulongType, _ulongType,
oldExec)),
"SAndn1SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseAnd,
_ulongType,
notLeft,
oldExec),
"SOrn1SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseOr,
_ulongType,
notLeft,
oldExec), oldExec),
left),
"SOrn2SaveexecB64" => _module.AddInstruction( "SOrn2SaveexecB64" => _module.AddInstruction(
SpirvOp.BitwiseOr, _ulongType, oldExec, notLeft), SpirvOp.BitwiseOr,
_ulongType,
left,
_module.AddInstruction(
SpirvOp.Not,
_ulongType,
oldExec)),
"SNandSaveexecB64" => _module.AddInstruction(
SpirvOp.Not,
_ulongType,
_module.AddInstruction(
SpirvOp.BitwiseAnd,
_ulongType,
left,
oldExec)),
"SNorSaveexecB64" => _module.AddInstruction(
SpirvOp.Not,
_ulongType,
_module.AddInstruction(
SpirvOp.BitwiseOr,
_ulongType,
left,
oldExec)),
"SXnorSaveexecB64" => _module.AddInstruction(
SpirvOp.Not,
_ulongType,
_module.AddInstruction(
SpirvOp.BitwiseXor,
_ulongType,
left,
oldExec)),
_ => 0u, _ => 0u,
}; };
if (newExec == 0) if (newExec == 0)
@@ -1508,6 +1679,22 @@ internal static partial class Gen5SpirvTranslator
} }
StoreS64(destination, value); StoreS64(destination, value);
if (instruction.Opcode is
"SNotB64" or
"SAndB64" or
"SOrB64" or
"SXorB64" or
"SAndn1B64" or
"SAndn2B64" or
"SOrn1B64" or
"SOrn2B64" or
"SNandB64" or
"SNorB64" or
"SXnorB64")
{
Store(_scc, IsNotZero64(value));
}
return true; return true;
} }
@@ -2067,6 +2254,14 @@ internal static partial class Gen5SpirvTranslator
return Bitcast(_uintType, value); return Bitcast(_uintType, value);
} }
private uint TruncateFloat32ForPack(uint value)
{
var raw = BitwiseAnd(
Bitcast(_uintType, value),
UInt(0xFFFF_E000));
return Bitcast(_floatType, raw);
}
private uint Ext(uint operation, uint resultType, params uint[] operands) private uint Ext(uint operation, uint resultType, params uint[] operands)
{ {
var values = new uint[2 + operands.Length]; var values = new uint[2 + operands.Length];
@@ -2086,6 +2281,53 @@ internal static partial class Gen5SpirvTranslator
value, value,
_module.Constant64(_ulongType, 0)); _module.Constant64(_ulongType, 0));
private uint SignBit(uint value) =>
ShiftRightLogical(value, UInt(31));
private uint SignedAddOverflow(uint left, uint right, uint result)
{
var leftSign = SignBit(left);
var rightSign = SignBit(right);
var resultSign = SignBit(result);
var sameSourceSign = _module.AddInstruction(
SpirvOp.IEqual,
_boolType,
leftSign,
rightSign);
var resultSignChanged = _module.AddInstruction(
SpirvOp.INotEqual,
_boolType,
leftSign,
resultSign);
return _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
sameSourceSign,
resultSignChanged);
}
private uint SignedSubOverflow(uint left, uint right, uint result)
{
var leftSign = SignBit(left);
var rightSign = SignBit(right);
var resultSign = SignBit(result);
var differentSourceSign = _module.AddInstruction(
SpirvOp.INotEqual,
_boolType,
leftSign,
rightSign);
var resultSignChanged = _module.AddInstruction(
SpirvOp.INotEqual,
_boolType,
leftSign,
resultSign);
return _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
differentSourceSign,
resultSignChanged);
}
private static bool TryDecodeInlineConstant(uint encoded, out uint value) private static bool TryDecodeInlineConstant(uint encoded, out uint value)
{ {
if (encoded == 125) if (encoded == 125)
@@ -50,6 +50,8 @@ internal sealed record VulkanGuestMemoryBuffer(
internal sealed record VulkanGuestVertexBuffer( internal sealed record VulkanGuestVertexBuffer(
uint Location, uint Location,
uint ComponentCount, uint ComponentCount,
uint DataFormat,
uint NumberFormat,
ulong BaseAddress, ulong BaseAddress,
uint Stride, uint Stride,
uint OffsetBytes, uint OffsetBytes,
@@ -163,6 +165,8 @@ internal static unsafe class VulkanVideoPresenter
private static readonly object _gate = new(); private static readonly object _gate = new();
private static readonly Queue<object> _pendingGuestWork = new(); private static readonly Queue<object> _pendingGuestWork = new();
private static readonly Dictionary<ulong, uint> _availableGuestImages = new(); private static readonly Dictionary<ulong, uint> _availableGuestImages = new();
private static readonly HashSet<(ulong Address, uint Width, uint Height)>
_tracedGuestImageSubmissions = [];
private static Thread? _thread; private static Thread? _thread;
private static Presentation? _latestPresentation; private static Presentation? _latestPresentation;
private static byte[]? _copyFragmentSpirv; private static byte[]? _copyFragmentSpirv;
@@ -558,44 +562,38 @@ internal static unsafe class VulkanVideoPresenter
uint height, uint height,
uint pitchInPixel) uint pitchInPixel)
{ {
uint format; var traceSubmission = false;
lock (_gate) lock (_gate)
{ {
if (_closed || if (_closed ||
!_availableGuestImages.TryGetValue(address, out format)) !_availableGuestImages.ContainsKey(address))
{ {
return false; return false;
} }
traceSubmission =
_tracedGuestImageSubmissions.Add((address, width, height));
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
_latestPresentation = new Presentation(
null,
width,
height,
sequence,
GuestDrawKind.None,
TranslatedDraw: null,
RequiredGuestWorkSequence: 0,
IsSplash: false,
GuestImageAddress: address);
} }
if (traceSubmission)
{
var effectivePitch = pitchInPixel == 0 ? width : pitchInPixel; var effectivePitch = pitchInPixel == 0 ? width : pitchInPixel;
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_guest_image addr=0x{address:X16} " + $"[LOADER][TRACE] vk.submit_guest_image addr=0x{address:X16} " +
$"size={width}x{height} pitch={effectivePitch}"); $"size={width}x{height} pitch={effectivePitch}");
if (!TryGetCopyFragmentShader(out var fragmentSpirv))
{
return false;
} }
SubmitTranslatedDraw(
fragmentSpirv,
[
new VulkanGuestDrawTexture(
address,
width,
height,
format,
NumberType: 0,
[],
IsFallback: false,
IsStorage: false),
],
[],
width,
height,
attributeCount: 1);
return true; return true;
} }
@@ -807,7 +805,8 @@ internal static unsafe class VulkanVideoPresenter
GuestDrawKind DrawKind, GuestDrawKind DrawKind,
VulkanTranslatedGuestDraw? TranslatedDraw, VulkanTranslatedGuestDraw? TranslatedDraw,
long RequiredGuestWorkSequence, long RequiredGuestWorkSequence,
bool IsSplash); bool IsSplash,
ulong GuestImageAddress = 0);
private sealed class Presenter : IDisposable private sealed class Presenter : IDisposable
{ {
@@ -855,11 +854,16 @@ internal static unsafe class VulkanVideoPresenter
private bool _firstGuestDrawPresented; private bool _firstGuestDrawPresented;
private bool _splashPresented; private bool _splashPresented;
private bool _swapchainRecreateDeferred; private bool _swapchainRecreateDeferred;
private bool _tracedPresentedSwapchain;
private bool _swapchainReadbackPending;
private int _directPresentationCount;
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new(); private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new(); private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new();
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureUploads = new(); private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureUploads = new();
private readonly HashSet<(ulong Address, uint Width, uint Height, uint Format)> _dumpedTextures = new();
private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new(); private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new();
private readonly HashSet<ulong> _tracedGuestImageContents = new(); private readonly HashSet<ulong> _tracedGuestImageContents = new();
private readonly Dictionary<ulong, int> _tracedGuestWriteCounts = new();
private int _tracedVertexBufferCount; private int _tracedVertexBufferCount;
private readonly Dictionary<byte[], Pipeline> _computePipelines = private readonly Dictionary<byte[], Pipeline> _computePipelines =
new(ReferenceEqualityComparer.Instance); new(ReferenceEqualityComparer.Instance);
@@ -922,6 +926,8 @@ internal static unsafe class VulkanVideoPresenter
public ulong Size; public ulong Size;
public uint Location; public uint Location;
public uint ComponentCount; public uint ComponentCount;
public uint DataFormat;
public uint NumberFormat;
public uint Stride; public uint Stride;
public uint OffsetBytes; public uint OffsetBytes;
} }
@@ -1361,7 +1367,10 @@ internal static unsafe class VulkanVideoPresenter
ImageColorSpace = surfaceFormat.ColorSpace, ImageColorSpace = surfaceFormat.ColorSpace,
ImageExtent = _extent, ImageExtent = _extent,
ImageArrayLayers = 1, ImageArrayLayers = 1,
ImageUsage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.ColorAttachmentBit, ImageUsage =
ImageUsageFlags.TransferDstBit |
ImageUsageFlags.TransferSrcBit |
ImageUsageFlags.ColorAttachmentBit,
ImageSharingMode = SharingMode.Exclusive, ImageSharingMode = SharingMode.Exclusive,
PreTransform = capabilities.CurrentTransform, PreTransform = capabilities.CurrentTransform,
CompositeAlpha = compositeAlpha, CompositeAlpha = compositeAlpha,
@@ -2233,7 +2242,10 @@ internal static unsafe class VulkanVideoPresenter
{ {
Location = vertexBuffer.Location, Location = vertexBuffer.Location,
Binding = (uint)index, Binding = (uint)index,
Format = ToVkVertexFormat(vertexBuffer.ComponentCount), Format = ToVkVertexFormat(
vertexBuffer.DataFormat,
vertexBuffer.NumberFormat,
vertexBuffer.ComponentCount),
Offset = 0, Offset = 0,
}; };
} }
@@ -2420,8 +2432,7 @@ internal static unsafe class VulkanVideoPresenter
var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); var vkFormat = GetTextureFormat(texture.Format, texture.NumberType);
if (texture.Address != 0 && if (texture.Address != 0 &&
_guestImages.TryGetValue(texture.Address, out var guestImage) && _guestImages.TryGetValue(texture.Address, out var guestImage) &&
guestImage.Width == texture.Width && IsCompatibleGuestImageAlias(texture, guestImage) &&
guestImage.Height == texture.Height &&
IsCompatibleViewFormat(guestImage.Format, vkFormat) && IsCompatibleViewFormat(guestImage.Format, vkFormat) &&
TryGetOrCreateGuestImageView( TryGetOrCreateGuestImageView(
guestImage, guestImage,
@@ -2440,6 +2451,16 @@ internal static unsafe class VulkanVideoPresenter
$"image_format={guestImage.Format} view_format={vkFormat}"); $"image_format={guestImage.Format} view_format={vkFormat}");
} }
if (guestImage.Width != texture.Width ||
guestImage.Height != texture.Height)
{
TraceVulkanShader(
$"vk.texture_cache_alias addr=0x{texture.Address:X16} " +
$"texture={texture.Width}x{texture.Height} " +
$"image={guestImage.Width}x{guestImage.Height} " +
$"tile={texture.TileMode} format={vkFormat}");
}
return new TextureResource return new TextureResource
{ {
Address = texture.Address, Address = texture.Address,
@@ -2457,6 +2478,27 @@ internal static unsafe class VulkanVideoPresenter
return CreateTextureResource(texture); return CreateTextureResource(texture);
} }
private static bool IsCompatibleGuestImageAlias(
VulkanGuestDrawTexture texture,
GuestImageResource guestImage)
{
if (guestImage.Width == texture.Width &&
guestImage.Height == texture.Height)
{
return true;
}
if (texture.TileMode == 0 ||
texture.Width == 0 ||
texture.Height == 0)
{
return false;
}
return texture.Width <= guestImage.Width &&
texture.Height <= guestImage.Height;
}
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
private TextureResource ResolveStorageImageResource(VulkanGuestDrawTexture texture) private TextureResource ResolveStorageImageResource(VulkanGuestDrawTexture texture)
{ {
@@ -2673,6 +2715,7 @@ internal static unsafe class VulkanVideoPresenter
var pixels = texture.RgbaPixels.Length == (int)expectedSize var pixels = texture.RgbaPixels.Length == (int)expectedSize
? texture.RgbaPixels ? texture.RgbaPixels
: CreateFallbackTexturePixels(texture.Format, rowLength, height, expectedSize); : CreateFallbackTexturePixels(texture.Format, rowLength, height, expectedSize);
DumpTextureUpload(texture, pixels, rowLength, width, height);
var uploadPixels = texture.Format == 13 var uploadPixels = texture.Format == 13
? ExpandRgb32Pixels(pixels) ? ExpandRgb32Pixels(pixels)
: pixels; : pixels;
@@ -2754,6 +2797,102 @@ internal static unsafe class VulkanVideoPresenter
}; };
} }
private void DumpTextureUpload(
VulkanGuestDrawTexture texture,
byte[] pixels,
uint rowLength,
uint width,
uint height)
{
if (!string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"),
"1",
StringComparison.Ordinal) ||
texture.IsFallback ||
texture.IsStorage ||
GetTextureBytesPerPixel(texture.Format) != 4 ||
width == 0 ||
height == 0 ||
!_dumpedTextures.Add((texture.Address, width, height, texture.Format)))
{
return;
}
var rowBytes = checked((int)rowLength * 4);
var visibleRowBytes = checked((int)width * 4);
if (pixels.Length < checked(rowBytes * (int)height))
{
return;
}
var directory = Path.Combine(AppContext.BaseDirectory, "texture-dumps");
Directory.CreateDirectory(directory);
var path = Path.Combine(
directory,
$"tex-{texture.Address:X16}-{width}x{height}-fmt{texture.Format}-row{rowLength}.bmp");
WriteRgbaBmp(path, pixels, rowBytes, visibleRowBytes, (int)width, (int)height);
}
private static void WriteRgbaBmp(
string path,
byte[] rgba,
int sourceRowBytes,
int visibleRowBytes,
int width,
int height)
{
const int fileHeaderSize = 14;
const int infoHeaderSize = 40;
const int bytesPerPixel = 4;
var pixelBytes = checked(width * height * bytesPerPixel);
var fileSize = fileHeaderSize + infoHeaderSize + pixelBytes;
var output = new byte[fileSize];
output[0] = (byte)'B';
output[1] = (byte)'M';
WriteUInt32(output, 2, (uint)fileSize);
WriteUInt32(output, 10, fileHeaderSize + infoHeaderSize);
WriteUInt32(output, 14, infoHeaderSize);
WriteInt32(output, 18, width);
WriteInt32(output, 22, -height);
WriteUInt16(output, 26, 1);
WriteUInt16(output, 28, 32);
WriteUInt32(output, 34, (uint)pixelBytes);
var destinationOffset = fileHeaderSize + infoHeaderSize;
for (var y = 0; y < height; y++)
{
var sourceOffset = y * sourceRowBytes;
for (var x = 0; x < visibleRowBytes; x += bytesPerPixel)
{
var destination = destinationOffset + y * visibleRowBytes + x;
output[destination + 0] = rgba[sourceOffset + x + 2];
output[destination + 1] = rgba[sourceOffset + x + 1];
output[destination + 2] = rgba[sourceOffset + x + 0];
output[destination + 3] = rgba[sourceOffset + x + 3];
}
}
File.WriteAllBytes(path, output);
}
private static void WriteUInt16(byte[] output, int offset, ushort value)
{
output[offset + 0] = (byte)value;
output[offset + 1] = (byte)(value >> 8);
}
private static void WriteUInt32(byte[] output, int offset, uint value)
{
output[offset + 0] = (byte)value;
output[offset + 1] = (byte)(value >> 8);
output[offset + 2] = (byte)(value >> 16);
output[offset + 3] = (byte)(value >> 24);
}
private static void WriteInt32(byte[] output, int offset, int value) =>
WriteUInt32(output, offset, unchecked((uint)value));
private Sampler CreateSampler(VulkanGuestSampler sampler) private Sampler CreateSampler(VulkanGuestSampler sampler)
{ {
var minLod = DecodeSamplerMipFilter(sampler) == 0 var minLod = DecodeSamplerMipFilter(sampler) == 0
@@ -2872,6 +3011,7 @@ internal static unsafe class VulkanVideoPresenter
$"vk.vertex_buffer loc={guestBuffer.Location} " + $"vk.vertex_buffer loc={guestBuffer.Location} " +
$"base=0x{guestBuffer.BaseAddress:X16} stride={guestBuffer.Stride} " + $"base=0x{guestBuffer.BaseAddress:X16} stride={guestBuffer.Stride} " +
$"offset={guestBuffer.OffsetBytes} comps={guestBuffer.ComponentCount} " + $"offset={guestBuffer.OffsetBytes} comps={guestBuffer.ComponentCount} " +
$"fmt={guestBuffer.DataFormat}/num={guestBuffer.NumberFormat} " +
$"bytes={guestBuffer.Data.Length}"); $"bytes={guestBuffer.Data.Length}");
} }
@@ -2882,6 +3022,8 @@ internal static unsafe class VulkanVideoPresenter
Size = size, Size = size,
Location = guestBuffer.Location, Location = guestBuffer.Location,
ComponentCount = guestBuffer.ComponentCount, ComponentCount = guestBuffer.ComponentCount,
DataFormat = guestBuffer.DataFormat,
NumberFormat = guestBuffer.NumberFormat,
Stride = guestBuffer.Stride, Stride = guestBuffer.Stride,
OffsetBytes = guestBuffer.OffsetBytes, OffsetBytes = guestBuffer.OffsetBytes,
}; };
@@ -2931,7 +3073,83 @@ internal static unsafe class VulkanVideoPresenter
_ => PrimitiveTopology.TriangleList, _ => PrimitiveTopology.TriangleList,
}; };
private static Format ToVkVertexFormat(uint componentCount) => private static Format ToVkVertexFormat(
uint dataFormat,
uint numberFormat,
uint componentCount) =>
(dataFormat, numberFormat) switch
{
(1, 0) => Format.R8Unorm,
(1, 1) => Format.R8SNorm,
(1, 4) => Format.R8Uint,
(1, 5) => Format.R8Sint,
(1, 9) => Format.R8Srgb,
(2, 0) => Format.R16Unorm,
(2, 1) => Format.R16SNorm,
(2, 4) => Format.R16Uint,
(2, 5) => Format.R16Sint,
(2, 7) => Format.R16Sfloat,
(3, 0) => Format.R8G8Unorm,
(3, 1) => Format.R8G8SNorm,
(3, 4) => Format.R8G8Uint,
(3, 5) => Format.R8G8Sint,
(3, 9) => Format.R8G8Srgb,
(4, 4) => Format.R32Uint,
(4, 5) => Format.R32Sint,
(4, 7) => Format.R32Sfloat,
(5, 0) => Format.R16G16Unorm,
(5, 1) => Format.R16G16SNorm,
(5, 2) => Format.R16G16Uscaled,
(5, 3) => Format.R16G16Sscaled,
(5, 4) => Format.R16G16Uint,
(5, 5) => Format.R16G16Sint,
(5, 7) => Format.R16G16Sfloat,
(6, 7) => Format.B10G11R11UfloatPack32,
(7, 7) => Format.B10G11R11UfloatPack32,
(8, 0) => Format.A2B10G10R10UnormPack32,
(8, 1) => Format.A2B10G10R10SNormPack32,
(8, 2) => Format.A2B10G10R10UscaledPack32,
(8, 3) => Format.A2B10G10R10SscaledPack32,
(8, 4) => Format.A2B10G10R10UintPack32,
(8, 5) => Format.A2B10G10R10SintPack32,
(9, 0) => Format.A2R10G10B10UnormPack32,
(9, 1) => Format.A2R10G10B10SNormPack32,
(9, 2) => Format.A2R10G10B10UscaledPack32,
(9, 3) => Format.A2R10G10B10SscaledPack32,
(9, 4) => Format.A2R10G10B10UintPack32,
(9, 5) => Format.A2R10G10B10SintPack32,
(10, 0) => Format.R8G8B8A8Unorm,
(10, 1) => Format.R8G8B8A8SNorm,
(10, 2) => Format.R8G8B8A8Uscaled,
(10, 3) => Format.R8G8B8A8Sscaled,
(10, 4) => Format.R8G8B8A8Uint,
(10, 5) => Format.R8G8B8A8Sint,
(10, 9) => Format.R8G8B8A8Srgb,
(11, 4) => Format.R32G32Uint,
(11, 5) => Format.R32G32Sint,
(11, 7) => Format.R32G32Sfloat,
(12, 0) => Format.R16G16B16A16Unorm,
(12, 1) => Format.R16G16B16A16SNorm,
(12, 2) => Format.R16G16B16A16Uscaled,
(12, 3) => Format.R16G16B16A16Sscaled,
(12, 4) => Format.R16G16B16A16Uint,
(12, 5) => Format.R16G16B16A16Sint,
(12, 6) => Format.R16G16B16A16SNorm,
(12, 7) => Format.R16G16B16A16Sfloat,
(13, 4) => Format.R32G32B32Uint,
(13, 5) => Format.R32G32B32Sint,
(13, 7) => Format.R32G32B32Sfloat,
(14, 4) => Format.R32G32B32A32Uint,
(14, 5) => Format.R32G32B32A32Sint,
(14, 7) => Format.R32G32B32A32Sfloat,
(16, 0) => Format.B5G6R5UnormPack16,
(17, 0) => Format.R5G5B5A1UnormPack16,
(19, 0) => Format.R4G4B4A4UnormPack16,
(34, 7) => Format.E5B9G9R9UfloatPack32,
_ => ToVkFloatVertexFormat(componentCount),
};
private static Format ToVkFloatVertexFormat(uint componentCount) =>
componentCount switch componentCount switch
{ {
1 => Format.R32Sfloat, 1 => Format.R32Sfloat,
@@ -3317,7 +3535,7 @@ internal static unsafe class VulkanVideoPresenter
{ {
_stagingBuffer = CreateBuffer( _stagingBuffer = CreateBuffer(
size, size,
BufferUsageFlags.TransferSrcBit, BufferUsageFlags.TransferSrcBit | BufferUsageFlags.TransferDstBit,
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit,
out _stagingMemory); out _stagingMemory);
_stagingSize = size; _stagingSize = size;
@@ -3588,6 +3806,27 @@ internal static unsafe class VulkanVideoPresenter
_availableGuestImages[target.Address] = guestTextureFormat; _availableGuestImages[target.Address] = guestTextureFormat;
} }
} }
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
{
var writeCount = _tracedGuestWriteCounts.TryGetValue(
target.Address,
out var previousCount)
? previousCount + 1
: 1;
_tracedGuestWriteCounts[target.Address] = writeCount;
if (writeCount <= 3)
{
_commandBuffer = _presentationCommandBuffer;
Check(
_vk.QueueWaitIdle(_queue),
"vkQueueWaitIdle(guest write trace)");
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.guest_write_sample " +
$"addr=0x{target.Address:X16} write={writeCount} " +
$"ps_bytes={work.Draw.PixelSpirv.Length}");
TraceGuestImageContents(target);
}
}
TraceVulkanShader( TraceVulkanShader(
$"vk.offscreen_draw addr=0x{target.Address:X16} " + $"vk.offscreen_draw addr=0x{target.Address:X16} " +
$"size={target.Width}x{target.Height} format={target.Format} " + $"size={target.Width}x{target.Height} format={target.Format} " +
@@ -4051,7 +4290,8 @@ internal static unsafe class VulkanVideoPresenter
if (presentation.Pixels is null && if (presentation.Pixels is null &&
presentation.DrawKind != GuestDrawKind.FullscreenBarycentric && presentation.DrawKind != GuestDrawKind.FullscreenBarycentric &&
presentation.TranslatedDraw is null) presentation.TranslatedDraw is null &&
presentation.GuestImageAddress == 0)
{ {
return; return;
} }
@@ -4074,6 +4314,28 @@ internal static unsafe class VulkanVideoPresenter
} }
TranslatedDrawResources? translatedResources = null; TranslatedDrawResources? translatedResources = null;
GuestImageResource? presentedGuestImage = null;
if (presentation.GuestImageAddress != 0 &&
(!_guestImages.TryGetValue(
presentation.GuestImageAddress,
out presentedGuestImage) ||
!presentedGuestImage.Initialized))
{
return;
}
if (presentedGuestImage is not null)
{
_directPresentationCount++;
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
_directPresentationCount is 1 or 30 or 120)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.present_sample frame={_directPresentationCount} " +
$"addr=0x{presentedGuestImage.Address:X16}");
TraceGuestImageContents(presentedGuestImage);
}
}
if (presentation.TranslatedDraw is { } translatedDraw) if (presentation.TranslatedDraw is { } translatedDraw)
{ {
try try
@@ -4175,6 +4437,11 @@ internal static unsafe class VulkanVideoPresenter
_vk.CmdEndRenderPass(_commandBuffer); _vk.CmdEndRenderPass(_commandBuffer);
waitStage = PipelineStageFlags.ColorAttachmentOutputBit; waitStage = PipelineStageFlags.ColorAttachmentOutputBit;
} }
else if (presentedGuestImage is not null)
{
RecordGuestImageBlit(imageIndex, presentedGuestImage);
waitStage = PipelineStageFlags.TransferBit;
}
else if (translatedResources is not null) else if (translatedResources is not null)
{ {
RecordTranslatedDraw(imageIndex, translatedResources); RecordTranslatedDraw(imageIndex, translatedResources);
@@ -4231,6 +4498,10 @@ internal static unsafe class VulkanVideoPresenter
CheckSwapchainResult(presentResult, "vkQueuePresentKHR"); CheckSwapchainResult(presentResult, "vkQueuePresentKHR");
recreateAfterPresent |= presentResult == Result.SuboptimalKhr; recreateAfterPresent |= presentResult == Result.SuboptimalKhr;
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
if (_swapchainReadbackPending)
{
TraceSwapchainReadback();
}
CollectCompletedGuestSubmissions(waitForOldest: false); CollectCompletedGuestSubmissions(waitForOldest: false);
if (translatedResources is not null) if (translatedResources is not null)
{ {
@@ -4259,8 +4530,11 @@ internal static unsafe class VulkanVideoPresenter
{ {
_firstGuestDrawPresented = true; _firstGuestDrawPresented = true;
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut presented translated guest draw: " + $"[LOADER][INFO] Vulkan VideoOut presented guest frame: " +
(presentation.TranslatedDraw is null (presentedGuestImage is not null
? $"image=0x{presentedGuestImage.Address:X16} " +
$"{presentedGuestImage.Width}x{presentedGuestImage.Height}"
: presentation.TranslatedDraw is null
? $"{presentation.DrawKind}" ? $"{presentation.DrawKind}"
: $"shader textures={presentation.TranslatedDraw.Textures.Count}")); : $"shader textures={presentation.TranslatedDraw.Textures.Count}"));
} }
@@ -4760,7 +5034,23 @@ internal static unsafe class VulkanVideoPresenter
private static bool ShouldTraceGuestImageAddressForDiagnostics(ulong address) private static bool ShouldTraceGuestImageAddressForDiagnostics(ulong address)
{ {
var addresses = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"); return AddressListContains(
"SHARPEMU_TRACE_GUEST_IMAGE_ADDRS",
address);
}
private static bool ShouldTraceGuestImageWriteForDiagnostics(ulong address)
{
return AddressListContains(
"SHARPEMU_TRACE_GUEST_WRITES",
address);
}
private static bool AddressListContains(
string environmentVariable,
ulong address)
{
var addresses = Environment.GetEnvironmentVariable(environmentVariable);
if (string.IsNullOrWhiteSpace(addresses)) if (string.IsNullOrWhiteSpace(addresses))
{ {
return false; return false;
@@ -4770,6 +5060,11 @@ internal static unsafe class VulkanVideoPresenter
[',', ';', ' ', '\t'], [',', ';', ' ', '\t'],
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{ {
if (token == "*")
{
return true;
}
var span = token.AsSpan(); var span = token.AsSpan();
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{ {
@@ -5105,6 +5400,234 @@ internal static unsafe class VulkanVideoPresenter
&toPresent); &toPresent);
} }
private void RecordGuestImageBlit(
uint imageIndex,
GuestImageResource source)
{
var traceDestination =
ShouldTracePresentedGuestImageContentsForDiagnostics() &&
!_tracedPresentedSwapchain;
_tracedPresentedSwapchain |= traceDestination;
BeginDebugLabel(
_commandBuffer,
$"SharpEmu present image 0x{source.Address:X16}");
var sourceToTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.ShaderReadBit,
DstAccessMask = AccessFlags.TransferReadBit,
OldLayout = ImageLayout.ShaderReadOnlyOptimal,
NewLayout = ImageLayout.TransferSrcOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = source.Image,
SubresourceRange = ColorSubresourceRange(),
};
var destinationToTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = _imageInitialized[imageIndex]
? AccessFlags.MemoryReadBit
: 0,
DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = _imageInitialized[imageIndex]
? ImageLayout.PresentSrcKhr
: ImageLayout.Undefined,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = _swapchainImages[imageIndex],
SubresourceRange = ColorSubresourceRange(),
};
var barriers = stackalloc ImageMemoryBarrier[2];
barriers[0] = sourceToTransfer;
barriers[1] = destinationToTransfer;
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.AllCommandsBit,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
2,
barriers);
var sourceOffsets = new ImageBlit.SrcOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(
checked((int)source.Width),
checked((int)source.Height),
1),
};
var destinationOffsets = new ImageBlit.DstOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(
checked((int)_extent.Width),
checked((int)_extent.Height),
1),
};
var region = new ImageBlit
{
SrcSubresource = new ImageSubresourceLayers(
ImageAspectFlags.ColorBit,
0,
0,
1),
SrcOffsets = sourceOffsets,
DstSubresource = new ImageSubresourceLayers(
ImageAspectFlags.ColorBit,
0,
0,
1),
DstOffsets = destinationOffsets,
};
_vk.CmdBlitImage(
_commandBuffer,
source.Image,
ImageLayout.TransferSrcOptimal,
_swapchainImages[imageIndex],
ImageLayout.TransferDstOptimal,
1,
&region,
Filter.Nearest);
if (traceDestination)
{
var destinationToReadback = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.TransferWriteBit,
DstAccessMask = AccessFlags.TransferReadBit,
OldLayout = ImageLayout.TransferDstOptimal,
NewLayout = ImageLayout.TransferSrcOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = _swapchainImages[imageIndex],
SubresourceRange = ColorSubresourceRange(),
};
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TransferBit,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
1,
&destinationToReadback);
var copyRegion = new BufferImageCopy
{
ImageSubresource = new ImageSubresourceLayers
{
AspectMask = ImageAspectFlags.ColorBit,
LayerCount = 1,
},
ImageExtent = new Extent3D(_extent.Width, _extent.Height, 1),
};
_vk.CmdCopyImageToBuffer(
_commandBuffer,
_swapchainImages[imageIndex],
ImageLayout.TransferSrcOptimal,
_stagingBuffer,
1,
&copyRegion);
_swapchainReadbackPending = true;
}
var sourceToShaderRead = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = AccessFlags.TransferReadBit,
DstAccessMask = AccessFlags.ShaderReadBit,
OldLayout = ImageLayout.TransferSrcOptimal,
NewLayout = ImageLayout.ShaderReadOnlyOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = source.Image,
SubresourceRange = ColorSubresourceRange(),
};
var destinationToPresent = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = traceDestination
? AccessFlags.TransferReadBit
: AccessFlags.TransferWriteBit,
DstAccessMask = AccessFlags.MemoryReadBit,
OldLayout = traceDestination
? ImageLayout.TransferSrcOptimal
: ImageLayout.TransferDstOptimal,
NewLayout = ImageLayout.PresentSrcKhr,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = _swapchainImages[imageIndex],
SubresourceRange = ColorSubresourceRange(),
};
barriers[0] = sourceToShaderRead;
barriers[1] = destinationToPresent;
_vk.CmdPipelineBarrier(
_commandBuffer,
PipelineStageFlags.TransferBit,
PipelineStageFlags.AllCommandsBit,
0,
0,
null,
0,
null,
2,
barriers);
EndDebugLabel(_commandBuffer);
}
private void TraceSwapchainReadback()
{
_swapchainReadbackPending = false;
var byteCount = checked((ulong)_extent.Width * _extent.Height * 4);
void* mapped;
Check(
_vk.MapMemory(_device, _stagingMemory, 0, byteCount, 0, &mapped),
"vkMapMemory(swapchain readback)");
try
{
var bytes = new ReadOnlySpan<byte>(mapped, checked((int)byteCount));
var nonzeroBytes = 0L;
var nonblackPixels = 0L;
ulong hash = 14695981039346656037UL;
for (var offset = 0; offset < bytes.Length; offset += 4)
{
var b0 = bytes[offset];
var b1 = bytes[offset + 1];
var b2 = bytes[offset + 2];
var b3 = bytes[offset + 3];
nonzeroBytes += b0 == 0 ? 0 : 1;
nonzeroBytes += b1 == 0 ? 0 : 1;
nonzeroBytes += b2 == 0 ? 0 : 1;
nonzeroBytes += b3 == 0 ? 0 : 1;
nonblackPixels += b0 != 0 || b1 != 0 || b2 != 0 ? 1 : 0;
hash = (hash ^ b0) * 1099511628211UL;
hash = (hash ^ b1) * 1099511628211UL;
hash = (hash ^ b2) * 1099511628211UL;
hash = (hash ^ b3) * 1099511628211UL;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.swapchain_image size={_extent.Width}x{_extent.Height} " +
$"format={_swapchainFormat} nonzero_bytes={nonzeroBytes}/{byteCount} " +
$"nonblack_pixels={nonblackPixels}/{(ulong)_extent.Width * _extent.Height} " +
$"hash=0x{hash:X16}");
}
finally
{
_vk.UnmapMemory(_device, _stagingMemory);
}
}
private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities) private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities)
{ {
if (capabilities.CurrentExtent.Width != uint.MaxValue) if (capabilities.CurrentExtent.Width != uint.MaxValue)