// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; using SharpEmu.Libs.Kernel; using SharpEmu.Libs.VideoOut; using System.Buffers.Binary; using System.Runtime.CompilerServices; namespace SharpEmu.Libs.Agc; public static class AgcExports { private const uint ShaderFileHeader = 0x34333231; private const uint ShaderVersion = 0x18; private const uint ItNop = 0x10; private const uint ItSetBase = 0x11; private const uint ItIndexBufferSize = 0x13; private const uint ItIndexBase = 0x26; private const uint ItDrawIndirect = 0x24; private const uint ItDrawIndexIndirect = 0x25; private const uint ItDrawIndex2 = 0x27; private const uint ItIndexType = 0x2A; private const uint ItDrawIndexAuto = 0x2D; private const uint ItNumInstances = 0x2F; private const uint ItDrawIndexMultiAuto = 0x30; private const uint ItDrawIndexOffset2 = 0x35; private const uint ItWriteData = 0x37; private const uint ItDispatchDirect = 0x15; private const uint ItDispatchIndirect = 0x16; private const uint ItWaitRegMem = 0x3C; private const uint ItIndirectBuffer = 0x3F; private const uint ItEventWrite = 0x46; private const uint ItDmaData = 0x50; private const uint ItSetContextReg = 0x69; private const uint ItSetShReg = 0x76; private const uint ItSetUconfigReg = 0x79; private const uint ItGetLodStats = 0x8E; private const uint RZero = 0x00; private const uint RDrawIndexAuto = 0x04; private const uint RDrawReset = 0x05; private const uint RWaitFlipDone = 0x06; private const uint RAcbReset = 0x09; private const uint RWaitMem32 = 0x0A; private const uint RPushMarker = 0x0B; private const uint RPopMarker = 0x0C; private const uint RShRegsIndirect = 0x11; private const uint RCxRegsIndirect = 0x12; private const uint RUcRegsIndirect = 0x13; private const uint RAcquireMem = 0x14; private const uint RWriteData = 0x15; private const uint RWaitMem64 = 0x16; private const uint RFlip = 0x17; private const uint RReleaseMem = 0x18; private const uint RDmaData = 0x19; private const uint SpiShaderPgmLoPs = 0x8; private const uint SpiShaderPgmHiPs = 0x9; private const uint SpiShaderPgmLoEs = 0xC8; private const uint SpiShaderPgmHiEs = 0xC9; private const uint SpiShaderPgmLoLs = 0x148; private const uint SpiShaderPgmHiLs = 0x149; private const uint SpiShaderPgmLoGs = 0x8A; private const uint SpiShaderPgmHiGs = 0x8B; private const uint SpiPsInputEna = 0x1B3; private const uint SpiPsInputAddr = 0x1B4; private const uint ComputePgmLo = 0x20C; private const uint ComputePgmHi = 0x20D; private const uint ComputePgmRsrc2 = 0x213; private const uint ComputeNumThreadX = 0x207; private const uint ComputeNumThreadY = 0x208; private const uint ComputeNumThreadZ = 0x209; private const uint SpiPsInputCntl0 = 0x191; private const uint VgtPrimitiveType = 0x242; private const uint PaScScreenScissorTl = 0x0C; private const uint PaScScreenScissorBr = 0x0D; private const uint CbTargetMask = 0x8E; private const uint PaScWindowOffset = 0x80; private const uint PaScWindowScissorTl = 0x81; private const uint PaScWindowScissorBr = 0x82; private const uint PaScGenericScissorTl = 0x90; private const uint PaScGenericScissorBr = 0x91; private const uint PaScVportScissor0Tl = 0x94; private const uint PaScVportScissor0Br = 0x95; private const uint PaClVportXScale = 0x10F; private const uint PaClVportXOffset = 0x110; private const uint PaClVportYScale = 0x111; private const uint PaClVportYOffset = 0x112; private const uint PaScVportZMin0 = 0xB4; private const uint PaScVportZMax0 = 0xB5; private const uint CbBlendRed = 0x105; private const uint CbBlendGreen = 0x106; private const uint CbBlendBlue = 0x107; private const uint CbBlendAlpha = 0x108; private const uint CbColorControl = 0x202; private const uint CbColor0Base = 0x318; private const uint CbColorRegisterStride = 15; private const uint CbColor0Info = 0x31C; private const uint CbColor0BaseExt = 0x390; private const uint CbColor0Attrib2 = 0x3B0; private const uint CbColor0Attrib3 = 0x3B8; private const uint CbBlend0Control = 0x1E0; private const uint PaScModeCntl0 = 0x292; private const int ColorTargetCount = 8; private const uint PsTextureUserDataRegister = 0xC; private const uint VsUserDataRegister = 0x4C; private const uint GsUserDataRegister = 0x8C; private const uint EsUserDataRegister = 0xCC; private const uint ComputeUserDataRegister = 0x240; private const uint NggUserDataScalarRegisterBase = 8; private const uint Gen5TextureFormatR8G8B8A8Unorm = 56; private const uint Gen5TextureFormatR16G16B16A16Float = 71; private const uint Gen5TextureType2D = 9; private const ulong MaxPresentedTextureBytes = 128UL * 1024UL * 1024UL; private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000; private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200; private const ulong VideoOutPixelFormatB8G8R8A8Unorm = 0x8100000000000000; private const ulong VideoOutPixelFormatR8G8B8A8Unorm = 0x8100000022000000; private const uint RegisterDefaultsVersion7 = 7; private const uint RegisterDefaultsVersion8 = 8; private const uint RegisterDefaultsVersion10 = 10; private const uint RegisterDefaultsVersion13 = 13; private const int RegisterDefaultsSize = 0x40; private const int RegisterDefaultBlockSize = 16 * 8; private const ulong ResourceRegistrationBytesPerResource = 0x118; private const ulong ResourceRegistrationBytesPerOwner = 0x1E0; private const int ResourceRegistrationMaxNameLength = 256; private const ulong ShaderUserDataOffset = 0x08; private const ulong ShaderCodeOffset = 0x10; private const ulong ShaderCxRegistersOffset = 0x18; private const ulong ShaderShRegistersOffset = 0x20; private const ulong ShaderSpecialsOffset = 0x28; private const ulong ShaderInputSemanticsOffset = 0x30; private const ulong ShaderOutputSemanticsOffset = 0x38; private const ulong ShaderNumInputSemanticsOffset = 0x50; private const ulong ShaderNumOutputSemanticsOffset = 0x56; private const ulong ShaderTypeOffset = 0x5A; private const ulong ShaderNumShRegistersOffset = 0x5C; private const ulong CommandBufferCursorUpOffset = 0x10; private const ulong CommandBufferCursorDownOffset = 0x18; private const ulong CommandBufferCallbackOffset = 0x20; private const ulong CommandBufferReservedDwOffset = 0x30; private const ulong ShaderSpecialGeCntlOffset = 0x00; private const ulong ShaderSpecialVgtShaderStagesEnOffset = 0x08; private const ulong ShaderSpecialVgtGsOutPrimTypeOffset = 0x20; private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28; private const uint CbSetShRegisterRangeMarker = 0x6875000D; private static readonly object _submitTraceGate = new(); private static readonly object _textureHashTraceGate = new(); private static readonly HashSet _tracedDcbSizes = new(); private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new(); private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new(); private static readonly HashSet<(ulong Es, ulong Ps, ulong Target, ulong Texture, uint VertexCount)> _tracedShaderDraws = new(); private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new(); private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new(); private static readonly HashSet _tracedComputeShaders = new(); private static readonly Dictionary<(ulong Address, uint Width, uint Height), ulong> _tracedTextureHashes = []; private static readonly HashSet _tracedSubmittedDrawOpcodes = new(); private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new(); private static readonly Dictionary< (ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes), (byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new(); private static readonly Dictionary< (ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ), byte[]> _computeSpirvCache = new(); private static readonly Dictionary _shaderHeadersByCode = new(); private static readonly bool _traceAgc = string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal); private static readonly bool _traceAgcShader = _traceAgc || string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"), "1", StringComparison.Ordinal); private static readonly bool _traceTextureHashes = string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_HASHES"), "1", StringComparison.Ordinal); private static long _dcbWriteDataTraceCount; private static long _dcbWaitRegMemTraceCount; private static long _createShaderTraceCount; private static long _packetPayloadTraceCount; private static bool _tracedMissingPixelShaderBindings; private static long _shaderTranslationMissTraceCount; private static long _translatedDrawTraceCount; private static long _standardDmaTraceCount; private static readonly object _softwarePresenterGate = new(); private static readonly Dictionary<(ulong Source, ulong Destination), ulong> _softwarePresenterFingerprints = new(); private static readonly Dictionary<(ulong Shader, ulong Source, ulong Destination), ulong> _softwareComputeBlitFingerprints = new(); private static readonly object _registerDefaultsGate = new(); private static readonly ConditionalWeakTable _registerDefaultsAllocations = new(); private static readonly ConditionalWeakTable _submittedGpuStates = new(); private static readonly RegisterDefaultGroup[] PrimaryRegisterDefaults = [ new(0, 0, 0xE24F806D, [new(CbColorControl, 0x00CC0010)]), new(0, 3, 0x0BC65DA4, [new(0x08F, 0)]), new(0, 4, 0x9E5AD592, [new(0x08E, 0)]), new(0, 12, 0x6DE4C312, [new(0x203, 0)]), new(0, 28, 0x1EB8D73A, [new(PaScModeCntl0, 0x00000002)]), new(0, 31, 0xA20EFC70, [new(PaScWindowOffset, 0)]), new(0, 58, 0x43FBD769, [ new(CbBlendRed, 0), new(CbBlendBlue, 0), new(CbBlendGreen, 0), new(CbBlendAlpha, 0), ]), new(0, 59, 0xEF550356, [new(CbBlend0Control, 0x20010001)]), new(0, 67, 0x918106BB, [ new(PaScGenericScissorTl, 0x80000000), new(PaScGenericScissorBr, 0x40004000), ]), new(0, 72, 0x38E92C91, [ new(0x318, 0), new(0x31B, 0), new(0x31C, 0), new(0x31D, 0), new(0x31E, 0x48), new(0x31F, 0), new(0x321, 0), new(0x323, 0), new(0x324, 0), new(0x325, 0), new(0x390, 0), new(0x398, 0), new(0x3A0, 0), new(0x3A8, 0), new(0x3B0, 0), new(0x3B8, 0x0006C000), ]), new(0, 73, 0x0B177B43, [new(0x00C, 0), new(0x00D, 0x40004000)]), new(0, 74, 0x48531062, [new(0x191, 0)]), new(0, 76, 0x7690AF6F, [ new(0x10F, 0x4E7E0000), new(0x111, 0x4E7E0000), new(0x113, 0x4E7E0000), new(0x110, 0), new(0x112, 0), new(0x114, 0), new(0x094, 0x80000000), new(0x095, 0x40004000), new(0x0B4, 0), new(0x0B5, 0), ]), new(0, 77, 0x078D7060, [ new(PaScWindowScissorTl, 0x80000000), new(PaScWindowScissorBr, 0x40004000), ]), new(1, 13, 0xC918DF3E, [new(0x20C, 0), new(0x20D, 0)]), new(1, 14, 0xC9751C9C, [new(0x0C8, 0), new(0x0C9, 0)]), new(1, 18, 0xC9E01B31, [new(0x008, 0), new(0x009, 0)]), new(2, 3, 0x105971C2, [new(0x25B, 0)]), new(2, 7, 0x40D49AD1, [new(0x262, 0)]), new(2, 12, 0x9EBFAB10, [new(0x242, 0)]), ]; private static readonly RegisterDefaultGroup[] InternalRegisterDefaults = [ new(0, 0, 0x8FB4EDB5, [new(0x00E, 0)]), new(0, 1, 0xB994AD29, [new(0x2AF, 0)]), new(0, 2, 0xD427322F, [new(0x314, 0)]), new(0, 3, 0xF58FEA31, [new(0x1B5, 0)]), new(1, 0, 0x6AC156EF, [new(0x216, 0)]), new(1, 1, 0x6AC15610, [new(0x217, 0)]), new(1, 2, 0x6AC15009, [new(0x219, 0)]), new(1, 3, 0x6AC153BA, [new(0x21A, 0)]), new(1, 4, 0xBE7DCD73, [new(0x27D, 0)]), new(1, 5, 0x0C4B1438, [new(0x22A, 0)]), new(1, 6, 0xDB00D71A, [new(0x204, 0)]), new(1, 7, 0xDB00D249, [new(0x205, 0)]), new(1, 8, 0xDB00EC60, [new(0x206, 0)]), new(1, 9, 0x0C4D6FE4, [new(0x080, 0)]), new(1, 10, 0x0C4A80EF, [new(0x100, 0)]), new(1, 11, 0x0DD283E7, [new(0x006, 0)]), new(1, 12, 0xC620E68C, [new(0x081, 0)]), new(1, 13, 0xC67EFACF, [new(0x101, 0)]), new(1, 14, 0xD9E6D9F7, [new(0x001, 0)]), new(2, 0, 0x31F34B9F, [new(0x24F, 0)]), new(2, 1, 0xAC0F9E76, [new(0x80003FFF, 0)]), new(2, 2, 0x929FD95D, [new(0x250, 0)]), ]; private readonly record struct TextureDescriptor( ulong Address, uint Width, uint Height, uint Format, uint NumberType, uint TileMode, uint Type, uint BaseLevel, uint LastLevel, uint Pitch, uint DstSelect) { public uint MipLevels { get { var largestDimension = Math.Max(Width, Height); uint maximumMipLevels = 1; while (largestDimension > 1) { largestDimension >>= 1; maximumMipLevels++; } var descriptorMipLevels = LastLevel >= BaseLevel ? LastLevel - BaseLevel + 1 : 1; return Math.Min(descriptorMipLevels, maximumMipLevels); } } } private readonly record struct RenderTargetDescriptor( uint Slot, ulong Address, uint Width, uint Height, uint Format, uint NumberType, uint TileMode); private sealed record TranslatedGuestDraw( ulong ExportShaderAddress, ulong PixelShaderAddress, uint PrimitiveType, byte[] VertexSpirv, byte[] PixelSpirv, uint AttributeCount, uint VertexCount, uint InstanceCount, VulkanGuestIndexBuffer? IndexBuffer, IReadOnlyList Textures, IReadOnlyList GlobalMemoryBindings, IReadOnlyList VertexInputs, IReadOnlyList RenderTargets, VulkanGuestRenderState RenderState); private sealed record TranslatedImageBinding( TextureDescriptor Descriptor, bool IsStorage, uint MipLevel, IReadOnlyList SamplerDescriptor); private readonly record struct RenderTargetWriter( ulong Sequence, ulong ExportShaderAddress, ulong PixelShaderAddress, uint VertexCount, uint PrimitiveType); private readonly record struct ComputeImageWriter( ulong Sequence, ulong ShaderAddress, string Opcode); private readonly record struct ComputeDispatch( uint GroupCountX, uint GroupCountY, uint GroupCountZ); private sealed class SubmittedDcbState { public Dictionary CxRegisters { get; } = new(); public Dictionary ShRegisters { get; } = new(); public Dictionary UcRegisters { get; } = new(); public TextureDescriptor? PresenterTexture { get; set; } public GuestDrawKind GuestDrawKind { get; set; } public TranslatedGuestDraw? TranslatedDraw { get; set; } public Dictionary RenderTargetWriters { get; } = new(); public ulong IndirectArgsAddress { get; set; } public bool SawIndexedDraw { get; set; } public ulong IndexBufferAddress { get; set; } public uint IndexBufferCount { get; set; } public uint IndexSize { get; set; } public uint InstanceCount { get; set; } = 1; public uint DrawIndexOffset { get; set; } } private sealed class SubmittedGpuState { public object Gate { get; } = new(); public SubmittedDcbState Graphics { get; } = new(); public Dictionary ComputeQueues { get; } = new(); public Dictionary ComputeImageWriters { get; } = new(); public Dictionary ResourceOwners { get; } = new(); public Dictionary RegisteredResources { get; } = new(); public bool ResourceRegistrationInitialized { get; set; } public ulong ResourceRegistrationMemory { get; set; } public ulong ResourceRegistrationMemorySize { get; set; } public uint ResourceRegistrationMaxOwners { get; set; } public uint DefaultOwner { get; set; } = DefaultAgcOwner; public uint NextOwner { get; set; } = 1; public uint NextResource { get; set; } = 1; public ulong WorkSequence { get; set; } } private readonly record struct RegisteredAgcResource( uint Owner, ulong Address, ulong Size, string Name, uint Type, uint Flags); private readonly record struct RegisterDefaultValue(uint Offset, uint Value); private readonly record struct RegisterDefaultGroup( uint Space, uint Index, uint Type, RegisterDefaultValue[] Registers); private sealed record RegisterDefaultsAllocation(ulong Primary, ulong Internal); [SysAbiExport( Nid = "23LRUSvYu1M", ExportName = "sceAgcInit", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int Init(CpuContext ctx) { var stateAddress = ctx[CpuRegister.Rdi]; var version = (uint)ctx[CpuRegister.Rsi]; if (stateAddress == 0 || !IsSupportedRegisterDefaultsVersion(version)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "2JtWUUiYBXs", ExportName = "sceAgcGetRegisterDefaults2", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int GetRegisterDefaults2(CpuContext ctx) => ReturnRegisterDefaults(ctx, internalDefaults: false); [SysAbiExport( Nid = "wRbq6ZjNop4", ExportName = "sceAgcGetRegisterDefaults2Internal", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int GetRegisterDefaults2Internal(CpuContext ctx) => ReturnRegisterDefaults(ctx, internalDefaults: true); [SysAbiExport( Nid = "f3dg2CSgRKY", ExportName = "sceAgcCreateShader", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int CreateShader(CpuContext ctx) { var destinationAddress = ctx[CpuRegister.Rdi]; var headerAddress = ctx[CpuRegister.Rsi]; var codeAddress = ctx[CpuRegister.Rdx]; if (headerAddress == 0 || codeAddress == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!ctx.TryReadUInt32(headerAddress, out var fileHeader) || !ctx.TryReadUInt32(headerAddress + sizeof(uint), out var version)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } if (fileHeader != ShaderFileHeader || version != ShaderVersion) { TraceCreateShader(destinationAddress, headerAddress, codeAddress, $"invalid-header file=0x{fileHeader:X8} version=0x{version:X8}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!RelocatePointerField(ctx, headerAddress + ShaderCxRegistersOffset) || !RelocatePointerField(ctx, headerAddress + ShaderShRegistersOffset) || !RelocatePointerField(ctx, headerAddress + ShaderUserDataOffset) || !RelocatePointerField(ctx, headerAddress + ShaderSpecialsOffset) || !RelocatePointerField(ctx, headerAddress + ShaderInputSemanticsOffset) || !RelocatePointerField(ctx, headerAddress + ShaderOutputSemanticsOffset) || !ctx.TryWriteUInt64(headerAddress + ShaderCodeOffset, codeAddress)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } if (!ctx.TryReadUInt64(headerAddress + ShaderUserDataOffset, out var userDataAddress)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } if (userDataAddress != 0 && (!RelocatePointerField(ctx, userDataAddress) || !RelocatePointerField(ctx, userDataAddress + 0x08) || !RelocatePointerField(ctx, userDataAddress + 0x10) || !RelocatePointerField(ctx, userDataAddress + 0x18) || !RelocatePointerField(ctx, userDataAddress + 0x20))) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } if (!PatchShaderProgramRegisters(ctx, headerAddress, codeAddress)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (destinationAddress != 0 && !ctx.TryWriteUInt64(destinationAddress, headerAddress)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } lock (_submitTraceGate) { _shaderHeadersByCode[codeAddress] = headerAddress; } TraceCreateShader(destinationAddress, headerAddress, codeAddress, "ok"); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "vcmNN+AAXnY", ExportName = "sceAgcSetCxRegIndirectPatchSetAddress", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int SetCxRegIndirectPatchSetAddress(CpuContext ctx) => SetIndirectPatchAddress(ctx, "cx"); [SysAbiExport( Nid = "Qrj4c+61z4A", ExportName = "sceAgcSetShRegIndirectPatchSetAddress", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int SetShRegIndirectPatchSetAddress(CpuContext ctx) => SetIndirectPatchAddress(ctx, "sh"); [SysAbiExport( Nid = "6lNcCp+fxi4", ExportName = "sceAgcSetUcRegIndirectPatchSetAddress", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int SetUcRegIndirectPatchSetAddress(CpuContext ctx) => SetIndirectPatchAddress(ctx, "uc"); [SysAbiExport( Nid = "d-6uF9sZDIU", ExportName = "sceAgcSetCxRegIndirectPatchAddRegisters", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int SetCxRegIndirectPatchAddRegisters(CpuContext ctx) => AddIndirectPatchRegisters(ctx, "cx"); [SysAbiExport( Nid = "z2duB-hHQSM", ExportName = "sceAgcSetShRegIndirectPatchAddRegisters", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int SetShRegIndirectPatchAddRegisters(CpuContext ctx) => AddIndirectPatchRegisters(ctx, "sh"); [SysAbiExport( Nid = "vRoArM9zaIk", ExportName = "sceAgcSetUcRegIndirectPatchAddRegisters", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int SetUcRegIndirectPatchAddRegisters(CpuContext ctx) => AddIndirectPatchRegisters(ctx, "uc"); [SysAbiExport( Nid = "D9sr1xGUriE", ExportName = "sceAgcCreatePrimState", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int CreatePrimState(CpuContext ctx) { var cxRegistersAddress = ctx[CpuRegister.Rdi]; var ucRegistersAddress = ctx[CpuRegister.Rsi]; var hullShaderAddress = ctx[CpuRegister.Rdx]; var geometryShaderAddress = ctx[CpuRegister.Rcx]; var primitiveType = (uint)ctx[CpuRegister.R8]; if (cxRegistersAddress == 0 || ucRegistersAddress == 0 || hullShaderAddress != 0 || geometryShaderAddress == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!ctx.TryReadByte(geometryShaderAddress + ShaderTypeOffset, out var shaderType) || !IsEsGeometryShaderType(shaderType) || !ctx.TryReadUInt64(geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) || specialsAddress == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!CopyShaderRegister(ctx, specialsAddress + ShaderSpecialVgtShaderStagesEnOffset, cxRegistersAddress) || !CopyShaderRegister(ctx, specialsAddress + ShaderSpecialVgtGsOutPrimTypeOffset, cxRegistersAddress + 8) || !CopyShaderRegister(ctx, specialsAddress + ShaderSpecialGeCntlOffset, ucRegistersAddress) || !CopyShaderRegister(ctx, specialsAddress + ShaderSpecialGeUserVgprEnOffset, ucRegistersAddress + 8) || !ctx.TryWriteUInt32(ucRegistersAddress + 16, VgtPrimitiveType) || !ctx.TryWriteUInt32(ucRegistersAddress + 20, primitiveType)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} type={shaderType} prim=0x{primitiveType:X8}"); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "HV4j+E0MBHE", ExportName = "sceAgcCreateInterpolantMapping", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int CreateInterpolantMapping(CpuContext ctx) { var registersAddress = ctx[CpuRegister.Rdi]; var geometryShaderAddress = ctx[CpuRegister.Rsi]; var pixelShaderAddress = ctx[CpuRegister.Rdx]; if (registersAddress == 0 || geometryShaderAddress == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!ctx.TryReadUInt64(geometryShaderAddress + ShaderOutputSemanticsOffset, out var outputSemanticsAddress) || !ctx.TryReadUInt32(geometryShaderAddress + ShaderNumOutputSemanticsOffset, out var outputSemanticsCount)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } ulong inputSemanticsAddress = 0; if (pixelShaderAddress != 0 && (!ctx.TryReadUInt64(pixelShaderAddress + ShaderInputSemanticsOffset, out inputSemanticsAddress) || !ctx.TryReadUInt32(pixelShaderAddress + ShaderNumInputSemanticsOffset, out _))) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } for (uint i = 0; i < 32; i++) { uint value = 0; if (i < outputSemanticsCount && outputSemanticsAddress != 0) { var flat = false; if (pixelShaderAddress != 0 && inputSemanticsAddress != 0 && ctx.TryReadUInt32(inputSemanticsAddress + (i * sizeof(uint)), out var inputSemantic)) { flat = ((inputSemantic >> 22) & 0x1) != 0; } value = i | (flat ? 0x400u : 0u); } var destination = registersAddress + (i * 8); if (!ctx.TryWriteUInt32(destination, SpiPsInputCntl0 + i) || !ctx.TryWriteUInt32(destination + sizeof(uint), value)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } } TraceAgc($"agc.create_interpolant_mapping regs=0x{registersAddress:X16} gs=0x{geometryShaderAddress:X16} ps=0x{pixelShaderAddress:X16}"); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "V++UgBtQhn0", ExportName = "sceAgcGetDataPacketPayloadAddress", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int GetDataPacketPayloadAddress(CpuContext ctx) { var outputAddress = ctx[CpuRegister.Rdi]; var commandAddress = ctx[CpuRegister.Rsi]; var type = (int)ctx[CpuRegister.Rdx]; if (outputAddress == 0 || commandAddress == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } var payloadAddress = commandAddress + 8; if (type == 0) { if (!ctx.TryReadUInt32(commandAddress, out var header)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } payloadAddress = (header & 0x3FFF_0000u) == 0x3FFF_0000u ? 0 : commandAddress + 4; } if (!ctx.TryWriteUInt64(outputAddress, payloadAddress)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } if (ShouldTraceHotPath(ref _packetPayloadTraceCount)) { TraceAgc( $"agc.get_packet_payload out=0x{outputAddress:X16} cmd=0x{commandAddress:X16} " + $"type={type} payload=0x{payloadAddress:X16}"); } ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "LtTouSCZjHM", ExportName = "sceAgcCbNop", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int CbNop(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var dwordCount = (uint)ctx[CpuRegister.Rsi]; if (commandBufferAddress == 0 || dwordCount < 2 || dwordCount > 0x4001) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, dwordCount, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(dwordCount, ItNop, RZero))) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } for (uint index = 1; index < dwordCount; index++) { if (!ctx.TryWriteUInt32(commandAddress + ((ulong)index * sizeof(uint)), 0)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "k3GhuSNmBLU", ExportName = "sceAgcCbDispatch", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int CbDispatch(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var groupCountX = (uint)ctx[CpuRegister.Rsi]; var groupCountY = (uint)ctx[CpuRegister.Rdx]; var groupCountZ = (uint)ctx[CpuRegister.Rcx]; var modifier = (uint)ctx[CpuRegister.R8]; if (commandBufferAddress == 0 || !TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(5, ItDispatchDirect, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, groupCountX) || !ctx.TryWriteUInt32(commandAddress + 8, groupCountY) || !ctx.TryWriteUInt32(commandAddress + 12, groupCountZ) || !ctx.TryWriteUInt32(commandAddress + 16, (modifier & 0xA038u) | 0x41u)) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "UZbQjYAwwXM", ExportName = "sceAgcCbSetShRegistersDirect", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int CbSetShRegistersDirect(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var registersAddress = ctx[CpuRegister.Rsi]; var registerCount = (uint)ctx[CpuRegister.Rdx]; if (registerCount == 0) { return ReturnPointer(ctx, 0); } if (commandBufferAddress == 0 || registersAddress == 0 || registerCount > 4096) { return ReturnPointer(ctx, 0); } var registers = new RegisterDefaultValue[registerCount]; for (uint index = 0; index < registerCount; index++) { var entryAddress = registersAddress + ((ulong)index * 8); if (!ctx.TryReadUInt32(entryAddress, out var offset) || !ctx.TryReadUInt32(entryAddress + sizeof(uint), out var value)) { return ReturnPointer(ctx, 0); } registers[index] = new RegisterDefaultValue(offset, value); } Array.Sort(registers, static (left, right) => left.Offset.CompareTo(right.Offset)); ulong firstCommandAddress = 0; var startIndex = 0; while (startIndex < registers.Length) { var endIndex = startIndex + 1; while (endIndex < registers.Length && registers[endIndex].Offset == registers[endIndex - 1].Offset + 1) { endIndex++; } var valueCount = (uint)(endIndex - startIndex); var packetDwords = valueCount + 2; if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(packetDwords, ItSetShReg, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, registers[startIndex].Offset & 0xFFFFu)) { return ReturnPointer(ctx, 0); } firstCommandAddress = firstCommandAddress == 0 ? commandAddress : firstCommandAddress; for (var index = startIndex; index < endIndex; index++) { if (!ctx.TryWriteUInt32( commandAddress + 8 + ((ulong)(index - startIndex) * sizeof(uint)), registers[index].Value)) { return ReturnPointer(ctx, 0); } } startIndex = endIndex; } return ReturnPointer(ctx, firstCommandAddress); } [SysAbiExport( Nid = "JrtiDtKeS38", ExportName = "sceAgcAcbResetQueue", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int AcbResetQueue(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; if (commandBufferAddress == 0 || !TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(2, ItNop, RAcbReset)) || !ctx.TryWriteUInt32(commandAddress + 4, 0)) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "cFazmnXpJOE", ExportName = "sceAgcAcbEventWrite", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int AcbEventWrite(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var eventType = (uint)(ctx[CpuRegister.Rsi] & 0xFF); var eventAddress = ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0 || eventType >= 0x40) { return ReturnPointer(ctx, 0); } var hasAddress = (eventType & ~1u) == 0x38; var packetDwords = hasAddress ? 4u : 2u; if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(packetDwords, ItEventWrite, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, hasAddress ? eventType | 0x100u : eventType & 0x3Fu)) { return ReturnPointer(ctx, 0); } if (hasAddress && (!ctx.TryWriteUInt32(commandAddress + 8, (uint)eventAddress & ~7u) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)(eventAddress >> 32)))) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "KT-hTp-Ch14", ExportName = "sceAgcAcbAcquireMem", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int AcbAcquireMem(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var gcrControl = (uint)ctx[CpuRegister.Rsi]; var baseAddress = ctx[CpuRegister.Rdx]; var sizeBytes = ctx[CpuRegister.Rcx]; var pollCycles = (uint)ctx[CpuRegister.R8]; var noSize = sizeBytes == ulong.MaxValue; if (commandBufferAddress == 0 || (!noSize && (sizeBytes & 0xFF) != 0) || (!noSize && (sizeBytes >> 40) != 0) || (baseAddress & 0xFF) != 0 || (baseAddress >> 40) != 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(8, ItNop, RAcquireMem)) || !ctx.TryWriteUInt32(commandAddress + 4, 0x8000_0000u) || !ctx.TryWriteUInt32(commandAddress + 8, noSize ? 0 : (uint)(sizeBytes >> 8)) || !ctx.TryWriteUInt32(commandAddress + 12, 0) || !ctx.TryWriteUInt32(commandAddress + 16, (uint)(baseAddress >> 8)) || !ctx.TryWriteUInt32(commandAddress + 20, 0) || !ctx.TryWriteUInt32(commandAddress + 24, pollCycles / 40) || !ctx.TryWriteUInt32(commandAddress + 28, gcrControl)) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "htn36gPnBk4", ExportName = "sceAgcAcbWaitRegMem", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int AcbWaitRegMem(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var size = (uint)(ctx[CpuRegister.Rsi] & 0xFF); var compareFunction = (uint)(ctx[CpuRegister.Rdx] & 0xFF); var cachePolicy = (uint)(ctx[CpuRegister.Rcx] & 0xFF); var address = ctx[CpuRegister.R8]; var reference = ctx[CpuRegister.R9]; var stackAddress = ctx[CpuRegister.Rsp]; if (!ctx.TryReadUInt64(stackAddress + sizeof(ulong), out var mask) || !ctx.TryReadUInt32(stackAddress + (2 * sizeof(ulong)), out var pollCycles) || commandBufferAddress == 0 || size > 1 || compareFunction > 7 || cachePolicy > 3) { return ReturnPointer(ctx, 0); } var packetDwords = size == 0 ? 6u : 9u; var packetRegister = size == 0 ? RWaitMem32 : RWaitMem64; if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(packetDwords, ItNop, packetRegister)) || !ctx.TryWriteUInt32(commandAddress + 4, (uint)address) || !ctx.TryWriteUInt32(commandAddress + 8, (uint)(address >> 32)) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)mask)) { return ReturnPointer(ctx, 0); } if (size == 0) { if (!ctx.TryWriteUInt32(commandAddress + 16, compareFunction) || !ctx.TryWriteUInt32(commandAddress + 20, (uint)reference)) { return ReturnPointer(ctx, 0); } } else if (!ctx.TryWriteUInt32(commandAddress + 16, (uint)(mask >> 32)) || !ctx.TryWriteUInt32(commandAddress + 20, (uint)reference) || !ctx.TryWriteUInt32(commandAddress + 24, (uint)(reference >> 32)) || !ctx.TryWriteUInt32(commandAddress + 28, compareFunction) || !ctx.TryWriteUInt32(commandAddress + 32, pollCycles / 40)) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "eZ4+17OQz4Q", ExportName = "sceAgcAcbWriteData", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int AcbWriteData(CpuContext ctx) => DcbWriteData(ctx); [SysAbiExport( Nid = "j3EtxFkSIhQ", ExportName = "sceAgcAcbDispatchIndirect", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int AcbDispatchIndirect(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var argumentsAddress = ctx[CpuRegister.Rsi]; var modifier = (uint)ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0 || !TryAllocateCommandDwords(ctx, commandBufferAddress, 4, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(4, ItDispatchIndirect, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, (uint)argumentsAddress) || !ctx.TryWriteUInt32(commandAddress + 8, (uint)(argumentsAddress >> 32)) || !ctx.TryWriteUInt32(commandAddress + 12, (modifier & 0xA038u) | 0x41u)) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "n2fD4A+pb+g", ExportName = "sceAgcCbSetShRegisterRangeDirect", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int CbSetShRegisterRangeDirect(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var offset = (uint)ctx[CpuRegister.Rsi]; var valuesAddress = ctx[CpuRegister.Rdx]; var valueCount = (uint)ctx[CpuRegister.Rcx]; if (commandBufferAddress == 0 || offset == 0 || offset > 0x3FF || valueCount == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var markerAddress) || !ctx.TryWriteUInt32(markerAddress, Pm4(2, ItNop, RZero)) || !ctx.TryWriteUInt32(markerAddress + 4, CbSetShRegisterRangeMarker) || !TryAllocateCommandDwords(ctx, commandBufferAddress, valueCount + 2, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(valueCount + 2, ItSetShReg, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, offset)) { return ReturnPointer(ctx, 0); } for (uint i = 0; i < valueCount; i++) { var value = 0u; if (valuesAddress != 0 && !ctx.TryReadUInt32(valuesAddress + (i * sizeof(uint)), out value)) { return ReturnPointer(ctx, 0); } if (!ctx.TryWriteUInt32(commandAddress + 8 + (i * sizeof(uint)), value)) { return ReturnPointer(ctx, 0); } } TraceAgc($"agc.cb_set_sh_range buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} offset=0x{offset:X8} count={valueCount}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "wr23dPKyWc0", ExportName = "sceAgcCbReleaseMem", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int CbReleaseMem(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var action = (uint)(ctx[CpuRegister.Rsi] & 0xFF); var gcrControl = (uint)(ctx[CpuRegister.Rdx] & 0xFFFF); var destination = (uint)(ctx[CpuRegister.Rcx] & 0xFF); var cachePolicy = (uint)(ctx[CpuRegister.R8] & 0xFF); var destinationAddress = ctx[CpuRegister.R9]; var stackAddress = ctx[CpuRegister.Rsp]; if (!ctx.TryReadUInt64(stackAddress + 8, out var dataSelectionRaw) || !ctx.TryReadUInt64(stackAddress + 16, out var data) || !ctx.TryReadUInt64(stackAddress + 24, out var gdsOffsetRaw) || !ctx.TryReadUInt64(stackAddress + 32, out var gdsSizeRaw) || !ctx.TryReadUInt64(stackAddress + 40, out var interruptRaw) || !ctx.TryReadUInt64(stackAddress + 48, out var interruptContextIdRaw)) { return ReturnPointer(ctx, 0); } var dataSelection = (uint)(dataSelectionRaw & 0xFF); var gdsOffset = (uint)(gdsOffsetRaw & 0xFFFF); var gdsSize = (uint)(gdsSizeRaw & 0xFFFF); var interrupt = (uint)(interruptRaw & 0xFF); var interruptContextId = (uint)interruptContextIdRaw; if (commandBufferAddress == 0 || destination > 1 || dataSelection > 3 || gdsOffset != 0 || gdsSize > 2 || interrupt > 3) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(8, ItNop, RReleaseMem)) || !ctx.TryWriteUInt32(commandAddress + 4, action | (cachePolicy << 8)) || !ctx.TryWriteUInt32( commandAddress + 8, gcrControl | (dataSelection << 16) | (interrupt << 24)) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)destinationAddress) || !ctx.TryWriteUInt32(commandAddress + 16, (uint)(destinationAddress >> 32)) || !ctx.TryWriteUInt32(commandAddress + 20, (uint)data) || !ctx.TryWriteUInt32(commandAddress + 24, (uint)(data >> 32)) || !ctx.TryWriteUInt32(commandAddress + 28, interruptContextId)) { return ReturnPointer(ctx, 0); } TraceAgc( $"agc.cb_release_mem buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + $"action=0x{action:X2} gcr=0x{gcrControl:X4} dst=0x{destinationAddress:X16} data_sel={dataSelection} data=0x{data:X16}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "TRO721eVt4g", ExportName = "sceAgcDcbResetQueue", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbResetQueue(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var op = (uint)ctx[CpuRegister.Rsi]; var state = (uint)ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0 || op != 0x3FF || state != 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(2, ItNop, RDrawReset)) || !ctx.TryWriteUInt32(commandAddress + 4, 0)) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_reset_queue buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "ZvwO9euwYzc", ExportName = "sceAgcDcbSetCxRegistersIndirect", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetCxRegistersIndirect(CpuContext ctx) => DcbSetRegistersIndirect(ctx, RCxRegsIndirect, "cx"); [SysAbiExport( Nid = "-HOOCn0JY48", ExportName = "sceAgcDcbSetShRegistersIndirect", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetShRegistersIndirect(CpuContext ctx) => DcbSetRegistersIndirect(ctx, RShRegsIndirect, "sh"); [SysAbiExport( Nid = "hvUfkUIQcOE", ExportName = "sceAgcDcbSetUcRegistersIndirect", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetUcRegistersIndirect(CpuContext ctx) => DcbSetRegistersIndirect(ctx, RUcRegsIndirect, "uc"); [SysAbiExport( Nid = "GIIW2J37e70", ExportName = "sceAgcDcbSetIndexSize", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetIndexSize(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var indexSize = (uint)(ctx[CpuRegister.Rsi] & 0xFF); var cachePolicy = (uint)(ctx[CpuRegister.Rdx] & 0xFF); if (commandBufferAddress == 0 || cachePolicy != 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(2, ItIndexType, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, indexSize)) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_set_index_size buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} size={indexSize}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "tSBxhAPyytQ", ExportName = "sceAgcDcbSetNumInstances", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetNumInstances(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var instanceCount = (uint)ctx[CpuRegister.Rsi]; if (commandBufferAddress == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(2, ItNumInstances, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, instanceCount)) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_set_num_instances buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} count={instanceCount}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "q88lQ+GP5Yk", ExportName = "sceAgcDcbDrawIndex", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbDrawIndex(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var indexCount = (uint)ctx[CpuRegister.Rsi]; var indexAddress = ctx[CpuRegister.Rdx]; var modifier = (uint)ctx[CpuRegister.Rcx]; if (commandBufferAddress == 0 || modifier != 0x4000_0000) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var baseCommand) || !ctx.TryWriteUInt32(baseCommand, Pm4(3, ItIndexBase, 0)) || !ctx.TryWriteUInt32(baseCommand + 4, (uint)indexAddress) || !ctx.TryWriteUInt32(baseCommand + 8, (uint)(indexAddress >> 32)) || !ctx.TryWriteUInt32(baseCommand + 12, Pm4(2, ItIndexBufferSize, 0)) || !ctx.TryWriteUInt32(baseCommand + 16, indexCount)) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var drawCommand) || !ctx.TryWriteUInt32(drawCommand, Pm4(5, ItDrawIndex2, 0)) || !ctx.TryWriteUInt32(drawCommand + 4, indexCount) || !ctx.TryWriteUInt32(drawCommand + 8, 0) || !ctx.TryWriteUInt32(drawCommand + 12, 0) || !ctx.TryWriteUInt32(drawCommand + 16, 0)) { return ReturnPointer(ctx, 0); } TraceAgc( $"agc.dcb_draw_index buf=0x{commandBufferAddress:X16} " + $"base=0x{baseCommand:X16} draw=0x{drawCommand:X16} " + $"count={indexCount} index=0x{indexAddress:X16}"); return ReturnPointer(ctx, drawCommand); } [SysAbiExport( Nid = "Yw0jKSqop+E", ExportName = "sceAgcDcbDrawIndexAuto", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbDrawIndexAuto(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var indexCount = (uint)ctx[CpuRegister.Rsi]; var modifier = ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0 || modifier != 0x4000_0000) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) || !ctx.TryWriteUInt32(commandAddress + 4, indexCount) || !ctx.TryWriteUInt32(commandAddress + 8, 0) || !ctx.TryWriteUInt32(commandAddress + 12, 0) || !ctx.TryWriteUInt32(commandAddress + 16, 0) || !ctx.TryWriteUInt32(commandAddress + 20, 0) || !ctx.TryWriteUInt32(commandAddress + 24, 0)) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_draw_index_auto buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} count={indexCount}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "rUuVjyR+Rd4", ExportName = "sceAgcDcbGetLodStatsGetSize", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbGetLodStatsGetSize(CpuContext ctx) { var counterCount = (uint)ctx[CpuRegister.Rdi]; ctx[CpuRegister.Rax] = 0x10u + (counterCount * sizeof(uint)); return (int)ctx[CpuRegister.Rax]; } [SysAbiExport( Nid = "vuSXe69VILM", ExportName = "sceAgcDcbGetLodStats", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbGetLodStats(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var cachePolicy = (uint)ctx[CpuRegister.Rsi] & 0x3u; var destinationAddress = ctx[CpuRegister.Rdx]; var control = (uint)ctx[CpuRegister.Rcx]; var counterMask = (uint)ctx[CpuRegister.R8] & 0xFFu; var resetCounters = (uint)ctx[CpuRegister.R9] & 0x1u; if (!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out var enableRaw) || !ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + (2 * sizeof(ulong)), out var counterSelectRaw) || commandBufferAddress == 0) { return ReturnPointer(ctx, 0); } var enable = (uint)enableRaw & 0x1u; var counterSelect = (uint)counterSelectRaw & 0xFFu; var packetControl = (cachePolicy << 28) | (enable << 19) | (resetCounters << 18) | (counterMask << 10) | (counterSelect << 2); if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(5, ItGetLodStats, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, control) || !ctx.TryWriteUInt32(commandAddress + 8, (uint)destinationAddress & ~0x3Fu) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)(destinationAddress >> 32)) || !ctx.TryWriteUInt32(commandAddress + 16, packetControl)) { return ReturnPointer(ctx, 0); } TraceAgc( $"agc.dcb_get_lod_stats buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + $"dst=0x{destinationAddress:X16} control=0x{control:X8} counters=0x{counterMask:X2}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "aJf+j5yntiU", ExportName = "sceAgcDcbEventWrite", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbEventWrite(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var eventType = (uint)(ctx[CpuRegister.Rsi] & 0xFF); var eventAddress = ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0 || eventType > 0x3F || eventAddress != 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(2, ItEventWrite, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, eventType)) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_event_write buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} type={eventType}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "57labkp+rSQ", ExportName = "sceAgcDcbAcquireMem", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbAcquireMem(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var engine = (uint)(ctx[CpuRegister.Rsi] & 0xFF); var cbDbOp = (uint)ctx[CpuRegister.Rdx]; var gcrControl = (uint)ctx[CpuRegister.Rcx]; var baseAddress = ctx[CpuRegister.R8]; var sizeBytes = ctx[CpuRegister.R9]; if (!ctx.TryReadUInt32(ctx[CpuRegister.Rsp] + sizeof(ulong), out var pollCycles)) { return ReturnPointer(ctx, 0); } var noSize = sizeBytes == ulong.MaxValue; if (commandBufferAddress == 0 || engine > 1 || (!noSize && (sizeBytes & 0xFF) != 0) || (!noSize && (sizeBytes >> 40) != 0) || (baseAddress & 0xFF) != 0 || (baseAddress >> 40) != 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(8, ItNop, RAcquireMem)) || !ctx.TryWriteUInt32(commandAddress + 4, (engine << 31) | cbDbOp) || !ctx.TryWriteUInt32(commandAddress + 8, noSize ? 0 : (uint)(sizeBytes >> 8)) || !ctx.TryWriteUInt32(commandAddress + 12, 0) || !ctx.TryWriteUInt32(commandAddress + 16, (uint)(baseAddress >> 8)) || !ctx.TryWriteUInt32(commandAddress + 20, 0) || !ctx.TryWriteUInt32(commandAddress + 24, pollCycles / 40) || !ctx.TryWriteUInt32(commandAddress + 28, gcrControl)) { return ReturnPointer(ctx, 0); } TraceAgc( $"agc.dcb_acquire_mem buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + $"engine={engine} cbdb=0x{cbDbOp:X8} gcr=0x{gcrControl:X8} base=0x{baseAddress:X16} size=0x{sizeBytes:X16}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "i1jyy49AjXU", ExportName = "sceAgcDcbWriteData", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbWriteData(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var destination = (uint)(ctx[CpuRegister.Rsi] & 0xFF); var cachePolicy = (uint)(ctx[CpuRegister.Rdx] & 0xFF); var destinationAddress = ctx[CpuRegister.Rcx]; var dataAddress = ctx[CpuRegister.R8]; var dwordCount = (uint)ctx[CpuRegister.R9]; var stackAddress = ctx[CpuRegister.Rsp]; if (!ctx.TryReadUInt64(stackAddress + sizeof(ulong), out var incrementRaw) || !ctx.TryReadUInt64(stackAddress + (2 * sizeof(ulong)), out var writeConfirmRaw)) { return ReturnPointer(ctx, 0); } var increment = (uint)(incrementRaw & 0xFF); var writeConfirm = (uint)(writeConfirmRaw & 0xFF); if (commandBufferAddress == 0 || destinationAddress == 0 || dataAddress == 0 || dwordCount > 0x3FFD) { return ReturnPointer(ctx, 0); } var packetDwords = dwordCount + 4; if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(packetDwords, ItNop, RWriteData)) || !ctx.TryWriteUInt32( commandAddress + 4, destination | (cachePolicy << 8) | (increment << 16) | (writeConfirm << 24)) || !ctx.TryWriteUInt32(commandAddress + 8, (uint)destinationAddress) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)(destinationAddress >> 32))) { return ReturnPointer(ctx, 0); } for (uint index = 0; index < dwordCount; index++) { if (!ctx.TryReadUInt32(dataAddress + ((ulong)index * sizeof(uint)), out var value) || !ctx.TryWriteUInt32(commandAddress + 16 + ((ulong)index * sizeof(uint)), value)) { return ReturnPointer(ctx, 0); } } if (ShouldTraceHotPath(ref _dcbWriteDataTraceCount)) { TraceAgc( $"agc.dcb_write_data buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + $"dst={destination} cache={cachePolicy} addr=0x{destinationAddress:X16} count={dwordCount} " + $"increment={increment} confirm={writeConfirm}"); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "VmW0Tdpy420", ExportName = "sceAgcDcbWaitRegMem", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbWaitRegMem(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var size = (uint)(ctx[CpuRegister.Rsi] & 0xFF); var compareFunction = (uint)(ctx[CpuRegister.Rdx] & 0xFF); var operation = (uint)(ctx[CpuRegister.Rcx] & 0xFF); var cachePolicy = (uint)(ctx[CpuRegister.R8] & 0xFF); var address = ctx[CpuRegister.R9]; var stackAddress = ctx[CpuRegister.Rsp]; if (!ctx.TryReadUInt64(stackAddress + sizeof(ulong), out var reference) || !ctx.TryReadUInt64(stackAddress + (2 * sizeof(ulong)), out var mask) || !ctx.TryReadUInt32(stackAddress + (3 * sizeof(ulong)), out var pollCycles)) { return ReturnPointer(ctx, 0); } if (commandBufferAddress == 0 || size > 1 || compareFunction > 7 || operation > 4 || cachePolicy > 3) { return ReturnPointer(ctx, 0); } var standardWait = operation is 2 or 3; var packetDwords = standardWait ? 7u : size == 0 ? 6u : 9u; var packetRegister = size == 0 ? RWaitMem32 : RWaitMem64; if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress)) { return ReturnPointer(ctx, 0); } if (standardWait) { if (!ctx.TryWriteUInt32(commandAddress, Pm4(packetDwords, ItWaitRegMem, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, compareFunction | ((operation & 1) << 8)) || !ctx.TryWriteUInt32(commandAddress + 8, (uint)address) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)(address >> 32)) || !ctx.TryWriteUInt32(commandAddress + 16, (uint)reference) || !ctx.TryWriteUInt32(commandAddress + 20, (uint)mask) || !ctx.TryWriteUInt32(commandAddress + 24, pollCycles / 40)) { return ReturnPointer(ctx, 0); } } else if (!ctx.TryWriteUInt32(commandAddress, Pm4(packetDwords, ItNop, packetRegister)) || !ctx.TryWriteUInt32(commandAddress + 4, (uint)address) || !ctx.TryWriteUInt32(commandAddress + 8, (uint)(address >> 32)) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)mask)) { return ReturnPointer(ctx, 0); } else if (size == 0) { if (!ctx.TryWriteUInt32(commandAddress + 16, compareFunction | (operation << 8)) || !ctx.TryWriteUInt32(commandAddress + 20, (uint)reference)) { return ReturnPointer(ctx, 0); } } else if (!ctx.TryWriteUInt32(commandAddress + 16, (uint)(mask >> 32)) || !ctx.TryWriteUInt32(commandAddress + 20, (uint)reference) || !ctx.TryWriteUInt32(commandAddress + 24, (uint)(reference >> 32)) || !ctx.TryWriteUInt32(commandAddress + 28, compareFunction | (operation << 8)) || !ctx.TryWriteUInt32(commandAddress + 32, pollCycles / 40)) { return ReturnPointer(ctx, 0); } if (ShouldTraceHotPath(ref _dcbWaitRegMemTraceCount)) { TraceAgc( $"agc.dcb_wait_reg_mem buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + $"size={size} compare={compareFunction} op={operation} cache={cachePolicy} " + $"addr=0x{address:X16} ref=0x{reference:X16} mask=0x{mask:X16} poll={pollCycles}"); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "WmAc2MEj6Io", ExportName = "sceAgcDcbDmaData", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbDmaData(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var destination = (uint)(ctx[CpuRegister.Rsi] & 0xFF); var destinationCachePolicy = (uint)(ctx[CpuRegister.Rdx] & 0xFF); var source = (uint)(ctx[CpuRegister.Rcx] & 0xFF); var destinationAddress = ctx[CpuRegister.R8]; var sourceCachePolicy = (uint)(ctx[CpuRegister.R9] & 0xFF); var stackAddress = ctx[CpuRegister.Rsp]; if (!ctx.TryReadUInt64(stackAddress + sizeof(ulong), out var control4Raw) || !ctx.TryReadUInt64(stackAddress + (2 * sizeof(ulong)), out var sourceAddress) || !ctx.TryReadUInt32(stackAddress + (3 * sizeof(ulong)), out var byteCount) || !ctx.TryReadUInt64(stackAddress + (4 * sizeof(ulong)), out var control7Raw) || !ctx.TryReadUInt64(stackAddress + (5 * sizeof(ulong)), out var control8Raw) || !ctx.TryReadUInt64(stackAddress + (6 * sizeof(ulong)), out var control9Raw)) { return ReturnPointer(ctx, 0); } if (commandBufferAddress == 0 || byteCount == 0 || (byteCount & 3) != 0) { return ReturnPointer(ctx, 0); } var control4 = (uint)(control4Raw & 0xFF); var control7 = (uint)(control7Raw & 0xFF); var control8 = (uint)(control8Raw & 0xFF); var control9 = (uint)(control9Raw & 0xFF); if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(8, ItNop, RDmaData)) || !ctx.TryWriteUInt32( commandAddress + 4, destination | (destinationCachePolicy << 8) | (source << 16) | (sourceCachePolicy << 24)) || !ctx.TryWriteUInt32( commandAddress + 8, control4 | (control7 << 8) | (control8 << 16) | (control9 << 24)) || !ctx.TryWriteUInt32(commandAddress + 12, byteCount) || !ctx.TryWriteUInt64(commandAddress + 16, destinationAddress) || !ctx.TryWriteUInt64(commandAddress + 24, sourceAddress)) { return ReturnPointer(ctx, 0); } TraceAgc( $"agc.dcb_dma_data buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + $"dst=0x{destinationAddress:X16} src=0x{sourceAddress:X16} bytes={byteCount} " + $"control0=0x{destination | (destinationCachePolicy << 8) | (source << 16) | (sourceCachePolicy << 24):X8}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "RmaJwLtc8rY", ExportName = "sceAgcDcbSetBaseIndirectArgs", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetBaseIndirectArgs(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var baseIndex = (uint)ctx[CpuRegister.Rsi]; var address = ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0 || !TryAllocateCommandDwords(ctx, commandBufferAddress, 4, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(4, ItSetBase, 0) | (baseIndex << 1)) || !ctx.TryWriteUInt32(commandAddress + 4, 1) || !ctx.TryWriteUInt32(commandAddress + 8, (uint)address & ~7u) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)(address >> 32))) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "CtB+A9-VxO0", ExportName = "sceAgcDcbDispatchIndirect", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbDispatchIndirect(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var dataOffset = (uint)ctx[CpuRegister.Rsi]; var modifier = (uint)ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0 || !TryAllocateCommandDwords(ctx, commandBufferAddress, 3, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(3, ItDispatchIndirect, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, dataOffset) || !ctx.TryWriteUInt32(commandAddress + 8, (modifier & 0xA038u) | 0x41u)) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "+kSrjIVxKFE", ExportName = "sceAgcDcbPushMarker", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbPushMarker(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var markerAddress = ctx[CpuRegister.Rsi]; if (commandBufferAddress == 0 || !TryReadGuestCString(ctx, markerAddress, 4095, out var marker)) { return ReturnPointer(ctx, 0); } var payloadDwords = Math.Max(((uint)marker.Length + 4) / 4, 1); var packetDwords = payloadDwords + 1; if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(packetDwords, ItNop, RPushMarker))) { return ReturnPointer(ctx, 0); } for (uint index = 0; index < payloadDwords; index++) { uint value = 0; for (uint byteIndex = 0; byteIndex < sizeof(uint); byteIndex++) { var markerIndex = (index * sizeof(uint)) + byteIndex; if (markerIndex < (uint)marker.Length) { value |= (uint)marker[(int)markerIndex] << ((int)byteIndex * 8); } } if (!ctx.TryWriteUInt32(commandAddress + 4 + ((ulong)index * sizeof(uint)), value)) { return ReturnPointer(ctx, 0); } } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "H7uZqCoNuWk", ExportName = "sceAgcDcbPopMarker", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbPopMarker(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; if (commandBufferAddress == 0 || !TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(2, ItNop, RPopMarker)) || !ctx.TryWriteUInt32(commandAddress + 4, 0)) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "IxYiarKlXxM", ExportName = "sceAgcDmaDataPatchSetDstAddressOrOffset", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DmaDataPatchSetDstAddressOrOffset(CpuContext ctx) { var commandAddress = ctx[CpuRegister.Rdi]; var destinationAddress = ctx[CpuRegister.Rsi]; if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register) || op != ItNop || register != RDmaData) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } return ctx.TryWriteUInt64(commandAddress + 16, destinationAddress) ? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK) : ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } [SysAbiExport( Nid = "3KDcnM3lrcU", ExportName = "sceAgcWaitRegMemPatchAddress", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int WaitRegMemPatchAddress(CpuContext ctx) { var commandAddress = ctx[CpuRegister.Rdi]; var address = ctx[CpuRegister.Rsi]; if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } var fieldOffset = op == ItWaitRegMem ? 8UL : op == ItNop && register is RWaitMem32 or RWaitMem64 ? 4UL : 0; if (fieldOffset == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } return ctx.TryWriteUInt64(commandAddress + fieldOffset, address) ? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK) : ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } [SysAbiExport( Nid = "0fWWK5uG9rQ", ExportName = "sceAgcQueueEndOfPipeActionPatchAddress", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int QueueEndOfPipeActionPatchAddress(CpuContext ctx) { var commandAddress = ctx[CpuRegister.Rdi]; var address = ctx[CpuRegister.Rsi]; if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register) || op != ItNop || register != RReleaseMem) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } return ctx.TryWriteUInt64(commandAddress + 12, address) ? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK) : ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } [SysAbiExport( Nid = "l4fM9K-Lyks", ExportName = "sceAgcDcbSetIndexBuffer", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetIndexBuffer(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var indexBufferAddress = ctx[CpuRegister.Rsi]; var indexCount = (uint)ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(3, ItIndexBase, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, (uint)(indexBufferAddress & 0xFFFF_FFFFUL)) || !ctx.TryWriteUInt32(commandAddress + 8, (uint)(indexBufferAddress >> 32)) || !ctx.TryWriteUInt32(commandAddress + 12, Pm4(2, ItIndexBufferSize, 0)) || !ctx.TryWriteUInt32(commandAddress + 16, indexCount)) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_set_index_buffer buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} addr=0x{indexBufferAddress:X16} count={indexCount}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "B+aG9DUnTKA", ExportName = "sceAgcDcbDrawIndexOffset", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbDrawIndexOffset(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var indexOffset = (uint)ctx[CpuRegister.Rsi]; var indexCount = (uint)ctx[CpuRegister.Rdx]; var flags = (uint)ctx[CpuRegister.Rcx]; if (commandBufferAddress == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(5, ItDrawIndexOffset2, 0)) || !ctx.TryWriteUInt32(commandAddress + 4, indexCount) || !ctx.TryWriteUInt32(commandAddress + 8, indexOffset) || !ctx.TryWriteUInt32(commandAddress + 12, indexCount) || !ctx.TryWriteUInt32(commandAddress + 16, flags & 0xE000_0001u)) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_draw_index_offset buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} offset={indexOffset} count={indexCount} flags=0x{flags:X8}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "MWiElSNE8j8", ExportName = "sceAgcDcbWaitUntilSafeForRendering", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbWaitUntilSafeForRendering(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var videoOutHandle = (uint)ctx[CpuRegister.Rsi]; var displayBufferIndex = (uint)ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(7, ItNop, RWaitFlipDone)) || !ctx.TryWriteUInt32(commandAddress + 4, videoOutHandle) || !ctx.TryWriteUInt32(commandAddress + 8, displayBufferIndex) || !ctx.TryWriteUInt32(commandAddress + 12, 0) || !ctx.TryWriteUInt32(commandAddress + 16, 0) || !ctx.TryWriteUInt32(commandAddress + 20, 0) || !ctx.TryWriteUInt32(commandAddress + 24, 0)) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_wait_safe buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} handle={videoOutHandle} index={displayBufferIndex}"); return ReturnPointer(ctx, commandAddress); } [SysAbiExport( Nid = "YUeqkyT7mEQ", ExportName = "sceAgcDcbSetFlip", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetFlip(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var videoOutHandle = (uint)ctx[CpuRegister.Rsi]; var displayBufferIndex = (int)ctx[CpuRegister.Rdx]; var flipMode = (uint)ctx[CpuRegister.Rcx]; var flipArg = unchecked((ulong)ctx[CpuRegister.R8]); if (commandBufferAddress == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 6, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(6, ItNop, RFlip)) || !ctx.TryWriteUInt32(commandAddress + 4, videoOutHandle) || !ctx.TryWriteUInt32(commandAddress + 8, unchecked((uint)displayBufferIndex)) || !ctx.TryWriteUInt32(commandAddress + 12, flipMode) || !ctx.TryWriteUInt32(commandAddress + 16, (uint)(flipArg & 0xFFFF_FFFFUL)) || !ctx.TryWriteUInt32(commandAddress + 20, (uint)(flipArg >> 32))) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_set_flip buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} handle={videoOutHandle} index={displayBufferIndex} mode={flipMode} arg=0x{flipArg:X16}"); return ReturnPointer(ctx, commandAddress); } // Guest draw-command builders: emit valid (skippable) packets and return a live // command pointer so the guest's command-buffer build succeeds; full draw processing is TODO. [SysAbiExport( Nid = "8N2tmT3jmC8", ExportName = "sceAgcDcbSetIndexCount", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetIndexCount(CpuContext ctx) { var dcb = ctx[CpuRegister.Rdi]; var indexCount = (uint)ctx[CpuRegister.Rsi]; if (dcb == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, dcb, 2, out var cmd) || !ctx.TryWriteUInt32(cmd, Pm4(2, ItNop, RZero)) || !ctx.TryWriteUInt32(cmd + 4, indexCount)) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, cmd); } [SysAbiExport( Nid = "xSAR0LTcRKM", ExportName = "sceAgcDcbJump", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbJump(CpuContext ctx) { var dcb = ctx[CpuRegister.Rdi]; var target = ctx[CpuRegister.Rsi]; var sizeDwords = (uint)ctx[CpuRegister.Rdx]; if (dcb == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) || !ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) || !ctx.TryWriteUInt32(cmd + 4, (uint)(target & 0xFFFF_FFFFUL)) || !ctx.TryWriteUInt32(cmd + 8, (uint)((target >> 32) & 0xFFFFUL)) || !ctx.TryWriteUInt32(cmd + 12, sizeDwords & 0xFFFFF)) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, cmd); } [SysAbiExport( Nid = "bbFueFP+J4k", ExportName = "sceAgcDcbSetPredication", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DcbSetPredication(CpuContext ctx) { var dcb = ctx[CpuRegister.Rdi]; var address = ctx[CpuRegister.Rsi]; if (dcb == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) || !ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) || !ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) || !ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32))) { return ReturnPointer(ctx, 0); } return ReturnPointer(ctx, cmd); } [SysAbiExport( Nid = "w6Dj1VJt5qY", ExportName = "sceAgcSetPacketPredication", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int SetPacketPredication(CpuContext ctx) { // Global predication toggle on a packet; a no-op is safe for rendering. ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "w2rJhmD+dsE", ExportName = "sceAgcDriverAddEqEvent", Target = Generation.Gen5, LibraryName = "libSceAgcDriver")] public static int DriverAddEqEvent(CpuContext ctx) { var equeue = ctx[CpuRegister.Rdi]; var eventId = ctx[CpuRegister.Rsi]; var userData = ctx[CpuRegister.Rdx]; if (!KernelEventQueueCompatExports.RegisterEvent( equeue, eventId, KernelEventQueueCompatExports.KernelEventFilterGraphics, userData)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); } TraceAgc($"agc.driver_add_eq_event eq=0x{equeue:X16} id=0x{eventId:X16} udata=0x{userData:X16}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "DL2RXaXOy88", ExportName = "sceAgcDriverDeleteEqEvent", Target = Generation.Gen5, LibraryName = "libSceAgcDriver")] public static int DriverDeleteEqEvent(CpuContext ctx) { var equeue = ctx[CpuRegister.Rdi]; var eventId = ctx[CpuRegister.Rsi]; if (!KernelEventQueueCompatExports.DeleteRegisteredEvent( equeue, eventId, KernelEventQueueCompatExports.KernelEventFilterGraphics)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND); } TraceAgc($"agc.driver_delete_eq_event eq=0x{equeue:X16} id=0x{eventId:X16}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "UglJIZjGssM", ExportName = "sceAgcDriverSubmitDcb", Target = Generation.Gen5, LibraryName = "libSceAgcDriver")] public static int DriverSubmitDcb(CpuContext ctx) { var packetAddress = ctx[CpuRegister.Rdi]; if (packetAddress == 0 || !ctx.TryReadUInt64(packetAddress, out var commandAddress) || !ctx.TryReadUInt32(packetAddress + 8, out var dwordCount)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } var tracePackets = false; if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal)) { lock (_submitTraceGate) { tracePackets = _tracedDcbSizes.Add(dwordCount); } } if (tracePackets) { TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}"); } var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); lock (gpuState.Gate) { Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope(); try { ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets); DrainResumableDcbs(ctx, gpuState, tracePackets); } finally { Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope(); } } ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } // ABI (reversed from Quake): rdi = array of DCB base addresses (u64 each), // rsi = array of DCB sizes in dwords (u32 each), rdx = buffer count. [SysAbiExport( Nid = "6UzEidRZwkg", ExportName = "sceAgcDriverSubmitMultiDcbs", Target = Generation.Gen5, LibraryName = "libSceAgcDriver")] public static int DriverSubmitMultiDcbs(CpuContext ctx) { var addressArray = ctx[CpuRegister.Rdi]; var sizeArray = ctx[CpuRegister.Rsi]; var bufferCount = (uint)ctx[CpuRegister.Rdx]; if (addressArray == 0 || sizeArray == 0 || bufferCount == 0 || bufferCount > 4096) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } var tracePackets = string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal); var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); lock (gpuState.Gate) { Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope(); try { for (uint i = 0; i < bufferCount; i++) { if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) || commandAddress == 0 || !ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) || dwordCount == 0) { continue; } if (tracePackets) { TraceAgc( $"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " + $"addr=0x{commandAddress:X16} dwords={dwordCount}"); } ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets); } DrainResumableDcbs(ctx, gpuState, tracePackets); } finally { Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope(); } } ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "gSRnr79F8tQ", ExportName = "sceAgcDriverSubmitAcb", Target = Generation.Gen5, LibraryName = "libSceAgcDriver")] public static int DriverSubmitAcb(CpuContext ctx) { var ownerHandle = (uint)ctx[CpuRegister.Rdi]; var packetAddress = ctx[CpuRegister.Rsi]; if (packetAddress == 0 || !ctx.TryReadUInt64(packetAddress, out var commandAddress) || !ctx.TryReadUInt32(packetAddress + 8, out var dwordCount)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } var tracePackets = false; if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal)) { lock (_submitTraceGate) { tracePackets = _tracedDcbSizes.Add(dwordCount); } } if (tracePackets) { TraceAgc( $"agc.driver_submit_acb owner={ownerHandle} packet=0x{packetAddress:X16} " + $"addr=0x{commandAddress:X16} dwords={dwordCount}"); } var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); lock (gpuState.Gate) { if (!gpuState.ComputeQueues.TryGetValue(ownerHandle, out var queueState)) { queueState = new SubmittedDcbState(); gpuState.ComputeQueues.Add(ownerHandle, queueState); } Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope(); try { ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets); DrainResumableDcbs(ctx, gpuState, tracePackets); } finally { Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope(); } } ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "uJziRsODk1c", ExportName = "sceAgcDriverGetResourceRegistrationMaxNameLength", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DriverGetResourceRegistrationMaxNameLength(CpuContext ctx) { var outAddress = ctx[CpuRegister.Rdi]; if (outAddress == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!ctx.TryWriteUInt32(outAddress, ResourceRegistrationMaxNameLength)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } TraceAgc( $"agc.driver_get_resource_registration_max_name_length " + $"out=0x{outAddress:X16} value={ResourceRegistrationMaxNameLength}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "AOLcoIkQDgM", ExportName = "sceAgcDriverQueryResourceRegistrationUserMemoryRequirements", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DriverQueryResourceRegistrationUserMemoryRequirements(CpuContext ctx) { var sizeAddress = ctx[CpuRegister.Rdi]; var resourceCount = ctx[CpuRegister.Rsi]; var ownerCount = ctx[CpuRegister.Rdx]; if (sizeAddress == 0 || resourceCount == 0 || ownerCount == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } ulong requiredSize; try { requiredSize = checked( resourceCount * ResourceRegistrationBytesPerResource + ownerCount * ResourceRegistrationBytesPerOwner); } catch (OverflowException) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!ctx.TryWriteUInt64(sizeAddress, requiredSize)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } TraceAgc( $"agc.driver_query_resource_registration_memory resources={resourceCount} " + $"owners={ownerCount} bytes=0x{requiredSize:X}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "F0Y42t-3e18", ExportName = "sceAgcDriverInitResourceRegistration", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DriverInitResourceRegistration(CpuContext ctx) { var memoryAddress = ctx[CpuRegister.Rdi]; var memorySize = ctx[CpuRegister.Rsi]; var ownerCount = ctx[CpuRegister.Rdx]; if (memoryAddress == 0 || memorySize == 0 || ownerCount == 0 || ownerCount > uint.MaxValue) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); lock (state.Gate) { state.ResourceRegistrationInitialized = true; state.ResourceRegistrationMemory = memoryAddress; state.ResourceRegistrationMemorySize = memorySize; state.ResourceRegistrationMaxOwners = (uint)ownerCount; state.ResourceOwners.Clear(); state.RegisteredResources.Clear(); state.DefaultOwner = DefaultAgcOwner; state.NextOwner = 1; state.NextResource = 1; } TraceAgc( $"agc.driver_init_resource_registration memory=0x{memoryAddress:X16} " + $"bytes=0x{memorySize:X} owners={ownerCount}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } private const uint DefaultAgcOwner = 1; [SysAbiExport( Nid = "F0ZXt5q0ZTA", ExportName = "sceAgcDriverGetDefaultOwner", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DriverGetDefaultOwner(CpuContext ctx) { var ownerAddress = ctx[CpuRegister.Rdi]; if (ownerAddress == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); uint owner; lock (state.Gate) { owner = state.DefaultOwner; } if (!ctx.TryWriteUInt32(ownerAddress, owner)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={owner}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "U9ueyEhSkF4", ExportName = "sceAgcDriverRegisterDefaultOwner", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DriverRegisterDefaultOwner(CpuContext ctx) { var owner = (uint)ctx[CpuRegister.Rdi]; var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); lock (state.Gate) { state.DefaultOwner = owner; } TraceAgc($"agc.driver_register_default_owner owner={owner}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "X-Nm5KLREeg", ExportName = "sceAgcDriverRegisterOwner", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DriverRegisterOwner(CpuContext ctx) { var ownerAddress = ctx[CpuRegister.Rdi]; var nameAddress = ctx[CpuRegister.Rsi]; if (ownerAddress == 0 || nameAddress == 0 || !TryReadGuestCString( ctx, nameAddress, ResourceRegistrationMaxNameLength, out var nameBytes)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); uint owner; lock (state.Gate) { if (!state.ResourceRegistrationInitialized || state.ResourceRegistrationMaxOwners != 0 && state.ResourceOwners.Count >= state.ResourceRegistrationMaxOwners) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } owner = state.NextOwner; while (owner == state.DefaultOwner || state.ResourceOwners.ContainsKey(owner)) { owner++; if (owner == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } } state.NextOwner = owner + 1; state.ResourceOwners.Add(owner, System.Text.Encoding.UTF8.GetString(nameBytes)); } if (!ctx.TryWriteUInt32(ownerAddress, owner)) { lock (state.Gate) { state.ResourceOwners.Remove(owner); } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } TraceAgc( $"agc.driver_register_owner out=0x{ownerAddress:X16} owner={owner} " + $"name={System.Text.Encoding.UTF8.GetString(nameBytes)}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "W5z4eZrjEas", ExportName = "sceAgcDriverRegisterResource", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DriverRegisterResource(CpuContext ctx) { var resourceHandleAddress = ctx[CpuRegister.Rdi]; var owner = (uint)ctx[CpuRegister.Rsi]; var resourceAddress = ctx[CpuRegister.Rdx]; var resourceSize = ctx[CpuRegister.Rcx]; var nameAddress = ctx[CpuRegister.R8]; var type = (uint)ctx[CpuRegister.R9]; if (resourceHandleAddress == 0 || resourceAddress == 0 || resourceSize == 0 || !ctx.TryReadUInt32(ctx[CpuRegister.Rsp] + sizeof(ulong), out var flags) || !TryReadGuestCString( ctx, nameAddress, ResourceRegistrationMaxNameLength, out var nameBytes)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); uint resourceHandle; lock (state.Gate) { if (!state.ResourceRegistrationInitialized || owner != state.DefaultOwner && !state.ResourceOwners.ContainsKey(owner)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } resourceHandle = state.NextResource++; if (resourceHandle == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } state.RegisteredResources.Add( resourceHandle, new RegisteredAgcResource( owner, resourceAddress, resourceSize, System.Text.Encoding.UTF8.GetString(nameBytes), type, flags)); } if (!ctx.TryWriteUInt32(resourceHandleAddress, resourceHandle)) { lock (state.Gate) { state.RegisteredResources.Remove(resourceHandle); } return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } TraceAgc( $"agc.driver_register_resource handle={resourceHandle} owner={owner} " + $"resource=0x{resourceAddress:X16} bytes=0x{resourceSize:X} " + $"name={System.Text.Encoding.UTF8.GetString(nameBytes)} type={type} flags={flags}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "pWLG7WOpVcw", ExportName = "sceAgcDriverUnregisterResource", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DriverUnregisterResource(CpuContext ctx) { var resourceHandle = (uint)ctx[CpuRegister.Rdi]; var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); lock (state.Gate) { if (!state.RegisteredResources.Remove(resourceHandle)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } } TraceAgc($"agc.driver_unregister_resource handle={resourceHandle}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "-KRzWekV120", ExportName = "sceAgcDriverUnknown_KRzWekV120", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int DriverUnknownKRzWekV120(CpuContext ctx) { TraceAgc( $"agc.driver_unknown_krz rdi=0x{ctx[CpuRegister.Rdi]:X16} " + $"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} " + $"rcx=0x{ctx[CpuRegister.Rcx]:X16} r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16}"); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); } [SysAbiExport( Nid = "h9z6+0hEydk", ExportName = "sceAgcSuspendPoint", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int SuspendPoint(CpuContext ctx) { TraceAgc("agc.suspend_point"); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } [SysAbiExport( Nid = "qj7QZpgr9Uw", ExportName = "sceAgcUnknownQj7QZpgr9Uw", Target = Generation.Gen5, LibraryName = "libSceAgc")] public static int UnknownQj7QZpgr9Uw(CpuContext ctx) { var commandBufferAddress = ctx[CpuRegister.Rdi]; if (commandBufferAddress == 0 || !TryAllocateCommandDwords(ctx, commandBufferAddress, 1, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, 0x8000_0000)) { return ReturnPointer(ctx, 0); } TraceAgc( $"agc.unknown_qj7 buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + $"arg1=0x{ctx[CpuRegister.Rsi]:X16} arg2=0x{ctx[CpuRegister.Rdx]:X16}"); return ReturnPointer(ctx, commandAddress); } // WAIT_REG_MEM packets whose condition is not met suspend their DCB into // GpuWaitRegistry. Each submit re-checks every suspended DCB against current guest // memory (labels are advanced by ReleaseMem/WriteData/DmaData packets or by direct // CPU writes) and resumes the ones whose condition is now satisfied. A resumed DCB // can itself write labels that unblock others, so loop until a fixed point. private static void DrainResumableDcbs( CpuContext ctx, SubmittedGpuState gpuState, bool tracePackets) { for (var pass = 0; pass < 256; pass++) { var woken = GpuWaitRegistry.CollectSatisfied((address, is64Bit) => { if (is64Bit) { return ctx.TryReadUInt64(address, out var value64) ? value64 : (ulong?)null; } return ctx.TryReadUInt32(address, out var value32) ? value32 : (ulong?)null; }); if (woken is null) { return; } foreach (var waiter in woken) { var remainingDwords = waiter.TotalDwords - waiter.ResumeOffset; if (remainingDwords == 0) { continue; } if (tracePackets) { TraceAgc( $"agc.dcb.resumed addr=0x{waiter.WaitAddress:X16} " + $"resume=0x{waiter.ResumeAddress:X16} dwords={remainingDwords}"); } var state = waiter.State as SubmittedDcbState ?? gpuState.Graphics; ParseSubmittedDcb( ctx, gpuState, state, waiter.ResumeAddress, remainingDwords, tracePackets); } } } private static void ParseSubmittedDcb( CpuContext ctx, SubmittedGpuState gpuState, SubmittedDcbState state, ulong commandAddress, uint dwordCount, bool tracePackets) { if (commandAddress == 0 || dwordCount == 0 || dwordCount > 1_000_000) { return; } var offset = 0u; while (offset < dwordCount) { var currentAddress = commandAddress + ((ulong)offset * sizeof(uint)); if (!ctx.TryReadUInt32(currentAddress, out var header)) { return; } var packetType = header >> 30; if (packetType == 2) { if (tracePackets) { TraceAgc( $"agc.dcb.packet dw={offset} addr=0x{currentAddress:X16} " + $"header=0x{header:X8} len=1 type=2"); } offset++; continue; } if (packetType != 3) { return; } var length = Pm4Length(header); if (length == 0 || offset + length > dwordCount) { return; } var op = (header >> 8) & 0xFFu; var register = (header >> 2) & 0x3Fu; if (tracePackets) { TraceSubmittedPacket(ctx, currentAddress, offset, header, length, op, register); } if (op == ItSetShReg && TryReadTextureDescriptor(ctx, currentAddress, length, out var texture)) { state.PresenterTexture = texture; } ApplySubmittedRegisters(ctx, state, currentAddress, length, op, register); if (op == ItSetBase && length >= 4 && ctx.TryReadUInt32(currentAddress + 4, out var baseSelector) && baseSelector == 1 && ctx.TryReadUInt64(currentAddress + 8, out var indirectArgsAddress)) { state.IndirectArgsAddress = indirectArgsAddress; } if (op == ItEventWrite && length >= 2 && ctx.TryReadUInt32(currentAddress + sizeof(uint), out var eventTypeRaw)) { var eventType = eventTypeRaw & 0x3Fu; var triggered = KernelEventQueueCompatExports.TriggerRegisteredEvents( eventType, KernelEventQueueCompatExports.KernelEventFilterGraphics, eventType); if (tracePackets) { TraceAgc($"agc.dcb.event type=0x{eventType:X2} queues={triggered}"); } } if (op == ItNop && register == RReleaseMem && length >= 7) { ApplySubmittedReleaseMem(ctx, currentAddress, tracePackets); } if (op == ItNop && register == RWriteData && length >= 4) { ApplySubmittedWriteData(ctx, currentAddress, length, tracePackets); } if (op == ItWriteData && length >= 4) { ApplySubmittedWriteData(ctx, currentAddress, length, tracePackets); } if (op == ItNop && register == RDmaData && length >= 8) { ApplySubmittedDmaData(ctx, currentAddress, tracePackets); } if (op == ItDmaData && length >= 7) { ApplySubmittedStandardDmaData(ctx, currentAddress); } if (op == ItIndexBase && length >= 3 && ctx.TryReadUInt32(currentAddress + 4, out var indexBaseLo) && ctx.TryReadUInt32(currentAddress + 8, out var indexBaseHi)) { state.IndexBufferAddress = indexBaseLo | ((ulong)indexBaseHi << 32); } if (op == ItIndexBufferSize && length >= 2 && ctx.TryReadUInt32(currentAddress + 4, out var indexBufferCount)) { state.IndexBufferCount = indexBufferCount; } if (op == ItIndexType && length >= 2 && ctx.TryReadUInt32(currentAddress + 4, out var indexSize)) { state.IndexSize = indexSize & 0x3; } if (op == ItNumInstances && length >= 2 && ctx.TryReadUInt32(currentAddress + 4, out var instanceCount)) { state.InstanceCount = Math.Max(instanceCount, 1); } // WAIT_REG_MEM (AGC NOP-encapsulated 32/64-bit variants): when the condition is // not yet satisfied, suspend this DCB; DrainResumableDcbs resumes it once the // watched memory advances. if (op == ItNop && register is RWaitMem32 or RWaitMem64 && length >= (register == RWaitMem32 ? 6u : 9u)) { if (TryParseWaitRegMem(ctx, currentAddress, register == RWaitMem64, out var waitAddr, out var refVal, out var waitMask, out var cmpFunc)) { ulong curVal = 0; bool hasCurVal; if (register == RWaitMem64) { hasCurVal = ctx.TryReadUInt64(waitAddr, out curVal); } else if (ctx.TryReadUInt32(waitAddr, out var curVal32)) { curVal = curVal32; hasCurVal = true; } else { hasCurVal = false; } var waiter = new GpuWaitRegistry.WaitingDcb { CommandBufferAddress = commandAddress, ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)), TotalDwords = dwordCount, ResumeOffset = offset + length, ReferenceValue = refVal, Mask = waitMask, CompareFunction = cmpFunc, Is64Bit = register == RWaitMem64, State = state, }; if (hasCurVal && !GpuWaitRegistry.Compare(waiter, curVal)) { GpuWaitRegistry.Register(waitAddr, waiter); if (tracePackets) { TraceAgc( $"agc.dcb.suspended addr=0x{waitAddr:X16} ref=0x{refVal:X16} " + $"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}"); } return; // suspend parsing of this DCB } } } if (op == ItWaitRegMem && length >= 7) { if (TryParseStandardWaitRegMem(ctx, currentAddress, out var waitAddr, out var refVal, out var waitMask, out var cmpFunc) && ctx.TryReadUInt32(waitAddr, out var curVal)) { var waiter = new GpuWaitRegistry.WaitingDcb { CommandBufferAddress = commandAddress, ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)), TotalDwords = dwordCount, ResumeOffset = offset + length, ReferenceValue = refVal, Mask = waitMask, CompareFunction = cmpFunc, Is64Bit = false, State = state, }; if (!GpuWaitRegistry.Compare(waiter, curVal)) { GpuWaitRegistry.Register(waitAddr, waiter); if (tracePackets) { TraceAgc( $"agc.dcb.suspended_std addr=0x{waitAddr:X16} ref=0x{refVal:X16} " + $"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}"); } return; } } } if (TryReadSubmittedDrawCount( ctx, state, currentAddress, length, op, out var indexCount) && indexCount != 0) { lock (_submitTraceGate) { if (_tracedSubmittedDrawOpcodes.Add(op)) { TraceAgcShader( $"agc.draw_packet op=0x{op:X2} count={indexCount}"); } } var indexed = op is ItDrawIndex2 or ItDrawIndexOffset2 or ItDrawIndexIndirect; state.SawIndexedDraw |= indexed; TryTranslateGuestDraw(ctx, gpuState, state, indexCount, indexed); } if (op == ItNop && register == RDrawIndexAuto && length >= 2 && ctx.TryReadUInt32(currentAddress + 4, out var autoIndexCount) && autoIndexCount != 0) { TryTranslateGuestDraw( ctx, gpuState, state, autoIndexCount, indexed: false); } if ((op is ItDispatchDirect or ItDispatchIndirect) && TryReadComputeDispatch( ctx, state, currentAddress, length, op, out var dispatch)) { ObserveComputeDispatch(ctx, gpuState, state, dispatch); } if (op == ItNop && register == RFlip && length >= 6) { if (!ctx.TryReadUInt32(currentAddress + 4, out var videoOutHandle) || !ctx.TryReadUInt32(currentAddress + 8, out var displayBufferIndexRaw) || !ctx.TryReadUInt32(currentAddress + 12, out var flipMode) || !ctx.TryReadUInt32(currentAddress + 16, out var flipArgLo) || !ctx.TryReadUInt32(currentAddress + 20, out var flipArgHi)) { return; } var flipArg = unchecked((long)(((ulong)flipArgHi << 32) | flipArgLo)); var displayBufferIndex = unchecked((int)displayBufferIndexRaw); var handle = unchecked((int)videoOutHandle); if (VideoOutExports.TryGetDisplayBufferInfo( handle, displayBufferIndex, out var cachedDisplayBuffer) && VulkanVideoPresenter.TrySubmitGuestImage( cachedDisplayBuffer.Address, cachedDisplayBuffer.Width, cachedDisplayBuffer.Height, cachedDisplayBuffer.PitchInPixel)) { TraceDisplayBuffer( handle, displayBufferIndex, cachedDisplayBuffer, "gpu-cache"); } else if (state.SawIndexedDraw && state.TranslatedDraw is { } translatedDraw && VideoOutExports.TryGetDisplayBufferInfo( handle, displayBufferIndex, out var translatedDisplayBuffer)) { TraceDisplayBuffer( handle, displayBufferIndex, translatedDisplayBuffer, "draw-fallback"); var textures = CreateVulkanGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount); var globalMemoryBuffers = CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings); VulkanVideoPresenter.SubmitTranslatedDraw( translatedDraw.PixelSpirv, textures, globalMemoryBuffers, translatedDisplayBuffer.Width, translatedDisplayBuffer.Height, translatedDraw.AttributeCount); TraceAgcShader( $"agc.shader_present ps=0x{translatedDraw.PixelShaderAddress:X16} " + $"spirv={translatedDraw.PixelSpirv.Length} textures={textures.Count} " + $"global_buffers={globalMemoryBuffers.Count} " + $"fallback={fallbackTextureCount} {translatedDisplayBuffer.Width}x{translatedDisplayBuffer.Height}"); for (var i = 0; i < translatedDraw.Textures.Count; i++) { var binding = translatedDraw.Textures[i]; var d = binding.Descriptor; TraceAgcShader( $"agc.present_desc[{i}] " + $"addr=0x{d.Address:X16} " + $"size={d.Width}x{d.Height} " + $"fmt={d.Format} " + $"num={d.NumberType} " + $"type={d.Type} " + $"tile={d.TileMode} " + $"storage={binding.IsStorage}"); } } else if (state.SawIndexedDraw && state.PresenterTexture is { } sourceTexture) { _ = TrySoftwarePresent( ctx, sourceTexture, unchecked((int)videoOutHandle), displayBufferIndex); } else if (state.SawIndexedDraw && state.GuestDrawKind != GuestDrawKind.None && VideoOutExports.TryGetDisplayBufferInfo( handle, displayBufferIndex, out var displayBuffer)) { VulkanVideoPresenter.SubmitGuestDraw( state.GuestDrawKind, displayBuffer.Width, displayBuffer.Height); } _ = VideoOutExports.SubmitFlipFromAgc(ctx, handle, displayBufferIndex, unchecked((int)flipMode), flipArg); state.SawIndexedDraw = false; state.GuestDrawKind = GuestDrawKind.None; state.TranslatedDraw = null; } offset += length; } } private static void TraceDisplayBuffer( int handle, int index, VideoOutExports.DisplayBufferInfo buffer, string path) { lock (_submitTraceGate) { if (!_tracedDisplayBuffers.Add((handle, index, buffer.Address, path))) { return; } } TraceAgcShader( $"agc.display_buffer handle={handle} index={index} " + $"addr=0x{buffer.Address:X16} fmt=0x{buffer.PixelFormat:X16} " + $"tile={buffer.TilingMode} size={buffer.Width}x{buffer.Height} " + $"pitch={buffer.PitchInPixel} path={path}"); } private static void ApplySubmittedDmaData( CpuContext ctx, ulong packetAddress, bool tracePacket) { if (!ctx.TryReadUInt32(packetAddress + 12, out var byteCount) || !ctx.TryReadUInt64(packetAddress + 16, out var destinationAddress) || !ctx.TryReadUInt64(packetAddress + 24, out var sourceAddress)) { return; } var copied = byteCount != 0 && byteCount <= 256u * 1024u * 1024u && destinationAddress != 0 && sourceAddress != 0 && TryCopyGuestMemory(ctx, sourceAddress, destinationAddress, byteCount); if (tracePacket) { TraceAgc( $"agc.dcb.dma_data dst=0x{destinationAddress:X16} src=0x{sourceAddress:X16} " + $"bytes={byteCount} copied={copied}"); } } private static void ApplySubmittedStandardDmaData( CpuContext ctx, ulong packetAddress) { if (!ctx.TryReadUInt32(packetAddress + 4, out var control) || !ctx.TryReadUInt32(packetAddress + 8, out var sourceLow) || !ctx.TryReadUInt32(packetAddress + 12, out var sourceHigh) || !ctx.TryReadUInt32(packetAddress + 16, out var destinationLow) || !ctx.TryReadUInt32(packetAddress + 20, out var destinationHigh) || !ctx.TryReadUInt32(packetAddress + 24, out var command)) { return; } var byteCount = command & 0x1F_FFFFu; var sourceSelect = (control >> 29) & 0x3u; var destinationSelect = (control >> 20) & 0x3u; var destinationSwap = (command >> 24) & 0x3u; var sourceAddressSpace = (command >> 26) & 0x1u; var destinationAddressSpace = (command >> 27) & 0x1u; var sourceAddressIncrement = (command >> 28) & 0x1u; if (byteCount == 0 || destinationSwap != 0 || destinationSelect is not (0 or 3) || (destinationSelect == 0 && destinationAddressSpace != 0)) { return; } var destinationAddress = destinationLow | ((ulong)destinationHigh << 32); bool copied; ulong sourceAddress; if (sourceSelect is 0 or 3 && (sourceSelect == 3 || sourceAddressSpace == 0)) { sourceAddress = sourceLow | ((ulong)sourceHigh << 32); if (sourceAddressIncrement != 0) { copied = ctx.TryReadUInt32(sourceAddress, out var fillValue) && TryFillGuestMemory( ctx, fillValue, destinationAddress, byteCount); } else { copied = TryCopyGuestMemory( ctx, sourceAddress, destinationAddress, byteCount); } } else if (sourceSelect == 2) { sourceAddress = 0; copied = TryFillGuestMemory( ctx, sourceLow, destinationAddress, byteCount); } else { return; } if (ShouldTraceHotPath(ref _standardDmaTraceCount)) { TraceAgcShader( $"agc.dma_packet dst=0x{destinationAddress:X16} " + $"src=0x{sourceAddress:X16} bytes={byteCount} " + $"src_sel={sourceSelect} fill={sourceAddressIncrement != 0 || sourceSelect == 2} " + $"copied={copied}"); } } private static void ApplySubmittedWriteData( CpuContext ctx, ulong packetAddress, uint packetLength, bool tracePacket) { if (!ctx.TryReadUInt32(packetAddress + 4, out var control) || !ctx.TryReadUInt64(packetAddress + 8, out var destinationAddress)) { return; } var destination = control & 0xFFu; var increment = (control >> 16) & 0xFFu; var dwordCount = packetLength - 4; var wroteData = destination is 1 or 2 or 4 or 5; for (uint index = 0; wroteData && index < dwordCount; index++) { var sourceAddress = packetAddress + 16 + ((ulong)index * sizeof(uint)); var targetAddress = destinationAddress + (increment == 0 ? (ulong)index * sizeof(uint) : 0); wroteData = ctx.TryReadUInt32(sourceAddress, out var value) && ctx.TryWriteUInt32(targetAddress, value); } if (tracePacket) { TraceAgc( $"agc.dcb.write_data dst={destination} addr=0x{destinationAddress:X16} " + $"count={dwordCount} increment={increment} wrote={wroteData}"); } } private static void ApplySubmittedReleaseMem( CpuContext ctx, ulong packetAddress, bool tracePacket) { if (!ctx.TryReadUInt32(packetAddress + 8, out var control) || !ctx.TryReadUInt32(packetAddress + 12, out var destinationLo) || !ctx.TryReadUInt32(packetAddress + 16, out var destinationHi) || !ctx.TryReadUInt32(packetAddress + 20, out var dataLo) || !ctx.TryReadUInt32(packetAddress + 24, out var dataHi)) { return; } var dataSelection = (control >> 16) & 0xFFu; var destinationAddress = ((ulong)destinationHi << 32) | destinationLo; var data = ((ulong)dataHi << 32) | dataLo; var wroteData = dataSelection switch { 1 => ctx.TryWriteUInt32(destinationAddress, dataLo), 2 or 3 => ctx.TryWriteUInt64(destinationAddress, data), _ => false, }; if (tracePacket) { TraceAgc( $"agc.dcb.release_mem dst=0x{destinationAddress:X16} data_sel={dataSelection} " + $"data=0x{data:X16} wrote={wroteData}"); } } private static void ApplySubmittedRegisters( CpuContext ctx, SubmittedDcbState state, ulong packetAddress, uint packetLength, uint op, uint register) { if (op is ItSetShReg or ItSetContextReg or ItSetUconfigReg) { if (packetLength < 3 || !ctx.TryReadUInt32(packetAddress + sizeof(uint), out var startRegister)) { return; } var directDestination = op switch { ItSetShReg => state.ShRegisters, ItSetContextReg => state.CxRegisters, _ => state.UcRegisters, }; for (uint index = 0; index < packetLength - 2; index++) { if (!ctx.TryReadUInt32( packetAddress + 8 + ((ulong)index * sizeof(uint)), out var value)) { return; } directDestination[startRegister + index] = value; } return; } if (op != ItNop || register is not (RCxRegsIndirect or RShRegsIndirect or RUcRegsIndirect) || packetLength < 4 || !ctx.TryReadUInt32(packetAddress + sizeof(uint), out var registerCount) || !ctx.TryReadUInt64(packetAddress + 8, out var registersAddress)) { return; } var destination = register switch { RCxRegsIndirect => state.CxRegisters, RShRegsIndirect => state.ShRegisters, _ => state.UcRegisters, }; for (uint index = 0; index < registerCount; index++) { var entryAddress = registersAddress + ((ulong)index * 8); if (!ctx.TryReadUInt32(entryAddress, out var registerOffset) || !ctx.TryReadUInt32(entryAddress + sizeof(uint), out var value)) { return; } if (registerOffset != 0) { destination[registerOffset] = value; } } } private static bool TryReadSubmittedDrawCount( CpuContext ctx, SubmittedDcbState state, ulong packetAddress, uint packetLength, uint op, out uint drawCount) { drawCount = 0; switch (op) { case ItDrawIndexAuto when packetLength >= 3: return ctx.TryReadUInt32(packetAddress + 4, out drawCount); case ItDrawIndex2 when packetLength >= 6: state.DrawIndexOffset = 0; return ctx.TryReadUInt32(packetAddress + 16, out drawCount); // 5-dword form emitted by DcbDrawIndex (count at +4, ItIndexBase/ // ItIndexBufferSize carried by separate preceding packets). case ItDrawIndex2 when packetLength >= 5: state.DrawIndexOffset = 0; return ctx.TryReadUInt32(packetAddress + 4, out drawCount); case ItDrawIndexOffset2 when packetLength >= 5: if (!ctx.TryReadUInt32(packetAddress + 8, out var indexOffset)) { return false; } state.DrawIndexOffset = indexOffset; return ctx.TryReadUInt32(packetAddress + 12, out drawCount); case ItDrawIndexMultiAuto when packetLength >= 4: if (!ctx.TryReadUInt32(packetAddress + 12, out var control)) { return false; } drawCount = (control >> 21) & 0x7FFu; return true; case ItDrawIndirect or ItDrawIndexIndirect when packetLength >= 5 && state.IndirectArgsAddress != 0: if (!ctx.TryReadUInt32(packetAddress + 4, out var dataOffset)) { return false; } return ctx.TryReadUInt32( state.IndirectArgsAddress + dataOffset, out drawCount); default: return false; } } private static void TryTranslateGuestDraw( CpuContext ctx, SubmittedGpuState gpuState, SubmittedDcbState state, uint vertexCount, bool indexed) { var hasExportShader = TryGetShaderAddress( state.ShRegisters, SpiShaderPgmLoEs, SpiShaderPgmHiEs, out var exportShaderAddress); var hasPixelShader = TryGetShaderAddress( state.ShRegisters, SpiShaderPgmLoPs, SpiShaderPgmHiPs, out var pixelShaderAddress); var hasPsInputEna = state.CxRegisters.TryGetValue(SpiPsInputEna, out var psInputEna); var hasPsInputAddr = state.CxRegisters.TryGetValue(SpiPsInputAddr, out var psInputAddr); state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType); var renderTargets = GetRenderTargets(state.CxRegisters); var drawSequence = ++gpuState.WorkSequence; state.TranslatedDraw = null; state.GuestDrawKind = GuestDrawKind.None; foreach (var target in renderTargets) { state.RenderTargetWriters[target.Address] = new RenderTargetWriter( drawSequence, hasExportShader ? exportShaderAddress : 0, hasPixelShader ? pixelShaderAddress : 0, vertexCount, primitiveType); TraceAgcShader( $"agc.rt_writer seq={drawSequence} target=0x{target.Address:X16} " + $"fmt={target.Format} tile={target.TileMode} " + $"size={target.Width}x{target.Height} vertices={vertexCount} " + $"es=0x{(hasExportShader ? exportShaderAddress : 0):X16} " + $"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16}"); } if (vertexCount == 0 || vertexCount > 1_048_576) { return; } var translationError = string.Empty; if (hasExportShader && hasPixelShader && hasPsInputEna && hasPsInputAddr && TryCreateTranslatedGuestDraw( ctx, state, exportShaderAddress, pixelShaderAddress, vertexCount, indexed, out var translatedDraw, out translationError)) { state.TranslatedDraw = translatedDraw; var firstTarget = translatedDraw.RenderTargets.FirstOrDefault(); if (firstTarget.Address != 0) { var textures = CreateVulkanGuestDrawTextures( ctx, translatedDraw.Textures, out _); var globalMemoryBuffers = CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings); var vertexBuffers = CreateVulkanGuestVertexBuffers(translatedDraw.VertexInputs); VulkanVideoPresenter.SubmitOffscreenTranslatedDraw( translatedDraw.PixelSpirv, textures, globalMemoryBuffers, translatedDraw.AttributeCount, translatedDraw.RenderTargets.Select(target => new VulkanGuestRenderTarget( target.Address, target.Width, target.Height, target.Format, target.NumberType)).ToArray(), translatedDraw.VertexSpirv, translatedDraw.VertexCount, translatedDraw.InstanceCount, translatedDraw.PrimitiveType, translatedDraw.IndexBuffer, vertexBuffers, translatedDraw.RenderState); } else { var storageTarget = translatedDraw.Textures .FirstOrDefault(binding => binding.IsStorage); if (storageTarget is not null) { var textures = CreateVulkanGuestDrawTextures( ctx, translatedDraw.Textures, out _); var globalMemoryBuffers = CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings); VulkanVideoPresenter.SubmitStorageTranslatedDraw( translatedDraw.PixelSpirv, textures, globalMemoryBuffers, translatedDraw.AttributeCount, storageTarget.Descriptor.Width, storageTarget.Descriptor.Height); } } if (ShouldTraceHotPath(ref _translatedDrawTraceCount)) { TraceAgcShader( $"agc.shader_draw_seen seq={drawSequence} " + $"es=0x{exportShaderAddress:X16} ps=0x{pixelShaderAddress:X16} " + $"target=0x{firstTarget.Address:X16}:{firstTarget.Width}x{firstTarget.Height}:fmt{firstTarget.Format}/tile{firstTarget.TileMode} " + $"textures={translatedDraw.Textures.Count}"); } lock (_submitTraceGate) { var firstTextureAddress = translatedDraw.Textures.FirstOrDefault()?.Descriptor.Address ?? 0; if (_tracedShaderDraws.Add( (exportShaderAddress, pixelShaderAddress, firstTarget.Address, firstTextureAddress, vertexCount))) { TraceTranslatedGuestDraw( ctx, gpuState, state, translatedDraw, psInputEna, psInputAddr); } } return; } TraceShaderTranslationMiss( ctx, state, vertexCount, hasExportShader, exportShaderAddress, hasPixelShader, pixelShaderAddress, hasPsInputEna, psInputEna, hasPsInputAddr, psInputAddr, hasExportShader && hasPixelShader ? translationError : null); } private static bool TryCreateTranslatedGuestDraw( CpuContext ctx, SubmittedDcbState state, ulong exportShaderAddress, ulong pixelShaderAddress, uint vertexCount, bool indexed, out TranslatedGuestDraw draw, out string error) { draw = default!; error = string.Empty; ulong exportShaderHeader; ulong pixelShaderHeader; lock (_submitTraceGate) { _shaderHeadersByCode.TryGetValue(exportShaderAddress, out exportShaderHeader); _shaderHeadersByCode.TryGetValue(pixelShaderAddress, out pixelShaderHeader); } if (!Gen5ShaderTranslator.TryCreateState( ctx, exportShaderAddress, exportShaderHeader, state.ShRegisters, SelectExportUserDataRegister(state.ShRegisters), out var exportState, out error, userDataScalarRegisterBase: NggUserDataScalarRegisterBase) || !Gen5ShaderScalarEvaluator.TryEvaluate( ctx, exportState, out var exportEvaluation, out error, resolveVertexInputs: true, vertexRecordLimit: indexed ? null : vertexCount) || !Gen5ShaderTranslator.TryCreateState( ctx, pixelShaderAddress, pixelShaderHeader, state.ShRegisters, PsTextureUserDataRegister, out var pixelState, out error) || !Gen5ShaderScalarEvaluator.TryEvaluate( ctx, pixelState, out var pixelEvaluation, out error)) { return false; } var renderTargets = GetRenderTargets(state.CxRegisters) .Where(target => HasPixelColorExport(pixelState, target.Slot)) .OrderBy(target => target.Slot) .ToArray(); var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length]; for (var index = 0; index < renderTargets.Length; index++) { var target = renderTargets[index]; if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat( target.Format, target.NumberType, out renderTargetFormats[index])) { error = $"unsupported color target format={target.Format} number_type={target.NumberType}"; return false; } } var pixelOutputs = renderTargets .Select((target, location) => new Gen5PixelOutputBinding( target.Slot, (uint)location, renderTargetFormats[location].OutputKind)) .ToArray(); var outputLayout = string.Join( ';', pixelOutputs.Select(output => $"{output.GuestSlot}:{output.HostLocation}:{(int)output.Kind}")); var attributeCount = GetInterpolatedAttributeCount(pixelState); var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation); var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation); var shaderKey = ( exportShaderAddress, exportStateFingerprint, pixelShaderAddress, pixelStateFingerprint, outputLayout, attributeCount); var totalGlobalBuffers = pixelEvaluation.GlobalMemoryBindings.Count + exportEvaluation.GlobalMemoryBindings.Count; (byte[] Vertex, byte[] Pixel) compiled; lock (_submitTraceGate) { _graphicsSpirvCache.TryGetValue(shaderKey, out compiled); } if (compiled.Vertex is null || compiled.Pixel is null) { if (!Gen5SpirvTranslator.TryCompilePixelShader( pixelState, pixelEvaluation, pixelOutputs, out var pixelShader, out error, globalBufferBase: 0, totalGlobalBufferCount: totalGlobalBuffers + 2, imageBindingBase: 0, scalarRegisterBufferIndex: totalGlobalBuffers) || !Gen5SpirvTranslator.TryCompileVertexShader( exportState, exportEvaluation, out var vertexShader, out error, globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count, totalGlobalBufferCount: totalGlobalBuffers + 2, imageBindingBase: pixelEvaluation.ImageBindings.Count, scalarRegisterBufferIndex: totalGlobalBuffers + 1)) { return false; } compiled = (vertexShader.Spirv, pixelShader.Spirv); DumpSpirv( "vs", exportShaderAddress, exportStateFingerprint, compiled.Vertex, exportState.Program); DumpSpirv( "ps", pixelShaderAddress, pixelStateFingerprint, compiled.Pixel, pixelState.Program); lock (_submitTraceGate) { _graphicsSpirvCache.TryAdd(shaderKey, compiled); } } var imageBindings = pixelEvaluation.ImageBindings .Concat(exportEvaluation.ImageBindings); var textures = new List( pixelEvaluation.ImageBindings.Count + exportEvaluation.ImageBindings.Count); foreach (var binding in imageBindings) { if (!TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture)) { error = $"invalid texture descriptor at pc=0x{binding.Pc:X}"; return false; } TraceAgcShader( $"agc.texture_binding ps=0x{pixelShaderAddress:X16} es=0x{exportShaderAddress:X16} " + $"pc=0x{binding.Pc:X} op={binding.Opcode} storage={(Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode) ? 1 : 0)} " + $"decoded={FormatTextureDescriptor(texture)} " + $"raw={FormatShaderDwords(binding.ResourceDescriptor)} sampler={FormatShaderDwords(binding.SamplerDescriptor)}"); textures.Add( new TranslatedImageBinding( texture, Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode), binding.MipLevel ?? 0, binding.SamplerDescriptor)); } var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings .Concat(exportEvaluation.GlobalMemoryBindings) .Append(CreateScalarRegisterBinding(pixelEvaluation)) .Append(CreateScalarRegisterBinding(exportEvaluation)) .ToArray(); IReadOnlyList vertexInputs = exportEvaluation.VertexInputs ?? []; state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType); draw = new TranslatedGuestDraw( exportShaderAddress, pixelShaderAddress, primitiveType, compiled.Vertex, compiled.Pixel, attributeCount, vertexCount, state.InstanceCount, indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null, textures, globalMemoryBindings, vertexInputs, renderTargets, ApplyTransparentPremultipliedFillClear( CreateRenderState(state.CxRegisters, renderTargets, pixelState), textures, vertexInputs, pixelEvaluation.InitialScalarRegisters)); return true; } private static readonly bool _transparentFillClearEnabled = !string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRANSPARENT_FILL_CLEAR"), "1", StringComparison.Ordinal); /// /// Chowdren resets its effect layers with an untextured transparent-black /// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw /// is otherwise a no-op, causing fog and vignette layers to accumulate. /// Treat precisely that draw shape as an overwrite only when every MRT /// attachment uses the same premultiplied blend pattern. /// private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear( VulkanGuestRenderState renderState, IReadOnlyList textures, IReadOnlyList vertexInputs, IReadOnlyList pixelUserData) { if (!_transparentFillClearEnabled || textures.Count != 0 || vertexInputs.Count != 0 || pixelUserData.Count < 4 || !renderState.Blends.All(IsTransparentPremultipliedFillBlend)) { return renderState; } for (var index = 0; index < 4; index++) { if ((pixelUserData[index] & 0x7FFF_FFFFu) != 0) { return renderState; } } return renderState with { Blends = renderState.Blends .Select(blend => blend with { Enable = false }) .ToArray(), }; } private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) => blend is { Enable: true, ColorSrcFactor: 1, ColorDstFactor: 5, ColorFunc: 0, }; private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer( CpuContext ctx, SubmittedDcbState state, uint indexCount) { if (state.IndexBufferAddress == 0 || indexCount == 0) { return null; } var is32Bit = state.IndexSize != 0; var bytesPerIndex = is32Bit ? sizeof(uint) : sizeof(ushort); var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)bytesPerIndex); var byteCount = checked((int)(indexCount * (uint)bytesPerIndex)); var data = new byte[byteCount]; var address = state.IndexBufferAddress + byteOffset; return (ctx.Memory.TryRead(address, data) || KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, data)) ? new VulkanGuestIndexBuffer(data, is32Bit) : null; } private static bool HasPixelColorExport(Gen5ShaderState state, uint target) => GetPixelColorExportMask(state, target) != 0; private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) => state.Program.Instructions .Select(instruction => instruction.Control) .OfType() .Where(export => export.Target == target) .Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu)); private static uint GetInterpolatedAttributeCount(Gen5ShaderState state) { var maxAttribute = -1; foreach (var instruction in state.Program.Instructions) { if (instruction.Control is Gen5InterpolationControl interpolation) { maxAttribute = Math.Max(maxAttribute, (int)interpolation.Attribute); } } return (uint)(maxAttribute + 1); } private const int ShaderScalarRegisterCount = 256; private static Gen5GlobalMemoryBinding CreateScalarRegisterBinding( Gen5ShaderEvaluation evaluation) { var data = new byte[ShaderScalarRegisterCount * sizeof(uint)]; var registers = evaluation.InitialScalarRegisters; var count = Math.Min(registers.Count, ShaderScalarRegisterCount); for (var index = 0; index < count; index++) { var value = registers[index]; var offset = index * sizeof(uint); data[offset] = (byte)value; data[offset + 1] = (byte)(value >> 8); data[offset + 2] = (byte)(value >> 16); data[offset + 3] = (byte)(value >> 24); } return new Gen5GlobalMemoryBinding(0, 0, [], data); } private static ulong ComputeShaderStructureFingerprint(Gen5ShaderEvaluation evaluation) { const ulong offsetBasis = 14695981039346656037UL; const ulong prime = 1099511628211UL; var hash = offsetBasis; void Mix(ulong value) => hash = (hash ^ value) * prime; Mix((ulong)evaluation.GlobalMemoryBindings.Count); foreach (var binding in evaluation.GlobalMemoryBindings) { Mix(binding.ScalarAddress); Mix((ulong)binding.InstructionPcs.Count); foreach (var pc in binding.InstructionPcs) { Mix(pc); } } Mix((ulong)evaluation.ImageBindings.Count); foreach (var image in evaluation.ImageBindings) { Mix(image.Pc); Mix((ulong)(uint)image.Opcode.GetHashCode()); foreach (var word in image.ResourceDescriptor) { Mix(word); } foreach (var word in image.SamplerDescriptor) { Mix(word); } Mix(image.MipLevel ?? uint.MaxValue); } if (evaluation.VertexInputs is { } vertexInputs) { Mix((ulong)vertexInputs.Count); foreach (var input in vertexInputs) { Mix(input.Pc); Mix(input.Location); Mix(input.ComponentCount); Mix(input.DataFormat); Mix(input.NumberFormat); Mix(input.Stride); } } if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters) { Mix(computeSystemRegisters.WorkGroupXRegister ?? uint.MaxValue); Mix(computeSystemRegisters.WorkGroupYRegister ?? uint.MaxValue); Mix(computeSystemRegisters.WorkGroupZRegister ?? uint.MaxValue); Mix(computeSystemRegisters.ThreadGroupSizeRegister ?? uint.MaxValue); } return hash; } private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation) { const ulong offsetBasis = 14695981039346656037UL; const ulong prime = 1099511628211UL; var hash = offsetBasis; foreach (var value in evaluation.ScalarRegisters) { hash = (hash ^ value) * prime; } if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters) { hash = (hash ^ (computeSystemRegisters.WorkGroupXRegister ?? uint.MaxValue)) * prime; hash = (hash ^ (computeSystemRegisters.WorkGroupYRegister ?? uint.MaxValue)) * prime; hash = (hash ^ (computeSystemRegisters.WorkGroupZRegister ?? uint.MaxValue)) * prime; hash = (hash ^ (computeSystemRegisters.ThreadGroupSizeRegister ?? uint.MaxValue)) * prime; } return hash; } private static IReadOnlyList GetRenderTargets( IReadOnlyDictionary registers) { var hasTargetMask = registers.TryGetValue(CbTargetMask, out var targetMask); var targets = new List(ColorTargetCount); for (uint slot = 0; slot < ColorTargetCount; slot++) { var baseRegister = CbColor0Base + slot * CbColorRegisterStride; if (!registers.TryGetValue(baseRegister, out var baseLow) || !registers.TryGetValue(CbColor0BaseExt + slot, out var baseHigh) || !registers.TryGetValue(CbColor0Attrib2 + slot, out var attrib2) || !registers.TryGetValue(CbColor0Attrib3 + slot, out var attrib3) || !registers.TryGetValue(CbColor0Info + slot * CbColorRegisterStride, out var info)) { continue; } var address = ((ulong)(baseHigh & 0xFFu) << 40) | ((ulong)baseLow << 8); var writeMask = (targetMask >> ((int)slot * 4)) & 0xFu; if (address == 0 || (hasTargetMask && writeMask == 0)) { continue; } targets.Add(new RenderTargetDescriptor( slot, address, ((attrib2 >> 14) & 0x3FFFu) + 1, (attrib2 & 0x3FFFu) + 1, (info >> 2) & 0x1Fu, (info >> 8) & 0x7u, (attrib3 >> 14) & 0x1Fu)); } return targets; } private static VulkanGuestRenderState CreateRenderState( IReadOnlyDictionary registers, IReadOnlyList targets, Gen5ShaderState pixelState) { if (targets.Count == 0) { return VulkanGuestRenderState.Default; } var target = targets[0]; var scissor = DecodeScissor(registers, target.Width, target.Height); return new VulkanGuestRenderState( targets.Select(target => { var blend = DecodeBlendState(registers, target.Slot); return blend with { WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot), }; }).ToArray(), scissor, DecodeViewport(registers, target.Width, target.Height, scissor)); } private static VulkanGuestBlendState DecodeBlendState( IReadOnlyDictionary registers, uint slot) { var writeMask = 0xFu; if (registers.TryGetValue(CbTargetMask, out var targetMask)) { writeMask = (targetMask >> checked((int)(slot * 4))) & 0xFu; } registers.TryGetValue(CbBlend0Control + slot, out var control); return new VulkanGuestBlendState( ((control >> 30) & 1u) != 0, control & 0x1Fu, (control >> 8) & 0x1Fu, (control >> 5) & 0x7u, (control >> 16) & 0x1Fu, (control >> 24) & 0x1Fu, (control >> 21) & 0x7u, ((control >> 29) & 1u) != 0, writeMask); } private static VulkanGuestRect? DecodeScissor( IReadOnlyDictionary registers, uint targetWidth, uint targetHeight) { if (targetWidth == 0 || targetHeight == 0) { return new VulkanGuestRect(0, 0, 0, 0); } var left = 0; var top = 0; var right = checked((int)Math.Min(targetWidth, int.MaxValue)); var bottom = checked((int)Math.Min(targetHeight, int.MaxValue)); var windowOffsetX = 0; var windowOffsetY = 0; var enableWindowOffset = true; if (registers.TryGetValue(PaScWindowScissorTl, out var windowScissorTl)) { enableWindowOffset = (windowScissorTl & 0x80000000u) == 0; } if (enableWindowOffset && registers.TryGetValue(PaScWindowOffset, out var windowOffset)) { windowOffsetX = (short)(windowOffset & 0xFFFFu); windowOffsetY = (short)(windowOffset >> 16); } IntersectScissorPair(registers, PaScScreenScissorTl, PaScScreenScissorBr, ref left, ref top, ref right, ref bottom); IntersectScissorPair( registers, PaScWindowScissorTl, PaScWindowScissorBr, ref left, ref top, ref right, ref bottom, windowOffsetX, windowOffsetY); IntersectScissorPair( registers, PaScGenericScissorTl, PaScGenericScissorBr, ref left, ref top, ref right, ref bottom, windowOffsetX, windowOffsetY); var vportScissorEnabled = !registers.TryGetValue(PaScModeCntl0, out var modeControl) || ((modeControl >> 1) & 1u) != 0; if (vportScissorEnabled) { IntersectScissorPair(registers, PaScVportScissor0Tl, PaScVportScissor0Br, ref left, ref top, ref right, ref bottom); } left = Math.Clamp(left, 0, checked((int)targetWidth)); top = Math.Clamp(top, 0, checked((int)targetHeight)); right = Math.Clamp(right, left, checked((int)targetWidth)); bottom = Math.Clamp(bottom, top, checked((int)targetHeight)); if (left == 0 && top == 0 && right == (int)targetWidth && bottom == (int)targetHeight) { return null; } return new VulkanGuestRect( left, top, checked((uint)(right - left)), checked((uint)(bottom - top))); } private static VulkanGuestViewport? DecodeViewport( IReadOnlyDictionary registers, uint targetWidth, uint targetHeight, VulkanGuestRect? scissor) { if (targetWidth == 0 || targetHeight == 0) { return new VulkanGuestViewport(0, 0, 0, 0, 0, 1); } var minDepth = 0f; var maxDepth = 1f; if (registers.TryGetValue(PaScVportZMin0, out var zMinBits) && registers.TryGetValue(PaScVportZMax0, out var zMaxBits)) { var decodedMin = BitConverter.UInt32BitsToSingle(zMinBits); var decodedMax = BitConverter.UInt32BitsToSingle(zMaxBits); if (float.IsFinite(decodedMin) && float.IsFinite(decodedMax) && decodedMax > decodedMin) { minDepth = decodedMin; maxDepth = decodedMax; } } if (TryDecodeFiniteFloat(registers, PaClVportXScale, out var xScale) && TryDecodeFiniteFloat(registers, PaClVportXOffset, out var xOffset) && TryDecodeFiniteFloat(registers, PaClVportYScale, out var yScale) && TryDecodeFiniteFloat(registers, PaClVportYOffset, out var yOffset) && xScale > 0f && yScale != 0f) { return new VulkanGuestViewport( xOffset - xScale, yOffset - yScale, xScale * 2f, yScale * 2f, minDepth, maxDepth); } if (scissor is not { } rect) { return minDepth == 0f && maxDepth == 1f ? null : new VulkanGuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth); } return new VulkanGuestViewport( rect.X, rect.Y, rect.Width, rect.Height, minDepth, maxDepth); } private static bool TryDecodeFiniteFloat( IReadOnlyDictionary registers, uint register, out float value) { value = 0; if (!registers.TryGetValue(register, out var bits)) { return false; } value = BitConverter.UInt32BitsToSingle(bits); return float.IsFinite(value); } private static void IntersectScissorPair( IReadOnlyDictionary registers, uint tlRegister, uint brRegister, ref int left, ref int top, ref int right, ref int bottom, int offsetX = 0, int offsetY = 0) { if (!TryDecodeScissorPair(registers, tlRegister, brRegister, out var pairLeft, out var pairTop, out var pairRight, out var pairBottom)) { return; } pairLeft += offsetX; pairTop += offsetY; pairRight += offsetX; pairBottom += offsetY; left = Math.Max(left, pairLeft); top = Math.Max(top, pairTop); right = Math.Min(right, pairRight); bottom = Math.Min(bottom, pairBottom); } private static bool TryDecodeScissorPair( IReadOnlyDictionary registers, uint tlRegister, uint brRegister, out int left, out int top, out int right, out int bottom) { left = 0; top = 0; right = 0; bottom = 0; if (!registers.TryGetValue(tlRegister, out var tl) || !registers.TryGetValue(brRegister, out var br)) { return false; } left = (int)(tl & 0x7FFFu); top = (int)((tl >> 16) & 0x7FFFu); right = (int)(br & 0x7FFFu); bottom = (int)((br >> 16) & 0x7FFFu); return true; } private static void TraceTranslatedGuestDraw( CpuContext ctx, SubmittedGpuState gpuState, SubmittedDcbState state, TranslatedGuestDraw draw, uint psInputEna, uint psInputAddr) { var targets = draw.RenderTargets.Count == 0 ? "none" : string.Join( ',', draw.RenderTargets.Select(target => $"{target.Slot}:0x{target.Address:X16}:{target.Width}x{target.Height}:" + $"fmt{target.Format}/num{target.NumberType}/tile{target.TileMode}")); var probes = new Dictionary(); var textures = string.Join( ',', draw.Textures.Select(binding => { var texture = binding.Descriptor; var targetSlot = draw.RenderTargets .FirstOrDefault(target => target.Address == texture.Address) .Slot; var target = draw.RenderTargets.Any(candidate => candidate.Address == texture.Address) ? $"/rt{targetSlot}" : string.Empty; if (!probes.TryGetValue(texture.Address, out var probe)) { probe = ProbeTexture(ctx, texture); probes.Add(texture.Address, probe); } state.RenderTargetWriters.TryGetValue(texture.Address, out var sourceWriter); gpuState.ComputeImageWriters.TryGetValue(texture.Address, out var computeWriter); var writer = sourceWriter.Sequence >= computeWriter.Sequence && sourceWriter.Sequence != 0 ? $"/writer={sourceWriter.Sequence}:" + $"es0x{sourceWriter.ExportShaderAddress:X}:" + $"ps0x{sourceWriter.PixelShaderAddress:X}:" + $"v{sourceWriter.VertexCount}:prim0x{sourceWriter.PrimitiveType:X}" : computeWriter.Sequence != 0 ? $"/compute={computeWriter.Sequence}:" + $"cs0x{computeWriter.ShaderAddress:X}:{computeWriter.Opcode}" : "/writer=none"; return $"0x{texture.Address:X16}:{texture.Width}x{texture.Height}:" + $"fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode}" + $"/storage={binding.IsStorage}{target}/{probe}{writer}"; })); var buffers = string.Join( ',', draw.GlobalMemoryBindings.Select((binding, index) => $"{index}:0x{binding.BaseAddress:X16}:{binding.Data.Length}:" + Convert.ToHexString(binding.Data.AsSpan(0, Math.Min(binding.Data.Length, 256))))); var indices = draw.IndexBuffer is { } indexBuffer ? $"{(indexBuffer.Is32Bit ? 32 : 16)}:" + Convert.ToHexString(indexBuffer.Data.AsSpan(0, Math.Min(indexBuffer.Data.Length, 32))) : "none"; var vertexInputs = draw.VertexInputs.Count == 0 ? "none" : string.Join( ',', draw.VertexInputs.Select(input => $"{input.Location}:pc=0x{input.Pc:X}:0x{input.BaseAddress:X16}" + $":stride{input.Stride}:off{input.OffsetBytes}:c{input.ComponentCount}" + $":fmt{input.DataFormat}/num{input.NumberFormat}")); var scissor = draw.RenderState.Scissor is { } drawScissor ? $"{drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height}" : "full"; var viewport = draw.RenderState.Viewport is { } drawViewport ? $"{drawViewport.X:0.###},{drawViewport.Y:0.###}," + $"{drawViewport.Width:0.###}x{drawViewport.Height:0.###}:" + $"{drawViewport.MinDepth:0.###}-{drawViewport.MaxDepth:0.###}" : "full"; var rasterRegisters = new (string Name, uint Offset)[] { ("screen_tl", PaScScreenScissorTl), ("screen_br", PaScScreenScissorBr), ("window_off", PaScWindowOffset), ("window_tl", PaScWindowScissorTl), ("window_br", PaScWindowScissorBr), ("generic_tl", PaScGenericScissorTl), ("generic_br", PaScGenericScissorBr), ("vport_tl", PaScVportScissor0Tl), ("vport_br", PaScVportScissor0Br), ("mode", PaScModeCntl0), ("xscale", PaClVportXScale), ("xoffset", PaClVportXOffset), ("yscale", PaClVportYScale), ("yoffset", PaClVportYOffset), }; var raster = string.Join( ',', rasterRegisters.Select(entry => state.CxRegisters.TryGetValue(entry.Offset, out var value) ? $"{entry.Name}=0x{value:X8}" : $"{entry.Name}=missing")); var blend = draw.RenderState.Blend; TraceAgcShader( $"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " + $"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelSpirv.Length} " + $"primitive=0x{draw.PrimitiveType:X} " + $"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " + $"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " + $"raster=[{raster}] " + $"ps_ena=0x{psInputEna:X8} ps_addr=0x{psInputAddr:X8} " + $"targets=[{targets}] textures=[{textures}] " + $"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]"); } private static IReadOnlyList CreateVulkanGuestDrawTextures( CpuContext ctx, IReadOnlyList bindings, out int fallbackTextureCount) { var textures = new List(bindings.Count); fallbackTextureCount = 0; foreach (var binding in bindings) { if (TryCreateVulkanGuestDrawTexture( ctx, binding.Descriptor, binding.IsStorage, binding.MipLevel, binding.SamplerDescriptor, out var texture)) { textures.Add(texture); if (texture.IsFallback) { fallbackTextureCount++; } } } return textures; } private static IReadOnlyList CreateVulkanGuestMemoryBuffers( IReadOnlyList bindings) { var buffers = new VulkanGuestMemoryBuffer[bindings.Count]; for (var index = 0; index < bindings.Count; index++) { buffers[index] = new VulkanGuestMemoryBuffer( bindings[index].BaseAddress, bindings[index].Data); } return buffers; } private static IReadOnlyList CreateVulkanGuestVertexBuffers( IReadOnlyList bindings) { var buffers = new VulkanGuestVertexBuffer[bindings.Count]; for (var index = 0; index < bindings.Count; index++) { var binding = bindings[index]; buffers[index] = new VulkanGuestVertexBuffer( binding.Location, binding.ComponentCount, binding.DataFormat, binding.NumberFormat, binding.BaseAddress, binding.Stride, binding.OffsetBytes, binding.Data); } return buffers; } private static bool TryCreateVulkanGuestDrawTexture( CpuContext ctx, TextureDescriptor descriptor, bool isStorage, uint mipLevel, IReadOnlyList samplerDescriptor, out VulkanGuestDrawTexture texture) { texture = default!; if (descriptor.Type != Gen5TextureType2D || descriptor.Width == 0 || descriptor.Height == 0 || descriptor.Width > 8192 || descriptor.Height > 8192) { texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType); return true; } var sourceWidth = descriptor.TileMode == 0 ? GetLinearTexturePitch( Math.Max(descriptor.Width, descriptor.Pitch), descriptor.Format) : descriptor.Width; var sourceByteCount = GetTextureByteCount( descriptor.Format, sourceWidth, descriptor.Height); if (sourceByteCount == 0 || sourceByteCount > MaxPresentedTextureBytes || sourceByteCount > int.MaxValue) { texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType); return true; } if (!isStorage && descriptor.Address != 0 && VulkanVideoPresenter.IsGpuGuestImageAvailable( descriptor.Address, descriptor.Format, descriptor.NumberType)) { texture = new VulkanGuestDrawTexture( descriptor.Address, descriptor.Width, descriptor.Height, descriptor.Format, descriptor.NumberType, [], IsFallback: false, IsStorage: false, MipLevels: descriptor.MipLevels, MipLevel: mipLevel, Pitch: sourceWidth, TileMode: descriptor.TileMode, DstSelect: descriptor.DstSelect, Sampler: ToVulkanSampler(samplerDescriptor)); return true; } if (isStorage) { var initialPixels = Array.Empty(); if (descriptor.Address != 0) { var storageSource = new byte[(int)sourceByteCount]; if (ctx.Memory.TryRead(descriptor.Address, storageSource) && storageSource.AsSpan().IndexOfAnyExcept((byte)0) >= 0) { initialPixels = storageSource; } } texture = new VulkanGuestDrawTexture( descriptor.Address, descriptor.Width, descriptor.Height, descriptor.Format, descriptor.NumberType, initialPixels, IsFallback: descriptor.Address == 0, IsStorage: true, MipLevels: descriptor.MipLevels, MipLevel: mipLevel, Pitch: sourceWidth, TileMode: descriptor.TileMode, DstSelect: descriptor.DstSelect, Sampler: ToVulkanSampler(samplerDescriptor)); return true; } var source = new byte[(int)sourceByteCount]; if (!ctx.Memory.TryRead(descriptor.Address, source)) { texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType); return true; } TraceTextureHash(descriptor, source); var nonZero = 0; for (var i = 0; i < source.Length; i++) { if (source[i] != 0) { nonZero++; if (nonZero >= 64) { break; } } } TraceAgcShader( $"agc.texture_source addr=0x{descriptor.Address:X16} " + $"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " + $"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " + $"dst=0x{descriptor.DstSelect:X3} " + $"bytes={source.Length} nonzero64={nonZero}"); var rgba = source; texture = new VulkanGuestDrawTexture( descriptor.Address, descriptor.Width, descriptor.Height, descriptor.Format, descriptor.NumberType, rgba, IsFallback: false, IsStorage: isStorage, MipLevels: descriptor.MipLevels, MipLevel: mipLevel, Pitch: sourceWidth, TileMode: descriptor.TileMode, DstSelect: descriptor.DstSelect, Sampler: ToVulkanSampler(samplerDescriptor)); return true; } private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture( bool isStorage, uint format, uint numberType) { var fallbackFormat = format == 0 ? 10u : format; var fallbackNumberType = numberType; return new( 0, 1, 1, fallbackFormat, fallbackNumberType, [0, 0, 0, 255], IsFallback: true, IsStorage: isStorage, MipLevels: 1, MipLevel: 0); } private static void TraceTextureHash(TextureDescriptor descriptor, ReadOnlySpan source) { if (!_traceTextureHashes || descriptor.Address == 0 || descriptor.Width > 256 || descriptor.Height > 256) { return; } var hash = ComputeFingerprint(source); var key = (descriptor.Address, descriptor.Width, descriptor.Height); lock (_textureHashTraceGate) { if (_tracedTextureHashes.TryGetValue(key, out var previousHash) && previousHash == hash) { return; } _tracedTextureHashes[key] = hash; } Console.Error.WriteLine( $"[LOADER][TRACE] agc.texture_hash addr=0x{descriptor.Address:X16} " + $"size={descriptor.Width}x{descriptor.Height} bytes={source.Length} hash=0x{hash:X16}"); } private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList descriptor) => descriptor.Count >= 4 ? new VulkanGuestSampler( descriptor[0], descriptor[1], descriptor[2], descriptor[3]) : default; private static byte[] ConvertRgba16FloatToRgba8(ReadOnlySpan source, uint width, uint height) { var destination = new byte[checked((int)((ulong)width * height * 4))]; var pixelCount = destination.Length / 4; for (var pixel = 0; pixel < pixelCount; pixel++) { var sourceOffset = pixel * 8; var destinationOffset = pixel * 4; destination[destinationOffset + 0] = HalfToByte(BinaryPrimitives.ReadUInt16LittleEndian(source[sourceOffset..])); destination[destinationOffset + 1] = HalfToByte(BinaryPrimitives.ReadUInt16LittleEndian(source[(sourceOffset + 2)..])); destination[destinationOffset + 2] = HalfToByte(BinaryPrimitives.ReadUInt16LittleEndian(source[(sourceOffset + 4)..])); destination[destinationOffset + 3] = HalfToByte(BinaryPrimitives.ReadUInt16LittleEndian(source[(sourceOffset + 6)..])); } return destination; } private static byte HalfToByte(ushort bits) { var value = (float)BitConverter.UInt16BitsToHalf(bits); if (!float.IsFinite(value)) { return 0; } return (byte)Math.Clamp((int)MathF.Round(value * 255.0f), 0, 255); } private static bool TryReadComputeDispatch( CpuContext ctx, SubmittedDcbState state, ulong packetAddress, uint packetLength, uint opcode, out ComputeDispatch dispatch) { dispatch = default; ulong dimensionsAddress; uint initiator; if (opcode == ItDispatchDirect) { if (packetLength < 5 || !ctx.TryReadUInt32(packetAddress + 16, out initiator)) { return false; } dimensionsAddress = packetAddress + 4; } else if (packetLength >= 4) { if (!ctx.TryReadUInt64(packetAddress + 4, out dimensionsAddress) || !ctx.TryReadUInt32(packetAddress + 12, out initiator)) { return false; } } else { if (packetLength < 3 || state.IndirectArgsAddress == 0 || !ctx.TryReadUInt32(packetAddress + 4, out var dataOffset) || !ctx.TryReadUInt32(packetAddress + 8, out initiator)) { return false; } dimensionsAddress = state.IndirectArgsAddress + dataOffset; } if ((initiator & 1) == 0 || !ctx.TryReadUInt32(dimensionsAddress, out var groupCountX) || !ctx.TryReadUInt32(dimensionsAddress + 4, out var groupCountY) || !ctx.TryReadUInt32(dimensionsAddress + 8, out var groupCountZ) || groupCountX == 0 || groupCountY == 0 || groupCountZ == 0) { return false; } dispatch = new ComputeDispatch(groupCountX, groupCountY, groupCountZ); return true; } private static void ObserveComputeDispatch( CpuContext ctx, SubmittedGpuState gpuState, SubmittedDcbState state, ComputeDispatch dispatch) { if (!TryGetShaderAddress( state.ShRegisters, ComputePgmLo, ComputePgmHi, out var shaderAddress)) { return; } var sequence = ++gpuState.WorkSequence; ulong shaderHeader; lock (_submitTraceGate) { _shaderHeadersByCode.TryGetValue(shaderAddress, out shaderHeader); } var computeSystemRegisters = DecodeComputeSystemRegisters(state.ShRegisters); if (!Gen5ShaderTranslator.TryCreateState( ctx, shaderAddress, shaderHeader, state.ShRegisters, ComputeUserDataRegister, out var shaderState, out var error, computeSystemRegisters) || !Gen5ShaderScalarEvaluator.TryEvaluate( ctx, shaderState, out var evaluation, out error)) { lock (_submitTraceGate) { if (_tracedComputeShaders.Add(shaderAddress)) { TraceAgcShader( $"agc.compute_shader cs=0x{shaderAddress:X16} error={error}"); } } return; } var bindings = evaluation.ImageBindings; var descriptions = new List(bindings.Count); var translatedBindings = new List(bindings.Count); var hasStorageBinding = false; foreach (var binding in bindings) { var isStorage = Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode); var descriptorValid = TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture); if (!descriptorValid) { texture = CreateFallbackTextureDescriptor(binding.ResourceDescriptor); } translatedBindings.Add( new TranslatedImageBinding( texture, isStorage, binding.MipLevel ?? 0, binding.SamplerDescriptor)); hasStorageBinding |= isStorage; var descriptorState = descriptorValid ? string.Empty : "/invalid-desc"; descriptions.Add( $"{binding.Opcode}@0x{binding.Pc:X}:" + $"0x{texture.Address:X16}:{texture.Width}x{texture.Height}:" + $"fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode}" + $"{descriptorState}/{ProbeTexture(ctx, texture)}"); if (isStorage && descriptorValid && texture.Address != 0) { gpuState.ComputeImageWriters[texture.Address] = new ComputeImageWriter( sequence, shaderAddress, binding.Opcode); TraceAgcShader( $"agc.compute_writer addr=0x{texture.Address:X16} " + $"fmt={texture.Format} num={texture.NumberType} tile={texture.TileMode} " + $"size={texture.Width}x{texture.Height} " + $"cs=0x{shaderAddress:X16} op={binding.Opcode}"); } } var localSizeX = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadX); var localSizeY = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadY); var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ); var gpuDispatch = false; var computeError = string.Empty; if ((hasStorageBinding || evaluation.GlobalMemoryBindings.Count != 0) && (ulong)localSizeX * localSizeY * localSizeZ <= 1024) { var shaderKey = ( shaderAddress, ComputeShaderStateFingerprint(evaluation), localSizeX, localSizeY, localSizeZ); byte[] computeSpirv; lock (_submitTraceGate) { _computeSpirvCache.TryGetValue(shaderKey, out computeSpirv!); } if (computeSpirv is null && Gen5SpirvTranslator.TryCompileComputeShader( shaderState, evaluation, localSizeX, localSizeY, localSizeZ, out var compiledCompute, out computeError)) { computeSpirv = compiledCompute.Spirv; DumpSpirv( "cs", shaderAddress, shaderKey.Item2, computeSpirv, shaderState.Program); } if (computeSpirv is not null) { lock (_submitTraceGate) { _computeSpirvCache.TryAdd(shaderKey, computeSpirv); } var textures = CreateVulkanGuestDrawTextures( ctx, translatedBindings, out _); var globalMemoryBuffers = CreateVulkanGuestMemoryBuffers(evaluation.GlobalMemoryBindings); VulkanVideoPresenter.SubmitComputeDispatch( shaderAddress, computeSpirv, textures, globalMemoryBuffers, dispatch.GroupCountX, dispatch.GroupCountY, dispatch.GroupCountZ); gpuDispatch = true; } } const int blitCount = 0; lock (_submitTraceGate) { if (_tracedComputeShaders.Add(shaderAddress)) { TraceAgcShader( $"agc.compute_shader cs=0x{shaderAddress:X16} " + $"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " + $"local={localSizeX}x{localSizeY}x{localSizeZ} " + $"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " + $"gpu={gpuDispatch} blits={blitCount} " + $"globals={evaluation.GlobalMemoryBindings.Count}" + (computeError.Length == 0 ? string.Empty : $" error={computeError}") + $" bindings=[{string.Join(',', descriptions)}]"); } } } private static Gen5ComputeSystemRegisters DecodeComputeSystemRegisters( IReadOnlyDictionary registers) { registers.TryGetValue(ComputePgmRsrc2, out var rsrc2); var nextRegister = (rsrc2 >> 1) & 0x1Fu; uint? workGroupX = null; uint? workGroupY = null; uint? workGroupZ = null; uint? threadGroupSize = null; if ((rsrc2 & (1u << 7)) != 0) { workGroupX = nextRegister++; } if ((rsrc2 & (1u << 8)) != 0) { workGroupY = nextRegister++; } if ((rsrc2 & (1u << 9)) != 0) { workGroupZ = nextRegister++; } if ((rsrc2 & (1u << 10)) != 0) { threadGroupSize = nextRegister++; } return new Gen5ComputeSystemRegisters( workGroupX, workGroupY, workGroupZ, threadGroupSize); } private static string DescribeComputeSystemRegisters(Gen5ComputeSystemRegisters registers) => $"x={DescribeRegister(registers.WorkGroupXRegister)}," + $"y={DescribeRegister(registers.WorkGroupYRegister)}," + $"z={DescribeRegister(registers.WorkGroupZRegister)}," + $"size={DescribeRegister(registers.ThreadGroupSizeRegister)}"; private static string DescribeRegister(uint? register) => register.HasValue ? $"s{register.Value}" : "-"; private static uint SelectExportUserDataRegister( IReadOnlyDictionary registers) { if (HasUserDataRange(registers, GsUserDataRegister)) { return GsUserDataRegister; } if (HasUserDataRange(registers, EsUserDataRegister)) { return EsUserDataRegister; } if (HasUserDataRange(registers, VsUserDataRegister)) { return VsUserDataRegister; } var esValues = CountUserDataValues(registers, EsUserDataRegister); var vsValues = CountUserDataValues(registers, VsUserDataRegister); return esValues == 0 && vsValues != 0 ? VsUserDataRegister : EsUserDataRegister; } private static bool HasUserDataRange( IReadOnlyDictionary registers, uint startRegister) { for (var index = 0u; index < 16; index++) { if (registers.ContainsKey(startRegister + index)) { return true; } } return false; } private static int CountUserDataValues( IReadOnlyDictionary registers, uint startRegister) { var count = 0; for (var index = 0u; index < 16; index++) { count += registers.TryGetValue(startRegister + index, out var value) && value != 0 ? 1 : 0; } return count; } private static uint GetComputeLocalSize( IReadOnlyDictionary registers, uint register) { return registers.TryGetValue(register, out var value) ? Math.Max(value & 0x3FFu, 1u) : 1u; } private static int TryApplySoftwareComputeBlits( CpuContext ctx, ulong shaderAddress, IReadOnlyList<(Gen5ImageBinding Binding, TextureDescriptor Texture)> bindings) { var blits = 0; TextureDescriptor? source = null; foreach (var (binding, texture) in bindings) { if (binding.Opcode.StartsWith("ImageStore", StringComparison.Ordinal)) { if (source is { } sourceTexture && TrySoftwareTextureBlit(ctx, sourceTexture, texture, out var fingerprint)) { blits++; var key = (shaderAddress, sourceTexture.Address, texture.Address); lock (_softwarePresenterGate) { if (!_softwareComputeBlitFingerprints.TryGetValue(key, out var previous) || previous != fingerprint) { _softwareComputeBlitFingerprints[key] = fingerprint; TraceAgcShader( $"agc.compute_blit cs=0x{shaderAddress:X16} " + $"src=0x{sourceTexture.Address:X16}:{sourceTexture.Width}x{sourceTexture.Height}:fmt{sourceTexture.Format}/num{sourceTexture.NumberType}/tile{sourceTexture.TileMode} " + $"dst=0x{texture.Address:X16}:{texture.Width}x{texture.Height}:fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode} " + $"fingerprint=0x{fingerprint:X16}"); } } } else if (source is { } cachedSourceTexture && VulkanVideoPresenter.TrySubmitGuestImageBlit( cachedSourceTexture.Address, cachedSourceTexture.Width, cachedSourceTexture.Height, cachedSourceTexture.Format, texture.Address, texture.Width, texture.Height, texture.Format)) { blits++; TraceAgcShader( $"agc.compute_gpu_blit cs=0x{shaderAddress:X16} " + $"src=0x{cachedSourceTexture.Address:X16}:{cachedSourceTexture.Width}x{cachedSourceTexture.Height}:fmt{cachedSourceTexture.Format}/num{cachedSourceTexture.NumberType}/tile{cachedSourceTexture.TileMode} " + $"dst=0x{texture.Address:X16}:{texture.Width}x{texture.Height}:fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode}"); } continue; } if (binding.Opcode.StartsWith("Image", StringComparison.Ordinal)) { source = texture; } } return blits; } private static bool TrySoftwareTextureBlit( CpuContext ctx, TextureDescriptor source, TextureDescriptor destination, out ulong fingerprint) { fingerprint = 0; var bytesPerTexel = GetTextureBytesPerTexel(source.Format); if (bytesPerTexel == 0 || bytesPerTexel != GetTextureBytesPerTexel(destination.Format) || source.Type != Gen5TextureType2D || destination.Type != Gen5TextureType2D || source.Width == 0 || source.Height == 0 || destination.Width == 0 || destination.Height == 0 || source.Width > 8192 || source.Height > 8192 || destination.Width > 8192 || destination.Height > 8192) { return false; } var sourceBytes = checked((ulong)source.Width * source.Height * bytesPerTexel); var destinationBytes = checked((ulong)destination.Width * destination.Height * bytesPerTexel); if (sourceBytes == 0 || destinationBytes == 0 || sourceBytes > MaxPresentedTextureBytes || destinationBytes > MaxPresentedTextureBytes || sourceBytes > int.MaxValue || destinationBytes > int.MaxValue) { return false; } var sourceData = new byte[(int)sourceBytes]; if (!ctx.Memory.TryRead(source.Address, sourceData)) { return false; } var nonzero = 0; foreach (var value in sourceData) { if (value != 0) { nonzero++; break; } } if (nonzero == 0) { return false; } var destinationData = new byte[(int)destinationBytes]; for (uint y = 0; y < destination.Height; y++) { var sourceY = (uint)(((ulong)y * source.Height) / destination.Height); for (uint x = 0; x < destination.Width; x++) { var sourceX = (uint)(((ulong)x * source.Width) / destination.Width); var sourceOffset = checked((int)(((ulong)sourceY * source.Width + sourceX) * bytesPerTexel)); var destinationOffset = checked((int)(((ulong)y * destination.Width + x) * bytesPerTexel)); sourceData.AsSpan(sourceOffset, (int)bytesPerTexel) .CopyTo(destinationData.AsSpan(destinationOffset, (int)bytesPerTexel)); } } if (!ctx.Memory.TryWrite(destination.Address, destinationData)) { return false; } fingerprint = ComputeFingerprint(destinationData); return true; } private static string ProbeTexture(CpuContext ctx, TextureDescriptor texture) { if (texture.Width == 0 || texture.Height == 0) { return "probe=unsupported"; } var totalBytes = GetTextureByteCount( texture.Format, texture.Width, texture.Height); if (totalBytes == 0) { return "probe=unsupported"; } const int sampleCount = 32; const int sampleSize = 256; var sample = new byte[sampleSize]; var reads = 0; var nonzero = 0; const ulong offsetBasis = 14695981039346656037UL; const ulong prime = 1099511628211UL; var hash = offsetBasis; for (var index = 0; index < sampleCount; index++) { var maxOffset = totalBytes > sampleSize ? totalBytes - sampleSize : 0; var offset = sampleCount == 1 ? 0 : maxOffset * (ulong)index / (sampleCount - 1); if (!ctx.Memory.TryRead(texture.Address + offset, sample)) { continue; } reads++; foreach (var value in sample) { if (value != 0) { nonzero++; } hash = (hash ^ value) * prime; } } var bytesPerTexel = GetTextureBytesPerTexel(texture.Format); var texels = bytesPerTexel is > 0 and <= 16 ? string.Join( '/', ProbeTextureTexel(ctx, texture.Address, (int)bytesPerTexel), ProbeTextureTexel( ctx, texture.Address + (((ulong)(texture.Height / 2) * texture.Width) + (texture.Width / 2)) * bytesPerTexel, (int)bytesPerTexel), ProbeTextureTexel( ctx, texture.Address + totalBytes - bytesPerTexel, (int)bytesPerTexel)) : "unsupported"; return $"probe={reads}/{sampleCount}:{nonzero}:0x{hash:X16}:texels={texels}"; } private static string ProbeTextureTexel(CpuContext ctx, ulong address, int size) { var texel = new byte[size]; return ctx.Memory.TryRead(address, texel) ? Convert.ToHexString(texel) : "unreadable"; } private static ulong GetTextureBytesPerTexel(uint format) => format switch { 1 => 1UL, 2 => 2UL, 3 => 2UL, 4 => 4UL, 5 => 4UL, 6 => 4UL, 7 => 4UL, 9 => 4UL, 10 => 4UL, 11 => 8UL, 12 => 8UL, 13 => 12UL, 14 => 16UL, 20 => 4UL, 22 => 8UL, 29 => 4UL, 36 => 1UL, 49 => 1UL, Gen5TextureFormatR8G8B8A8Unorm => 4UL, 62 => 4UL, 64 => 4UL, Gen5TextureFormatR16G16B16A16Float => 8UL, 75 => 8UL, _ => 0UL, }; private static ulong GetTextureByteCount(uint format, uint width, uint height) { var bytesPerTexel = GetTextureBytesPerTexel(format); if (bytesPerTexel != 0) { return checked((ulong)width * height * bytesPerTexel); } var blockBytes = format switch { 169 or 170 => 8UL, 171 or 172 or 173 or 174 or 175 or 176 or 177 or 178 or 179 or 180 or 181 or 182 => 16UL, _ => 0UL, }; return blockBytes == 0 ? 0 : checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes); } private static uint GetLinearTexturePitch(uint pitch, uint format) { var bytesPerTexel = GetTextureBytesPerTexel(format); if (bytesPerTexel == 0) { return pitch; } // GFX10 ADDR_SW_LINEAR aligns each row to 256 bytes. var pitchAlignment = Math.Max(1UL, 256UL / bytesPerTexel); return checked((uint)AlignUp(pitch, pitchAlignment)); } private static ulong AlignUp(ulong value, ulong alignment) => (value + alignment - 1) & ~(alignment - 1); private static void TraceShaderTranslationMiss( CpuContext ctx, SubmittedDcbState state, uint vertexCount, bool hasExportShader, ulong exportShaderAddress, bool hasPixelShader, ulong pixelShaderAddress, bool hasPsInputEna, uint psInputEna, bool hasPsInputAddr, uint psInputAddr, string? translationError = null) { var firstFailure = false; if (!string.IsNullOrEmpty(translationError)) { lock (_submitTraceGate) { firstFailure = _tracedShaderFailures.Add( (pixelShaderAddress, translationError)); } } if (!firstFailure && !ShouldTraceHotPath(ref _shaderTranslationMissTraceCount)) { return; } if ((!hasPixelShader || !hasPsInputEna || !hasPsInputAddr) && TryMarkMissingPixelShaderBindingsTrace()) { TraceAgcShader( $"agc.shader_register_candidates " + DescribeShaderRegisterCandidates(ctx, state.ShRegisters)); } var shaderDecode = string.Empty; if (hasExportShader && hasPixelShader) { var shouldDescribe = false; ulong exportShaderHeader; ulong pixelShaderHeader; lock (_submitTraceGate) { shouldDescribe = _tracedShaderDecodePairs.Add((exportShaderAddress, pixelShaderAddress)); _shaderHeadersByCode.TryGetValue(exportShaderAddress, out exportShaderHeader); _shaderHeadersByCode.TryGetValue(pixelShaderAddress, out pixelShaderHeader); } if (shouldDescribe) { shaderDecode = $" decode={Gen5ShaderTranslator.Describe(ctx, exportShaderAddress, pixelShaderAddress)}"; TraceAgcShader( $"agc.shader_words es=0x{exportShaderAddress:X16} " + Gen5ShaderTranslator.DescribeWords(ctx, exportShaderAddress)); if (Gen5ShaderTranslator.TryCreateState( ctx, exportShaderAddress, exportShaderHeader, state.ShRegisters, SelectExportUserDataRegister(state.ShRegisters), out var exportState, out _, userDataScalarRegisterBase: NggUserDataScalarRegisterBase) && Gen5ShaderTranslator.TryCreateState( ctx, pixelShaderAddress, pixelShaderHeader, state.ShRegisters, PsTextureUserDataRegister, out var pixelState, out _)) { TraceAgcShader( $"agc.shader_state es=0x{exportShaderAddress:X16} " + Gen5ShaderTranslator.DescribeState(exportState)); TraceAgcShader( $"agc.shader_state ps=0x{pixelShaderAddress:X16} " + Gen5ShaderTranslator.DescribeState(pixelState)); if (Gen5ShaderScalarEvaluator.TryEvaluate( ctx, pixelState, out var evaluation, out var bindingError)) { foreach (var binding in evaluation.ImageBindings) { TraceAgcShader( $"agc.shader_binding ps=0x{pixelShaderAddress:X16} " + $"pc=0x{binding.Pc:X} op={binding.Opcode} " + $"resource={FormatShaderDwords(binding.ResourceDescriptor)} " + $"sampler={FormatShaderDwords(binding.SamplerDescriptor)}"); } foreach (var binding in evaluation.GlobalMemoryBindings) { TraceAgcShader( $"agc.shader_global_binding ps=0x{pixelShaderAddress:X16} " + $"saddr=s{binding.ScalarAddress} " + $"base=0x{binding.BaseAddress:X16} bytes={binding.Data.Length} " + $"pcs={string.Join(',', binding.InstructionPcs.Select(pc => $"0x{pc:X}"))}"); } if (Gen5SpirvTranslator.TryCompilePixelShader( pixelState, evaluation, [new(0, 0, Gen5PixelOutputKind.Float)], out var compiledPixel, out var compileError)) { TraceAgcShader( $"agc.shader_spirv ps=0x{pixelShaderAddress:X16} " + $"bytes={compiledPixel.Spirv.Length} bindings={evaluation.ImageBindings.Count} " + $"global_buffers={evaluation.GlobalMemoryBindings.Count}"); } else { TraceAgcShader( $"agc.shader_spirv_error ps=0x{pixelShaderAddress:X16} " + compileError.ReplaceLineEndings(" ")); } } else { TraceAgcShader( $"agc.shader_binding_error ps=0x{pixelShaderAddress:X16} " + bindingError); } } } } TraceAgcShader( $"agc.shader_translate_miss vertices={vertexCount} " + $"es={(hasExportShader ? $"0x{exportShaderAddress:X16}" : "missing")} " + $"ps={(hasPixelShader ? $"0x{pixelShaderAddress:X16}" : "missing")} " + $"ps_ena={(hasPsInputEna ? $"0x{psInputEna:X8}" : "missing")} " + $"ps_addr={(hasPsInputAddr ? $"0x{psInputAddr:X8}" : "missing")}" + (string.IsNullOrEmpty(translationError) ? string.Empty : $" error={translationError}") + shaderDecode); } private static bool TryMarkMissingPixelShaderBindingsTrace() { lock (_submitTraceGate) { if (_tracedMissingPixelShaderBindings) { return false; } _tracedMissingPixelShaderBindings = true; return true; } } private static string DescribeShaderRegisterCandidates( CpuContext ctx, IReadOnlyDictionary registers) { var candidates = new List<(uint Register, ulong Address, ulong Header)>(); lock (_submitTraceGate) { foreach (var (register, lo) in registers) { if (!registers.TryGetValue(register + 1, out var hi)) { continue; } var address = ((ulong)hi << 40) | ((ulong)lo << 8); if (address != 0 && _shaderHeadersByCode.TryGetValue(address, out var header)) { candidates.Add((register, address, header)); } } } if (candidates.Count == 0) { return "none"; } return string.Join( ',', candidates .OrderBy(candidate => candidate.Register) .Take(16) .Select(candidate => { var type = ctx.TryReadByte( candidate.Header + ShaderTypeOffset, out var shaderType) ? shaderType.ToString() : "?"; return $"sh[0x{candidate.Register:X}/0x{candidate.Register + 1:X}]=" + $"0x{candidate.Address:X16}:type{type}"; })); } private static bool TryGetShaderAddress( IReadOnlyDictionary registers, uint loRegister, uint hiRegister, out ulong address) { address = 0; if (!registers.TryGetValue(loRegister, out var lo) || !registers.TryGetValue(hiRegister, out var hi)) { return false; } address = ((ulong)hi << 40) | ((ulong)lo << 8); return address != 0; } private static bool TryReadTextureDescriptor( CpuContext ctx, ulong packetAddress, uint packetLength, out TextureDescriptor descriptor) { descriptor = default; if (packetLength < 10 || !ctx.TryReadUInt32(packetAddress + 4, out var startRegister)) { return false; } var valueCount = packetLength - 2; if (startRegister > PsTextureUserDataRegister || startRegister + valueCount < PsTextureUserDataRegister + 8) { return false; } var descriptorAddress = packetAddress + 8 + ((ulong)(PsTextureUserDataRegister - startRegister) * sizeof(uint)); Span fields = stackalloc uint[4]; for (var i = 0; i < fields.Length; i++) { if (!ctx.TryReadUInt32(descriptorAddress + ((ulong)i * sizeof(uint)), out fields[i])) { return false; } } return TryDecodeTextureDescriptor(fields.ToArray(), out descriptor); } private static bool TryDecodeTextureDescriptor( IReadOnlyList fields, out TextureDescriptor descriptor) { descriptor = default; if (fields.Count < 4) { return false; } // GFX10/RDNA2 T# layout: WIDTH is split across word1[31:30] (lo 2 bits) // and word2[11:0] (hi 12 bits); FORMAT is the combined 9-bit field at // word1[28:20]. Verified against Kyty's decode of the same game // descriptors (fmt=56=8_8_8_8_UNORM, extent 1280x720, sw_mode 27). // GNM T# exposes a 38-bit baseaddr256 field, but RPCSX and the // Demon's Souls descriptors both show that only the low 32 bits are // part of the guest GPU VA. The upper baseaddr bits carry resource // metadata and produce bogus addresses such as 0x00003804... if used. var address = ((ulong)(uint)((((ulong)fields[1] << 32) | fields[0]) & 0x3F_FFFF_FFFFUL)) << 8; var width = (((fields[1] >> 30) & 0x3u) | ((fields[2] & 0xFFFu) << 2)) + 1; var height = ((fields[2] >> 14) & 0x3FFFu) + 1; var format = (fields[1] >> 20) & 0x1FFu; var numberType = (fields[1] >> 26) & 0xFu; var tileMode = (fields[3] >> 20) & 0x1Fu; var type = (fields[3] >> 28) & 0xFu; var baseLevel = (fields[3] >> 12) & 0xFu; var lastLevel = (fields[3] >> 16) & 0xFu; // The 128-bit RDNA2 2D resource derives pitch[12:0] from width; // the optional extension word only supplies pitch[13]. var pitch = width; if (fields.Count >= 5) { pitch = ((((width - 1) & 0x1FFFu) | (((fields[4] >> 13) & 1u) << 13)) + 1); } var dstSelect = fields[3] & 0xFFFu; if (address == 0 || width == 0 || height == 0) { return false; } descriptor = new TextureDescriptor( address, width, height, format, numberType, tileMode, type, baseLevel, lastLevel, pitch, dstSelect); return true; } private static TextureDescriptor CreateFallbackTextureDescriptor(IReadOnlyList fields) { var format = Gen5TextureFormatR8G8B8A8Unorm; var numberType = 0u; var tileMode = 0u; if (fields.Count >= 4) { format = (fields[1] >> 20) & 0x1FFu; numberType = (fields[1] >> 26) & 0xFu; tileMode = (fields[3] >> 20) & 0x1Fu; if (format == 0) { format = Gen5TextureFormatR8G8B8A8Unorm; } } return new TextureDescriptor( Address: 0, Width: 1, Height: 1, Format: format, NumberType: numberType, TileMode: tileMode, Type: Gen5TextureType2D, BaseLevel: 0, LastLevel: 0, Pitch: 1, DstSelect: 0xFAC); } private static bool TrySoftwarePresent( CpuContext ctx, TextureDescriptor source, int videoOutHandle, int displayBufferIndex) { if (source.Format != Gen5TextureFormatR8G8B8A8Unorm || source.TileMode != 0 || source.Type != Gen5TextureType2D || source.Width > 8192 || source.Height > 8192 || !VideoOutExports.TryGetDisplayBufferInfo(videoOutHandle, displayBufferIndex, out var destination) || destination.Address == 0 || destination.Width == 0 || destination.Height == 0 || destination.Width > 8192 || destination.Height > 8192 || destination.TilingMode != 0 || destination.PixelFormat is not ( VideoOutPixelFormatA8R8G8B8Srgb or VideoOutPixelFormatA8B8G8R8Srgb or VideoOutPixelFormatB8G8R8A8Unorm or VideoOutPixelFormatR8G8B8A8Unorm)) { return false; } var sourceByteCount = checked((ulong)source.Width * source.Height * 4); if (sourceByteCount > 256UL * 1024UL * 1024UL) { return false; } var sourceBytes = new byte[(int)sourceByteCount]; if (!ctx.Memory.TryRead(source.Address, sourceBytes)) { return false; } var fingerprint = ComputeFingerprint(sourceBytes); var fingerprintKey = (source.Address, destination.Address); lock (_softwarePresenterGate) { if (_softwarePresenterFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) && previousFingerprint == fingerprint) { return true; } } var destinationPitch = destination.PitchInPixel == 0 ? destination.Width : destination.PitchInPixel; if (destinationPitch < destination.Width) { return false; } var destinationRow = new byte[checked((int)destinationPitch * 4)]; var rgbaDestination = destination.PixelFormat is VideoOutPixelFormatA8B8G8R8Srgb or VideoOutPixelFormatR8G8B8A8Unorm; for (uint y = 0; y < destination.Height; y++) { var sourceY = (uint)(((ulong)y * source.Height) / destination.Height); for (uint x = 0; x < destination.Width; x++) { var sourceX = (uint)(((ulong)x * source.Width) / destination.Width); var sourceOffset = checked((int)(((ulong)sourceY * source.Width + sourceX) * 4)); var destinationOffset = checked((int)x * 4); if (rgbaDestination) { destinationRow[destinationOffset + 0] = sourceBytes[sourceOffset + 0]; destinationRow[destinationOffset + 1] = sourceBytes[sourceOffset + 1]; destinationRow[destinationOffset + 2] = sourceBytes[sourceOffset + 2]; } else { destinationRow[destinationOffset + 0] = sourceBytes[sourceOffset + 2]; destinationRow[destinationOffset + 1] = sourceBytes[sourceOffset + 1]; destinationRow[destinationOffset + 2] = sourceBytes[sourceOffset + 0]; } destinationRow[destinationOffset + 3] = sourceBytes[sourceOffset + 3]; } var destinationAddress = destination.Address + ((ulong)y * destinationPitch * 4); if (!ctx.Memory.TryWrite(destinationAddress, destinationRow)) { return false; } } lock (_softwarePresenterGate) { _softwarePresenterFingerprints[fingerprintKey] = fingerprint; } VideoOutExports.SubmitHostRgbaFrame(sourceBytes, source.Width, source.Height); TraceAgc( $"agc.software_presenter src=0x{source.Address:X16} {source.Width}x{source.Height} fmt={source.Format}/num{source.NumberType} " + $"dst=0x{destination.Address:X16} {destination.Width}x{destination.Height} fingerprint=0x{fingerprint:X16}"); return true; } private static ulong ComputeFingerprint(ReadOnlySpan bytes) { const ulong fnvOffsetBasis = 14695981039346656037UL; const ulong fnvPrime = 1099511628211UL; var fingerprint = fnvOffsetBasis; foreach (var value in bytes) { fingerprint = (fingerprint ^ value) * fnvPrime; } return fingerprint; } private static void TraceSubmittedPacket( CpuContext ctx, ulong packetAddress, uint dwordOffset, uint header, uint length, uint op, uint register) { TraceAgc( $"agc.dcb.packet dw={dwordOffset} addr=0x{packetAddress:X16} header=0x{header:X8} len={length} op=0x{op:X2} reg=0x{register:X2}"); var payloadCount = Math.Min(length - 1, 32u); for (uint i = 0; i < payloadCount; i++) { if (!ctx.TryReadUInt32(packetAddress + ((ulong)(i + 1) * sizeof(uint)), out var value)) { return; } TraceAgc($"agc.dcb.payload dw={dwordOffset + i + 1} value=0x{value:X8}"); } if (op != ItNop || register is not (RCxRegsIndirect or RShRegsIndirect or RUcRegsIndirect) || length < 4 || !ctx.TryReadUInt32(packetAddress + 4, out var registerCount) || !ctx.TryReadUInt64(packetAddress + 8, out var registersAddress)) { return; } var registerSpace = register == RCxRegsIndirect ? "cx" : register == RShRegsIndirect ? "sh" : "uc"; var tracedCount = Math.Min(registerCount, 256u); TraceAgc($"agc.dcb.indirect space={registerSpace} regs=0x{registersAddress:X16} count={registerCount}"); for (uint i = 0; i < tracedCount; i++) { var entryAddress = registersAddress + ((ulong)i * 8); if (!ctx.TryReadUInt32(entryAddress, out var registerOffset) || !ctx.TryReadUInt32(entryAddress + 4, out var value)) { TraceAgc($"agc.dcb.indirect_read_failed space={registerSpace} index={i} addr=0x{entryAddress:X16}"); return; } TraceAgc($"agc.dcb.reg space={registerSpace} index={i} offset=0x{registerOffset:X4} value=0x{value:X8}"); } if (tracedCount != registerCount) { TraceAgc($"agc.dcb.indirect_truncated space={registerSpace} traced={tracedCount} total={registerCount}"); } } private static bool PatchShaderProgramRegisters(CpuContext ctx, ulong headerAddress, ulong codeAddress) { if (!ctx.TryReadUInt64(headerAddress + ShaderShRegistersOffset, out var shRegistersAddress) || !ctx.TryReadByte(headerAddress + ShaderTypeOffset, out var shaderType) || !ctx.TryReadByte(headerAddress + ShaderNumShRegistersOffset, out var registerCount)) { return false; } if (shRegistersAddress == 0 || registerCount < 2) { return false; } if (!ctx.TryReadUInt32(shRegistersAddress, out var loRegister) || !ctx.TryReadUInt32(shRegistersAddress + 8, out var hiRegister)) { return false; } var expectedLo = shaderType switch { 0 => ComputePgmLo, 1 => SpiShaderPgmLoPs, 2 or 6 => SpiShaderPgmLoEs, 4 => SpiShaderPgmLoGs, 7 => SpiShaderPgmLoLs, _ => 0u, }; var expectedHi = shaderType switch { 0 => ComputePgmHi, 1 => SpiShaderPgmHiPs, 2 or 6 => SpiShaderPgmHiEs, 4 => SpiShaderPgmHiGs, 7 => SpiShaderPgmHiLs, _ => 0u, }; if (expectedLo == 0 || loRegister != expectedLo || hiRegister != expectedHi) { TraceCreateShader(0, headerAddress, codeAddress, $"unexpected-registers type={shaderType} lo=0x{loRegister:X8} hi=0x{hiRegister:X8}"); return false; } var loValue = (uint)((codeAddress >> 8) & 0xFFFF_FFFFUL); var hiValue = (uint)((codeAddress >> 40) & 0xFFUL); return ctx.TryWriteUInt32(shRegistersAddress + sizeof(uint), loValue) && ctx.TryWriteUInt32(shRegistersAddress + 8 + sizeof(uint), hiValue); } private static bool IsEsGeometryShaderType(byte shaderType) => shaderType is 2 or 4 or 6; private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace) { var commandAddress = ctx[CpuRegister.Rdi]; var registersAddress = ctx[CpuRegister.Rsi]; if (commandAddress == 0 || registersAddress == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!ctx.TryWriteUInt32(commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)(registersAddress >> 32))) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } TraceAgc($"agc.patch_{registerSpace}_addr cmd=0x{commandAddress:X16} regs=0x{registersAddress:X16}"); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } private static int AddIndirectPatchRegisters(CpuContext ctx, string registerSpace) { var commandAddress = ctx[CpuRegister.Rdi]; var registerCount = (uint)ctx[CpuRegister.Rsi]; if (commandAddress == 0) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } if (!ctx.TryReadUInt32(commandAddress + 4, out var currentCount) || !ctx.TryWriteUInt32(commandAddress + 4, currentCount + registerCount)) { return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } TraceAgc($"agc.patch_{registerSpace}_add cmd=0x{commandAddress:X16} add={registerCount} total={currentCount + registerCount}"); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } private static int DcbSetRegistersIndirect(CpuContext ctx, uint packetRegister, string registerSpace) { var commandBufferAddress = ctx[CpuRegister.Rdi]; var registersAddress = ctx[CpuRegister.Rsi]; var registerCount = (uint)ctx[CpuRegister.Rdx]; if (commandBufferAddress == 0) { return ReturnPointer(ctx, 0); } if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 4, out var commandAddress) || !ctx.TryWriteUInt32(commandAddress, Pm4(4, ItNop, packetRegister)) || !ctx.TryWriteUInt32(commandAddress + 4, registerCount) || !ctx.TryWriteUInt32(commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) || !ctx.TryWriteUInt32(commandAddress + 12, (uint)(registersAddress >> 32))) { return ReturnPointer(ctx, 0); } TraceAgc($"agc.dcb_set_{registerSpace}_indirect buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} regs=0x{registersAddress:X16} count={registerCount}"); return ReturnPointer(ctx, commandAddress); } private static bool TryAllocateCommandDwords(CpuContext ctx, ulong commandBufferAddress, uint sizeDwords, out ulong commandAddress) { commandAddress = 0; if (sizeDwords == 0 || !ctx.TryReadUInt64(commandBufferAddress + CommandBufferCursorUpOffset, out var cursorUp) || !ctx.TryReadUInt64(commandBufferAddress + CommandBufferCursorDownOffset, out var cursorDown) || !ctx.TryReadUInt64(commandBufferAddress + CommandBufferCallbackOffset, out var callback) || !ctx.TryReadUInt32(commandBufferAddress + CommandBufferReservedDwOffset, out var reservedDwords)) { return false; } var availableDwords = cursorDown >= cursorUp ? Math.Min((cursorDown - cursorUp) / sizeof(uint), uint.MaxValue) : 0; var remainingDwords = (uint)Math.Max(availableDwords, reservedDwords) - reservedDwords; if (sizeDwords > remainingDwords) { TraceAgc($"agc.cmd_alloc_full buf=0x{commandBufferAddress:X16} need={sizeDwords} remaining={remainingDwords} callback=0x{callback:X16}"); return false; } var nextCursor = cursorUp + ((ulong)sizeDwords * sizeof(uint)); if (!ctx.TryWriteUInt64(commandBufferAddress + CommandBufferCursorUpOffset, nextCursor)) { return false; } commandAddress = cursorUp; return true; } private static bool CopyShaderRegister(CpuContext ctx, ulong sourceAddress, ulong destinationAddress) { if (!ctx.TryReadUInt32(sourceAddress, out var offset) || !ctx.TryReadUInt32(sourceAddress + sizeof(uint), out var value)) { return false; } return ctx.TryWriteUInt32(destinationAddress, offset) && ctx.TryWriteUInt32(destinationAddress + sizeof(uint), value); } private static bool RelocatePointerField(CpuContext ctx, ulong fieldAddress) { if (!ctx.TryReadUInt64(fieldAddress, out var relativeAddress)) { return false; } if (relativeAddress == 0) { return true; } return ctx.TryWriteUInt64(fieldAddress, fieldAddress + relativeAddress); } private static int ReturnRegisterDefaults(CpuContext ctx, bool internalDefaults) { var version = (uint)ctx[CpuRegister.Rdi]; if (!IsSupportedRegisterDefaultsVersion(version)) { return ReturnPointer(ctx, 0); } if (!TryGetRegisterDefaultsAllocation(ctx, out var allocation)) { return ReturnPointer(ctx, 0); } var address = internalDefaults ? allocation.Internal : allocation.Primary; TraceAgc($"agc.get_register_defaults internal={internalDefaults} version={version} address=0x{address:X16}"); return ReturnPointer(ctx, address); } private static bool IsSupportedRegisterDefaultsVersion(uint version) { return version is RegisterDefaultsVersion7 or RegisterDefaultsVersion8 or RegisterDefaultsVersion10 or RegisterDefaultsVersion13; } private static bool TryGetRegisterDefaultsAllocation( CpuContext ctx, out RegisterDefaultsAllocation allocation) { lock (_registerDefaultsGate) { if (_registerDefaultsAllocations.TryGetValue(ctx.Memory, out allocation!)) { return true; } if (!TryBuildRegisterDefaults( ctx, PrimaryRegisterDefaults, cxTableLength: 78, shTableLength: 29, ucTableLength: 20, out var primaryAddress) || !TryBuildRegisterDefaults( ctx, InternalRegisterDefaults, cxTableLength: 4, shTableLength: 15, ucTableLength: 3, out var internalAddress)) { allocation = null!; return false; } allocation = new RegisterDefaultsAllocation(primaryAddress, internalAddress); _registerDefaultsAllocations.Add(ctx.Memory, allocation); return true; } } private static bool TryBuildRegisterDefaults( CpuContext ctx, RegisterDefaultGroup[] groups, int cxTableLength, int shTableLength, int ucTableLength, out ulong address) { var cxTableOffset = AlignUp(RegisterDefaultsSize, sizeof(ulong)); var shTableOffset = cxTableOffset + (cxTableLength * sizeof(ulong)); var ucTableOffset = shTableOffset + (shTableLength * sizeof(ulong)); var typesOffset = AlignUp(ucTableOffset + (ucTableLength * sizeof(ulong)), sizeof(uint)); var registerBlocksOffset = AlignUp(typesOffset + (groups.Length * 3 * sizeof(uint)), sizeof(ulong)); var blobLength = registerBlocksOffset + (groups.Length * RegisterDefaultBlockSize); if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, (ulong)blobLength, 0x1000, out address)) { return false; } var blob = new byte[blobLength]; WriteBlobUInt64(blob, 0x00, address + (ulong)cxTableOffset); WriteBlobUInt64(blob, 0x08, address + (ulong)shTableOffset); WriteBlobUInt64(blob, 0x10, address + (ulong)ucTableOffset); WriteBlobUInt64(blob, 0x30, address + (ulong)typesOffset); WriteBlobUInt32(blob, 0x38, (uint)groups.Length); for (var groupIndex = 0; groupIndex < groups.Length; groupIndex++) { var group = groups[groupIndex]; if (group.Registers.Length > 16) { return false; } var tableOffset = group.Space switch { 0 => cxTableOffset, 1 => shTableOffset, 2 => ucTableOffset, _ => -1, }; var tableLength = group.Space switch { 0 => cxTableLength, 1 => shTableLength, 2 => ucTableLength, _ => 0, }; if (tableOffset < 0 || group.Index >= tableLength) { return false; } var registerBlockOffset = registerBlocksOffset + (groupIndex * RegisterDefaultBlockSize); WriteBlobUInt64( blob, tableOffset + ((int)group.Index * sizeof(ulong)), address + (ulong)registerBlockOffset); var typeEntryOffset = typesOffset + (groupIndex * 3 * sizeof(uint)); WriteBlobUInt32(blob, typeEntryOffset, group.Type); WriteBlobUInt32(blob, typeEntryOffset + sizeof(uint), (group.Index * 4) + group.Space); for (var registerIndex = 0; registerIndex < group.Registers.Length; registerIndex++) { var register = group.Registers[registerIndex]; var registerOffset = registerBlockOffset + (registerIndex * 2 * sizeof(uint)); WriteBlobUInt32(blob, registerOffset, register.Offset); WriteBlobUInt32(blob, registerOffset + sizeof(uint), register.Value); } } return ctx.Memory.TryWrite(address, blob); } private static int AlignUp(int value, int alignment) => (value + alignment - 1) & -alignment; private static void WriteBlobUInt32(Span blob, int offset, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(blob[offset..], value); private static void WriteBlobUInt64(Span blob, int offset, ulong value) => BinaryPrimitives.WriteUInt64LittleEndian(blob[offset..], value); private static int ReturnPointer(CpuContext ctx, ulong pointer) { ctx[CpuRegister.Rax] = pointer; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } private static uint Pm4(uint lengthDwords, uint op, uint register) => 0xC0000000u | ((((ushort)lengthDwords - 2u) & 0x3FFFu) << 16) | ((op & 0xFFu) << 8) | ((register & 0x3Fu) << 2); private static uint Pm4Length(uint header) => ((header >> 16) & 0x3FFFu) + 2u; private static bool TryReadGuestCString( CpuContext ctx, ulong address, int maximumLength, out byte[] bytes) { if (address == 0) { bytes = []; return true; } var values = new List(Math.Min(maximumLength, 128)); for (var index = 0; index < maximumLength; index++) { if (!ctx.TryReadByte(address + (ulong)index, out var value)) { bytes = []; return false; } if (value == 0) { bytes = [.. values]; return true; } values.Add(value); } bytes = []; return false; } private static bool TryGetPacketIdentity( CpuContext ctx, ulong commandAddress, out uint op, out uint register) { op = 0; register = 0; if (commandAddress == 0 || !ctx.TryReadUInt32(commandAddress, out var header)) { return false; } op = (header >> 8) & 0xFFu; register = (header >> 2) & 0x3Fu; return true; } private static bool TryCopyGuestMemory( CpuContext ctx, ulong sourceAddress, ulong destinationAddress, uint byteCount) { if (sourceAddress == destinationAddress) { return true; } var buffer = new byte[Math.Min(byteCount, 64u * 1024u)]; ulong offset = 0; while (offset < byteCount) { var chunkLength = (int)Math.Min((ulong)buffer.Length, byteCount - offset); var chunk = buffer.AsSpan(0, chunkLength); if (!ctx.Memory.TryRead(sourceAddress + offset, chunk) || !ctx.Memory.TryWrite(destinationAddress + offset, chunk)) { return false; } offset += (uint)chunkLength; } return true; } private static bool TryFillGuestMemory( CpuContext ctx, uint value, ulong destinationAddress, uint byteCount) { var buffer = new byte[Math.Min(byteCount, 64u * 1024u)]; Span encoded = stackalloc byte[sizeof(uint)]; BinaryPrimitives.WriteUInt32LittleEndian(encoded, value); for (var offset = 0; offset < buffer.Length; offset += sizeof(uint)) { var remaining = Math.Min(sizeof(uint), buffer.Length - offset); encoded[..remaining].CopyTo(buffer.AsSpan(offset, remaining)); } ulong destinationOffset = 0; while (destinationOffset < byteCount) { var chunkLength = (int)Math.Min( (ulong)buffer.Length, byteCount - destinationOffset); if (!ctx.Memory.TryWrite( destinationAddress + destinationOffset, buffer.AsSpan(0, chunkLength))) { return false; } destinationOffset += (uint)chunkLength; } return true; } private static bool ShouldTraceHotPath(ref long counter) { var count = Interlocked.Increment(ref counter); return count <= 8 || count % 100_000 == 0; } private static void TraceAgc(string message) { if (!_traceAgc) { return; } Console.Error.WriteLine($"[LOADER][TRACE] {message}"); } private static void TraceAgcShader(string message) { if (!_traceAgcShader) { return; } Console.Error.WriteLine($"[LOADER][TRACE] {message}"); } private static string FormatShaderDwords(IReadOnlyList values) => values.Count == 0 ? "none" : string.Join(',', values.Select(static value => $"{value:X8}")); private static string FormatTextureDescriptor(TextureDescriptor descriptor) => $"addr=0x{descriptor.Address:X16} {descriptor.Width}x{descriptor.Height} " + $"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " + $"type={descriptor.Type} levels={descriptor.BaseLevel}-{descriptor.LastLevel} " + $"pitch={descriptor.Pitch} dst=0x{descriptor.DstSelect:X3}"; private static void DumpSpirv( string stage, ulong shaderAddress, ulong stateFingerprint, byte[] spirv, Gen5ShaderProgram program) { if (spirv.Length == 0 || !string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"), "1", StringComparison.Ordinal)) { return; } var directory = Path.Combine(AppContext.BaseDirectory, "shader-dumps"); Directory.CreateDirectory(directory); var name = $"{shaderAddress:X16}-{stateFingerprint:X16}.{stage}"; File.WriteAllBytes(Path.Combine(directory, $"{name}.spv"), spirv); var lines = new List(program.Instructions.Count + 2) { $"address=0x{program.Address:X16}", "pc words opcode destinations <- sources control", }; foreach (var instruction in program.Instructions) { lines.Add( $"0x{instruction.Pc:X4} " + $"{string.Join('_', instruction.Words.Select(static word => $"{word:X8}"))} " + $"{instruction.Opcode} " + $"{string.Join(',', instruction.Destinations)} <- " + $"{string.Join(',', instruction.Sources)} " + $"{instruction.Control}"); } File.WriteAllLines(Path.Combine(directory, $"{name}.ir.txt"), lines); } private static void TraceCreateShader(ulong destinationAddress, ulong headerAddress, ulong codeAddress, string detail) { var isOk = string.Equals(detail, "ok", StringComparison.Ordinal); if (isOk && (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) || !ShouldTraceHotPath(ref _createShaderTraceCount))) { return; } Console.Error.WriteLine( $"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}"); } // Packet layouts mirror what DcbWaitRegMem emits. // 32-bit (ItNop/RWaitMem32): +4 addrLo, +8 addrHi, +12 mask, +16 cmp|op<<8, +20 ref. // 64-bit (ItNop/RWaitMem64): +4 addrLo, +8 addrHi, +12 maskLo, +16 maskHi, // +20 refLo, +24 refHi, +28 cmp|op<<8, +32 poll. private static bool TryParseWaitRegMem( CpuContext ctx, ulong addr, bool is64, out ulong waitAddr, out ulong refVal, out ulong mask, out uint cmpFunc) { waitAddr = refVal = mask = 0; cmpFunc = 0; if (!ctx.TryReadUInt32(addr + 4, out var lo) || !ctx.TryReadUInt32(addr + 8, out var hi)) { return false; } waitAddr = ((ulong)hi << 32) | lo; if (!is64) { if (!ctx.TryReadUInt32(addr + 12, out var mask32) || !ctx.TryReadUInt32(addr + 16, out var cmpRaw) || !ctx.TryReadUInt32(addr + 20, out var refVal32)) { return false; } mask = mask32; refVal = refVal32; cmpFunc = cmpRaw & 0x7; return true; } if (!ctx.TryReadUInt32(addr + 12, out var maskLo) || !ctx.TryReadUInt32(addr + 16, out var maskHi) || !ctx.TryReadUInt32(addr + 20, out var refLo) || !ctx.TryReadUInt32(addr + 24, out var refHi) || !ctx.TryReadUInt32(addr + 28, out var cmpRaw64)) { return false; } mask = ((ulong)maskHi << 32) | maskLo; refVal = ((ulong)refHi << 32) | refLo; cmpFunc = cmpRaw64 & 0x7; return true; } // Standard ItWaitRegMem (7 dwords, 32-bit compare): // +4 cmp|(op&1)<<8, +8 addrLo, +12 addrHi, +16 ref, +20 mask, +24 poll. private static bool TryParseStandardWaitRegMem( CpuContext ctx, ulong addr, out ulong waitAddr, out ulong refVal, out ulong mask, out uint cmpFunc) { waitAddr = refVal = mask = 0; cmpFunc = 0; if (!ctx.TryReadUInt32(addr + 4, out var cmpRaw) || !ctx.TryReadUInt32(addr + 8, out var lo) || !ctx.TryReadUInt32(addr + 12, out var hi) || !ctx.TryReadUInt32(addr + 16, out var reference) || !ctx.TryReadUInt32(addr + 20, out var mask32)) { return false; } cmpFunc = cmpRaw & 0x7; waitAddr = ((ulong)hi << 32) | lo; refVal = reference; mask = mask32; return true; } }