From d4ff32a1d27d33afe7b7fb640b9d7567f16fea89 Mon Sep 17 00:00:00 2001 From: Astell <34811047+Astellou@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:00:42 +0200 Subject: [PATCH] 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 --- .../DirectExecutionBackend.Exceptions.cs | 12 +- .../Cpu/Native/DirectExecutionBackend.cs | 14 +- src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs | 87 +++++++++--- .../GpuCommands/Registers/RegisterOffsets.cs | 2 + .../Registers/RegisterWriters.Shader.cs | 2 + .../Registers/ShaderProgramRegisters.cs | 5 +- src/SharpEmu.Libs/Gpu/Images/CachedImage.cs | 4 +- .../Images/ImageRequestBuilders.Texture.cs | 26 +++- .../Gpu/Pipelines/ShaderPipelineCache.cs | 6 +- .../Gpu/Pipelines/ShaderProgramCache.cs | 5 +- .../Gpu/Rendering/ShaderStageResources.cs | 2 + src/SharpEmu.Libs/Kernel/KernelExports.cs | 14 ++ .../Kernel/KernelMemoryCompatExports.cs | 81 ++++++++++- src/SharpEmu.Libs/Stubs/GameServiceStubs.cs | 10 ++ .../VulkanVideoPresenter.Rendering.cs | 3 +- .../Gen5MslTranslator.Alu.cs | 4 + .../Gen5MslTranslator.cs | 2 + .../Gen5SpirvTranslator.Alu.cs | 81 +++++------ .../Gen5SpirvTranslator.Resources.cs | 45 +++++- .../Gen5SpirvTranslator.cs | 131 ++++++++++++++++-- src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs | 15 ++ .../Gen5ShaderTranslator.cs | 105 +++++++++++++- .../Resources/BindingLayout.cs | 42 +++++- .../Resources/GuestImageFormat.cs | 6 + .../Resources/MemoryAccessInfo.cs | 6 +- .../Resources/ResourceMaterializer.cs | 27 +++- .../Resources/ResourceSpecialization.cs | 3 +- .../Resources/ResourceTableReadPlanner.cs | 7 + .../Resources/ResourceTracker.cs | 16 ++- .../Resources/RuntimeValueEvaluator.cs | 9 ++ .../Resources/ScalarValueGraph.Builder.cs | 47 +++++-- .../Resources/ScalarValueGraph.cs | 11 +- .../Resources/ShaderResourceInfo.cs | 5 + .../Resources/ShaderResourcePlan.cs | 4 +- .../Agc/Gen5ShaderAtomicDecodeTests.cs | 66 ++++++++- .../Agc/Gen5SpirvAtomicTranslationTests.cs | 8 +- .../Ampr/AmprFileRegistryTests.cs | 50 +++++++ .../Cpu/MemcpyHleRoutingTests.cs | 21 +-- .../Gpu/Images/GuestImageCacheTests.Depth.cs | 26 +++- .../Pipelines/ComputeRejectionPolicyTests.cs | 4 +- .../Gpu/Pipelines/ShaderPipelineCacheTests.cs | 24 ++++ .../Kernel/DirectMemoryAllocationMapTests.cs | 14 +- .../VideoOut/PresenterImageBindingTests.cs | 17 +++ .../MslTranslationTests.cs | 4 +- .../Gen5DataShareRead64Tests.cs | 4 +- .../Gen5DataShareWrite64Tests.cs | 2 +- .../Gen5ImageTests.cs | 26 +++- .../Resources/BindingLayoutTests.cs | 24 ++-- .../Resources/ResourceTestProgram.cs | 4 +- .../Resources/ResourceTrackerTests.cs | 42 +++++- .../Resources/ScalarValueGraphTests.cs | 21 +++ .../Resources/SpirvBindingDeclarationTests.cs | 40 ++++++ 52 files changed, 1065 insertions(+), 171 deletions(-) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index 5881f80..d4a1372 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -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)) { diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 5fdce4d..b707afc 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -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) diff --git a/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs index efae4a7..f24fe97 100644 --- a/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs +++ b/src/SharpEmu.Libs/Ampr/AmprFileRegistry.cs @@ -15,14 +15,29 @@ internal static class AmprFileRegistry private const uint CacheMagicV3 = 0x33495041u; // 'API3' private const uint CacheVersionV3 = 3; - private static readonly ConcurrentDictionary _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 _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 _resolvedIdsByPath = new(HostFsPath.Comparer); private static readonly ConcurrentDictionary _resolvedPathsById = new(); - private static readonly ConcurrentDictionary _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; } /// Test hook: wipe registry state between cases. @@ -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(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)) diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterOffsets.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterOffsets.cs index 53b8fe4..ef96974 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterOffsets.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterOffsets.cs @@ -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; diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Shader.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Shader.cs index 3fde92e..00e8036 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Shader.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/RegisterWriters.Shader.cs @@ -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); diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ShaderProgramRegisters.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ShaderProgramRegisters.cs index 2716e1b..e2d3e45 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ShaderProgramRegisters.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/Registers/ShaderProgramRegisters.cs @@ -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; diff --git a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs index d4a2de9..398d061 100644 --- a/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs +++ b/src/SharpEmu.Libs/Gpu/Images/CachedImage.cs @@ -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; } diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs index 4d268ba..9212e10 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.Texture.cs @@ -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) || diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs index 4398b2f..8660f20 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs @@ -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; diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs index 8fd5bc4..98fd6af 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs @@ -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, diff --git a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs index fa3d11e..caddd5a 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/ShaderStageResources.cs @@ -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; } diff --git a/src/SharpEmu.Libs/Kernel/KernelExports.cs b/src/SharpEmu.Libs/Kernel/KernelExports.cs index a62242f..a935ef5 100644 --- a/src/SharpEmu.Libs/Kernel/KernelExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelExports.cs @@ -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", diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index e14c2f1..5b32193 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -122,6 +122,12 @@ public static partial class KernelMemoryCompatExports // "/app0/Data.bin" even though that file exists. private static readonly HashSet _negativeStatCache = new(HostFsPath.Comparer); private static readonly ConcurrentDictionary _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 _verifiedMountComponents = new(HostFsPath.Comparer); + private static readonly ConcurrentDictionary _verifiedMountDirectories = new(HostFsPath.Comparer); + private static readonly ConcurrentDictionary _fullMountRoots = new(StringComparer.Ordinal); + private static readonly ConcurrentDictionary _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; } diff --git a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs index d359e13..197fa7b 100644 --- a/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs +++ b/src/SharpEmu.Libs/Stubs/GameServiceStubs.cs @@ -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); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs index 1b41240..2f3d3da 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs @@ -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) { diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs index 58b0b2f..eaf7b8a 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.Alu.cs @@ -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)})", diff --git a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs index 167e3b9..698c0d0 100644 --- a/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Metal/Gen5MslTranslator.cs @@ -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 diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs index 525a104..4ca62c9 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Alu.cs @@ -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, diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs index 0fafe2a..cf87259 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.Resources.cs @@ -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; } diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 0a67fbd..ddedb51 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -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 { 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 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 => diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs index 53d4570..52d5116 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderIr.cs @@ -199,6 +199,21 @@ public sealed record Gen5ImageControl( : VectorAddress + (uint)component; } +public sealed record Gen5RayIntersectControl( + uint VectorAddress, + IReadOnlyList 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, diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs index 8c25b75..150756f 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderTranslator.cs @@ -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(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(addressRegisters.Count + 2); foreach (var addressRegister in addressRegisters) { diff --git a/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs b/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs index a7df5d6..4f66edf 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/BindingLayout.cs @@ -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 { 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; diff --git a/src/SharpEmu.ShaderCompiler/Resources/GuestImageFormat.cs b/src/SharpEmu.ShaderCompiler/Resources/GuestImageFormat.cs index b341909..4954f58 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/GuestImageFormat.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/GuestImageFormat.cs @@ -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; diff --git a/src/SharpEmu.ShaderCompiler/Resources/MemoryAccessInfo.cs b/src/SharpEmu.ShaderCompiler/Resources/MemoryAccessInfo.cs index 32148b6..7c252a0 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/MemoryAccessInfo.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/MemoryAccessInfo.cs @@ -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. diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs index dd23a5d..2b2303c 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializer.cs @@ -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 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 = []; } diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceSpecialization.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceSpecialization.cs index ce35b3f..37c7c06 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceSpecialization.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceSpecialization.cs @@ -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. diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceTableReadPlanner.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceTableReadPlanner.cs index bf9e357..60d73a9 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceTableReadPlanner.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceTableReadPlanner.cs @@ -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)) diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs index 0395c28..05040a0 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceTracker.cs @@ -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, }, }; diff --git a/src/SharpEmu.ShaderCompiler/Resources/RuntimeValueEvaluator.cs b/src/SharpEmu.ShaderCompiler/Resources/RuntimeValueEvaluator.cs index a952816..2b238be 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/RuntimeValueEvaluator.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/RuntimeValueEvaluator.cs @@ -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; } diff --git a/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.Builder.cs b/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.Builder.cs index 1489279..4b051e2 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.Builder.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.Builder.cs @@ -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); } } diff --git a/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.cs b/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.cs index 6232677..0500c84 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.cs @@ -17,9 +17,11 @@ public sealed partial class ScalarValueGraph private readonly Dictionary _interned = []; private readonly List _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? fixedFunctionVertexLoads = null) + IReadOnlySet? 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; } diff --git a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourceInfo.cs b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourceInfo.cs index 69a814f..f2413bd 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourceInfo.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourceInfo.cs @@ -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); diff --git a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs index c625208..8a349a1 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs @@ -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? fixedFunctionVertexLoads = null, Action? beforeResourceTracking = null) + IReadOnlySet? fixedFunctionVertexLoads = null, Action? 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(); diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs index ff18975..0489bf2 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5ShaderAtomicDecodeTests.cs @@ -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(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(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(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); diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs index a6b66b3..fdb9fd8 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5SpirvAtomicTranslationTests.cs @@ -102,8 +102,8 @@ public sealed class Gen5SpirvAtomicTranslationTests { var opcodes = CompileComputeOpcodes( [ - 0xD8FA0014, 0x07000000, - 0xD8F60014, 0x08000000, + 0xD8F80014, 0x07000000, + 0xD8F40014, 0x08000000, ], new Dictionary()); @@ -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. diff --git a/tests/SharpEmu.Libs.Tests/Ampr/AmprFileRegistryTests.cs b/tests/SharpEmu.Libs.Tests/Ampr/AmprFileRegistryTests.cs index 3a1fa1b..8f3e656 100644 --- a/tests/SharpEmu.Libs.Tests/Ampr/AmprFileRegistryTests.cs +++ b/tests/SharpEmu.Libs.Tests/Ampr/AmprFileRegistryTests.cs @@ -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() { diff --git a/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs index 48d0bfe..94410f7 100644 --- a/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs +++ b/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs @@ -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] diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Depth.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Depth.cs index 6875216..952e624 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Depth.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/GuestImageCacheTests.Depth.cs @@ -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); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ComputeRejectionPolicyTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ComputeRejectionPolicyTests.cs index 6baa711..d8d1b28 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ComputeRejectionPolicyTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ComputeRejectionPolicyTests.cs @@ -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(() => guest.Programs.TryGetProgram( source, PipelineTestGuest.ComputeOptions(1), false, ref cursor, out _, out _, out _)); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPipelineCacheTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPipelineCacheTests.cs index b63b120..29e6d09 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPipelineCacheTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPipelineCacheTests.cs @@ -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() { diff --git a/tests/SharpEmu.Libs.Tests/Kernel/DirectMemoryAllocationMapTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/DirectMemoryAllocationMapTests.cs index 8e04c0f..263fd22 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/DirectMemoryAllocationMapTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/DirectMemoryAllocationMapTests.cs @@ -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] diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs index 988c5dc..9e322e5 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/PresenterImageBindingTests.cs @@ -430,6 +430,23 @@ public sealed class PresenterImageBindingTests : IClassFixture 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() { diff --git a/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslTranslationTests.cs b/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslTranslationTests.cs index 6578ad0..c044560 100644 --- a/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslTranslationTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Metal.Tests/MslTranslationTests.cs @@ -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, diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5DataShareRead64Tests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5DataShareRead64Tests.cs index 3790542..ee5f1ce 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5DataShareRead64Tests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5DataShareRead64Tests.cs @@ -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]); diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5DataShareWrite64Tests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5DataShareWrite64Tests.cs index 7564d7b..3410bdc 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5DataShareWrite64Tests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5DataShareWrite64Tests.cs @@ -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++) diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs index 1b9c879..b19ff7a 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImageTests.cs @@ -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); diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BindingLayoutTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/BindingLayoutTests.cs index 5e0669f..165bb08 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/BindingLayoutTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/BindingLayoutTests.cs @@ -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); diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceTestProgram.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceTestProgram.cs index 5883292..a04a707 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceTestProgram.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceTestProgram.cs @@ -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, diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceTrackerTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceTrackerTests.cs index 67a35a0..43b8724 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceTrackerTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceTrackerTests.cs @@ -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(() => 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] diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ScalarValueGraphTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ScalarValueGraphTests.cs index 169b610..094e602 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ScalarValueGraphTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ScalarValueGraphTests.cs @@ -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() { diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs index bd5dbbe..e9dc8cc 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/SpirvBindingDeclarationTests.cs @@ -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() {