Amd fix opcode nid filecache wave32 (#903)
* AMD RDNA2 FIX, WAVE32 FIX, GatherResourceFileInfo imporove with cache, sop 0x0F * multiple vulkan fixes * Astrobot video black square fix * Fix tests: supported depth/stencil formats on AMD, non-null unmapped pointer, robust allocation check
This commit is contained in:
parent
22e73fbea1
commit
d4ff32a1d2
|
|
@ -127,11 +127,6 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
return -1;
|
||||
}
|
||||
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
|
|
@ -145,6 +140,13 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
return -1;
|
||||
}
|
||||
// Only a fault none of the handlers above resolves aborts a TBB worker: a write
|
||||
// to a lazily committed or GPU-tracked page is routine, and aborting the worker
|
||||
// there strands the guest allocator lock it may hold.
|
||||
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverIllegalInstruction(contextRecord, rip))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1554,8 +1554,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
0x75, 0xE7,
|
||||
0xC3,
|
||||
],
|
||||
// strcasecmp: ASCII folding like the HLE export, and the same answer for null
|
||||
// arguments (0 when both are null, 1 otherwise) instead of faulting.
|
||||
"AV6ipCNa4Rw" =>
|
||||
[
|
||||
0x48, 0x85, 0xFF, // test rdi, rdi
|
||||
0x74, 0x34, // jz null
|
||||
0x48, 0x85, 0xF6, // test rsi, rsi
|
||||
0x74, 0x2F, // jz null
|
||||
0x0F, 0xB6, 0x07,
|
||||
0x0F, 0xB6, 0x16,
|
||||
0x8D, 0x48, 0xBF,
|
||||
|
|
@ -1573,6 +1579,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
0x48, 0xFF, 0xC7,
|
||||
0x48, 0xFF, 0xC6,
|
||||
0xEB, 0xD4,
|
||||
0x48, 0x98, // cdqe: sign-extend like the HLE export
|
||||
0xC3,
|
||||
0x31, 0xC0, // null: xor eax, eax
|
||||
0x48, 0x39, 0xF7, // cmp rdi, rsi
|
||||
0x0F, 0x95, 0xC0, // setne al
|
||||
0xC3,
|
||||
],
|
||||
"viiwFMaNamA" =>
|
||||
|
|
@ -1823,8 +1834,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||
private static bool IsHlePreferredNid(string nid)
|
||||
{
|
||||
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
|
||||
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal) ||
|
||||
string.Equals(nid, "AV6ipCNa4Rw", StringComparison.Ordinal);
|
||||
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsLibcLibrary(string libraryName)
|
||||
|
|
|
|||
|
|
@ -15,14 +15,29 @@ internal static class AmprFileRegistry
|
|||
private const uint CacheMagicV3 = 0x33495041u; // 'API3'
|
||||
private const uint CacheVersionV3 = 3;
|
||||
|
||||
private static readonly ConcurrentDictionary<uint, string> _hostPathsById = new(
|
||||
// Which spelling produced a compatibility id. When two files collide on one
|
||||
// 31-bit id, the better-ranked spelling owns it; only a tie is ambiguous.
|
||||
// Ranking makes the outcome independent of publish order, which the
|
||||
// parallel app0 index and concurrent guest resolves do not guarantee.
|
||||
private enum AliasRank : byte
|
||||
{
|
||||
// The title resolved this exact guest path through APR.
|
||||
Resolved,
|
||||
// "$/" and "/app0/": spellings titles bake into asset tables.
|
||||
GuestPath,
|
||||
// "app0/" and bare relative paths: emulator-side fallbacks.
|
||||
Compatibility,
|
||||
}
|
||||
|
||||
private readonly record struct AliasEntry(string HostPath, AliasRank Rank, bool Ambiguous);
|
||||
|
||||
private static readonly ConcurrentDictionary<uint, AliasEntry> _hostPathsById = new(
|
||||
concurrencyLevel: Math.Max(4, Environment.ProcessorCount),
|
||||
capacity: 1_048_576);
|
||||
private static readonly object _indexGate = new();
|
||||
private static readonly object _resolvedFileGate = new();
|
||||
private static readonly Dictionary<string, uint> _resolvedIdsByPath = new(HostFsPath.Comparer);
|
||||
private static readonly ConcurrentDictionary<uint, string> _resolvedPathsById = new();
|
||||
private static readonly ConcurrentDictionary<uint, byte> _ambiguousCompatibilityIds = new();
|
||||
// Resolved handles use a separate range from the 31-bit compatibility hashes.
|
||||
// Never reuse a handle while queued reads can still refer to it.
|
||||
private static uint _nextResolvedId = 0x80000000;
|
||||
|
|
@ -62,8 +77,7 @@ internal static class AmprFileRegistry
|
|||
// process-local handles used by Register() separate from this path so a
|
||||
// title can compare resolved ids with ids baked into its asset tables.
|
||||
var fileId = ComputeFileId(guestPath);
|
||||
_hostPathsById[fileId] = hostPath;
|
||||
_ambiguousCompatibilityIds.TryRemove(fileId, out _);
|
||||
PublishCompatibilityPath(fileId, hostPath, AliasRank.Resolved);
|
||||
return fileId;
|
||||
}
|
||||
|
||||
|
|
@ -71,8 +85,14 @@ internal static class AmprFileRegistry
|
|||
{
|
||||
if ((id & 0x80000000) != 0)
|
||||
return _resolvedPathsById.TryGetValue(id, out hostPath!);
|
||||
if (_hostPathsById.TryGetValue(id, out var entry) && !entry.Ambiguous)
|
||||
{
|
||||
hostPath = entry.HostPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
hostPath = null!;
|
||||
return !_ambiguousCompatibilityIds.ContainsKey(id) && _hostPathsById.TryGetValue(id, out hostPath!);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Test hook: wipe registry state between cases.</summary>
|
||||
|
|
@ -81,7 +101,6 @@ internal static class AmprFileRegistry
|
|||
lock (_indexGate)
|
||||
{
|
||||
_hostPathsById.Clear();
|
||||
_ambiguousCompatibilityIds.Clear();
|
||||
lock (_resolvedFileGate)
|
||||
{
|
||||
_resolvedIdsByPath.Clear();
|
||||
|
|
@ -287,25 +306,48 @@ internal static class AmprFileRegistry
|
|||
private static void RegisterApp0Relative(string relative, string hostPath)
|
||||
{
|
||||
// "$/" + relative
|
||||
Publish(FnvContinueAscii(FnvContinueAscii(OffsetBasis, (byte)'$'), (byte)'/'), relative, hostPath);
|
||||
Publish(FnvContinueAscii(FnvContinueAscii(OffsetBasis, (byte)'$'), (byte)'/'), relative, hostPath, AliasRank.GuestPath);
|
||||
// "/app0/" + relative
|
||||
Publish(FnvContinueAsciiPrefix(OffsetBasis, "/app0/"u8), relative, hostPath);
|
||||
Publish(FnvContinueAsciiPrefix(OffsetBasis, "/app0/"u8), relative, hostPath, AliasRank.GuestPath);
|
||||
// "app0/" + relative
|
||||
Publish(FnvContinueAsciiPrefix(OffsetBasis, "app0/"u8), relative, hostPath);
|
||||
Publish(FnvContinueAsciiPrefix(OffsetBasis, "app0/"u8), relative, hostPath, AliasRank.Compatibility);
|
||||
// bare relative
|
||||
Publish(OffsetBasis, relative, hostPath);
|
||||
Publish(OffsetBasis, relative, hostPath, AliasRank.Compatibility);
|
||||
}
|
||||
|
||||
private static void Publish(uint hash, string relative, string hostPath)
|
||||
private static void Publish(uint hash, string relative, string hostPath, AliasRank rank)
|
||||
{
|
||||
PublishCompatibilityPath(NormalizeFileId(FnvContinueUtf8(hash, relative)), hostPath);
|
||||
PublishCompatibilityPath(NormalizeFileId(FnvContinueUtf8(hash, relative)), hostPath, rank);
|
||||
}
|
||||
|
||||
private static void PublishCompatibilityPath(uint id, string hostPath)
|
||||
private static void PublishCompatibilityPath(uint id, string hostPath, AliasRank rank)
|
||||
{
|
||||
var existingPath = _hostPathsById.GetOrAdd(id, hostPath);
|
||||
if (!HostFsPath.Comparer.Equals(existingPath, hostPath))
|
||||
_ambiguousCompatibilityIds.TryAdd(id, 0);
|
||||
var incoming = new AliasEntry(hostPath, rank, Ambiguous: false);
|
||||
while (true)
|
||||
{
|
||||
if (!_hostPathsById.TryGetValue(id, out var existing))
|
||||
{
|
||||
if (_hostPathsById.TryAdd(id, incoming))
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
var merged = MergeAlias(existing, incoming);
|
||||
if (merged == existing || _hostPathsById.TryUpdate(id, merged, existing))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static AliasEntry MergeAlias(AliasEntry existing, AliasEntry incoming)
|
||||
{
|
||||
if (incoming.Rank != existing.Rank)
|
||||
return incoming.Rank < existing.Rank ? incoming : existing;
|
||||
// A title may re-resolve an id; its latest resolve owns it.
|
||||
if (incoming.Rank == AliasRank.Resolved)
|
||||
return incoming;
|
||||
if (existing.Ambiguous || HostFsPath.Comparer.Equals(existing.HostPath, incoming.HostPath))
|
||||
return existing;
|
||||
return existing with { Ambiguous = true };
|
||||
}
|
||||
|
||||
internal static uint ComputeFileId(string guestPath)
|
||||
|
|
@ -477,10 +519,11 @@ internal static class AmprFileRegistry
|
|||
: normalizedRoot + Path.DirectorySeparatorChar +
|
||||
entry.Relative.Replace('/', Path.DirectorySeparatorChar);
|
||||
// Normalize cached identifiers to the guest's 31-bit range.
|
||||
PublishCompatibilityPath(NormalizeFileId(entry.Id0), hostPath);
|
||||
PublishCompatibilityPath(NormalizeFileId(entry.Id1), hostPath);
|
||||
PublishCompatibilityPath(NormalizeFileId(entry.Id2), hostPath);
|
||||
PublishCompatibilityPath(NormalizeFileId(entry.Id3), hostPath);
|
||||
// Ids are stored in RegisterApp0Relative order: $/ /app0/ app0/ bare.
|
||||
PublishCompatibilityPath(NormalizeFileId(entry.Id0), hostPath, AliasRank.GuestPath);
|
||||
PublishCompatibilityPath(NormalizeFileId(entry.Id1), hostPath, AliasRank.GuestPath);
|
||||
PublishCompatibilityPath(NormalizeFileId(entry.Id2), hostPath, AliasRank.Compatibility);
|
||||
PublishCompatibilityPath(NormalizeFileId(entry.Id3), hostPath, AliasRank.Compatibility);
|
||||
});
|
||||
}
|
||||
else
|
||||
|
|
@ -538,9 +581,9 @@ internal static class AmprFileRegistry
|
|||
}
|
||||
|
||||
var relatives = new HashSet<string>(HostFsPath.Comparer);
|
||||
foreach (var hostPath in _hostPathsById.Values)
|
||||
foreach (var entry in _hostPathsById.Values)
|
||||
{
|
||||
var relative = Path.GetRelativePath(normalizedRoot, hostPath)
|
||||
var relative = Path.GetRelativePath(normalizedRoot, entry.HostPath)
|
||||
.Replace('\\', '/');
|
||||
if (string.IsNullOrEmpty(relative) ||
|
||||
relative.StartsWith("..", StringComparison.Ordinal))
|
||||
|
|
|
|||
|
|
@ -189,6 +189,8 @@ public static class ShaderRegisterOffset
|
|||
public const uint SpiShaderUserAccumEsGs0 = 0x0B2;
|
||||
public const uint SpiShaderPgmLoEs = 0x0C8;
|
||||
public const uint SpiShaderPgmHiEs = 0x0C9;
|
||||
public const uint SpiShaderPgmRsrc1Es = 0x0CA;
|
||||
public const uint SpiShaderPgmRsrc2Es = 0x0CB;
|
||||
public const uint SpiShaderUserDataEs0 = 0x0CC;
|
||||
public const uint SpiShaderPgmChksumHs = 0x100;
|
||||
public const uint SpiShaderPgmRsrc4Hs = 0x101;
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ internal static partial class RegisterWriters
|
|||
indirect[SpiShaderPgmHiLs] = static (banks, _, value) => banks.Shader.Vertex.LocalAddress = RegisterField.WithHighAddress(banks.Shader.Vertex.LocalAddress, value);
|
||||
indirect[SpiShaderPgmLoEs] = static (banks, _, value) => banks.Shader.Vertex.ExportAddress = RegisterField.WithLowAddress(banks.Shader.Vertex.ExportAddress, value);
|
||||
indirect[SpiShaderPgmHiEs] = static (banks, _, value) => banks.Shader.Vertex.ExportAddress = RegisterField.WithHighAddress(banks.Shader.Vertex.ExportAddress, value);
|
||||
indirect[SpiShaderPgmRsrc1Es] = static (banks, _, value) => banks.Shader.Vertex.ExportResource1 = value;
|
||||
indirect[SpiShaderPgmRsrc2Es] = static (banks, _, value) => banks.Shader.Vertex.ExportResource2 = value;
|
||||
indirect[SpiShaderPgmLoGs] = static (banks, _, value) => banks.Shader.Vertex.GeometryAddress = RegisterField.WithLowAddress(banks.Shader.Vertex.GeometryAddress, value);
|
||||
indirect[SpiShaderPgmHiGs] = static (banks, _, value) => banks.Shader.Vertex.GeometryAddress = RegisterField.WithHighAddress(banks.Shader.Vertex.GeometryAddress, value);
|
||||
indirect[SpiShaderPgmRsrc1Gs] = static (banks, _, value) => banks.Shader.Vertex.GeometryResource1 = GeometryResource1.Decode(value);
|
||||
|
|
|
|||
|
|
@ -189,7 +189,10 @@ public sealed class VertexStageRegisters
|
|||
public GeometryResource2 GeometryResource2;
|
||||
public UserScalarRegisters HullUserScalars = new();
|
||||
public UserScalarRegisters GeometryUserScalars = new();
|
||||
// The legacy vertex block and the export user scalars are stored, not decoded.
|
||||
// The legacy vertex block and the export resources and user scalars are stored, not
|
||||
// decoded: the merged export stage runs with the geometry resources.
|
||||
public uint ExportResource1;
|
||||
public uint ExportResource2;
|
||||
public ulong LegacyVertexAddress;
|
||||
public uint LegacyVertexResource1;
|
||||
public uint LegacyVertexResource2;
|
||||
|
|
|
|||
|
|
@ -260,7 +260,9 @@ public sealed unsafe partial class CachedImage : IDisposable
|
|||
usage |= ImageUsageFlags.ColorAttachmentBit;
|
||||
}
|
||||
|
||||
if (description.Samples == 1)
|
||||
var storageFormat = ViewFormatRules.SrgbStorageFormat(description.PixelFormat);
|
||||
var storageFeatures = storageFormat == Format.Undefined ? features : device.GetFormatProperties(storageFormat).OptimalTilingFeatures;
|
||||
if (description.Samples == 1 && (storageFeatures & FormatFeatureFlags.StorageImageBit) != 0)
|
||||
{
|
||||
usage |= ImageUsageFlags.StorageBit;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ public static partial class ImageRequestBuilders
|
|||
TextureNumericClass.Sint => (Format.R32Sint, GuestPixelFormat.Bits32SInt),
|
||||
_ => throw SubmissionScheduler.Fatal($"A null image needs a supported numeric class: class={numericClass}."),
|
||||
};
|
||||
// Depth-reference sampling needs a depth view even for the placeholder.
|
||||
var depthCompare = shape.DepthCompare && numericClass == TextureNumericClass.Float && !storage;
|
||||
if (depthCompare)
|
||||
{
|
||||
format = Format.D32Sfloat;
|
||||
}
|
||||
|
||||
var description = ImageDescription.Create();
|
||||
description.PixelFormat = format;
|
||||
description.GuestFormat = guestFormat;
|
||||
|
|
@ -44,12 +51,20 @@ public static partial class ImageRequestBuilders
|
|||
: shape.OneDimensional
|
||||
? shape.Arrayed ? ImageViewType.Type1DArray : ImageViewType.Type1D
|
||||
: shape.Arrayed ? ImageViewType.Type2DArray : ImageViewType.Type2D,
|
||||
Aspect = ImageAspectFlags.ColorBit,
|
||||
Aspect = depthCompare ? ImageAspectFlags.DepthBit : ImageAspectFlags.ColorBit,
|
||||
Usage = storage ? ImageUsageFlags.StorageBit : ImageUsageFlags.SampledBit,
|
||||
};
|
||||
return new ImageRequest(description, view, storage ? ImageRole.StorageImage : ImageRole.Texture);
|
||||
}
|
||||
|
||||
// A depth placeholder reads its value into red, as sampled depth views do.
|
||||
private static TextureRequestResolution NullTextureResolution(in ShaderImageShape shape)
|
||||
{
|
||||
var request = NullTexture(shape);
|
||||
var swizzle = request.Description.IsDepth ? ViewFormatRules.PackDestinationSelect(4, 0, 0, 1) : 0u;
|
||||
return new TextureRequestResolution(request, false, request.View.Format, swizzle);
|
||||
}
|
||||
|
||||
// Fills the mip layout of a texture from the guest tiling rules.
|
||||
public static void PopulateTextureMipLayout(ref ImageDescription description)
|
||||
{
|
||||
|
|
@ -143,8 +158,7 @@ public static partial class ImageRequestBuilders
|
|||
var storage = shape.Storage;
|
||||
if (descriptor.BaseAddress == 0)
|
||||
{
|
||||
var nullRequest = NullTexture(shape);
|
||||
return new TextureRequestResolution(nullRequest, false, nullRequest.View.Format, 0);
|
||||
return NullTextureResolution(shape);
|
||||
}
|
||||
|
||||
var address = descriptor.BaseAddress;
|
||||
|
|
@ -162,6 +176,12 @@ public static partial class ImageRequestBuilders
|
|||
var depthTile = tile == GuestTileMode.Depth;
|
||||
var msaaTile = depthTile || tile == GuestTileMode.RenderTarget;
|
||||
var msaaArray = type == GuestImageType.Color2DMsaaArray;
|
||||
|
||||
if (!multisampled && baseLevel >= levels)
|
||||
{
|
||||
return NullTextureResolution(shape);
|
||||
}
|
||||
|
||||
if ((!multisampled && (baseLevel > viewLastLevel || viewLastLevel >= levels)) ||
|
||||
(multisampled &&
|
||||
(baseLevel != 0 || lastLevel == 0 || lastLevel > 3 || maxMip != lastLevel || !msaaTile || (descriptor.MsaaDepth && !depthTile) ||
|
||||
|
|
|
|||
|
|
@ -439,7 +439,11 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
|
|||
}
|
||||
|
||||
renderingState.ColorFormats[index] = format;
|
||||
parameters.SetColorMask(index, color.Resolution.ExportMapping.ApplyMask(context.RenderTargetMaskForSlot(color.Slot)));
|
||||
// A target the pixel program never exports keeps its contents, as on hardware; the
|
||||
// host output would otherwise write an undefined value (e.g. depth-only passes that
|
||||
// leave a color target bound and export only to the null target).
|
||||
var exported = pixelStage is null || ((pixelStage.PixelColorExportMasks >> (int)(color.Slot * 4)) & 0xFu) != 0;
|
||||
parameters.SetColorMask(index, exported ? color.Resolution.ExportMapping.ApplyMask(context.RenderTargetMaskForSlot(color.Slot)) : 0);
|
||||
}
|
||||
|
||||
var withDepth = depth.HasTarget;
|
||||
|
|
|
|||
|
|
@ -305,7 +305,9 @@ internal sealed class ShaderProgramCache
|
|||
{
|
||||
plan = ShaderResourcePlan.Extract(program, source.Stage, source.Hash, source.UserDataBase, (uint)source.UserData.Length,
|
||||
fetch?.Loads.Select(load => load.Pc).ToHashSet(),
|
||||
beforeResourceTracking: dumpPlanning ? resourcePlan => ShaderPlanningDump.WriteGraph(source, resourcePlan) : null);
|
||||
beforeResourceTracking: dumpPlanning ? resourcePlan => ShaderPlanningDump.WriteGraph(source, resourcePlan) : null,
|
||||
// Graphics stages compile as wave32 (see the compile request); compute follows the dispatch.
|
||||
waveSize: source.Stage == ShaderStage.Compute ? options.ComputeInfo?.WaveSize ?? 64u : 32u);
|
||||
}
|
||||
catch (ResourcePlanException exception)
|
||||
{
|
||||
|
|
@ -629,6 +631,7 @@ internal sealed class ShaderProgramCache
|
|||
UserDataBase = source.UserDataBase,
|
||||
UserDataCount = (uint)source.UserData.Length,
|
||||
ParameterExportMask = entry.Program.ParameterExportMask,
|
||||
PixelColorExportMasks = entry.Program.PixelColorExportMasks,
|
||||
VertexOffsetScalarRegister = entry.EmbeddedFetch?.VertexOffsetScalarRegister ?? ShaderProgramInfo.NoScalarRegister,
|
||||
InstanceOffsetScalarRegister = entry.EmbeddedFetch?.InstanceOffsetScalarRegister ?? ShaderProgramInfo.NoScalarRegister,
|
||||
UsesDeviceAddresses = info.UsesDeviceAddresses,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ public class ShaderProgramInfo
|
|||
public uint UserDataBase { get; init; }
|
||||
public uint UserDataCount { get; init; }
|
||||
public uint ParameterExportMask { get; init; }
|
||||
// Four component bits per color target (MRT0 in the low nibble) the pixel program exports.
|
||||
public uint PixelColorExportMasks { get; init; } = uint.MaxValue;
|
||||
public int VertexOffsetScalarRegister { get; init; } = NoScalarRegister;
|
||||
public int InstanceOffsetScalarRegister { get; init; } = NoScalarRegister;
|
||||
public bool UsesDeviceAddresses { get; init; }
|
||||
|
|
|
|||
|
|
@ -30,6 +30,20 @@ public static class KernelExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "NH6xARDOVv8",
|
||||
ExportName = "sceKernelGetOperationMode",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelGetOperationMode(CpuContext ctx)
|
||||
{
|
||||
// SCE_KERNEL_MODE_RELEASE = 0. Reporting retail/release mode keeps guest
|
||||
// code from taking devkit/tool-only branches we do not emulate.
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8zLSfEfW5AU",
|
||||
ExportName = "sceCoredumpRegisterCoredumpHandler",
|
||||
|
|
|
|||
|
|
@ -122,6 +122,12 @@ public static partial class KernelMemoryCompatExports
|
|||
// "/app0/Data.bin" even though that file exists.
|
||||
private static readonly HashSet<string> _negativeStatCache = new(HostFsPath.Comparer);
|
||||
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(HostFsPath.Comparer);
|
||||
// Mount components already found to exist without being reparse points. Titles resolve
|
||||
// every asset path at startup; re-reading each directory's attributes costs seconds.
|
||||
private static readonly ConcurrentDictionary<string, byte> _verifiedMountComponents = new(HostFsPath.Comparer);
|
||||
private static readonly ConcurrentDictionary<string, byte> _verifiedMountDirectories = new(HostFsPath.Comparer);
|
||||
private static readonly ConcurrentDictionary<string, string> _fullMountRoots = new(StringComparer.Ordinal);
|
||||
private static readonly ConcurrentDictionary<string, byte> _aprScannedDirectories = new(HostFsPath.Comparer);
|
||||
private static long _nextFileDescriptor = 2;
|
||||
private static string _applicationTitleId = "UNKNOWN";
|
||||
|
||||
|
|
@ -1741,7 +1747,10 @@ public static partial class KernelMemoryCompatExports
|
|||
}
|
||||
|
||||
var fileId = AmprFileRegistry.RegisterAprResolvedPath(guestPath, hostPath);
|
||||
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
|
||||
if (_logIo)
|
||||
{
|
||||
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
|
||||
}
|
||||
|
||||
if (idsAddress != 0 &&
|
||||
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
|
||||
|
|
@ -1761,6 +1770,7 @@ public static partial class KernelMemoryCompatExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
|
||||
// WithPrefix sibling of sceKernelAprResolveFilepathsToIdsAndFileSizes.
|
||||
// Resource streamers resolve relative asset paths against a shared directory
|
||||
// prefix. Without HLE, every call returned the generic NOT_FOUND sentinel
|
||||
|
|
@ -5000,7 +5010,7 @@ public static partial class KernelMemoryCompatExports
|
|||
string candidate;
|
||||
try
|
||||
{
|
||||
fullRoot = Path.GetFullPath(mountRoot);
|
||||
fullRoot = _fullMountRoots.GetOrAdd(mountRoot, static root => Path.GetFullPath(root));
|
||||
candidate = Path.GetFullPath(Path.Combine(fullRoot, relative));
|
||||
}
|
||||
catch (Exception ex) when (
|
||||
|
|
@ -5047,7 +5057,12 @@ public static partial class KernelMemoryCompatExports
|
|||
return false;
|
||||
}
|
||||
|
||||
var relative = Path.GetRelativePath(rootTrimmed, candidate);
|
||||
// Callers pass full paths; one already under the root needs no renormalization.
|
||||
var relative = candidate.Length > rootTrimmed.Length + 1 &&
|
||||
candidate.StartsWith(rootTrimmed, HostFsPath.Comparison) &&
|
||||
candidate[rootTrimmed.Length] == Path.DirectorySeparatorChar
|
||||
? candidate[(rootTrimmed.Length + 1)..]
|
||||
: Path.GetRelativePath(rootTrimmed, candidate);
|
||||
// A leading ".." segment means the candidate is not under the root. Match
|
||||
// the segment precisely: bare ".." or a "../" prefix, NOT a legitimate
|
||||
// file merely named "..foo". This branch is a defensive fallback (lexical
|
||||
|
|
@ -5066,13 +5081,46 @@ public static partial class KernelMemoryCompatExports
|
|||
Path.DirectorySeparatorChar,
|
||||
StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var parent = current;
|
||||
current = Path.Combine(current, segment);
|
||||
if (_verifiedMountComponents.ContainsKey(current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// One listing returns every entry's attributes: verify the whole parent directory
|
||||
// at once. Reparse points stay unverified and are rejected below.
|
||||
if (_verifiedMountDirectories.TryAdd(parent, 0))
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var entry in new DirectoryInfo(parent).EnumerateFileSystemInfos())
|
||||
{
|
||||
if ((entry.Attributes & FileAttributes.ReparsePoint) == 0)
|
||||
{
|
||||
_verifiedMountComponents.TryAdd(entry.FullName, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (
|
||||
ex is IOException or UnauthorizedAccessException or System.Security.SecurityException)
|
||||
{
|
||||
}
|
||||
|
||||
if (_verifiedMountComponents.ContainsKey(current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if ((File.GetAttributes(current) & FileAttributes.ReparsePoint) != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_verifiedMountComponents.TryAdd(current, 0);
|
||||
}
|
||||
catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException)
|
||||
{
|
||||
|
|
@ -6773,6 +6821,27 @@ public static partial class KernelMemoryCompatExports
|
|||
return true;
|
||||
}
|
||||
|
||||
// One directory listing returns every file's size; titles resolve whole asset
|
||||
// directories at startup, so this replaces one host query per file.
|
||||
if (Path.GetDirectoryName(cachePath) is { } directory && _aprScannedDirectories.TryAdd(directory, 0))
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var file in new DirectoryInfo(directory).EnumerateFiles())
|
||||
{
|
||||
_aprFileSizeCache.TryAdd(file.FullName, file.Length < 0 ? 0UL : unchecked((ulong)file.Length));
|
||||
}
|
||||
}
|
||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or System.Security.SecurityException)
|
||||
{
|
||||
}
|
||||
|
||||
if (_aprFileSizeCache.TryGetValue(cachePath, out size))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(cachePath);
|
||||
|
|
@ -6976,9 +7045,13 @@ public static partial class KernelMemoryCompatExports
|
|||
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
||||
}
|
||||
|
||||
// Read once: file-heavy titles call LogIoTrace for every resolved path.
|
||||
private static readonly bool _logIo =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IO"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static void LogIoTrace(string operation, string path, string detail)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IO"), "1", StringComparison.Ordinal))
|
||||
if (!_logIo)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,10 +124,20 @@ public static class GameServiceStubs
|
|||
Target = Generation.Gen5, LibraryName = "libSceVoice")]
|
||||
public static int VoiceEnd(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
|
||||
[SysAbiExport(Nid = "amuBfI-AQc4", ExportName = "sceRudpInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceRudp")]
|
||||
public static int RudpInit(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "6PBNpsgyaxw", ExportName = "sceRudpEnableInternalIOThread",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceRudp")]
|
||||
public static int RudpEnableInternalIOThread(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
[SysAbiExport(Nid = "SUEVes8gvmw", ExportName = "sceRudpSetEventHandler",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceRudp")]
|
||||
public static int RudpSetEventHandler(CpuContext ctx) => Ok(ctx);
|
||||
|
||||
|
||||
[SysAbiExport(Nid = "84fDxStrG44", ExportName = "sceDeviceServiceInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceDeviceService")]
|
||||
public static int DeviceServiceInitialize(CpuContext ctx) => Ok(ctx);
|
||||
|
|
|
|||
|
|
@ -308,7 +308,8 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
return new BufferBinding(stream.Handle.Handle, offset);
|
||||
}
|
||||
|
||||
var buffer = CreateHostBuffer(data, BufferUsageFlags.IndexBufferBit | BufferUsageFlags.VertexBufferBit, out var memory, out _);
|
||||
// Transient uploads also back storage-buffer descriptors, so match the ring's usage.
|
||||
var buffer = CreateHostBuffer(data, GpuBuffer.AllFlags, out var memory, out _);
|
||||
preparation.OverflowBuffers.Add((buffer, memory));
|
||||
if (RenderTrace.Enabled)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -345,6 +345,10 @@ public static partial class Gen5MslTranslator
|
|||
$"extract_bits({RawSource(instruction, 0)}, ({RawSource(instruction, 1)}) & 31u, ({RawSource(instruction, 2)}) & 31u)",
|
||||
"VBfiB32" =>
|
||||
$"((({RawSource(instruction, 0)}) & ({RawSource(instruction, 1)})) | (~({RawSource(instruction, 0)}) & ({RawSource(instruction, 2)})))",
|
||||
"VAlignbitB32" =>
|
||||
$"uint(((ulong({RawSource(instruction, 0)}) << 32) | ulong({RawSource(instruction, 1)})) >> (({RawSource(instruction, 2)}) & 31u))",
|
||||
"VAlignbyteB32" =>
|
||||
$"uint(((ulong({RawSource(instruction, 0)}) << 32) | ulong({RawSource(instruction, 1)})) >> ((({RawSource(instruction, 2)}) & 3u) * 8u))",
|
||||
"VBfmB32" =>
|
||||
$"(((1u << (({RawSource(instruction, 0)}) & 31u)) - 1u) << (({RawSource(instruction, 1)}) & 31u))",
|
||||
"VBfrevB32" => $"reverse_bits({RawSource(instruction, 0)})",
|
||||
|
|
|
|||
|
|
@ -912,6 +912,8 @@ public static partial class Gen5MslTranslator
|
|||
case "SWaitcnt":
|
||||
case "SInstPrefetch":
|
||||
case "STtraceData":
|
||||
// Wave scheduling priority hint; no effect on results.
|
||||
case "SSetprio":
|
||||
case "SClause":
|
||||
case "VNop":
|
||||
// NGG shaders bracket their exports with s_sendmsg
|
||||
|
|
|
|||
|
|
@ -508,9 +508,11 @@ public static partial class Gen5SpirvTranslator
|
|||
result = EmitIntegerBinary(instruction, SpirvOp.ISub, reverse: true);
|
||||
break;
|
||||
case "VSubbU32":
|
||||
case "VSubCoCiU32":
|
||||
result = EmitSubtractWithBorrow(instruction, reverse: false);
|
||||
break;
|
||||
case "VSubbrevU32":
|
||||
case "VSubrevCoCiU32":
|
||||
result = EmitSubtractWithBorrow(instruction, reverse: true);
|
||||
break;
|
||||
case "VMulI32I24":
|
||||
|
|
@ -1061,10 +1063,14 @@ public static partial class Gen5SpirvTranslator
|
|||
break;
|
||||
}
|
||||
case "VAlignbitB32":
|
||||
case "VAlignbyteB32":
|
||||
{
|
||||
var high = GetRawSource(instruction, 0);
|
||||
var low = GetRawSource(instruction, 1);
|
||||
var shift = BitwiseAnd(GetRawSource(instruction, 2), UInt(31));
|
||||
// ({S0,S1} >> shift)[31:0]; ALIGNBYTE shifts by whole bytes of S2[1:0].
|
||||
var shift = instruction.Opcode == "VAlignbyteB32"
|
||||
? ShiftLeftLogical(BitwiseAnd(GetRawSource(instruction, 2), UInt(3)), UInt(3))
|
||||
: BitwiseAnd(GetRawSource(instruction, 2), UInt(31));
|
||||
var lowPart = ShiftRightLogical(low, shift);
|
||||
var inverse = BitwiseAnd(ISubU(UInt(32), shift), UInt(31));
|
||||
var highPartRaw = ShiftLeftLogical(high, inverse);
|
||||
|
|
@ -1750,14 +1756,16 @@ public static partial class Gen5SpirvTranslator
|
|||
else if (opcode is
|
||||
"VCmpFF32" or "VCmpxFF32" or
|
||||
"VCmpFF16" or "VCmpxFF16" or
|
||||
"VCmpFI32" or "VCmpFU32")
|
||||
"VCmpFI32" or "VCmpFU32" or
|
||||
"VCmpFI64" or "VCmpxFI64" or "VCmpFU64" or "VCmpxFU64")
|
||||
{
|
||||
condition = _module.ConstantBool(false);
|
||||
}
|
||||
else if (opcode is
|
||||
"VCmpTruF32" or "VCmpxTruF32" or
|
||||
"VCmpTruF16" or "VCmpxTruF16" or
|
||||
"VCmpTI32" or "VCmpTU32")
|
||||
"VCmpTI32" or "VCmpTU32" or
|
||||
"VCmpTI64" or "VCmpxTI64" or "VCmpTU64" or "VCmpxTU64")
|
||||
{
|
||||
condition = _module.ConstantBool(true);
|
||||
}
|
||||
|
|
@ -1880,6 +1888,11 @@ public static partial class Gen5SpirvTranslator
|
|||
right = Bitcast(signedType, right);
|
||||
}
|
||||
}
|
||||
else if (opcode.EndsWith("I64", StringComparison.Ordinal))
|
||||
{
|
||||
left = Bitcast(_longType, left);
|
||||
right = Bitcast(_longType, right);
|
||||
}
|
||||
|
||||
var operation = opcode switch
|
||||
{
|
||||
|
|
@ -1895,6 +1908,16 @@ public static partial class Gen5SpirvTranslator
|
|||
"VCmpLeI16" or "VCmpxLeI16" => SpirvOp.SLessThanEqual,
|
||||
"VCmpGtI16" or "VCmpxGtI16" => SpirvOp.SGreaterThan,
|
||||
"VCmpGeI16" or "VCmpxGeI16" => SpirvOp.SGreaterThanEqual,
|
||||
"VCmpEqU64" or "VCmpxEqU64" or
|
||||
"VCmpEqI64" or "VCmpxEqI64" => SpirvOp.IEqual,
|
||||
"VCmpLtU64" or "VCmpxLtU64" => SpirvOp.ULessThan,
|
||||
"VCmpLeU64" or "VCmpxLeU64" => SpirvOp.ULessThanEqual,
|
||||
"VCmpGtU64" or "VCmpxGtU64" => SpirvOp.UGreaterThan,
|
||||
"VCmpGeU64" or "VCmpxGeU64" => SpirvOp.UGreaterThanEqual,
|
||||
"VCmpLtI64" or "VCmpxLtI64" => SpirvOp.SLessThan,
|
||||
"VCmpLeI64" or "VCmpxLeI64" => SpirvOp.SLessThanEqual,
|
||||
"VCmpGtI64" or "VCmpxGtI64" => SpirvOp.SGreaterThan,
|
||||
"VCmpGeI64" or "VCmpxGeI64" => SpirvOp.SGreaterThanEqual,
|
||||
"VCmpLtI32" or "VCmpxLtI32" => SpirvOp.SLessThan,
|
||||
"VCmpLeI32" or "VCmpxLeI32" => SpirvOp.SLessThanEqual,
|
||||
"VCmpGtI32" or "VCmpxGtI32" => SpirvOp.SGreaterThan,
|
||||
|
|
@ -1955,10 +1978,12 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
else
|
||||
{
|
||||
var compareDestination = instruction.Control is Gen5SdwaControl
|
||||
{ ScalarDestination: { } scalarDestination }
|
||||
? scalarDestination
|
||||
: 106u;
|
||||
var compareDestination = instruction.Control switch
|
||||
{
|
||||
Gen5SdwaControl { ScalarDestination: { } scalarDestination } => scalarDestination,
|
||||
Gen5Vop3Control { ScalarDestination: { } scalarDestination } => scalarDestination,
|
||||
_ => 106u,
|
||||
};
|
||||
StoreWaveMask(compareDestination, activeCondition);
|
||||
}
|
||||
|
||||
|
|
@ -3162,12 +3187,7 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
var targetLane = IAdd(BitwiseAnd(lane, UInt(0xFFFF_FFF8)), selector);
|
||||
targetLane = BitwiseAnd(targetLane, UInt(31));
|
||||
var shuffled = _module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformShuffle,
|
||||
_uintType,
|
||||
UInt(3),
|
||||
value,
|
||||
targetLane);
|
||||
var shuffled = ShuffleLane(value, targetLane);
|
||||
if (control.FetchInactive)
|
||||
{
|
||||
return shuffled;
|
||||
|
|
@ -3180,12 +3200,7 @@ public static partial class Gen5SpirvTranslator
|
|||
UInt(1),
|
||||
UInt(0));
|
||||
var sourceActive = IsNotZero(
|
||||
_module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformShuffle,
|
||||
_uintType,
|
||||
UInt(3),
|
||||
activeWord,
|
||||
targetLane));
|
||||
ShuffleLane(activeWord, targetLane));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
|
|
@ -3205,12 +3220,7 @@ public static partial class Gen5SpirvTranslator
|
|||
targetLane,
|
||||
lane);
|
||||
safeTarget = BitwiseAnd(safeTarget, UInt(31));
|
||||
var shuffled = _module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformShuffle,
|
||||
_uintType,
|
||||
UInt(3),
|
||||
value,
|
||||
safeTarget);
|
||||
var shuffled = ShuffleLane(value, safeTarget);
|
||||
|
||||
var sourceAvailable = inRange;
|
||||
if (!control.FetchInactive)
|
||||
|
|
@ -3221,12 +3231,7 @@ public static partial class Gen5SpirvTranslator
|
|||
Load(_boolType, _exec),
|
||||
UInt(1),
|
||||
UInt(0));
|
||||
var shuffledActive = _module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformShuffle,
|
||||
_uintType,
|
||||
UInt(3),
|
||||
activeWord,
|
||||
safeTarget);
|
||||
var shuffledActive = ShuffleLane(activeWord, safeTarget);
|
||||
sourceAvailable = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
|
|
@ -4168,12 +4173,7 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
var targetLane = IAdd(rowBase, selector);
|
||||
targetLane = BitwiseAnd(targetLane, UInt(31));
|
||||
var shuffled = _module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformShuffle,
|
||||
_uintType,
|
||||
UInt(3),
|
||||
value,
|
||||
targetLane);
|
||||
var shuffled = ShuffleLane(value, targetLane);
|
||||
var fetchInactive = (control.OperandSelect & 1) != 0;
|
||||
if (fetchInactive)
|
||||
{
|
||||
|
|
@ -4187,12 +4187,7 @@ public static partial class Gen5SpirvTranslator
|
|||
UInt(1),
|
||||
UInt(0));
|
||||
var sourceActive = IsNotZero(
|
||||
_module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformShuffle,
|
||||
_uintType,
|
||||
UInt(3),
|
||||
activeWord,
|
||||
targetLane));
|
||||
ShuffleLane(activeWord, targetLane));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ public static partial class Gen5SpirvTranslator
|
|||
private uint _physicalUintPointer;
|
||||
private uint _deviceEntryScratch;
|
||||
private uint _deviceWordScratch;
|
||||
private uint _deviceBufferWordScratch;
|
||||
private uint _pushDataBlockPointer;
|
||||
private uint _wordRuntimeArray;
|
||||
private uint _addressRuntimeArray;
|
||||
|
|
@ -213,6 +214,12 @@ public static partial class Gen5SpirvTranslator
|
|||
_module.AddName(_deviceWordScratch, "deviceAddressWord");
|
||||
_interfaces.Add(_deviceEntryScratch);
|
||||
_interfaces.Add(_deviceWordScratch);
|
||||
if (request.Memory.Entries.Any(static memory => memory.DeviceDescriptor))
|
||||
{
|
||||
_deviceBufferWordScratch = _module.AddGlobalVariable(_privateUintPointer, SpirvStorageClass.Private, UInt(0));
|
||||
_module.AddName(_deviceBufferWordScratch, "deviceBufferWord");
|
||||
_interfaces.Add(_deviceBufferWordScratch);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var memoryIndex in request.IndirectKeyMemoryIndices)
|
||||
|
|
@ -654,6 +661,29 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
// ---- scalar memory ----
|
||||
|
||||
// One dword of a scalar buffer load through the V# in s[descriptor:descriptor+3]:
|
||||
// base in dwords 0-1 (48 bits), stride in dword 1 [29:16], records in dword 2.
|
||||
// A load past the end of the buffer returns zero, as on hardware.
|
||||
private uint LoadDeviceDescriptorBufferWord(uint descriptor, uint byteOffset)
|
||||
{
|
||||
var word1 = LoadS(descriptor + 1);
|
||||
var baseAddress = Pair64(LoadS(descriptor), BitwiseAnd(word1, UInt(0xFFFF)));
|
||||
var stride = BitwiseAnd(ShiftRightLogical(word1, UInt(16)), UInt(0x3FFF));
|
||||
var records = Widen(LoadS(descriptor + 2));
|
||||
var size = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_ulongType,
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, stride, UInt(0)),
|
||||
records,
|
||||
_module.AddInstruction(SpirvOp.IMul, _ulongType, records, Widen(stride)));
|
||||
var aligned = Widen(BitwiseAnd(byteOffset, UInt(~3u)));
|
||||
var inRange = ULessThan64(IAdd64(aligned, ULong(sizeof(uint) - 1)), size);
|
||||
Store(_deviceBufferWordScratch, UInt(0));
|
||||
EmitConditional(inRange, () =>
|
||||
Store(_deviceBufferWordScratch, LoadDeviceDword(And64(IAdd64(baseAddress, aligned), ULong(DeviceAddressMask & ~3ul)))));
|
||||
return Load(_uintType, _deviceBufferWordScratch);
|
||||
}
|
||||
|
||||
private bool TryEmitLayoutScalarMemory(Gen5ShaderInstruction instruction, Gen5ScalarMemoryControl control, out string error)
|
||||
{
|
||||
_deviceAddressInstructionPc = instruction.Pc;
|
||||
|
|
@ -690,6 +720,18 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
value = LoadFlattenedWord(UInt(slot));
|
||||
}
|
||||
else if (entry.Kind == MemoryResourceKind.ScalarBuffer && entry.DeviceDescriptor)
|
||||
{
|
||||
if (instruction.Sources.Count == 0 || instruction.Sources[0].Kind != Gen5OperandKind.ScalarRegister)
|
||||
{
|
||||
error = "invalid scalar-buffer descriptor";
|
||||
return false;
|
||||
}
|
||||
|
||||
value = LoadDeviceDescriptorBufferWord(
|
||||
instruction.Sources[0].Value,
|
||||
IAdd(dynamicOffset, UInt(unchecked((uint)control.ImmediateOffsetBytes + (uint)component * sizeof(uint)))));
|
||||
}
|
||||
else if (entry.Kind == MemoryResourceKind.ScalarBuffer)
|
||||
{
|
||||
if (entry.Resource == MemoryAccessInfo.NoResource)
|
||||
|
|
@ -1203,7 +1245,8 @@ public static partial class Gen5SpirvTranslator
|
|||
imageClass.Multisampled,
|
||||
imageClass.Dimension,
|
||||
imageInfo.ConversionFormat,
|
||||
imageInfo.ShaderSwizzle);
|
||||
imageInfo.ShaderSwizzle,
|
||||
imageInfo.EmulatedCompareFunction);
|
||||
dstSelect = imageInfo.ShaderSwizzle;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,8 @@ public static partial class Gen5SpirvTranslator
|
|||
bool Multisampled,
|
||||
SpirvImageDim Dimension,
|
||||
uint ConversionFormat,
|
||||
uint ShaderSwizzle);
|
||||
uint ShaderSwizzle,
|
||||
int EmulatedCompareFunction = -1);
|
||||
|
||||
private readonly record struct SpirvVertexInput(
|
||||
uint Variable,
|
||||
|
|
@ -1418,6 +1419,8 @@ public static partial class Gen5SpirvTranslator
|
|||
"SWaitcnt" or
|
||||
"SInstPrefetch" or
|
||||
"STtraceData" or
|
||||
// Wave scheduling priority hint; no effect on results.
|
||||
"SSetprio" or
|
||||
// NGG shaders bracket their exports with s_sendmsg
|
||||
// (GS_ALLOC_REQ/DEALLOC) to reserve hardware export space;
|
||||
// exports are translated directly, so the message is moot.
|
||||
|
|
@ -1456,6 +1459,12 @@ public static partial class Gen5SpirvTranslator
|
|||
return TryEmitImage(instruction, image, out error);
|
||||
}
|
||||
|
||||
if (instruction.Control is Gen5RayIntersectControl rayIntersect)
|
||||
{
|
||||
EmitRayIntersectMiss(rayIntersect);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Control is Gen5GlobalMemoryControl globalMemory)
|
||||
{
|
||||
return TryEmitGlobalMemory(instruction, globalMemory, out error);
|
||||
|
|
@ -1838,12 +1847,7 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
else
|
||||
{
|
||||
broadcast = _module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformShuffle,
|
||||
_uintType,
|
||||
UInt(3),
|
||||
firstValue,
|
||||
firstLane);
|
||||
broadcast = ShuffleLane(firstValue, firstLane);
|
||||
}
|
||||
|
||||
var validResult = _module.AddInstruction(
|
||||
|
|
@ -3479,6 +3483,27 @@ public static partial class Gen5SpirvTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
// BVH nodes are not traversed yet: every ray misses the node it is tested against.
|
||||
// The node type sits in the low three bits of the node pointer. A box node (4-7)
|
||||
// returns four invalid child pointers, a triangle node (0-3) returns t = +inf over
|
||||
// a denominator of 1, so the guest's traversal loop ends with no hit.
|
||||
private void EmitRayIntersectMiss(Gen5RayIntersectControl ray)
|
||||
{
|
||||
var nodeType = BitwiseAnd(LoadV(ray.GetAddressRegister(0)), UInt(7));
|
||||
var isTriangle = _module.AddInstruction(SpirvOp.ULessThan, _boolType, nodeType, UInt(4));
|
||||
uint[] triangleMiss = [0x7F800000u, 0x3F800000u, 0u, 0u];
|
||||
for (var component = 0u; component < Gen5RayIntersectControl.ResultDwords; component++)
|
||||
{
|
||||
var value = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
isTriangle,
|
||||
UInt(triangleMiss[component]),
|
||||
UInt(0xFFFFFFFFu));
|
||||
StoreV(ray.VectorData + component, value);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryEmitImage(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5ImageControl image,
|
||||
|
|
@ -3976,7 +4001,27 @@ public static partial class Gen5SpirvTranslator
|
|||
|
||||
}
|
||||
|
||||
if (hasCompare)
|
||||
if (hasCompare && resource.EmulatedCompareFunction >= 0)
|
||||
{
|
||||
// A color format cannot back a Vulkan depth-compare view; compare
|
||||
// the sampled first channel like RDNA does for such formats.
|
||||
var texel = _module.AddInstruction(
|
||||
explicitLod ? SpirvOp.ImageSampleExplicitLod : SpirvOp.ImageSampleImplicitLod,
|
||||
resource.VectorType,
|
||||
[.. operands]);
|
||||
var depth = EmulatedDepthCompare(
|
||||
reference,
|
||||
_module.AddInstruction(SpirvOp.CompositeExtract, _floatType, texel, 0u),
|
||||
resource.EmulatedCompareFunction);
|
||||
sampled = _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
resource.VectorType,
|
||||
depth,
|
||||
depth,
|
||||
depth,
|
||||
Float(1f));
|
||||
}
|
||||
else if (hasCompare)
|
||||
{
|
||||
// The sampler carries the compare; the depth result fills x, y, z.
|
||||
var drefOperands = new List<uint> { imageObject, coordinates, reference };
|
||||
|
|
@ -4083,13 +4128,15 @@ public static partial class Gen5SpirvTranslator
|
|||
imageObject,
|
||||
coordinates,
|
||||
};
|
||||
if (hasCompare)
|
||||
var emulatedCompare = hasCompare && resource.EmulatedCompareFunction >= 0;
|
||||
if (emulatedCompare)
|
||||
{
|
||||
operands.Add(reference);
|
||||
// Gather the first channel and compare each texel in the shader.
|
||||
operands.Add(UInt(0));
|
||||
}
|
||||
else if (hasCompare)
|
||||
{
|
||||
operands.Add(UInt(0));
|
||||
operands.Add(reference);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -4124,10 +4171,24 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
sampled = _module.AddInstruction(
|
||||
hasCompare ? SpirvOp.ImageDrefGather : SpirvOp.ImageGather,
|
||||
hasCompare && !emulatedCompare ? SpirvOp.ImageDrefGather : SpirvOp.ImageGather,
|
||||
resource.VectorType,
|
||||
[.. operands]);
|
||||
if (!hasCompare)
|
||||
if (emulatedCompare)
|
||||
{
|
||||
var gathered = sampled;
|
||||
var compared = new uint[4];
|
||||
for (var texel = 0u; texel < 4; texel++)
|
||||
{
|
||||
compared[texel] = EmulatedDepthCompare(
|
||||
reference,
|
||||
_module.AddInstruction(SpirvOp.CompositeExtract, _floatType, gathered, texel),
|
||||
resource.EmulatedCompareFunction);
|
||||
}
|
||||
|
||||
sampled = _module.AddInstruction(SpirvOp.CompositeConstruct, resource.VectorType, compared);
|
||||
}
|
||||
else if (!hasCompare)
|
||||
{
|
||||
sampled = UnpackImageGather(resource, image.Dmask, sampled);
|
||||
}
|
||||
|
|
@ -6175,6 +6236,36 @@ public static partial class Gen5SpirvTranslator
|
|||
return UInt(0);
|
||||
}
|
||||
|
||||
// Reads value from another lane of the host subgroup. Without subgroup
|
||||
// support the invocation is a one-lane wave, so the only lane is itself;
|
||||
// emitting the shuffle there would need a capability the module lacks.
|
||||
// 1.0 when "reference <function> texel" holds, else 0.0. The function is the
|
||||
// guest sampler's depth compare field, which uses VkCompareOp's order.
|
||||
private uint EmulatedDepthCompare(uint reference, uint texel, int function)
|
||||
{
|
||||
SpirvOp op;
|
||||
switch (function)
|
||||
{
|
||||
case 0: return Float(0f);
|
||||
case 7: return Float(1f);
|
||||
case 1: op = SpirvOp.FOrdLessThan; break;
|
||||
case 2: op = SpirvOp.FOrdEqual; break;
|
||||
case 3: op = SpirvOp.FOrdLessThanEqual; break;
|
||||
case 4: op = SpirvOp.FOrdGreaterThan; break;
|
||||
case 5: op = SpirvOp.FUnordNotEqual; break;
|
||||
case 6: op = SpirvOp.FOrdGreaterThanEqual; break;
|
||||
default: throw new InvalidOperationException($"invalid depth compare function {function}");
|
||||
}
|
||||
|
||||
var passed = _module.AddInstruction(op, _boolType, reference, texel);
|
||||
return _module.AddInstruction(SpirvOp.Select, _floatType, passed, Float(1f), Float(0f));
|
||||
}
|
||||
|
||||
private uint ShuffleLane(uint value, uint lane) =>
|
||||
_subgroupInvocationIdInput == 0
|
||||
? value
|
||||
: _module.AddInstruction(SpirvOp.GroupNonUniformShuffle, _uintType, UInt(3), value, lane);
|
||||
|
||||
private uint CurrentLaneBit()
|
||||
{
|
||||
if (_subgroupInvocationIdInput == 0)
|
||||
|
|
@ -6351,8 +6442,18 @@ public static partial class Gen5SpirvTranslator
|
|||
mask,
|
||||
CurrentLaneBit()));
|
||||
|
||||
private void StoreWaveMask(uint register, uint condition) =>
|
||||
// In wave32 a lane mask fills its register alone and the next one keeps its value.
|
||||
// That includes VCC and EXEC: compilers use VCC_HI (s107) as an ordinary SGPR.
|
||||
private void StoreWaveMask(uint register, uint condition)
|
||||
{
|
||||
if (_waveLaneCount == 32)
|
||||
{
|
||||
StoreS(register, Narrow(BooleanToWaveMask(condition)));
|
||||
return;
|
||||
}
|
||||
|
||||
StoreS64(register, BooleanToWaveMask(condition));
|
||||
}
|
||||
|
||||
private void EmitExecConditional(Action emit)
|
||||
{
|
||||
|
|
@ -6400,7 +6501,7 @@ public static partial class Gen5SpirvTranslator
|
|||
_request.Program.Instructions.Any(instruction =>
|
||||
instruction.Control is Gen5DppControl or Gen5Dpp8Control ||
|
||||
instruction.Opcode is "VPermlane16B32" or "VPermlanex16B32" or "VReadlaneB32" or
|
||||
"DsAppend" or "DsConsume" or "DsSwizzleB32");
|
||||
"DsAppend" or "DsConsume" or "DsSwizzleB32" or "DsBpermuteB32");
|
||||
|
||||
private bool UsesSubgroupBroadcast() =>
|
||||
_request.Program.Instructions.Any(instruction =>
|
||||
|
|
|
|||
|
|
@ -199,6 +199,21 @@ public sealed record Gen5ImageControl(
|
|||
: VectorAddress + (uint)component;
|
||||
}
|
||||
|
||||
public sealed record Gen5RayIntersectControl(
|
||||
uint VectorAddress,
|
||||
IReadOnlyList<uint> AddressRegisters,
|
||||
uint VectorData,
|
||||
uint ScalarResource,
|
||||
bool A16) : Gen5InstructionControl
|
||||
{
|
||||
public const uint ResultDwords = 4;
|
||||
|
||||
public uint GetAddressRegister(int component) =>
|
||||
component < AddressRegisters.Count
|
||||
? AddressRegisters[component]
|
||||
: VectorAddress + (uint)component;
|
||||
}
|
||||
|
||||
public sealed record Gen5GlobalMemoryControl(
|
||||
uint DwordCount,
|
||||
uint VectorAddress,
|
||||
|
|
|
|||
|
|
@ -738,6 +738,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x09 => "SCbranchExecnz",
|
||||
0x0A => "SBarrier",
|
||||
0x0C => "SWaitcnt",
|
||||
0x0F => "SSetprio",
|
||||
0x10 => "SSendmsg",
|
||||
0x12 => "STrap",
|
||||
0x16 => "STtraceData",
|
||||
|
|
@ -921,7 +922,13 @@ public static partial class Gen5ShaderTranslator
|
|||
var src0 = word & 0x1FF;
|
||||
sizeDwords = src0 is 0xE9 or 0xEA or 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||
error = string.Empty;
|
||||
name = opcode switch
|
||||
name = VopcOpcodeName(opcode);
|
||||
return FinishDecode(name, $"unknown-vopc op=0x{opcode:X2}", out error);
|
||||
}
|
||||
|
||||
// VOPC names, shared by the VOP3 encoding of the same compares (VOP3 opcodes 0x000-0x0FF).
|
||||
private static string VopcOpcodeName(uint opcode) =>
|
||||
opcode switch
|
||||
{
|
||||
0x00 => "VCmpFF32",
|
||||
0x01 => "VCmpLtF32",
|
||||
|
|
@ -985,6 +992,22 @@ public static partial class Gen5ShaderTranslator
|
|||
0x9C => "VCmpxGtI16",
|
||||
0x9D => "VCmpxNeI16",
|
||||
0x9E => "VCmpxGeI16",
|
||||
0xA0 => "VCmpFI64",
|
||||
0xA1 => "VCmpLtI64",
|
||||
0xA2 => "VCmpEqI64",
|
||||
0xA3 => "VCmpLeI64",
|
||||
0xA4 => "VCmpGtI64",
|
||||
0xA5 => "VCmpNeI64",
|
||||
0xA6 => "VCmpGeI64",
|
||||
0xA7 => "VCmpTI64",
|
||||
0xB0 => "VCmpxFI64",
|
||||
0xB1 => "VCmpxLtI64",
|
||||
0xB2 => "VCmpxEqI64",
|
||||
0xB3 => "VCmpxLeI64",
|
||||
0xB4 => "VCmpxGtI64",
|
||||
0xB5 => "VCmpxNeI64",
|
||||
0xB6 => "VCmpxGeI64",
|
||||
0xB7 => "VCmpxTI64",
|
||||
0xC0 => "VCmpFU32",
|
||||
0xC1 => "VCmpLtU32",
|
||||
0xC2 => "VCmpEqU32",
|
||||
|
|
@ -1017,7 +1040,14 @@ public static partial class Gen5ShaderTranslator
|
|||
0xDD => "VCmpxLgF16",
|
||||
0xDE => "VCmpxGeF16",
|
||||
0xDF => "VCmpxOF16",
|
||||
0xE0 => "VCmpFU64",
|
||||
0xE1 => "VCmpLtU64",
|
||||
0xE2 => "VCmpEqU64",
|
||||
0xE3 => "VCmpLeU64",
|
||||
0xE4 => "VCmpGtU64",
|
||||
0xE5 => "VCmpNeU64",
|
||||
0xE6 => "VCmpGeU64",
|
||||
0xE7 => "VCmpTU64",
|
||||
0xE8 => "VCmpUF16",
|
||||
0xE9 => "VCmpNgeF16",
|
||||
0xEA => "VCmpNlgF16",
|
||||
|
|
@ -1026,8 +1056,14 @@ public static partial class Gen5ShaderTranslator
|
|||
0xED => "VCmpNeqF16",
|
||||
0xEE => "VCmpNltF16",
|
||||
0xEF => "VCmpTruF16",
|
||||
0xF0 => "VCmpxFU64",
|
||||
0xF1 => "VCmpxLtU64",
|
||||
0xF2 => "VCmpxEqU64",
|
||||
0xF3 => "VCmpxLeU64",
|
||||
0xF4 => "VCmpxGtU64",
|
||||
0xF5 => "VCmpxNeU64",
|
||||
0xB5 => "VCmpxNeI64",
|
||||
0xF6 => "VCmpxGeU64",
|
||||
0xF7 => "VCmpxTU64",
|
||||
0xF8 => "VCmpxUF16",
|
||||
0xF9 => "VCmpxNgeF16",
|
||||
0xFA => "VCmpxNlgF16",
|
||||
|
|
@ -1039,9 +1075,6 @@ public static partial class Gen5ShaderTranslator
|
|||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return FinishDecode(name, $"unknown-vopc op=0x{opcode:X2}", out error);
|
||||
}
|
||||
|
||||
private static bool DecodeVop3(
|
||||
uint word,
|
||||
uint extra,
|
||||
|
|
@ -1056,10 +1089,18 @@ public static partial class Gen5ShaderTranslator
|
|||
var src2 = (extra >> 18) & 0x1FF;
|
||||
sizeDwords = src0 == 0xFF || src1 == 0xFF || src2 == 0xFF ? 3u : 2u;
|
||||
error = string.Empty;
|
||||
if (!isVop3B && opcode < 0x100 && VopcOpcodeName(opcode) is { Length: > 0 } compare)
|
||||
{
|
||||
name = compare;
|
||||
return true;
|
||||
}
|
||||
|
||||
name = isVop3B
|
||||
? opcode switch
|
||||
{
|
||||
0x128 => "VAddCoCiU32",
|
||||
0x129 => "VSubCoCiU32",
|
||||
0x12A => "VSubrevCoCiU32",
|
||||
0x30F => "VAddCoU32",
|
||||
0x310 => "VSubCoU32",
|
||||
0x319 => "VSubrevCoU32",
|
||||
|
|
@ -1087,6 +1128,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x147 => "VCubemaF32",
|
||||
0x14A => "VBfiB32",
|
||||
0x14E => "VAlignbitB32",
|
||||
0x14F => "VAlignbyteB32",
|
||||
0x14B => "VFmaF32",
|
||||
0x151 => "VMin3F32",
|
||||
0x152 => "VMin3I32",
|
||||
|
|
@ -1140,7 +1182,7 @@ public static partial class Gen5ShaderTranslator
|
|||
}
|
||||
|
||||
private static bool IsVop3BOpcode(uint opcode) =>
|
||||
opcode is 0x128 or 0x16D or 0x16E or 0x176 or 0x177 or 0x30F or 0x310 or 0x319;
|
||||
opcode is 0x128 or 0x129 or 0x12A or 0x16D or 0x16E or 0x176 or 0x177 or 0x30F or 0x310 or 0x319;
|
||||
|
||||
private static bool DecodeRaw2(
|
||||
uint word,
|
||||
|
|
@ -1485,6 +1527,14 @@ public static partial class Gen5ShaderTranslator
|
|||
var opcode = ((word >> 18) & 0x7F) | ((word & 1) << 7);
|
||||
sizeDwords = 2 + ((word >> 1) & 0x3);
|
||||
error = string.Empty;
|
||||
// Bit 0 (OPM) holds opcode bit 7.
|
||||
var fullOpcode = opcode | ((word & 1) << 7);
|
||||
if (fullOpcode is 0xE6 or 0xE7)
|
||||
{
|
||||
name = fullOpcode == 0xE6 ? "ImageBvhIntersectRay" : "ImageBvh64IntersectRay";
|
||||
return true;
|
||||
}
|
||||
|
||||
name = opcode switch
|
||||
{
|
||||
0x00 => "ImageLoad",
|
||||
|
|
@ -2114,6 +2164,23 @@ public static partial class Gen5ShaderTranslator
|
|||
destinations = [Gen5Operand.Scalar(word & 0xFF)];
|
||||
}
|
||||
var isVop3B = IsVop3BOpcode((word >> 16) & 0x3FF);
|
||||
if (opcode.StartsWith("VCmp", StringComparison.Ordinal))
|
||||
{
|
||||
// VOP3 compares take two sources and write their mask to the SGPR pair in
|
||||
// the vdst byte; GFX10 VCMPX writes EXEC only.
|
||||
var isExecCompare = opcode.StartsWith("VCmpx", StringComparison.Ordinal);
|
||||
sources = [sources[0], sources[1]];
|
||||
destinations = isExecCompare ? [] : [Gen5Operand.Scalar(word & 0xFF)];
|
||||
control = new Gen5Vop3Control(
|
||||
(word >> 8) & 0x7,
|
||||
(extra >> 29) & 0x7,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
isExecCompare ? null : word & 0xFF);
|
||||
break;
|
||||
}
|
||||
|
||||
control = new Gen5Vop3Control(
|
||||
isVop3B ? 0 : (word >> 8) & 0x7,
|
||||
(extra >> 29) & 0x7,
|
||||
|
|
@ -2152,10 +2219,11 @@ public static partial class Gen5ShaderTranslator
|
|||
var vectorData0 = (extra >> 8) & 0xFF;
|
||||
var vectorData1 = (extra >> 16) & 0xFF;
|
||||
var vectorDestination = (extra >> 24) & 0xFF;
|
||||
// GFX10 DS: offset0 [7:0], offset1 [15:8], bit 16 reserved, GDS [17], op [25:18].
|
||||
control = new Gen5DataShareControl(
|
||||
word & 0xFF,
|
||||
(word >> 8) & 0xFF,
|
||||
((word >> 16) & 1) != 0);
|
||||
((word >> 17) & 1) != 0);
|
||||
sources = opcode switch
|
||||
{
|
||||
"DsAppend" or "DsConsume" or "DsReadAddtidB32" => [Gen5Operand.Scalar(124)],
|
||||
|
|
@ -2480,6 +2548,29 @@ public static partial class Gen5ShaderTranslator
|
|||
}
|
||||
}
|
||||
|
||||
if (opcode.StartsWith("ImageBvh", StringComparison.Ordinal))
|
||||
{
|
||||
var a16 = ((extra >> 30) & 1) != 0;
|
||||
var rayControl = new Gen5RayIntersectControl(vectorAddress, addressRegisters, vectorData, scalarResource, a16);
|
||||
// Node pointer (2 dwords for BVH64), extent, origin, then direction and
|
||||
// inverse direction, which A16 packs as halves into three dwords.
|
||||
var addressCount = (opcode == "ImageBvh64IntersectRay" ? 2 : 1) + 1 + 3 + (a16 ? 3 : 6);
|
||||
var raySources = new List<Gen5Operand>(addressCount + 1);
|
||||
for (var component = 0; component < addressCount; component++)
|
||||
{
|
||||
raySources.Add(Gen5Operand.Vector(rayControl.GetAddressRegister(component)));
|
||||
}
|
||||
|
||||
raySources.Add(Gen5Operand.Scalar(scalarResource));
|
||||
sources = raySources;
|
||||
destinations = Enumerable
|
||||
.Range((int)vectorData, (int)Gen5RayIntersectControl.ResultDwords)
|
||||
.Select(index => Gen5Operand.Vector((uint)index))
|
||||
.ToArray();
|
||||
control = rayControl;
|
||||
break;
|
||||
}
|
||||
|
||||
var imageSources = new List<Gen5Operand>(addressRegisters.Count + 2);
|
||||
foreach (var addressRegister in addressRegisters)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,9 +50,35 @@ public static class ImageDescriptorBinding
|
|||
private const uint StorageCubeFloatBinding = 40;
|
||||
private const uint StorageCubeUintBinding = 41;
|
||||
private const uint AtomicCubeUintBinding = 42;
|
||||
// Depth-reference sampling gets its own arrays: validation checks every element
|
||||
// of an array sampled with Dref, so sharing one with color images reports them.
|
||||
private const uint SampledCompare2DBinding = 43;
|
||||
private const uint SampledCompare2DArrayBinding = 44;
|
||||
private const uint SampledCompareCubeBinding = 45;
|
||||
|
||||
public static DescriptorBindingKind? ForImage(ImageResource image)
|
||||
{
|
||||
if (image.DepthCompare && image.ResourceClass == ImageResourceClass.Sampled && !image.Atomic &&
|
||||
image.NumericClass == ImageNumericClass.Float)
|
||||
{
|
||||
if (image.Cube)
|
||||
{
|
||||
return image.Dimension is ImageDimension.Dim2D or ImageDimension.Dim2DArray
|
||||
? (DescriptorBindingKind)SampledCompareCubeBinding
|
||||
: null;
|
||||
}
|
||||
|
||||
if (image.Dimension == ImageDimension.Dim2D)
|
||||
{
|
||||
return (DescriptorBindingKind)SampledCompare2DBinding;
|
||||
}
|
||||
|
||||
if (image.Dimension == ImageDimension.Dim2DArray)
|
||||
{
|
||||
return (DescriptorBindingKind)SampledCompare2DArrayBinding;
|
||||
}
|
||||
}
|
||||
|
||||
if (image.Cube)
|
||||
{
|
||||
if (image.Dimension is not (ImageDimension.Dim2D or ImageDimension.Dim2DArray))
|
||||
|
|
@ -196,7 +222,8 @@ public static class ImageDescriptorBinding
|
|||
public static ImageResourceClass ResourceClass(DescriptorBindingKind kind)
|
||||
{
|
||||
var value = (uint)kind;
|
||||
if (value is >= SampledCubeFloatBinding and <= SampledCubeSintBinding)
|
||||
if (value is >= SampledCubeFloatBinding and <= SampledCubeSintBinding or
|
||||
>= SampledCompare2DBinding and <= SampledCompareCubeBinding)
|
||||
{
|
||||
return ImageResourceClass.Sampled;
|
||||
}
|
||||
|
|
@ -222,7 +249,7 @@ public static class ImageDescriptorBinding
|
|||
public static uint ArrayIndex(DescriptorBindingKind kind) => (uint)kind - BindingLayout.FirstImageBinding;
|
||||
|
||||
public static bool IsCube(DescriptorBindingKind kind) =>
|
||||
(uint)kind is >= SampledCubeFloatBinding and <= AtomicCubeUintBinding;
|
||||
(uint)kind is >= SampledCubeFloatBinding and <= AtomicCubeUintBinding or SampledCompareCubeBinding;
|
||||
|
||||
private static readonly ImageDimension[] SampledDimensions =
|
||||
[
|
||||
|
|
@ -239,6 +266,15 @@ public static class ImageDescriptorBinding
|
|||
public static (ImageResourceClass ResourceClass, ImageNumericClass NumericClass, ImageDimension Dimension, bool Atomic) Describe(DescriptorBindingKind kind)
|
||||
{
|
||||
var index = (uint)kind;
|
||||
switch (index)
|
||||
{
|
||||
case SampledCompare2DBinding:
|
||||
return (ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2D, false);
|
||||
case SampledCompare2DArrayBinding:
|
||||
case SampledCompareCubeBinding:
|
||||
return (ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2DArray, false);
|
||||
}
|
||||
|
||||
if (index is >= SampledCubeFloatBinding and <= SampledCubeSintBinding)
|
||||
{
|
||||
var numericClass = index switch
|
||||
|
|
@ -290,7 +326,7 @@ public sealed class BindingLayout : IEquatable<BindingLayout>
|
|||
{
|
||||
public const uint FirstImageBinding = 1;
|
||||
public const uint FirstStorageImageBinding = 22;
|
||||
public const uint ImageBindingCount = 42;
|
||||
public const uint ImageBindingCount = 45;
|
||||
public const uint NoShaderBase = uint.MaxValue;
|
||||
public const uint ShaderBaseDwordCount = 2;
|
||||
private const int ScalarRegisterCount = 256;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,14 @@ namespace SharpEmu.ShaderCompiler.Resources;
|
|||
public static class GuestImageFormat
|
||||
{
|
||||
public const uint Invalid = 0;
|
||||
public const uint Format16Unorm = 7;
|
||||
public const uint Format32Uint = 20;
|
||||
public const uint Format32Sint = 21;
|
||||
public const uint Format32Float = 22;
|
||||
|
||||
// Only these sample as a Vulkan depth format; comparison sampling of any
|
||||
// other format is evaluated in the shader.
|
||||
public static bool HasDepthEquivalent(uint format) => format is Format16Unorm or Format32Float;
|
||||
public const uint Format11x2x10Uint = 34;
|
||||
public const uint MaxFormat = 182;
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ public sealed class MemoryAccessInfo
|
|||
public bool IndexEnabled { get; init; }
|
||||
public bool OffsetEnabled { get; init; }
|
||||
public bool PlanningOnly { get; set; }
|
||||
// A scalar buffer load whose descriptor only exists on the device: the shader reads
|
||||
// through the descriptor in its registers instead of a host-bound buffer.
|
||||
public bool DeviceDescriptor { get; set; }
|
||||
|
||||
public bool IsAddressKind =>
|
||||
Kind is MemoryResourceKind.ScalarAddress or MemoryResourceKind.Flat or
|
||||
|
|
@ -106,7 +109,8 @@ public sealed class MemoryAccessInfo
|
|||
DataSigned == other.DataSigned && Typed == other.Typed && Formatted == other.Formatted &&
|
||||
ImageHasMip == other.ImageHasMip && ImageR128 == other.ImageR128 && Glc == other.Glc && Slc == other.Slc &&
|
||||
IndexEnabled == other.IndexEnabled && OffsetEnabled == other.OffsetEnabled &&
|
||||
Resource == other.Resource && Sampler == other.Sampler && PlanningOnly == other.PlanningOnly;
|
||||
Resource == other.Resource && Sampler == other.Sampler && PlanningOnly == other.PlanningOnly &&
|
||||
DeviceDescriptor == other.DeviceDescriptor;
|
||||
}
|
||||
|
||||
// Every memory access of a program, indexed by program counter and component.
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@ public static class ResourceMaterializer
|
|||
Enumerable.Range(0, (int)probeCount).Select(index => residue + (ulong)index * step);
|
||||
foreach (var offset in offsets)
|
||||
{
|
||||
if (!ReadScalarBufferWord(material.Dwords, (uint)offset, 0, inputs, out var key))
|
||||
if (!ReadScalarBufferWord(material.Dwords, (uint)offset, indirect.MaterialImmediate, inputs, out var key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -622,6 +622,21 @@ public static class ResourceMaterializer
|
|||
private static bool RequiresPointSampler(ImageNumericClass numericClass, uint conversionFormat) =>
|
||||
numericClass == ImageNumericClass.Sint || conversionFormat != GuestImageFormat.Invalid;
|
||||
|
||||
// The depth compare function (sampler word 0, bits 12..14) of the sampler paired
|
||||
// with an image; an image sampled with comparison always has one.
|
||||
private static int SamplerCompareFunction(ShaderResourceInfo info, MaterializedSnapshot snapshot, uint image)
|
||||
{
|
||||
foreach (var pair in info.SampledPairs)
|
||||
{
|
||||
if (pair.Image == image && pair.Sampler < snapshot.Samplers.Length && snapshot.Samplers[pair.Sampler].Length != 0)
|
||||
{
|
||||
return (int)((snapshot.Samplers[pair.Sampler][0] >> 12) & 0x7);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static uint StorageMipCount(ImageResource image, ReadOnlySpan<uint> descriptor)
|
||||
{
|
||||
if (image.MipMode != ImageMipMode.DynamicStorage || NullImageDescriptor(descriptor))
|
||||
|
|
@ -829,6 +844,9 @@ public static class ResourceMaterializer
|
|||
ConversionFormat = conversionFormat,
|
||||
ShaderSwizzle = shaderSwizzle,
|
||||
NumericClass = numericClass,
|
||||
EmulatedCompareFunction = !storage && baseImage.DepthCompare && !GuestImageFormat.HasDepthEquivalent(format)
|
||||
? SamplerCompareFunction(info, snapshot, baseIndex)
|
||||
: -1,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1046,6 +1064,13 @@ public static class ResourceMaterializer
|
|||
image.IndirectMappingOffset = specialized.IndirectMappingOffset;
|
||||
image.IndirectSearchIterations = specialized.IndirectSearchIterations;
|
||||
image.Cube = specialized.Cube;
|
||||
image.EmulatedCompareFunction = specialized.EmulatedCompareFunction;
|
||||
if (specialized.EmulatedCompareFunction >= 0)
|
||||
{
|
||||
// The shader compares instead; the host keeps a color view and a plain sampler.
|
||||
image.DepthCompare = false;
|
||||
}
|
||||
|
||||
image.IndirectResources = [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ public readonly record struct ImageSpecialization(
|
|||
uint IndirectRoot,
|
||||
uint IndirectMappingOffset,
|
||||
uint IndirectSearchIterations,
|
||||
bool Cube);
|
||||
bool Cube,
|
||||
int EmulatedCompareFunction = -1);
|
||||
|
||||
// The module-affecting resource state of one draw. Addresses and descriptor payloads
|
||||
// stay in the snapshot, so they never create a permutation.
|
||||
|
|
|
|||
|
|
@ -141,6 +141,13 @@ public sealed class ResourceTableReadPlanner
|
|||
return;
|
||||
}
|
||||
|
||||
// A read whose address depends on values the host cannot know (a loop over data the
|
||||
// shader loaded itself) cannot be flattened; the shader performs it on the device.
|
||||
if (!new RuntimeValueValidator(_graph, _graph.UserDataBase, _graph.UserDataCount, 0).Validate(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var slot = 0; slot < _reads.Count; slot++)
|
||||
{
|
||||
if (_graph.Equivalent(value, _reads[slot].Value))
|
||||
|
|
|
|||
|
|
@ -261,6 +261,10 @@ public sealed partial class ResourceTracker
|
|||
return (uint)(_sources.Count - 1);
|
||||
}
|
||||
|
||||
private bool IsHostBufferHandle(ScalarValue? handle) =>
|
||||
handle is { Kind: ScalarValueKind.BufferHandle, Operands.Length: 4 } &&
|
||||
handle.Operands.All(dword => dword.Type == ScalarValueType.U32 && _plan.ValidateRuntimeValue(dword));
|
||||
|
||||
private uint GetHandleSource(ScalarValue? handle, ScalarValueKind expected, uint width, uint pc, bool sampler = false, bool sampleAdjust = false)
|
||||
{
|
||||
if (handle is null || handle.Kind != expected)
|
||||
|
|
@ -470,6 +474,15 @@ public sealed partial class ResourceTracker
|
|||
|
||||
if (isBuffer)
|
||||
{
|
||||
// A scalar buffer descriptor built from data the shader loads itself cannot be
|
||||
// bound by the host; the device reads through the descriptor in registers.
|
||||
if (memory.Kind == MemoryResourceKind.ScalarBuffer && !IsHostBufferHandle(access.Handle))
|
||||
{
|
||||
memory.DeviceDescriptor = true;
|
||||
_info.UsesDeviceAddresses = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var source = GetHandleSource(access.Handle, ScalarValueKind.BufferHandle, 4, memory.Pc);
|
||||
var resource = AddBuffer(source, memory, memory.Pc);
|
||||
if (resource == DescriptorConstants.NoIndex)
|
||||
|
|
@ -759,7 +772,7 @@ public sealed partial class ResourceTracker
|
|||
|
||||
var materialRead = heapOffset.Operands[0];
|
||||
var materialMemory = ScalarReadMemory(materialRead, out var materialMemoryIndex);
|
||||
if (materialMemory is null || materialMemory.Offset != 0 || !MemoryIndexBelongsTo(materialMemoryIndex, materialRead))
|
||||
if (materialMemory is null || !MemoryIndexBelongsTo(materialMemoryIndex, materialRead))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -798,6 +811,7 @@ public sealed partial class ResourceTracker
|
|||
IndirectImage = new IndirectImageSelector(materialSourceIndex, heapSourceIndex, selectorStride, selectorOffset, 0)
|
||||
{
|
||||
SelectorValues = IndirectSelectorValues.Create(_plan, selector),
|
||||
MaterialImmediate = materialMemory.Offset,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -251,6 +251,15 @@ public sealed class RuntimeValueEvaluator
|
|||
|
||||
if (_inputs.ReadMemory is null || !_inputs.ReadMemory(address, out var word))
|
||||
{
|
||||
// Every read is evaluated up front, including ones in branches the shader skips
|
||||
// when a pointer is null. A load through a null base cannot execute on hardware,
|
||||
// so its value is never used.
|
||||
if ((baseAddress & ~3ul) == 0)
|
||||
{
|
||||
result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -703,6 +703,36 @@ public sealed partial class ScalarValueGraph
|
|||
|
||||
// Writes a dword pair; a pair that is a lane mask also keeps its mask value, and a
|
||||
// write to EXEC or VCC updates the active mask.
|
||||
// A lane mask a vector instruction writes to an SGPR: in wave32 it fills that register
|
||||
// alone, and the next one keeps its value. It is often live: a pointer's high half, or
|
||||
// VCC_HI, which compilers use as an ordinary SGPR in wave32.
|
||||
private void WriteVectorMask(RegisterState state, uint destinationRegister, ScalarValue low, ScalarValue mask)
|
||||
{
|
||||
if (_graph.WaveSize != 32)
|
||||
{
|
||||
WriteMaskPair(state, destinationRegister, low, _graph.Constant(0u), mask);
|
||||
return;
|
||||
}
|
||||
|
||||
state.WriteScalar(destinationRegister, low);
|
||||
switch (destinationRegister)
|
||||
{
|
||||
case ExecLow:
|
||||
state.Exec = mask;
|
||||
break;
|
||||
case VccLow:
|
||||
state.Vcc = mask;
|
||||
break;
|
||||
default:
|
||||
if (!mask.IsUndefined)
|
||||
{
|
||||
state.ThreadBits[destinationRegister] = mask;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteMaskPair(RegisterState state, uint destinationRegister, ScalarValue low, ScalarValue high, ScalarValue mask)
|
||||
{
|
||||
state.WritePair(destinationRegister, low, high);
|
||||
|
|
@ -992,7 +1022,8 @@ public sealed partial class ScalarValueGraph
|
|||
}
|
||||
}
|
||||
|
||||
if (instruction.Encoding == Gen5ShaderEncoding.Vopc)
|
||||
if (instruction.Encoding == Gen5ShaderEncoding.Vopc ||
|
||||
(instruction.Encoding == Gen5ShaderEncoding.Vop3 && opcode.StartsWith("VCmp", StringComparison.Ordinal)))
|
||||
{
|
||||
ApplyVectorCompare(instruction, state);
|
||||
return;
|
||||
|
|
@ -1003,15 +1034,15 @@ public sealed partial class ScalarValueGraph
|
|||
Gen5SdwaControl or Gen5DppControl or Gen5Dpp8Control or Gen5Vop3pControl;
|
||||
var value = hasModifiers ? _graph.Undefined(ScalarValueType.U32) : VectorResult(instruction, state);
|
||||
if (instruction.Control is Gen5Vop3Control { ScalarDestination: { } carryDestination } && !hasModifiers &&
|
||||
opcode is "VAddCoU32" or "VSubCoU32" or "VSubrevCoU32" or "VAddCoCiU32" or "VMadU64U32")
|
||||
opcode is "VAddCoU32" or "VSubCoU32" or "VSubrevCoU32" or "VAddCoCiU32" or "VSubCoCiU32" or "VSubrevCoCiU32" or "VMadU64U32")
|
||||
{
|
||||
var carry = value.IsUndefined ? _graph.Undefined(ScalarValueType.Bool) : state.CarryOut;
|
||||
WriteMaskPair(state, carryDestination, _graph.Select(carry, _graph.Constant(1u), _graph.Constant(0u)), _graph.Constant(0u), carry);
|
||||
WriteVectorMask(state, carryDestination, _graph.Select(carry, _graph.Constant(1u), _graph.Constant(0u)), carry);
|
||||
}
|
||||
else if (opcode is "VAddcU32" or "VSubbU32" or "VSubbrevU32" or "VAddCoU32" or "VSubCoU32" or "VSubrevCoU32")
|
||||
{
|
||||
var carry = value.IsUndefined ? _graph.Undefined(ScalarValueType.Bool) : state.CarryOut;
|
||||
WriteMaskPair(state, VccLow, _graph.Select(carry, _graph.Constant(1u), _graph.Constant(0u)), _graph.Constant(0u), carry);
|
||||
WriteVectorMask(state, VccLow, _graph.Select(carry, _graph.Constant(1u), _graph.Constant(0u)), carry);
|
||||
}
|
||||
|
||||
foreach (var destination in instruction.Destinations)
|
||||
|
|
@ -1214,7 +1245,7 @@ public sealed partial class ScalarValueGraph
|
|||
var right = instruction.Sources.Count > 1 ? ReadVectorOperand(instruction.Sources[1], state) : _graph.Undefined(ScalarValueType.U32);
|
||||
var updatesExecutionMask = opcode.StartsWith("VCmpx", StringComparison.Ordinal);
|
||||
var suffix = opcode[(updatesExecutionMask ? "VCmpx".Length : "VCmp".Length)..];
|
||||
var result = instruction.Control is Gen5SdwaControl
|
||||
var result = instruction.Control is Gen5SdwaControl or Gen5Vop3Control { AbsoluteMask: not 0 } or Gen5Vop3Control { NegateMask: not 0 }
|
||||
? _graph.Undefined(ScalarValueType.Bool)
|
||||
: suffix switch
|
||||
{
|
||||
|
|
@ -1241,18 +1272,18 @@ public sealed partial class ScalarValueGraph
|
|||
// Execution-mask comparisons leave the vector condition registers unchanged.
|
||||
if (updatesExecutionMask)
|
||||
{
|
||||
WriteMaskPair(state, ExecLow, raw, _graph.Constant(0u), masked);
|
||||
WriteVectorMask(state, ExecLow, raw, masked);
|
||||
return;
|
||||
}
|
||||
|
||||
var scalarDestination = instruction.Destinations.FirstOrDefault(destination => destination.Kind == Gen5OperandKind.ScalarRegister);
|
||||
if (scalarDestination.Kind == Gen5OperandKind.ScalarRegister && instruction.Destinations.Count != 0)
|
||||
{
|
||||
WriteMaskPair(state, scalarDestination.Value, raw, _graph.Constant(0u), masked);
|
||||
WriteVectorMask(state, scalarDestination.Value, raw, masked);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteMaskPair(state, VccLow, raw, _graph.Constant(0u), masked);
|
||||
WriteVectorMask(state, VccLow, raw, masked);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ public sealed partial class ScalarValueGraph
|
|||
private readonly Dictionary<string, ScalarValue> _interned = [];
|
||||
private readonly List<ScalarValue> _values = [];
|
||||
|
||||
private ScalarValueGraph(Gen5ShaderProgram program, IrControlFlowGraph controlFlow, MemoryAccessTable memory, uint userDataBase, uint userDataCount)
|
||||
private ScalarValueGraph(Gen5ShaderProgram program, IrControlFlowGraph controlFlow, MemoryAccessTable memory, uint userDataBase, uint userDataCount,
|
||||
uint waveSize)
|
||||
{
|
||||
Program = program;
|
||||
WaveSize = waveSize;
|
||||
ControlFlow = controlFlow;
|
||||
Memory = memory;
|
||||
UserDataBase = userDataBase;
|
||||
|
|
@ -31,6 +33,8 @@ public sealed partial class ScalarValueGraph
|
|||
public MemoryAccessTable Memory { get; }
|
||||
public uint UserDataBase { get; }
|
||||
public uint UserDataCount { get; }
|
||||
// A wave32 lane mask fills one SGPR; wave64 fills an aligned pair.
|
||||
public uint WaveSize { get; }
|
||||
|
||||
// Index-aligned with Memory.Entries; null when the access has no descriptor value.
|
||||
public MemoryAccessBinding?[] Accesses { get; private set; } = [];
|
||||
|
|
@ -44,10 +48,11 @@ public sealed partial class ScalarValueGraph
|
|||
public ScalarValue? ResolveInvariantPhi(ScalarValue value) => ScalarValueEquivalence.ResolveInvariantPhi(Memory, value);
|
||||
|
||||
public static ScalarValueGraph Build(Gen5ShaderProgram program, uint userDataBase, uint userDataCount,
|
||||
IReadOnlySet<uint>? fixedFunctionVertexLoads = null)
|
||||
IReadOnlySet<uint>? fixedFunctionVertexLoads = null, uint waveSize = 64)
|
||||
{
|
||||
var controlFlow = IrControlFlowGraph.Build(program.Instructions, Gen5IrBranchResolver.Instance);
|
||||
var graph = new ScalarValueGraph(program, controlFlow, MemoryAccessTable.Build(program, fixedFunctionVertexLoads), userDataBase, userDataCount);
|
||||
var graph = new ScalarValueGraph(program, controlFlow, MemoryAccessTable.Build(program, fixedFunctionVertexLoads), userDataBase, userDataCount,
|
||||
waveSize);
|
||||
new Builder(graph).Run();
|
||||
return graph;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ public sealed class ImageResource
|
|||
public bool Written { get; set; }
|
||||
public bool Atomic { get; set; }
|
||||
public bool DepthCompare { get; set; }
|
||||
// Guest compare function (0..7) evaluated in the shader for a depth-compare
|
||||
// image whose format has no Vulkan depth equivalent; -1 when not emulated.
|
||||
public int EmulatedCompareFunction { get; set; } = -1;
|
||||
public bool Cube { get; set; }
|
||||
public bool R128 { get; set; }
|
||||
public uint IndirectRoot { get; set; } = DescriptorConstants.NoIndex;
|
||||
|
|
@ -189,6 +192,8 @@ public sealed record IndirectImageSelector(
|
|||
public uint TableOffset { get; init; }
|
||||
public uint DynamicOffsetBase { get; init; }
|
||||
public uint KeyBound { get; init; }
|
||||
// The key read's immediate offset. The hardware adds it after the 32-bit selector offset, without wrapping.
|
||||
public uint MaterialImmediate { get; init; }
|
||||
}
|
||||
|
||||
public sealed record DirectImageCandidate(uint Offset, uint Source);
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ public sealed class ShaderResourcePlan
|
|||
public MemoryAccessBinding?[] Accesses { get; private set; } = [];
|
||||
|
||||
public static ShaderResourcePlan Extract(Gen5ShaderProgram program, ShaderStage stage, ulong hash, uint userDataBase, uint userDataCount,
|
||||
IReadOnlySet<uint>? fixedFunctionVertexLoads = null, Action<ShaderResourcePlan>? beforeResourceTracking = null)
|
||||
IReadOnlySet<uint>? fixedFunctionVertexLoads = null, Action<ShaderResourcePlan>? beforeResourceTracking = null, uint waveSize = 64)
|
||||
{
|
||||
var graph = ScalarValueGraph.Build(program, userDataBase, userDataCount, fixedFunctionVertexLoads);
|
||||
var graph = ScalarValueGraph.Build(program, userDataBase, userDataCount, fixedFunctionVertexLoads, waveSize);
|
||||
var plan = new ShaderResourcePlan(graph, stage, hash);
|
||||
var reads = ResourceTableReadPlanner.Plan(graph, stage, hash);
|
||||
var memo = new Dictionary<ScalarValue, ScalarValue>();
|
||||
|
|
|
|||
|
|
@ -100,6 +100,58 @@ public sealed class Gen5ShaderAtomicDecodeTests
|
|||
Assert.Equal(new[] { Gen5Operand.Vector(2) }, instruction.Destinations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImageBvhIntersectRay_UsesOpcodeBitSevenFromOpm()
|
||||
{
|
||||
// IMAGE_BVH_INTERSECT_RAY v[12:15], v[0:10], s[4:7] (op 0xE6: word0[24:18]=0x66, OPM=1)
|
||||
var instruction = DecodeSingle(0xF1980F01, 0x00010C00);
|
||||
|
||||
Assert.Equal("ImageBvhIntersectRay", instruction.Opcode);
|
||||
var control = Assert.IsType<Gen5RayIntersectControl>(instruction.Control);
|
||||
Assert.Equal(12u, control.VectorData);
|
||||
Assert.Equal(4u, control.ScalarResource);
|
||||
Assert.Equal(
|
||||
Enumerable.Range(12, 4).Select(index => Gen5Operand.Vector((uint)index)),
|
||||
instruction.Destinations);
|
||||
Assert.Equal(
|
||||
Enumerable.Range(0, 11).Select(index => Gen5Operand.Vector((uint)index)).Append(Gen5Operand.Scalar(4)),
|
||||
instruction.Sources);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DsAppend_ReadsTheGdsBitAtBit17()
|
||||
{
|
||||
// DS_APPEND v0 offset:16 gds (Astro Bot's indirect-dispatch producer)
|
||||
var instruction = DecodeSingle(0xD8FA0010, 0x00000000);
|
||||
|
||||
Assert.Equal("DsAppend", instruction.Opcode);
|
||||
var control = Assert.IsType<Gen5DataShareControl>(instruction.Control);
|
||||
Assert.True(control.Gds);
|
||||
Assert.Equal(16u, control.SingleOffsetBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vop3CompareGtU64_WritesTheScalarPairInVdst()
|
||||
{
|
||||
// V_CMP_GT_U64 s[4:5], v[0:1], v[2:3] (VOP3 op 0x0E4)
|
||||
var instruction = DecodeSingle(0xD4E40004, 0x00020500);
|
||||
|
||||
Assert.Equal("VCmpGtU64", instruction.Opcode);
|
||||
var control = Assert.IsType<Gen5Vop3Control>(instruction.Control);
|
||||
Assert.Equal(4u, control.ScalarDestination);
|
||||
Assert.Equal(new[] { Gen5Operand.Scalar(4) }, instruction.Destinations);
|
||||
Assert.Equal(new[] { Gen5Operand.Vector(0), Gen5Operand.Vector(2) }, instruction.Sources);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SSetprio_Decodes()
|
||||
{
|
||||
// S_SETPRIO 3 (SOPP op 0x0F)
|
||||
var instruction = DecodeSingle(0xBF8F0003);
|
||||
|
||||
Assert.Equal("SSetprio", instruction.Opcode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DsAddU32_HasAddressAndDataSourcesButNoDestination()
|
||||
{
|
||||
|
|
@ -182,8 +234,8 @@ public sealed class Gen5ShaderAtomicDecodeTests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0xD8FA3412u, "DsAppend")]
|
||||
[InlineData(0xD8F63412u, "DsConsume")]
|
||||
[InlineData(0xD8F83412u, "DsAppend")]
|
||||
[InlineData(0xD8F43412u, "DsConsume")]
|
||||
public void DsWaveCounter_UsesM0AndReturnsOldValue(uint word, string opcode)
|
||||
{
|
||||
var instruction = DecodeSingle(word, 0x07000000);
|
||||
|
|
@ -254,6 +306,16 @@ public sealed class Gen5ShaderAtomicDecodeTests
|
|||
Assert.Equal(new[] { Gen5Operand.Vector(1) }, instruction.Destinations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VAlignbyteB32_DecodesVop3Opcode14F()
|
||||
{
|
||||
var instruction = DecodeSingle(0xD14F0001, 0x040E0502);
|
||||
|
||||
Assert.Equal("VAlignbyteB32", instruction.Opcode);
|
||||
Assert.Equal(new[] { Gen5Operand.Vector(2), Gen5Operand.Vector(2), Gen5Operand.Vector(3) }, instruction.Sources);
|
||||
Assert.Equal(new[] { Gen5Operand.Vector(1) }, instruction.Destinations);
|
||||
}
|
||||
|
||||
private static Gen5ShaderInstruction DecodeSingle(params uint[] words)
|
||||
{
|
||||
var memory = new FakeCpuMemory(ShaderAddress, 0x1000);
|
||||
|
|
|
|||
|
|
@ -102,8 +102,8 @@ public sealed class Gen5SpirvAtomicTranslationTests
|
|||
{
|
||||
var opcodes = CompileComputeOpcodes(
|
||||
[
|
||||
0xD8FA0014, 0x07000000,
|
||||
0xD8F60014, 0x08000000,
|
||||
0xD8F80014, 0x07000000,
|
||||
0xD8F40014, 0x08000000,
|
||||
],
|
||||
new Dictionary<uint, uint>());
|
||||
|
||||
|
|
@ -120,8 +120,8 @@ public sealed class Gen5SpirvAtomicTranslationTests
|
|||
{
|
||||
var opcodes = CompileVertexOpcodes(
|
||||
[
|
||||
0xD8FA0014, 0x07000000,
|
||||
0xD8F60014, 0x08000000,
|
||||
0xD8F80014, 0x07000000,
|
||||
0xD8F40014, 0x08000000,
|
||||
]);
|
||||
|
||||
// Private graphics storage applies the counter once per active wave.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,56 @@ public class AmprFileRegistryTests
|
|||
Assert.Equal(firstHost, firstResult);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void GuestPathSpellingOwnsIdOverCompatibilityAlias(bool reverseOrder)
|
||||
{
|
||||
// Demon's Souls: the cooked "/app0/" id of this animation collides with
|
||||
// the emulator-only "app0/" alias of an unrelated texture.
|
||||
AmprFileRegistry.ClearForTests();
|
||||
const string animation = "characters/c0000_main/animation/a202/_cmn/bp202_weapontakeoutrightshoulder_run.cani";
|
||||
const string texture = "parts/m_8120_ancientking/textures/_ps5/hd_m_8120_ancientking_mud_nml.chunk0.ctxc";
|
||||
var cookedId = AmprFileRegistry.ComputeFileId("/app0/" + animation);
|
||||
Assert.Equal(cookedId, AmprFileRegistry.ComputeFileId("app0/" + texture));
|
||||
var animationHost = Path.Combine(Path.GetTempPath(), "apr-animation.cani");
|
||||
var textureHost = Path.Combine(Path.GetTempPath(), "apr-texture.ctxc");
|
||||
|
||||
if (reverseOrder)
|
||||
{
|
||||
AmprFileRegistry.RegisterApp0RelativeForTests(texture, textureHost);
|
||||
AmprFileRegistry.RegisterApp0RelativeForTests(animation, animationHost);
|
||||
}
|
||||
else
|
||||
{
|
||||
AmprFileRegistry.RegisterApp0RelativeForTests(animation, animationHost);
|
||||
AmprFileRegistry.RegisterApp0RelativeForTests(texture, textureHost);
|
||||
}
|
||||
|
||||
Assert.True(AmprFileRegistry.TryGetHostPath(cookedId, out var resolved));
|
||||
Assert.Equal(animationHost, resolved);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void TitleResolvedPathKeepsIdAgainstLaterCollidingAlias(bool aliasFirst)
|
||||
{
|
||||
AmprFileRegistry.ClearForTests();
|
||||
const string firstPath = "$/assets/bce5a816.bin";
|
||||
const string secondRelative = "assets/1b9e7058.bin";
|
||||
var firstHost = Path.Combine(Path.GetTempPath(), "apr-first.bin");
|
||||
var secondHost = Path.Combine(Path.GetTempPath(), "apr-second.bin");
|
||||
|
||||
if (aliasFirst)
|
||||
AmprFileRegistry.RegisterApp0RelativeForTests(secondRelative, secondHost);
|
||||
var id = AmprFileRegistry.RegisterAprResolvedPath(firstPath, firstHost);
|
||||
AmprFileRegistry.RegisterApp0RelativeForTests(secondRelative, secondHost);
|
||||
|
||||
Assert.True(AmprFileRegistry.TryGetHostPath(id, out var resolved));
|
||||
Assert.Equal(firstHost, resolved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeFileId_matches_utf8_fnv1a()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,11 +36,13 @@ public sealed class MemcpyHleRoutingTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHlePreferredNid_PrefersHleForStrcasecmp()
|
||||
public void IsHlePreferredNid_LetsStrcasecmpUseItsIntrinsic()
|
||||
{
|
||||
Assert.True(
|
||||
// Demon's Souls calls strcasecmp ~2M times while loading; the HLE round
|
||||
// trip cost ~13s. The intrinsic reproduces the HLE null-argument answer.
|
||||
Assert.False(
|
||||
InvokeIsHlePreferredNid(StrcasecmpNid),
|
||||
$"strcasecmp ({StrcasecmpNid}) must route through HLE so null-argument recovery remains active.");
|
||||
$"strcasecmp ({StrcasecmpNid}) should use its null-safe intrinsic stub.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -62,7 +64,7 @@ public sealed class MemcpyHleRoutingTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCreateNativeImportIntrinsic_DoesNotClaimStrcasecmp()
|
||||
public void TryCreateNativeImportIntrinsic_ClaimsStrcasecmp()
|
||||
{
|
||||
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
|
|
@ -71,10 +73,13 @@ public sealed class MemcpyHleRoutingTests
|
|||
|
||||
var claimed = InvokeTryCreateNativeImportIntrinsic(StrcasecmpNid, out var address);
|
||||
|
||||
Assert.False(
|
||||
claimed,
|
||||
$"strcasecmp ({StrcasecmpNid}) must fall through to the HLE trampoline so a null guest pointer does not fault in the intrinsic stub.");
|
||||
Assert.Equal(0, address);
|
||||
Assert.True(claimed, $"strcasecmp ({StrcasecmpNid}) should receive its intrinsic stub.");
|
||||
Assert.NotEqual(0, address);
|
||||
|
||||
unsafe
|
||||
{
|
||||
Assert.True(HostMemory.Free((void*)address, 0, HostMemory.MEM_RELEASE));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -234,6 +234,9 @@ public sealed unsafe partial class GuestImageCacheTests
|
|||
public void UnequalSampleDepthOverlap_RunsTheColorToDepthBlit()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan, sampleRateShading: true)) return;
|
||||
// Z16 with stencil may use any of these; AMD RDNA has no D24S8, so take one the device offers.
|
||||
var stencilFormat = SupportedStencilFormat(_vulkan, SampleCountFlags.Count2Bit);
|
||||
if (stencilFormat == Format.Undefined) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
using var reader = new MultisampleDepthSampleReader(_vulkan, harness.Scheduler, harness.Worker.Run);
|
||||
var address = harness.MapBacked(0x100000, ReadWrite);
|
||||
|
|
@ -252,7 +255,7 @@ public sealed unsafe partial class GuestImageCacheTests
|
|||
harness.Cache.FillBuffer(stencilAddress, stencilSize, 0x41414141u, isGds: false);
|
||||
});
|
||||
|
||||
var depth = AsDepthTarget(color, Format.D24UnormS8Uint);
|
||||
var depth = AsDepthTarget(color, stencilFormat);
|
||||
depth.Description.Stencil = new GuestSpan(stencilAddress, stencilSize);
|
||||
depth.Description.GuestFormat = GuestPixelFormat.Bits16UNorm;
|
||||
depth.Description.BytesPerBlock = 2;
|
||||
|
|
@ -496,18 +499,35 @@ public sealed unsafe partial class GuestImageCacheTests
|
|||
harness.Shutdown();
|
||||
}
|
||||
|
||||
private static Format SupportedStencilFormat(SharpEmu.Libs.Tests.Gpu.Vulkan.HeadlessVulkan vulkan, SampleCountFlags samples)
|
||||
{
|
||||
const ImageUsageFlags usage = ImageUsageFlags.TransferSrcBit | ImageUsageFlags.TransferDstBit | ImageUsageFlags.DepthStencilAttachmentBit;
|
||||
foreach (var format in DepthFormatRule.Find(GuestDepthFormat.Z16)!.StencilAttachmentFormats)
|
||||
{
|
||||
if (vulkan.DeviceInfo.TryGetImageFormatProperties(format, ImageType.Type2D, ImageTiling.Optimal, usage, 0, out var properties) &&
|
||||
(properties.SampleCounts & samples) != 0)
|
||||
{
|
||||
return format;
|
||||
}
|
||||
}
|
||||
|
||||
return Format.Undefined;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void D16Fallback_CopiesDepthIntoStorageColor()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
var stencilFormat = SupportedStencilFormat(_vulkan, SampleCountFlags.Count1Bit);
|
||||
if (stencilFormat == Format.Undefined) return;
|
||||
using var harness = new CacheHarness(_vulkan);
|
||||
var address = harness.MapBacked(0x10000, ReadWrite);
|
||||
var depthAddress = address + 0xc000;
|
||||
var stencilAddress = address + 0xd000;
|
||||
harness.Write(depthAddress, Bytes((ushort)0x0000, (ushort)0x2468, (ushort)0xabcd, (ushort)0xffff));
|
||||
harness.Write(stencilAddress, [0x6d, 0x6d, 0x6d, 0x6d]);
|
||||
var depth = LinearRequest(depthAddress, 8, Format.D24UnormS8Uint, GuestPixelFormat.Bits16UNorm, GuestImageType.Color2D, new Extent3D(4, 1, 1), 1, 2, 1);
|
||||
depth = AsDepthTarget(depth, Format.D24UnormS8Uint);
|
||||
var depth = LinearRequest(depthAddress, 8, stencilFormat, GuestPixelFormat.Bits16UNorm, GuestImageType.Color2D, new Extent3D(4, 1, 1), 1, 2, 1);
|
||||
depth = AsDepthTarget(depth, stencilFormat);
|
||||
depth.Description.Stencil = new GuestSpan(stencilAddress, 4);
|
||||
depth.View = depth.View with { Aspect = ImageAspectFlags.DepthBit | ImageAspectFlags.StencilBit };
|
||||
var depthId = harness.Acquire(ref depth);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,9 @@ public sealed class ComputeRejectionPolicyTests
|
|||
var guest = new PipelineTestGuest();
|
||||
guest.RegisterProgram(CodeAddress, HeaderAddress,
|
||||
[0xF4080000, 0xFA000000, 0xE0000000, 0x80000000, 0xBF810000]);
|
||||
var source = guest.Source(CodeAddress, ShaderStage.Compute, new uint[2]);
|
||||
// The descriptor pointer is mapped nowhere. A null pointer would not do: reads through
|
||||
// a null base are evaluated as zero, because shaders branch around them.
|
||||
var source = guest.Source(CodeAddress, ShaderStage.Compute, [0u, 2u]);
|
||||
var cursor = 0u;
|
||||
var failure = Assert.Throws<SchedulerFatalException>(() => guest.Programs.TryGetProgram(
|
||||
source, PipelineTestGuest.ComputeOptions(1), false, ref cursor, out _, out _, out _));
|
||||
|
|
|
|||
|
|
@ -279,6 +279,30 @@ public sealed class ShaderPipelineCacheTests : IDisposable
|
|||
Assert.Equal(0u, description.StaticParameters.ColorCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x0u, 0x0u)]
|
||||
[InlineData(0xFu, 0xFu)]
|
||||
public void ColorTarget_TheProgramNeverExports_IsNotWritten(uint exportMasks, uint expectedMask)
|
||||
{
|
||||
// Astro Bot's depth-only pattern pass keeps a color target bound but exports only to
|
||||
// the null target; writing the undefined host output drew a rectangle over the movie.
|
||||
var banks = Banks();
|
||||
banks.Context.RenderTargetMask = 0xF;
|
||||
var programs = Programs(pixelStage: Stage(new ShaderProgramInfo { Stage = ShaderStageKind.Pixel, PixelColorExportMasks = exportMasks }));
|
||||
var resolution = new ColorTargetResolution(
|
||||
default, 0x1000, 0x10000, new Extent2D(64, 64), 0, 0, 1, ColorComponentMap.Identity, false, false, default);
|
||||
ColorTargetState[] colors = [new(in resolution, 0, new SharpEmu.Libs.Gpu.Buffers.ResourceSlotIdentifier(1, 1))];
|
||||
var rendering = new RenderingState { Samples = 1, ColorAttachmentCount = 1 };
|
||||
rendering.ColorAttachments[0] = new RenderingAttachment(
|
||||
default, ImageLayout.ColorAttachmentOptimal, Format.R8G8B8A8Unorm, 0, 0, 0, 0, false, false, false, false, false);
|
||||
|
||||
var description = ShaderPipelineCache.BuildGraphicsDescription(
|
||||
colors, default, programs.VertexInput, programs.PixelInput, banks.Context, in rendering, PrimitiveTopology.TriangleList, false, false,
|
||||
programs.Vertex, programs.Pixel, SampleCountFlags.Count1Bit);
|
||||
|
||||
Assert.Equal(expectedMask, description.StaticParameters.GetColorMask(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoAttachments_AtAnUnsupportedSampleCount_AreFatal()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -163,9 +163,13 @@ public sealed class DirectMemoryAllocationMapTests
|
|||
var allocations = new DirectMemoryAllocationMap(10000);
|
||||
for (ulong address = 0; address < 10000; address += 2)
|
||||
Allocate(allocations, address, 1);
|
||||
var matches = 0;
|
||||
for (var pass = 0; pass < 2; pass++)
|
||||
// Tiered JIT recompilation can still allocate on this thread during the first measured
|
||||
// passes when the machine is busy; storage allocated by the queries would show in every pass.
|
||||
const int passes = 5;
|
||||
var steadyStateAllocation = long.MaxValue;
|
||||
for (var pass = 0; pass < passes; pass++)
|
||||
{
|
||||
var matches = 0;
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (ulong address = 0; address < 10000; address += 2)
|
||||
{
|
||||
|
|
@ -174,10 +178,12 @@ public sealed class DirectMemoryAllocationMapTests
|
|||
if (allocations.TryFindAvailableRange(address, address + 2, 1, out _, out _)) matches++;
|
||||
}
|
||||
var allocatedBytes = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.Equal(15000, matches);
|
||||
if (pass != 0)
|
||||
Assert.Equal(0L, allocatedBytes);
|
||||
steadyStateAllocation = Math.Min(steadyStateAllocation, allocatedBytes);
|
||||
}
|
||||
Assert.Equal(30000, matches);
|
||||
|
||||
Assert.Equal(0L, steadyStateAllocation);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -430,6 +430,23 @@ public sealed class PresenterImageBindingTests : IClassFixture<HeadlessVulkanFix
|
|||
presenter.Harness.Shutdown();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullTexture_ForDepthReferenceSampling_IsADepthView()
|
||||
{
|
||||
if (!GatePrerequisites.Ready(_vulkan)) return;
|
||||
using var presenter = new PresenterUnderTest(_vulkan);
|
||||
var texture = new GuestDrawTexture(0, 1, 1, 0, 0, [], false, false,
|
||||
Descriptor: [], Shape: new ShaderImageShape(false, false, false, false, TextureNumericClass.Float) with { DepthCompare = true });
|
||||
|
||||
var resource = presenter.Run(() => AcquireTexture(presenter, texture));
|
||||
Assert.NotEqual(0UL, ((ImageView)GetFieldValue(resource, "View")).Handle);
|
||||
var resolution = (TextureRequestResolution)GetFieldValue(resource, "Resolution");
|
||||
Assert.Equal(Format.D32Sfloat, resolution.ViewFormat);
|
||||
presenter.Run(() => presenter.InvokeMethod("ResetImageBindings"));
|
||||
presenter.Harness.Finish();
|
||||
presenter.Harness.Shutdown();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ColorTarget_IsFoundInTheStoreAndBoundForTheDraw()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -406,8 +406,8 @@ public sealed class MslTranslationTests
|
|||
var fixture = new Gen5ComputeFixture(
|
||||
"data-share-wave-counters",
|
||||
[
|
||||
0xD8FA0014, 0x07000000, // ds_append v7 offset:20
|
||||
0xD8F60014, 0x08000000, // ds_consume v8 offset:20
|
||||
0xD8F80014, 0x07000000, // ds_append v7 offset:20
|
||||
0xD8F40014, 0x08000000, // ds_consume v8 offset:20
|
||||
0xBF810000, // s_endpgm
|
||||
],
|
||||
StoreScalarResourceBase: 0,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public sealed class Gen5DataShareRead64Tests
|
|||
public static Gen5ShaderProgram CreatePairedReadbackProgram(bool global, uint firstOffset, uint secondOffset, bool readEnabled)
|
||||
{
|
||||
var memory = new ShaderMemory();
|
||||
uint[] words = [0xD9DC0000u | (global ? 1u << 16 : 0) | firstOffset | (secondOffset << 8), 0x03000003, 0xBF810000];
|
||||
uint[] words = [0xD9DC0000u | (global ? 1u << 17 : 0) | firstOffset | (secondOffset << 8), 0x03000003, 0xBF810000];
|
||||
var bytes = new byte[words.Length * sizeof(uint)];
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)), words[index]);
|
||||
|
|
@ -100,7 +100,7 @@ public sealed class Gen5DataShareRead64Tests
|
|||
{
|
||||
const ulong shaderAddress = 0x1000;
|
||||
var memory = new ShaderMemory();
|
||||
uint[] words = [0xD9D80000u | (global ? 1u << 16 : 0) | offset, 0x03000003u, 0xBF810000u];
|
||||
uint[] words = [0xD9D80000u | (global ? 1u << 17 : 0) | offset, 0x03000003u, 0xBF810000u];
|
||||
var bytes = new byte[words.Length * sizeof(uint)];
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)), words[index]);
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ public sealed class Gen5DataShareWrite64Tests
|
|||
public static Gen5ShaderProgram CreateReadbackProgram(bool global, uint firstOffset, uint secondOffset, bool writeEnabled,
|
||||
uint secondSource, bool largeStride)
|
||||
{
|
||||
uint[] words = [(largeStride ? 0xD93C0000u : 0xD9380000u) | (global ? 1u << 16 : 0) | firstOffset | (secondOffset << 8),
|
||||
uint[] words = [(largeStride ? 0xD93C0000u : 0xD9380000u) | (global ? 1u << 17 : 0) | firstOffset | (secondOffset << 8),
|
||||
3u | (10u << 8) | (secondSource << 16), 0xBF810000];
|
||||
var bytes = new byte[words.Length * sizeof(uint)];
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
|
|
|
|||
|
|
@ -102,11 +102,32 @@ public sealed class Gen5ImageTests
|
|||
CompileImageOperation(
|
||||
"ImageSampleCLz",
|
||||
dimension: 1,
|
||||
samplerWord0: compareFunction << 12));
|
||||
samplerWord0: compareFunction << 12,
|
||||
unifiedFormat: 22u));
|
||||
|
||||
Assert.Contains(instructions, item => item.Opcode == SpirvOp.ImageSampleDrefExplicitLod);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1u, SpirvOp.FOrdLessThan)]
|
||||
[InlineData(4u, SpirvOp.FOrdGreaterThan)]
|
||||
[InlineData(6u, SpirvOp.FOrdGreaterThanEqual)]
|
||||
public void ImageSampleCompareOnAColorFormatComparesInTheShader(
|
||||
uint compareFunction,
|
||||
SpirvOp expectedComparison)
|
||||
{
|
||||
// RGBA8 has no Vulkan depth-compare view, so the shader samples and compares.
|
||||
var instructions = ReadSpirvInstructions(
|
||||
CompileImageOperation(
|
||||
"ImageSampleCLz",
|
||||
dimension: 1,
|
||||
samplerWord0: compareFunction << 12));
|
||||
|
||||
Assert.DoesNotContain(instructions, item => item.Opcode == SpirvOp.ImageSampleDrefExplicitLod);
|
||||
Assert.Contains(instructions, item => item.Opcode == SpirvOp.ImageSampleExplicitLod);
|
||||
Assert.Contains(instructions, item => item.Opcode == expectedComparison);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImageSampleCompareLzUsesNativeFilteringWithTheSampler()
|
||||
{
|
||||
|
|
@ -115,7 +136,8 @@ public sealed class Gen5ImageTests
|
|||
"ImageSampleCLz",
|
||||
dimension: 1,
|
||||
samplerWord0: 0x00006012,
|
||||
samplerWord2: 0x00500000));
|
||||
samplerWord2: 0x00500000,
|
||||
unifiedFormat: 22u));
|
||||
|
||||
Assert.Single(instructions, item => item.Opcode == SpirvOp.ImageSampleDrefExplicitLod);
|
||||
Assert.DoesNotContain(instructions, item => item.Opcode == SpirvOp.ImageFetch);
|
||||
|
|
|
|||
|
|
@ -150,15 +150,15 @@ public sealed class BindingLayoutTests
|
|||
[Fact]
|
||||
public void ImageBindingAbi()
|
||||
{
|
||||
Assert.Equal(42u, BindingLayout.ImageBindingCount);
|
||||
Assert.Equal(45u, BindingLayout.ImageBindingCount);
|
||||
Assert.Equal(0u, (uint)DescriptorBindingKind.Buffers);
|
||||
Assert.Equal(43u, (uint)DescriptorBindingKind.Samplers);
|
||||
Assert.Equal(44u, (uint)DescriptorBindingKind.GlobalDataShare);
|
||||
Assert.Equal(45u, (uint)DescriptorBindingKind.DeviceAddressPageTable);
|
||||
Assert.Equal(46u, (uint)DescriptorBindingKind.FaultBuffer);
|
||||
Assert.Equal(47u, (uint)DescriptorBindingKind.FlattenedResourceTable);
|
||||
Assert.Equal(48u, (uint)DescriptorBindingKind.ShaderData);
|
||||
Assert.Equal(49u, (uint)DescriptorBindingKind.Count);
|
||||
Assert.Equal(46u, (uint)DescriptorBindingKind.Samplers);
|
||||
Assert.Equal(47u, (uint)DescriptorBindingKind.GlobalDataShare);
|
||||
Assert.Equal(48u, (uint)DescriptorBindingKind.DeviceAddressPageTable);
|
||||
Assert.Equal(49u, (uint)DescriptorBindingKind.FaultBuffer);
|
||||
Assert.Equal(50u, (uint)DescriptorBindingKind.FlattenedResourceTable);
|
||||
Assert.Equal(51u, (uint)DescriptorBindingKind.ShaderData);
|
||||
Assert.Equal(52u, (uint)DescriptorBindingKind.Count);
|
||||
|
||||
ImageDimension[] sampledDimensions =
|
||||
[
|
||||
|
|
@ -170,9 +170,9 @@ public sealed class BindingLayoutTests
|
|||
ImageDimension.Dim1D, ImageDimension.Dim1DArray, ImageDimension.Dim2D, ImageDimension.Dim2DArray, ImageDimension.Dim3D,
|
||||
];
|
||||
var index = 0u;
|
||||
void CheckBinding(ImageResourceClass resourceClass, ImageNumericClass numericClass, ImageDimension dimension, bool atomic, bool cube = false)
|
||||
void CheckBinding(ImageResourceClass resourceClass, ImageNumericClass numericClass, ImageDimension dimension, bool atomic, bool cube = false, bool depthCompare = false)
|
||||
{
|
||||
var image = new ImageResource { ResourceClass = resourceClass, NumericClass = numericClass, Dimension = dimension, Atomic = atomic, Cube = cube };
|
||||
var image = new ImageResource { ResourceClass = resourceClass, NumericClass = numericClass, Dimension = dimension, Atomic = atomic, Cube = cube, DepthCompare = depthCompare };
|
||||
var kind = ImageDescriptorBinding.ForImage(image);
|
||||
Assert.NotNull(kind);
|
||||
Assert.Equal(BindingLayout.FirstImageBinding + index, (uint)kind!.Value);
|
||||
|
|
@ -209,6 +209,10 @@ public sealed class BindingLayoutTests
|
|||
foreach (var numericClass in new[] { ImageNumericClass.Float, ImageNumericClass.Uint })
|
||||
CheckBinding(ImageResourceClass.Storage, numericClass, ImageDimension.Dim2D, false, cube: true);
|
||||
CheckBinding(ImageResourceClass.Storage, ImageNumericClass.Uint, ImageDimension.Dim2D, true, cube: true);
|
||||
// Depth-reference sampling never shares an array with ordinary images.
|
||||
CheckBinding(ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2D, false, depthCompare: true);
|
||||
CheckBinding(ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2DArray, false, depthCompare: true);
|
||||
CheckBinding(ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2D, false, cube: true, depthCompare: true);
|
||||
|
||||
Assert.Equal(BindingLayout.ImageBindingCount, index);
|
||||
|
||||
|
|
|
|||
|
|
@ -133,12 +133,12 @@ internal static class ResourceTestProgram
|
|||
}
|
||||
|
||||
public static Gen5ShaderInstruction DataShareWrite(uint pc, bool gds) =>
|
||||
new(pc, Gen5ShaderEncoding.Ds, "DsWriteB32", [gds ? 1u << 16 : 0u, 0u], [Gen5Operand.Vector(0), Gen5Operand.Vector(1)], [],
|
||||
new(pc, Gen5ShaderEncoding.Ds, "DsWriteB32", [gds ? 1u << 17 : 0u, 0u], [Gen5Operand.Vector(0), Gen5Operand.Vector(1)], [],
|
||||
new Gen5DataShareControl(0, 0, gds));
|
||||
|
||||
// Any DS instruction: sources first (address, data...), then the vector destinations.
|
||||
public static Gen5ShaderInstruction DataShare(uint pc, string opcode, bool gds, Gen5Operand[] sources, uint[] destinations, uint offset0 = 0, uint offset1 = 0) =>
|
||||
new(pc, Gen5ShaderEncoding.Ds, opcode, [gds ? 1u << 16 : 0u, 0u], sources, destinations.Select(Gen5Operand.Vector).ToArray(),
|
||||
new(pc, Gen5ShaderEncoding.Ds, opcode, [gds ? 1u << 17 : 0u, 0u], sources, destinations.Select(Gen5Operand.Vector).ToArray(),
|
||||
new Gen5DataShareControl(offset0, offset1, gds));
|
||||
|
||||
// A global or flat access with explicit data registers: a load writes destination,
|
||||
|
|
|
|||
|
|
@ -107,6 +107,26 @@ public sealed class ResourceTrackerTests
|
|||
Assert.Equal(0u, plan.Info.Buffers[0].ImageAlias);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScalarBufferWithDeviceOnlyDescriptor_ReadsThroughRegisters()
|
||||
{
|
||||
// The descriptor base comes from a lane value: the host cannot bind it, the device reads through it.
|
||||
var program = Program(
|
||||
ReadFirstLane(0, 12, 0),
|
||||
MoveScalar(4, 13, 0),
|
||||
MoveScalar(8, 14, 16),
|
||||
MoveScalar(12, 15, 0),
|
||||
ScalarBufferLoad(16, 12, destination: 20),
|
||||
EndProgram(24));
|
||||
|
||||
var plan = Extract(program);
|
||||
|
||||
Assert.Empty(plan.Info.Buffers);
|
||||
Assert.True(plan.Info.UsesDeviceAddresses);
|
||||
Assert.True(plan.Memory.TryGetIndex(16, 0, out var index));
|
||||
Assert.True(plan.Memory[index].DeviceDescriptor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SamplerWithDivergentBits_IsRejected()
|
||||
{
|
||||
|
|
@ -387,10 +407,26 @@ public sealed class ResourceTrackerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void WrappedScalarImmediate_DoesNotEnterTheIndirectImageProof()
|
||||
public void MaterialKeyImmediateIsReadAfterTheWrappedSelectorOffset()
|
||||
{
|
||||
var error = Assert.Throws<ResourcePlanException>(() => Extract(IndirectImageProgram(false, materialImmediate: 4)));
|
||||
Assert.Contains("not a valid runtime value", error.Message);
|
||||
var plan = Extract(IndirectImageProgram(false, materialImmediate: 4));
|
||||
var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!;
|
||||
Assert.Equal(4u, selector.SelectorOffset);
|
||||
Assert.Equal(4u, selector.MaterialImmediate);
|
||||
|
||||
uint[] userData = [0x1000, 224 << 16, 2, 0, 0x2000, 16 << 16, 4, 0, 7];
|
||||
var memory = LinearMemory();
|
||||
var first = ImageDescriptor();
|
||||
var second = ImageDescriptor();
|
||||
second[0] += 1;
|
||||
WriteImage(memory, 0x2000, first);
|
||||
WriteImage(memory, 0x2020, second);
|
||||
// The dynamic offset selects byte 4 of the record; the immediate moves the key to byte 8.
|
||||
memory.At(0x1000 + 224 + 8) = 1;
|
||||
var snapshot = new ResourceSnapshot();
|
||||
var specialization = new ResourceSpecialization();
|
||||
Assert.True(ResourceMaterializer.Materialize(plan, Inputs(userData, readCleanMemory: memory.Read), ref snapshot, ref specialization));
|
||||
Assert.Equal([first, second], snapshot.Images);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -396,6 +396,27 @@ public sealed class ScalarValueGraphTests
|
|||
Assert.True(plan.Info.UsesDeviceAddresses);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(32u, 0x1000u)]
|
||||
[InlineData(64u, 0u)]
|
||||
public void VectorCompareKeepsVccHighInWave32(uint waveSize, uint expected)
|
||||
{
|
||||
// Wave32 compilers use VCC_HI (s107) as an ordinary SGPR; a compare writes VCC_LO only.
|
||||
var program = Program(
|
||||
MoveScalar(0, 107, 0x1000),
|
||||
Vopc(4, "VCmpEqU32", Gen5Operand.Scalar(0), 1),
|
||||
MoveScalarRegister(8, 8, 107),
|
||||
MoveScalar(12, 9, 0),
|
||||
MoveScalar(16, 10, 16),
|
||||
MoveScalar(20, 11, 0),
|
||||
BufferLoad(24, 8),
|
||||
EndProgram(32));
|
||||
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 64, waveSize: waveSize);
|
||||
|
||||
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs([]), out var result));
|
||||
Assert.Equal(expected, result.Dwords[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UniformVectorDerivedValue_IsAccepted()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -381,6 +381,46 @@ public sealed class SpirvBindingDeclarationTests
|
|||
Assert.Contains((uint)SpirvCapability.GroupNonUniformShuffle, module.Capabilities);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataShareBpermute_DeclaresTheShuffleCapability()
|
||||
{
|
||||
// DS_BPERMUTE_B32 v2, v0, v1 lowers to OpGroupNonUniformShuffle.
|
||||
var program = Program(
|
||||
DataShare(0, "DsBpermuteB32", gds: false, [Gen5Operand.Vector(0), Gen5Operand.Vector(1)], [2]),
|
||||
EndProgram(8));
|
||||
var request = Request(program, ShaderStage.Compute);
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
|
||||
var module = new SpirvModuleInspector(shader.Spirv);
|
||||
Assert.Contains((ushort)SpirvOp.GroupNonUniformShuffle, module.Opcodes);
|
||||
Assert.Contains((uint)SpirvCapability.GroupNonUniformShuffle, module.Capabilities);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DppWithoutGraphicsSubgroups_KeepsTheLaneValue(bool enableGraphicsSubgroupOperations)
|
||||
{
|
||||
// V_MOV_B32_DPP8 v1, v0: a one-lane wave reads its own lane, so no shuffle
|
||||
// (and no missing GroupNonUniformShuffle capability) when subgroups are off.
|
||||
var dpp = new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Vop1, "VMovB32", [0u],
|
||||
[Gen5Operand.Vector(0)], [Gen5Operand.Vector(1)], new Gen5Dpp8Control(0xFAC688, FetchInactive: true));
|
||||
var program = Program(dpp, EndProgram(8));
|
||||
var (plan, resources, layout) = Prepare(program, ShaderStage.Pixel, userDataCount: 0);
|
||||
var request = new ShaderCompileRequest(plan, resources, layout)
|
||||
{
|
||||
PixelOutputs = [new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)],
|
||||
EnableGraphicsSubgroupOperations = enableGraphicsSubgroupOperations,
|
||||
};
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
|
||||
var module = new SpirvModuleInspector(shader.Spirv);
|
||||
Assert.Equal(
|
||||
module.Opcodes.Contains((ushort)SpirvOp.GroupNonUniformShuffle),
|
||||
module.Capabilities.Contains((uint)SpirvCapability.GroupNonUniformShuffle));
|
||||
Assert.Equal(enableGraphicsSubgroupOperations, module.Opcodes.Contains((ushort)SpirvOp.GroupNonUniformShuffle));
|
||||
}
|
||||
|
||||
// A storage image whose descriptor spans two mips, written through a register-selected mip.
|
||||
internal static Gen5ShaderProgram DynamicMipStoreProgram()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue