mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
11211 lines
431 KiB
C#
11211 lines
431 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Collections.Concurrent;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.Gpu;
|
|
using SharpEmu.ShaderCompiler;
|
|
using SharpEmu.Libs.Kernel;
|
|
using SharpEmu.Libs.VideoOut;
|
|
using System.Buffers.Binary;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace SharpEmu.Libs.Agc;
|
|
|
|
public static partial class AgcExports
|
|
{
|
|
#if DEBUG
|
|
static AgcExports()
|
|
{
|
|
ValidateWriteDataControlDecoders();
|
|
ValidateDispatchInitiators();
|
|
ValidateSubmittedQueueAndReleaseMemDecoders();
|
|
ValidateAcquireMemAndQueueResetDecoders();
|
|
ValidateDepthTargetDecoder();
|
|
}
|
|
#endif
|
|
|
|
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 ItReleaseMem = 0x49;
|
|
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 RIndexBase = 0x1B;
|
|
private const uint RIndexCount = 0x1C;
|
|
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 ComputeStartX = 0x204;
|
|
private const uint ComputeStartY = 0x205;
|
|
private const uint ComputeStartZ = 0x206;
|
|
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 CbColorControl = 0x202;
|
|
private const uint CbBlendRed = 0x105;
|
|
private const uint CbBlendGreen = 0x106;
|
|
private const uint CbBlendBlue = 0x107;
|
|
private const uint CbBlendAlpha = 0x108;
|
|
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;
|
|
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
|
|
private const uint DbRenderControl = 0x000;
|
|
private const uint DbDepthView = 0x002;
|
|
private const uint DbDepthSizeXy = 0x007;
|
|
private const uint DbDepthClear = 0x00B;
|
|
private const uint DbZInfo = 0x010;
|
|
private const uint DbZReadBase = 0x012;
|
|
private const uint DbZWriteBase = 0x014;
|
|
private const uint DbZReadBaseHi = 0x01A;
|
|
private const uint DbZWriteBaseHi = 0x01C;
|
|
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 = 10;
|
|
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
|
|
private const uint Gen5TextureType1D = 8;
|
|
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 VideoOutPixelFormat2R8G8B8A8Srgb = 0x8000000022000000;
|
|
private const ulong VideoOutPixelFormat2B8G8R8A8Srgb = 0x8000000000000000;
|
|
private const ulong VideoOutPixelFormat2R10G10B10A2 = 0x8100000622000000;
|
|
private const ulong VideoOutPixelFormat2B10G10R10A2 = 0x8100000600000000;
|
|
private const ulong VideoOutPixelFormat2R10G10B10A2Srgb = 0x8100000022000000;
|
|
private const ulong VideoOutPixelFormat2B10G10R10A2Srgb = 0x8100000000000000;
|
|
private const ulong VideoOutPixelFormat2R10G10B10A2Bt2100Pq = 0x8100070422000000;
|
|
private const ulong VideoOutPixelFormat2B10G10R10A2Bt2100Pq = 0x8100070400000000;
|
|
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 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 ResourceRegistrationBytesPerResource = 0x118;
|
|
private const ulong ResourceRegistrationBytesPerOwner = 0x1E0;
|
|
private const int ResourceRegistrationMaxNameLength = 256;
|
|
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 CommandBufferUserDataOffset = 0x28;
|
|
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 HashSet<uint> _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<ulong> _tracedComputeShaders = new();
|
|
private static readonly HashSet<(ulong Address, uint X, uint Y, uint Z)>
|
|
_tracedDispatchArguments = new();
|
|
private static readonly HashSet<(ulong Address, uint Initiator, string Reason)>
|
|
_rejectedDispatchArguments = new();
|
|
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
|
// Concurrent so the per-draw/per-dispatch hit path is lock-free (and no longer
|
|
// shares _submitTraceGate with tracing).
|
|
private static readonly ConcurrentDictionary<
|
|
(ulong Es, ulong EsState, ulong Ps, ulong PsState, ulong OutputLayout,
|
|
uint OutputCount, uint Attributes, uint PsInputEna, uint PsInputAddr,
|
|
ulong AliasAlignment),
|
|
(IGuestCompiledShader Vertex, IGuestCompiledShader Pixel)> _graphicsShaderCache = new();
|
|
private static readonly ConcurrentDictionary<
|
|
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ,
|
|
uint WaveLanes, ulong AliasAlignment),
|
|
IGuestCompiledShader> _computeShaderCache = new();
|
|
private static readonly ConcurrentDictionary<
|
|
(ulong Es, ulong State, ulong AliasAlignment),
|
|
IGuestCompiledShader> _depthOnlyVertexShaderCache = new();
|
|
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
|
|
private static readonly bool _traceAgc = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
// Drop a draw on an undecodable texture descriptor instead of substituting
|
|
// a 1x1 fallback binding. Off by default so a garbage descriptor degrades
|
|
// the pass rather than dropping it (Demon's Souls composite feeders).
|
|
private static readonly bool _strictShaderDescriptors = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_STRICT_SHADER_DESCRIPTORS"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
private static readonly bool _traceAgcShader =
|
|
_traceAgc ||
|
|
string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
private static readonly ulong? _traceComputeShaderAddress = ParseOptionalHexAddress(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_COMPUTE_SHADER_ADDRESS"));
|
|
private static readonly ulong? _tracePixelShaderAddress = ParseOptionalHexAddress(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_PIXEL_SHADER_ADDRESS"));
|
|
private static readonly ulong? _traceRenderTargetAddress = ParseOptionalHexAddress(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_RENDER_TARGET_ADDRESS"));
|
|
private static readonly bool _traceDraws = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_DRAWS"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
private static readonly bool _traceFramePackets = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_FRAME_PACKETS"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
private static readonly bool _traceVertexRanges = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_VERTEX_RANGES"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
private static readonly bool _compatibilitySubmitCompletionEvent = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_AGC_SUBMIT_COMPLETION_EVENT"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
// Escape hatch for the cached-texture copy skip (per-draw texel copies
|
|
// are re-enabled unconditionally when set), for A/B-ing rendering issues.
|
|
private static readonly bool _textureCopySkipDisabled = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_NO_TEXTURE_SKIP"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
private static long _dcbWriteDataTraceCount;
|
|
private static int _tracedVertexRangeCount;
|
|
private static long _dcbWaitRegMemTraceCount;
|
|
private static long _createShaderTraceCount;
|
|
private static long _packetPayloadTraceCount;
|
|
private static bool _tracedMissingPixelShaderBindings;
|
|
private static long _unsatisfiedWaitTraceCount;
|
|
private static long _labelProducerSequence;
|
|
private static readonly object _labelProducerGate = new();
|
|
private static readonly List<LabelProducerTrace> _labelProducers = [];
|
|
private static readonly HashSet<(object Memory, ulong Address, ulong SubmissionId)>
|
|
_tracedProducerlessWaits = new();
|
|
private static long _shaderTranslationMissTraceCount;
|
|
private static long _translatedDrawTraceCount;
|
|
private static long _standardDmaTraceCount;
|
|
private static long _packetParseFailureTraceCount;
|
|
private static int _textureFallbackTraceCount;
|
|
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<object, RegisterDefaultsAllocation> _registerDefaultsAllocations = new();
|
|
private static readonly ConditionalWeakTable<object, SubmittedGpuState> _submittedGpuStates = new();
|
|
|
|
private static readonly RegisterDefaultGroup[] PrimaryRegisterDefaults =
|
|
CreatePrimaryRegisterDefaults();
|
|
|
|
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,
|
|
uint Depth = 1,
|
|
uint BaseArray = 0,
|
|
uint ArrayPitch = 0,
|
|
uint MaxMip = 0,
|
|
uint MinLod = 0,
|
|
uint MinLodWarn = 0,
|
|
uint BcSwizzle = 0,
|
|
ulong MetadataAddress = 0,
|
|
uint DescriptorFlags = 0,
|
|
bool HasExtendedDescriptor = false)
|
|
{
|
|
public uint ResourceMipLevels
|
|
{
|
|
get
|
|
{
|
|
// RDNA2 table 45 explicitly distinguishes MAX_MIP (the
|
|
// resource allocation) from BASE_LEVEL/LAST_LEVEL (the
|
|
// resource view). Do not size a Vulkan image from a view:
|
|
// another descriptor for the same allocation may expose a
|
|
// different subset of its mip chain.
|
|
var maximumMipLevels = GetMaximumMipLevels();
|
|
var resourceMipLevels = HasExtendedDescriptor
|
|
? MaxMip + 1
|
|
: maximumMipLevels;
|
|
return Math.Min(Math.Max(resourceMipLevels, 1u), maximumMipLevels);
|
|
}
|
|
}
|
|
|
|
public uint MipLevels
|
|
{
|
|
get
|
|
{
|
|
var descriptorMipLevels = LastLevel >= ViewBaseLevel
|
|
? LastLevel - ViewBaseLevel + 1
|
|
: 1;
|
|
return Math.Min(
|
|
descriptorMipLevels,
|
|
ResourceMipLevels - ViewBaseLevel);
|
|
}
|
|
}
|
|
|
|
public uint ViewBaseLevel
|
|
{
|
|
get
|
|
{
|
|
// Some single-mip Gen5 descriptors use the reserved/inverted
|
|
// 15-0 range as a mip-disabled sentinel. The resource still
|
|
// has exactly one addressable level (MAX_MIP=0). Treating 15
|
|
// literally makes Vulkan reject an otherwise compatible GPU
|
|
// image and falls back to stale guest-memory pixels. For any
|
|
// malformed range, keep BASE_LEVEL's meaning and clamp it to
|
|
// the allocation's last addressable mip. In particular, the
|
|
// common 15-0/MAX_MIP=0 sentinel resolves to mip 0 without
|
|
// making LAST_LEVEL the base of unrelated inverted views.
|
|
return Math.Min(BaseLevel, ResourceMipLevels - 1);
|
|
}
|
|
}
|
|
|
|
private uint GetMaximumMipLevels()
|
|
{
|
|
var largestDimension = Type == 10
|
|
? Math.Max(Math.Max(Width, Height), Depth)
|
|
: Math.Max(Width, Height);
|
|
uint maximumMipLevels = 1;
|
|
while (largestDimension > 1)
|
|
{
|
|
largestDimension >>= 1;
|
|
maximumMipLevels++;
|
|
}
|
|
|
|
return 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,
|
|
IGuestCompiledShader VertexShader,
|
|
IGuestCompiledShader PixelShader,
|
|
uint AttributeCount,
|
|
uint VertexCount,
|
|
uint InstanceCount,
|
|
GuestIndexBuffer? IndexBuffer,
|
|
IReadOnlyList<TranslatedImageBinding> Textures,
|
|
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
|
IReadOnlyList<Gen5VertexInputBinding> VertexInputs,
|
|
IReadOnlyList<RenderTargetDescriptor> RenderTargets,
|
|
GuestDepthTarget? DepthTarget,
|
|
// Seam-shaped color targets are built once with the cached translation.
|
|
IReadOnlyList<GuestRenderTarget> GuestTargets,
|
|
GuestRenderState RenderState,
|
|
IReadOnlyList<uint> PixelUserData,
|
|
uint RawBlendControl,
|
|
uint RawColorInfo,
|
|
IReadOnlyList<uint> PixelInitialScalars,
|
|
IReadOnlyList<uint> VertexInitialScalars);
|
|
|
|
private sealed record TranslatedImageBinding(
|
|
TextureDescriptor Descriptor,
|
|
bool IsStorage,
|
|
uint MipLevel,
|
|
IReadOnlyList<uint> 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,
|
|
uint BaseGroupX,
|
|
uint BaseGroupY,
|
|
uint BaseGroupZ,
|
|
uint WaveLaneCount,
|
|
bool IsIndirect,
|
|
uint ThreadCountX,
|
|
uint ThreadCountY,
|
|
uint ThreadCountZ);
|
|
|
|
private readonly record struct SubmittedAcquireMem(
|
|
uint Engine,
|
|
uint CbDbControl,
|
|
ulong BaseAddress,
|
|
ulong SizeBytes,
|
|
uint PollInterval,
|
|
uint GcrControl)
|
|
{
|
|
// GFX10 GCR_CNTL invalidation controls. The host has no separate GLI,
|
|
// GLM, GLK, GLV, GL1 and GL2 caches; they all converge on the guest
|
|
// memory snapshots used to build Vulkan resources.
|
|
private const uint GliInvalidateMask = 0x3u;
|
|
private const int Gl1RangeShift = 2;
|
|
private const uint Gl1RangeMask = 0x3u;
|
|
private const uint GlmInvalidate = 1u << 5;
|
|
private const uint GlkInvalidate = 1u << 7;
|
|
private const uint GlvInvalidate = 1u << 8;
|
|
private const uint Gl1Invalidate = 1u << 9;
|
|
private const uint Gl2Discard = 1u << 13;
|
|
private const uint Gl2Invalidate = 1u << 14;
|
|
private const int Gl2RangeShift = 11;
|
|
private const uint Gl2RangeMask = 0x3u;
|
|
|
|
public bool InvalidatesGuestResources =>
|
|
(GcrControl & (GliInvalidateMask |
|
|
GlmInvalidate |
|
|
GlkInvalidate |
|
|
GlvInvalidate |
|
|
Gl1Invalidate |
|
|
Gl2Discard |
|
|
Gl2Invalidate)) != 0;
|
|
|
|
// sceAgc encodes its all-memory sentinel with a zero COHER_SIZE. GFX10
|
|
// can also request ALL independently in GLI_INV, GL1_RANGE or
|
|
// GL2_RANGE; in the host's unified resource cache, any invalidated
|
|
// domain with ALL scope expands the operation to all tracked images.
|
|
public bool CoversAllGuestMemory =>
|
|
SizeBytes == 0 ||
|
|
(GcrControl & GliInvalidateMask) == 1u ||
|
|
((GcrControl & (GlmInvalidate |
|
|
GlkInvalidate |
|
|
GlvInvalidate |
|
|
Gl1Invalidate)) != 0 &&
|
|
((GcrControl >> Gl1RangeShift) & Gl1RangeMask) == 0) ||
|
|
((GcrControl & (Gl2Discard | Gl2Invalidate)) != 0 &&
|
|
((GcrControl >> Gl2RangeShift) & Gl2RangeMask) == 0);
|
|
}
|
|
|
|
private sealed class SubmittedDcbState
|
|
{
|
|
public readonly record struct PendingSubmission(
|
|
ulong CommandAddress,
|
|
uint DwordCount,
|
|
ulong SubmissionId,
|
|
bool TracePackets);
|
|
|
|
public Dictionary<uint, uint> CxRegisters { get; } = new();
|
|
public Dictionary<uint, uint> ShRegisters { get; } = new();
|
|
public Dictionary<uint, uint> UcRegisters { get; } = new();
|
|
public TextureDescriptor? PresenterTexture { get; set; }
|
|
public GuestDrawKind GuestDrawKind { get; set; }
|
|
public TranslatedGuestDraw? TranslatedDraw { get; set; }
|
|
public TranslatedGuestDraw? PendingTargetlessDraw { get; set; }
|
|
public Dictionary<ulong, RenderTargetDescriptor> KnownRenderTargets { get; } = new();
|
|
public Dictionary<ulong, RenderTargetWriter> 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; }
|
|
public string QueueName { get; set; } = "graphics";
|
|
public ulong ActiveSubmissionId { get; set; }
|
|
public Queue<PendingSubmission> PendingSubmissions { get; } = new();
|
|
public bool HasActiveSubmission { get; set; }
|
|
public bool IsSuspended { get; set; }
|
|
public ulong CompletionEventNotifiedSubmissionId { get; set; }
|
|
public Dictionary<(uint Op, uint Register), uint> FramePacketCounts { get; } = new();
|
|
public uint FramePacketCount { get; set; }
|
|
public uint FrameDrawCount { get; set; }
|
|
public uint FrameDispatchCount { get; set; }
|
|
public ulong FlipCount { get; set; }
|
|
}
|
|
|
|
private sealed class SubmittedGpuState
|
|
{
|
|
public object Gate { get; } = new();
|
|
public SubmittedDcbState Graphics { get; } = new();
|
|
public Dictionary<uint, SubmittedDcbState> ComputeQueues { get; } = new();
|
|
public Dictionary<ulong, ComputeImageWriter> ComputeImageWriters { get; } = new();
|
|
public Dictionary<uint, string> ResourceOwners { get; } = new();
|
|
public Dictionary<uint, RegisteredAgcResource> 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; }
|
|
public ulong SubmissionSequence { get; set; }
|
|
public bool WaitMonitorRunning { get; set; }
|
|
}
|
|
|
|
private readonly record struct RegisteredAgcResource(
|
|
uint Owner,
|
|
ulong Address,
|
|
ulong Size,
|
|
string Name,
|
|
uint Type,
|
|
uint Flags);
|
|
|
|
private sealed class LabelProducerTrace
|
|
{
|
|
public long Sequence;
|
|
public required object Memory;
|
|
public ulong Address;
|
|
public ulong Length;
|
|
public ulong PacketAddress;
|
|
public ulong SubmissionId;
|
|
public required string QueueName;
|
|
public required string DebugName;
|
|
public bool Completed;
|
|
}
|
|
|
|
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);
|
|
|
|
// NID captured from shipped titles; 'sceAgcInit' is a working label that collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
|
#pragma warning disable SHEM004
|
|
[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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}");
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
}
|
|
#pragma warning restore SHEM004
|
|
|
|
[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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
if (!TryReadUInt32(ctx, headerAddress, out var fileHeader) ||
|
|
!TryReadUInt32(ctx, headerAddress + sizeof(uint), out var version))
|
|
{
|
|
return SetReturn(ctx, 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 SetReturn(ctx, 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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
if (!TryReadUInt64(ctx, headerAddress + ShaderUserDataOffset, out var userDataAddress))
|
|
{
|
|
return SetReturn(ctx, 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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
if (!PatchShaderProgramRegisters(ctx, headerAddress, codeAddress))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
if (destinationAddress != 0 &&
|
|
!ctx.TryWriteUInt64(destinationAddress, headerAddress))
|
|
{
|
|
return SetReturn(ctx, 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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
if (!TryReadByte(ctx, geometryShaderAddress + ShaderTypeOffset, out var shaderType) || !IsEsGeometryShaderType(shaderType) ||
|
|
!TryReadUInt64(ctx, geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) ||
|
|
specialsAddress == 0)
|
|
{
|
|
return SetReturn(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, ucRegistersAddress + 16, VgtPrimitiveType) ||
|
|
!TryWriteUInt32(ctx, ucRegistersAddress + 20, primitiveType))
|
|
{
|
|
return SetReturn(ctx, 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;
|
|
}
|
|
|
|
// NID captured from shipped titles; the friendly name collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
|
#pragma warning disable SHEM004
|
|
[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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
if (!TryReadUInt64(ctx, geometryShaderAddress + ShaderOutputSemanticsOffset, out var outputSemanticsAddress) ||
|
|
!TryReadUInt32(ctx, geometryShaderAddress + ShaderNumOutputSemanticsOffset, out var outputSemanticsCount))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
ulong inputSemanticsAddress = 0;
|
|
if (pixelShaderAddress != 0 &&
|
|
(!TryReadUInt64(ctx, pixelShaderAddress + ShaderInputSemanticsOffset, out inputSemanticsAddress) ||
|
|
!TryReadUInt32(ctx, pixelShaderAddress + ShaderNumInputSemanticsOffset, out _)))
|
|
{
|
|
return SetReturn(ctx, 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 &&
|
|
TryReadUInt32(ctx, inputSemanticsAddress + (i * sizeof(uint)), out var inputSemantic))
|
|
{
|
|
flat = ((inputSemantic >> 22) & 0x1) != 0;
|
|
}
|
|
|
|
value = i | (flat ? 0x400u : 0u);
|
|
}
|
|
|
|
var destination = registersAddress + (i * 8);
|
|
if (!TryWriteUInt32(ctx, destination, SpiPsInputCntl0 + i) ||
|
|
!TryWriteUInt32(ctx, destination + sizeof(uint), value))
|
|
{
|
|
return SetReturn(ctx, 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;
|
|
}
|
|
#pragma warning restore SHEM004
|
|
|
|
// NID captured from shipped titles; the friendly name collides with a real catalog symbol of a different NID. Rename pending AGC API confirmation.
|
|
#pragma warning disable SHEM004
|
|
[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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var payloadAddress = commandAddress + 8;
|
|
if (type == 0)
|
|
{
|
|
if (!TryReadUInt32(ctx, commandAddress, out var header))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
payloadAddress = (header & 0x3FFF_0000u) == 0x3FFF_0000u
|
|
? 0
|
|
: commandAddress + 4;
|
|
}
|
|
|
|
if (!ctx.TryWriteUInt64(outputAddress, payloadAddress))
|
|
{
|
|
return SetReturn(ctx, 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;
|
|
}
|
|
#pragma warning restore SHEM004
|
|
|
|
[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 ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, dwordCount, out var commandAddress) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(dwordCount, ItNop, RZero)))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
for (uint index = 1; index < dwordCount; index++)
|
|
{
|
|
if (!TryWriteUInt32(ctx, commandAddress + ((ulong)index * sizeof(uint)), 0))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
}
|
|
|
|
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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(5, ItDispatchDirect, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, groupCountX) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, groupCountY) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, groupCountZ) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 16, DirectDispatchInitiator(modifier)))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
return ReturnPointer(ctx, commandAddress);
|
|
}
|
|
|
|
private static uint DirectDispatchInitiator(uint modifier) =>
|
|
// AGC's direct API takes workgroup counts by default. Preserve the
|
|
// caller's USE_THREAD_DIMENSIONS bit when explicitly requested; do not
|
|
// force it. Demon's Souls' 0xF00100 dispatch is paired with a
|
|
// 0x3C004000 element bound (exactly 64 lanes per group), proving the
|
|
// default packet is group-dimensional.
|
|
(modifier & 0xA038u) | 0x41u;
|
|
|
|
[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 (!TryReadUInt32(ctx, entryAddress, out var offset) ||
|
|
!TryReadUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItSetShReg, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, registers[startIndex].Offset & 0xFFFFu))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
firstCommandAddress = firstCommandAddress == 0 ? commandAddress : firstCommandAddress;
|
|
for (var index = startIndex; index < endIndex; index++)
|
|
{
|
|
if (!TryWriteUInt32(
|
|
ctx,
|
|
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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RAcbReset)) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItEventWrite, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, hasAddress ? eventType | 0x100u : eventType & 0x3Fu))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
if (hasAddress &&
|
|
(!TryWriteUInt32(ctx, commandAddress + 8, (uint)eventAddress & ~7u) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(8, ItNop, RAcquireMem)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, 0x8000_0000u) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, noSize ? 0 : (uint)(sizeBytes >> 8)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(baseAddress >> 8)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 24, pollCycles / 40) ||
|
|
!TryWriteUInt32(ctx, 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 (!TryReadUInt64(ctx, stackAddress + sizeof(ulong), out var mask) ||
|
|
!TryReadUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, packetRegister)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)mask))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
if (size == 0)
|
|
{
|
|
if (!TryWriteUInt32(ctx, commandAddress + 16, compareFunction) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
}
|
|
else if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)(mask >> 32)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(reference >> 32)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 28, compareFunction) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(4, ItDispatchIndirect, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, (uint)argumentsAddress) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(argumentsAddress >> 32)) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, markerAddress, Pm4(2, ItNop, RZero)) ||
|
|
!TryWriteUInt32(ctx, markerAddress + 4, CbSetShRegisterRangeMarker) ||
|
|
!TryAllocateCommandDwords(ctx, commandBufferAddress, valueCount + 2, out var commandAddress) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(valueCount + 2, ItSetShReg, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, offset))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
for (uint i = 0; i < valueCount; i++)
|
|
{
|
|
var value = 0u;
|
|
if (valuesAddress != 0 &&
|
|
!TryReadUInt32(ctx, valuesAddress + (i * sizeof(uint)), out value))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
if (!TryWriteUInt32(ctx, 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 (!TryReadUInt64(ctx, stackAddress + 8, out var dataSelectionRaw) ||
|
|
!TryReadUInt64(ctx, stackAddress + 16, out var data) ||
|
|
!TryReadUInt64(ctx, stackAddress + 24, out var gdsOffsetRaw) ||
|
|
!TryReadUInt64(ctx, stackAddress + 32, out var gdsSizeRaw) ||
|
|
!TryReadUInt64(ctx, stackAddress + 40, out var interruptRaw) ||
|
|
!TryReadUInt64(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(8, ItNop, RReleaseMem)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, action | (cachePolicy << 8)) ||
|
|
!TryWriteUInt32(
|
|
ctx,
|
|
commandAddress + 8,
|
|
gcrControl | (dataSelection << 16) | (interrupt << 24)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)destinationAddress) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(destinationAddress >> 32)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, (uint)data) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(data >> 32)) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RDrawReset)) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItIndexType, 0)) ||
|
|
!TryWriteUInt32(ctx, 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 = "8N2tmT3jmC8",
|
|
ExportName = "sceAgcDcbSetIndexCount",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int DcbSetIndexCount(CpuContext ctx)
|
|
{
|
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
|
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
|
if (commandBufferAddress == 0 ||
|
|
!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RIndexCount)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, indexCount))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
return ReturnPointer(ctx, commandAddress);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "mljzuGDZRQ4",
|
|
ExportName = "sceAgcDcbSetIndexCountGetSize",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int DcbSetIndexCountGetSize(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] = 7u * sizeof(uint);
|
|
return (int)ctx[CpuRegister.Rax];
|
|
}
|
|
|
|
[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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNumInstances, 0)) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, baseCommand, Pm4(3, ItIndexBase, 0)) ||
|
|
!TryWriteUInt32(ctx, baseCommand + 4, (uint)indexAddress) ||
|
|
!TryWriteUInt32(ctx, baseCommand + 8, (uint)(indexAddress >> 32)) ||
|
|
!TryWriteUInt32(ctx, baseCommand + 12, Pm4(2, ItIndexBufferSize, 0)) ||
|
|
!TryWriteUInt32(ctx, baseCommand + 16, indexCount))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
// DRAW_INDEX_2 is six dwords: header, maximum index count, the
|
|
// 64-bit index-buffer base, the draw count and the initiator. The
|
|
// former five-dword packet omitted both the real base and the count
|
|
// field, so every call made by Unity looked like a zero-count draw to
|
|
// the submitted-command parser and the complete scene was discarded.
|
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 6, out var drawCommand) ||
|
|
!TryWriteUInt32(ctx, drawCommand, Pm4(6, ItDrawIndex2, 0)) ||
|
|
!TryWriteUInt32(ctx, drawCommand + 4, indexCount) ||
|
|
!TryWriteUInt32(ctx, drawCommand + 8, (uint)indexAddress) ||
|
|
!TryWriteUInt32(ctx, drawCommand + 12, (uint)(indexAddress >> 32)) ||
|
|
!TryWriteUInt32(ctx, drawCommand + 16, indexCount) ||
|
|
!TryWriteUInt32(ctx, drawCommand + 20, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, indexCount) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 16, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
|
!TryWriteUInt32(ctx, 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 = "t1vNu082-jM",
|
|
ExportName = "sceAgcDcbDrawIndexIndirect",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int DcbDrawIndexIndirect(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, 5, out var commandAddress) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(5, ItDrawIndexIndirect, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, dataOffset) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 16, modifier))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
TraceAgc(
|
|
$"agc.dcb_draw_index_indirect buf=0x{commandBufferAddress:X16} " +
|
|
$"cmd=0x{commandAddress:X16} offset=0x{dataOffset:X8} modifier=0x{modifier:X8}");
|
|
return ReturnPointer(ctx, commandAddress);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "mStuvI0zOtc",
|
|
ExportName = "sceAgcDcbDrawIndexIndirectGetSize",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int DcbDrawIndexIndirectGetSize(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] = 5u * sizeof(uint);
|
|
return (int)ctx[CpuRegister.Rax];
|
|
}
|
|
|
|
[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 (!TryReadUInt64(ctx, ctx[CpuRegister.Rsp] + sizeof(ulong), out var enableRaw) ||
|
|
!TryReadUInt64(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(5, ItGetLodStats, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, control) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)destinationAddress & ~0x3Fu) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(destinationAddress >> 32)) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItEventWrite, 0)) ||
|
|
!TryWriteUInt32(ctx, 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 (!TryReadUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(8, ItNop, RAcquireMem)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, (engine << 31) | cbDbOp) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, noSize ? 0 : (uint)(sizeBytes >> 8)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(baseAddress >> 8)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 24, pollCycles / 40) ||
|
|
!TryWriteUInt32(ctx, 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 (!TryReadUInt64(ctx, stackAddress + sizeof(ulong), out var incrementRaw) ||
|
|
!TryReadUInt64(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, RWriteData)) ||
|
|
!TryWriteUInt32(
|
|
ctx,
|
|
commandAddress + 4,
|
|
destination | (cachePolicy << 8) | (increment << 16) | (writeConfirm << 24)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)destinationAddress) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(destinationAddress >> 32)))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
for (uint index = 0; index < dwordCount; index++)
|
|
{
|
|
if (!TryReadUInt32(ctx, dataAddress + ((ulong)index * sizeof(uint)), out var value) ||
|
|
!TryWriteUInt32(ctx, 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 (!TryReadUInt64(ctx, stackAddress + sizeof(ulong), out var reference) ||
|
|
!TryReadUInt64(ctx, stackAddress + (2 * sizeof(ulong)), out var mask) ||
|
|
!TryReadUInt32(ctx, 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 (!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItWaitRegMem, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, compareFunction | ((operation & 1) << 8)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)address) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(address >> 32)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 16, (uint)reference) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, (uint)mask) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 24, pollCycles / 40))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
}
|
|
else if (!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItNop, packetRegister)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, (uint)address) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(address >> 32)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)mask))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
else if (size == 0)
|
|
{
|
|
if (!TryWriteUInt32(ctx, commandAddress + 16, compareFunction | (operation << 8)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
}
|
|
else if (!TryWriteUInt32(ctx, commandAddress + 16, (uint)(mask >> 32)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, (uint)reference) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(reference >> 32)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 28, compareFunction | (operation << 8)) ||
|
|
!TryWriteUInt32(ctx, 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 = "u2T2DiA5hRI",
|
|
ExportName = "sceAgcDcbStallCommandBufferParser",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int DcbStallCommandBufferParser(CpuContext ctx)
|
|
{
|
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
|
var size = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
|
|
var address = ctx[CpuRegister.Rdx];
|
|
var reference = ctx[CpuRegister.Rcx];
|
|
if (commandBufferAddress == 0 || size > 1 ||
|
|
!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RZero)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, 0))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
// Direct execution submits work synchronously, so there is no independent
|
|
// hardware command processor to stall. Keep a well-formed no-op in the DCB
|
|
// so packet addresses and the command-buffer cursor remain coherent.
|
|
TraceAgc(
|
|
$"agc.dcb_stall_parser buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " +
|
|
$"size={size} addr=0x{address:X16} reference=0x{reference:X16}");
|
|
return ReturnPointer(ctx, commandAddress);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "+u6dKSLWM2o",
|
|
ExportName = "sceAgcDcbStallCommandBufferParserGetSize",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int DcbStallCommandBufferParserGetSize(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] = 2u * sizeof(uint);
|
|
return (int)ctx[CpuRegister.Rax];
|
|
}
|
|
|
|
[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 (!TryReadUInt64(ctx, stackAddress + sizeof(ulong), out var control4Raw) ||
|
|
!TryReadUInt64(ctx, stackAddress + (2 * sizeof(ulong)), out var sourceAddress) ||
|
|
!TryReadUInt32(ctx, stackAddress + (3 * sizeof(ulong)), out var byteCount) ||
|
|
!TryReadUInt64(ctx, stackAddress + (4 * sizeof(ulong)), out var control7Raw) ||
|
|
!TryReadUInt64(ctx, stackAddress + (5 * sizeof(ulong)), out var control8Raw) ||
|
|
!TryReadUInt64(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(8, ItNop, RDmaData)) ||
|
|
!TryWriteUInt32(
|
|
ctx,
|
|
commandAddress + 4,
|
|
destination |
|
|
(destinationCachePolicy << 8) |
|
|
(source << 16) |
|
|
(sourceCachePolicy << 24)) ||
|
|
!TryWriteUInt32(
|
|
ctx,
|
|
commandAddress + 8,
|
|
control4 | (control7 << 8) | (control8 << 16) | (control9 << 24)) ||
|
|
!TryWriteUInt32(ctx, 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 = "2ccJz9LQI+w",
|
|
ExportName = "sceAgcDcbDmaDataGetSize",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int DcbDmaDataGetSize(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] = 8u * sizeof(uint);
|
|
return (int)ctx[CpuRegister.Rax];
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "-RnpfpxIhec",
|
|
ExportName = "sceAgcAcbDmaData",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int AcbDmaData(CpuContext ctx)
|
|
{
|
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
|
var sourceSelector = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
|
|
var destinationSelector = (uint)(ctx[CpuRegister.Rdx] & 0xFF);
|
|
var destinationAddress = ctx[CpuRegister.Rcx];
|
|
var stackAddress = ctx[CpuRegister.Rsp];
|
|
if (!TryReadUInt64(ctx, stackAddress + sizeof(ulong), out var sourceOrImmediate) ||
|
|
!TryReadUInt32(ctx, stackAddress + (2 * sizeof(ulong)), out var byteCount) ||
|
|
commandBufferAddress == 0 ||
|
|
byteCount == 0 ||
|
|
byteCount > 256u * 1024u * 1024u ||
|
|
!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDmaData)) ||
|
|
!ctx.TryWriteUInt64(commandAddress + 4, destinationAddress) ||
|
|
!ctx.TryWriteUInt64(commandAddress + 12, sourceOrImmediate) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, byteCount) ||
|
|
!TryWriteUInt32(
|
|
ctx,
|
|
commandAddress + 24,
|
|
sourceSelector | (destinationSelector << 8)))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
return ReturnPointer(ctx, commandAddress);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "M0ttm8h7SKA",
|
|
ExportName = "sceAgcAcbDmaDataGetSize",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int AcbDmaDataGetSize(CpuContext ctx)
|
|
{
|
|
ctx[CpuRegister.Rax] = 8u * sizeof(uint);
|
|
return (int)ctx[CpuRegister.Rax];
|
|
}
|
|
|
|
[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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(4, ItSetBase, 0) | (baseIndex << 1)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, 1) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)address & ~7u) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(3, ItDispatchIndirect, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, dataOffset) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, 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 (!TryWriteUInt32(ctx, commandAddress + 4 + ((ulong)index * sizeof(uint)), value))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
}
|
|
|
|
return ReturnPointer(ctx, commandAddress);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "cpCILPya5Zk",
|
|
ExportName = "sceAgcAcbPushMarker",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int AcbPushMarker(CpuContext ctx) => DcbPushMarker(ctx);
|
|
|
|
[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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNop, RPopMarker)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, 0))
|
|
{
|
|
return ReturnPointer(ctx, 0);
|
|
}
|
|
|
|
return ReturnPointer(ctx, commandAddress);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "6mFxkVqdmbQ",
|
|
ExportName = "sceAgcAcbPopMarker",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int AcbPopMarker(CpuContext ctx) => DcbPopMarker(ctx);
|
|
|
|
[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 ||
|
|
!TryReadUInt32(ctx, commandAddress, out var header))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var packetLength = ((header >> 16) & 0x3FFFu) + 2;
|
|
var destinationOffset = packetLength == 7 ? 4UL : 16UL;
|
|
return ctx.TryWriteUInt64(commandAddress + destinationOffset, destinationAddress)
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
// The SRC counterpart of sceAgcDmaDataPatchSetDstAddressOrOffset. Patches
|
|
// the source field (offset +24, matching the layout written by
|
|
// sceAgcDcbDmaData) of a NOP/RDmaData packet. Games patch this to point a
|
|
// GPU DMA at the data it should copy — commonly a completion/label write.
|
|
// When it is missing the source stays 0, ApplySubmittedDmaData skips the
|
|
// copy (copied=False), and whatever the guest waits on that label for never
|
|
// fires (observed: Void Terrarium's first draw batch presents a black frame
|
|
// then the render pipeline stalls with no further flips).
|
|
[SysAbiExport(
|
|
Nid = "cdDRpqcFGbU",
|
|
ExportName = "sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int DmaDataPatchSetSrcAddressOrOffsetOrImmediate(CpuContext ctx)
|
|
{
|
|
var commandAddress = ctx[CpuRegister.Rdi];
|
|
var sourceValue = ctx[CpuRegister.Rsi];
|
|
if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register) ||
|
|
op != ItNop ||
|
|
register != RDmaData ||
|
|
!TryReadUInt32(ctx, commandAddress, out var header))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var packetLength = ((header >> 16) & 0x3FFFu) + 2;
|
|
var sourceOffset = packetLength == 7 ? 12UL : 24UL;
|
|
return ctx.TryWriteUInt64(commandAddress + sourceOffset, sourceValue)
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "eAy8eGNsCuU",
|
|
ExportName = "sceAgcWriteDataPatchSetCachePolicy",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int WriteDataPatchSetCachePolicy(CpuContext ctx) =>
|
|
PatchWriteDataControlByte(ctx, byteIndex: 1);
|
|
|
|
[SysAbiExport(
|
|
Nid = "tmy-+rBpspY",
|
|
ExportName = "sceAgcWriteDataPatchSetDst",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int WriteDataPatchSetDst(CpuContext ctx) =>
|
|
PatchWriteDataControlByte(ctx, byteIndex: 0);
|
|
|
|
[SysAbiExport(
|
|
Nid = "fPSCdQxgpSw",
|
|
ExportName = "sceAgcWriteDataPatchSetAddressOrOffset",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int WriteDataPatchSetAddressOrOffset(CpuContext ctx)
|
|
{
|
|
// SDK revisions disagree on whether the packet or destination is the
|
|
// first argument. Astro passes (destination, packet), while older
|
|
// captures use (packet, destination), so identify the packet by its
|
|
// header instead of hard-coding one ordering.
|
|
var first = ctx[CpuRegister.Rdi];
|
|
var second = ctx[CpuRegister.Rsi];
|
|
ulong commandAddress;
|
|
ulong destinationAddress;
|
|
if (TryGetPacketIdentity(ctx, first, out var firstOp, out var firstRegister) &&
|
|
firstOp == ItNop && firstRegister == RWriteData)
|
|
{
|
|
commandAddress = first;
|
|
destinationAddress = second;
|
|
}
|
|
else if (TryGetPacketIdentity(ctx, second, out var secondOp, out var secondRegister) &&
|
|
secondOp == ItNop && secondRegister == RWriteData)
|
|
{
|
|
commandAddress = second;
|
|
destinationAddress = first;
|
|
}
|
|
else
|
|
{
|
|
// Astro's SDK 9 ABI passes (address-or-offset, pointer-to-field)
|
|
// rather than the whole packet. The field is already the packet's
|
|
// 64-bit address payload, so patch it directly.
|
|
if (second == 0 || !ctx.TryWriteUInt64(second, first))
|
|
{
|
|
return SetReturn(ctx, second == 0
|
|
? OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
TraceAgc(
|
|
$"agc.patch_write_data_field field=0x{second:X16} value=0x{first:X16}");
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
}
|
|
|
|
TraceAgc(
|
|
$"agc.patch_write_data_addr cmd=0x{commandAddress:X16} dst=0x{destinationAddress:X16}");
|
|
return ctx.TryWriteUInt64(commandAddress + 8, destinationAddress)
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, 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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var fieldOffset = op == ItWaitRegMem
|
|
? 8UL
|
|
: op == ItNop && register is RWaitMem32 or RWaitMem64
|
|
? 4UL
|
|
: 0;
|
|
if (fieldOffset == 0)
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
return ctx.TryWriteUInt64(commandAddress + fieldOffset, address)
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "n485EBnIWmk",
|
|
ExportName = "sceAgcWaitRegMemPatchCompareFunction",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int WaitRegMemPatchCompareFunction(CpuContext ctx)
|
|
{
|
|
var commandAddress = ctx[CpuRegister.Rdi];
|
|
var compareFunction = (uint)ctx[CpuRegister.Rsi];
|
|
if (compareFunction > 7 ||
|
|
!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var fieldOffset = op == ItWaitRegMem
|
|
? 4UL
|
|
: op == ItNop && register == RWaitMem32
|
|
? 16UL
|
|
: op == ItNop && register == RWaitMem64
|
|
? 28UL
|
|
: 0;
|
|
return fieldOffset != 0 &&
|
|
TryPatchUInt32Bits(ctx, commandAddress + fieldOffset, 0x7u, compareFunction)
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, fieldOffset == 0
|
|
? OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "7nOoijNPvEU",
|
|
ExportName = "sceAgcWaitRegMemPatchReference",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int WaitRegMemPatchReference(CpuContext ctx)
|
|
{
|
|
var commandAddress = ctx[CpuRegister.Rdi];
|
|
var reference = ctx[CpuRegister.Rsi];
|
|
if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var wrote = op == ItWaitRegMem
|
|
? TryWriteUInt32(ctx, commandAddress + 16, (uint)reference)
|
|
: op == ItNop && register == RWaitMem32
|
|
? TryWriteUInt32(ctx, commandAddress + 20, (uint)reference)
|
|
: op == ItNop && register == RWaitMem64 &&
|
|
ctx.TryWriteUInt64(commandAddress + 20, reference);
|
|
return wrote
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, op == ItWaitRegMem ||
|
|
(op == ItNop && register is RWaitMem32 or RWaitMem64)
|
|
? OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT
|
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "hXAnLgDHCoI",
|
|
ExportName = "sceAgcWaitRegMemPatchMask",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int WaitRegMemPatchMask(CpuContext ctx)
|
|
{
|
|
var commandAddress = ctx[CpuRegister.Rdi];
|
|
var mask = ctx[CpuRegister.Rsi];
|
|
if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var wrote = op == ItWaitRegMem
|
|
? TryWriteUInt32(ctx, commandAddress + 20, (uint)mask)
|
|
: op == ItNop && register == RWaitMem32
|
|
? TryWriteUInt32(ctx, commandAddress + 12, (uint)mask)
|
|
: op == ItNop && register == RWaitMem64 &&
|
|
ctx.TryWriteUInt64(commandAddress + 12, mask);
|
|
return wrote
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, op == ItWaitRegMem ||
|
|
(op == ItNop && register is RWaitMem32 or RWaitMem64)
|
|
? OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT
|
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
[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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
return ctx.TryWriteUInt64(commandAddress + 12, address)
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "J8YCgfKAMQs",
|
|
ExportName = "sceAgcQueueEndOfPipeActionPatchGcrCntl",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int QueueEndOfPipeActionPatchGcrCntl(CpuContext ctx)
|
|
{
|
|
var commandAddress = ctx[CpuRegister.Rdi];
|
|
if (!IsAgcReleaseMemPacket(ctx, commandAddress))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
return TryPatchUInt32Bits(
|
|
ctx,
|
|
commandAddress + 8,
|
|
0x0000_FFFFu,
|
|
(uint)ctx[CpuRegister.Rsi] & 0xFFFFu)
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "MlEw1feXcjg",
|
|
ExportName = "sceAgcQueueEndOfPipeActionPatchData",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int QueueEndOfPipeActionPatchData(CpuContext ctx)
|
|
{
|
|
var commandAddress = ctx[CpuRegister.Rdi];
|
|
if (!IsAgcReleaseMemPacket(ctx, commandAddress))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
return ctx.TryWriteUInt64(commandAddress + 20, ctx[CpuRegister.Rsi])
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "T9fjQIINoeE",
|
|
ExportName = "sceAgcQueueEndOfPipeActionPatchType",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int QueueEndOfPipeActionPatchType(CpuContext ctx)
|
|
{
|
|
var commandAddress = ctx[CpuRegister.Rdi];
|
|
var dataSelection = (uint)ctx[CpuRegister.Rsi];
|
|
TraceAgc(
|
|
$"agc.eop_patch_type cmd=0x{commandAddress:X16} value=0x{dataSelection:X8}");
|
|
if (dataSelection > 3 || !IsAgcReleaseMemPacket(ctx, commandAddress))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
return TryPatchUInt32Bits(
|
|
ctx,
|
|
commandAddress + 8,
|
|
0x00FF_0000u,
|
|
dataSelection << 16)
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
private static bool IsAgcReleaseMemPacket(CpuContext ctx, ulong commandAddress) =>
|
|
TryGetPacketIdentity(ctx, commandAddress, out var op, out var register) &&
|
|
op == ItNop &&
|
|
register == RReleaseMem;
|
|
|
|
private static bool TryPatchUInt32Bits(
|
|
CpuContext ctx,
|
|
ulong address,
|
|
uint mask,
|
|
uint value)
|
|
{
|
|
return TryReadUInt32(ctx, address, out var current) &&
|
|
TryWriteUInt32(ctx, address, PatchUInt32Bits(current, mask, value));
|
|
}
|
|
|
|
private static uint PatchUInt32Bits(uint current, uint mask, uint value) =>
|
|
(current & ~mask) | (value & mask);
|
|
|
|
[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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(3, ItIndexBase, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, (uint)(indexBufferAddress & 0xFFFF_FFFFUL)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(indexBufferAddress >> 32)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, Pm4(2, ItIndexBufferSize, 0)) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(5, ItDrawIndexOffset2, 0)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, indexCount) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, indexOffset) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, indexCount) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RWaitFlipDone)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, videoOutHandle) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, displayBufferIndex) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 16, 0) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
|
!TryWriteUInt32(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(6, ItNop, RFlip)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, videoOutHandle) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, unchecked((uint)displayBufferIndex)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, flipMode) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(flipArg & 0xFFFF_FFFFUL)) ||
|
|
!TryWriteUInt32(ctx, 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);
|
|
}
|
|
|
|
[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 SetReturn(ctx, 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 SetReturn(ctx, 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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
|
}
|
|
|
|
TraceAgc($"agc.driver_delete_eq_event eq=0x{equeue:X16} id=0x{eventId:X16}");
|
|
return SetReturn(ctx, 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 ||
|
|
!TryReadUInt64(ctx, packetAddress, out var commandAddress) ||
|
|
!TryReadUInt32(ctx, packetAddress + 8, out var dwordCount))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var tracePackets = false;
|
|
if (_traceAgc)
|
|
{
|
|
lock (_submitTraceGate)
|
|
{
|
|
tracePackets = _tracedDcbSizes.Add(dwordCount);
|
|
}
|
|
}
|
|
|
|
if (tracePackets)
|
|
{
|
|
TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}");
|
|
}
|
|
|
|
VulkanVideoPresenter.AttachGuestMemory(ctx.Memory);
|
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
|
lock (gpuState.Gate)
|
|
{
|
|
gpuState.Graphics.QueueName = "dcb.graphics";
|
|
EnqueueSubmittedDcb(
|
|
ctx,
|
|
gpuState,
|
|
gpuState.Graphics,
|
|
commandAddress,
|
|
dwordCount,
|
|
++gpuState.SubmissionSequence,
|
|
tracePackets);
|
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
|
}
|
|
|
|
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 ||
|
|
!TryReadUInt64(ctx, packetAddress, out var commandAddress) ||
|
|
!TryReadUInt32(ctx, packetAddress + 8, out var dwordCount))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var tracePackets = false;
|
|
if (_traceAgc)
|
|
{
|
|
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}");
|
|
}
|
|
|
|
VulkanVideoPresenter.AttachGuestMemory(ctx.Memory);
|
|
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);
|
|
}
|
|
|
|
queueState.QueueName = $"acb.compute[{ownerHandle}]";
|
|
EnqueueSubmittedDcb(
|
|
ctx,
|
|
gpuState,
|
|
queueState,
|
|
commandAddress,
|
|
dwordCount,
|
|
++gpuState.SubmissionSequence,
|
|
tracePackets);
|
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
|
}
|
|
|
|
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
if (!TryWriteUInt32(ctx, outAddress, 256))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
|
|
return SetReturn(ctx, 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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
if (!TryWriteUInt32(ctx, ownerAddress, DefaultAgcOwner))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
}
|
|
|
|
[SysAbiExport(
|
|
Nid = "W5z4eZrjEas",
|
|
ExportName = "sceAgcDriverRegisterResource",
|
|
Target = Generation.Gen5,
|
|
LibraryName = "libSceAgc")]
|
|
public static int DriverRegisterResource(CpuContext ctx)
|
|
{
|
|
var resourceAddress = ctx[CpuRegister.Rdi];
|
|
var owner = (uint)ctx[CpuRegister.Rsi];
|
|
var nameAddress = ctx[CpuRegister.Rdx];
|
|
var type = (uint)ctx[CpuRegister.R8];
|
|
var flags = (uint)ctx[CpuRegister.R9];
|
|
|
|
TraceAgc(
|
|
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
|
|
$"name=0x{nameAddress:X16} type={type} flags={flags}");
|
|
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
}
|
|
|
|
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
|
|
#pragma warning disable SHEM006
|
|
[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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
|
}
|
|
#pragma warning restore SHEM006
|
|
|
|
[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;
|
|
}
|
|
|
|
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
|
|
#pragma warning disable SHEM006
|
|
[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) ||
|
|
!TryWriteUInt32(ctx, 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);
|
|
}
|
|
#pragma warning restore SHEM006
|
|
|
|
private static void EnqueueSubmittedDcb(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
ulong commandAddress,
|
|
uint dwordCount,
|
|
ulong submissionId,
|
|
bool tracePackets)
|
|
{
|
|
state.PendingSubmissions.Enqueue(new SubmittedDcbState.PendingSubmission(
|
|
commandAddress,
|
|
dwordCount,
|
|
submissionId,
|
|
tracePackets));
|
|
PumpSubmittedQueue(ctx, gpuState, state);
|
|
}
|
|
|
|
private static void PumpSubmittedQueue(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state)
|
|
{
|
|
if (state.IsSuspended)
|
|
{
|
|
return;
|
|
}
|
|
|
|
while (!state.HasActiveSubmission &&
|
|
state.PendingSubmissions.TryDequeue(out var submission))
|
|
{
|
|
state.HasActiveSubmission = true;
|
|
state.ActiveSubmissionId = submission.SubmissionId;
|
|
state.IsSuspended = ParseSubmittedDcb(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
submission.CommandAddress,
|
|
submission.DwordCount,
|
|
submission.TracePackets);
|
|
if (state.IsSuspended)
|
|
{
|
|
return;
|
|
}
|
|
|
|
state.HasActiveSubmission = false;
|
|
NotifySubmittedDcbCompleted(gpuState, state, submission.SubmissionId);
|
|
}
|
|
}
|
|
|
|
private static void NotifySubmittedDcbCompleted(
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
ulong submissionId)
|
|
{
|
|
if (!ReferenceEquals(state, gpuState.Graphics) ||
|
|
state.CompletionEventNotifiedSubmissionId == submissionId)
|
|
{
|
|
return;
|
|
}
|
|
|
|
state.CompletionEventNotifiedSubmissionId = submissionId;
|
|
void TriggerCompletionEvents()
|
|
{
|
|
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEvents(
|
|
ident: 0,
|
|
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
|
data: 0);
|
|
if (_compatibilitySubmitCompletionEvent)
|
|
{
|
|
triggered += KernelEventQueueCompatExports.TriggerRegisteredEventsDistinct(
|
|
KernelEventQueueCompatExports.KernelEventFilterGraphics);
|
|
}
|
|
TraceAgc(
|
|
$"agc.driver_submit_dcb completion submission={submissionId} " +
|
|
$"queues={triggered}");
|
|
}
|
|
|
|
// A DCB is complete only after its translated Vulkan work and ordered
|
|
// guest-memory writes have finished. Put the notification on that same
|
|
// logical graphics queue instead of approximating completion with a
|
|
// timer, which can wake Unity while its upload data is still stale.
|
|
if (VulkanVideoPresenter.SubmitOrderedGuestAction(
|
|
TriggerCompletionEvents,
|
|
$"agc submit completion {submissionId}") == 0)
|
|
{
|
|
TriggerCompletionEvents();
|
|
}
|
|
}
|
|
|
|
// Returns true only when parsing stopped on an unsatisfied WAIT_REG_MEM.
|
|
// Malformed packets are dropped as completed so one bad submission cannot
|
|
// permanently wedge all later work on the same hardware queue.
|
|
private static bool ParseSubmittedDcb(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
ulong commandAddress,
|
|
uint dwordCount,
|
|
bool tracePackets)
|
|
{
|
|
if (commandAddress == 0 || dwordCount == 0 || dwordCount > 1_000_000)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
using var guestQueueScope = VulkanVideoPresenter.EnterGuestQueue(
|
|
state.QueueName,
|
|
state.ActiveSubmissionId);
|
|
var windowByteCount = checked((int)(dwordCount * sizeof(uint)));
|
|
var rented = VulkanVideoPresenter.GuestDataPool.Rent(windowByteCount);
|
|
try
|
|
{
|
|
if (ctx.Memory.TryRead(commandAddress, rented.AsSpan(0, windowByteCount)))
|
|
{
|
|
_dcbWindowBuffer = rented;
|
|
_dcbWindowStart = commandAddress;
|
|
_dcbWindowByteLength = windowByteCount;
|
|
}
|
|
|
|
return ParseSubmittedDcbCore(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
commandAddress,
|
|
dwordCount,
|
|
tracePackets);
|
|
}
|
|
finally
|
|
{
|
|
_dcbWindowBuffer = null;
|
|
_dcbWindowByteLength = 0;
|
|
VulkanVideoPresenter.GuestDataPool.Return(rented);
|
|
}
|
|
}
|
|
|
|
private static bool ParseSubmittedDcbCore(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
ulong commandAddress,
|
|
uint dwordCount,
|
|
bool tracePackets)
|
|
{
|
|
var offset = 0u;
|
|
while (offset < dwordCount)
|
|
{
|
|
var currentAddress = commandAddress + ((ulong)offset * sizeof(uint));
|
|
if (!TryReadUInt32(ctx, currentAddress, out var header))
|
|
{
|
|
TracePacketParseFailure(state, currentAddress, offset, 0, "header-read");
|
|
return false;
|
|
}
|
|
|
|
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)
|
|
{
|
|
TracePacketParseFailure(
|
|
state,
|
|
currentAddress,
|
|
offset,
|
|
header,
|
|
$"packet-type-{packetType}");
|
|
return false;
|
|
}
|
|
|
|
var length = Pm4Length(header);
|
|
if (length == 0 || offset + length > dwordCount)
|
|
{
|
|
TracePacketParseFailure(
|
|
state,
|
|
currentAddress,
|
|
offset,
|
|
header,
|
|
$"length-{length}-remaining-{dwordCount - offset}");
|
|
return false;
|
|
}
|
|
|
|
var op = (header >> 8) & 0xFFu;
|
|
var register = (header >> 2) & 0x3Fu;
|
|
if (_traceFramePackets && ReferenceEquals(state, gpuState.Graphics))
|
|
{
|
|
var packetKey = (op, op == ItNop ? register : uint.MaxValue);
|
|
state.FramePacketCounts[packetKey] =
|
|
state.FramePacketCounts.TryGetValue(packetKey, out var packetCount)
|
|
? packetCount + 1
|
|
: 1;
|
|
state.FramePacketCount++;
|
|
}
|
|
if (tracePackets)
|
|
{
|
|
TraceSubmittedPacket(ctx, currentAddress, offset, header, length, op, register);
|
|
}
|
|
|
|
if (_traceDraws)
|
|
{
|
|
CountSubmittedOpcode(op, register);
|
|
}
|
|
|
|
if (op == ItNop &&
|
|
register is RDrawReset or RAcbReset &&
|
|
length >= 2)
|
|
{
|
|
ResetSubmittedParserState(state);
|
|
TraceAgc(
|
|
$"agc.queue_reset queue={state.QueueName} " +
|
|
$"submission={state.ActiveSubmissionId} " +
|
|
$"kind={(register == RDrawReset ? "draw" : "acb")} " +
|
|
$"packet=0x{currentAddress:X16}");
|
|
}
|
|
|
|
if (op == ItNop && register == RAcquireMem && length >= 8)
|
|
{
|
|
ApplySubmittedAcquireMem(
|
|
ctx,
|
|
state,
|
|
currentAddress,
|
|
tracePackets);
|
|
}
|
|
|
|
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 &&
|
|
TryReadUInt32(ctx, currentAddress + 4, out var baseSelector) &&
|
|
baseSelector == 1 &&
|
|
TryReadUInt64(ctx, currentAddress + 8, out var indirectArgsAddress))
|
|
{
|
|
state.IndirectArgsAddress = indirectArgsAddress;
|
|
}
|
|
|
|
if (op == ItEventWrite &&
|
|
length >= 2 &&
|
|
TryReadUInt32(ctx, currentAddress + sizeof(uint), out var eventTypeRaw))
|
|
{
|
|
var eventType = eventTypeRaw & 0x3Fu;
|
|
SubmitOrderedGpuSideEffect(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
() =>
|
|
{
|
|
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter(
|
|
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
|
eventType);
|
|
if (tracePackets)
|
|
{
|
|
TraceAgc($"agc.dcb.event type=0x{eventType:X2} queues={triggered}");
|
|
}
|
|
},
|
|
$"event_write type=0x{eventType:X2}",
|
|
currentAddress);
|
|
}
|
|
|
|
if (op == ItNop && register == RReleaseMem && length >= 7)
|
|
{
|
|
ApplySubmittedReleaseMem(ctx, gpuState, state, currentAddress, tracePackets);
|
|
}
|
|
|
|
if (op == ItReleaseMem && length >= 8)
|
|
{
|
|
ApplySubmittedStandardReleaseMem(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
currentAddress,
|
|
tracePackets);
|
|
}
|
|
|
|
if (op == ItNop && register == RWriteData && length >= 4)
|
|
{
|
|
ApplySubmittedWriteData(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
currentAddress,
|
|
length,
|
|
standardPacket: false,
|
|
tracePacket: tracePackets);
|
|
}
|
|
|
|
if (op == ItWriteData && length >= 4)
|
|
{
|
|
ApplySubmittedWriteData(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
currentAddress,
|
|
length,
|
|
standardPacket: true,
|
|
tracePacket: tracePackets);
|
|
}
|
|
|
|
if (op == ItNop && register == RDmaData && length >= 7)
|
|
{
|
|
ApplySubmittedDmaData(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
currentAddress,
|
|
compactLayout: length == 7,
|
|
tracePacket: tracePackets);
|
|
}
|
|
|
|
if (op == ItDmaData && length >= 7)
|
|
{
|
|
ApplySubmittedStandardDmaData(ctx, gpuState, state, currentAddress);
|
|
}
|
|
|
|
if (op == ItIndexBase &&
|
|
length >= 3 &&
|
|
TryReadUInt32(ctx, currentAddress + 4, out var indexBaseLo) &&
|
|
TryReadUInt32(ctx, currentAddress + 8, out var indexBaseHi))
|
|
{
|
|
state.IndexBufferAddress =
|
|
indexBaseLo | ((ulong)indexBaseHi << 32);
|
|
}
|
|
|
|
if (op == ItIndexBufferSize &&
|
|
length >= 2 &&
|
|
TryReadUInt32(ctx, currentAddress + 4, out var indexBufferCount))
|
|
{
|
|
state.IndexBufferCount = indexBufferCount;
|
|
}
|
|
|
|
if (op == ItNop &&
|
|
register == RIndexCount &&
|
|
length >= 2 &&
|
|
TryReadUInt32(ctx, currentAddress + 4, out var customIndexCount))
|
|
{
|
|
state.IndexBufferCount = customIndexCount;
|
|
}
|
|
|
|
if (op == ItIndexType &&
|
|
length >= 2 &&
|
|
TryReadUInt32(ctx, currentAddress + 4, out var indexSize))
|
|
{
|
|
state.IndexSize = indexSize & 0x3;
|
|
}
|
|
|
|
if (op == ItNumInstances &&
|
|
length >= 2 &&
|
|
TryReadUInt32(ctx, currentAddress + 4, out var instanceCount))
|
|
{
|
|
state.InstanceCount = Math.Max(instanceCount, 1);
|
|
}
|
|
|
|
if (op == ItNop &&
|
|
register is RWaitMem32 or RWaitMem64 &&
|
|
length >= (register == RWaitMem32 ? 6u : 9u))
|
|
{
|
|
if (HandleSubmittedWaitRegMem(
|
|
ctx, state, commandAddress, currentAddress, offset, length,
|
|
dwordCount, is64Bit: register == RWaitMem64, isStandard: false,
|
|
tracePackets))
|
|
{
|
|
return true; // DCB suspended until the awaited label is written
|
|
}
|
|
}
|
|
|
|
if (op == ItWaitRegMem && length >= 7)
|
|
{
|
|
if (HandleSubmittedWaitRegMem(
|
|
ctx, state, commandAddress, currentAddress, offset, length,
|
|
dwordCount, is64Bit: false, isStandard: true, tracePackets))
|
|
{
|
|
return true; // DCB suspended until the awaited label is written
|
|
}
|
|
}
|
|
|
|
if (TryReadSubmittedDrawCount(
|
|
ctx,
|
|
state,
|
|
currentAddress,
|
|
length,
|
|
op,
|
|
out var indexCount) &&
|
|
indexCount != 0)
|
|
{
|
|
state.FrameDrawCount++;
|
|
if (_traceAgcShader)
|
|
{
|
|
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 &&
|
|
TryReadUInt32(ctx, currentAddress + 4, out var autoIndexCount) &&
|
|
autoIndexCount != 0)
|
|
{
|
|
state.FrameDrawCount++;
|
|
TryTranslateGuestDraw(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
autoIndexCount,
|
|
indexed: false);
|
|
}
|
|
|
|
if ((op is ItDispatchDirect or ItDispatchIndirect) &&
|
|
TryReadComputeDispatch(
|
|
ctx,
|
|
state,
|
|
currentAddress,
|
|
length,
|
|
op,
|
|
out var dispatch))
|
|
{
|
|
state.FrameDispatchCount++;
|
|
ObserveComputeDispatch(ctx, gpuState, state, dispatch);
|
|
}
|
|
|
|
if (op == ItNop &&
|
|
register == RWaitFlipDone &&
|
|
length >= 3 &&
|
|
TryReadUInt32(ctx, currentAddress + 4, out var waitVideoOutHandle) &&
|
|
TryReadUInt32(ctx, currentAddress + 8, out var waitDisplayBufferIndex))
|
|
{
|
|
var waitSequence = VulkanVideoPresenter.SubmitOrderedGuestFlipWait(
|
|
unchecked((int)waitVideoOutHandle),
|
|
unchecked((int)waitDisplayBufferIndex));
|
|
TraceAgcShader(
|
|
$"agc.flip_wait_safe queue={state.QueueName} " +
|
|
$"submission={state.ActiveSubmissionId} " +
|
|
$"handle={waitVideoOutHandle} index={waitDisplayBufferIndex} " +
|
|
$"work_sequence={waitSequence}");
|
|
}
|
|
|
|
if (op == ItNop && register == RFlip && length >= 6)
|
|
{
|
|
TraceFramePacketSummary(state);
|
|
SyncCpuWrittenGuestImages(ctx);
|
|
if (!TryReadUInt32(ctx, currentAddress + 4, out var videoOutHandle) ||
|
|
!TryReadUInt32(ctx, currentAddress + 8, out var displayBufferIndexRaw) ||
|
|
!TryReadUInt32(ctx, currentAddress + 12, out var flipMode) ||
|
|
!TryReadUInt32(ctx, currentAddress + 16, out var flipArgLo) ||
|
|
!TryReadUInt32(ctx, currentAddress + 20, out var flipArgHi))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var flipArg = unchecked((long)(((ulong)flipArgHi << 32) | flipArgLo));
|
|
var displayBufferIndex = unchecked((int)displayBufferIndexRaw);
|
|
var handle = unchecked((int)videoOutHandle);
|
|
if (state.PendingTargetlessDraw is { } pendingComposite &&
|
|
VideoOutExports.TryGetDisplayBufferInfo(
|
|
handle,
|
|
displayBufferIndex,
|
|
out var pendingDisplayBuffer) &&
|
|
state.KnownRenderTargets.TryGetValue(
|
|
pendingDisplayBuffer.Address,
|
|
out var pendingDisplayTarget))
|
|
{
|
|
var textures = CreateGuestDrawTextures(
|
|
ctx,
|
|
pendingComposite.Textures,
|
|
out _);
|
|
var globalMemoryBuffers =
|
|
CreateTranslatedDrawGlobalBuffers(pendingComposite);
|
|
var vertexBuffers =
|
|
CreateGuestVertexBuffers(pendingComposite.VertexInputs);
|
|
ProvideRenderTargetInitialData(ctx, pendingDisplayTarget);
|
|
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
|
|
pendingComposite.PixelShader,
|
|
textures,
|
|
globalMemoryBuffers,
|
|
pendingComposite.AttributeCount,
|
|
[new GuestRenderTarget(
|
|
pendingDisplayTarget.Address,
|
|
pendingDisplayTarget.Width,
|
|
pendingDisplayTarget.Height,
|
|
pendingDisplayTarget.Format,
|
|
pendingDisplayTarget.NumberType)],
|
|
pendingComposite.VertexShader,
|
|
pendingComposite.VertexCount,
|
|
pendingComposite.InstanceCount,
|
|
pendingComposite.PrimitiveType,
|
|
pendingComposite.IndexBuffer,
|
|
vertexBuffers,
|
|
pendingComposite.RenderState,
|
|
pendingComposite.DepthTarget,
|
|
pendingComposite.PixelShaderAddress);
|
|
TraceAgcShader(
|
|
$"agc.deferred_composite ps=0x{pendingComposite.PixelShaderAddress:X16} " +
|
|
$"src=0x{pendingComposite.Textures.FirstOrDefault()?.Descriptor.Address ?? 0:X16} " +
|
|
$"dst=0x{pendingDisplayTarget.Address:X16} " +
|
|
$"size={pendingDisplayTarget.Width}x{pendingDisplayTarget.Height}");
|
|
state.PendingTargetlessDraw = null;
|
|
state.TranslatedDraw = null;
|
|
}
|
|
|
|
if (VideoOutExports.TryGetDisplayBufferInfo(
|
|
handle,
|
|
displayBufferIndex,
|
|
out var cachedDisplayBuffer) &&
|
|
GuestGpu.Current.TrySubmitOrderedGuestImageFlip(
|
|
handle,
|
|
displayBufferIndex,
|
|
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 = CreateGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount);
|
|
var globalMemoryBuffers =
|
|
CreateTranslatedDrawGlobalBuffersForPresent(ctx, translatedDraw);
|
|
GuestGpu.Current.SubmitTranslatedDraw(
|
|
translatedDraw.PixelShader,
|
|
textures,
|
|
globalMemoryBuffers,
|
|
translatedDisplayBuffer.Width,
|
|
translatedDisplayBuffer.Height,
|
|
translatedDraw.AttributeCount);
|
|
TraceAgcShader(
|
|
$"agc.shader_present ps=0x{translatedDraw.PixelShaderAddress:X16} " +
|
|
$"spirv={translatedDraw.PixelShader.Payload.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))
|
|
{
|
|
GuestGpu.Current.SubmitGuestDraw(
|
|
state.GuestDrawKind,
|
|
displayBuffer.Width,
|
|
displayBuffer.Height);
|
|
}
|
|
|
|
_ = VideoOutExports.SubmitFlipFromAgc(ctx, handle, displayBufferIndex, unchecked((int)flipMode), flipArg);
|
|
state.SawIndexedDraw = false;
|
|
state.GuestDrawKind = GuestDrawKind.None;
|
|
if (state.PendingTargetlessDraw is { } unusedPendingDraw)
|
|
{
|
|
ReturnPooledDrawArrays(
|
|
unusedPendingDraw,
|
|
globals: true,
|
|
vertex: true,
|
|
index: true);
|
|
state.PendingTargetlessDraw = null;
|
|
}
|
|
state.TranslatedDraw = null;
|
|
}
|
|
|
|
offset += length;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void TraceFramePacketSummary(SubmittedDcbState state)
|
|
{
|
|
if (!_traceFramePackets)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var flip = ++state.FlipCount;
|
|
if (flip <= 8 || flip % 60 == 0 || state.FrameDrawCount == 0)
|
|
{
|
|
var opcodes = string.Join(
|
|
',',
|
|
state.FramePacketCounts
|
|
.OrderByDescending(entry => entry.Value)
|
|
.ThenBy(entry => entry.Key.Op)
|
|
.Take(32)
|
|
.Select(entry => entry.Key.Register == uint.MaxValue
|
|
? $"0x{entry.Key.Op:X2}:{entry.Value}"
|
|
: $"0x{entry.Key.Op:X2}/r{entry.Key.Register}:{entry.Value}"));
|
|
Console.Error.WriteLine(
|
|
$"[FRAMEPKT] flip={flip} submission={state.ActiveSubmissionId} " +
|
|
$"packets={state.FramePacketCount} draws={state.FrameDrawCount} " +
|
|
$"dispatches={state.FrameDispatchCount} opcodes=[{opcodes}]");
|
|
}
|
|
|
|
state.FramePacketCounts.Clear();
|
|
state.FramePacketCount = 0;
|
|
state.FrameDrawCount = 0;
|
|
state.FrameDispatchCount = 0;
|
|
}
|
|
|
|
private static void TracePacketParseFailure(
|
|
SubmittedDcbState state,
|
|
ulong address,
|
|
uint offset,
|
|
uint header,
|
|
string reason)
|
|
{
|
|
if (!_traceFramePackets ||
|
|
Interlocked.Increment(ref _packetParseFailureTraceCount) > 128)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[FRAMEPKT] parse-failure queue={state.QueueName} " +
|
|
$"submission={state.ActiveSubmissionId} offset={offset} " +
|
|
$"address=0x{address:X16} header=0x{header:X8} reason={reason}");
|
|
}
|
|
|
|
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,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
ulong packetAddress,
|
|
bool compactLayout,
|
|
bool tracePacket)
|
|
{
|
|
var byteCountOffset = compactLayout ? 20UL : 12UL;
|
|
var destinationOffset = compactLayout ? 4UL : 16UL;
|
|
var sourceOffset = compactLayout ? 12UL : 24UL;
|
|
if (!TryReadUInt32(ctx, packetAddress + byteCountOffset, out var byteCount) ||
|
|
!TryReadUInt64(ctx, packetAddress + destinationOffset, out var destinationAddress) ||
|
|
!TryReadUInt64(ctx, packetAddress + sourceOffset, out var sourceAddress))
|
|
{
|
|
return;
|
|
}
|
|
|
|
SubmitOrderedGpuSideEffect(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
() =>
|
|
{
|
|
InvalidateDcbWindowIfOverlaps(destinationAddress, byteCount);
|
|
var immediateFill =
|
|
compactLayout &&
|
|
destinationAddress >= 0x10000 &&
|
|
sourceAddress <= uint.MaxValue;
|
|
var copied =
|
|
byteCount != 0 &&
|
|
byteCount <= 256u * 1024u * 1024u &&
|
|
destinationAddress != 0 &&
|
|
(immediateFill
|
|
? TryFillGuestMemory(ctx, (uint)sourceAddress, destinationAddress, byteCount)
|
|
: sourceAddress != 0 &&
|
|
TryCopyGuestMemory(ctx, sourceAddress, destinationAddress, byteCount));
|
|
if (copied)
|
|
{
|
|
MirrorDmaWriteToGuestImage(
|
|
ctx,
|
|
destinationAddress,
|
|
byteCount,
|
|
immediateFill ? (uint)sourceAddress : null);
|
|
}
|
|
|
|
if (tracePacket)
|
|
{
|
|
TraceAgc(
|
|
$"agc.dcb.dma_data dst=0x{destinationAddress:X16} " +
|
|
$"src=0x{sourceAddress:X16} bytes={byteCount} " +
|
|
$"fill={immediateFill} copied={copied}");
|
|
}
|
|
},
|
|
$"agc_dma_data dst=0x{destinationAddress:X16} bytes={byteCount}",
|
|
packetAddress,
|
|
destinationAddress,
|
|
byteCount);
|
|
}
|
|
|
|
private static void SubmitOrderedGpuSideEffect(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
Action action,
|
|
string debugName,
|
|
ulong packetAddress,
|
|
ulong producerAddress = 0,
|
|
ulong producerLength = 0)
|
|
{
|
|
var producer = RegisterLabelProducer(
|
|
ctx.Memory,
|
|
state,
|
|
packetAddress,
|
|
producerAddress,
|
|
producerLength,
|
|
debugName);
|
|
|
|
void CompleteAndWake()
|
|
{
|
|
CompleteLabelProducer(producer);
|
|
if (GpuWaitRegistry.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Resuming a DCB can enqueue another compute dispatch and wait for
|
|
// it. Never do that reentrantly on the Vulkan render thread.
|
|
ThreadPool.UnsafeQueueUserWorkItem(
|
|
static state =>
|
|
{
|
|
var (resumeContext, resumeGpuState) = state;
|
|
lock (resumeGpuState.Gate)
|
|
{
|
|
DrainResumableDcbs(
|
|
resumeContext,
|
|
resumeGpuState,
|
|
tracePackets: _traceAgc);
|
|
}
|
|
},
|
|
(ctx, gpuState),
|
|
preferLocal: false);
|
|
}
|
|
|
|
void ApplyAndQueueCompletion()
|
|
{
|
|
action();
|
|
// DMA side effects can enqueue a Vulkan image mirror while this
|
|
// ordered action is executing. Completing the label here would
|
|
// wake another queue before that mirror is visible. Queue a
|
|
// second same-queue ordered action after all immediate follow-up
|
|
// writes; it fences those writes before publishing the producer.
|
|
if (VulkanVideoPresenter.SubmitOrderedGuestAction(
|
|
CompleteAndWake,
|
|
$"{debugName} completion") == 0)
|
|
{
|
|
CompleteAndWake();
|
|
}
|
|
}
|
|
|
|
if (VulkanVideoPresenter.SubmitOrderedGuestAction(
|
|
ApplyAndQueueCompletion,
|
|
debugName) == 0)
|
|
{
|
|
// Headless/startup submissions have no Vulkan queue to order
|
|
// against, so retaining the previous immediate behavior is exact.
|
|
ApplyAndQueueCompletion();
|
|
}
|
|
}
|
|
|
|
private static LabelProducerTrace? RegisterLabelProducer(
|
|
object memory,
|
|
SubmittedDcbState state,
|
|
ulong packetAddress,
|
|
ulong address,
|
|
ulong length,
|
|
string debugName)
|
|
{
|
|
if (address == 0 || length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var producer = new LabelProducerTrace
|
|
{
|
|
Sequence = Interlocked.Increment(ref _labelProducerSequence),
|
|
Memory = memory,
|
|
Address = address,
|
|
Length = length,
|
|
PacketAddress = packetAddress,
|
|
SubmissionId = state.ActiveSubmissionId,
|
|
QueueName = state.QueueName,
|
|
DebugName = debugName,
|
|
};
|
|
lock (_labelProducerGate)
|
|
{
|
|
if (_labelProducers.Count >= 4096)
|
|
{
|
|
_labelProducers.RemoveRange(0, 1024);
|
|
}
|
|
|
|
_labelProducers.Add(producer);
|
|
}
|
|
|
|
if (_traceAgc)
|
|
{
|
|
foreach (var waiting in GpuWaitRegistry.SnapshotInRange(memory, address, length))
|
|
{
|
|
TraceAgc(
|
|
$"agc.wait_producer_scheduled label=0x{waiting.Address:X16} " +
|
|
$"waiters={waiting.Count} producer_seq={producer.Sequence} " +
|
|
$"queue={producer.QueueName} submission={producer.SubmissionId} " +
|
|
$"packet=0x{packetAddress:X16} action='{debugName}'");
|
|
}
|
|
}
|
|
|
|
return producer;
|
|
}
|
|
|
|
private static void CompleteLabelProducer(LabelProducerTrace? producer)
|
|
{
|
|
if (producer is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (_labelProducerGate)
|
|
{
|
|
producer.Completed = true;
|
|
}
|
|
|
|
if (_traceAgc)
|
|
{
|
|
foreach (var waiting in GpuWaitRegistry.SnapshotInRange(
|
|
producer.Memory,
|
|
producer.Address,
|
|
producer.Length))
|
|
{
|
|
TraceAgc(
|
|
$"agc.wait_producer_completed label=0x{waiting.Address:X16} " +
|
|
$"waiters={waiting.Count} producer_seq={producer.Sequence} " +
|
|
$"queue={producer.QueueName} submission={producer.SubmissionId} " +
|
|
$"action='{producer.DebugName}'");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void TraceWaitProducerState(
|
|
object memory,
|
|
in GpuWaitRegistry.WaitingDcb waiter,
|
|
ulong commandAddress,
|
|
ulong packetAddress,
|
|
bool stale,
|
|
ulong? currentValue = null)
|
|
{
|
|
LabelProducerTrace? producer = null;
|
|
lock (_labelProducerGate)
|
|
{
|
|
for (var index = _labelProducers.Count - 1; index >= 0; index--)
|
|
{
|
|
var candidate = _labelProducers[index];
|
|
if (!ReferenceEquals(candidate.Memory, memory) ||
|
|
!RangesOverlap(
|
|
candidate.Address,
|
|
candidate.Length,
|
|
waiter.WaitAddress,
|
|
waiter.Is64Bit ? (ulong)sizeof(ulong) : sizeof(uint)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
producer = candidate;
|
|
break;
|
|
}
|
|
|
|
if (_tracedProducerlessWaits.Count >= 4096)
|
|
{
|
|
_tracedProducerlessWaits.Clear();
|
|
}
|
|
|
|
if (!stale && producer is null &&
|
|
!_tracedProducerlessWaits.Add(
|
|
(memory, waiter.WaitAddress, waiter.SubmissionId)))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Producer-backed waits are trace-only. Keep the producer lookup above
|
|
// because producerless waits are always warned, but do not build the
|
|
// detailed condition strings when AGC tracing is disabled.
|
|
if (producer is not null && !_traceAgc)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var prefix = stale ? "agc.wait_stale" : "agc.wait_suspended";
|
|
var current = currentValue.HasValue
|
|
? $"0x{currentValue.Value:X16}"
|
|
: "unreadable";
|
|
var condition =
|
|
$"value={current} mask=0x{waiter.Mask:X16} " +
|
|
$"ref=0x{waiter.ReferenceValue:X16} cmp={waiter.CompareFunction} " +
|
|
$"control=0x{waiter.ControlValue:X8} bits={(waiter.Is64Bit ? 64 : 32)} " +
|
|
$"form={(waiter.IsStandard ? "standard" : "agc-nop")}";
|
|
if (producer is null)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARN] {prefix} label=0x{waiter.WaitAddress:X16} " +
|
|
$"queue={waiter.QueueName} submission={waiter.SubmissionId} " +
|
|
$"command=0x{commandAddress:X16} packet=0x{packetAddress:X16} " +
|
|
condition + " " +
|
|
"producer=none-observed; remaining-suspended");
|
|
return;
|
|
}
|
|
|
|
TraceAgc(
|
|
$"{prefix} label=0x{waiter.WaitAddress:X16} " +
|
|
$"queue={waiter.QueueName} submission={waiter.SubmissionId} " +
|
|
condition + " " +
|
|
$"producer_seq={producer.Sequence} producer_state=" +
|
|
$"{(producer.Completed ? "completed" : "queued")} " +
|
|
$"producer_queue={producer.QueueName} " +
|
|
$"producer_submission={producer.SubmissionId} " +
|
|
$"producer_packet=0x{producer.PacketAddress:X16} " +
|
|
$"action='{producer.DebugName}'");
|
|
}
|
|
|
|
private static void ApplySubmittedAcquireMem(
|
|
CpuContext ctx,
|
|
SubmittedDcbState state,
|
|
ulong packetAddress,
|
|
bool tracePacket)
|
|
{
|
|
if (!TryDecodeSubmittedAcquireMem(ctx, packetAddress, out var acquire))
|
|
{
|
|
TraceAgc(
|
|
$"agc.acquire_mem_decode_failed queue={state.QueueName} " +
|
|
$"submission={state.ActiveSubmissionId} packet=0x{packetAddress:X16}");
|
|
return;
|
|
}
|
|
|
|
var queueName = state.QueueName;
|
|
var submissionId = state.ActiveSubmissionId;
|
|
var debugName =
|
|
$"acquire_mem base=0x{acquire.BaseAddress:X16} size=0x{acquire.SizeBytes:X16} " +
|
|
$"gcr=0x{acquire.GcrControl:X8}";
|
|
void ApplyAcquire()
|
|
{
|
|
// ExecuteOrderedGuestAction first flushes and waits for this guest
|
|
// queue, then writes back dirty guest buffers. At that exact PM4
|
|
// point, refresh only tracked guest images covered by the acquire
|
|
// range. Cached sampled textures use the same dirty tracker and
|
|
// are evicted by the presenter without throwing away clean cache
|
|
// entries (hardware invalidation does not imply changed bytes).
|
|
if (acquire.InvalidatesGuestResources)
|
|
{
|
|
SyncCpuWrittenGuestImages(
|
|
ctx,
|
|
acquire.BaseAddress,
|
|
acquire.CoversAllGuestMemory
|
|
? ulong.MaxValue
|
|
: acquire.SizeBytes);
|
|
}
|
|
|
|
if (tracePacket)
|
|
{
|
|
TraceAgc(
|
|
$"agc.acquire_mem_applied queue={queueName} " +
|
|
$"submission={submissionId} packet=0x{packetAddress:X16} " +
|
|
$"work_sequence={VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics}");
|
|
}
|
|
}
|
|
|
|
var sequence = VulkanVideoPresenter.SubmitOrderedGuestAction(
|
|
ApplyAcquire,
|
|
debugName);
|
|
if (sequence == 0)
|
|
{
|
|
// Headless startup has no host GPU queue, but the guest-memory
|
|
// cache model still needs the same invalidation semantics.
|
|
ApplyAcquire();
|
|
}
|
|
|
|
// The bulk PM4 read is itself a parser-side cache. Do not retain it
|
|
// across a guest cache-invalidation point; subsequent packets return
|
|
// to live guest memory while the host barrier remains ordered in the
|
|
// logical GPU queue. Submission stays asynchronous, matching hardware
|
|
// and avoiding a CPU stall for every ACQUIRE_MEM packet.
|
|
_dcbWindowBuffer = null;
|
|
_dcbWindowByteLength = 0;
|
|
|
|
if (tracePacket)
|
|
{
|
|
TraceAgc(
|
|
$"agc.acquire_mem queue={queueName} " +
|
|
$"submission={submissionId} packet=0x{packetAddress:X16} " +
|
|
$"engine={acquire.Engine} cbdb=0x{acquire.CbDbControl:X8} " +
|
|
$"base=0x{acquire.BaseAddress:X16} size=0x{acquire.SizeBytes:X16} " +
|
|
$"scope={(acquire.CoversAllGuestMemory ? "all" : "range")} " +
|
|
$"poll={acquire.PollInterval} gcr=0x{acquire.GcrControl:X8} " +
|
|
$"resource_inv={acquire.InvalidatesGuestResources} " +
|
|
$"sequence={sequence} scheduled={(sequence != 0)}");
|
|
}
|
|
}
|
|
|
|
private static bool TryDecodeSubmittedAcquireMem(
|
|
CpuContext ctx,
|
|
ulong packetAddress,
|
|
out SubmittedAcquireMem acquire)
|
|
{
|
|
if (!TryReadUInt32(ctx, packetAddress + 4, out var coherControl) ||
|
|
!TryReadUInt32(ctx, packetAddress + 8, out var sizeLow) ||
|
|
!TryReadUInt32(ctx, packetAddress + 12, out var sizeHigh) ||
|
|
!TryReadUInt32(ctx, packetAddress + 16, out var baseLow) ||
|
|
!TryReadUInt32(ctx, packetAddress + 20, out var baseHigh) ||
|
|
!TryReadUInt32(ctx, packetAddress + 24, out var pollInterval) ||
|
|
!TryReadUInt32(ctx, packetAddress + 28, out var gcrControl))
|
|
{
|
|
acquire = default;
|
|
return false;
|
|
}
|
|
|
|
acquire = DecodeSubmittedAcquireMem(
|
|
coherControl,
|
|
sizeLow,
|
|
sizeHigh,
|
|
baseLow,
|
|
baseHigh,
|
|
pollInterval,
|
|
gcrControl);
|
|
return true;
|
|
}
|
|
|
|
private static SubmittedAcquireMem DecodeSubmittedAcquireMem(
|
|
uint coherControl,
|
|
uint sizeLow,
|
|
uint sizeHigh,
|
|
uint baseLow,
|
|
uint baseHigh,
|
|
uint pollInterval,
|
|
uint gcrControl)
|
|
{
|
|
// GFX10 ACQUIRE_MEM expresses COHER_SIZE and COHER_BASE in 256-byte
|
|
// units. SIZE_HI is 8 bits and BASE_HI is 24 bits in the packet.
|
|
var sizeUnits = sizeLow | ((ulong)(sizeHigh & 0xFFu) << 32);
|
|
var baseUnits = baseLow | ((ulong)(baseHigh & 0x00FF_FFFFu) << 32);
|
|
return new SubmittedAcquireMem(
|
|
Engine: coherControl >> 31,
|
|
CbDbControl: coherControl & 0x7FFF_FFFFu,
|
|
BaseAddress: baseUnits << 8,
|
|
SizeBytes: sizeUnits << 8,
|
|
PollInterval: pollInterval & 0xFFFFu,
|
|
GcrControl: gcrControl & 0x7FFFFu);
|
|
}
|
|
|
|
private static void ResetSubmittedParserState(SubmittedDcbState state)
|
|
{
|
|
// Queue ownership, pending submissions and suspension bookkeeping are
|
|
// deliberately retained. Work emitted before this packet already owns
|
|
// immutable snapshots; clearing these fields affects only commands
|
|
// translated after RESET at this precise packet position.
|
|
state.CxRegisters.Clear();
|
|
state.ShRegisters.Clear();
|
|
state.UcRegisters.Clear();
|
|
state.PresenterTexture = null;
|
|
state.GuestDrawKind = GuestDrawKind.None;
|
|
state.TranslatedDraw = null;
|
|
state.RenderTargetWriters.Clear();
|
|
state.IndirectArgsAddress = 0;
|
|
state.SawIndexedDraw = false;
|
|
state.IndexBufferAddress = 0;
|
|
state.IndexBufferCount = 0;
|
|
state.IndexSize = 0;
|
|
state.InstanceCount = 1;
|
|
state.DrawIndexOffset = 0;
|
|
}
|
|
|
|
private static bool RangesOverlap(
|
|
ulong leftAddress,
|
|
ulong leftLength,
|
|
ulong rightAddress,
|
|
ulong rightLength)
|
|
{
|
|
var leftEnd = leftAddress > ulong.MaxValue - leftLength
|
|
? ulong.MaxValue
|
|
: leftAddress + leftLength;
|
|
var rightEnd = rightAddress > ulong.MaxValue - rightLength
|
|
? ulong.MaxValue
|
|
: rightAddress + rightLength;
|
|
return leftAddress < rightEnd && rightAddress < leftEnd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// PS5 render targets alias guest memory, so a CP DMA fill or copy that
|
|
/// lands on an RT is visible to later GPU reads. Our render targets live
|
|
/// in Vulkan images, so mirror DMA writes into them: fills become
|
|
/// vkCmdClearColorImage, copies re-upload the guest bytes. Without this,
|
|
/// per-frame DMA clears never reach the image (the fog layer in Dreaming
|
|
/// Sarah accumulates until it saturates, washing the scene out).
|
|
/// </summary>
|
|
/// <summary>
|
|
/// PS5 render targets alias unified memory, so the game's CPU can rewrite a
|
|
/// surface (Chowdren memsets its fog-noise layer every frame) and the GPU
|
|
/// observes it. Our Vulkan guest images are separate storage, so re-upload
|
|
/// CPU-authored surfaces once per flip. Surfaces only the GPU writes keep
|
|
/// all-zero guest memory and are skipped, preserving their GPU content.
|
|
/// </summary>
|
|
private static long _guestImageSyncTraceCount;
|
|
|
|
private static void SyncCpuWrittenGuestImages(
|
|
CpuContext ctx,
|
|
ulong scopeAddress = 0,
|
|
ulong scopeByteCount = ulong.MaxValue)
|
|
{
|
|
if (!SharpEmu.HLE.GuestImageWriteTracker.Enabled || scopeByteCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var (address, width, height, byteCount) in VulkanVideoPresenter.GetGuestImageExtents())
|
|
{
|
|
if (scopeByteCount != ulong.MaxValue &&
|
|
!RangesOverlap(address, byteCount, scopeAddress, scopeByteCount))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!SharpEmu.HLE.GuestImageWriteTracker.ConsumeDirty(address))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (byteCount == 0 || byteCount > MaxPresentedTextureBytes)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var pixels = new byte[byteCount];
|
|
if (ctx.Memory.TryRead(address, pixels))
|
|
{
|
|
VulkanVideoPresenter.SubmitGuestImageWrite(address, pixels);
|
|
if (Interlocked.Increment(ref _guestImageSyncTraceCount) <= 64)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[SYNC] cpu-write addr=0x{address:X} {width}x{height}");
|
|
}
|
|
}
|
|
|
|
SharpEmu.HLE.GuestImageWriteTracker.Rearm(address);
|
|
}
|
|
}
|
|
|
|
private static long _dmaMirrorTraceCount;
|
|
private static readonly Dictionary<(uint Op, uint Register), long> _submittedOpcodeCounts = new();
|
|
private static long _submittedOpcodeTotal;
|
|
|
|
private static void CountSubmittedOpcode(uint op, uint register)
|
|
{
|
|
var key = (op, op == ItNop ? register : uint.MaxValue);
|
|
lock (_submittedOpcodeCounts)
|
|
{
|
|
_submittedOpcodeCounts[key] =
|
|
_submittedOpcodeCounts.TryGetValue(key, out var count) ? count + 1 : 1;
|
|
if (++_submittedOpcodeTotal % 500_000 == 0)
|
|
{
|
|
var summary = string.Join(
|
|
' ',
|
|
_submittedOpcodeCounts
|
|
.OrderByDescending(entry => entry.Value)
|
|
.Select(entry => entry.Key.Register == uint.MaxValue
|
|
? $"0x{entry.Key.Op:X2}:{entry.Value}"
|
|
: $"0x{entry.Key.Op:X2}/r{entry.Key.Register}:{entry.Value}"));
|
|
Console.Error.WriteLine($"[PKT] total={_submittedOpcodeTotal} {summary}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void MirrorDmaWriteToGuestImage(
|
|
CpuContext ctx,
|
|
ulong destinationAddress,
|
|
ulong byteCount,
|
|
uint? fillValue)
|
|
{
|
|
var hasImage = VulkanVideoPresenter.TryGetGuestImageExtent(
|
|
destinationAddress,
|
|
out var width,
|
|
out var height,
|
|
out var imageBytes);
|
|
if (_traceDraws && Interlocked.Increment(ref _dmaMirrorTraceCount) <= 400)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[DMA] dst=0x{destinationAddress:X} bytes={byteCount} " +
|
|
$"fill={(fillValue is { } f ? $"0x{f:X8}" : "copy")} image={hasImage}");
|
|
}
|
|
|
|
if (!hasImage)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (imageBytes == 0 || byteCount < imageBytes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (fillValue is { } fill)
|
|
{
|
|
VulkanVideoPresenter.SubmitGuestImageFill(destinationAddress, fill);
|
|
return;
|
|
}
|
|
|
|
var pixels = new byte[imageBytes];
|
|
if (ctx.Memory.TryRead(destinationAddress, pixels))
|
|
{
|
|
VulkanVideoPresenter.SubmitGuestImageWrite(destinationAddress, pixels);
|
|
}
|
|
}
|
|
|
|
private static void ApplySubmittedStandardDmaData(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
ulong packetAddress)
|
|
{
|
|
if (!TryReadUInt32(ctx, packetAddress + 4, out var control) ||
|
|
!TryReadUInt32(ctx, packetAddress + 8, out var sourceLow) ||
|
|
!TryReadUInt32(ctx, packetAddress + 12, out var sourceHigh) ||
|
|
!TryReadUInt32(ctx, packetAddress + 16, out var destinationLow) ||
|
|
!TryReadUInt32(ctx, packetAddress + 20, out var destinationHigh) ||
|
|
!TryReadUInt32(ctx, packetAddress + 24, out var command))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var byteCount = command & 0x1F_FFFFu;
|
|
var destinationSelect = (control >> 20) & 0x3u;
|
|
var destinationSwap = (command >> 24) & 0x3u;
|
|
var destinationAddressSpace = (command >> 27) & 0x1u;
|
|
var destinationAddress = destinationLow | ((ulong)destinationHigh << 32);
|
|
var writesGuestMemory =
|
|
byteCount != 0 &&
|
|
destinationSwap == 0 &&
|
|
destinationSelect is 0 or 3 &&
|
|
(destinationSelect == 3 || destinationAddressSpace == 0);
|
|
|
|
SubmitOrderedGpuSideEffect(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
() => ApplySubmittedStandardDmaDataSnapshot(
|
|
ctx,
|
|
control,
|
|
sourceLow,
|
|
sourceHigh,
|
|
destinationLow,
|
|
destinationHigh,
|
|
command),
|
|
$"dma_data dst=0x{destinationHigh:X8}{destinationLow:X8} bytes={byteCount}",
|
|
packetAddress,
|
|
writesGuestMemory ? destinationAddress : 0,
|
|
writesGuestMemory ? byteCount : 0);
|
|
}
|
|
|
|
private static void ApplySubmittedStandardDmaDataSnapshot(
|
|
CpuContext ctx,
|
|
uint control,
|
|
uint sourceLow,
|
|
uint sourceHigh,
|
|
uint destinationLow,
|
|
uint destinationHigh,
|
|
uint command)
|
|
{
|
|
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);
|
|
InvalidateDcbWindowIfOverlaps(destinationAddress, byteCount);
|
|
bool copied;
|
|
ulong sourceAddress;
|
|
if (sourceSelect is 0 or 3 &&
|
|
(sourceSelect == 3 || sourceAddressSpace == 0))
|
|
{
|
|
sourceAddress = sourceLow | ((ulong)sourceHigh << 32);
|
|
if (sourceAddressIncrement != 0)
|
|
{
|
|
copied =
|
|
TryReadUInt32(ctx, sourceAddress, out var fillValue) &&
|
|
TryFillGuestMemory(
|
|
ctx,
|
|
fillValue,
|
|
destinationAddress,
|
|
byteCount);
|
|
if (copied)
|
|
{
|
|
MirrorDmaWriteToGuestImage(ctx, destinationAddress, byteCount, fillValue);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
copied = TryCopyGuestMemory(
|
|
ctx,
|
|
sourceAddress,
|
|
destinationAddress,
|
|
byteCount);
|
|
if (copied)
|
|
{
|
|
MirrorDmaWriteToGuestImage(ctx, destinationAddress, byteCount, fillValue: null);
|
|
}
|
|
}
|
|
}
|
|
else if (sourceSelect == 2)
|
|
{
|
|
sourceAddress = 0;
|
|
copied = TryFillGuestMemory(
|
|
ctx,
|
|
sourceLow,
|
|
destinationAddress,
|
|
byteCount);
|
|
if (copied)
|
|
{
|
|
MirrorDmaWriteToGuestImage(ctx, destinationAddress, byteCount, sourceLow);
|
|
}
|
|
}
|
|
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,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
ulong packetAddress,
|
|
uint packetLength,
|
|
bool standardPacket,
|
|
bool tracePacket)
|
|
{
|
|
if (!TryReadUInt32(ctx, packetAddress + 4, out var control) ||
|
|
!TryReadUInt64(ctx, packetAddress + 8, out var destinationAddress))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var (destination, incrementAddress, writeConfirm, cachePolicy) = standardPacket
|
|
? DecodeStandardWriteDataControl(control)
|
|
: DecodeAgcWriteDataControl(control);
|
|
var dwordCount = packetLength - 4;
|
|
var values = new uint[dwordCount];
|
|
for (uint index = 0; index < dwordCount; index++)
|
|
{
|
|
var sourceAddress = packetAddress + 16 + ((ulong)index * sizeof(uint));
|
|
if (!TryReadUInt32(ctx, sourceAddress, out values[index]))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
SubmitOrderedGpuSideEffect(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
() =>
|
|
{
|
|
InvalidateDcbWindowIfOverlaps(
|
|
destinationAddress,
|
|
incrementAddress ? (ulong)dwordCount * sizeof(uint) : sizeof(uint));
|
|
var wroteData = destination is 1 or 2 or 4 or 5;
|
|
for (uint index = 0; wroteData && index < dwordCount; index++)
|
|
{
|
|
var targetAddress = destinationAddress +
|
|
(incrementAddress ? (ulong)index * sizeof(uint) : 0);
|
|
wroteData = TryWriteUInt32(ctx, targetAddress, values[index]);
|
|
}
|
|
|
|
if (tracePacket)
|
|
{
|
|
TraceAgc(
|
|
$"agc.dcb.write_data dst={destination} " +
|
|
$"addr=0x{destinationAddress:X16} count={dwordCount} " +
|
|
$"increment={incrementAddress} confirm={writeConfirm} " +
|
|
$"cache={cachePolicy} standard={standardPacket} wrote={wroteData}");
|
|
}
|
|
},
|
|
$"write_data dst=0x{destinationAddress:X16} count={dwordCount}",
|
|
packetAddress,
|
|
destination is 1 or 2 or 4 or 5 ? destinationAddress : 0,
|
|
destination is 1 or 2 or 4 or 5
|
|
? incrementAddress ? (ulong)dwordCount * sizeof(uint) : sizeof(uint)
|
|
: 0);
|
|
}
|
|
|
|
private static (uint Destination, bool IncrementAddress, bool WriteConfirm, uint CachePolicy)
|
|
DecodeStandardWriteDataControl(uint control)
|
|
{
|
|
// GFX10 PKT3_WRITE_DATA is not byte-packed like sceAgcDcbWriteData's
|
|
// NOP wrapper: DST_SEL is 11:8, ADDR_INCR is bit 16 (0 increments),
|
|
// WR_CONFIRM is bit 20, and CACHE_POLICY is 26:25. In particular, the
|
|
// low byte is reserved and must never be interpreted as DST_SEL.
|
|
return (
|
|
Destination: (control >> 8) & 0xFu,
|
|
IncrementAddress: (control & (1u << 16)) == 0,
|
|
WriteConfirm: (control & (1u << 20)) != 0,
|
|
CachePolicy: (control >> 25) & 0x3u);
|
|
}
|
|
|
|
private static (uint Destination, bool IncrementAddress, bool WriteConfirm, uint CachePolicy)
|
|
DecodeAgcWriteDataControl(uint control) =>
|
|
(
|
|
Destination: control & 0xFFu,
|
|
IncrementAddress: ((control >> 16) & 0xFFu) == 0,
|
|
WriteConfirm: ((control >> 24) & 0xFFu) != 0,
|
|
CachePolicy: (control >> 8) & 0xFFu);
|
|
|
|
#if DEBUG
|
|
private static void ValidateWriteDataControlDecoders()
|
|
{
|
|
// Regression vector: reserved low-byte noise previously decoded 0xA5
|
|
// as DST_SEL, causing a valid standard memory write to be discarded.
|
|
const uint standardControl = 0xA5u | (5u << 8) | (1u << 16) | (1u << 20) | (2u << 25);
|
|
var standard = DecodeStandardWriteDataControl(standardControl);
|
|
System.Diagnostics.Debug.Assert(standard.Destination == 5u);
|
|
System.Diagnostics.Debug.Assert(!standard.IncrementAddress);
|
|
System.Diagnostics.Debug.Assert(standard.WriteConfirm);
|
|
System.Diagnostics.Debug.Assert(standard.CachePolicy == 2u);
|
|
|
|
const uint agcControl = 4u | (3u << 8) | (1u << 24);
|
|
var agc = DecodeAgcWriteDataControl(agcControl);
|
|
System.Diagnostics.Debug.Assert(agc.Destination == 4u);
|
|
System.Diagnostics.Debug.Assert(agc.IncrementAddress);
|
|
System.Diagnostics.Debug.Assert(agc.WriteConfirm);
|
|
System.Diagnostics.Debug.Assert(agc.CachePolicy == 3u);
|
|
}
|
|
|
|
private static void ValidateDispatchInitiators()
|
|
{
|
|
const uint threadCount = 0x00F0_0100u;
|
|
const uint localSize = 64u;
|
|
var initiator = DirectDispatchInitiator(0);
|
|
System.Diagnostics.Debug.Assert((initiator & (1u << 5)) == 0);
|
|
System.Diagnostics.Debug.Assert((initiator & (1u << 6)) != 0);
|
|
System.Diagnostics.Debug.Assert(threadCount * localSize == 0x3C00_4000u);
|
|
System.Diagnostics.Debug.Assert(CeilDivide(20, 8) == 3);
|
|
System.Diagnostics.Debug.Assert(CeilDivide(12, 8) == 2);
|
|
}
|
|
|
|
private static void ValidateSubmittedQueueAndReleaseMemDecoders()
|
|
{
|
|
var nggRegisters = new Dictionary<uint, uint>
|
|
{
|
|
[GsUserDataRegister - 1] = 3u << 1,
|
|
};
|
|
System.Diagnostics.Debug.Assert(
|
|
SelectExportUserDataRegister(nggRegisters) == GsUserDataRegister);
|
|
|
|
var queue = new SubmittedDcbState();
|
|
queue.PendingSubmissions.Enqueue(new(0x1000, 8, 11, false));
|
|
queue.PendingSubmissions.Enqueue(new(0x2000, 16, 12, true));
|
|
System.Diagnostics.Debug.Assert(
|
|
queue.PendingSubmissions.Dequeue().SubmissionId == 11);
|
|
System.Diagnostics.Debug.Assert(
|
|
queue.PendingSubmissions.Dequeue().SubmissionId == 12);
|
|
|
|
var control = (1u << 16) | (2u << 29);
|
|
var decoded = DecodeStandardReleaseMemControl(control);
|
|
System.Diagnostics.Debug.Assert(decoded.Destination == 1u);
|
|
System.Diagnostics.Debug.Assert(decoded.DataSelection == 2u);
|
|
System.Diagnostics.Debug.Assert(
|
|
PatchUInt32Bits(0xABCD_1234u, 0x00FF_0000u, 3u << 16) ==
|
|
0xAB03_1234u);
|
|
}
|
|
|
|
private static void ValidateAcquireMemAndQueueResetDecoders()
|
|
{
|
|
var range = DecodeSubmittedAcquireMem(
|
|
0x8000_7FC0u,
|
|
0x0000_0123u,
|
|
0x45u,
|
|
0x89AB_CDEFu,
|
|
0x0012_3456u,
|
|
0x1_000Au,
|
|
0x0001_0388u);
|
|
System.Diagnostics.Debug.Assert(range.Engine == 1u);
|
|
System.Diagnostics.Debug.Assert(range.CbDbControl == 0x7FC0u);
|
|
System.Diagnostics.Debug.Assert(range.SizeBytes == 0x0000_4500_0001_2300UL);
|
|
System.Diagnostics.Debug.Assert(range.BaseAddress == 0x1234_5689_ABCD_EF00UL);
|
|
System.Diagnostics.Debug.Assert(range.PollInterval == 0xAu);
|
|
System.Diagnostics.Debug.Assert(range.InvalidatesGuestResources);
|
|
System.Diagnostics.Debug.Assert(!range.CoversAllGuestMemory);
|
|
|
|
var all = DecodeSubmittedAcquireMem(0, 0, 0, 0, 0, 0, 0x280u);
|
|
System.Diagnostics.Debug.Assert(all.CoversAllGuestMemory);
|
|
System.Diagnostics.Debug.Assert(all.InvalidatesGuestResources);
|
|
var explicitAll = DecodeSubmittedAcquireMem(0, 1, 0, 0, 0, 0, 0x103C0u);
|
|
System.Diagnostics.Debug.Assert(explicitAll.CoversAllGuestMemory);
|
|
|
|
var queue = new SubmittedDcbState
|
|
{
|
|
QueueName = "validator",
|
|
ActiveSubmissionId = 7,
|
|
HasActiveSubmission = true,
|
|
IsSuspended = true,
|
|
IndexBufferAddress = 0x1000,
|
|
IndexBufferCount = 12,
|
|
IndexSize = 1,
|
|
InstanceCount = 4,
|
|
DrawIndexOffset = 2,
|
|
IndirectArgsAddress = 0x2000,
|
|
SawIndexedDraw = true,
|
|
GuestDrawKind = GuestDrawKind.FullscreenBarycentric,
|
|
};
|
|
queue.CxRegisters.Add(1, 2);
|
|
queue.ShRegisters.Add(3, 4);
|
|
queue.UcRegisters.Add(5, 6);
|
|
queue.PendingSubmissions.Enqueue(new(0x3000, 2, 8, false));
|
|
ResetSubmittedParserState(queue);
|
|
System.Diagnostics.Debug.Assert(queue.CxRegisters.Count == 0);
|
|
System.Diagnostics.Debug.Assert(queue.ShRegisters.Count == 0);
|
|
System.Diagnostics.Debug.Assert(queue.UcRegisters.Count == 0);
|
|
System.Diagnostics.Debug.Assert(queue.IndexBufferAddress == 0);
|
|
System.Diagnostics.Debug.Assert(queue.IndexBufferCount == 0);
|
|
System.Diagnostics.Debug.Assert(queue.IndexSize == 0);
|
|
System.Diagnostics.Debug.Assert(queue.InstanceCount == 1);
|
|
System.Diagnostics.Debug.Assert(queue.DrawIndexOffset == 0);
|
|
System.Diagnostics.Debug.Assert(queue.IndirectArgsAddress == 0);
|
|
System.Diagnostics.Debug.Assert(!queue.SawIndexedDraw);
|
|
System.Diagnostics.Debug.Assert(queue.GuestDrawKind == GuestDrawKind.None);
|
|
System.Diagnostics.Debug.Assert(queue.QueueName == "validator");
|
|
System.Diagnostics.Debug.Assert(queue.ActiveSubmissionId == 7);
|
|
System.Diagnostics.Debug.Assert(queue.HasActiveSubmission);
|
|
System.Diagnostics.Debug.Assert(queue.IsSuspended);
|
|
System.Diagnostics.Debug.Assert(queue.PendingSubmissions.Count == 1);
|
|
}
|
|
|
|
private static void ValidateDepthTargetDecoder()
|
|
{
|
|
var registers = new Dictionary<uint, uint>
|
|
{
|
|
[DbDepthControl] = 0x2u | 0x4u | (1u << 4),
|
|
[DbDepthSizeXy] = 1919u | (1079u << 16),
|
|
[DbDepthClear] = BitConverter.SingleToUInt32Bits(1f),
|
|
[DbZInfo] = 3u | (24u << 4),
|
|
[DbZReadBase] = 0x0123_4567u,
|
|
[DbZWriteBase] = 0x0123_4567u,
|
|
[DbZReadBaseHi] = 2u,
|
|
[DbZWriteBaseHi] = 2u,
|
|
};
|
|
var depth = DecodeDepthTarget(registers);
|
|
System.Diagnostics.Debug.Assert(depth is not null);
|
|
System.Diagnostics.Debug.Assert(depth.Width == 1920 && depth.Height == 1080);
|
|
System.Diagnostics.Debug.Assert(depth.GuestFormat == 3u);
|
|
System.Diagnostics.Debug.Assert(depth.SwizzleMode == 24u);
|
|
System.Diagnostics.Debug.Assert(depth.Address == 0x0000_0201_2345_6700UL);
|
|
System.Diagnostics.Debug.Assert(depth.ClearDepth == 1f);
|
|
}
|
|
#endif
|
|
|
|
// SHARPEMU_GPU_WAIT_MODE=force reverts to the legacy behaviour of faking a
|
|
// satisfying value at parse time. Default (suspend) properly suspends the
|
|
// DCB on an unmet WAIT_REG_MEM and resumes it once the awaited completion
|
|
// label is genuinely written by a later submit — preserving cross-submit
|
|
// ordering so the work after a wait (e.g. the final composite) does not run
|
|
// ahead of the compute it samples.
|
|
private static readonly bool _gpuWaitSuspendEnabled = !string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_GPU_WAIT_MODE"),
|
|
"force",
|
|
StringComparison.OrdinalIgnoreCase);
|
|
|
|
// Optional age for one-shot missing-producer diagnostics. Stale waits are
|
|
// never removed or force-satisfied in the default suspend mode: doing so
|
|
// advances a queue without its real cross-queue producer and can publish
|
|
// incomplete CPU/GPU state. Only SHARPEMU_GPU_WAIT_MODE=force retains the
|
|
// explicit legacy mutation path above. Default 0 disables age diagnostics.
|
|
private static readonly long _gpuWaitStaleTicks =
|
|
(long.TryParse(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_GPU_WAIT_FALLBACK_MS"),
|
|
out var fallbackMs) && fallbackMs >= 0
|
|
? fallbackMs
|
|
: 0L) * System.Diagnostics.Stopwatch.Frequency / 1000L;
|
|
|
|
// Reads the WAIT_REG_MEM watched address, reference, mask, and 3-bit compare
|
|
// function for both the AGC NOP-encapsulated (RWaitMem32/64) and the standard
|
|
// ItWaitRegMem packet layouts.
|
|
private static bool TryParseSubmittedWait(
|
|
CpuContext ctx,
|
|
ulong packetAddress,
|
|
bool is64Bit,
|
|
bool isStandard,
|
|
out ulong waitAddress,
|
|
out ulong reference,
|
|
out ulong mask,
|
|
out uint compareFunction,
|
|
out uint controlValue)
|
|
{
|
|
waitAddress = 0;
|
|
reference = 0;
|
|
mask = 0;
|
|
compareFunction = 0;
|
|
controlValue = 0;
|
|
if (isStandard)
|
|
{
|
|
if (!TryReadUInt32(ctx, packetAddress + 4, out var stdControl) ||
|
|
!TryReadUInt64(ctx, packetAddress + 8, out waitAddress) ||
|
|
!TryReadUInt32(ctx, packetAddress + 16, out var stdRef) ||
|
|
!TryReadUInt32(ctx, packetAddress + 20, out var stdMask))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
compareFunction = stdControl & 0x7u;
|
|
controlValue = stdControl;
|
|
reference = stdRef;
|
|
mask = stdMask;
|
|
return true;
|
|
}
|
|
|
|
if (!TryReadUInt64(ctx, packetAddress + 4, out waitAddress) ||
|
|
!TryReadUInt32(ctx, packetAddress + (is64Bit ? 28u : 16u), out var control))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
compareFunction = control & 0x7u;
|
|
controlValue = control;
|
|
if (is64Bit)
|
|
{
|
|
return TryReadUInt64(ctx, packetAddress + 12, out mask) &&
|
|
TryReadUInt64(ctx, packetAddress + 20, out reference);
|
|
}
|
|
|
|
if (!TryReadUInt32(ctx, packetAddress + 12, out var mask32) ||
|
|
!TryReadUInt32(ctx, packetAddress + 20, out var reference32))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
mask = mask32;
|
|
reference = reference32;
|
|
return true;
|
|
}
|
|
|
|
// Returns true when the DCB should suspend parsing at this wait (its
|
|
// continuation was registered into GpuWaitRegistry); false to keep parsing
|
|
// (already satisfied, unreadable, or legacy force-satisfy mode).
|
|
private static bool HandleSubmittedWaitRegMem(
|
|
CpuContext ctx,
|
|
SubmittedDcbState state,
|
|
ulong commandAddress,
|
|
ulong packetAddress,
|
|
uint offset,
|
|
uint length,
|
|
uint dwordCount,
|
|
bool is64Bit,
|
|
bool isStandard,
|
|
bool tracePacket)
|
|
{
|
|
if (!TryParseSubmittedWait(
|
|
ctx, packetAddress, is64Bit, isStandard,
|
|
out var waitAddress, out var reference, out var mask, out var compareFunction,
|
|
out var controlValue))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// COMPARE_FUNC=0 is the hardware "always" condition. Reserved 7 is
|
|
// also fail-open; neither condition may register a waiter. Validate
|
|
// the watched memory before any read so null/malformed packets cannot
|
|
// become permanent entries keyed by address zero.
|
|
if (compareFunction is 0 or 7)
|
|
{
|
|
TraceSubmittedWait(
|
|
waitAddress,
|
|
0,
|
|
mask,
|
|
reference,
|
|
compareFunction,
|
|
is64Bit ? 64 : 32,
|
|
tracePacket);
|
|
return false;
|
|
}
|
|
|
|
var requiredAlignment = is64Bit ? sizeof(ulong) : sizeof(uint);
|
|
if (waitAddress == 0 ||
|
|
mask == 0 ||
|
|
waitAddress % (ulong)requiredAlignment != 0)
|
|
{
|
|
TraceAgc(
|
|
$"agc.dcb.wait_reject addr=0x{waitAddress:X16} " +
|
|
$"mask=0x{mask:X16} compare={compareFunction} bits=" +
|
|
$"{(is64Bit ? 64 : 32)} standard={isStandard} " +
|
|
$"packet=0x{packetAddress:X16} reason=invalid-address-or-mask");
|
|
return false;
|
|
}
|
|
|
|
ulong currentValue = 0;
|
|
bool hasCurrent;
|
|
if (is64Bit)
|
|
{
|
|
hasCurrent = TryReadUInt64(ctx, waitAddress, out currentValue);
|
|
}
|
|
else if (TryReadUInt32(ctx, waitAddress, out var current32))
|
|
{
|
|
currentValue = current32;
|
|
hasCurrent = true;
|
|
}
|
|
else
|
|
{
|
|
hasCurrent = false;
|
|
}
|
|
|
|
TraceSubmittedWait(
|
|
waitAddress, currentValue, mask, reference, compareFunction,
|
|
is64Bit ? 64 : 32, tracePacket);
|
|
|
|
var waiter = new GpuWaitRegistry.WaitingDcb
|
|
{
|
|
CommandBufferAddress = commandAddress,
|
|
ResumeAddress = packetAddress + ((ulong)length * sizeof(uint)),
|
|
TotalDwords = dwordCount,
|
|
ResumeOffset = offset + length,
|
|
ReferenceValue = reference,
|
|
Mask = mask,
|
|
CompareFunction = compareFunction,
|
|
ControlValue = controlValue,
|
|
Is64Bit = is64Bit,
|
|
IsStandard = isStandard,
|
|
WaitAddress = waitAddress,
|
|
Memory = ctx.Memory,
|
|
QueueName = state.QueueName,
|
|
SubmissionId = state.ActiveSubmissionId,
|
|
RegisteredTicks = System.Diagnostics.Stopwatch.GetTimestamp(),
|
|
State = state,
|
|
};
|
|
|
|
if (hasCurrent && GpuWaitRegistry.Compare(waiter, currentValue))
|
|
{
|
|
return false; // already satisfied — keep parsing
|
|
}
|
|
|
|
if (!_gpuWaitSuspendEnabled)
|
|
{
|
|
if (hasCurrent)
|
|
{
|
|
ForceSatisfyGpuWait(ctx, waiter, currentValue);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!hasCurrent)
|
|
{
|
|
return false; // cannot evaluate the label — do not stall the DCB
|
|
}
|
|
|
|
GpuWaitRegistry.Register(waitAddress, waiter);
|
|
var gpuState = _submittedGpuStates.GetValue(
|
|
ctx.Memory,
|
|
static _ => new SubmittedGpuState());
|
|
EnsureGpuWaitMonitor(ctx, gpuState);
|
|
TraceWaitProducerState(
|
|
ctx.Memory,
|
|
waiter,
|
|
commandAddress,
|
|
packetAddress,
|
|
stale: false,
|
|
currentValue);
|
|
if (tracePacket)
|
|
{
|
|
TraceAgc(
|
|
$"agc.dcb.suspended addr=0x{waitAddress:X16} ref=0x{reference:X16} " +
|
|
$"mask=0x{mask:X16} cur=0x{currentValue:X16} cmp={compareFunction}");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Direct guest CPU stores can satisfy a GPU wait without crossing another
|
|
/// AGC import. Keep one low-frequency monitor per guest memory while waits
|
|
/// exist so those real stores wake their queues. The monitor never changes
|
|
/// a label: it uses the same masked comparison as submission-time parsing
|
|
/// and resumes only after the guest value genuinely satisfies the packet.
|
|
/// </summary>
|
|
private static void EnsureGpuWaitMonitor(
|
|
CpuContext submitContext,
|
|
SubmittedGpuState gpuState)
|
|
{
|
|
if (gpuState.WaitMonitorRunning)
|
|
{
|
|
return;
|
|
}
|
|
|
|
gpuState.WaitMonitorRunning = true;
|
|
var monitorContext = new CpuContext(
|
|
submitContext.Memory,
|
|
submitContext.TargetGeneration);
|
|
ThreadPool.UnsafeQueueUserWorkItem(
|
|
static state => MonitorGpuWaits(state.Context, state.GpuState),
|
|
(Context: monitorContext, GpuState: gpuState),
|
|
preferLocal: false);
|
|
}
|
|
|
|
private static void MonitorGpuWaits(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState)
|
|
{
|
|
var delayMilliseconds = 1;
|
|
while (true)
|
|
{
|
|
var madeProgress = false;
|
|
lock (gpuState.Gate)
|
|
{
|
|
var before = GpuWaitRegistry.CountForMemory(ctx.Memory);
|
|
if (before == 0)
|
|
{
|
|
gpuState.WaitMonitorRunning = false;
|
|
return;
|
|
}
|
|
|
|
DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc);
|
|
var after = GpuWaitRegistry.CountForMemory(ctx.Memory);
|
|
madeProgress = after < before;
|
|
if (madeProgress)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] agc.wait_monitor_resumed count={before - after} " +
|
|
$"remaining={after}");
|
|
}
|
|
if (after == 0)
|
|
{
|
|
gpuState.WaitMonitorRunning = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
delayMilliseconds = madeProgress
|
|
? 1
|
|
: Math.Min(delayMilliseconds * 2, 16);
|
|
Thread.Sleep(delayMilliseconds);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a value that satisfies the waiter's comparison. This deliberately
|
|
/// exists only behind SHARPEMU_GPU_WAIT_MODE=force for legacy A/B testing;
|
|
/// normal and stale waits must never mutate their watched label.
|
|
/// </summary>
|
|
private static void ForceSatisfyGpuWait(
|
|
CpuContext ctx,
|
|
in GpuWaitRegistry.WaitingDcb waiter,
|
|
ulong value)
|
|
{
|
|
var address = waiter.WaitAddress;
|
|
var mask = waiter.Mask;
|
|
if (address == 0 || mask == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var maskedRef = waiter.ReferenceValue & mask;
|
|
ulong? satisfyMasked = waiter.CompareFunction switch
|
|
{
|
|
1 => maskedRef == 0 ? null : (maskedRef - 1) & mask, // <
|
|
2 => maskedRef, // <=
|
|
3 => maskedRef, // ==
|
|
4 => (~maskedRef) & mask, // !=
|
|
5 => maskedRef, // >=
|
|
6 => maskedRef == mask ? null : (maskedRef + 1) & mask, // >
|
|
_ => null,
|
|
};
|
|
|
|
if (satisfyMasked is not { } satisfy)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var newValue = (value & ~mask) | (satisfy & mask);
|
|
if (waiter.Is64Bit)
|
|
{
|
|
ctx.TryWriteUInt64(address, newValue);
|
|
}
|
|
else
|
|
{
|
|
TryWriteUInt32(ctx, address, unchecked((uint)newValue));
|
|
}
|
|
}
|
|
|
|
// 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 direct CPU writes) and resumes the ones now satisfied. A resumed DCB
|
|
// can itself write labels that unblock others, so loop to a fixed point.
|
|
private static void DrainResumableDcbs(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
bool tracePackets)
|
|
{
|
|
if (!_gpuWaitSuspendEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var pass = 0; pass < 256; pass++)
|
|
{
|
|
var woken = GpuWaitRegistry.CollectSatisfied(ctx.Memory, (address, is64Bit) =>
|
|
is64Bit
|
|
? TryReadUInt64(ctx, address, out var value64) ? value64 : (ulong?)null
|
|
: TryReadUInt32(ctx, address, out var value32) ? value32 : (ulong?)null);
|
|
|
|
if (woken is null)
|
|
{
|
|
if (_gpuWaitStaleTicks > 0 &&
|
|
GpuWaitRegistry.CollectUnreportedStale(
|
|
ctx.Memory,
|
|
System.Diagnostics.Stopwatch.GetTimestamp(),
|
|
_gpuWaitStaleTicks) is { } stale)
|
|
{
|
|
foreach (var waiter in stale)
|
|
{
|
|
ulong? currentValue = waiter.Is64Bit
|
|
? TryReadUInt64(ctx, waiter.WaitAddress, out var value64)
|
|
? value64
|
|
: null
|
|
: TryReadUInt32(ctx, waiter.WaitAddress, out var value32)
|
|
? value32
|
|
: null;
|
|
TraceWaitProducerState(
|
|
ctx.Memory,
|
|
waiter,
|
|
waiter.CommandBufferAddress,
|
|
waiter.ResumeAddress,
|
|
stale: true,
|
|
currentValue);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
foreach (var waiter in woken)
|
|
{
|
|
ResumeSuspendedDcb(ctx, gpuState, waiter, tracePackets);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ResumeSuspendedDcb(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
in GpuWaitRegistry.WaitingDcb waiter,
|
|
bool tracePackets)
|
|
{
|
|
var state = waiter.State as SubmittedDcbState ?? gpuState.Graphics;
|
|
var remainingDwords = waiter.TotalDwords - waiter.ResumeOffset;
|
|
var waitedMilliseconds = waiter.RegisteredTicks == 0
|
|
? 0.0
|
|
: (System.Diagnostics.Stopwatch.GetTimestamp() - waiter.RegisteredTicks) *
|
|
1000.0 / System.Diagnostics.Stopwatch.Frequency;
|
|
TraceAgcShader(
|
|
$"agc.queue_resumed queue={waiter.QueueName} " +
|
|
$"submission={waiter.SubmissionId} label=0x{waiter.WaitAddress:X16} " +
|
|
$"resume=0x{waiter.ResumeAddress:X16} remaining_dwords={remainingDwords} " +
|
|
$"waited_ms={waitedMilliseconds:F3}");
|
|
if (remainingDwords == 0)
|
|
{
|
|
state.IsSuspended = false;
|
|
state.HasActiveSubmission = false;
|
|
NotifySubmittedDcbCompleted(gpuState, state, waiter.SubmissionId);
|
|
PumpSubmittedQueue(ctx, gpuState, state);
|
|
return;
|
|
}
|
|
|
|
if (tracePackets)
|
|
{
|
|
TraceAgc(
|
|
$"agc.dcb.resumed addr=0x{waiter.WaitAddress:X16} " +
|
|
$"resume=0x{waiter.ResumeAddress:X16} dwords={remainingDwords} forced=False");
|
|
}
|
|
|
|
System.Diagnostics.Debug.Assert(state.HasActiveSubmission);
|
|
System.Diagnostics.Debug.Assert(state.IsSuspended);
|
|
state.QueueName = waiter.QueueName ?? state.QueueName;
|
|
state.ActiveSubmissionId = waiter.SubmissionId;
|
|
state.IsSuspended = false;
|
|
if (ParseSubmittedDcb(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
waiter.ResumeAddress,
|
|
remainingDwords,
|
|
tracePackets))
|
|
{
|
|
state.IsSuspended = true;
|
|
return;
|
|
}
|
|
|
|
state.HasActiveSubmission = false;
|
|
NotifySubmittedDcbCompleted(gpuState, state, waiter.SubmissionId);
|
|
PumpSubmittedQueue(ctx, gpuState, state);
|
|
}
|
|
|
|
private static void TraceSubmittedWait(
|
|
ulong address,
|
|
ulong value,
|
|
ulong mask,
|
|
ulong reference,
|
|
uint compareFunction,
|
|
int bits,
|
|
bool tracePacket)
|
|
{
|
|
var maskedValue = value & mask;
|
|
var satisfied = compareFunction switch
|
|
{
|
|
0 => true,
|
|
1 => maskedValue < reference,
|
|
2 => maskedValue <= reference,
|
|
3 => maskedValue == reference,
|
|
4 => maskedValue != reference,
|
|
5 => maskedValue >= reference,
|
|
6 => maskedValue > reference,
|
|
_ => true,
|
|
};
|
|
if (!tracePacket && (satisfied || !ShouldTraceHotPath(ref _unsatisfiedWaitTraceCount)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
TraceAgc(
|
|
$"agc.dcb.wait_reg_mem bits={bits} addr=0x{address:X16} " +
|
|
$"value=0x{value:X16} mask=0x{mask:X16} ref=0x{reference:X16} " +
|
|
$"compare={compareFunction} satisfied={satisfied}");
|
|
}
|
|
|
|
private static void ApplySubmittedStandardReleaseMem(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
ulong packetAddress,
|
|
bool tracePacket)
|
|
{
|
|
if (!TryReadUInt32(ctx, packetAddress + 8, out var control) ||
|
|
!TryReadUInt32(ctx, packetAddress + 12, out var destinationLo) ||
|
|
!TryReadUInt32(ctx, packetAddress + 16, out var destinationHi) ||
|
|
!TryReadUInt32(ctx, packetAddress + 20, out var dataLo) ||
|
|
!TryReadUInt32(ctx, packetAddress + 24, out var dataHi))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var (destination, dataSelection) = DecodeStandardReleaseMemControl(control);
|
|
var destinationAddress = ((ulong)destinationHi << 32) | destinationLo;
|
|
var data = ((ulong)dataHi << 32) | dataLo;
|
|
var writeLength = dataSelection switch
|
|
{
|
|
1 => (ulong)sizeof(uint),
|
|
2 or 3 or 4 => (ulong)sizeof(ulong),
|
|
_ => 0UL,
|
|
};
|
|
var writesGuestMemory = destination is 0 or 1 &&
|
|
destinationAddress != 0 &&
|
|
writeLength != 0;
|
|
|
|
SubmitOrderedGpuSideEffect(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
() =>
|
|
{
|
|
if (writesGuestMemory)
|
|
{
|
|
InvalidateDcbWindowIfOverlaps(destinationAddress, writeLength);
|
|
}
|
|
|
|
var wroteData = writesGuestMemory && (dataSelection switch
|
|
{
|
|
1 => TryWriteUInt32(ctx, destinationAddress, dataLo),
|
|
2 => ctx.TryWriteUInt64(destinationAddress, data),
|
|
// Hardware counter writes are timing values sampled at the
|
|
// release point, not the immediate payload in ordinal 6/7.
|
|
3 or 4 => ctx.TryWriteUInt64(
|
|
destinationAddress,
|
|
unchecked((ulong)System.Diagnostics.Stopwatch.GetTimestamp())),
|
|
_ => false,
|
|
});
|
|
|
|
if (tracePacket)
|
|
{
|
|
TraceAgc(
|
|
$"agc.dcb.release_mem_standard dst_sel={destination} " +
|
|
$"dst=0x{destinationAddress:X16} data_sel={dataSelection} " +
|
|
$"data=0x{data:X16} wrote={wroteData}");
|
|
}
|
|
},
|
|
$"release_mem_standard dst=0x{destinationAddress:X16} data=0x{data:X16}",
|
|
packetAddress,
|
|
writesGuestMemory ? destinationAddress : 0,
|
|
writesGuestMemory ? writeLength : 0);
|
|
}
|
|
|
|
private static (uint Destination, uint DataSelection)
|
|
DecodeStandardReleaseMemControl(uint control) =>
|
|
(
|
|
Destination: (control >> 16) & 0x3u,
|
|
DataSelection: (control >> 29) & 0x7u);
|
|
|
|
private static void ApplySubmittedReleaseMem(
|
|
CpuContext ctx,
|
|
SubmittedGpuState gpuState,
|
|
SubmittedDcbState state,
|
|
ulong packetAddress,
|
|
bool tracePacket)
|
|
{
|
|
if (!TryReadUInt32(ctx, packetAddress + 8, out var control) ||
|
|
!TryReadUInt32(ctx, packetAddress + 12, out var destinationLo) ||
|
|
!TryReadUInt32(ctx, packetAddress + 16, out var destinationHi) ||
|
|
!TryReadUInt32(ctx, packetAddress + 20, out var dataLo) ||
|
|
!TryReadUInt32(ctx, packetAddress + 24, out var dataHi))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var dataSelection = (control >> 16) & 0xFFu;
|
|
var destinationAddress = ((ulong)destinationHi << 32) | destinationLo;
|
|
var data = ((ulong)dataHi << 32) | dataLo;
|
|
var writeLength = dataSelection switch
|
|
{
|
|
1 => (ulong)sizeof(uint),
|
|
2 or 3 => (ulong)sizeof(ulong),
|
|
_ => 0UL,
|
|
};
|
|
SubmitOrderedGpuSideEffect(
|
|
ctx,
|
|
gpuState,
|
|
state,
|
|
() =>
|
|
{
|
|
InvalidateDcbWindowIfOverlaps(destinationAddress, writeLength);
|
|
var wroteData = dataSelection switch
|
|
{
|
|
1 => TryWriteUInt32(ctx, destinationAddress, dataLo),
|
|
2 => ctx.TryWriteUInt64(destinationAddress, data),
|
|
// Data selection 3 samples the GPU clock at the release
|
|
// point. The packet payload is ignored by hardware; Unity
|
|
// uses the nonzero timestamp as submit-completion state.
|
|
3 => ctx.TryWriteUInt64(
|
|
destinationAddress,
|
|
unchecked((ulong)System.Diagnostics.Stopwatch.GetTimestamp())),
|
|
_ => false,
|
|
};
|
|
|
|
if (tracePacket)
|
|
{
|
|
TraceAgc(
|
|
$"agc.dcb.release_mem dst=0x{destinationAddress:X16} " +
|
|
$"data_sel={dataSelection} data=0x{data:X16} wrote={wroteData}");
|
|
}
|
|
},
|
|
$"release_mem dst=0x{destinationAddress:X16} data=0x{data:X16}",
|
|
packetAddress,
|
|
dataSelection is 1 or 2 or 3 ? destinationAddress : 0,
|
|
writeLength);
|
|
}
|
|
|
|
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 ||
|
|
!TryReadUInt32(ctx, 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 (!TryReadUInt32(
|
|
ctx,
|
|
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 ||
|
|
!TryReadUInt32(ctx, packetAddress + sizeof(uint), out var registerCount) ||
|
|
!TryReadUInt64(ctx, 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 (!TryReadUInt32(ctx, entryAddress, out var registerOffset) ||
|
|
!TryReadUInt32(ctx, entryAddress + sizeof(uint), out var value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The indirect table has an explicit count; offset zero is a real
|
|
// context-register index (DB_RENDER_CONTROL), not a terminator.
|
|
// Dropping it leaves stale depth/render-control state active in
|
|
// later passes.
|
|
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 TryReadUInt32(ctx, packetAddress + 4, out drawCount);
|
|
case ItDrawIndex2 when packetLength >= 6:
|
|
state.DrawIndexOffset = 0;
|
|
return TryReadUInt32(ctx, packetAddress + 16, out drawCount);
|
|
case ItDrawIndexOffset2 when packetLength >= 5:
|
|
if (!TryReadUInt32(ctx, packetAddress + 8, out var indexOffset))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
state.DrawIndexOffset = indexOffset;
|
|
return TryReadUInt32(ctx, packetAddress + 12, out drawCount);
|
|
case ItDrawIndexMultiAuto when packetLength >= 4:
|
|
if (!TryReadUInt32(ctx, packetAddress + 12, out var control))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
drawCount = (control >> 21) & 0x7FFu;
|
|
return true;
|
|
case ItDrawIndirect or ItDrawIndexIndirect
|
|
when packetLength >= 5 && state.IndirectArgsAddress != 0:
|
|
if (!TryReadUInt32(ctx, packetAddress + 4, out var dataOffset))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return TryReadUInt32(
|
|
ctx,
|
|
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;
|
|
if (state.PendingTargetlessDraw is { } stalePendingDraw)
|
|
{
|
|
ReturnPooledDrawArrays(
|
|
stalePendingDraw,
|
|
globals: true,
|
|
vertex: true,
|
|
index: true);
|
|
state.PendingTargetlessDraw = null;
|
|
}
|
|
state.TranslatedDraw = null;
|
|
state.GuestDrawKind = GuestDrawKind.None;
|
|
foreach (var target in renderTargets)
|
|
{
|
|
state.KnownRenderTargets[target.Address] = target;
|
|
// Colour exports originate in the pixel stage. A depth-only draw
|
|
// can leave old CB registers bound, but it must not become the
|
|
// advertised writer of those surfaces merely because they remain
|
|
// in state.
|
|
if (hasPixelShader)
|
|
{
|
|
state.RenderTargetWriters[target.Address] = new RenderTargetWriter(
|
|
drawSequence,
|
|
hasExportShader ? exportShaderAddress : 0,
|
|
pixelShaderAddress,
|
|
vertexCount,
|
|
primitiveType);
|
|
}
|
|
|
|
if (_traceAgcShader ||
|
|
_tracePixelShaderAddress == pixelShaderAddress ||
|
|
_traceRenderTargetAddress == target.Address)
|
|
{
|
|
Console.Error.WriteLine(
|
|
"[LOADER][TRACE] " +
|
|
$"agc.rt_writer seq={drawSequence} target=0x{target.Address:X16} " +
|
|
$"fmt={target.Format} tile={target.TileMode} " +
|
|
$"size={target.Width}x{target.Height} vertices={vertexCount} " +
|
|
$"prim=0x{primitiveType:X} indexed={indexed} " +
|
|
$"es=0x{(hasExportShader ? exportShaderAddress : 0):X16} " +
|
|
$"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16} " +
|
|
$"color_write={(hasPixelShader ? 1 : 0)}");
|
|
}
|
|
}
|
|
|
|
if (vertexCount == 0 || vertexCount > 1_048_576)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var translationError = string.Empty;
|
|
var depthState = DecodeDepthState(state.CxRegisters);
|
|
var depthTarget = DecodeDepthTarget(state.CxRegisters);
|
|
var hasDepthOnlyCandidate = hasExportShader &&
|
|
!hasPixelShader &&
|
|
depthTarget is not null &&
|
|
(depthState.TestEnable || depthState.WriteEnable || depthState.ClearEnable);
|
|
if (hasDepthOnlyCandidate &&
|
|
TryCreateTranslatedDepthOnlyGuestDraw(
|
|
ctx,
|
|
state,
|
|
exportShaderAddress,
|
|
vertexCount,
|
|
indexed,
|
|
depthTarget!,
|
|
out var depthOnlyDraw,
|
|
out translationError))
|
|
{
|
|
state.TranslatedDraw = depthOnlyDraw;
|
|
var activeDepthTarget = depthOnlyDraw.DepthTarget!;
|
|
var textures = CreateGuestDrawTextures(
|
|
ctx,
|
|
depthOnlyDraw.Textures,
|
|
out _);
|
|
var globalMemoryBuffers =
|
|
CreateTranslatedDrawGlobalBuffers(depthOnlyDraw);
|
|
var vertexBuffers =
|
|
CreateGuestVertexBuffers(depthOnlyDraw.VertexInputs);
|
|
var renderState = depthOnlyDraw.RenderState;
|
|
if (activeDepthTarget.ReadOnly && renderState.Depth.WriteEnable)
|
|
{
|
|
renderState = renderState with
|
|
{
|
|
Depth = renderState.Depth with { WriteEnable = false },
|
|
};
|
|
}
|
|
|
|
TraceDrawCompact(
|
|
drawSequence,
|
|
depthOnlyDraw,
|
|
textures,
|
|
vertexBuffers);
|
|
GuestGpu.Current.SubmitDepthOnlyTranslatedDraw(
|
|
depthOnlyDraw.PixelShader,
|
|
textures,
|
|
globalMemoryBuffers,
|
|
depthOnlyDraw.AttributeCount,
|
|
activeDepthTarget,
|
|
depthOnlyDraw.VertexShader,
|
|
depthOnlyDraw.VertexCount,
|
|
depthOnlyDraw.InstanceCount,
|
|
depthOnlyDraw.PrimitiveType,
|
|
depthOnlyDraw.IndexBuffer,
|
|
vertexBuffers,
|
|
renderState,
|
|
depthOnlyDraw.PixelShaderAddress);
|
|
|
|
if (_traceAgcShader)
|
|
{
|
|
TraceAgcShader(
|
|
$"agc.depth_only_draw seq={drawSequence} " +
|
|
$"es=0x{exportShaderAddress:X16} " +
|
|
$"depth=0x{activeDepthTarget.Address:X16}:" +
|
|
$"{activeDepthTarget.Width}x{activeDepthTarget.Height}:" +
|
|
$"fmt{activeDepthTarget.GuestFormat}/sw{activeDepthTarget.SwizzleMode} " +
|
|
$"test={(renderState.Depth.TestEnable ? 1 : 0)} " +
|
|
$"write={(renderState.Depth.WriteEnable ? 1 : 0)} " +
|
|
$"func={renderState.Depth.CompareOp} ro={(activeDepthTarget.ReadOnly ? 1 : 0)}");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (hasExportShader &&
|
|
hasPixelShader &&
|
|
hasPsInputEna &&
|
|
hasPsInputAddr &&
|
|
TryCreateTranslatedGuestDraw(
|
|
ctx,
|
|
state,
|
|
exportShaderAddress,
|
|
pixelShaderAddress,
|
|
psInputEna,
|
|
psInputAddr,
|
|
vertexCount,
|
|
indexed,
|
|
out var translatedDraw,
|
|
out translationError))
|
|
{
|
|
state.TranslatedDraw = translatedDraw;
|
|
if (TryGetHardwareColorResolveTargets(
|
|
state.CxRegisters,
|
|
out var resolveSource,
|
|
out var resolveDestination))
|
|
{
|
|
state.KnownRenderTargets[resolveSource.Address] = resolveSource;
|
|
state.KnownRenderTargets[resolveDestination.Address] = resolveDestination;
|
|
ProvideRenderTargetInitialData(ctx, resolveSource);
|
|
if (VulkanVideoPresenter.TrySubmitGuestImageBlit(
|
|
resolveSource.Address,
|
|
resolveSource.Width,
|
|
resolveSource.Height,
|
|
resolveSource.Format,
|
|
resolveSource.NumberType,
|
|
resolveDestination.Address,
|
|
resolveDestination.Width,
|
|
resolveDestination.Height,
|
|
resolveDestination.Format,
|
|
resolveDestination.NumberType))
|
|
{
|
|
state.RenderTargetWriters[resolveDestination.Address] =
|
|
new RenderTargetWriter(
|
|
drawSequence,
|
|
exportShaderAddress,
|
|
pixelShaderAddress,
|
|
vertexCount,
|
|
primitiveType);
|
|
TraceAgcShader(
|
|
$"agc.hardware_color_resolve seq={drawSequence} " +
|
|
$"src=0x{resolveSource.Address:X16}:" +
|
|
$"{resolveSource.Width}x{resolveSource.Height}:" +
|
|
$"fmt{resolveSource.Format}/num{resolveSource.NumberType} " +
|
|
$"dst=0x{resolveDestination.Address:X16}:" +
|
|
$"{resolveDestination.Width}x{resolveDestination.Height}:" +
|
|
$"fmt{resolveDestination.Format}/num{resolveDestination.NumberType}");
|
|
ReturnPooledDrawArrays(
|
|
translatedDraw,
|
|
globals: true,
|
|
vertex: true,
|
|
index: true);
|
|
state.TranslatedDraw = null;
|
|
return;
|
|
}
|
|
|
|
TraceAgcShader(
|
|
$"agc.hardware_color_resolve_unavailable seq={drawSequence} " +
|
|
$"src=0x{resolveSource.Address:X16} " +
|
|
$"dst=0x{resolveDestination.Address:X16}");
|
|
}
|
|
|
|
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
|
|
if (firstTarget.Address != 0)
|
|
{
|
|
// Render every bound color target. A deferred G-buffer draw
|
|
// writes several targets in one guest pass; we render one bound
|
|
// target per Vulkan pass, each with the pixel variant that
|
|
// routes that target's MRT export slot to the fragment output.
|
|
// Every pass is enqueued in order on the same guest render
|
|
// queue. Share the immutable snapshots between those passes
|
|
// and let only the final pass return pooled arrays after its
|
|
// host upload. Copying the full vertex/global payload for each
|
|
// secondary target made deferred G-buffer draws allocate
|
|
// hundreds of MiB per second on the managed large-object heap.
|
|
var drawRenderTargets = translatedDraw.RenderTargets;
|
|
var lastTargetIndex = 0;
|
|
for (var targetIndex = 1; targetIndex < drawRenderTargets.Count; targetIndex++)
|
|
{
|
|
if (drawRenderTargets[targetIndex].Address != 0)
|
|
{
|
|
lastTargetIndex = targetIndex;
|
|
}
|
|
}
|
|
|
|
var sharedTextures = CreateGuestDrawTextures(
|
|
ctx,
|
|
translatedDraw.Textures,
|
|
out _);
|
|
var sharedGlobalMemoryBuffers =
|
|
CreateTranslatedDrawGlobalBuffers(translatedDraw);
|
|
var sharedVertexBuffers =
|
|
CreateGuestVertexBuffers(translatedDraw.VertexInputs);
|
|
TraceRectListVertices(translatedDraw, sharedVertexBuffers);
|
|
TraceGrassDrawVertices(translatedDraw, sharedTextures, sharedVertexBuffers);
|
|
TraceDrawCompact(
|
|
drawSequence,
|
|
translatedDraw,
|
|
sharedTextures,
|
|
sharedVertexBuffers);
|
|
foreach (var renderTarget in drawRenderTargets)
|
|
{
|
|
if (renderTarget.Address != 0)
|
|
{
|
|
ProvideRenderTargetInitialData(ctx, renderTarget);
|
|
}
|
|
}
|
|
|
|
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
|
|
translatedDraw.PixelShader,
|
|
sharedTextures,
|
|
sharedGlobalMemoryBuffers,
|
|
translatedDraw.AttributeCount,
|
|
translatedDraw.GuestTargets,
|
|
translatedDraw.VertexShader,
|
|
translatedDraw.VertexCount,
|
|
translatedDraw.InstanceCount,
|
|
translatedDraw.PrimitiveType,
|
|
translatedDraw.IndexBuffer,
|
|
sharedVertexBuffers,
|
|
translatedDraw.RenderState,
|
|
translatedDraw.DepthTarget,
|
|
translatedDraw.PixelShaderAddress);
|
|
}
|
|
else
|
|
{
|
|
if (translatedDraw.DepthTarget is { } translatedDepthTarget)
|
|
{
|
|
var textures = CreateGuestDrawTextures(
|
|
ctx,
|
|
translatedDraw.Textures,
|
|
out _);
|
|
var globalMemoryBuffers =
|
|
CreateTranslatedDrawGlobalBuffers(translatedDraw);
|
|
var vertexBuffers =
|
|
CreateGuestVertexBuffers(translatedDraw.VertexInputs);
|
|
var renderState = translatedDraw.RenderState;
|
|
if (translatedDepthTarget.ReadOnly && renderState.Depth.WriteEnable)
|
|
{
|
|
renderState = renderState with
|
|
{
|
|
Depth = renderState.Depth with { WriteEnable = false },
|
|
};
|
|
}
|
|
|
|
TraceDrawCompact(
|
|
drawSequence,
|
|
translatedDraw,
|
|
textures,
|
|
vertexBuffers);
|
|
GuestGpu.Current.SubmitDepthOnlyTranslatedDraw(
|
|
translatedDraw.PixelShader,
|
|
textures,
|
|
globalMemoryBuffers,
|
|
translatedDraw.AttributeCount,
|
|
translatedDepthTarget,
|
|
translatedDraw.VertexShader,
|
|
translatedDraw.VertexCount,
|
|
translatedDraw.InstanceCount,
|
|
translatedDraw.PrimitiveType,
|
|
translatedDraw.IndexBuffer,
|
|
vertexBuffers,
|
|
renderState,
|
|
translatedDraw.PixelShaderAddress);
|
|
}
|
|
else
|
|
{
|
|
var storageTarget = translatedDraw.Textures
|
|
.FirstOrDefault(binding => binding.IsStorage);
|
|
if (storageTarget is not null)
|
|
{
|
|
var textures = CreateGuestDrawTextures(
|
|
ctx,
|
|
translatedDraw.Textures,
|
|
out _);
|
|
var globalMemoryBuffers =
|
|
CreateTranslatedDrawGlobalBuffers(translatedDraw);
|
|
TraceDrawCompact(drawSequence, translatedDraw, textures, []);
|
|
GuestGpu.Current.SubmitStorageTranslatedDraw(
|
|
translatedDraw.PixelShader,
|
|
textures,
|
|
globalMemoryBuffers,
|
|
translatedDraw.AttributeCount,
|
|
storageTarget.Descriptor.Width,
|
|
storageTarget.Descriptor.Height,
|
|
translatedDraw.PixelShaderAddress);
|
|
// The storage submit consumes the global buffers (the
|
|
// presenter returns them) but never the vertex/index
|
|
// arrays; return those here so they don't leak the pool.
|
|
ReturnPooledDrawArrays(
|
|
translatedDraw,
|
|
globals: false,
|
|
vertex: true,
|
|
index: true);
|
|
}
|
|
else
|
|
{
|
|
if (translatedDraw.Textures.Count != 0)
|
|
{
|
|
// Unity's PS5 final blit can omit CB registers and
|
|
// rely on the following AGC flip to name the scanout
|
|
// target. Retain that sampled draw until RFlip, then
|
|
// enqueue it against the known display surface before
|
|
// the ordered capture.
|
|
state.PendingTargetlessDraw = translatedDraw;
|
|
}
|
|
else
|
|
{
|
|
// No render target, storage sink or sampled source:
|
|
// nothing can consume this draw.
|
|
ReturnPooledDrawArrays(
|
|
translatedDraw,
|
|
globals: true,
|
|
vertex: true,
|
|
index: true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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}");
|
|
}
|
|
|
|
// Trace-only: gated on the flag so the dedup set and the dump —
|
|
// which reads pooled buffer data the presenter may already have
|
|
// recycled (harmless for diagnostics, garbage bytes at worst) —
|
|
// cost nothing in normal runs.
|
|
if (_traceAgcShader)
|
|
{
|
|
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;
|
|
}
|
|
|
|
TraceDrawCompactMiss(
|
|
drawSequence,
|
|
vertexCount,
|
|
hasExportShader && hasPixelShader
|
|
? translationError
|
|
: hasDepthOnlyCandidate && !string.IsNullOrEmpty(translationError)
|
|
? $"depth-only: {translationError}"
|
|
: $"missing-shaders es={hasExportShader} ps={hasPixelShader} ena={hasPsInputEna} addr={hasPsInputAddr}");
|
|
TraceShaderTranslationMiss(
|
|
ctx,
|
|
state,
|
|
vertexCount,
|
|
hasExportShader,
|
|
exportShaderAddress,
|
|
hasPixelShader,
|
|
pixelShaderAddress,
|
|
hasPsInputEna,
|
|
psInputEna,
|
|
hasPsInputAddr,
|
|
psInputAddr,
|
|
hasExportShader && hasPixelShader || hasDepthOnlyCandidate
|
|
? translationError
|
|
: null);
|
|
}
|
|
|
|
private static bool TryCreateTranslatedDepthOnlyGuestDraw(
|
|
CpuContext ctx,
|
|
SubmittedDcbState state,
|
|
ulong exportShaderAddress,
|
|
uint vertexCount,
|
|
bool indexed,
|
|
GuestDepthTarget depthTarget,
|
|
out TranslatedGuestDraw draw,
|
|
out string error)
|
|
{
|
|
draw = default!;
|
|
error = string.Empty;
|
|
ulong exportShaderHeader;
|
|
lock (_submitTraceGate)
|
|
{
|
|
_shaderHeadersByCode.TryGetValue(exportShaderAddress, out exportShaderHeader);
|
|
}
|
|
|
|
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,
|
|
requiredVertexRecordCount: TryGetRequiredVertexRecordCount(
|
|
ctx,
|
|
state,
|
|
vertexCount,
|
|
indexed,
|
|
out var depthVertexRecords)
|
|
? depthVertexRecords
|
|
: null))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var exportFingerprint = _bakeScalars
|
|
? ComputeShaderStateFingerprint(exportEvaluation)
|
|
: ComputeShaderStructuralFingerprint(exportEvaluation);
|
|
var cacheKey = (
|
|
exportShaderAddress,
|
|
exportFingerprint,
|
|
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment);
|
|
_depthOnlyVertexShaderCache.TryGetValue(cacheKey, out var vertexShader);
|
|
|
|
if (vertexShader is null)
|
|
{
|
|
var guestGlobalBufferCount = exportEvaluation.GlobalMemoryBindings.Count;
|
|
// CreateTranslatedDrawGlobalBuffers appends both stage scalar
|
|
// blocks. The pixel block is unused by the fixed fragment stage;
|
|
// the vertex block remains at guestCount+1, matching this layout.
|
|
var totalGlobalBufferCount = _bakeScalars
|
|
? guestGlobalBufferCount
|
|
: guestGlobalBufferCount + 2;
|
|
if (!GuestGpu.Current.TryCompileVertexShader(
|
|
exportState,
|
|
exportEvaluation,
|
|
out vertexShader,
|
|
out error,
|
|
globalBufferBase: 0,
|
|
totalGlobalBufferCount: totalGlobalBufferCount,
|
|
imageBindingBase: 0,
|
|
scalarRegisterBufferIndex: _bakeScalars
|
|
? -1
|
|
: guestGlobalBufferCount + 1,
|
|
requiredVertexOutputCount: 0,
|
|
storageBufferOffsetAlignment:
|
|
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment))
|
|
{
|
|
ReturnPooledEvaluationArrays(exportEvaluation);
|
|
return false;
|
|
}
|
|
|
|
DumpCompiledShader(
|
|
"depth-vs",
|
|
exportShaderAddress,
|
|
exportFingerprint,
|
|
vertexShader!,
|
|
exportState.Program);
|
|
VulkanVideoPresenter.CountSpirvCompilation();
|
|
_depthOnlyVertexShaderCache.TryAdd(cacheKey, vertexShader!);
|
|
}
|
|
|
|
var textures = new List<TranslatedImageBinding>(
|
|
exportEvaluation.ImageBindings.Count);
|
|
foreach (var binding in exportEvaluation.ImageBindings)
|
|
{
|
|
if (!TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture))
|
|
{
|
|
if (_strictShaderDescriptors)
|
|
{
|
|
error = $"invalid export texture descriptor at pc=0x{binding.Pc:X}";
|
|
ReturnPooledEvaluationArrays(exportEvaluation);
|
|
return false;
|
|
}
|
|
|
|
texture = new TextureDescriptor(
|
|
0,
|
|
1,
|
|
1,
|
|
Gen5TextureFormatR8G8B8A8Unorm,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
1,
|
|
0xFAC);
|
|
}
|
|
|
|
textures.Add(new TranslatedImageBinding(
|
|
texture,
|
|
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode),
|
|
binding.MipLevel ?? 0,
|
|
binding.SamplerDescriptor));
|
|
}
|
|
|
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
|
exportEvaluation.VertexInputs ?? [];
|
|
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
|
var syntheticTarget = new RenderTargetDescriptor(
|
|
Slot: 0,
|
|
Address: 0,
|
|
depthTarget.Width,
|
|
depthTarget.Height,
|
|
Format: 0,
|
|
NumberType: 0,
|
|
TileMode: 0);
|
|
var renderState = CreateRenderState(state.CxRegisters, syntheticTarget) with
|
|
{
|
|
// A guest pass without a pixel shader has no colour exports. The
|
|
// presenter uses a private compatibility attachment, so disable
|
|
// all writes to it and expose only the persistent DB result.
|
|
Blends = [GuestBlendState.Default with { WriteMask = 0 }],
|
|
};
|
|
if (depthTarget.Width == 1 &&
|
|
depthTarget.Height == 1 &&
|
|
renderState.Viewport is { } depthViewport)
|
|
{
|
|
var inferredWidth = (uint)Math.Clamp(
|
|
MathF.Ceiling(MathF.Abs(depthViewport.Width)),
|
|
1f,
|
|
16384f);
|
|
var inferredHeight = (uint)Math.Clamp(
|
|
MathF.Ceiling(MathF.Abs(depthViewport.Height)),
|
|
1f,
|
|
16384f);
|
|
if (inferredWidth > 1 || inferredHeight > 1)
|
|
{
|
|
depthTarget = depthTarget with
|
|
{
|
|
Width = inferredWidth,
|
|
Height = inferredHeight,
|
|
};
|
|
syntheticTarget = syntheticTarget with
|
|
{
|
|
Width = inferredWidth,
|
|
Height = inferredHeight,
|
|
};
|
|
renderState = CreateRenderState(state.CxRegisters, syntheticTarget) with
|
|
{
|
|
Blends = [GuestBlendState.Default with { WriteMask = 0 }],
|
|
};
|
|
}
|
|
}
|
|
draw = new TranslatedGuestDraw(
|
|
exportShaderAddress,
|
|
PixelShaderAddress: 0,
|
|
primitiveType,
|
|
vertexShader!,
|
|
GuestGpu.Current.GetDepthOnlyFragmentShader(),
|
|
AttributeCount: 0,
|
|
vertexCount,
|
|
state.InstanceCount,
|
|
indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
|
|
textures,
|
|
exportEvaluation.GlobalMemoryBindings,
|
|
vertexInputs,
|
|
RenderTargets: [],
|
|
depthTarget,
|
|
GuestTargets: [],
|
|
renderState,
|
|
PixelUserData: [],
|
|
RawBlendControl: 0,
|
|
RawColorInfo: 0,
|
|
PixelInitialScalars: [],
|
|
exportEvaluation.InitialScalarRegisters);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryCreateTranslatedGuestDraw(
|
|
CpuContext ctx,
|
|
SubmittedDcbState state,
|
|
ulong exportShaderAddress,
|
|
ulong pixelShaderAddress,
|
|
uint psInputEna,
|
|
uint psInputAddr,
|
|
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);
|
|
}
|
|
|
|
// Sequential (not short-circuited into one condition) so a failure
|
|
// after an evaluation succeeded can return that evaluation's pooled
|
|
// buffer arrays to the pool instead of leaking them.
|
|
if (!Gen5ShaderTranslator.TryCreateState(
|
|
ctx,
|
|
exportShaderAddress,
|
|
exportShaderHeader,
|
|
state.ShRegisters,
|
|
SelectExportUserDataRegister(state.ShRegisters),
|
|
out var exportState,
|
|
out error,
|
|
userDataScalarRegisterBase: NggUserDataScalarRegisterBase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Gen5ShaderScalarEvaluator.TryEvaluate(
|
|
ctx,
|
|
exportState,
|
|
out var exportEvaluation,
|
|
out error,
|
|
resolveVertexInputs: true,
|
|
requiredVertexRecordCount: TryGetRequiredVertexRecordCount(
|
|
ctx,
|
|
state,
|
|
vertexCount,
|
|
indexed,
|
|
out var vertexRecords)
|
|
? vertexRecords
|
|
: null))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!Gen5ShaderTranslator.TryCreateState(
|
|
ctx,
|
|
pixelShaderAddress,
|
|
pixelShaderHeader,
|
|
state.ShRegisters,
|
|
PsTextureUserDataRegister,
|
|
out var pixelState,
|
|
out error))
|
|
{
|
|
ReturnPooledEvaluationArrays(exportEvaluation);
|
|
return false;
|
|
}
|
|
|
|
if (!Gen5ShaderScalarEvaluator.TryEvaluate(
|
|
ctx,
|
|
pixelState,
|
|
out var pixelEvaluation,
|
|
out error))
|
|
{
|
|
ReturnPooledEvaluationArrays(exportEvaluation);
|
|
return false;
|
|
}
|
|
|
|
if (pixelShaderAddress == 0x0000000500781200 &&
|
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_GLOBALS") == "1")
|
|
{
|
|
TraceAstroTitlePixelGlobals(pixelEvaluation);
|
|
}
|
|
|
|
if (pixelShaderAddress == 0x0000000500781200 &&
|
|
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TITLE_GLOBALS_LIVE") == "1")
|
|
{
|
|
TraceAstroTitlePixelGlobalProbe(pixelEvaluation);
|
|
}
|
|
|
|
// Every bound color target the shader exports to. Deferred renderers
|
|
// draw a multi-render-target G-buffer (up to eight slots) in one pass.
|
|
// Fall back to slot 0 if we cannot match any export to a bound target.
|
|
var pixelColorExportMasks = pixelState.Program.PixelColorExportMasks;
|
|
var allBoundTargets = GetRenderTargets(state.CxRegisters);
|
|
// At most 8 slots; a manual filter avoids the per-draw LINQ iterator/
|
|
// closure allocations. Slots are distinct, so sorting by slot is stable.
|
|
var selectedTargets = new List<RenderTargetDescriptor>(allBoundTargets.Count);
|
|
foreach (var target in allBoundTargets)
|
|
{
|
|
if (GetPixelColorExportMask(pixelColorExportMasks, target.Slot) != 0)
|
|
{
|
|
selectedTargets.Add(target);
|
|
}
|
|
}
|
|
|
|
if (selectedTargets.Count == 0)
|
|
{
|
|
foreach (var target in allBoundTargets)
|
|
{
|
|
if (target.Slot == 0)
|
|
{
|
|
selectedTargets.Add(target);
|
|
}
|
|
}
|
|
}
|
|
|
|
selectedTargets.Sort(static (left, right) => left.Slot.CompareTo(right.Slot));
|
|
var renderTargets = selectedTargets.ToArray();
|
|
if (_traceAgcShader && allBoundTargets.Count > 1)
|
|
{
|
|
TraceAgcShader(
|
|
$"agc.mrt_filter ps=0x{pixelShaderAddress:X16} " +
|
|
$"bound=[{string.Join(",", allBoundTargets.Select(t => $"s{t.Slot}:0x{t.Address:X}:exp{(GetPixelColorExportMask(pixelColorExportMasks, t.Slot) != 0 ? 1 : 0)}"))}] " +
|
|
$"kept={renderTargets.Length}");
|
|
}
|
|
|
|
var renderTargetOutputKinds = new Gen5PixelOutputKind[renderTargets.Length];
|
|
for (var index = 0; index < renderTargets.Length; index++)
|
|
{
|
|
var target = renderTargets[index];
|
|
if (!GuestGpu.Current.TryGetRenderTargetOutputKind(
|
|
target.Format,
|
|
target.NumberType,
|
|
out renderTargetOutputKinds[index]))
|
|
{
|
|
error =
|
|
$"unsupported color target format={target.Format} number_type={target.NumberType}";
|
|
ReturnPooledEvaluationArrays(exportEvaluation);
|
|
ReturnPooledEvaluationArrays(pixelEvaluation);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Exact packed encoding of the output layout — guest slot (6 bits, CB targets are
|
|
// 0-7) plus output kind (2 bits) per target, host locations being the sequential
|
|
// byte positions. Replaces a per-draw LINQ + string build that allocated on every
|
|
// draw, cache hit or not; the target count disambiguates trailing zero bytes.
|
|
var outputLayout = 0UL;
|
|
for (var index = 0; index < renderTargets.Length; index++)
|
|
{
|
|
outputLayout |= (ulong)(((renderTargets[index].Slot & 0x3Fu) << 2) |
|
|
(uint)renderTargetOutputKinds[index]) << (index * 8);
|
|
}
|
|
|
|
var attributeCount = GetInterpolatedAttributeCount(pixelState);
|
|
var exportStateFingerprint = _bakeScalars
|
|
? ComputeShaderStateFingerprint(exportEvaluation)
|
|
: ComputeShaderStructuralFingerprint(exportEvaluation);
|
|
var pixelStateFingerprint = _bakeScalars
|
|
? ComputeShaderStateFingerprint(pixelEvaluation)
|
|
: ComputeShaderStructuralFingerprint(pixelEvaluation);
|
|
var shaderKey = (
|
|
exportShaderAddress,
|
|
exportStateFingerprint,
|
|
pixelShaderAddress,
|
|
pixelStateFingerprint,
|
|
outputLayout,
|
|
(uint)renderTargets.Length,
|
|
attributeCount,
|
|
psInputEna,
|
|
psInputAddr,
|
|
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment);
|
|
|
|
var guestGlobalBuffers =
|
|
pixelEvaluation.GlobalMemoryBindings.Count +
|
|
exportEvaluation.GlobalMemoryBindings.Count;
|
|
// Two per-draw initial-scalar buffers ride after the guest buffers:
|
|
// [pixel guest][vertex guest][pixel sgprs][vertex sgprs].
|
|
var totalGlobalBuffers = _bakeScalars
|
|
? guestGlobalBuffers
|
|
: guestGlobalBuffers + 2;
|
|
_graphicsShaderCache.TryGetValue(shaderKey, out var compiled);
|
|
|
|
if (compiled.Vertex is null || compiled.Pixel is null)
|
|
{
|
|
var pixelOutputs = new Gen5PixelOutputBinding[renderTargets.Length];
|
|
for (var location = 0; location < renderTargets.Length; location++)
|
|
{
|
|
pixelOutputs[location] = new Gen5PixelOutputBinding(
|
|
renderTargets[location].Slot,
|
|
(uint)location,
|
|
renderTargetOutputKinds[location]);
|
|
}
|
|
|
|
if (!GuestGpu.Current.TryCompilePixelShader(
|
|
pixelState,
|
|
pixelEvaluation,
|
|
pixelOutputs,
|
|
out var pixelShader,
|
|
out error,
|
|
globalBufferBase: 0,
|
|
totalGlobalBufferCount: totalGlobalBuffers,
|
|
imageBindingBase: 0,
|
|
scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers,
|
|
pixelInputEnable: psInputEna,
|
|
pixelInputAddress: psInputAddr,
|
|
storageBufferOffsetAlignment:
|
|
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment) ||
|
|
!GuestGpu.Current.TryCompileVertexShader(
|
|
exportState,
|
|
exportEvaluation,
|
|
out var vertexShader,
|
|
out error,
|
|
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
|
totalGlobalBufferCount: totalGlobalBuffers,
|
|
imageBindingBase: pixelEvaluation.ImageBindings.Count,
|
|
scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers + 1,
|
|
requiredVertexOutputCount: (int)GetInterpolatedAttributeCount(pixelState),
|
|
storageBufferOffsetAlignment:
|
|
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment))
|
|
{
|
|
ReturnPooledEvaluationArrays(exportEvaluation);
|
|
ReturnPooledEvaluationArrays(pixelEvaluation);
|
|
return false;
|
|
}
|
|
|
|
compiled = (vertexShader!, pixelShader!);
|
|
DumpCompiledShader(
|
|
"vs",
|
|
exportShaderAddress,
|
|
exportStateFingerprint,
|
|
compiled.Vertex,
|
|
exportState.Program);
|
|
DumpCompiledShader(
|
|
"ps",
|
|
pixelShaderAddress,
|
|
pixelStateFingerprint,
|
|
compiled.Pixel,
|
|
pixelState.Program);
|
|
VulkanVideoPresenter.CountSpirvCompilation();
|
|
_graphicsShaderCache.TryAdd(shaderKey, compiled);
|
|
}
|
|
|
|
var textures = new List<TranslatedImageBinding>(
|
|
pixelEvaluation.ImageBindings.Count +
|
|
exportEvaluation.ImageBindings.Count);
|
|
if (!TryAppendTranslatedImageBindings(
|
|
pixelEvaluation.ImageBindings,
|
|
textures,
|
|
pixelShaderAddress,
|
|
exportShaderAddress,
|
|
out error) ||
|
|
!TryAppendTranslatedImageBindings(
|
|
exportEvaluation.ImageBindings,
|
|
textures,
|
|
pixelShaderAddress,
|
|
exportShaderAddress,
|
|
out error))
|
|
{
|
|
ReturnPooledEvaluationArrays(exportEvaluation);
|
|
ReturnPooledEvaluationArrays(pixelEvaluation);
|
|
return false;
|
|
}
|
|
|
|
var globalMemoryBindings = new Gen5GlobalMemoryBinding[
|
|
pixelEvaluation.GlobalMemoryBindings.Count +
|
|
exportEvaluation.GlobalMemoryBindings.Count];
|
|
for (var index = 0; index < pixelEvaluation.GlobalMemoryBindings.Count; index++)
|
|
{
|
|
globalMemoryBindings[index] = pixelEvaluation.GlobalMemoryBindings[index];
|
|
}
|
|
for (var index = 0; index < exportEvaluation.GlobalMemoryBindings.Count; index++)
|
|
{
|
|
globalMemoryBindings[pixelEvaluation.GlobalMemoryBindings.Count + index] =
|
|
exportEvaluation.GlobalMemoryBindings[index];
|
|
}
|
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
|
exportEvaluation.VertexInputs ?? [];
|
|
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
|
|
var guestTargets = new GuestRenderTarget[renderTargets.Length];
|
|
for (var index = 0; index < renderTargets.Length; index++)
|
|
{
|
|
guestTargets[index] = new GuestRenderTarget(
|
|
renderTargets[index].Address,
|
|
renderTargets[index].Width,
|
|
renderTargets[index].Height,
|
|
renderTargets[index].Format,
|
|
renderTargets[index].NumberType);
|
|
}
|
|
|
|
var pixelUserDataCount = Math.Min(pixelEvaluation.InitialScalarRegisters.Count, 8);
|
|
var pixelUserData = new uint[pixelUserDataCount];
|
|
for (var index = 0; index < pixelUserDataCount; index++)
|
|
{
|
|
pixelUserData[index] = pixelEvaluation.InitialScalarRegisters[index];
|
|
}
|
|
|
|
draw = new TranslatedGuestDraw(
|
|
exportShaderAddress,
|
|
pixelShaderAddress,
|
|
primitiveType,
|
|
compiled.Vertex,
|
|
compiled.Pixel,
|
|
GetInterpolatedAttributeCount(pixelState),
|
|
vertexCount,
|
|
state.InstanceCount,
|
|
indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
|
|
textures,
|
|
globalMemoryBindings,
|
|
vertexInputs,
|
|
renderTargets,
|
|
DecodeDepthTarget(state.CxRegisters),
|
|
guestTargets,
|
|
ApplyTransparentPremultipliedFillClear(
|
|
CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks),
|
|
textures,
|
|
vertexInputs,
|
|
pixelEvaluation.InitialScalarRegisters),
|
|
pixelUserData,
|
|
state.CxRegisters.TryGetValue(CbBlend0Control, out var rawBlend) ? rawBlend : 0,
|
|
state.CxRegisters.TryGetValue(
|
|
CbColor0Info + renderTargets.FirstOrDefault().Slot * CbColorRegisterStride,
|
|
out var rawInfo)
|
|
? rawInfo
|
|
: 0,
|
|
pixelEvaluation.InitialScalarRegisters,
|
|
exportEvaluation.InitialScalarRegisters);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryAppendTranslatedImageBindings(
|
|
IReadOnlyList<Gen5ImageBinding> bindings,
|
|
List<TranslatedImageBinding> textures,
|
|
ulong pixelShaderAddress,
|
|
ulong exportShaderAddress,
|
|
out string error)
|
|
{
|
|
foreach (var binding in bindings)
|
|
{
|
|
if (!TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture))
|
|
{
|
|
// A garbage/zeroed texture descriptor (from a per-draw descriptor
|
|
// setup race — the same root as scalar-load-failed) would drop
|
|
// the whole draw, so deferred-lighting/composite passes that
|
|
// produce the composite's feeder targets never run. Keep the
|
|
// existing 1x1 fallback unless strict diagnostics are requested.
|
|
if (_strictShaderDescriptors)
|
|
{
|
|
error = $"invalid texture descriptor at pc=0x{binding.Pc:X}";
|
|
return false;
|
|
}
|
|
|
|
texture = new TextureDescriptor(
|
|
0, 1, 1, Gen5TextureFormatR8G8B8A8Unorm, 0, 0, 0, 0, 0, 1, 0xFAC);
|
|
}
|
|
|
|
var isStorage =
|
|
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
|
if (_traceAgcShader || _tracePixelShaderAddress == pixelShaderAddress)
|
|
{
|
|
Console.Error.WriteLine(
|
|
"[LOADER][TRACE] " +
|
|
$"agc.texture_binding ps=0x{pixelShaderAddress:X16} es=0x{exportShaderAddress:X16} " +
|
|
$"pc=0x{binding.Pc:X} op={binding.Opcode} storage={(isStorage ? 1 : 0)} " +
|
|
$"decoded={FormatTextureDescriptor(texture)} " +
|
|
$"raw={FormatShaderDwords(binding.ResourceDescriptor)} sampler={FormatShaderDwords(binding.SamplerDescriptor)}");
|
|
}
|
|
textures.Add(
|
|
new TranslatedImageBinding(
|
|
texture,
|
|
isStorage,
|
|
binding.MipLevel ?? 0,
|
|
binding.SamplerDescriptor));
|
|
}
|
|
|
|
error = string.Empty;
|
|
return true;
|
|
}
|
|
|
|
private static int _tracedAstroTitlePixelGlobals;
|
|
private static int _tracedAstroTitlePixelGlobalProbe;
|
|
|
|
private static void TraceAstroTitlePixelGlobalProbe(Gen5ShaderEvaluation evaluation)
|
|
{
|
|
const int probeOffset = 17216;
|
|
var draw = Interlocked.Increment(ref _tracedAstroTitlePixelGlobalProbe);
|
|
foreach (var (binding, index) in evaluation.GlobalMemoryBindings.Select((value, index) => (value, index)))
|
|
{
|
|
if (probeOffset + 16 > binding.DataLength)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[TITLE-GLOBALS-LIVE] draw={draw} binding={index} " +
|
|
$"base=0x{binding.BaseAddress:X16} offset=0x{probeOffset:X} " +
|
|
$"bytes={Convert.ToHexString(binding.Data.AsSpan(probeOffset, 16))}");
|
|
}
|
|
}
|
|
|
|
private static void TraceAstroTitlePixelGlobals(Gen5ShaderEvaluation evaluation)
|
|
{
|
|
if (Interlocked.Exchange(ref _tracedAstroTitlePixelGlobals, 1) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[TITLE-GLOBALS] initial_s0_31=" +
|
|
string.Join(',', evaluation.InitialScalarRegisters
|
|
.Take(32)
|
|
.Select((value, index) => $"s{index}={value:X8}")));
|
|
|
|
var probeOffsets = new[]
|
|
{
|
|
0, 16, 24, 32, 48,
|
|
192, 256, 400, 432,
|
|
17100, 17104, 17136, 17168, 17184, 17200, 17216,
|
|
};
|
|
foreach (var binding in evaluation.GlobalMemoryBindings)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[TITLE-GLOBALS] binding s{binding.ScalarAddress} " +
|
|
$"base=0x{binding.BaseAddress:X16} bytes={binding.DataLength} " +
|
|
$"pcs={string.Join(',', binding.InstructionPcs.Select(pc => $"0x{pc:X}"))}");
|
|
foreach (var offset in probeOffsets)
|
|
{
|
|
if (offset < 0 || offset + 16 > binding.DataLength)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[TITLE-GLOBALS] s{binding.ScalarAddress}+0x{offset:X}=" +
|
|
Convert.ToHexString(binding.Data.AsSpan(offset, 16)));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static readonly bool _fillClearHack = !string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_FILL_CLEAR"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
|
|
/// <summary>
|
|
/// Treat an untextured fill that outputs pure transparent black through
|
|
/// premultiplied blending as an overwrite. Chowdren issues exactly this
|
|
/// draw once per frame to reset its effect layers (fog smoke, vignette
|
|
/// masks); under the blend factors it sets (One, OneMinusSrcAlpha) a
|
|
/// (0,0,0,0) source is a mathematical no-op, so without this the layers
|
|
/// accumulate until they saturate and the fog composites as a flat veil
|
|
/// over the whole scene. The workaround applies only when every MRT
|
|
/// attachment uses the same blend pattern. Disable with
|
|
/// SHARPEMU_DISABLE_FILL_CLEAR=1.
|
|
/// </summary>
|
|
private static GuestRenderState ApplyTransparentPremultipliedFillClear(
|
|
GuestRenderState renderState,
|
|
IReadOnlyList<TranslatedImageBinding> textures,
|
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
|
|
IReadOnlyList<uint> pixelUserData)
|
|
{
|
|
if (!_fillClearHack ||
|
|
textures.Count != 0 ||
|
|
vertexInputs.Count != 0 ||
|
|
pixelUserData.Count < 4 ||
|
|
!renderState.Blends.All(IsTransparentPremultipliedFillBlend))
|
|
{
|
|
return renderState;
|
|
}
|
|
|
|
for (var index = 0; index < 4; index++)
|
|
{
|
|
// Positive or negative zero.
|
|
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(GuestBlendState blend) =>
|
|
blend is
|
|
{
|
|
Enable: true,
|
|
ColorSrcFactor: 1,
|
|
ColorDstFactor: 5,
|
|
ColorFunc: 0,
|
|
};
|
|
|
|
private static GuestIndexBuffer? CreateGuestIndexBuffer(
|
|
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 = VulkanVideoPresenter.GuestDataPool.Rent(byteCount);
|
|
var span = data.AsSpan(0, byteCount);
|
|
var address = state.IndexBufferAddress + byteOffset;
|
|
if (ctx.Memory.TryRead(address, span) ||
|
|
KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, span))
|
|
{
|
|
return new GuestIndexBuffer(data, byteCount, is32Bit, Pooled: true);
|
|
}
|
|
|
|
VulkanVideoPresenter.GuestDataPool.Return(data);
|
|
return null;
|
|
}
|
|
|
|
private static bool TryGetRequiredVertexRecordCount(
|
|
CpuContext ctx,
|
|
SubmittedDcbState state,
|
|
uint drawCount,
|
|
bool indexed,
|
|
out uint recordCount)
|
|
{
|
|
recordCount = Math.Max(drawCount, Math.Max(state.InstanceCount, 1u));
|
|
if (!indexed)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (state.IndexBufferAddress == 0 || drawCount == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var is32Bit = state.IndexSize != 0;
|
|
var bytesPerIndex = is32Bit ? sizeof(uint) : sizeof(ushort);
|
|
var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)bytesPerIndex);
|
|
var address = state.IndexBufferAddress + byteOffset;
|
|
const int chunkBytes = 64 * 1024;
|
|
var scratch = VulkanVideoPresenter.GuestDataPool.Rent(chunkBytes);
|
|
var remaining = drawCount;
|
|
var maxIndex = 0u;
|
|
var sawIndex = false;
|
|
try
|
|
{
|
|
while (remaining != 0)
|
|
{
|
|
var chunkIndices = (int)Math.Min(
|
|
remaining,
|
|
(uint)(chunkBytes / bytesPerIndex));
|
|
var bytes = chunkIndices * bytesPerIndex;
|
|
var span = scratch.AsSpan(0, bytes);
|
|
if (!ctx.Memory.TryRead(address, span) &&
|
|
!KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, span))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (var index = 0; index < chunkIndices; index++)
|
|
{
|
|
var value = is32Bit
|
|
? BinaryPrimitives.ReadUInt32LittleEndian(
|
|
span.Slice(index * sizeof(uint), sizeof(uint)))
|
|
: BinaryPrimitives.ReadUInt16LittleEndian(
|
|
span.Slice(index * sizeof(ushort), sizeof(ushort)));
|
|
if (value == (is32Bit ? uint.MaxValue : ushort.MaxValue))
|
|
{
|
|
// Primitive-restart markers do not address vertex data.
|
|
continue;
|
|
}
|
|
|
|
maxIndex = Math.Max(maxIndex, value);
|
|
sawIndex = true;
|
|
}
|
|
|
|
address += (uint)bytes;
|
|
remaining -= (uint)chunkIndices;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
VulkanVideoPresenter.GuestDataPool.Return(scratch);
|
|
}
|
|
|
|
var indexedRecords = sawIndex && maxIndex != uint.MaxValue
|
|
? maxIndex + 1
|
|
: 1u;
|
|
recordCount = Math.Max(indexedRecords, Math.Max(state.InstanceCount, 1u));
|
|
if (_traceVertexRanges &&
|
|
Interlocked.Increment(ref _tracedVertexRangeCount) <= 512)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] agc.vertex_range indexed=1 draw_count={drawCount} " +
|
|
$"max_index={(sawIndex ? maxIndex : 0)} records={recordCount} " +
|
|
$"instances={state.InstanceCount} index_size={(is32Bit ? 32 : 16)} " +
|
|
$"index_addr=0x{state.IndexBufferAddress:X16} offset={state.DrawIndexOffset}");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static uint GetPixelColorExportMask(uint packedMasks, uint target) =>
|
|
target < ColorTargetCount
|
|
? (packedMasks >> (int)(target * 4)) & 0xFu
|
|
: 0;
|
|
|
|
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 static readonly bool _bakeScalars = string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_BAKE_SGPRS"),
|
|
"1",
|
|
StringComparison.Ordinal);
|
|
|
|
/// <summary>
|
|
/// Fingerprint of everything that shapes the translated SPIR-V besides
|
|
/// scalar register values (those arrive in a per-draw buffer): the
|
|
/// resolved binding set with its format-shaping descriptor words, vertex
|
|
/// input layouts, and compute system registers. Value churn in user data
|
|
/// no longer forces a new translation and pipeline.
|
|
/// </summary>
|
|
private static ulong ComputeShaderStructuralFingerprint(Gen5ShaderEvaluation evaluation)
|
|
{
|
|
const ulong prime = 1099511628211UL;
|
|
var hash = 14695981039346656037UL;
|
|
void Mix(ulong value) => hash = (hash ^ value) * prime;
|
|
|
|
foreach (var binding in evaluation.ImageBindings)
|
|
{
|
|
Mix(binding.Pc);
|
|
Mix((ulong)(uint)binding.Opcode.GetHashCode());
|
|
if (binding.ResourceDescriptor.Count > 1)
|
|
{
|
|
// The generated image type depends only on unified format.
|
|
// Bounds are queried from the bound view in SPIR-V; guest image
|
|
// addresses, dimensions, swizzles and sampler state are all
|
|
// runtime descriptor data and must not create pipeline variants.
|
|
Mix(binding.ResourceDescriptor[1] & 0x1FF0_0000u);
|
|
}
|
|
|
|
Mix(binding.MipLevel ?? 0xFFFF_FFFFUL);
|
|
}
|
|
|
|
foreach (var binding in evaluation.GlobalMemoryBindings)
|
|
{
|
|
Mix(binding.ScalarAddress);
|
|
Mix((ulong)binding.InstructionPcs.Count);
|
|
foreach (var pc in binding.InstructionPcs)
|
|
{
|
|
Mix(pc);
|
|
}
|
|
}
|
|
|
|
if (evaluation.VertexInputs is { } vertexInputs)
|
|
{
|
|
foreach (var input in vertexInputs)
|
|
{
|
|
Mix(input.Pc);
|
|
Mix(input.Location);
|
|
Mix(input.ComponentCount);
|
|
Mix(input.DataFormat);
|
|
Mix(input.NumberFormat);
|
|
Mix(input.Stride);
|
|
Mix(input.OffsetBytes);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Baked-scalar mode has no runtime state block from which the shader
|
|
// can load descriptor-alignment biases, so the low guest address bits
|
|
// remain part of the generated module and must participate in its key.
|
|
foreach (var binding in evaluation.GlobalMemoryBindings)
|
|
{
|
|
hash = (hash ^ (
|
|
binding.BaseAddress &
|
|
(VulkanVideoPresenter.GuestStorageBufferOffsetAlignment - 1))) * 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 bool TryGetHardwareColorResolveTargets(
|
|
IReadOnlyDictionary<uint, uint> registers,
|
|
out RenderTargetDescriptor source,
|
|
out RenderTargetDescriptor destination)
|
|
{
|
|
source = default;
|
|
destination = default;
|
|
if (!registers.TryGetValue(CbColorControl, out var colorControl) ||
|
|
((colorControl >> 4) & 0x7u) != 3u)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// CB_COLOR_CONTROL.MODE=RESOLVE uses color slot 0 as the multisampled
|
|
// source and slot 1 as the single-sample destination. CB_TARGET_MASK
|
|
// still enables only slot 0, so treating this like a normal MRT draw
|
|
// rewrites the source and leaves the following composite's input blank.
|
|
var boundTargets = GetRenderTargets(registers, includeMaskedTargets: true);
|
|
source = boundTargets.FirstOrDefault(target => target.Slot == 0);
|
|
destination = boundTargets.FirstOrDefault(target => target.Slot == 1);
|
|
return source.Address != 0 &&
|
|
destination.Address != 0 &&
|
|
source.Width == destination.Width &&
|
|
source.Height == destination.Height &&
|
|
source.Format == destination.Format;
|
|
}
|
|
|
|
private static IReadOnlyList<RenderTargetDescriptor> GetRenderTargets(
|
|
IReadOnlyDictionary<uint, uint> registers,
|
|
bool includeMaskedTargets = false)
|
|
{
|
|
var hasTargetMask = registers.TryGetValue(CbTargetMask, out var targetMask);
|
|
var targets = new List<RenderTargetDescriptor>(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 ||
|
|
(!includeMaskedTargets && 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 GuestRenderState CreateRenderState(
|
|
IReadOnlyDictionary<uint, uint> registers,
|
|
RenderTargetDescriptor target)
|
|
{
|
|
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
|
return new GuestRenderState(
|
|
[DecodeBlendState(registers, target.Slot)],
|
|
scissor,
|
|
DecodeViewport(registers, target.Width, target.Height, scissor),
|
|
DecodeRasterState(registers),
|
|
DecodeDepthState(registers));
|
|
}
|
|
|
|
private static GuestRenderState CreateRenderState(
|
|
IReadOnlyDictionary<uint, uint> registers,
|
|
IReadOnlyList<RenderTargetDescriptor> targets,
|
|
uint pixelColorExportMasks)
|
|
{
|
|
if (targets.Count == 0)
|
|
{
|
|
return GuestRenderState.Default;
|
|
}
|
|
|
|
var target = targets[0];
|
|
var scissor = DecodeScissor(registers, target.Width, target.Height);
|
|
var blends = new GuestBlendState[targets.Count];
|
|
for (var index = 0; index < targets.Count; index++)
|
|
{
|
|
var blend = DecodeBlendState(registers, targets[index].Slot);
|
|
blends[index] = blend with
|
|
{
|
|
WriteMask = blend.WriteMask &
|
|
GetPixelColorExportMask(
|
|
pixelColorExportMasks,
|
|
targets[index].Slot),
|
|
};
|
|
}
|
|
|
|
return new GuestRenderState(
|
|
blends,
|
|
scissor,
|
|
DecodeViewport(registers, target.Width, target.Height, scissor),
|
|
DecodeRasterState(registers),
|
|
DecodeDepthState(registers));
|
|
}
|
|
|
|
// DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE
|
|
// bit2, ZFUNC bits[6:4] (GCN compare, matches Vulkan CompareOp ordering).
|
|
// DB_RENDER_CONTROL (context register 0x000): DEPTH_CLEAR_ENABLE bit0.
|
|
private const uint DbDepthControl = 0x200;
|
|
|
|
internal static GuestDepthState DecodeDepthState(
|
|
IReadOnlyDictionary<uint, uint> registers)
|
|
{
|
|
var hasDepthControl = registers.TryGetValue(DbDepthControl, out var control);
|
|
registers.TryGetValue(DbRenderControl, out var renderControl);
|
|
var testEnable = (control & 0x2u) != 0;
|
|
var writeEnable = (control & 0x4u) != 0;
|
|
var compareOp = hasDepthControl
|
|
? (control >> 4) & 0x7u
|
|
: GuestDepthState.Default.CompareOp;
|
|
var clearEnable = (renderControl & 0x1u) != 0;
|
|
return new GuestDepthState(testEnable, writeEnable, compareOp, clearEnable);
|
|
}
|
|
|
|
private static GuestDepthTarget? DecodeDepthTarget(
|
|
IReadOnlyDictionary<uint, uint> registers)
|
|
{
|
|
var depthState = DecodeDepthState(registers);
|
|
if (!depthState.TestEnable &&
|
|
!depthState.WriteEnable &&
|
|
!depthState.ClearEnable)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!registers.TryGetValue(DbZInfo, out var zInfo) ||
|
|
!registers.TryGetValue(DbDepthSizeXy, out var sizeXy))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var guestFormat = zInfo & 0x3u;
|
|
if (guestFormat == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
registers.TryGetValue(DbZReadBase, out var readBase);
|
|
registers.TryGetValue(DbZWriteBase, out var writeBase);
|
|
registers.TryGetValue(DbZReadBaseHi, out var readBaseHi);
|
|
registers.TryGetValue(DbZWriteBaseHi, out var writeBaseHi);
|
|
var readAddress = ((ulong)(readBaseHi & 0xFFu) << 40) | ((ulong)readBase << 8);
|
|
var writeAddress = ((ulong)(writeBaseHi & 0xFFu) << 40) | ((ulong)writeBase << 8);
|
|
if (readAddress == 0 && writeAddress == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var width = (sizeXy & 0x3FFFu) + 1;
|
|
var height = ((sizeXy >> 16) & 0x3FFFu) + 1;
|
|
if (width == 0 || height == 0 || width > 16384 || height > 16384)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
registers.TryGetValue(DbDepthView, out var depthView);
|
|
var clearDepth = registers.TryGetValue(DbDepthClear, out var clearBits)
|
|
? BitConverter.UInt32BitsToSingle(clearBits)
|
|
: 1f;
|
|
if (!float.IsFinite(clearDepth) || clearDepth < 0f || clearDepth > 1f)
|
|
{
|
|
clearDepth = 1f;
|
|
}
|
|
|
|
return new GuestDepthTarget(
|
|
readAddress,
|
|
writeAddress,
|
|
width,
|
|
height,
|
|
guestFormat,
|
|
(zInfo >> 4) & 0x1Fu,
|
|
clearDepth,
|
|
ReadOnly: (depthView & (1u << 24)) != 0 || writeAddress == 0);
|
|
}
|
|
|
|
// PA_SU_SC_MODE_CNTL (context register 0x205) carries face culling, the
|
|
// front-face winding and polygon (wireframe) mode.
|
|
private const uint PaSuScModeCntl = 0x205;
|
|
|
|
private static GuestRasterState DecodeRasterState(
|
|
IReadOnlyDictionary<uint, uint> registers)
|
|
{
|
|
if (!registers.TryGetValue(PaSuScModeCntl, out var mode))
|
|
{
|
|
return GuestRasterState.Default;
|
|
}
|
|
|
|
var cullFront = (mode & 0x1u) != 0;
|
|
var cullBack = (mode & 0x2u) != 0;
|
|
var frontFaceClockwise = (mode & 0x4u) != 0;
|
|
var polyMode = (mode >> 3) & 0x3u;
|
|
var frontPtype = (mode >> 5) & 0x7u;
|
|
// POLY_MODE != 0 with a line front primitive type renders wireframe.
|
|
var wireframe = polyMode != 0 && frontPtype == 1;
|
|
return new GuestRasterState(cullFront, cullBack, frontFaceClockwise, wireframe);
|
|
}
|
|
|
|
private static GuestBlendState DecodeBlendState(
|
|
IReadOnlyDictionary<uint, uint> 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 GuestBlendState(
|
|
((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 GuestRect? DecodeScissor(
|
|
IReadOnlyDictionary<uint, uint> registers,
|
|
uint targetWidth,
|
|
uint targetHeight)
|
|
{
|
|
if (targetWidth == 0 || targetHeight == 0)
|
|
{
|
|
return new GuestRect(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);
|
|
}
|
|
|
|
// AGC reset-state blocks can carry an all-zero screen-scissor pair as
|
|
// an unpatched placeholder while the generic/viewport scissors hold
|
|
// the active bounds. Treat only that exact reset value as absent. A
|
|
// nonzero empty rectangle remains meaningful and still clips the draw.
|
|
IntersectScissorPair(
|
|
registers,
|
|
PaScScreenScissorTl,
|
|
PaScScreenScissorBr,
|
|
ref left,
|
|
ref top,
|
|
ref right,
|
|
ref bottom,
|
|
ignoreAllZeroPair: true);
|
|
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 GuestRect(
|
|
left,
|
|
top,
|
|
checked((uint)(right - left)),
|
|
checked((uint)(bottom - top)));
|
|
}
|
|
|
|
private static GuestViewport? DecodeViewport(
|
|
IReadOnlyDictionary<uint, uint> registers,
|
|
uint targetWidth,
|
|
uint targetHeight,
|
|
GuestRect? scissor)
|
|
{
|
|
if (targetWidth == 0 || targetHeight == 0)
|
|
{
|
|
return new GuestViewport(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 GuestViewport(
|
|
xOffset - xScale,
|
|
yOffset - yScale,
|
|
xScale * 2f,
|
|
yScale * 2f,
|
|
minDepth,
|
|
maxDepth);
|
|
}
|
|
|
|
if (scissor is not { } rect)
|
|
{
|
|
return minDepth == 0f && maxDepth == 1f
|
|
? null
|
|
: new GuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth);
|
|
}
|
|
|
|
return new GuestViewport(
|
|
rect.X,
|
|
rect.Y,
|
|
rect.Width,
|
|
rect.Height,
|
|
minDepth,
|
|
maxDepth);
|
|
}
|
|
|
|
private static bool TryDecodeFiniteFloat(
|
|
IReadOnlyDictionary<uint, uint> 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<uint, uint> registers,
|
|
uint tlRegister,
|
|
uint brRegister,
|
|
ref int left,
|
|
ref int top,
|
|
ref int right,
|
|
ref int bottom,
|
|
int offsetX = 0,
|
|
int offsetY = 0,
|
|
bool ignoreAllZeroPair = false)
|
|
{
|
|
if (!TryDecodeScissorPair(
|
|
registers,
|
|
tlRegister,
|
|
brRegister,
|
|
out var pairLeft,
|
|
out var pairTop,
|
|
out var pairRight,
|
|
out var pairBottom,
|
|
out var allZero) ||
|
|
(ignoreAllZeroPair && allZero))
|
|
{
|
|
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<uint, uint> registers,
|
|
uint tlRegister,
|
|
uint brRegister,
|
|
out int left,
|
|
out int top,
|
|
out int right,
|
|
out int bottom,
|
|
out bool allZero)
|
|
{
|
|
left = 0;
|
|
top = 0;
|
|
right = 0;
|
|
bottom = 0;
|
|
allZero = false;
|
|
if (!registers.TryGetValue(tlRegister, out var tl) ||
|
|
!registers.TryGetValue(brRegister, out var br))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
allZero = tl == 0 && br == 0;
|
|
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 depthTarget = draw.DepthTarget is { } depth
|
|
? $"0x{depth.Address:X16}:{depth.Width}x{depth.Height}:" +
|
|
$"fmt{depth.GuestFormat}/sw{depth.SwizzleMode}:" +
|
|
$"read=0x{depth.ReadAddress:X16}/write=0x{depth.WriteAddress:X16}:" +
|
|
$"clear={depth.ClearDepth:0.######}/ro={(depth.ReadOnly ? 1 : 0)}"
|
|
: "none";
|
|
var probes = new Dictionary<ulong, string>();
|
|
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.DataLength}:" +
|
|
Convert.ToHexString(binding.Data.AsSpan(0, Math.Min(binding.DataLength, 256)))));
|
|
var indices = draw.IndexBuffer is { } indexBuffer
|
|
? $"{(indexBuffer.Is32Bit ? 32 : 16)}:" +
|
|
Convert.ToHexString(indexBuffer.Data.AsSpan(0, Math.Min(indexBuffer.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.PixelShader.Payload.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}] depth=[{depthTarget}] textures=[{textures}] " +
|
|
$"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]");
|
|
}
|
|
|
|
private static IReadOnlyList<GuestDrawTexture> CreateGuestDrawTextures(
|
|
CpuContext ctx,
|
|
IReadOnlyList<TranslatedImageBinding> bindings,
|
|
out int fallbackTextureCount)
|
|
{
|
|
var textures = new List<GuestDrawTexture>(bindings.Count);
|
|
fallbackTextureCount = 0;
|
|
foreach (var binding in bindings)
|
|
{
|
|
if (TryCreateGuestDrawTexture(
|
|
ctx,
|
|
binding.Descriptor,
|
|
binding.IsStorage,
|
|
binding.MipLevel,
|
|
binding.SamplerDescriptor,
|
|
out var texture))
|
|
{
|
|
textures.Add(texture);
|
|
if (texture.IsFallback)
|
|
{
|
|
fallbackTextureCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return textures;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Guest storage buffers for a translated draw, followed by the per-draw
|
|
/// initial scalar registers of each stage (pixel then vertex), matching
|
|
/// the binding layout the shaders were compiled against.
|
|
/// </summary>
|
|
private static IReadOnlyList<GuestMemoryBuffer> CreateTranslatedDrawGlobalBuffers(
|
|
TranslatedGuestDraw translatedDraw)
|
|
{
|
|
var buffers = CreateGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings);
|
|
if (_bakeScalars)
|
|
{
|
|
return buffers;
|
|
}
|
|
|
|
var combined = new List<GuestMemoryBuffer>(buffers.Count + 2);
|
|
combined.AddRange(buffers);
|
|
var runtimeStateLength = GetRuntimeScalarBufferLength(
|
|
translatedDraw.GlobalMemoryBindings.Count);
|
|
combined.Add(new GuestMemoryBuffer(
|
|
0,
|
|
PackRuntimeScalarState(
|
|
translatedDraw.PixelInitialScalars,
|
|
translatedDraw.GlobalMemoryBindings),
|
|
runtimeStateLength,
|
|
Pooled: true));
|
|
combined.Add(new GuestMemoryBuffer(
|
|
0,
|
|
PackRuntimeScalarState(
|
|
translatedDraw.VertexInitialScalars,
|
|
translatedDraw.GlobalMemoryBindings),
|
|
runtimeStateLength,
|
|
Pooled: true));
|
|
return combined;
|
|
}
|
|
|
|
private static IReadOnlyList<GuestMemoryBuffer>
|
|
CreateGlobalBufferOwnershipView(
|
|
IReadOnlyList<GuestMemoryBuffer> buffers,
|
|
bool ownsPooledData)
|
|
{
|
|
var view = new GuestMemoryBuffer[buffers.Count];
|
|
for (var index = 0; index < buffers.Count; index++)
|
|
{
|
|
var buffer = buffers[index];
|
|
view[index] = buffer with
|
|
{
|
|
Pooled = ownsPooledData && buffer.Pooled,
|
|
};
|
|
}
|
|
|
|
return view;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Present-time variant: the flip path can reuse the same translated
|
|
/// draw across several flips and swapchain retries, so it must not wrap
|
|
/// the (pooled, single-consumption) binding arrays. Buffer contents are
|
|
/// re-read from guest memory instead, which also presents current data.
|
|
/// </summary>
|
|
private static IReadOnlyList<GuestMemoryBuffer> CreateTranslatedDrawGlobalBuffersForPresent(
|
|
CpuContext ctx,
|
|
TranslatedGuestDraw translatedDraw)
|
|
{
|
|
var bindings = translatedDraw.GlobalMemoryBindings;
|
|
var combined = new List<GuestMemoryBuffer>(bindings.Count + 2);
|
|
foreach (var binding in bindings)
|
|
{
|
|
var data = new byte[Math.Max(binding.DataLength, sizeof(uint))];
|
|
var guestMemoryBacked = binding.BaseAddress != 0 &&
|
|
(ctx.Memory.TryRead(binding.BaseAddress, data) ||
|
|
KernelMemoryCompatExports.TryReadTrackedLibcHeap(binding.BaseAddress, data));
|
|
if (!guestMemoryBacked)
|
|
{
|
|
// Keep the zero-filled buffer; layout must match the shader.
|
|
}
|
|
|
|
combined.Add(new GuestMemoryBuffer(
|
|
binding.BaseAddress,
|
|
data,
|
|
data.Length,
|
|
Pooled: false,
|
|
Writable: binding.Writable,
|
|
WriteBackToGuest: binding.WriteBackToGuest && guestMemoryBacked));
|
|
}
|
|
|
|
if (!_bakeScalars)
|
|
{
|
|
var runtimeStateLength = GetRuntimeScalarBufferLength(bindings.Count);
|
|
combined.Add(new GuestMemoryBuffer(
|
|
0,
|
|
PackRuntimeScalarStateUnpooled(
|
|
translatedDraw.PixelInitialScalars,
|
|
bindings),
|
|
runtimeStateLength,
|
|
Pooled: false));
|
|
combined.Add(new GuestMemoryBuffer(
|
|
0,
|
|
PackRuntimeScalarStateUnpooled(
|
|
translatedDraw.VertexInitialScalars,
|
|
bindings),
|
|
runtimeStateLength,
|
|
Pooled: false));
|
|
}
|
|
|
|
return combined;
|
|
}
|
|
|
|
private static int GetRuntimeScalarBufferLength(int bindingCount) =>
|
|
checked((256 + bindingCount) * sizeof(uint));
|
|
|
|
private static byte[] PackRuntimeScalarState(
|
|
IReadOnlyList<uint> registers,
|
|
IReadOnlyList<Gen5GlobalMemoryBinding> bindings)
|
|
{
|
|
var bytes = VulkanVideoPresenter.GuestDataPool.Rent(
|
|
GetRuntimeScalarBufferLength(bindings.Count));
|
|
PackRuntimeScalarStateInto(bytes, registers, bindings);
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] PackRuntimeScalarStateUnpooled(
|
|
IReadOnlyList<uint> registers,
|
|
IReadOnlyList<Gen5GlobalMemoryBinding> bindings)
|
|
{
|
|
var bytes = new byte[GetRuntimeScalarBufferLength(bindings.Count)];
|
|
PackRuntimeScalarStateInto(bytes, registers, bindings);
|
|
return bytes;
|
|
}
|
|
|
|
private static void PackRuntimeScalarStateInto(
|
|
byte[] bytes,
|
|
IReadOnlyList<uint> registers,
|
|
IReadOnlyList<Gen5GlobalMemoryBinding> bindings)
|
|
{
|
|
PackScalarRegistersInto(bytes, registers);
|
|
var biasOffset = 256 * sizeof(uint);
|
|
for (var index = 0; index < bindings.Count; index++)
|
|
{
|
|
var byteBias = checked((uint)(
|
|
bindings[index].BaseAddress &
|
|
(VulkanVideoPresenter.GuestStorageBufferOffsetAlignment - 1)));
|
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
|
bytes.AsSpan(biasOffset + index * sizeof(uint), sizeof(uint)),
|
|
byteBias);
|
|
}
|
|
}
|
|
|
|
private static void PackScalarRegistersInto(byte[] bytes, IReadOnlyList<uint> registers)
|
|
{
|
|
if (registers is uint[] { Length: >= 256 } array)
|
|
{
|
|
// Guest scalar registers are little-endian dwords and the host
|
|
// is x86-64, so a bulk copy replaces 256 per-element writes.
|
|
System.Runtime.InteropServices.MemoryMarshal
|
|
.AsBytes(array.AsSpan(0, 256))
|
|
.CopyTo(bytes);
|
|
return;
|
|
}
|
|
|
|
// Rented arrays carry stale bytes; clear the packed window first.
|
|
Array.Clear(bytes, 0, 256 * sizeof(uint));
|
|
var count = Math.Min(registers.Count, 256);
|
|
for (var index = 0; index < count; index++)
|
|
{
|
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
|
bytes.AsSpan(index * sizeof(uint)),
|
|
registers[index]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the pooled buffer arrays an evaluation produced. Called only
|
|
/// on translation-failure paths, where no <see cref="TranslatedGuestDraw"/>
|
|
/// is built to take ownership; on success the draw's consumers return them.
|
|
/// </summary>
|
|
private static void ReturnPooledEvaluationArrays(Gen5ShaderEvaluation evaluation)
|
|
{
|
|
foreach (var binding in evaluation.GlobalMemoryBindings)
|
|
{
|
|
if (binding.DataPooled)
|
|
{
|
|
VulkanVideoPresenter.GuestDataPool.Return(binding.Data);
|
|
}
|
|
}
|
|
|
|
if (evaluation.VertexInputs is { } vertexInputs)
|
|
{
|
|
foreach (var binding in vertexInputs)
|
|
{
|
|
if (binding.DataPooled)
|
|
{
|
|
VulkanVideoPresenter.GuestDataPool.Return(binding.Data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns pooled data arrays a translated draw owns but did not hand to
|
|
/// a presenter consumer. The offscreen path hands globals, vertex and
|
|
/// index buffers to the presenter (which returns them), so it passes all
|
|
/// three false; other draw sinks pass true for whatever they dropped.
|
|
/// </summary>
|
|
private static void ReturnPooledDrawArrays(
|
|
TranslatedGuestDraw draw,
|
|
bool globals,
|
|
bool vertex,
|
|
bool index)
|
|
{
|
|
if (globals)
|
|
{
|
|
foreach (var binding in draw.GlobalMemoryBindings)
|
|
{
|
|
if (binding.DataPooled)
|
|
{
|
|
VulkanVideoPresenter.GuestDataPool.Return(binding.Data);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (vertex)
|
|
{
|
|
foreach (var binding in draw.VertexInputs)
|
|
{
|
|
if (binding.DataPooled)
|
|
{
|
|
VulkanVideoPresenter.GuestDataPool.Return(binding.Data);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (index && draw.IndexBuffer is { Pooled: true } indexBuffer)
|
|
{
|
|
VulkanVideoPresenter.GuestDataPool.Return(indexBuffer.Data);
|
|
}
|
|
}
|
|
|
|
private static IReadOnlyList<GuestMemoryBuffer> CreateGuestMemoryBuffers(
|
|
IReadOnlyList<Gen5GlobalMemoryBinding> bindings)
|
|
{
|
|
var buffers = new GuestMemoryBuffer[bindings.Count];
|
|
for (var index = 0; index < bindings.Count; index++)
|
|
{
|
|
buffers[index] = new GuestMemoryBuffer(
|
|
bindings[index].BaseAddress,
|
|
bindings[index].Data,
|
|
bindings[index].DataLength,
|
|
bindings[index].DataPooled,
|
|
bindings[index].Writable,
|
|
bindings[index].WriteBackToGuest);
|
|
}
|
|
|
|
return buffers;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Guest storage buffers for a compute dispatch followed by its initial
|
|
/// scalar registers. Dispatch-specific SGPR values remain runtime data so
|
|
/// one translated pipeline serves every matching shader/resource shape.
|
|
/// </summary>
|
|
private static IReadOnlyList<GuestMemoryBuffer> CreateTranslatedComputeGlobalBuffers(
|
|
Gen5ShaderEvaluation evaluation)
|
|
{
|
|
var buffers = CreateGuestMemoryBuffers(evaluation.GlobalMemoryBindings);
|
|
if (_bakeScalars)
|
|
{
|
|
return buffers;
|
|
}
|
|
|
|
var combined = new List<GuestMemoryBuffer>(buffers.Count + 1);
|
|
combined.AddRange(buffers);
|
|
combined.Add(new GuestMemoryBuffer(
|
|
0,
|
|
PackRuntimeScalarState(
|
|
evaluation.InitialScalarRegisters,
|
|
evaluation.GlobalMemoryBindings),
|
|
GetRuntimeScalarBufferLength(evaluation.GlobalMemoryBindings.Count),
|
|
Pooled: true));
|
|
return combined;
|
|
}
|
|
|
|
private static IReadOnlyList<GuestVertexBuffer> CreateGuestVertexBuffers(
|
|
IReadOnlyList<Gen5VertexInputBinding> bindings)
|
|
{
|
|
var buffers = new GuestVertexBuffer[bindings.Count];
|
|
for (var index = 0; index < bindings.Count; index++)
|
|
{
|
|
var binding = bindings[index];
|
|
buffers[index] = new GuestVertexBuffer(
|
|
binding.Location,
|
|
binding.ComponentCount,
|
|
binding.DataFormat,
|
|
binding.NumberFormat,
|
|
binding.BaseAddress,
|
|
binding.Stride,
|
|
binding.OffsetBytes,
|
|
binding.Data,
|
|
binding.DataLength,
|
|
binding.DataPooled);
|
|
}
|
|
|
|
return buffers;
|
|
}
|
|
|
|
private static IReadOnlyList<GuestVertexBuffer>
|
|
CreateVertexBufferOwnershipView(
|
|
IReadOnlyList<GuestVertexBuffer> buffers,
|
|
bool ownsPooledData)
|
|
{
|
|
var view = new GuestVertexBuffer[buffers.Count];
|
|
for (var index = 0; index < buffers.Count; index++)
|
|
{
|
|
var buffer = buffers[index];
|
|
view[index] = buffer with
|
|
{
|
|
Pooled = ownsPooledData && buffer.Pooled,
|
|
};
|
|
}
|
|
|
|
return view;
|
|
}
|
|
|
|
private static GuestIndexBuffer? CreateIndexBufferOwnershipView(
|
|
GuestIndexBuffer? buffer,
|
|
bool ownsPooledData) =>
|
|
buffer is null
|
|
? null
|
|
: buffer with { Pooled = ownsPooledData && buffer.Pooled };
|
|
|
|
// BCn block-compressed guest formats and the bytes per 4x4 block.
|
|
private static int GetBlockCompressedBlockBytes(uint format) => format switch
|
|
{
|
|
169 or 170 or 175 or 176 => 8,
|
|
171 or 172 or 173 or 174 or 177 or 178 or 179 or 180 or 181 or 182 => 16,
|
|
_ => 0,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Deswizzles a tiled texture source into linear layout when tiling is
|
|
/// enabled and the format is understood; returns null to keep the raw
|
|
/// bytes (linear surfaces, unknown modes, or non-power-of-two elements).
|
|
/// </summary>
|
|
private static bool TryGetTextureElementLayout(
|
|
TextureDescriptor descriptor,
|
|
uint sourceWidth,
|
|
out int elementsWide,
|
|
out int elementsHigh,
|
|
out int bytesPerElement)
|
|
{
|
|
var blockBytes = GetBlockCompressedBlockBytes(descriptor.Format);
|
|
if (blockBytes != 0)
|
|
{
|
|
bytesPerElement = blockBytes;
|
|
elementsWide = (int)((sourceWidth + 3) / 4);
|
|
elementsHigh = (int)((descriptor.Height + 3) / 4);
|
|
}
|
|
else
|
|
{
|
|
bytesPerElement = (int)GetTextureBytesPerTexel(descriptor.Format);
|
|
if (bytesPerElement == 0)
|
|
{
|
|
elementsWide = 0;
|
|
elementsHigh = 0;
|
|
return false;
|
|
}
|
|
|
|
elementsWide = (int)sourceWidth;
|
|
elementsHigh = (int)descriptor.Height;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static byte[]? TryDetileTextureSource(
|
|
TextureDescriptor descriptor,
|
|
uint sourceWidth,
|
|
int logicalByteCount,
|
|
byte[] source)
|
|
{
|
|
if (!GnmTiling.NeedsDetile(descriptor.TileMode) ||
|
|
!TryGetTextureElementLayout(
|
|
descriptor,
|
|
sourceWidth,
|
|
out var elementsWide,
|
|
out var elementsHigh,
|
|
out var bytesPerElement))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var linear = new byte[logicalByteCount];
|
|
return GnmTiling.TryDetile(
|
|
source,
|
|
linear,
|
|
descriptor.TileMode,
|
|
elementsWide,
|
|
elementsHigh,
|
|
bytesPerElement)
|
|
? linear
|
|
: null;
|
|
}
|
|
|
|
private static void TraceTextureFallback(TextureDescriptor descriptor, string reason)
|
|
{
|
|
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
|
if ((!string.Equals(mode, "1", StringComparison.Ordinal) &&
|
|
!string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase)) ||
|
|
Interlocked.Increment(ref _textureFallbackTraceCount) > 64)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] agc.texture_fallback reason={reason} " +
|
|
$"addr=0x{descriptor.Address:X16} type={descriptor.Type} " +
|
|
$"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " +
|
|
$"fmt={descriptor.Format} num={descriptor.NumberType} " +
|
|
$"tile={descriptor.TileMode} mip={descriptor.MipLevels} " +
|
|
$"dst=0x{descriptor.DstSelect:X3}");
|
|
}
|
|
|
|
private static bool TryCreateGuestDrawTexture(
|
|
CpuContext ctx,
|
|
TextureDescriptor descriptor,
|
|
bool isStorage,
|
|
uint mipLevel,
|
|
IReadOnlyList<uint> samplerDescriptor,
|
|
out GuestDrawTexture texture)
|
|
{
|
|
texture = default!;
|
|
if ((descriptor.Type != Gen5TextureType1D &&
|
|
descriptor.Type != Gen5TextureType2D) ||
|
|
descriptor.Width == 0 ||
|
|
descriptor.Height == 0 ||
|
|
descriptor.Width > 8192 ||
|
|
descriptor.Height > 8192)
|
|
{
|
|
TraceTextureFallback(descriptor, "invalid-descriptor");
|
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
|
return true;
|
|
}
|
|
|
|
var sourceWidth = descriptor.TileMode == 0
|
|
? GetLinearTexturePitch(
|
|
Math.Max(descriptor.Width, descriptor.Pitch),
|
|
descriptor.Height,
|
|
descriptor.Format)
|
|
: descriptor.Width;
|
|
var sourceByteCount = GetTextureByteCount(
|
|
descriptor.Format,
|
|
sourceWidth,
|
|
descriptor.Height);
|
|
if (sourceByteCount == 0 ||
|
|
sourceByteCount > MaxPresentedTextureBytes ||
|
|
sourceByteCount > int.MaxValue)
|
|
{
|
|
TraceTextureFallback(
|
|
descriptor,
|
|
$"invalid-byte-count:{sourceByteCount}");
|
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
|
return true;
|
|
}
|
|
|
|
var physicalSourceByteCount = sourceByteCount;
|
|
if (GnmTiling.NeedsDetile(descriptor.TileMode) &&
|
|
TryGetTextureElementLayout(
|
|
descriptor,
|
|
sourceWidth,
|
|
out var elementsWide,
|
|
out var elementsHigh,
|
|
out var bytesPerElement) &&
|
|
GnmTiling.TryGetTiledByteCount(
|
|
descriptor.TileMode,
|
|
elementsWide,
|
|
elementsHigh,
|
|
bytesPerElement,
|
|
out var tiledByteCount))
|
|
{
|
|
physicalSourceByteCount = tiledByteCount;
|
|
}
|
|
|
|
if (physicalSourceByteCount > MaxPresentedTextureBytes ||
|
|
physicalSourceByteCount > int.MaxValue)
|
|
{
|
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
|
return true;
|
|
}
|
|
|
|
if (!isStorage &&
|
|
descriptor.Address != 0 &&
|
|
VulkanVideoPresenter.IsGuestImageAvailable(
|
|
descriptor.Address,
|
|
descriptor.Format,
|
|
descriptor.NumberType))
|
|
{
|
|
texture = new GuestDrawTexture(
|
|
descriptor.Address,
|
|
descriptor.Width,
|
|
descriptor.Height,
|
|
descriptor.Format,
|
|
descriptor.NumberType,
|
|
[],
|
|
IsFallback: false,
|
|
IsStorage: false,
|
|
MipLevels: descriptor.MipLevels,
|
|
MipLevel: mipLevel,
|
|
BaseMipLevel: descriptor.ViewBaseLevel,
|
|
ResourceMipLevels: descriptor.ResourceMipLevels,
|
|
Pitch: sourceWidth,
|
|
TileMode: descriptor.TileMode,
|
|
DstSelect: descriptor.DstSelect,
|
|
Sampler: ToGuestSampler(samplerDescriptor));
|
|
return true;
|
|
}
|
|
|
|
if (isStorage)
|
|
{
|
|
var initialPixels = Array.Empty<byte>();
|
|
var uploadKnown = descriptor.Address != 0 &&
|
|
VulkanVideoPresenter.IsGuestImageUploadKnown(
|
|
descriptor.Address,
|
|
descriptor.Format,
|
|
descriptor.NumberType);
|
|
var readSucceeded = false;
|
|
var linearNonzero = false;
|
|
if (descriptor.Address != 0 && !uploadKnown)
|
|
{
|
|
// Storage images can be pre-populated in tiled guest memory
|
|
// just like sampled images. Reading only the logical linear
|
|
// byte count both truncates 64 KiB swizzle blocks and uploads
|
|
// tiled bytes as scanlines. Read the full physical footprint
|
|
// and run the same AddrLib-derived detile path used below for
|
|
// sampled textures before seeding the Vulkan image.
|
|
var storageSource = new byte[(int)physicalSourceByteCount];
|
|
if (ctx.Memory.TryRead(descriptor.Address, storageSource))
|
|
{
|
|
readSucceeded = true;
|
|
var linearStorage = TryDetileTextureSource(
|
|
descriptor,
|
|
sourceWidth,
|
|
checked((int)sourceByteCount),
|
|
storageSource) ?? storageSource
|
|
.AsSpan(0, checked((int)sourceByteCount))
|
|
.ToArray();
|
|
if (linearStorage.AsSpan().IndexOfAnyExcept((byte)0) >= 0)
|
|
{
|
|
linearNonzero = true;
|
|
initialPixels = linearStorage;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ParseOptionalHexAddress(
|
|
Environment.GetEnvironmentVariable(
|
|
"SHARPEMU_TRACE_STORAGE_IMAGE_INIT_ADDRESS")) ==
|
|
descriptor.Address)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] agc.storage_initial_data " +
|
|
$"addr=0x{descriptor.Address:X16} op_storage={isStorage} " +
|
|
$"upload_known={uploadKnown} read={readSucceeded} " +
|
|
$"nonzero={linearNonzero} initial_bytes={initialPixels.Length} " +
|
|
$"logical_bytes={sourceByteCount} physical_bytes={physicalSourceByteCount} " +
|
|
$"size={descriptor.Width}x{descriptor.Height} pitch={sourceWidth} " +
|
|
$"fmt={descriptor.Format} num={descriptor.NumberType} " +
|
|
$"tile={descriptor.TileMode} mip={mipLevel}");
|
|
}
|
|
|
|
texture = new GuestDrawTexture(
|
|
descriptor.Address,
|
|
descriptor.Width,
|
|
descriptor.Height,
|
|
descriptor.Format,
|
|
descriptor.NumberType,
|
|
initialPixels,
|
|
IsFallback: descriptor.Address == 0,
|
|
IsStorage: true,
|
|
MipLevels: descriptor.MipLevels,
|
|
MipLevel: mipLevel,
|
|
BaseMipLevel: descriptor.ViewBaseLevel,
|
|
ResourceMipLevels: descriptor.ResourceMipLevels,
|
|
Pitch: sourceWidth,
|
|
TileMode: descriptor.TileMode,
|
|
DstSelect: descriptor.DstSelect,
|
|
Sampler: ToGuestSampler(samplerDescriptor));
|
|
return true;
|
|
}
|
|
|
|
// When the presenter already holds this exact texture identity in
|
|
// its cache, the texel copy below would be discarded on arrival; for
|
|
// scenes that sample large textures every draw this copy dominated
|
|
// CPU time. The dirty peek closes the race with eviction: a texture
|
|
// the guest rewrote must ship fresh texels with this draw, because
|
|
// the render thread evicts the stale cache entry before executing it
|
|
// (skipping would leave the draw with no pixels and a fallback
|
|
// texture for the frame — visible flicker on animated textures).
|
|
var sampler = ToGuestSampler(samplerDescriptor);
|
|
if (!_textureCopySkipDisabled &&
|
|
descriptor.Address != 0 &&
|
|
!SharpEmu.HLE.GuestImageWriteTracker.PeekDirty(descriptor.Address) &&
|
|
VulkanVideoPresenter.IsTextureContentCached(
|
|
new VulkanVideoPresenter.TextureContentIdentity(
|
|
descriptor.Address,
|
|
descriptor.Width,
|
|
descriptor.Height,
|
|
descriptor.Format,
|
|
descriptor.NumberType,
|
|
descriptor.DstSelect,
|
|
descriptor.TileMode,
|
|
sourceWidth,
|
|
sampler)))
|
|
{
|
|
texture = new GuestDrawTexture(
|
|
descriptor.Address,
|
|
descriptor.Width,
|
|
descriptor.Height,
|
|
descriptor.Format,
|
|
descriptor.NumberType,
|
|
[],
|
|
IsFallback: false,
|
|
IsStorage: false,
|
|
MipLevels: descriptor.MipLevels,
|
|
MipLevel: mipLevel,
|
|
BaseMipLevel: descriptor.ViewBaseLevel,
|
|
ResourceMipLevels: descriptor.ResourceMipLevels,
|
|
Pitch: sourceWidth,
|
|
TileMode: descriptor.TileMode,
|
|
DstSelect: descriptor.DstSelect,
|
|
Sampler: sampler);
|
|
return true;
|
|
}
|
|
|
|
var source = new byte[(int)physicalSourceByteCount];
|
|
if (!ctx.Memory.TryRead(descriptor.Address, source))
|
|
{
|
|
TraceTextureFallback(
|
|
descriptor,
|
|
$"guest-read-failed:{sourceByteCount}");
|
|
texture = CreateFallbackGuestDrawTexture(isStorage, descriptor.Format, descriptor.NumberType);
|
|
return true;
|
|
}
|
|
|
|
if (_traceAgcShader)
|
|
{
|
|
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} logical_bytes={sourceByteCount} nonzero64={nonZero}");
|
|
}
|
|
DumpTextureSourceIfRequested(descriptor, sourceWidth, source);
|
|
|
|
var rgba = TryDetileTextureSource(
|
|
descriptor,
|
|
sourceWidth,
|
|
checked((int)sourceByteCount),
|
|
source) ?? source.AsSpan(0, checked((int)sourceByteCount)).ToArray();
|
|
DumpLinearTextureIfRequested(descriptor, sourceWidth, rgba);
|
|
texture = new GuestDrawTexture(
|
|
descriptor.Address,
|
|
descriptor.Width,
|
|
descriptor.Height,
|
|
descriptor.Format,
|
|
descriptor.NumberType,
|
|
rgba,
|
|
IsFallback: false,
|
|
IsStorage: isStorage,
|
|
MipLevels: descriptor.MipLevels,
|
|
MipLevel: mipLevel,
|
|
BaseMipLevel: descriptor.ViewBaseLevel,
|
|
ResourceMipLevels: descriptor.ResourceMipLevels,
|
|
Pitch: sourceWidth,
|
|
TileMode: descriptor.TileMode,
|
|
DstSelect: descriptor.DstSelect,
|
|
Sampler: ToGuestSampler(samplerDescriptor));
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// On PS5 render targets alias guest memory, so pixels the game wrote with
|
|
/// the CPU are visible before the first GPU draw (Chowdren pre-fills its
|
|
/// fog/overlay layers that way). Seed newly created Vulkan guest images
|
|
/// with the current guest memory contents to preserve that base layer.
|
|
/// </summary>
|
|
private static void ProvideRenderTargetInitialData(
|
|
CpuContext ctx,
|
|
RenderTargetDescriptor target)
|
|
{
|
|
if (!VulkanVideoPresenter.GuestImageWantsInitialData(target.Address))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var byteCount = (ulong)target.Width * target.Height * 4;
|
|
if (byteCount == 0 || byteCount > MaxPresentedTextureBytes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var initialData = new byte[byteCount];
|
|
var readOk = ctx.Memory.TryRead(target.Address, initialData);
|
|
var nonZero = readOk && initialData.AsSpan().IndexOfAnyExcept((byte)0) >= 0;
|
|
if (_traceDraws && _rtSeedTraced.Add(target.Address))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[RTSEED] addr=0x{target.Address:X} {target.Width}x{target.Height} " +
|
|
$"read={readOk} nonZero={nonZero}");
|
|
}
|
|
|
|
if (nonZero)
|
|
{
|
|
VulkanVideoPresenter.ProvideGuestImageInitialData(target.Address, initialData);
|
|
}
|
|
}
|
|
|
|
private static readonly HashSet<ulong> _rtSeedTraced = new();
|
|
|
|
private static void TraceDrawCompact(
|
|
ulong sequence,
|
|
TranslatedGuestDraw draw,
|
|
IReadOnlyList<GuestDrawTexture> textures,
|
|
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
|
|
{
|
|
if (!_traceDraws)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var target = draw.RenderTargets.FirstOrDefault();
|
|
var blend = draw.RenderState.Blend;
|
|
var viewport = draw.RenderState.Viewport is { } vp
|
|
? $"{vp.X:0.#},{vp.Y:0.#},{vp.Width:0.#}x{vp.Height:0.#}"
|
|
: "none";
|
|
var textureList = string.Join(
|
|
'|',
|
|
textures.Select(texture =>
|
|
$"0x{texture.Address:X}:{texture.Width}x{texture.Height}" +
|
|
$":f{texture.Format}/n{texture.NumberType}/d{texture.DstSelect:X3}" +
|
|
(texture.IsFallback ? ":FALLBACK" : string.Empty)));
|
|
var positions = string.Empty;
|
|
var positionBuffer = vertexBuffers.FirstOrDefault(buffer => buffer.Location == 0);
|
|
if (positionBuffer is { Length: >= 8 })
|
|
{
|
|
var stride = Math.Max(positionBuffer.Stride, 4u);
|
|
var vertexTotal = (int)((positionBuffer.Length - positionBuffer.OffsetBytes) / stride);
|
|
var sampled = new List<string>();
|
|
foreach (var vertex in new[] { 0, 1, vertexTotal - 1 })
|
|
{
|
|
var baseOffset = (int)(positionBuffer.OffsetBytes + vertex * stride);
|
|
if (vertex < 0 || baseOffset + 8 > positionBuffer.Length)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
sampled.Add(
|
|
$"{BitConverter.ToSingle(positionBuffer.Data, baseOffset):0.##}," +
|
|
$"{BitConverter.ToSingle(positionBuffer.Data, baseOffset + 4):0.##}");
|
|
}
|
|
|
|
positions = string.Join(';', sampled);
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[DRAW] seq={sequence} es=0x{draw.ExportShaderAddress:X} ps=0x{draw.PixelShaderAddress:X} " +
|
|
$"target=0x{target.Address:X}:{target.Width}x{target.Height}:f{target.Format}/n{target.NumberType} " +
|
|
$"prim=0x{draw.PrimitiveType:X} verts={draw.VertexCount} indexed={draw.IndexBuffer is not null} " +
|
|
$"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc}" +
|
|
$":a{blend.AlphaSrcFactor}/{blend.AlphaDstFactor}/{blend.AlphaFunc}/s{(blend.SeparateAlphaBlend ? 1 : 0)} " +
|
|
$"mask=0x{blend.WriteMask:X} viewport={viewport} textures={textureList} pos={positions} " +
|
|
$"ps_s0..3={string.Join(',', draw.PixelUserData.Take(4).Select(value => BitConverter.UInt32BitsToSingle(value).ToString("0.###")))} " +
|
|
$"rawblend=0x{draw.RawBlendControl:X8} info=0x{draw.RawColorInfo:X8}");
|
|
}
|
|
|
|
private static void TraceDrawCompactMiss(ulong sequence, uint vertexCount, string error)
|
|
{
|
|
if (!_traceDraws)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[DRAW] seq={sequence} MISS verts={vertexCount} error={error}");
|
|
}
|
|
|
|
private static int _grassTraceCount;
|
|
|
|
private static void TraceGrassDrawVertices(
|
|
TranslatedGuestDraw draw,
|
|
IReadOnlyList<GuestDrawTexture> textures,
|
|
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
|
|
{
|
|
if (_grassTraceCount >= 6 ||
|
|
!textures.Any(texture => texture.Width == 288 && texture.Height == 160) ||
|
|
vertexBuffers.Count == 0 ||
|
|
Interlocked.Increment(ref _grassTraceCount) > 6)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var text = new System.Text.StringBuilder();
|
|
text.Append($"agc.grassdraw prim=0x{draw.PrimitiveType:X} verts={draw.VertexCount} ");
|
|
text.Append($"indexed={draw.IndexBuffer is not null} buffers={vertexBuffers.Count}");
|
|
foreach (var buffer in vertexBuffers)
|
|
{
|
|
text.Append(
|
|
$"\n loc={buffer.Location} fmt={buffer.DataFormat}/{buffer.NumberFormat}x{buffer.ComponentCount} " +
|
|
$"stride={buffer.Stride} offset={buffer.OffsetBytes} bytes={buffer.Length}");
|
|
var stride = Math.Max(buffer.Stride, 4u);
|
|
var maxVerts = Math.Min(6, (int)((buffer.Length - buffer.OffsetBytes) / stride));
|
|
for (var vertex = 0; vertex < maxVerts; vertex++)
|
|
{
|
|
var baseOffset = (int)(buffer.OffsetBytes + vertex * stride);
|
|
var components = Math.Min(4, (int)((buffer.Length - baseOffset) / 4));
|
|
text.Append($"\n v{vertex}:");
|
|
for (var c = 0; c < components; c++)
|
|
{
|
|
text.Append($" {BitConverter.ToSingle(buffer.Data, baseOffset + c * 4):0.#####}");
|
|
}
|
|
}
|
|
}
|
|
|
|
TraceAgcShader(text.ToString());
|
|
}
|
|
|
|
private static int _rectListTraceCount;
|
|
|
|
private static void TraceRectListVertices(
|
|
TranslatedGuestDraw draw,
|
|
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
|
|
{
|
|
if (draw.PrimitiveType != 0x11 ||
|
|
draw.IndexBuffer is not null ||
|
|
vertexBuffers.Count == 0 ||
|
|
_rectListTraceCount >= 8 ||
|
|
Interlocked.Increment(ref _rectListTraceCount) > 8)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var buffer = vertexBuffers[0];
|
|
var stride = Math.Max(buffer.Stride, 4u);
|
|
var text = new System.Text.StringBuilder();
|
|
for (var vertex = 0; vertex < 3; vertex++)
|
|
{
|
|
var baseOffset = (int)(buffer.OffsetBytes + vertex * stride);
|
|
if (baseOffset + 16 > buffer.Length)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var x = BitConverter.ToSingle(buffer.Data, baseOffset);
|
|
var y = BitConverter.ToSingle(buffer.Data, baseOffset + 4);
|
|
var z = BitConverter.ToSingle(buffer.Data, baseOffset + 8);
|
|
var w = BitConverter.ToSingle(buffer.Data, baseOffset + 12);
|
|
text.Append($" v{vertex}=({x:0.###},{y:0.###},{z:0.###},{w:0.###})");
|
|
}
|
|
|
|
TraceAgcShader(
|
|
$"agc.rectlist verts={draw.VertexCount} stride={buffer.Stride} " +
|
|
$"fmt={buffer.DataFormat}/{buffer.NumberFormat}x{buffer.ComponentCount}{text}");
|
|
}
|
|
|
|
private static int _textureDumpCount;
|
|
private static readonly ConcurrentDictionary<string, int> _textureDumpKeys = new();
|
|
|
|
/// <summary>
|
|
/// Writes raw sampled-texture bytes (as read from guest memory) when
|
|
/// SHARPEMU_TEXTURE_DUMP_DIR is set, so upload-time content can be
|
|
/// inspected offline. File name records size and effective pitch.
|
|
/// </summary>
|
|
private static void DumpTextureSourceIfRequested(
|
|
in TextureDescriptor descriptor,
|
|
uint sourcePitch,
|
|
byte[] source)
|
|
{
|
|
var directory = Environment.GetEnvironmentVariable("SHARPEMU_TEXTURE_DUMP_DIR");
|
|
if (string.IsNullOrWhiteSpace(directory))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var key = $"0x{descriptor.Address:X}-{descriptor.Width}x{descriptor.Height}";
|
|
var occurrence = _textureDumpKeys.AddOrUpdate(key, 1, static (_, count) => count + 1);
|
|
// First uses plus periodic later snapshots (the game reuses the same
|
|
// allocation for successive full-screen images).
|
|
if ((occurrence > 3 && occurrence % 500 >= 3) ||
|
|
Interlocked.Increment(ref _textureDumpCount) > 200)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var index = _textureDumpCount;
|
|
|
|
try
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
var path = Path.Combine(
|
|
directory,
|
|
$"{index:D3}-0x{descriptor.Address:X}-{descriptor.Width}x{descriptor.Height}" +
|
|
$"-p{sourcePitch}-f{descriptor.Format}-t{descriptor.TileMode}.bin");
|
|
File.WriteAllBytes(path, source);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes the bytes after detiling when SHARPEMU_TEXTURE_LINEAR_DUMP_DIR is
|
|
/// set. Keeping this separate from the raw-source dump makes AddrLib
|
|
/// equation changes directly inspectable with ordinary image tools.
|
|
/// </summary>
|
|
private static void DumpLinearTextureIfRequested(
|
|
in TextureDescriptor descriptor,
|
|
uint sourcePitch,
|
|
byte[] source)
|
|
{
|
|
var directory = Environment.GetEnvironmentVariable("SHARPEMU_TEXTURE_LINEAR_DUMP_DIR");
|
|
if (string.IsNullOrWhiteSpace(directory))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var key = $"linear-0x{descriptor.Address:X}-{descriptor.Width}x{descriptor.Height}";
|
|
var occurrence = _textureDumpKeys.AddOrUpdate(key, 1, static (_, count) => count + 1);
|
|
if ((occurrence > 3 && occurrence % 500 >= 3) ||
|
|
Interlocked.Increment(ref _textureDumpCount) > 200)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var index = _textureDumpCount;
|
|
try
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
var path = Path.Combine(
|
|
directory,
|
|
$"{index:D3}-0x{descriptor.Address:X}-{descriptor.Width}x{descriptor.Height}" +
|
|
$"-p{sourcePitch}-f{descriptor.Format}-t{descriptor.TileMode}.linear.bin");
|
|
File.WriteAllBytes(path, source);
|
|
}
|
|
catch (IOException)
|
|
{
|
|
}
|
|
}
|
|
|
|
private static GuestDrawTexture 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 GuestSampler ToGuestSampler(IReadOnlyList<uint> descriptor) =>
|
|
descriptor.Count >= 4
|
|
? new GuestSampler(
|
|
descriptor[0],
|
|
descriptor[1],
|
|
descriptor[2],
|
|
descriptor[3])
|
|
: default;
|
|
|
|
private static byte[] ConvertRgba16FloatToRgba8(ReadOnlySpan<byte> 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;
|
|
string dispatchSource;
|
|
if (opcode == ItDispatchDirect)
|
|
{
|
|
if (packetLength < 5 ||
|
|
!TryReadUInt32(ctx, packetAddress + 16, out initiator))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
dimensionsAddress = packetAddress + 4;
|
|
dispatchSource = "direct";
|
|
}
|
|
else if (packetLength >= 4)
|
|
{
|
|
if (!TryReadUInt64(ctx, packetAddress + 4, out dimensionsAddress) ||
|
|
!TryReadUInt32(ctx, packetAddress + 12, out initiator))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
dispatchSource = "absolute-indirect";
|
|
}
|
|
else
|
|
{
|
|
if (packetLength < 3 ||
|
|
state.IndirectArgsAddress == 0 ||
|
|
!TryReadUInt32(ctx, packetAddress + 4, out var dataOffset) ||
|
|
!TryReadUInt32(ctx, packetAddress + 8, out initiator))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
dimensionsAddress = state.IndirectArgsAddress + dataOffset;
|
|
dispatchSource = "base-indirect";
|
|
}
|
|
|
|
if ((initiator & 1) == 0 ||
|
|
!TryReadUInt32(ctx, dimensionsAddress, out var dispatchEndX) ||
|
|
!TryReadUInt32(ctx, dimensionsAddress + 4, out var dispatchEndY) ||
|
|
!TryReadUInt32(ctx, dimensionsAddress + 8, out var dispatchEndZ))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (dispatchEndX == 0 || dispatchEndY == 0 || dispatchEndZ == 0)
|
|
{
|
|
return RejectComputeDispatch(
|
|
dimensionsAddress,
|
|
initiator,
|
|
dispatchSource,
|
|
dispatchEndX,
|
|
dispatchEndY,
|
|
dispatchEndZ,
|
|
"zero-dimension");
|
|
}
|
|
|
|
// When FORCE_START_AT_000 is clear, RDNA2 interprets the three packet
|
|
// values as end coordinates, not group counts. Vulkan expresses the
|
|
// same operation as vkCmdDispatchBase(base, end - base). Ignoring the
|
|
// COMPUTE_START registers turned small high-base clears into apparent
|
|
// multi-million/billion-group dispatches and forced an unsafe cap.
|
|
const uint forceStartAtZero = 1u << 2;
|
|
const uint partialThreadGroupEnabled = 1u << 1;
|
|
const uint useThreadDimensions = 1u << 5;
|
|
uint baseGroupX = 0;
|
|
uint baseGroupY = 0;
|
|
uint baseGroupZ = 0;
|
|
if ((initiator & forceStartAtZero) == 0)
|
|
{
|
|
state.ShRegisters.TryGetValue(ComputeStartX, out baseGroupX);
|
|
state.ShRegisters.TryGetValue(ComputeStartY, out baseGroupY);
|
|
state.ShRegisters.TryGetValue(ComputeStartZ, out baseGroupZ);
|
|
}
|
|
|
|
var localSizeX = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadX);
|
|
var localSizeY = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadY);
|
|
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
|
|
uint groupCountX;
|
|
uint groupCountY;
|
|
uint groupCountZ;
|
|
var threadCountX = uint.MaxValue;
|
|
var threadCountY = uint.MaxValue;
|
|
var threadCountZ = uint.MaxValue;
|
|
if ((initiator & useThreadDimensions) != 0)
|
|
{
|
|
// In thread-dimension mode the packet contains thread counts, not
|
|
// group end coordinates. Vulkan still dispatches whole workgroups,
|
|
// so round up and pass the exact exclusive thread bounds to the
|
|
// translated shader. Its entry guard disables invocations in the
|
|
// partially populated final group before any guest instruction.
|
|
var startThreadX = (ulong)baseGroupX * localSizeX;
|
|
var startThreadY = (ulong)baseGroupY * localSizeY;
|
|
var startThreadZ = (ulong)baseGroupZ * localSizeZ;
|
|
if ((ulong)dispatchEndX <= startThreadX ||
|
|
(ulong)dispatchEndY <= startThreadY ||
|
|
(ulong)dispatchEndZ <= startThreadZ)
|
|
{
|
|
return RejectComputeDispatch(
|
|
dimensionsAddress,
|
|
initiator,
|
|
dispatchSource,
|
|
dispatchEndX,
|
|
dispatchEndY,
|
|
dispatchEndZ,
|
|
$"thread-end-not-after-base(" +
|
|
$"{startThreadX}x{startThreadY}x{startThreadZ})");
|
|
}
|
|
|
|
groupCountX = CeilDivide((ulong)dispatchEndX - startThreadX, localSizeX);
|
|
groupCountY = CeilDivide((ulong)dispatchEndY - startThreadY, localSizeY);
|
|
groupCountZ = CeilDivide((ulong)dispatchEndZ - startThreadZ, localSizeZ);
|
|
threadCountX = dispatchEndX;
|
|
threadCountY = dispatchEndY;
|
|
threadCountZ = dispatchEndZ;
|
|
}
|
|
else
|
|
{
|
|
if (dispatchEndX <= baseGroupX ||
|
|
dispatchEndY <= baseGroupY ||
|
|
dispatchEndZ <= baseGroupZ)
|
|
{
|
|
return RejectComputeDispatch(
|
|
dimensionsAddress,
|
|
initiator,
|
|
dispatchSource,
|
|
dispatchEndX,
|
|
dispatchEndY,
|
|
dispatchEndZ,
|
|
$"end-not-after-base({baseGroupX}x{baseGroupY}x{baseGroupZ})");
|
|
}
|
|
|
|
groupCountX = dispatchEndX - baseGroupX;
|
|
groupCountY = dispatchEndY - baseGroupY;
|
|
groupCountZ = dispatchEndZ - baseGroupZ;
|
|
}
|
|
|
|
if ((initiator & partialThreadGroupEnabled) != 0)
|
|
{
|
|
var partialSizeX = GetComputePartialSize(state.ShRegisters, ComputeNumThreadX);
|
|
var partialSizeY = GetComputePartialSize(state.ShRegisters, ComputeNumThreadY);
|
|
var partialSizeZ = GetComputePartialSize(state.ShRegisters, ComputeNumThreadZ);
|
|
if (partialSizeX == 0 || partialSizeX > localSizeX ||
|
|
partialSizeY == 0 || partialSizeY > localSizeY ||
|
|
partialSizeZ == 0 || partialSizeZ > localSizeZ)
|
|
{
|
|
return RejectComputeDispatch(
|
|
dimensionsAddress,
|
|
initiator,
|
|
dispatchSource,
|
|
dispatchEndX,
|
|
dispatchEndY,
|
|
dispatchEndZ,
|
|
$"invalid-partial-size({partialSizeX}x{partialSizeY}x{partialSizeZ}/" +
|
|
$"{localSizeX}x{localSizeY}x{localSizeZ})");
|
|
}
|
|
|
|
if (partialSizeX != localSizeX ||
|
|
partialSizeY != localSizeY ||
|
|
partialSizeZ != localSizeZ)
|
|
{
|
|
return RejectComputeDispatch(
|
|
dimensionsAddress,
|
|
initiator,
|
|
dispatchSource,
|
|
dispatchEndX,
|
|
dispatchEndY,
|
|
dispatchEndZ,
|
|
$"unrepresentable-partial-group({partialSizeX}x{partialSizeY}x{partialSizeZ}/" +
|
|
$"{localSizeX}x{localSizeY}x{localSizeZ})");
|
|
}
|
|
}
|
|
|
|
var waveLaneCount = (initiator & (1u << 15)) != 0 ? 32u : 64u;
|
|
|
|
if (_traceAgcShader &&
|
|
((ulong)groupCountX * groupCountY * groupCountZ >= 1_000_000UL ||
|
|
groupCountX >= 1_000_000u))
|
|
{
|
|
lock (_submitTraceGate)
|
|
{
|
|
if (_tracedDispatchArguments.Add(
|
|
(dimensionsAddress, groupCountX, groupCountY, groupCountZ)))
|
|
{
|
|
TraceAgcShader(
|
|
$"agc.dispatch_args source={dispatchSource} op=0x{opcode:X2} " +
|
|
$"queue={state.QueueName} submission={state.ActiveSubmissionId} " +
|
|
$"packet=0x{packetAddress:X16} len={packetLength} " +
|
|
$"dims=0x{dimensionsAddress:X16} " +
|
|
$"raw={dispatchEndX:X8}/{dispatchEndY:X8}/{dispatchEndZ:X8} " +
|
|
$"base={baseGroupX:X8}/{baseGroupY:X8}/{baseGroupZ:X8} " +
|
|
$"count={groupCountX:X8}/{groupCountY:X8}/{groupCountZ:X8} " +
|
|
$"wave={waveLaneCount} " +
|
|
$"initiator=0x{initiator:X8} " +
|
|
$"indirect_base=0x{state.IndirectArgsAddress:X16}");
|
|
}
|
|
}
|
|
}
|
|
|
|
dispatch = new ComputeDispatch(
|
|
groupCountX,
|
|
groupCountY,
|
|
groupCountZ,
|
|
baseGroupX,
|
|
baseGroupY,
|
|
baseGroupZ,
|
|
waveLaneCount,
|
|
IsIndirect: opcode == ItDispatchIndirect,
|
|
threadCountX,
|
|
threadCountY,
|
|
threadCountZ);
|
|
return true;
|
|
}
|
|
|
|
private static uint CeilDivide(ulong value, uint divisor) =>
|
|
checked((uint)((value + divisor - 1) / divisor));
|
|
|
|
private static bool RejectComputeDispatch(
|
|
ulong dimensionsAddress,
|
|
uint initiator,
|
|
string source,
|
|
uint rawX,
|
|
uint rawY,
|
|
uint rawZ,
|
|
string reason)
|
|
{
|
|
lock (_submitTraceGate)
|
|
{
|
|
if (_rejectedDispatchArguments.Count < 256 &&
|
|
_rejectedDispatchArguments.Add((dimensionsAddress, initiator, reason)))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][WARN] agc.dispatch_reject source={source} " +
|
|
$"dims=0x{dimensionsAddress:X16} raw={rawX:X8}/{rawY:X8}/{rawZ:X8} " +
|
|
$"initiator=0x{initiator:X8} reason={reason}");
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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<string>(bindings.Count);
|
|
var translatedBindings = new List<TranslatedImageBinding>(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);
|
|
if (_traceComputeShaderAddress == shaderAddress)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][TRACE] agc.compute_dispatch_trace seq={sequence} " +
|
|
$"cs=0x{shaderAddress:X16} " +
|
|
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
|
|
$"base={dispatch.BaseGroupX}x{dispatch.BaseGroupY}x{dispatch.BaseGroupZ} " +
|
|
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
|
|
$"bindings=[{string.Join(',', descriptions)}]");
|
|
}
|
|
|
|
var writesGlobalMemory = evaluation.GlobalMemoryBindings.Any(static binding =>
|
|
binding.Writable);
|
|
var gpuDispatch = false;
|
|
var evaluationHandledByCpu = false;
|
|
var computeError = string.Empty;
|
|
if (!hasStorageBinding &&
|
|
writesGlobalMemory &&
|
|
TrySubmitMaskedDwordCopyKernel(
|
|
ctx,
|
|
shaderState.Program,
|
|
evaluation,
|
|
dispatch,
|
|
localSizeX,
|
|
localSizeY,
|
|
localSizeZ,
|
|
out var semanticCopySequence,
|
|
out var copyDescription))
|
|
{
|
|
gpuDispatch = true;
|
|
evaluationHandledByCpu = true;
|
|
TraceAgcShader(
|
|
$"agc.compute_semantic_fast_path cs=0x{shaderAddress:X16} " +
|
|
$"queue={state.QueueName} submission={state.ActiveSubmissionId} " +
|
|
copyDescription);
|
|
// The scalar evaluator snapshots guest buffers while parsing the
|
|
// command stream. Do not let another submission (or the CPU)
|
|
// observe that snapshot until the semantic replacement has
|
|
// reached the same CPU-visible completion point as a translated
|
|
// writable-buffer dispatch below. Returning early here allowed
|
|
// the guest to reuse a transient heap while its delayed clear was
|
|
// still queued, so the clear could erase newly constructed CPU
|
|
// objects. Waiting on the work sequence also retires preceding
|
|
// Vulkan writes before the next evaluator snapshot is captured.
|
|
if (!VulkanVideoPresenter.WaitForGuestWork(semanticCopySequence))
|
|
{
|
|
computeError =
|
|
$"semantic-global-write-sync-timeout sequence={semanticCopySequence}";
|
|
}
|
|
}
|
|
else if ((hasStorageBinding || writesGlobalMemory) &&
|
|
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
|
|
{
|
|
var shaderKey = (
|
|
shaderAddress,
|
|
_bakeScalars
|
|
? ComputeShaderStateFingerprint(evaluation)
|
|
: ComputeShaderStructuralFingerprint(evaluation),
|
|
localSizeX,
|
|
localSizeY,
|
|
localSizeZ,
|
|
dispatch.WaveLaneCount,
|
|
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment);
|
|
var guestGlobalBufferCount = evaluation.GlobalMemoryBindings.Count;
|
|
var totalGlobalBufferCount = _bakeScalars
|
|
? guestGlobalBufferCount
|
|
: guestGlobalBufferCount + 1;
|
|
_computeShaderCache.TryGetValue(shaderKey, out var computeShader);
|
|
|
|
if (computeShader is null &&
|
|
GuestGpu.Current.TryCompileComputeShader(
|
|
shaderState,
|
|
evaluation,
|
|
localSizeX,
|
|
localSizeY,
|
|
localSizeZ,
|
|
out computeShader,
|
|
out computeError,
|
|
totalGlobalBufferCount,
|
|
initialScalarBufferIndex: _bakeScalars
|
|
? -1
|
|
: guestGlobalBufferCount,
|
|
waveLaneCount: dispatch.WaveLaneCount,
|
|
storageBufferOffsetAlignment:
|
|
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment))
|
|
{
|
|
DumpCompiledShader(
|
|
"cs",
|
|
shaderAddress,
|
|
shaderKey.Item2,
|
|
computeShader!,
|
|
shaderState.Program);
|
|
}
|
|
|
|
if (computeShader is not null)
|
|
{
|
|
_computeShaderCache.TryAdd(shaderKey, computeShader);
|
|
|
|
var textures = CreateGuestDrawTextures(
|
|
ctx,
|
|
translatedBindings,
|
|
out _);
|
|
var globalMemoryBuffers =
|
|
CreateTranslatedComputeGlobalBuffers(evaluation);
|
|
var workSequence = GuestGpu.Current.SubmitComputeDispatch(
|
|
shaderAddress,
|
|
computeShader,
|
|
textures,
|
|
globalMemoryBuffers,
|
|
dispatch.GroupCountX,
|
|
dispatch.GroupCountY,
|
|
dispatch.GroupCountZ,
|
|
dispatch.BaseGroupX,
|
|
dispatch.BaseGroupY,
|
|
dispatch.BaseGroupZ,
|
|
localSizeX,
|
|
localSizeY,
|
|
localSizeZ,
|
|
dispatch.IsIndirect,
|
|
writesGlobalMemory,
|
|
dispatch.ThreadCountX,
|
|
dispatch.ThreadCountY,
|
|
dispatch.ThreadCountZ);
|
|
gpuDispatch = true;
|
|
if (writesGlobalMemory &&
|
|
!VulkanVideoPresenter.WaitForGuestWork(workSequence))
|
|
{
|
|
computeError = $"global-write-sync-timeout sequence={workSequence}";
|
|
}
|
|
}
|
|
}
|
|
|
|
const int blitCount = 0;
|
|
|
|
lock (_submitTraceGate)
|
|
{
|
|
if (_tracedComputeShaders.Add(shaderAddress))
|
|
{
|
|
var globalBuffers = evaluation.GlobalMemoryBindings.Count == 0
|
|
? string.Empty
|
|
: $" global_buffers=[{string.Join(',', evaluation.GlobalMemoryBindings.Select(
|
|
binding => $"0x{binding.BaseAddress:X16}:{binding.DataLength}"))}]";
|
|
var scalarProbe = string.Join(
|
|
',',
|
|
evaluation.InitialScalarRegisters
|
|
.Take(16)
|
|
.Select((value, index) => $"s{index}={value:X8}"));
|
|
var globalProbes = evaluation.GlobalMemoryBindings.Count == 0
|
|
? string.Empty
|
|
: $" global_heads=[{string.Join(',', evaluation.GlobalMemoryBindings.Select(
|
|
binding =>
|
|
$"0x{binding.BaseAddress:X16}:" +
|
|
Convert.ToHexString(binding.Data.AsSpan(
|
|
0,
|
|
Math.Min(binding.DataLength, 16)))))}]";
|
|
var globalDescriptors = evaluation.GlobalMemoryBindings.Count == 0
|
|
? string.Empty
|
|
: $" global_descriptors=[{string.Join(',', evaluation.GlobalMemoryBindings.Select(
|
|
binding =>
|
|
$"s{binding.ScalarAddress}=" +
|
|
string.Join(':', evaluation.ScalarRegisters
|
|
.Skip(checked((int)binding.ScalarAddress))
|
|
.Take(4)
|
|
.Select(value => $"{value:X8}"))))}]";
|
|
var opcodes = string.Join(
|
|
',',
|
|
shaderState.Program.Instructions
|
|
.Select(instruction => instruction.Opcode)
|
|
.Distinct()
|
|
.Take(48));
|
|
TraceAgcShader(
|
|
$"agc.compute_shader cs=0x{shaderAddress:X16} " +
|
|
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
|
|
$"base={dispatch.BaseGroupX}x{dispatch.BaseGroupY}x{dispatch.BaseGroupZ} " +
|
|
$"wave={dispatch.WaveLaneCount} " +
|
|
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
|
|
$"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " +
|
|
$"gpu={gpuDispatch} blits={blitCount} globals={evaluation.GlobalMemoryBindings.Count} " +
|
|
$"global_writes={writesGlobalMemory}" +
|
|
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
|
|
$" sgprs=[{scalarProbe}]" +
|
|
globalBuffers +
|
|
globalProbes +
|
|
globalDescriptors +
|
|
$" opcodes=[{opcodes}]" +
|
|
$" bindings=[{string.Join(',', descriptions)}]");
|
|
}
|
|
}
|
|
|
|
if (evaluationHandledByCpu)
|
|
{
|
|
ReturnPooledEvaluationArrays(evaluation);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recognizes the SDK's masked-dword resource initialization kernel and
|
|
/// executes its exact semantics over the guest-memory window that the
|
|
/// emulator can map. The guest dispatches this kernel over multi-gigabyte
|
|
/// virtual heaps (up to ~67 million 64-lane workgroups); translating every
|
|
/// out-of-window invocation to Vulkan dominated startup despite those
|
|
/// stores being bounds-discarded. This is a semantic kernel replacement,
|
|
/// not a generic dispatch cap: the complete instruction shape and SGPR
|
|
/// bindings must match before the ordered CPU action is used.
|
|
/// </summary>
|
|
private static bool TrySubmitMaskedDwordCopyKernel(
|
|
CpuContext ctx,
|
|
Gen5ShaderProgram program,
|
|
Gen5ShaderEvaluation evaluation,
|
|
ComputeDispatch dispatch,
|
|
uint localSizeX,
|
|
uint localSizeY,
|
|
uint localSizeZ,
|
|
out long workSequence,
|
|
out string description)
|
|
{
|
|
workSequence = 0;
|
|
description = string.Empty;
|
|
var instructions = program.Instructions;
|
|
string[] expectedOpcodes =
|
|
[
|
|
"SMovB32",
|
|
"STtraceData",
|
|
"SInstPrefetch",
|
|
"VLshlAddU32",
|
|
"SBufferLoadDword",
|
|
"SWaitcnt",
|
|
"VCmpxGtU32",
|
|
"SCbranchExecz",
|
|
"SBufferLoadDword",
|
|
"SWaitcnt",
|
|
"VAndB32",
|
|
"BufferLoadFormatX",
|
|
"SWaitcnt",
|
|
"BufferStoreFormatX",
|
|
"SEndpgm",
|
|
];
|
|
if (instructions.Count != expectedOpcodes.Length ||
|
|
!instructions.Select(static instruction => instruction.Opcode)
|
|
.SequenceEqual(expectedOpcodes) ||
|
|
!IsExactMaskedDwordCopyInstructionShape(instructions) ||
|
|
dispatch.BaseGroupX != 0 ||
|
|
dispatch.BaseGroupY != 0 ||
|
|
dispatch.BaseGroupZ != 0 ||
|
|
dispatch.GroupCountY != 1 ||
|
|
dispatch.GroupCountZ != 1 ||
|
|
localSizeX != 64 ||
|
|
localSizeY != 1 ||
|
|
localSizeZ != 1 ||
|
|
evaluation.ComputeSystemRegisters?.WorkGroupXRegister != 12)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var control = evaluation.GlobalMemoryBindings.SingleOrDefault(
|
|
static binding => binding.ScalarAddress == 8 && !binding.Writable);
|
|
var source = evaluation.GlobalMemoryBindings.SingleOrDefault(
|
|
static binding => binding.ScalarAddress == 0 && !binding.Writable);
|
|
var destination = evaluation.GlobalMemoryBindings.SingleOrDefault(
|
|
static binding => binding.ScalarAddress == 4 &&
|
|
binding.Writable &&
|
|
binding.WriteBackToGuest);
|
|
if (control is null || source is null || destination is null ||
|
|
control.DataLength < 2 * sizeof(uint) ||
|
|
source.DataLength < sizeof(uint) ||
|
|
destination.BaseAddress == 0 ||
|
|
destination.DataLength < sizeof(uint) ||
|
|
!IsExactMaskedDwordCopyDescriptor(
|
|
evaluation.InitialScalarRegisters,
|
|
source.ScalarAddress,
|
|
source.BaseAddress) ||
|
|
!IsExactMaskedDwordCopyDescriptor(
|
|
evaluation.InitialScalarRegisters,
|
|
destination.ScalarAddress,
|
|
destination.BaseAddress))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var elementCount = BinaryPrimitives.ReadUInt32LittleEndian(
|
|
control.Data.AsSpan(0, sizeof(uint)));
|
|
var sourceMask = BinaryPrimitives.ReadUInt32LittleEndian(
|
|
control.Data.AsSpan(sizeof(uint), sizeof(uint)));
|
|
var dispatchedThreads = dispatch.ThreadCountX != uint.MaxValue
|
|
? dispatch.ThreadCountX
|
|
: Math.Min(
|
|
(ulong)uint.MaxValue,
|
|
(ulong)dispatch.GroupCountX * localSizeX);
|
|
var writableDwords = (uint)(destination.DataLength / sizeof(uint));
|
|
var outputDwords = (uint)Math.Min(
|
|
Math.Min((ulong)elementCount, dispatchedThreads),
|
|
writableDwords);
|
|
if (outputDwords == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var output = new byte[checked((int)outputDwords * sizeof(uint))];
|
|
var outputWords = System.Runtime.InteropServices.MemoryMarshal.Cast<byte, uint>(
|
|
output.AsSpan());
|
|
if (sourceMask == 0)
|
|
{
|
|
outputWords.Fill(BinaryPrimitives.ReadUInt32LittleEndian(
|
|
source.Data.AsSpan(0, sizeof(uint))));
|
|
}
|
|
else
|
|
{
|
|
var sourceWords = System.Runtime.InteropServices.MemoryMarshal.Cast<byte, uint>(
|
|
source.Data.AsSpan(0, source.DataLength - (source.DataLength % sizeof(uint))));
|
|
for (uint index = 0; index < outputDwords; index++)
|
|
{
|
|
var sourceIndex = index & sourceMask;
|
|
outputWords[(int)index] = sourceIndex < (uint)sourceWords.Length
|
|
? sourceWords[(int)sourceIndex]
|
|
: 0;
|
|
}
|
|
}
|
|
|
|
var destinationAddress = destination.BaseAddress;
|
|
workSequence = VulkanVideoPresenter.SubmitOrderedGuestAction(
|
|
() =>
|
|
{
|
|
if (!ctx.Memory.TryWrite(destinationAddress, output))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[LOADER][ERROR] AGC masked-copy fast path failed " +
|
|
$"dst=0x{destinationAddress:X16} bytes={output.Length}");
|
|
return;
|
|
}
|
|
|
|
GuestImageWriteTracker.Track(
|
|
destinationAddress,
|
|
(ulong)output.Length,
|
|
VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics,
|
|
"agc.masked-dword-copy");
|
|
},
|
|
$"masked_dword_copy dst=0x{destinationAddress:X16} bytes={output.Length}");
|
|
description =
|
|
$"dst=0x{destinationAddress:X16} bytes={output.Length} " +
|
|
$"elements={elementCount} mask=0x{sourceMask:X8} " +
|
|
$"dispatch={dispatch.GroupCountX}x{localSizeX}";
|
|
return workSequence > 0;
|
|
}
|
|
|
|
private static bool IsExactMaskedDwordCopyInstructionShape(
|
|
IReadOnlyList<Gen5ShaderInstruction> instructions)
|
|
{
|
|
static bool IsOperand(
|
|
Gen5Operand operand,
|
|
Gen5OperandKind kind,
|
|
uint value) =>
|
|
operand.Kind == kind && operand.Value == value;
|
|
|
|
static bool IsBufferControl(
|
|
Gen5ShaderInstruction instruction,
|
|
uint vectorAddress,
|
|
uint vectorData,
|
|
uint scalarResource) =>
|
|
instruction.Control is Gen5BufferMemoryControl
|
|
{
|
|
DwordCount: 1,
|
|
OffsetBytes: 0,
|
|
IndexEnabled: true,
|
|
OffsetEnabled: false,
|
|
} control &&
|
|
control.VectorAddress == vectorAddress &&
|
|
control.VectorData == vectorData &&
|
|
control.ScalarResource == scalarResource;
|
|
|
|
static bool IsScalarLoad(
|
|
Gen5ShaderInstruction instruction,
|
|
int offsetBytes) =>
|
|
instruction.Control is Gen5ScalarMemoryControl
|
|
{
|
|
DestinationCount: 1,
|
|
DynamicOffsetRegister: null,
|
|
} control &&
|
|
control.ImmediateOffsetBytes == offsetBytes &&
|
|
instruction.Destinations.Count == 1 &&
|
|
IsOperand(
|
|
instruction.Destinations[0],
|
|
Gen5OperandKind.ScalarRegister,
|
|
106) &&
|
|
instruction.Sources.Count >= 1 &&
|
|
IsOperand(
|
|
instruction.Sources[0],
|
|
Gen5OperandKind.ScalarRegister,
|
|
8);
|
|
|
|
// This replacement depends on the operands as much as the opcode
|
|
// sequence. Reversing V_CMPX_GT or enabling offen on either MUBUF
|
|
// operation changes the set or address of written lanes.
|
|
var globalId = instructions[3];
|
|
var compare = instructions[6];
|
|
var sourceIndex = instructions[10];
|
|
var load = instructions[11];
|
|
var store = instructions[13];
|
|
return
|
|
globalId.Destinations.Count == 1 &&
|
|
IsOperand(globalId.Destinations[0], Gen5OperandKind.VectorRegister, 0) &&
|
|
globalId.Sources.Count == 3 &&
|
|
IsOperand(globalId.Sources[0], Gen5OperandKind.ScalarRegister, 12) &&
|
|
IsOperand(globalId.Sources[1], Gen5OperandKind.EncodedConstant, 134) &&
|
|
IsOperand(globalId.Sources[2], Gen5OperandKind.VectorRegister, 0) &&
|
|
IsScalarLoad(instructions[4], offsetBytes: 0) &&
|
|
compare.Sources.Count == 2 &&
|
|
IsOperand(compare.Sources[0], Gen5OperandKind.ScalarRegister, 106) &&
|
|
IsOperand(compare.Sources[1], Gen5OperandKind.VectorRegister, 0) &&
|
|
instructions[7].Words.Count == 1 &&
|
|
(instructions[7].Words[0] & 0xFFFFu) == 9 &&
|
|
IsScalarLoad(instructions[8], offsetBytes: sizeof(uint)) &&
|
|
sourceIndex.Destinations.Count == 1 &&
|
|
IsOperand(sourceIndex.Destinations[0], Gen5OperandKind.VectorRegister, 1) &&
|
|
sourceIndex.Sources.Count == 2 &&
|
|
IsOperand(sourceIndex.Sources[0], Gen5OperandKind.ScalarRegister, 106) &&
|
|
IsOperand(sourceIndex.Sources[1], Gen5OperandKind.VectorRegister, 0) &&
|
|
IsBufferControl(load, vectorAddress: 1, vectorData: 1, scalarResource: 0) &&
|
|
IsBufferControl(store, vectorAddress: 0, vectorData: 1, scalarResource: 4);
|
|
}
|
|
|
|
private static bool IsExactMaskedDwordCopyDescriptor(
|
|
IReadOnlyList<uint> scalarRegisters,
|
|
uint scalarBase,
|
|
ulong expectedBaseAddress)
|
|
{
|
|
if (scalarBase + 3 >= scalarRegisters.Count)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var word0 = scalarRegisters[(int)scalarBase];
|
|
var word1 = scalarRegisters[(int)scalarBase + 1];
|
|
var word3 = scalarRegisters[(int)scalarBase + 3];
|
|
var baseAddress = word0 | ((ulong)(word1 & 0xFFFFu) << 32);
|
|
var stride = (word1 >> 16) & 0x3FFFu;
|
|
var cacheSwizzle = (word1 & (1u << 30)) != 0;
|
|
var swizzleEnabled = (word1 & (1u << 31)) != 0;
|
|
var unifiedFormat = (word3 >> 12) & 0x7Fu;
|
|
var addTidEnabled = (word3 & (1u << 23)) != 0;
|
|
var outOfBoundsSelect = (word3 >> 28) & 0x3u;
|
|
var type = word3 >> 30;
|
|
var dstSelectX = word3 & 0x7u;
|
|
|
|
// RDNA2 tables 35 and 37: OOB_SELECT=0 is structured indexing, so
|
|
// NUM_RECORDS counts stride-sized records. FORMAT=20 is 32_UINT and
|
|
// dst_sel_x=4 selects its R component. ADD_TID and either swizzle bit
|
|
// alter addressing and are therefore outside this replacement.
|
|
return baseAddress == expectedBaseAddress &&
|
|
stride == sizeof(uint) &&
|
|
!cacheSwizzle &&
|
|
!swizzleEnabled &&
|
|
unifiedFormat == 20 &&
|
|
!addTidEnabled &&
|
|
outOfBoundsSelect == 0 &&
|
|
type == 0 &&
|
|
dstSelectX == 4;
|
|
}
|
|
|
|
private static Gen5ComputeSystemRegisters DecodeComputeSystemRegisters(
|
|
IReadOnlyDictionary<uint, uint> 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<uint, uint> registers)
|
|
{
|
|
// RSRC2 is the authoritative stage selector: its USER_SGPR field
|
|
// describes the hardware SGPR window even when the shader has zero
|
|
// user-data dwords and therefore no USER_DATA register was written.
|
|
// GFX10 NGG export shaders use the GS user-data bank (RSRC2 at 0x8B),
|
|
// while their program address is carried in the ES/NGG registers.
|
|
// Looking only for a populated USER_DATA range made those shaders
|
|
// fall through to ES (0xCC) and reject every graphics draw because
|
|
// the unrelated ES RSRC2 register at 0xCB was legitimately absent.
|
|
if (HasShaderResource2(registers, GsUserDataRegister))
|
|
{
|
|
return GsUserDataRegister;
|
|
}
|
|
|
|
if (HasShaderResource2(registers, EsUserDataRegister))
|
|
{
|
|
return EsUserDataRegister;
|
|
}
|
|
|
|
if (HasShaderResource2(registers, VsUserDataRegister))
|
|
{
|
|
return VsUserDataRegister;
|
|
}
|
|
|
|
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 HasShaderResource2(
|
|
IReadOnlyDictionary<uint, uint> registers,
|
|
uint userDataBaseRegister) =>
|
|
registers.ContainsKey(userDataBaseRegister - 1);
|
|
|
|
private static bool HasUserDataRange(
|
|
IReadOnlyDictionary<uint, uint> registers,
|
|
uint startRegister)
|
|
{
|
|
for (var index = 0u; index < 16; index++)
|
|
{
|
|
if (registers.ContainsKey(startRegister + index))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static int CountUserDataValues(
|
|
IReadOnlyDictionary<uint, uint> 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<uint, uint> registers,
|
|
uint register)
|
|
{
|
|
return registers.TryGetValue(register, out var value)
|
|
? Math.Max(value & 0xFFFFu, 1u)
|
|
: 1u;
|
|
}
|
|
|
|
private static uint GetComputePartialSize(
|
|
IReadOnlyDictionary<uint, uint> registers,
|
|
uint register) =>
|
|
registers.TryGetValue(register, out var value)
|
|
? value >> 16
|
|
: 0u;
|
|
|
|
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 &&
|
|
GuestGpu.Current.TrySubmitGuestImageBlit(
|
|
cachedSourceTexture.Address,
|
|
cachedSourceTexture.Width,
|
|
cachedSourceTexture.Height,
|
|
cachedSourceTexture.Format,
|
|
cachedSourceTexture.NumberType,
|
|
texture.Address,
|
|
texture.Width,
|
|
texture.Height,
|
|
texture.Format,
|
|
texture.NumberType))
|
|
{
|
|
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,
|
|
_ => 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 = (ulong)GetBlockCompressedBlockBytes(format);
|
|
return blockBytes == 0
|
|
? 0
|
|
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
|
|
}
|
|
|
|
private static uint GetLinearTexturePitch(uint pitch, uint height, uint format)
|
|
{
|
|
var bytesPerTexel = GetTextureBytesPerTexel(format);
|
|
if (bytesPerTexel == 0 || height == 0)
|
|
{
|
|
return pitch;
|
|
}
|
|
|
|
// GNM linear surfaces align the row pitch to 256 bytes, so a 32px
|
|
// RGBA8 texture is stored with a 64px (256-byte) pitch and a 288px
|
|
// one with 320px. Reading at the unpadded width made every padded
|
|
// tail land on the next row, which showed as transparent gaps every
|
|
// other row on small tiles and diagonal dashes on wider surfaces.
|
|
var pitchBytes = AlignUp((ulong)pitch * bytesPerTexel, 256UL);
|
|
return checked((uint)(pitchBytes / bytesPerTexel));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Translation failures are compatibility issues, not merely verbose
|
|
// shader diagnostics. Report each distinct failure once even when AGC
|
|
// tracing is disabled so normal runs preserve the missing opcode or
|
|
// unsupported translation reason needed to fix the game.
|
|
if (firstFailure)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[COMPAT][SHADER] ps=0x{pixelShaderAddress:X16} " +
|
|
$"es=0x{exportShaderAddress:X16} error={translationError}");
|
|
}
|
|
|
|
if ((!hasPixelShader || !hasPsInputEna || !hasPsInputAddr) &&
|
|
TryMarkMissingPixelShaderBindingsTrace())
|
|
{
|
|
TraceAgcShader(
|
|
$"agc.shader_register_candidates " +
|
|
DescribeShaderRegisterCandidates(ctx, state.ShRegisters));
|
|
}
|
|
|
|
if (!hasPixelShader)
|
|
{
|
|
state.CxRegisters.TryGetValue(DbDepthControl, out var rawDepthControl);
|
|
state.CxRegisters.TryGetValue(DbZInfo, out var rawZInfo);
|
|
state.CxRegisters.TryGetValue(DbDepthSizeXy, out var rawDepthSize);
|
|
state.CxRegisters.TryGetValue(DbDepthView, out var rawDepthView);
|
|
var depthState = DecodeDepthState(state.CxRegisters);
|
|
var depthTarget = DecodeDepthTarget(state.CxRegisters);
|
|
TraceAgcShader(
|
|
$"agc.shader_depth_state control=0x{rawDepthControl:X8} " +
|
|
$"zinfo=0x{rawZInfo:X8} size=0x{rawDepthSize:X8} " +
|
|
$"view=0x{rawDepthView:X8} " +
|
|
$"test={(depthState.TestEnable ? 1 : 0)} " +
|
|
$"write={(depthState.WriteEnable ? 1 : 0)} " +
|
|
$"func={depthState.CompareOp} " +
|
|
(depthTarget is null
|
|
? "target=none"
|
|
: $"target=0x{depthTarget.Address:X16}:" +
|
|
$"{depthTarget.Width}x{depthTarget.Height}:" +
|
|
$"fmt{depthTarget.GuestFormat}/sw{depthTarget.SwizzleMode}:" +
|
|
$"ro={(depthTarget.ReadOnly ? 1 : 0)}"));
|
|
}
|
|
|
|
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.DataLength} " +
|
|
$"pcs={string.Join(',', binding.InstructionPcs.Select(pc => $"0x{pc:X}"))}");
|
|
}
|
|
|
|
if (GuestGpu.Current.TryCompilePixelShader(
|
|
pixelState,
|
|
evaluation,
|
|
[new(0, 0, Gen5PixelOutputKind.Float)],
|
|
out var compiledPixel,
|
|
out var compileError,
|
|
pixelInputEnable: psInputEna,
|
|
pixelInputAddress: psInputAddr,
|
|
storageBufferOffsetAlignment:
|
|
VulkanVideoPresenter.GuestStorageBufferOffsetAlignment))
|
|
{
|
|
TraceAgcShader(
|
|
$"agc.shader_spirv ps=0x{pixelShaderAddress:X16} " +
|
|
$"bytes={compiledPixel!.Payload.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<uint, uint> 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 = TryReadByte(
|
|
ctx,
|
|
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<uint, uint> 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 ||
|
|
!TryReadUInt32(ctx, 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<uint> fields = stackalloc uint[8];
|
|
for (var i = 0; i < fields.Length; i++)
|
|
{
|
|
if (!TryReadUInt32(ctx, descriptorAddress + ((ulong)i * sizeof(uint)), out fields[i]))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return TryDecodeTextureDescriptor(fields.ToArray(), out descriptor);
|
|
}
|
|
|
|
private static bool TryDecodeTextureDescriptor(
|
|
IReadOnlyList<uint> fields,
|
|
out TextureDescriptor descriptor)
|
|
{
|
|
descriptor = default;
|
|
if (fields.Count < 4)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// RDNA2 ISA table 45: BASE_ADDRESS is addr[47:8], WIDTH is the full
|
|
// 16-bit field split across word1/word2, and HEIGHT is word2[29:14].
|
|
// Keeping the high base byte is required for legal guest VAs above
|
|
// 1 TiB; it is not descriptor metadata.
|
|
var address = (((ulong)(fields[1] & 0xFFu) << 32) | fields[0]) << 8;
|
|
var width = (((fields[1] >> 30) & 0x3u) | ((fields[2] & 0x3FFFu) << 2)) + 1;
|
|
var height = ((fields[2] >> 14) & 0xFFFFu) + 1;
|
|
var unifiedFormat = (fields[1] >> 20) & 0x1FFu;
|
|
if (unifiedFormat == 0 ||
|
|
!Gfx10UnifiedFormat.TryDecode(
|
|
unifiedFormat,
|
|
out var format,
|
|
out var numberType))
|
|
{
|
|
return false;
|
|
}
|
|
var tileMode = (fields[3] >> 20) & 0x1Fu;
|
|
var type = (fields[3] >> 28) & 0xFu;
|
|
var baseLevel = (fields[3] >> 12) & 0xFu;
|
|
var lastLevel = (fields[3] >> 16) & 0xFu;
|
|
var bcSwizzle = (fields[3] >> 25) & 0x7u;
|
|
var hasExtendedDescriptor = fields.Count >= 8;
|
|
var word4 = fields.Count >= 5 ? fields[4] : 0u;
|
|
var depthOrLastSlice = (word4 & 0x1FFFu) + 1;
|
|
var baseArray = (word4 >> 16) & 0x1FFFu;
|
|
// In a 256-bit 1D/2D/2D-MSAA descriptor word4[13:0] is
|
|
// (pitch-1). A zeroed upper half denotes the common 128-bit resource,
|
|
// where pitch is implicit; use width rather than inventing pitch=1.
|
|
var pitch = type is 8u or 9u or 14u && word4 != 0
|
|
? (word4 & 0x3FFFu) + 1
|
|
: width;
|
|
var depth = type is 10u or 11u or 12u or 13u or 15u
|
|
? depthOrLastSlice
|
|
: 1u;
|
|
var word5 = fields.Count >= 6 ? fields[5] : 0u;
|
|
var arrayPitch = word5 & 0xFu;
|
|
var maxMip = (word5 >> 4) & 0xFu;
|
|
var minLod = (fields[1] >> 8) & 0xFFFu;
|
|
var minLodWarn = (word5 >> 8) & 0xFFFu;
|
|
var word6 = fields.Count >= 7 ? fields[6] : 0u;
|
|
var word7 = fields.Count >= 8 ? fields[7] : 0u;
|
|
var metadataAddress = ((((ulong)word7 << 8) | (word6 >> 24)) << 8);
|
|
var descriptorFlags = word6 & 0x00FF_FFFFu;
|
|
var dstSelect = fields[3] & 0xFFFu;
|
|
if (address == 0 || width == 0 || height == 0 || type is >= 1 and <= 7)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
descriptor = new TextureDescriptor(
|
|
address,
|
|
width,
|
|
height,
|
|
format,
|
|
numberType,
|
|
tileMode,
|
|
type,
|
|
baseLevel,
|
|
lastLevel,
|
|
pitch,
|
|
dstSelect,
|
|
depth,
|
|
baseArray,
|
|
arrayPitch,
|
|
maxMip,
|
|
minLod,
|
|
minLodWarn,
|
|
bcSwizzle,
|
|
metadataAddress,
|
|
descriptorFlags,
|
|
hasExtendedDescriptor);
|
|
return true;
|
|
}
|
|
|
|
private static TextureDescriptor CreateFallbackTextureDescriptor(IReadOnlyList<uint> fields)
|
|
{
|
|
var format = Gen5TextureFormatR8G8B8A8Unorm;
|
|
var numberType = 0u;
|
|
var tileMode = 0u;
|
|
if (fields.Count >= 4)
|
|
{
|
|
var unifiedFormat = (fields[1] >> 20) & 0x1FFu;
|
|
if (!Gfx10UnifiedFormat.TryDecode(
|
|
unifiedFormat,
|
|
out format,
|
|
out numberType))
|
|
{
|
|
format = Gen5TextureFormatR8G8B8A8Unorm;
|
|
numberType = 0;
|
|
}
|
|
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
|
|
VideoOutPixelFormat2R8G8B8A8Srgb or
|
|
VideoOutPixelFormat2B8G8R8A8Srgb or
|
|
VideoOutPixelFormat2R10G10B10A2 or
|
|
VideoOutPixelFormat2B10G10R10A2 or
|
|
VideoOutPixelFormat2R10G10B10A2Srgb or
|
|
VideoOutPixelFormat2B10G10R10A2Srgb or
|
|
VideoOutPixelFormat2R10G10B10A2Bt2100Pq or
|
|
VideoOutPixelFormat2B10G10R10A2Bt2100Pq))
|
|
{
|
|
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
|
|
VideoOutPixelFormat2R8G8B8A8Srgb;
|
|
var packed10Destination =
|
|
VideoOutExports.IsPacked10BitPixelFormat(destination.PixelFormat);
|
|
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 (packed10Destination)
|
|
{
|
|
if (!VideoOutExports.TryPackRgba8Pixel(
|
|
destination.PixelFormat,
|
|
sourceBytes[sourceOffset + 0],
|
|
sourceBytes[sourceOffset + 1],
|
|
sourceBytes[sourceOffset + 2],
|
|
sourceBytes[sourceOffset + 3],
|
|
out var packed))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
|
destinationRow.AsSpan(destinationOffset, sizeof(uint)),
|
|
packed);
|
|
}
|
|
else 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];
|
|
}
|
|
|
|
if (!packed10Destination)
|
|
{
|
|
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<byte> 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 (!TryReadUInt32(ctx, 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 ||
|
|
!TryReadUInt32(ctx, packetAddress + 4, out var registerCount) ||
|
|
!TryReadUInt64(ctx, 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 (!TryReadUInt32(ctx, entryAddress, out var registerOffset) ||
|
|
!TryReadUInt32(ctx, 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 (!TryReadUInt64(ctx, headerAddress + ShaderShRegistersOffset, out var shRegistersAddress) ||
|
|
!TryReadByte(ctx, headerAddress + ShaderTypeOffset, out var shaderType) ||
|
|
!TryReadByte(ctx, headerAddress + ShaderNumShRegistersOffset, out var registerCount))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (shRegistersAddress == 0 || registerCount < 2)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!TryReadUInt32(ctx, shRegistersAddress, out var loRegister) ||
|
|
!TryReadUInt32(ctx, 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 TryWriteUInt32(ctx, shRegistersAddress + sizeof(uint), loValue) &&
|
|
TryWriteUInt32(ctx, shRegistersAddress + 8 + sizeof(uint), hiValue);
|
|
}
|
|
|
|
private static bool IsEsGeometryShaderType(byte shaderType) =>
|
|
shaderType is 2 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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
if (!TryWriteUInt32(ctx, commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(registersAddress >> 32)))
|
|
{
|
|
return SetReturn(ctx, 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 PatchWriteDataControlByte(CpuContext ctx, int byteIndex)
|
|
{
|
|
if (!TryResolveWriteDataPatchArguments(
|
|
ctx,
|
|
ctx[CpuRegister.Rdi],
|
|
ctx[CpuRegister.Rsi],
|
|
out var commandAddress,
|
|
out var value) ||
|
|
!TryReadUInt32(ctx, commandAddress + 4, out var control))
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
var shift = byteIndex * 8;
|
|
var patchedControl = (control & ~(0xFFu << shift)) | (((uint)value & 0xFFu) << shift);
|
|
return TryWriteUInt32(ctx, commandAddress + 4, patchedControl)
|
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
}
|
|
|
|
private static bool TryResolveWriteDataPatchArguments(
|
|
CpuContext ctx,
|
|
ulong first,
|
|
ulong second,
|
|
out ulong commandAddress,
|
|
out ulong value)
|
|
{
|
|
if (IsWriteDataPacket(ctx, first))
|
|
{
|
|
commandAddress = first;
|
|
value = second;
|
|
return true;
|
|
}
|
|
|
|
if (IsWriteDataPacket(ctx, second))
|
|
{
|
|
commandAddress = second;
|
|
value = first;
|
|
return true;
|
|
}
|
|
|
|
commandAddress = 0;
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
private static bool IsWriteDataPacket(CpuContext ctx, ulong commandAddress)
|
|
{
|
|
if (!TryGetPacketIdentity(ctx, commandAddress, out var op, out var register))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return op == ItWriteData || (op == ItNop && register == RWriteData);
|
|
}
|
|
|
|
private static int AddIndirectPatchRegisters(CpuContext ctx, string registerSpace)
|
|
{
|
|
var commandAddress = ctx[CpuRegister.Rdi];
|
|
var registerCount = (uint)ctx[CpuRegister.Rsi];
|
|
if (commandAddress == 0)
|
|
{
|
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
}
|
|
|
|
if (!TryReadUInt32(ctx, commandAddress + 4, out var currentCount) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, currentCount + registerCount))
|
|
{
|
|
return SetReturn(ctx, 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) ||
|
|
!TryWriteUInt32(ctx, commandAddress, Pm4(4, ItNop, packetRegister)) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 4, registerCount) ||
|
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)(registersAddress & 0xFFFF_FFFFUL)) ||
|
|
!TryWriteUInt32(ctx, 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 ||
|
|
!TryReadUInt64(ctx, commandBufferAddress + CommandBufferCursorUpOffset, out var cursorUp) ||
|
|
!TryReadUInt64(ctx, commandBufferAddress + CommandBufferCursorDownOffset, out var cursorDown) ||
|
|
!TryReadUInt64(ctx, commandBufferAddress + CommandBufferCallbackOffset, out var callback) ||
|
|
!TryReadUInt64(ctx, commandBufferAddress + CommandBufferUserDataOffset, out var userData) ||
|
|
!TryReadUInt32(ctx, commandBufferAddress + CommandBufferReservedDwOffset, out var reservedDwords))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var remainingDwords = GetRemainingCommandDwords(cursorUp, cursorDown, reservedDwords);
|
|
if (sizeDwords > remainingDwords)
|
|
{
|
|
TraceAgc($"agc.cmd_alloc_full buf=0x{commandBufferAddress:X16} need={sizeDwords} remaining={remainingDwords} callback=0x{callback:X16}");
|
|
var scheduler = GuestThreadExecution.Scheduler;
|
|
ulong callbackResult = 0;
|
|
string? callbackError = null;
|
|
if (callback == 0 ||
|
|
scheduler is null ||
|
|
!scheduler.TryCallGuestFunction(
|
|
ctx,
|
|
callback,
|
|
commandBufferAddress,
|
|
(ulong)sizeDwords + reservedDwords,
|
|
userData,
|
|
0,
|
|
0,
|
|
"agc_command_buffer_full",
|
|
out callbackResult,
|
|
out callbackError))
|
|
{
|
|
TraceAgc(
|
|
$"agc.cmd_alloc_callback_failed buf=0x{commandBufferAddress:X16} " +
|
|
$"callback=0x{callback:X16} result=0x{callbackResult:X16} " +
|
|
$"error={callbackError ?? "none"}");
|
|
return false;
|
|
}
|
|
|
|
TraceAgc(
|
|
$"agc.cmd_alloc_callback_complete buf=0x{commandBufferAddress:X16} " +
|
|
$"callback=0x{callback:X16} result=0x{callbackResult:X16}");
|
|
|
|
if (!TryReadUInt64(ctx, commandBufferAddress + CommandBufferCursorUpOffset, out cursorUp) ||
|
|
!TryReadUInt64(ctx, commandBufferAddress + CommandBufferCursorDownOffset, out cursorDown) ||
|
|
!TryReadUInt32(ctx, commandBufferAddress + CommandBufferReservedDwOffset, out reservedDwords) ||
|
|
sizeDwords > GetRemainingCommandDwords(cursorUp, cursorDown, reservedDwords))
|
|
{
|
|
TraceAgc($"agc.cmd_alloc_callback_no_space buf=0x{commandBufferAddress:X16} need={sizeDwords}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var nextCursor = cursorUp + ((ulong)sizeDwords * sizeof(uint));
|
|
if (!ctx.TryWriteUInt64(commandBufferAddress + CommandBufferCursorUpOffset, nextCursor))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
commandAddress = cursorUp;
|
|
return true;
|
|
}
|
|
|
|
private static uint GetRemainingCommandDwords(
|
|
ulong cursorUp,
|
|
ulong cursorDown,
|
|
uint reservedDwords)
|
|
{
|
|
var availableDwords = cursorDown >= cursorUp
|
|
? Math.Min((cursorDown - cursorUp) / sizeof(uint), uint.MaxValue)
|
|
: 0;
|
|
return availableDwords > reservedDwords
|
|
? (uint)availableDwords - reservedDwords
|
|
: 0;
|
|
}
|
|
|
|
private static bool CopyShaderRegister(CpuContext ctx, ulong sourceAddress, ulong destinationAddress)
|
|
{
|
|
if (!TryReadUInt32(ctx, sourceAddress, out var offset) ||
|
|
!TryReadUInt32(ctx, sourceAddress + sizeof(uint), out var value))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return TryWriteUInt32(ctx, destinationAddress, offset) &&
|
|
TryWriteUInt32(ctx, destinationAddress + sizeof(uint), value);
|
|
}
|
|
|
|
private static bool RelocatePointerField(CpuContext ctx, ulong fieldAddress)
|
|
{
|
|
if (!TryReadUInt64(ctx, 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<byte> blob, int offset, uint value) =>
|
|
BinaryPrimitives.WriteUInt32LittleEndian(blob[offset..], value);
|
|
|
|
private static void WriteBlobUInt64(Span<byte> 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 int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
|
{
|
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
|
return (int)result;
|
|
}
|
|
|
|
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 TryReadByte(CpuContext ctx, ulong address, out byte value)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[1];
|
|
if (!ctx.Memory.TryRead(address, buffer))
|
|
{
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
value = buffer[0];
|
|
return true;
|
|
}
|
|
|
|
// A submitted command buffer is bulk-copied once per submit and served
|
|
// from this thread-local window: the previous per-dword reads each took
|
|
// the guest-memory reader lock and ran a region binary search, which
|
|
// dominated submit parsing (thousands of locked 4-byte reads per DCB).
|
|
[ThreadStatic]
|
|
private static byte[]? _dcbWindowBuffer;
|
|
[ThreadStatic]
|
|
private static ulong _dcbWindowStart;
|
|
[ThreadStatic]
|
|
private static int _dcbWindowByteLength;
|
|
|
|
/// <summary>
|
|
/// Drops the bulk-read window when a self-patching command buffer writes
|
|
/// into its own bytes during parse, so subsequent reads see live guest
|
|
/// memory instead of the pre-write snapshot. Self-patching is rare, so
|
|
/// paying live-read cost for the rest of that one submit is acceptable.
|
|
/// </summary>
|
|
private static void InvalidateDcbWindowIfOverlaps(ulong address, ulong length)
|
|
{
|
|
if (_dcbWindowBuffer is null || length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var windowEnd = _dcbWindowStart + (ulong)_dcbWindowByteLength;
|
|
if (address < windowEnd && address + length > _dcbWindowStart)
|
|
{
|
|
_dcbWindowBuffer = null;
|
|
_dcbWindowByteLength = 0;
|
|
}
|
|
}
|
|
|
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
|
{
|
|
if (_dcbWindowBuffer is { } window &&
|
|
address >= _dcbWindowStart &&
|
|
address - _dcbWindowStart + sizeof(uint) <= (ulong)_dcbWindowByteLength)
|
|
{
|
|
value = BinaryPrimitives.ReadUInt32LittleEndian(
|
|
window.AsSpan((int)(address - _dcbWindowStart)));
|
|
return true;
|
|
}
|
|
|
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
|
if (!ctx.Memory.TryRead(address, buffer))
|
|
{
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
|
return ctx.Memory.TryWrite(address, buffer);
|
|
}
|
|
|
|
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
|
{
|
|
if (_dcbWindowBuffer is { } window &&
|
|
address >= _dcbWindowStart &&
|
|
address - _dcbWindowStart + sizeof(ulong) <= (ulong)_dcbWindowByteLength)
|
|
{
|
|
value = BinaryPrimitives.ReadUInt64LittleEndian(
|
|
window.AsSpan((int)(address - _dcbWindowStart)));
|
|
return true;
|
|
}
|
|
|
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
|
if (!ctx.Memory.TryRead(address, buffer))
|
|
{
|
|
value = 0;
|
|
return false;
|
|
}
|
|
|
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadGuestCString(
|
|
CpuContext ctx,
|
|
ulong address,
|
|
int maximumLength,
|
|
out byte[] bytes)
|
|
{
|
|
if (address == 0)
|
|
{
|
|
bytes = [];
|
|
return true;
|
|
}
|
|
|
|
var values = new List<byte>(Math.Min(maximumLength, 128));
|
|
for (var index = 0; index < maximumLength; index++)
|
|
{
|
|
if (!TryReadByte(ctx, 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 || !TryReadUInt32(ctx, 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<byte> 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;
|
|
}
|
|
|
|
// Interpolated-string handlers gated on the trace flags: when tracing is
|
|
// off (the normal case) the compiler skips every AppendFormatted call, so
|
|
// the interpolation never runs. These functions are on the hottest guest
|
|
// paths — e.g. AddIndirectPatchRegisters fires tens of thousands of times
|
|
// per second — and previously formatted a discarded string every call.
|
|
[System.Runtime.CompilerServices.InterpolatedStringHandler]
|
|
private ref struct AgcTraceHandler
|
|
{
|
|
private System.Runtime.CompilerServices.DefaultInterpolatedStringHandler _inner;
|
|
private readonly bool _enabled;
|
|
|
|
public AgcTraceHandler(int literalLength, int formattedCount, out bool shouldAppend)
|
|
{
|
|
_enabled = _traceAgc;
|
|
shouldAppend = _enabled;
|
|
_inner = _enabled
|
|
? new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(literalLength, formattedCount)
|
|
: default;
|
|
}
|
|
|
|
public void AppendLiteral(string value) => _inner.AppendLiteral(value);
|
|
public void AppendFormatted<T>(T value) => _inner.AppendFormatted(value);
|
|
public void AppendFormatted<T>(T value, string? format) => _inner.AppendFormatted(value, format);
|
|
public string ToStringAndClear() => _enabled ? _inner.ToStringAndClear() : string.Empty;
|
|
}
|
|
|
|
[System.Runtime.CompilerServices.InterpolatedStringHandler]
|
|
private ref struct AgcShaderTraceHandler
|
|
{
|
|
private System.Runtime.CompilerServices.DefaultInterpolatedStringHandler _inner;
|
|
private readonly bool _enabled;
|
|
|
|
public AgcShaderTraceHandler(int literalLength, int formattedCount, out bool shouldAppend)
|
|
{
|
|
_enabled = _traceAgcShader;
|
|
shouldAppend = _enabled;
|
|
_inner = _enabled
|
|
? new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(literalLength, formattedCount)
|
|
: default;
|
|
}
|
|
|
|
public void AppendLiteral(string value) => _inner.AppendLiteral(value);
|
|
public void AppendFormatted<T>(T value) => _inner.AppendFormatted(value);
|
|
public void AppendFormatted<T>(T value, string? format) => _inner.AppendFormatted(value, format);
|
|
public string ToStringAndClear() => _enabled ? _inner.ToStringAndClear() : string.Empty;
|
|
}
|
|
|
|
private static void TraceAgc(
|
|
[System.Runtime.CompilerServices.InterpolatedStringHandlerArgument] ref AgcTraceHandler message)
|
|
{
|
|
if (_traceAgc)
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] {message.ToStringAndClear()}");
|
|
}
|
|
}
|
|
|
|
private static void TraceAgc(string message)
|
|
{
|
|
if (!_traceAgc)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
|
}
|
|
|
|
private static void TraceAgcShader(
|
|
[System.Runtime.CompilerServices.InterpolatedStringHandlerArgument] ref AgcShaderTraceHandler message)
|
|
{
|
|
if (_traceAgcShader)
|
|
{
|
|
Console.Error.WriteLine($"[LOADER][TRACE] {message.ToStringAndClear()}");
|
|
}
|
|
}
|
|
|
|
private static void TraceAgcShader(string message)
|
|
{
|
|
if (!_traceAgcShader)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
|
}
|
|
|
|
private static string FormatShaderDwords(IReadOnlyList<uint> 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} depth={descriptor.Depth} base_array={descriptor.BaseArray} " +
|
|
$"levels={descriptor.BaseLevel}-{descriptor.LastLevel}/max{descriptor.MaxMip} " +
|
|
$"pitch={descriptor.Pitch} array_pitch={descriptor.ArrayPitch} " +
|
|
$"lod={descriptor.MinLod:X3}/{descriptor.MinLodWarn:X3} " +
|
|
$"bc={descriptor.BcSwizzle} meta=0x{descriptor.MetadataAddress:X16} " +
|
|
$"flags=0x{descriptor.DescriptorFlags:X6} dst=0x{descriptor.DstSelect:X3}";
|
|
|
|
private static ulong? ParseOptionalHexAddress(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var span = value.AsSpan().Trim();
|
|
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
span = span[2..];
|
|
}
|
|
|
|
return ulong.TryParse(
|
|
span,
|
|
System.Globalization.NumberStyles.HexNumber,
|
|
System.Globalization.CultureInfo.InvariantCulture,
|
|
out var address)
|
|
? address
|
|
: null;
|
|
}
|
|
|
|
private static void DumpCompiledShader(
|
|
string stage,
|
|
ulong shaderAddress,
|
|
ulong stateFingerprint,
|
|
IGuestCompiledShader shader,
|
|
Gen5ShaderProgram program)
|
|
{
|
|
if (shader.Payload.Length == 0 ||
|
|
!string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"),
|
|
"1",
|
|
StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var addressFilter = Environment.GetEnvironmentVariable(
|
|
"SHARPEMU_DUMP_SPIRV_ADDRESS");
|
|
if (!string.IsNullOrWhiteSpace(addressFilter))
|
|
{
|
|
var span = addressFilter.AsSpan();
|
|
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
span = span[2..];
|
|
}
|
|
|
|
if (!ulong.TryParse(
|
|
span,
|
|
System.Globalization.NumberStyles.HexNumber,
|
|
System.Globalization.CultureInfo.InvariantCulture,
|
|
out var filteredAddress) ||
|
|
shaderAddress != filteredAddress)
|
|
{
|
|
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}.{shader.PayloadFileExtension}"),
|
|
shader.Payload);
|
|
|
|
var lines = new List<string>(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}");
|
|
}
|
|
|
|
[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;
|
|
}
|
|
|
|
// 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 = "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);
|
|
}
|
|
|
|
[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 = "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);
|
|
}
|
|
}
|