fix(gen5): handle runtime and shader buffer paths (#904)
* Implement AMPR wait-on-address commands * Accept Gen5 AJM initialization options * Preserve opaque context register table values * Support shader-loaded buffer descriptors * Support V_CVT_U16_F16 shader instruction
This commit is contained in:
parent
7dbdd8b2cc
commit
8feea60d31
|
|
@ -7,6 +7,7 @@ using SharpEmu.Libs.Kernel;
|
|||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Libs.Ampr;
|
||||
|
|
@ -35,6 +36,9 @@ public static class AmprExports
|
|||
private const ulong AprMaxAppAddress = 0x0000f00000000000;
|
||||
private const ulong KernelEventQueueRecordSize = 0x20;
|
||||
private const ulong WriteAddressRecordSize = 0x20;
|
||||
private const ulong WaitAddressRecordSizeWithoutReference = 0x08;
|
||||
private const ulong WaitAddressRecordSizeWith32BitReference = 0x0C;
|
||||
private const ulong WaitAddressRecordSizeWith64BitReference = 0x10;
|
||||
private const uint ReadFileRecordType = 0x17;
|
||||
private const uint KernelEventQueueRecordType = 2;
|
||||
private const uint WriteAddressRecordType = 3;
|
||||
|
|
@ -55,6 +59,7 @@ public static class AmprExports
|
|||
public readonly List<ReadFileCommand> ReadFileCommands = [];
|
||||
public readonly List<KernelEventCommand> KernelEventCommands = [];
|
||||
public readonly List<WriteAddressCommand> WriteAddressCommands = [];
|
||||
public readonly List<WaitAddressCommand> WaitAddressCommands = [];
|
||||
public bool GatherScatterValid;
|
||||
public uint GatherScatterFileId;
|
||||
public ulong GatherScatterDestination;
|
||||
|
|
@ -86,6 +91,15 @@ public static class AmprExports
|
|||
public ulong Value;
|
||||
}
|
||||
|
||||
private sealed class WaitAddressCommand
|
||||
{
|
||||
public ulong RecordOffset;
|
||||
public ulong Address;
|
||||
public ulong Reference;
|
||||
public uint Compare;
|
||||
public uint Flush;
|
||||
}
|
||||
|
||||
private sealed class CachedHostFile : IDisposable
|
||||
{
|
||||
public CachedHostFile(string path)
|
||||
|
|
@ -578,6 +592,37 @@ public static class AmprExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jIlc4p5dSD0",
|
||||
ExportName = "sceAmprMeasureCommandSizeWaitOnAddress",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int MeasureCommandSizeWaitOnAddress(CpuContext ctx) => MeasureCommandSizeWaitOnAddress(ctx, legacy: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0BMj1hgG+kE",
|
||||
ExportName = "sceAmprMeasureCommandSizeWaitOnAddress_04_00",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int MeasureCommandSizeWaitOnAddress0400(CpuContext ctx) => MeasureCommandSizeWaitOnAddress(ctx, legacy: true);
|
||||
|
||||
private static int MeasureCommandSizeWaitOnAddress(CpuContext ctx, bool legacy)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
var reference = ctx[CpuRegister.Rsi];
|
||||
var compare = ctx[CpuRegister.Rdx];
|
||||
var flush = ctx[CpuRegister.Rcx];
|
||||
if (!IsValidWaitAddressParameters(address, compare, flush, legacy))
|
||||
{
|
||||
return SetAmprResult(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var recordSize = GetWaitAddressRecordSize(reference);
|
||||
TraceAmpr(ctx, legacy ? "measure_wait_address_04_00" : "measure_wait_address", address, recordSize, reference);
|
||||
ctx[CpuRegister.Rax] = recordSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "tZDDEo2tE5k",
|
||||
ExportName = "sceAmprCommandBufferGetSize",
|
||||
|
|
@ -769,6 +814,42 @@ public static class AmprExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "V7GQTEeUfhw",
|
||||
ExportName = "sceAmprCommandBufferWaitOnAddress",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int CommandBufferWaitOnAddress(CpuContext ctx) => CommandBufferWaitOnAddress(ctx, legacy: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DLfoNxTFNVk",
|
||||
ExportName = "sceAmprCommandBufferWaitOnAddress_04_00",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int CommandBufferWaitOnAddress0400(CpuContext ctx) => CommandBufferWaitOnAddress(ctx, legacy: true);
|
||||
|
||||
private static int CommandBufferWaitOnAddress(CpuContext ctx, bool legacy)
|
||||
{
|
||||
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||
var address = ctx[CpuRegister.Rsi];
|
||||
var reference = ctx[CpuRegister.Rdx];
|
||||
var compare = ctx[CpuRegister.Rcx];
|
||||
var flush = ctx[CpuRegister.R8];
|
||||
if (commandBuffer == 0 || !IsValidWaitAddressParameters(address, compare, flush, legacy))
|
||||
{
|
||||
return SetAmprResult(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!AppendWaitAddressRecord(ctx, commandBuffer, address, reference, (uint)compare, (uint)flush))
|
||||
{
|
||||
return SetAmprResult(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceAmpr(ctx, legacy ? "wait_address_04_00" : "wait_address", commandBuffer, address, reference);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer)
|
||||
{
|
||||
return CompleteCommandBuffer(ctx, commandBuffer, out _, out _);
|
||||
|
|
@ -797,16 +878,18 @@ public static class AmprExports
|
|||
ReadFileCommand[] readCommands;
|
||||
KernelEventCommand[] kernelEventCommands;
|
||||
WriteAddressCommand[] writeAddressCommands;
|
||||
WaitAddressCommand[] waitAddressCommands;
|
||||
lock (state)
|
||||
{
|
||||
writeOffset = state.WriteOffset;
|
||||
readCommands = state.ReadFileCommands.ToArray();
|
||||
kernelEventCommands = state.KernelEventCommands.ToArray();
|
||||
writeAddressCommands = state.WriteAddressCommands.ToArray();
|
||||
waitAddressCommands = state.WaitAddressCommands.ToArray();
|
||||
}
|
||||
|
||||
var commands = new List<(ulong Offset, int Kind, int Index)>(
|
||||
readCommands.Length + kernelEventCommands.Length + writeAddressCommands.Length);
|
||||
readCommands.Length + kernelEventCommands.Length + writeAddressCommands.Length + waitAddressCommands.Length);
|
||||
for (var i = 0; i < readCommands.Length; i++)
|
||||
{
|
||||
commands.Add((readCommands[i].RecordOffset, 0, i));
|
||||
|
|
@ -819,6 +902,10 @@ public static class AmprExports
|
|||
{
|
||||
commands.Add((writeAddressCommands[i].RecordOffset, 2, i));
|
||||
}
|
||||
for (var i = 0; i < waitAddressCommands.Length; i++)
|
||||
{
|
||||
commands.Add((waitAddressCommands[i].RecordOffset, 3, i));
|
||||
}
|
||||
commands.Sort(static (left, right) => left.Offset.CompareTo(right.Offset));
|
||||
|
||||
foreach (var commandEntry in commands)
|
||||
|
|
@ -860,6 +947,17 @@ public static class AmprExports
|
|||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
var waitCommand = waitAddressCommands[commandEntry.Index];
|
||||
var waitResult = WaitForAddress(ctx, waitCommand);
|
||||
if (waitResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
executionResult = waitResult;
|
||||
errorOffset = checked((uint)commandEntry.Offset);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -957,6 +1055,7 @@ public static class AmprExports
|
|||
state.ReadFileCommands.Clear();
|
||||
state.KernelEventCommands.Clear();
|
||||
state.WriteAddressCommands.Clear();
|
||||
state.WaitAddressCommands.Clear();
|
||||
state.GatherScatterValid = false;
|
||||
state.GatherScatterFileId = 0;
|
||||
state.GatherScatterDestination = 0;
|
||||
|
|
@ -1023,6 +1122,7 @@ public static class AmprExports
|
|||
state.ReadFileCommands.Clear();
|
||||
state.KernelEventCommands.Clear();
|
||||
state.WriteAddressCommands.Clear();
|
||||
state.WaitAddressCommands.Clear();
|
||||
state.GatherScatterValid = false;
|
||||
state.GatherScatterFileId = 0;
|
||||
state.GatherScatterDestination = 0;
|
||||
|
|
@ -1044,7 +1144,8 @@ public static class AmprExports
|
|||
{
|
||||
return state.ReadFileCommands.Count != 0 ||
|
||||
state.KernelEventCommands.Count != 0 ||
|
||||
state.WriteAddressCommands.Count != 0;
|
||||
state.WriteAddressCommands.Count != 0 ||
|
||||
state.WaitAddressCommands.Count != 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1515,6 +1616,139 @@ public static class AmprExports
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool AppendWaitAddressRecord(
|
||||
CpuContext ctx,
|
||||
ulong commandBuffer,
|
||||
ulong address,
|
||||
ulong reference,
|
||||
uint compare,
|
||||
uint flush)
|
||||
{
|
||||
var recordSize = GetWaitAddressRecordSize(reference);
|
||||
Span<byte> record = stackalloc byte[(int)recordSize];
|
||||
record.Clear();
|
||||
|
||||
var dwordCount = (uint)(recordSize / sizeof(uint));
|
||||
var header = (uint)((address >> 16) & 0xFFFF0000UL);
|
||||
header |= (((dwordCount << 8) + 0xF00u) & 0xEF00u) |
|
||||
((compare & 7u) << 13) |
|
||||
((flush & 1u) << 12) |
|
||||
1u;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(record, header);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(record[sizeof(uint)..], (uint)address);
|
||||
if (reference != 0)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(record[(2 * sizeof(uint))..], (uint)reference);
|
||||
if (dwordCount == 4)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(record[(3 * sizeof(uint))..], (uint)(reference >> 32));
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (state)
|
||||
{
|
||||
if (state.Buffer == 0 ||
|
||||
state.WriteOffset > state.Size ||
|
||||
recordSize > state.Size - state.WriteOffset ||
|
||||
!ctx.Memory.TryWrite(state.Buffer + state.WriteOffset, record))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var recordOffset = state.WriteOffset;
|
||||
state.WaitAddressCommands.Add(new WaitAddressCommand
|
||||
{
|
||||
RecordOffset = recordOffset,
|
||||
Address = address,
|
||||
Reference = reference,
|
||||
Compare = compare,
|
||||
Flush = flush,
|
||||
});
|
||||
state.WriteOffset += recordSize;
|
||||
state.CommandCount++;
|
||||
if (!WriteCommandBufferProgress(ctx, commandBuffer, state))
|
||||
{
|
||||
state.WaitAddressCommands.RemoveAt(state.WaitAddressCommands.Count - 1);
|
||||
state.WriteOffset -= recordSize;
|
||||
state.CommandCount--;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int WaitForAddress(CpuContext ctx, WaitAddressCommand command)
|
||||
{
|
||||
var spinWait = new SpinWait();
|
||||
while (true)
|
||||
{
|
||||
if (command.Flush != 0)
|
||||
{
|
||||
Thread.MemoryBarrier();
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(command.Address, out var value))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (IsWaitConditionSatisfied(value, command.Reference, command.Compare))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
spinWait.SpinOnce();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWaitConditionSatisfied(ulong value, ulong reference, uint compare) => compare switch
|
||||
{
|
||||
0 => value == reference,
|
||||
1 => value > reference,
|
||||
2 => value < reference,
|
||||
3 => value != reference,
|
||||
4 => unchecked((long)(value - reference)) >= 0,
|
||||
5 => unchecked((long)value) > unchecked((long)reference),
|
||||
6 => unchecked((long)value) < unchecked((long)reference),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static bool IsValidWaitAddressParameters(ulong address, ulong compare, ulong flush, bool legacy)
|
||||
{
|
||||
var maxCompare = legacy ? 6UL : 3UL;
|
||||
return (address & 7UL) == 0 &&
|
||||
address <= AprMaxAppAddress &&
|
||||
AprMaxAppAddress - address >= sizeof(ulong) &&
|
||||
(!legacy || address != 0) &&
|
||||
compare <= maxCompare &&
|
||||
flush <= 1;
|
||||
}
|
||||
|
||||
private static ulong GetWaitAddressRecordSize(ulong reference)
|
||||
{
|
||||
if (reference == 0)
|
||||
{
|
||||
return WaitAddressRecordSizeWithoutReference;
|
||||
}
|
||||
|
||||
return (reference >> 32) == 0
|
||||
? WaitAddressRecordSizeWith32BitReference
|
||||
: WaitAddressRecordSizeWith64BitReference;
|
||||
}
|
||||
|
||||
private static int SetAmprResult(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool AppendCommandBufferRecord(CpuContext ctx, ulong commandBuffer, ReadOnlySpan<byte> record)
|
||||
{
|
||||
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
|
||||
|
|
|
|||
|
|
@ -96,9 +96,15 @@ public static class AjmExports
|
|||
|
||||
public static int AjmInitialize(CpuContext ctx)
|
||||
{
|
||||
var reserved = ctx[CpuRegister.Rdi];
|
||||
var reservedOrOptions = ctx[CpuRegister.Rdi];
|
||||
var outputAddress = ctx[CpuRegister.Rsi];
|
||||
if (reserved != 0 || outputAddress == 0)
|
||||
// The Gen4 AJM ABI requires a zero reserved argument. Gen5 callers
|
||||
// also use this slot for an opaque initialization value (observed as
|
||||
// 0x0000000300000000 in a retail title), which the software backend
|
||||
// does not need to interpret. Keep the output pointer validation for
|
||||
// both generations; only relax the legacy reserved-field check on
|
||||
// Gen5.
|
||||
if ((ctx.TargetGeneration == Generation.Gen4 && reservedOrOptions != 0) || outputAddress == 0)
|
||||
{
|
||||
return unchecked((int)0x806A0001);
|
||||
}
|
||||
|
|
@ -115,7 +121,8 @@ public static class AjmExports
|
|||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] ajm.initialize reserved={reserved} out=0x{outputAddress:X16} context={contextId}");
|
||||
$"[LOADER][TRACE] ajm.initialize reserved_or_options=0x{reservedOrOptions:X16} " +
|
||||
$"generation={ctx.TargetGeneration} out=0x{outputAddress:X16} context={contextId}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
|
|
|
|||
|
|
@ -375,6 +375,8 @@ public sealed class ContextRegisters
|
|||
// The two reserved registers behind the depth block, written by the composite depth binding.
|
||||
public uint ReservedDepthRegister1;
|
||||
public uint ReservedDepthRegister3;
|
||||
// Preserve context-table values for in-bank registers without a decoded renderer field.
|
||||
public Dictionary<uint, uint> UnmodeledTableRegisters = new();
|
||||
public float DepthClearValue;
|
||||
public float DepthBoundsMin;
|
||||
public float DepthBoundsMax = 1f;
|
||||
|
|
@ -397,6 +399,7 @@ public sealed class ContextRegisters
|
|||
copy.ScreenViewport = ScreenViewport.Copy();
|
||||
copy.SampleLocations = SampleLocations.Copy();
|
||||
copy.ShaderInterface = ShaderInterface.Copy();
|
||||
copy.UnmodeledTableRegisters = new Dictionary<uint, uint>(UnmodeledTableRegisters);
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,16 @@ public static class RegisterWriteTable
|
|||
|
||||
public static void WriteContextEntry(RegisterBanks banks, uint offset, uint value, ulong tableAddress)
|
||||
{
|
||||
WriteEntry(banks, offset, value, ContextIndirect, "context", tableAddress);
|
||||
if (offset < ContextIndirect.Length && ContextIndirect[offset] is null)
|
||||
{
|
||||
// Keep unknown in-bank table values so a later renderer implementation can decode them.
|
||||
banks.Context.UnmodeledTableRegisters[offset] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteEntry(banks, offset, value, ContextIndirect, "context", tableAddress);
|
||||
}
|
||||
|
||||
if (Rendering.RenderTrace.Enabled)
|
||||
{
|
||||
TraceDepthStateWrite(offset, value, tableAddress, "table");
|
||||
|
|
|
|||
|
|
@ -239,6 +239,8 @@ public static partial class Gen5MslTranslator
|
|||
"VCvtF32I32" => FloatResult(instruction, $"(float)as_type<int>({RawSource(instruction, 0)})"),
|
||||
"VCvtF32U32" => FloatResult(instruction, $"(float)({RawSource(instruction, 0)})"),
|
||||
"VCvtU32F32" => $"(uint)({F(instruction, 0)})",
|
||||
"VCvtU16F16" =>
|
||||
$"(uint)clamp(trunc(isnan({F16(instruction, 0)}) ? 0.0f : {F16(instruction, 0)}), 0.0f, 65535.0f)",
|
||||
"VCvtI32F32" => AsUInt($"(int)({F(instruction, 0)})"),
|
||||
// RPI rounds toward positive infinity; FLR toward negative.
|
||||
"VCvtRpiI32F32" => AsUInt($"(int)ceil({F(instruction, 0)})"),
|
||||
|
|
|
|||
|
|
@ -152,6 +152,25 @@ public static partial class Gen5SpirvTranslator
|
|||
_uintType,
|
||||
GetFloatSource(instruction, 0));
|
||||
break;
|
||||
case "VCvtU16F16":
|
||||
{
|
||||
var source = GetFloat16Source(instruction, 0);
|
||||
var sourceIsNan = _module.AddInstruction(
|
||||
SpirvOp.IsNan,
|
||||
_boolType,
|
||||
source);
|
||||
source = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_floatType,
|
||||
sourceIsNan,
|
||||
Float(0),
|
||||
source);
|
||||
var bounded = Ext(43, _floatType, source, Float(0), Float(65535));
|
||||
result = BitwiseAnd(
|
||||
_module.AddInstruction(SpirvOp.ConvertFToU, _uintType, bounded),
|
||||
UInt(0xFFFF));
|
||||
break;
|
||||
}
|
||||
case "VCvtI32F32":
|
||||
case "VCvtRpiI32F32":
|
||||
case "VCvtFlrI32F32":
|
||||
|
|
|
|||
|
|
@ -664,7 +664,7 @@ public static partial class Gen5SpirvTranslator
|
|||
// One dword of a scalar buffer load through the V# in s[descriptor:descriptor+3]:
|
||||
// base in dwords 0-1 (48 bits), stride in dword 1 [29:16], records in dword 2.
|
||||
// A load past the end of the buffer returns zero, as on hardware.
|
||||
private uint LoadDeviceDescriptorBufferWord(uint descriptor, uint byteOffset)
|
||||
private (uint BaseAddress, uint Size, uint Stride, uint Word3) LoadDeviceBufferDescriptor(uint descriptor)
|
||||
{
|
||||
var word1 = LoadS(descriptor + 1);
|
||||
var baseAddress = Pair64(LoadS(descriptor), BitwiseAnd(word1, UInt(0xFFFF)));
|
||||
|
|
@ -676,14 +676,52 @@ public static partial class Gen5SpirvTranslator
|
|||
_module.AddInstruction(SpirvOp.IEqual, _boolType, stride, UInt(0)),
|
||||
records,
|
||||
_module.AddInstruction(SpirvOp.IMul, _ulongType, records, Widen(stride)));
|
||||
var aligned = Widen(BitwiseAnd(byteOffset, UInt(~3u)));
|
||||
var inRange = ULessThan64(IAdd64(aligned, ULong(sizeof(uint) - 1)), size);
|
||||
return (baseAddress, size, stride, LoadS(descriptor + 3));
|
||||
}
|
||||
|
||||
private uint IsDeviceBufferByteRangeInRange(uint size64, uint byteOffset, uint byteCount) =>
|
||||
ULessThan64(
|
||||
IAdd64(Widen(byteOffset), ULong((ulong)byteCount - 1)),
|
||||
size64);
|
||||
|
||||
private uint IsDeviceBufferElementInRange(uint size64, uint byteOffset, uint lastByteOffset) =>
|
||||
ULessThan64(
|
||||
IAdd64(Widen(byteOffset), Widen(lastByteOffset)),
|
||||
size64);
|
||||
|
||||
private uint LoadDeviceBufferWord(uint baseAddress, uint size64, uint byteOffset)
|
||||
=> LoadDeviceBufferWord(baseAddress, size64, byteOffset, _module.ConstantBool(true));
|
||||
|
||||
private uint LoadDeviceBufferWord(uint baseAddress, uint size64, uint byteOffset, uint accessAllowed)
|
||||
{
|
||||
var alignedOffset = BitwiseAnd(byteOffset, UInt(~3u));
|
||||
var inRange = LogicalAnd(
|
||||
accessAllowed,
|
||||
IsDeviceBufferByteRangeInRange(size64, alignedOffset, sizeof(uint)));
|
||||
var address = And64(
|
||||
IAdd64(baseAddress, Widen(alignedOffset)),
|
||||
ULong(DeviceAddressMask & ~3ul));
|
||||
Store(_deviceBufferWordScratch, UInt(0));
|
||||
EmitConditional(inRange, () =>
|
||||
Store(_deviceBufferWordScratch, LoadDeviceDword(And64(IAdd64(baseAddress, aligned), ULong(DeviceAddressMask & ~3ul)))));
|
||||
EmitConditional(inRange, () => Store(_deviceBufferWordScratch, LoadDeviceDword(address)));
|
||||
return Load(_uintType, _deviceBufferWordScratch);
|
||||
}
|
||||
|
||||
private void StoreDeviceBufferWord(uint baseAddress, uint size64, uint byteOffset, uint value)
|
||||
{
|
||||
var alignedOffset = BitwiseAnd(byteOffset, UInt(~3u));
|
||||
var inRange = IsDeviceBufferByteRangeInRange(size64, alignedOffset, sizeof(uint));
|
||||
var address = And64(
|
||||
IAdd64(baseAddress, Widen(alignedOffset)),
|
||||
ULong(DeviceAddressMask & ~3ul));
|
||||
EmitConditional(inRange, () => StoreDeviceDword(address, value, _module.ConstantBool(true)));
|
||||
}
|
||||
|
||||
private uint LoadDeviceDescriptorBufferWord(uint descriptor, uint byteOffset)
|
||||
{
|
||||
var (baseAddress, size, _, _) = LoadDeviceBufferDescriptor(descriptor);
|
||||
return LoadDeviceBufferWord(baseAddress, size, byteOffset);
|
||||
}
|
||||
|
||||
private bool TryEmitLayoutScalarMemory(Gen5ShaderInstruction instruction, Gen5ScalarMemoryControl control, out string error)
|
||||
{
|
||||
_deviceAddressInstructionPc = instruction.Pc;
|
||||
|
|
|
|||
|
|
@ -2220,6 +2220,11 @@ public static partial class Gen5SpirvTranslator
|
|||
return TryEmitVertexInputFetch(control, vertexInput, out error);
|
||||
}
|
||||
|
||||
if (_request.Memory.Find(instruction.Pc) is { DeviceDescriptor: true })
|
||||
{
|
||||
return TryEmitDeviceDescriptorBufferMemory(instruction, control, out error);
|
||||
}
|
||||
|
||||
int bindingIndex;
|
||||
uint stride;
|
||||
uint descriptorWord3;
|
||||
|
|
@ -2476,6 +2481,351 @@ public static partial class Gen5SpirvTranslator
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool TryEmitDeviceDescriptorBufferMemory(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5BufferMemoryControl control,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (control.Typed || instruction.Opcode.StartsWith("TBuffer", StringComparison.Ordinal))
|
||||
{
|
||||
error = $"device buffer descriptor operation {instruction.Opcode} is not supported";
|
||||
return false;
|
||||
}
|
||||
|
||||
_deviceAddressInstructionPc = instruction.Pc;
|
||||
var (baseAddress, size, stride, descriptorWord3) = LoadDeviceBufferDescriptor(control.ScalarResource);
|
||||
var scalarOffset = instruction.Sources.Count > 2
|
||||
? GetRawSource(instruction, 2)
|
||||
: UInt(0);
|
||||
var vectorIndex = control.IndexEnabled
|
||||
? LoadV(control.VectorAddress)
|
||||
: UInt(0);
|
||||
var vectorOffset = control.OffsetEnabled
|
||||
? LoadV(control.VectorAddress + (control.IndexEnabled ? 1u : 0u))
|
||||
: UInt(0);
|
||||
var byteAddress = IAdd(UInt(unchecked((uint)control.OffsetBytes)), scalarOffset);
|
||||
byteAddress = IAdd(byteAddress, vectorOffset);
|
||||
byteAddress = IAdd(byteAddress, _module.AddInstruction(SpirvOp.IMul, _uintType, vectorIndex, stride));
|
||||
|
||||
if (IsFormatBufferLoad(instruction.Opcode))
|
||||
{
|
||||
EmitDeviceBufferFormatLoad(
|
||||
baseAddress,
|
||||
size,
|
||||
byteAddress,
|
||||
descriptorWord3,
|
||||
control.VectorData,
|
||||
control.DwordCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode.Contains("Format", StringComparison.Ordinal))
|
||||
{
|
||||
error = $"device buffer descriptor operation {instruction.Opcode} is not supported";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal))
|
||||
{
|
||||
return TryEmitDeviceDescriptorBufferAtomic(instruction, control, baseAddress, size, byteAddress, out error);
|
||||
}
|
||||
|
||||
if (TryGetSubdwordStoreInfo(instruction.Opcode, out var storeByteCount, out var sourceShift))
|
||||
{
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var inRange = IsDeviceBufferByteRangeInRange(size, byteAddress, storeByteCount);
|
||||
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask));
|
||||
EmitConditional(inRange, () =>
|
||||
StoreDeviceBytes(address, LoadV(control.VectorData), storeByteCount, sourceShift, _module.ConstantBool(true)));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode.StartsWith("BufferStoreDword", StringComparison.Ordinal))
|
||||
{
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
for (uint index = 0; index < control.DwordCount; index++)
|
||||
{
|
||||
var componentAddress = index == 0
|
||||
? byteAddress
|
||||
: IAdd(byteAddress, UInt(index * sizeof(uint)));
|
||||
StoreDeviceBufferWord(
|
||||
baseAddress,
|
||||
size,
|
||||
componentAddress,
|
||||
LoadV(control.VectorData + index));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetSubdwordLoadInfo(
|
||||
instruction.Opcode,
|
||||
out var loadByteCount,
|
||||
out var signExtend,
|
||||
out var d16,
|
||||
out var d16High))
|
||||
{
|
||||
var inRange = IsDeviceBufferByteRangeInRange(size, byteAddress, loadByteCount);
|
||||
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask));
|
||||
Store(_deviceBufferWordScratch, UInt(0));
|
||||
EmitConditional(inRange, () => Store(
|
||||
_deviceBufferWordScratch,
|
||||
LoadSubdwordDeviceValue(
|
||||
address,
|
||||
LoadV(control.VectorData),
|
||||
loadByteCount,
|
||||
signExtend,
|
||||
d16,
|
||||
d16High)));
|
||||
StoreV(control.VectorData, Load(_uintType, _deviceBufferWordScratch));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode.StartsWith("BufferLoadDword", StringComparison.Ordinal))
|
||||
{
|
||||
for (uint index = 0; index < control.DwordCount; index++)
|
||||
{
|
||||
var componentAddress = index == 0
|
||||
? byteAddress
|
||||
: IAdd(byteAddress, UInt(index * sizeof(uint)));
|
||||
StoreV(
|
||||
control.VectorData + index,
|
||||
LoadDeviceBufferWord(baseAddress, size, componentAddress));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
error = $"unsupported device buffer descriptor opcode {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryEmitDeviceDescriptorBufferAtomic(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5BufferMemoryControl control,
|
||||
uint baseAddress,
|
||||
uint size,
|
||||
uint byteAddress,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
var atomicSuffix = instruction.Opcode["BufferAtomic".Length..];
|
||||
if (atomicSuffix is "SwapX2" or "OrX2")
|
||||
{
|
||||
var atomicOp = atomicSuffix == "SwapX2" ? SpirvOp.AtomicExchange : SpirvOp.AtomicOr;
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var valid = IsDeviceBufferByteRangeInRange(size, byteAddress, 2 * sizeof(uint));
|
||||
var firstAddress = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask & ~3ul));
|
||||
var secondAddress = And64(IAdd64(baseAddress, Widen(IAdd(byteAddress, UInt(sizeof(uint))))), ULong(DeviceAddressMask & ~3ul));
|
||||
EmitConditional(valid, () =>
|
||||
{
|
||||
var (firstPointer, firstMapped) = ResolveDeviceAddress(firstAddress);
|
||||
var (secondPointer, secondMapped) = ResolveDeviceAddress(secondAddress);
|
||||
var bothMapped = LogicalAnd(firstMapped, secondMapped);
|
||||
EmitConditional(bothMapped, () =>
|
||||
{
|
||||
var originalLow = EmitAtomic(
|
||||
atomicOp, _uintType, DeviceWordPointer(firstPointer), 1, 0x48,
|
||||
() => LoadV(control.VectorData), () => UInt(0));
|
||||
var originalHigh = EmitAtomic(
|
||||
atomicOp, _uintType, DeviceWordPointer(secondPointer), 1, 0x48,
|
||||
() => LoadV(control.VectorData + 1), () => UInt(0));
|
||||
if (control.Glc)
|
||||
{
|
||||
StoreV(control.VectorData, originalLow);
|
||||
StoreV(control.VectorData + 1, originalHigh);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (atomicSuffix is "Fmin" or "Fmax")
|
||||
{
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var valid = IsDeviceBufferByteRangeInRange(size, byteAddress, sizeof(uint));
|
||||
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask & ~3ul));
|
||||
EmitConditional(valid, () =>
|
||||
{
|
||||
var (pointer, mapped) = ResolveDeviceAddress(address);
|
||||
EmitConditional(mapped, () =>
|
||||
{
|
||||
var original = EmitBufferFloatAtomic(
|
||||
DeviceWordPointer(pointer),
|
||||
LoadV(control.VectorData),
|
||||
maxValue: atomicSuffix == "Fmax",
|
||||
scope: 1,
|
||||
semantics: 0x48);
|
||||
if (control.Glc)
|
||||
StoreV(control.VectorData, original);
|
||||
});
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryGetAtomicOp(atomicSuffix, out var atomicOperation))
|
||||
{
|
||||
error = $"unsupported buffer atomic opcode {instruction.Opcode}";
|
||||
return false;
|
||||
}
|
||||
|
||||
EmitExecConditional(() =>
|
||||
{
|
||||
var valid = IsDeviceBufferByteRangeInRange(size, byteAddress, sizeof(uint));
|
||||
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask & ~3ul));
|
||||
EmitConditional(valid, () =>
|
||||
{
|
||||
var (pointer, mapped) = ResolveDeviceAddress(address);
|
||||
EmitConditional(mapped, () =>
|
||||
{
|
||||
var original = EmitAtomic(
|
||||
atomicOperation,
|
||||
_uintType,
|
||||
DeviceWordPointer(pointer),
|
||||
1,
|
||||
0x48,
|
||||
() => LoadV(control.VectorData),
|
||||
() => LoadV(control.VectorData + 1));
|
||||
if (control.Glc)
|
||||
StoreV(control.VectorData, original);
|
||||
});
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EmitDeviceBufferFormatLoad(
|
||||
uint baseAddress,
|
||||
uint size,
|
||||
uint byteAddress,
|
||||
uint descriptorWord3,
|
||||
uint vectorData,
|
||||
uint componentCount)
|
||||
{
|
||||
var unifiedFormat = BitwiseAnd(
|
||||
ShiftRightLogical(descriptorWord3, UInt(12)),
|
||||
UInt(0x7F));
|
||||
var (dataFormat, numberFormat) = DecodeGfx10BufferFormat(unifiedFormat);
|
||||
|
||||
var canonical = new uint[4];
|
||||
var componentBounds = new uint[4];
|
||||
for (var component = 0; component < canonical.Length; component++)
|
||||
{
|
||||
canonical[component] = LoadGfx10DeviceBufferFormatComponent(
|
||||
baseAddress,
|
||||
size,
|
||||
byteAddress,
|
||||
dataFormat,
|
||||
numberFormat,
|
||||
component,
|
||||
out componentBounds[component]);
|
||||
}
|
||||
|
||||
var selectors = new uint[componentCount];
|
||||
var inBounds = _module.ConstantBool(true);
|
||||
for (uint destination = 0; destination < componentCount; destination++)
|
||||
{
|
||||
var selector = BitwiseAnd(
|
||||
ShiftRightLogical(descriptorWord3, UInt(destination * 3)),
|
||||
UInt(7));
|
||||
selectors[destination] = selector;
|
||||
var selectedInBounds = _module.ConstantBool(true);
|
||||
for (uint component = 0; component < 4; component++)
|
||||
{
|
||||
selectedInBounds = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_boolType,
|
||||
_module.AddInstruction(SpirvOp.IEqual, _boolType, selector, UInt(component + 4)),
|
||||
componentBounds[component],
|
||||
selectedInBounds);
|
||||
}
|
||||
|
||||
inBounds = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, inBounds, selectedInBounds);
|
||||
}
|
||||
|
||||
var one = Gfx10FormatOne(numberFormat);
|
||||
for (uint destination = 0; destination < componentCount; destination++)
|
||||
{
|
||||
var selector = selectors[destination];
|
||||
var constant = SelectUInt(selector, 1, one, UInt(0));
|
||||
var value = constant;
|
||||
value = SelectUInt(selector, 4, canonical[0], value);
|
||||
value = SelectUInt(selector, 5, canonical[1], value);
|
||||
value = SelectUInt(selector, 6, canonical[2], value);
|
||||
value = SelectUInt(selector, 7, canonical[3], value);
|
||||
StoreV(
|
||||
vectorData + destination,
|
||||
_module.AddInstruction(SpirvOp.Select, _uintType, inBounds, value, constant));
|
||||
}
|
||||
}
|
||||
|
||||
private uint LoadGfx10DeviceBufferFormatComponent(
|
||||
uint baseAddress,
|
||||
uint size,
|
||||
uint elementAddress,
|
||||
uint dataFormat,
|
||||
uint numberFormat,
|
||||
int component,
|
||||
out uint componentInBounds)
|
||||
{
|
||||
var byteOffset = UInt(0);
|
||||
var bitOffset = UInt(0);
|
||||
var bitCount = UInt(0);
|
||||
|
||||
void SetLayout(uint format, uint bytes, uint bits, uint count)
|
||||
{
|
||||
var matches = _module.AddInstruction(SpirvOp.IEqual, _boolType, dataFormat, UInt(format));
|
||||
byteOffset = _module.AddInstruction(SpirvOp.Select, _uintType, matches, UInt(bytes), byteOffset);
|
||||
bitOffset = _module.AddInstruction(SpirvOp.Select, _uintType, matches, UInt(bits), bitOffset);
|
||||
bitCount = _module.AddInstruction(SpirvOp.Select, _uintType, matches, UInt(count), bitCount);
|
||||
}
|
||||
|
||||
foreach (var layout in Gfx10UnifiedFormat.ComponentLayouts)
|
||||
{
|
||||
if (layout.Component == (uint)component)
|
||||
SetLayout(layout.DataFormat, layout.ByteOffset, layout.BitOffset, layout.BitCount);
|
||||
}
|
||||
|
||||
var componentAddress = IAdd(elementAddress, byteOffset);
|
||||
var componentBytes = ShiftRightLogical(IAdd(IAdd(bitOffset, bitCount), UInt(7)), UInt(3));
|
||||
var lastByteOffset = _module.AddInstruction(SpirvOp.ISub, _uintType, componentBytes, UInt(1));
|
||||
var hasComponent = _module.AddInstruction(SpirvOp.INotEqual, _boolType, bitCount, UInt(0));
|
||||
var inRange = IsDeviceBufferElementInRange(size, componentAddress, lastByteOffset);
|
||||
var accessAllowed = LogicalAnd(hasComponent, inRange);
|
||||
componentInBounds = _module.AddInstruction(
|
||||
SpirvOp.LogicalOr,
|
||||
_boolType,
|
||||
LogicalNot(hasComponent),
|
||||
inRange);
|
||||
var packed = UInt(0);
|
||||
for (uint index = 0; index < sizeof(uint); index++)
|
||||
{
|
||||
var address = index == 0 ? componentAddress : IAdd(componentAddress, UInt(index));
|
||||
var word = LoadDeviceBufferWord(baseAddress, size, address, accessAllowed);
|
||||
var shift = ShiftLeftLogical(BitwiseAnd(address, UInt(3)), UInt(3));
|
||||
var value = BitwiseAnd(ShiftRightLogical(word, shift), UInt(0xFF));
|
||||
packed = BitwiseOr(packed, ShiftLeftLogical(value, UInt(index * 8)));
|
||||
}
|
||||
|
||||
var raw = _module.AddInstruction(SpirvOp.BitFieldUExtract, _uintType, packed, bitOffset, bitCount);
|
||||
var converted = ConvertGfx10BufferComponent(raw, bitCount, numberFormat, dataFormat);
|
||||
var valid = _module.AddInstruction(SpirvOp.INotEqual, _boolType, bitCount, UInt(0));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
valid,
|
||||
converted,
|
||||
component == 3 ? Gfx10FormatOne(numberFormat) : UInt(0));
|
||||
}
|
||||
|
||||
private void EmitBufferFormatLoad(
|
||||
int bindingIndex,
|
||||
uint byteAddress,
|
||||
|
|
|
|||
|
|
@ -832,6 +832,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x43 => "VMovrelsB32",
|
||||
0x44 => "VMovrelsdB32",
|
||||
0x48 => "VMovrelsd2B32",
|
||||
0x52 => "VCvtU16F16",
|
||||
0x56 => "VRsqF16",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
|
@ -1170,6 +1171,8 @@ public static partial class Gen5ShaderTranslator
|
|||
0x36D => "VAdd3U32",
|
||||
0x36F => "VLshlOrU32",
|
||||
0x178 => "VXor3B32",
|
||||
// VOP1 opcode 0x52 is available through VOP3 as opcode 0x1D2.
|
||||
0x1D2 => "VCvtU16F16",
|
||||
0x300 => "VLshrrevB64",
|
||||
0x371 => "VAndOrB32",
|
||||
0x372 => "VOr3U32",
|
||||
|
|
|
|||
|
|
@ -265,6 +265,39 @@ public sealed partial class ResourceTracker
|
|||
handle is { Kind: ScalarValueKind.BufferHandle, Operands.Length: 4 } &&
|
||||
handle.Operands.All(dword => dword.Type == ScalarValueType.U32 && _plan.ValidateRuntimeValue(dword));
|
||||
|
||||
private bool IsDeviceLoadedBufferHandle(ScalarValue? handle) =>
|
||||
handle is { Kind: ScalarValueKind.BufferHandle, Operands.Length: 4 } &&
|
||||
handle.Operands.All(dword =>
|
||||
dword.Type == ScalarValueType.U32 &&
|
||||
(_plan.ValidateRuntimeValue(dword) || DependsOnScalarBufferWord(dword))) &&
|
||||
handle.Operands.Any(DependsOnScalarBufferWord);
|
||||
|
||||
private static bool DependsOnScalarBufferWord(ScalarValue value)
|
||||
{
|
||||
var pending = new Stack<ScalarValue>();
|
||||
var visited = new HashSet<ScalarValue>();
|
||||
pending.Push(value);
|
||||
while (pending.TryPop(out var current))
|
||||
{
|
||||
if (!visited.Add(current))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current.Kind == ScalarValueKind.ScalarBufferWord)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var operand in current.Operands)
|
||||
{
|
||||
pending.Push(operand);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private uint GetHandleSource(ScalarValue? handle, ScalarValueKind expected, uint width, uint pc, bool sampler = false, bool sampleAdjust = false)
|
||||
{
|
||||
if (handle is null || handle.Kind != expected)
|
||||
|
|
@ -474,9 +507,10 @@ public sealed partial class ResourceTracker
|
|||
|
||||
if (isBuffer)
|
||||
{
|
||||
// A scalar buffer descriptor built from data the shader loads itself cannot be
|
||||
// bound by the host; the device reads through the descriptor in registers.
|
||||
if (memory.Kind == MemoryResourceKind.ScalarBuffer && !IsHostBufferHandle(access.Handle))
|
||||
// Scalar loads can address buffers that have no host descriptor binding, while
|
||||
// vector buffer descriptors loaded from scalar-buffer data must stay device-side.
|
||||
if ((memory.Kind == MemoryResourceKind.ScalarBuffer && !IsHostBufferHandle(access.Handle)) ||
|
||||
(memory.Kind == MemoryResourceKind.Buffer && IsDeviceLoadedBufferHandle(access.Handle)))
|
||||
{
|
||||
memory.DeviceDescriptor = true;
|
||||
_info.UsesDeviceAddresses = true;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using System.Buffers.Binary;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Ampr;
|
||||
|
||||
[Collection("AmprFileRegistry")]
|
||||
public sealed class AmprWaitOnAddressTests
|
||||
{
|
||||
[Fact]
|
||||
public void MeasureCommandSizeWaitOnAddress_UsesReferenceWidth()
|
||||
{
|
||||
const string nid = "jIlc4p5dSD0";
|
||||
const ulong memoryBase = 0x2_0000_0000;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
var manager = CreateManagerWithExport(nid, "sceAmprMeasureCommandSizeWaitOnAddress");
|
||||
|
||||
Assert.Equal(8UL, Measure(manager, context, nid, memoryBase + 0x100, 0, compare: 0, flush: 0));
|
||||
Assert.Equal(12UL, Measure(manager, context, nid, memoryBase + 0x100, 0xCAFE, compare: 0, flush: 0));
|
||||
Assert.Equal(16UL, Measure(manager, context, nid, memoryBase + 0x100, 0x1_0000_0000, compare: 0, flush: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeasureCommandSizeWaitOnAddress_ValidatesModernAndLegacyEnums()
|
||||
{
|
||||
const ulong memoryBase = 0x2_0000_0000;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
var modern = CreateManagerWithExport("jIlc4p5dSD0", "sceAmprMeasureCommandSizeWaitOnAddress");
|
||||
var legacy = CreateManagerWithExport("0BMj1hgG+kE", "sceAmprMeasureCommandSizeWaitOnAddress_04_00");
|
||||
|
||||
context[CpuRegister.Rdi] = memoryBase + 0x100;
|
||||
context[CpuRegister.Rsi] = 0xCAFE;
|
||||
context[CpuRegister.Rdx] = 4;
|
||||
context[CpuRegister.Rcx] = 0;
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
modern.Dispatch("jIlc4p5dSD0", context));
|
||||
|
||||
context[CpuRegister.Rdx] = 6;
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, legacy.Dispatch("0BMj1hgG+kE", context));
|
||||
Assert.Equal(12UL, context[CpuRegister.Rax]);
|
||||
|
||||
context[CpuRegister.Rdi] = 0;
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
|
||||
legacy.Dispatch("0BMj1hgG+kE", context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandBufferWaitOnAddress_EncodesWaitAndCompletesWhenPredicateIsSatisfied()
|
||||
{
|
||||
const string nid = "V7GQTEeUfhw";
|
||||
const ulong memoryBase = 0x2_0000_0000;
|
||||
const ulong commandBufferAddress = memoryBase + 0x100;
|
||||
const ulong recordBufferAddress = memoryBase + 0x200;
|
||||
const ulong waitAddress = memoryBase + 0x800;
|
||||
const ulong reference = 0xCAFE;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
var manager = CreateManagerWithExport(nid, "sceAmprCommandBufferWaitOnAddress");
|
||||
|
||||
context[CpuRegister.Rdi] = commandBufferAddress;
|
||||
context[CpuRegister.Rsi] = recordBufferAddress;
|
||||
context[CpuRegister.Rdx] = 0x100;
|
||||
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
|
||||
Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context));
|
||||
|
||||
Assert.True(context.TryWriteUInt64(waitAddress, reference));
|
||||
context[CpuRegister.Rdi] = commandBufferAddress;
|
||||
context[CpuRegister.Rsi] = waitAddress;
|
||||
context[CpuRegister.Rdx] = reference;
|
||||
context[CpuRegister.Rcx] = 0;
|
||||
context[CpuRegister.R8] = 0;
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, manager.Dispatch(nid, context));
|
||||
|
||||
Assert.True(context.TryReadUInt32(commandBufferAddress + 4, out var offset));
|
||||
Assert.True(context.TryReadUInt32(commandBufferAddress + 8, out var commandCount));
|
||||
Assert.Equal(12U, offset);
|
||||
Assert.Equal(1U, commandCount);
|
||||
|
||||
Span<byte> record = stackalloc byte[12];
|
||||
Assert.True(memory.TryRead(recordBufferAddress, record));
|
||||
var expectedHeader = (uint)((waitAddress >> 16) & 0xFFFF0000UL) | 0x0201U;
|
||||
Assert.Equal(expectedHeader, BinaryPrimitives.ReadUInt32LittleEndian(record));
|
||||
Assert.Equal(0x00000800U, BinaryPrimitives.ReadUInt32LittleEndian(record[4..]));
|
||||
Assert.Equal(0x0000CAFEU, BinaryPrimitives.ReadUInt32LittleEndian(record[8..]));
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
AmprExports.CompleteCommandBuffer(context, commandBufferAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandBufferWaitOnAddress_SupportsAllModernAndLegacyComparisons()
|
||||
{
|
||||
const ulong memoryBase = 0x2_0000_0000;
|
||||
const ulong commandBufferAddress = memoryBase + 0x100;
|
||||
const ulong recordBufferAddress = memoryBase + 0x200;
|
||||
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
var modern = CreateManagerWithExport("V7GQTEeUfhw", "sceAmprCommandBufferWaitOnAddress");
|
||||
var legacy = CreateManagerWithExport("DLfoNxTFNVk", "sceAmprCommandBufferWaitOnAddress_04_00");
|
||||
var comparisons = new (uint Mode, ulong Value, ulong Reference, bool Legacy)[]
|
||||
{
|
||||
(0, 10, 10, false),
|
||||
(1, 11, 10, false),
|
||||
(2, 9, 10, false),
|
||||
(3, 11, 10, false),
|
||||
(4, 0, ulong.MaxValue, true),
|
||||
(5, 1, 0, true),
|
||||
(6, ulong.MaxValue, 0, true),
|
||||
};
|
||||
|
||||
context[CpuRegister.Rdi] = commandBufferAddress;
|
||||
context[CpuRegister.Rsi] = recordBufferAddress;
|
||||
context[CpuRegister.Rdx] = 0x200;
|
||||
Assert.Equal(0, AmprExports.CommandBufferConstructor(context));
|
||||
Assert.Equal(0, AmprExports.CommandBufferSetBuffer(context));
|
||||
|
||||
for (var index = 0; index < comparisons.Length; index++)
|
||||
{
|
||||
var comparison = comparisons[index];
|
||||
var address = memoryBase + 0x800 + (ulong)index * sizeof(ulong);
|
||||
Assert.True(context.TryWriteUInt64(address, comparison.Value));
|
||||
context[CpuRegister.Rdi] = commandBufferAddress;
|
||||
context[CpuRegister.Rsi] = address;
|
||||
context[CpuRegister.Rdx] = comparison.Reference;
|
||||
context[CpuRegister.Rcx] = comparison.Mode;
|
||||
context[CpuRegister.R8] = 0;
|
||||
var manager = comparison.Legacy ? legacy : modern;
|
||||
var nid = comparison.Legacy ? "DLfoNxTFNVk" : "V7GQTEeUfhw";
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, manager.Dispatch(nid, context));
|
||||
}
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||
AmprExports.CompleteCommandBuffer(context, commandBufferAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandBufferWaitOnAddress0400_RegistersLegacyVariant()
|
||||
{
|
||||
const string nid = "DLfoNxTFNVk";
|
||||
var manager = CreateManagerWithExport(nid, "sceAmprCommandBufferWaitOnAddress_04_00");
|
||||
|
||||
Assert.True(manager.TryGetExport(nid, out var export));
|
||||
Assert.Equal("libSceAmpr", export.LibraryName);
|
||||
Assert.Equal(Generation.Gen5, export.Target);
|
||||
}
|
||||
|
||||
private static ulong Measure(
|
||||
ModuleManager manager,
|
||||
CpuContext context,
|
||||
string nid,
|
||||
ulong address,
|
||||
ulong reference,
|
||||
ulong compare,
|
||||
ulong flush)
|
||||
{
|
||||
context[CpuRegister.Rdi] = address;
|
||||
context[CpuRegister.Rsi] = reference;
|
||||
context[CpuRegister.Rdx] = compare;
|
||||
context[CpuRegister.Rcx] = flush;
|
||||
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, manager.Dispatch(nid, context));
|
||||
return context[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
private static ModuleManager CreateManagerWithExport(string nid, string exportName)
|
||||
{
|
||||
var manager = new ModuleManager();
|
||||
manager.RegisterExports(
|
||||
SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5));
|
||||
|
||||
Assert.True(manager.TryGetExport(nid, out var export), $"NID {nid} did not register.");
|
||||
Assert.Equal(exportName, export.Name);
|
||||
Assert.Equal("libSceAmpr", export.LibraryName);
|
||||
Assert.Equal(Generation.Gen5, export.Target);
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +51,31 @@ public sealed class AjmExportsTests : IDisposable
|
|||
Assert.Equal(InvalidInstance, DestroyInstance(contextId, 0x4001));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_Gen5AcceptsOpaqueInitializationValueAndWritesContext()
|
||||
{
|
||||
_ctx[CpuRegister.Rdi] = 0x0000000300000000;
|
||||
_ctx[CpuRegister.Rsi] = ContextAddress;
|
||||
|
||||
Assert.Equal(0, AjmExports.AjmInitialize(_ctx));
|
||||
var contextId = ReadUInt32(ContextAddress);
|
||||
Assert.NotEqual(0u, contextId);
|
||||
Assert.Equal(0, RegisterCodec(contextId, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_Gen4StillRejectsNonzeroReservedValue()
|
||||
{
|
||||
var context = new CpuContext(_memory, Generation.Gen4);
|
||||
const uint sentinel = 0xCCCCCCCC;
|
||||
WriteUInt32(ContextAddress, sentinel);
|
||||
context[CpuRegister.Rdi] = 0x0000000300000000;
|
||||
context[CpuRegister.Rsi] = ContextAddress;
|
||||
|
||||
Assert.Equal(unchecked((int)0x806A0001), AjmExports.AjmInitialize(context));
|
||||
Assert.Equal(sentinel, ReadUInt32(ContextAddress));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstanceCreate_UnregisteredCodecDoesNotWriteOutput()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -211,9 +211,19 @@ public sealed class RegisterWriteTableTests
|
|||
Assert.Equal(0xFu, banks.Context.RenderTargetMask);
|
||||
Assert.Equal(0x1_0000ul, banks.Shader.Pixel.Address);
|
||||
Assert.Equal(1u, banks.IndexTypeAndSize);
|
||||
var error = Assert.Throws<InvalidOperationException>(() => RegisterWriteTable.WriteContextEntry(banks, 0x1B2, 1, 0x5000));
|
||||
Assert.Contains("table register is not supported", error.Message);
|
||||
Assert.Contains("table=0x0000000000005000", error.Message);
|
||||
RegisterWriteTable.WriteContextEntry(banks, 0x024A, 0, 0x5000);
|
||||
Assert.True(banks.Context.UnmodeledTableRegisters.TryGetValue(0x024A, out var value));
|
||||
Assert.Equal(0u, value);
|
||||
|
||||
var clone = banks.Clone();
|
||||
RegisterWriteTable.WriteContextEntry(banks, 0x024A, 1, 0x5000);
|
||||
Assert.Equal(0u, clone.Context.UnmodeledTableRegisters[0x024A]);
|
||||
Assert.Equal(1u, banks.Context.UnmodeledTableRegisters[0x024A]);
|
||||
|
||||
banks.ApplyContextState(ContextStateOperation.Push);
|
||||
RegisterWriteTable.WriteContextEntry(banks, 0x024A, 2, 0x5000);
|
||||
banks.ApplyContextState(ContextStateOperation.Pop);
|
||||
Assert.Equal(1u, banks.Context.UnmodeledTableRegisters[0x024A]);
|
||||
}
|
||||
|
||||
// Registers without a decoded field are stored so a title that writes them does not stop.
|
||||
|
|
|
|||
|
|
@ -104,6 +104,33 @@ public sealed class SpirvBindingDeclarationTests
|
|||
Assert.DoesNotContain(request.Bindings.Descriptors, descriptor => descriptor.Kind == DescriptorBindingKind.DeviceAddressPageTable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BufferDescriptorLoadedByShader_UsesDeviceAddressAccess()
|
||||
{
|
||||
var program = Program(
|
||||
ReadFirstLane(0, 12, 0),
|
||||
MoveScalar(4, 13, 0),
|
||||
MoveScalar(8, 14, 16),
|
||||
MoveScalar(12, 15, 0),
|
||||
ScalarBufferLoad(16, 12, destination: 20, count: 4),
|
||||
BufferLoad(24, 20, dwords: 4),
|
||||
BufferLoad(32, 20, dwords: 3, formatted: true),
|
||||
EndProgram(40));
|
||||
var request = Request(program);
|
||||
var memories = request.Memory.Entries.Where(entry => entry.Pc is 24 or 32).ToArray();
|
||||
|
||||
Assert.Equal(2, memories.Length);
|
||||
Assert.All(memories, memory => Assert.True(
|
||||
memory.DeviceDescriptor,
|
||||
$"kind={memory.Kind} resource={memory.Resource} planningOnly={memory.PlanningOnly}"));
|
||||
Assert.True(request.Resources.Info.UsesDeviceAddresses);
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
|
||||
var module = new SpirvModuleInspector(shader.Spirv);
|
||||
Assert.Contains((uint)SpirvCapability.PhysicalStorageBufferAddresses, module.Capabilities);
|
||||
Assert.Contains((ushort)SpirvOp.ConvertUToPtr, module.Opcodes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpilledUserData_ReadsTheShaderDataBufferInsteadOfPushData()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue