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:
Astell 2026-09-24 14:00:42 +02:00 committed by GitHub
parent 22e73fbea1
commit d4ff32a1d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 1065 additions and 171 deletions

View File

@ -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))
{

View File

@ -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)

View File

@ -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))

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;
}

View File

@ -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) ||

View File

@ -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;

View File

@ -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,

View File

@ -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; }

View File

@ -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",

View File

@ -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;
}

View File

@ -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);

View File

@ -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)
{

View File

@ -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)})",

View File

@ -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

View File

@ -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,

View File

@ -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;
}

View File

@ -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 =>

View File

@ -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,

View File

@ -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)
{

View File

@ -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;

View File

@ -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;

View File

@ -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.

View File

@ -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 = [];
}

View File

@ -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.

View File

@ -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))

View File

@ -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,
},
};

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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>();

View File

@ -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);

View File

@ -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.

View File

@ -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()
{

View File

@ -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]

View File

@ -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);

View File

@ -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 _));

View File

@ -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()
{

View File

@ -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]

View File

@ -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()
{

View File

@ -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,

View File

@ -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]);

View File

@ -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++)

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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]

View File

@ -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()
{

View File

@ -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()
{