Merge branch 'main' of https://github.com/sharpemu/sharpemu
This commit is contained in:
commit
dfb7efa5df
|
|
@ -520,7 +520,18 @@ internal static partial class Program
|
|||
return false;
|
||||
}
|
||||
|
||||
string[] childArgs = [MitigatedChildFlag, .. args];
|
||||
string[] childArgs;
|
||||
var commandLineArgs = Environment.GetCommandLineArgs();
|
||||
var entryAssembly = commandLineArgs.Length != 0 ? commandLineArgs[0] : null;
|
||||
if (Path.GetFileNameWithoutExtension(processPath).Equals("dotnet", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrWhiteSpace(entryAssembly))
|
||||
{
|
||||
childArgs = [entryAssembly, MitigatedChildFlag, .. args];
|
||||
}
|
||||
else
|
||||
{
|
||||
childArgs = [MitigatedChildFlag, .. args];
|
||||
}
|
||||
|
||||
var commandLine = BuildCommandLine(processPath, childArgs);
|
||||
var startupInfoEx = new STARTUPINFOEX();
|
||||
|
|
|
|||
|
|
@ -148,11 +148,6 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == 3221225477u &&
|
||||
TryRecoverInflateFastEpilogue(exceptionRecord, contextRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverIllegalInstruction(contextRecord, rip))
|
||||
{
|
||||
|
|
@ -676,179 +671,6 @@ public sealed partial class DirectExecutionBackend
|
|||
return true;
|
||||
}
|
||||
|
||||
private unsafe bool TryRecoverInflateFastEpilogue(EXCEPTION_RECORD* exceptionRecord, void* contextRecord, ulong rip, ulong rsp)
|
||||
{
|
||||
// Only apply this compat recovery for Ghostrunner (PPSA03685)
|
||||
var currentTitleId = CurrentTitleId ?? SharpEmu.Libs.VideoOut.VideoOutExports.GetApplicationTitleId();
|
||||
if (!string.Equals(currentTitleId, "PPSA03685", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const ulong epilogueStoreRip = 0x0000000804A2C23EuL;
|
||||
const ulong epilogueStateStoreRip = 0x0000000804A2C25AuL;
|
||||
|
||||
if (rip != epilogueStoreRip && rip != epilogueStateStoreRip)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this is the state fault at 0x804A2C25A (mov [rcx + 0x48], rax)
|
||||
if (rip == epilogueStateStoreRip)
|
||||
{
|
||||
ulong rcxFault = ReadCtxU64(contextRecord, CTX_RCX);
|
||||
if (rcxFault >= 0x10000UL && exceptionRecord->ExceptionInformation[1] != 0x48)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE STATE RECOVERY TRIGGERED AT 0x804A2C25A <<<");
|
||||
|
||||
// Recover strm from rdi or caller frame
|
||||
ulong strm = ReadCtxU64(contextRecord, CTX_RDI);
|
||||
if (strm < 0x10000UL)
|
||||
{
|
||||
if (TryReadQword(rsp + 0x108, out ulong candidate) && candidate >= 0x10000)
|
||||
{
|
||||
strm = candidate;
|
||||
}
|
||||
else if (TryReadQword(rsp + 0x80, out ulong savedRbp))
|
||||
{
|
||||
if (TryReadQword((ulong)((long)savedRbp - 0x58), out ulong candidate2) && candidate2 >= 0x10000)
|
||||
{
|
||||
strm = candidate2;
|
||||
}
|
||||
else if (TryReadQword(savedRbp + 0x20, out ulong candidate3) && candidate3 >= 0x10000)
|
||||
{
|
||||
strm = candidate3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strm < 0x10000UL)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Could not locate strm for state recovery at 0x804A2C25A!");
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong state = *(ulong*)(strm + 0x38);
|
||||
if (state < 0x10000UL)
|
||||
{
|
||||
state = *(ulong*)(strm + 0x28);
|
||||
}
|
||||
|
||||
if (state < 0x10000UL)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Invalid state pointer from strm (0x{strm:X16}): 0x{state:X16}");
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong raxVal = ReadCtxU64(contextRecord, CTX_RAX);
|
||||
ulong r8Val = ReadCtxU64(contextRecord, CTX_R8);
|
||||
|
||||
*(ulong*)(state + 0x48) = raxVal;
|
||||
*(uint*)(state + 0x50) = (uint)r8Val;
|
||||
|
||||
WriteCtxU64(contextRecord, CTX_RCX, state);
|
||||
WriteCtxU64(contextRecord, CTX_RIP, 0x0000000804A2C262uL);
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Recovered state: 0x{state:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Executed state->hold = 0x{raxVal:X16}, state->bits = {r8Val & 0xFF}");
|
||||
Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE STATE RECOVERY COMPLETED - RESUMING <<<");
|
||||
Console.Error.Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, fault at 0x804A2C23E
|
||||
ulong rdiVal = ReadCtxU64(contextRecord, CTX_RDI);
|
||||
if (rdiVal != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE RECOVERY TRIGGERED AT 0x804A2C23E <<<");
|
||||
|
||||
// 1. Recover strm from caller frame:
|
||||
// In inflate_fast frame:
|
||||
// [rsp + 0x80] is caller's saved RBP
|
||||
// [rsp + 0x108] holds strm (0x00007FFFF01FF470)
|
||||
ulong strmVal = 0;
|
||||
if (TryReadQword(rsp + 0x108, out ulong cand) && cand >= 0x10000)
|
||||
{
|
||||
strmVal = cand;
|
||||
}
|
||||
if (strmVal == 0 && TryReadQword(rsp + 0x80, out ulong savedRbpVal))
|
||||
{
|
||||
if (TryReadQword((ulong)((long)savedRbpVal - 0x58), out ulong cand2) && cand2 >= 0x10000)
|
||||
{
|
||||
strmVal = cand2;
|
||||
}
|
||||
else if (TryReadQword(savedRbpVal + 0x20, out ulong cand3) && cand3 >= 0x10000)
|
||||
{
|
||||
strmVal = cand3;
|
||||
}
|
||||
}
|
||||
|
||||
if (strmVal == 0)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][ERROR] Could not locate strm in stack frame for inflate_fast recovery!");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Recovered strm: 0x{strmVal:X16}");
|
||||
|
||||
// 2. Set rdi = strm in CPU context
|
||||
WriteCtxU64(contextRecord, CTX_RDI, strmVal);
|
||||
|
||||
// 3. If r10 is invalid or underflowed, recover actual input position
|
||||
ulong r10Val = ReadCtxU64(contextRecord, CTX_R10);
|
||||
bool isInvalidInPointer = (r10Val < 0x10000UL) || (r10Val > 0x00007FFFFFFFFFFEUL);
|
||||
if (isInvalidInPointer)
|
||||
{
|
||||
ulong inEnd = 0;
|
||||
if (TryReadQword(rsp, out inEnd) && inEnd >= 0x10000)
|
||||
{
|
||||
ulong raxVal = ReadCtxU64(contextRecord, CTX_RAX);
|
||||
ulong unusedBytes = raxVal & 0xFFFFFFFFUL; // bits >> 3
|
||||
ulong recoveredNextIn = inEnd - unusedBytes + 1;
|
||||
r10Val = recoveredNextIn - 1;
|
||||
WriteCtxU64(contextRecord, CTX_R10, r10Val);
|
||||
WriteCtxU64(contextRecord, CTX_RDX, recoveredNextIn);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Recovered r10 (in): 0x{r10Val:X16} (in_end=0x{inEnd:X16}, unused={unusedBytes})");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Recover rcx (strm->state) if needed so subsequent stores at 0x804A2C25A succeed
|
||||
ulong rcxVal = ReadCtxU64(contextRecord, CTX_RCX);
|
||||
if (rcxVal < 0x10000UL)
|
||||
{
|
||||
ulong state = *(ulong*)(strmVal + 0x38);
|
||||
if (state < 0x10000UL)
|
||||
{
|
||||
state = *(ulong*)(strmVal + 0x28);
|
||||
}
|
||||
if (state >= 0x10000UL)
|
||||
{
|
||||
WriteCtxU64(contextRecord, CTX_RCX, state);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Recovered rcx (strm->state): 0x{state:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Perform the stores directly to ensure stream consistency
|
||||
ulong rdxVal = ReadCtxU64(contextRecord, CTX_RDX);
|
||||
ulong r9Val = ReadCtxU64(contextRecord, CTX_R9);
|
||||
|
||||
*(ulong*)strmVal = rdxVal;
|
||||
*(ulong*)(strmVal + 0x18) = r9Val + 1;
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Executed strm->next_in = 0x{rdxVal:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Executed strm->next_out = 0x{r9Val + 1:X16}");
|
||||
Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE RECOVERY COMPLETED - RESUMING <<<");
|
||||
Console.Error.Flush();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsBenignHostDebugException(uint exceptionCode)
|
||||
{
|
||||
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
|
||||
|
|
|
|||
|
|
@ -1623,6 +1623,9 @@ public sealed partial class DirectExecutionBackend
|
|||
"LtTouSCZjHM" or // sceAgcCbNop
|
||||
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
||||
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
||||
"pFLArOT53+w" or // sceAgcDcbSetShRegisterDirect
|
||||
"QhPDD513V0w" or // sceAgcDcbSetShRegisterDirectGetSize
|
||||
"43WJ08sSugE" or // sceAgcDcbWaitOnAddressGetSize
|
||||
"JrtiDtKeS38" or // sceAgcAcbResetQueue
|
||||
"cFazmnXpJOE" or // sceAgcAcbEventWrite
|
||||
"KT-hTp-Ch14" or // sceAgcAcbAcquireMem
|
||||
|
|
@ -1880,13 +1883,17 @@ public sealed partial class DirectExecutionBackend
|
|||
return elapsedTicks >= (long)(_importLoopGuardSeconds * Stopwatch.Frequency);
|
||||
}
|
||||
|
||||
private static bool IsImportLoopGuardBoundary(string nid) =>
|
||||
internal static bool IsImportLoopGuardBoundary(string nid) =>
|
||||
nid is
|
||||
"1jfXLRVzisc" or // sceKernelUsleep
|
||||
"WKAXJ4XBPQ4" or // scePthreadCondWait
|
||||
"BmMjYxmew1w" or // scePthreadCondTimedwait
|
||||
"Op8TBGY5KHg" or // pthread_cond_wait
|
||||
"27bAgiJmOh0"; // pthread_cond_timedwait
|
||||
"27bAgiJmOh0" or // pthread_cond_timedwait
|
||||
"n88vx3C5nW8" or // gettimeofday
|
||||
"-2IRUCO--PM" or // clock_gettime
|
||||
"0V5nU-Z6t4U" or // sceKernelGetProcessTime
|
||||
"aI6lQW5v57k"; // sceKernelGetProcessTimeCounter
|
||||
|
||||
private void ResetImportLoopPattern()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -982,6 +982,25 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||
return _hostMemory.Protect(address, size, ResolveProtection(protection), out _);
|
||||
}
|
||||
|
||||
public bool TryCommitRange(ulong address, ulong size)
|
||||
{
|
||||
if (size == 0 || ulong.MaxValue - address < size)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var region = FindRegion(address, size);
|
||||
return region is not null && EnsureRangeCommitted(address, size, region);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduces the decomposition KernelMemoryCompatExports.ResolveHostProtection
|
||||
// performed before this seam existed; the Windows backend maps each case back
|
||||
// to the identical PAGE_* value.
|
||||
|
|
|
|||
|
|
@ -642,6 +642,11 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true),
|
||||
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true),
|
||||
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true),
|
||||
// Retail packages may keep application PRX files beside eboot.bin
|
||||
// (for example, native middleware shipped at the app root). Index
|
||||
// those modules too so their imports are available when the guest
|
||||
// requests them through sceKernelLoadStartModule.
|
||||
(Path: ebootDirectory, StartAtBoot: false),
|
||||
// Unity native plugins are loaded later through sceKernelLoadStartModule. Map
|
||||
// them up front so the HLE loader can return a real module handle and dlsym
|
||||
// can resolve their exports, but defer DT_INIT until the guest requests them.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator
|
|||
/// </summary>
|
||||
bool TryBackFixedRange(ulong address, ulong size, bool executable);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures every page in an existing guest mapping is committed for native
|
||||
/// execution. This is required before returning a raw identity-mapped range
|
||||
/// to guest code, which cannot pass through <c>ICpuMemory</c> on each access.
|
||||
/// </summary>
|
||||
bool TryCommitRange(ulong address, ulong size);
|
||||
|
||||
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||
|
||||
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||
|
|
|
|||
|
|
@ -1488,6 +1488,7 @@ public static partial class AgcExports
|
|||
uint VertexCount,
|
||||
uint InstanceCount,
|
||||
int BaseVertex,
|
||||
int VertexBufferBaseVertex,
|
||||
GuestIndexBuffer? IndexBuffer,
|
||||
IReadOnlyList<TranslatedImageBinding> Textures,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
|
|
@ -2918,6 +2919,25 @@ public static partial class AgcExports
|
|||
public static int DcbSetShRegistersIndirect(CpuContext ctx) =>
|
||||
DcbSetRegistersIndirect(ctx, RShRegsIndirect, "sh");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pFLArOT53+w",
|
||||
ExportName = "sceAgcDcbSetShRegisterDirect",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbSetShRegisterDirect(CpuContext ctx) =>
|
||||
DcbSetRegisterDirect(ctx, ItSetShReg, "sh");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QhPDD513V0w",
|
||||
ExportName = "sceAgcDcbSetShRegisterDirectGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbSetShRegisterDirectGetSize(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 3u * sizeof(uint);
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hvUfkUIQcOE",
|
||||
ExportName = "sceAgcDcbSetUcRegistersIndirect",
|
||||
|
|
@ -3563,6 +3583,23 @@ public static partial class AgcExports
|
|||
return ReturnPointer(ctx, commandAddress);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "43WJ08sSugE",
|
||||
ExportName = "sceAgcDcbWaitOnAddressGetSize",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAgc")]
|
||||
public static int DcbWaitOnAddressGetSize(CpuContext ctx)
|
||||
{
|
||||
var size = (uint)ctx[CpuRegister.Rdi];
|
||||
ctx[CpuRegister.Rax] = size switch
|
||||
{
|
||||
0 => 14u * sizeof(uint),
|
||||
1 => 16u * sizeof(uint),
|
||||
_ => 0,
|
||||
};
|
||||
return (int)ctx[CpuRegister.Rax];
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "u2T2DiA5hRI",
|
||||
ExportName = "sceAgcDcbStallCommandBufferParser",
|
||||
|
|
@ -5304,7 +5341,9 @@ public static partial class AgcExports
|
|||
var globalMemoryBuffers =
|
||||
CreateTranslatedDrawGlobalBuffers(pendingComposite);
|
||||
var vertexBuffers =
|
||||
CreateGuestVertexBuffers(pendingComposite.VertexInputs);
|
||||
CreateGuestVertexBuffers(
|
||||
pendingComposite.VertexInputs,
|
||||
pendingComposite.VertexBufferBaseVertex);
|
||||
ProvideRenderTargetInitialData(ctx, pendingDisplayTarget);
|
||||
GuestGpu.Current.SubmitOffscreenTranslatedDraw(
|
||||
pendingComposite.PixelShader,
|
||||
|
|
@ -8232,7 +8271,9 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
var globalMemoryBuffers =
|
||||
CreateTranslatedDrawGlobalBuffers(depthOnlyDraw);
|
||||
var vertexBuffers =
|
||||
CreateGuestVertexBuffers(depthOnlyDraw.VertexInputs);
|
||||
CreateGuestVertexBuffers(
|
||||
depthOnlyDraw.VertexInputs,
|
||||
depthOnlyDraw.VertexBufferBaseVertex);
|
||||
var renderState = depthOnlyDraw.RenderState;
|
||||
if (activeDepthTarget.ReadOnly && renderState.Depth.WriteEnable)
|
||||
{
|
||||
|
|
@ -8427,7 +8468,9 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
var sharedGlobalMemoryBuffers =
|
||||
CreateTranslatedDrawGlobalBuffers(translatedDraw);
|
||||
var sharedVertexBuffers =
|
||||
CreateGuestVertexBuffers(translatedDraw.VertexInputs);
|
||||
CreateGuestVertexBuffers(
|
||||
translatedDraw.VertexInputs,
|
||||
translatedDraw.VertexBufferBaseVertex);
|
||||
TraceRectListVertices(translatedDraw, sharedVertexBuffers);
|
||||
TraceGrassDrawVertices(translatedDraw, sharedTextures, sharedVertexBuffers);
|
||||
TraceDrawCompact(
|
||||
|
|
@ -8484,7 +8527,9 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
var globalMemoryBuffers =
|
||||
CreateTranslatedDrawGlobalBuffers(translatedDraw);
|
||||
var vertexBuffers =
|
||||
CreateGuestVertexBuffers(translatedDraw.VertexInputs);
|
||||
CreateGuestVertexBuffers(
|
||||
translatedDraw.VertexInputs,
|
||||
translatedDraw.VertexBufferBaseVertex);
|
||||
var renderState = translatedDraw.RenderState;
|
||||
if (translatedDepthTarget.ReadOnly && renderState.Depth.WriteEnable)
|
||||
{
|
||||
|
|
@ -8656,8 +8701,18 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
SelectExportUserDataRegister(state.ShRegisters),
|
||||
out var exportState,
|
||||
out error,
|
||||
userDataScalarRegisterBase: NggUserDataScalarRegisterBase) ||
|
||||
!Gen5ShaderScalarEvaluator.TryEvaluate(
|
||||
userDataScalarRegisterBase: NggUserDataScalarRegisterBase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Embedded fetch prologs add their own base to gl_VertexID. Keep the
|
||||
// host draw offset at zero in that case; otherwise Vulkan applies the
|
||||
// same first-vertex adjustment a second time.
|
||||
var baseVertex = GetBaseVertex(state, exportState);
|
||||
var vertexBufferBaseVertex = GetVertexBufferBaseVertex(exportState);
|
||||
var recordBaseVertex = GetVertexRecordBaseVertex(state, exportState);
|
||||
if (!Gen5ShaderScalarEvaluator.TryEvaluate(
|
||||
ctx,
|
||||
exportState,
|
||||
out var exportEvaluation,
|
||||
|
|
@ -8668,6 +8723,7 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
state,
|
||||
vertexCount,
|
||||
indexed,
|
||||
recordBaseVertex,
|
||||
out var depthVertexRecords)
|
||||
? depthVertexRecords
|
||||
: null))
|
||||
|
|
@ -8816,7 +8872,8 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
AttributeCount: 0,
|
||||
vertexCount,
|
||||
state.InstanceCount,
|
||||
GetBaseVertex(state),
|
||||
baseVertex,
|
||||
vertexBufferBaseVertex,
|
||||
indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
|
||||
textures,
|
||||
exportEvaluation.GlobalMemoryBindings,
|
||||
|
|
@ -8871,6 +8928,13 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
return false;
|
||||
}
|
||||
|
||||
// Embedded fetch prologs add their own base to gl_VertexID. Keep the
|
||||
// host draw offset at zero in that case; otherwise Vulkan applies the
|
||||
// same first-vertex adjustment a second time.
|
||||
var baseVertex = GetBaseVertex(state, exportState);
|
||||
var vertexBufferBaseVertex = GetVertexBufferBaseVertex(exportState);
|
||||
var recordBaseVertex = GetVertexRecordBaseVertex(state, exportState);
|
||||
|
||||
if (!Gen5ShaderScalarEvaluator.TryEvaluate(
|
||||
ctx,
|
||||
exportState,
|
||||
|
|
@ -8882,6 +8946,7 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
state,
|
||||
vertexCount,
|
||||
indexed,
|
||||
recordBaseVertex,
|
||||
out var vertexRecords)
|
||||
? vertexRecords
|
||||
: null))
|
||||
|
|
@ -9328,7 +9393,8 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
GetInterpolatedAttributeCount(pixelState),
|
||||
vertexCount,
|
||||
state.InstanceCount,
|
||||
GetBaseVertex(state),
|
||||
baseVertex,
|
||||
vertexBufferBaseVertex,
|
||||
indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
|
||||
textures,
|
||||
globalMemoryBindings,
|
||||
|
|
@ -9945,14 +10011,64 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
AgcIndexHelpers.Decode(state.IndexSize);
|
||||
|
||||
/// <summary>
|
||||
/// ResolveVertexOffset for the common UC path: GE_INDX_OFFSET is the
|
||||
/// DrawIndexed vertexOffset / DrawAuto firstVertex. Embedded-fetch SGPR
|
||||
/// fallback is not required when the game latches this register (GTA UI).
|
||||
/// Resolve the Vulkan host vertex offset. GE_INDX_OFFSET is the
|
||||
/// DrawIndexed vertexOffset / DrawAuto firstVertex when the translated
|
||||
/// shader does not contain an embedded fetch prolog. Embedded prologs add
|
||||
/// their user-SGPR offset to gl_VertexID themselves, so the host offset
|
||||
/// must remain zero in that case.
|
||||
/// </summary>
|
||||
private static int GetBaseVertex(SubmittedDcbState state) =>
|
||||
state.UcRegisters.TryGetValue(GeIndxOffset, out var indexOffset)
|
||||
? unchecked((int)indexOffset)
|
||||
: 0;
|
||||
private static int GetBaseVertex(
|
||||
SubmittedDcbState state,
|
||||
Gen5ShaderState exportState)
|
||||
{
|
||||
if (Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset(
|
||||
exportState,
|
||||
out _))
|
||||
{
|
||||
// The translated vertex shader already adds this value to
|
||||
// gl_VertexID, so passing it through CmdDrawIndexed would double it.
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (state.UcRegisters.TryGetValue(GeIndxOffset, out var indexOffset))
|
||||
{
|
||||
return unchecked((int)indexOffset);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int GetVertexRecordBaseVertex(
|
||||
SubmittedDcbState state,
|
||||
Gen5ShaderState exportState)
|
||||
{
|
||||
if (Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset(
|
||||
exportState,
|
||||
out var embeddedOffset))
|
||||
{
|
||||
return embeddedOffset;
|
||||
}
|
||||
|
||||
if (state.UcRegisters.TryGetValue(GeIndxOffset, out var indexOffset))
|
||||
{
|
||||
return unchecked((int)indexOffset);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the record offset for host vertex attributes when the guest
|
||||
/// shader applies its first-vertex value itself. The host draw stays at
|
||||
/// vertex zero so gl_VertexID remains relative, while the buffer binding
|
||||
/// starts at the guest record selected by the fetch prolog.
|
||||
/// </summary>
|
||||
private static int GetVertexBufferBaseVertex(Gen5ShaderState exportState) =>
|
||||
Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset(
|
||||
exportState,
|
||||
out var embeddedOffset)
|
||||
? embeddedOffset
|
||||
: 0;
|
||||
|
||||
private static GuestIndexBuffer? CreateGuestIndexBuffer(
|
||||
CpuContext ctx,
|
||||
|
|
@ -10009,9 +10125,10 @@ var renderTargets = GetRenderTargets(state.CxRegisters);
|
|||
SubmittedDcbState state,
|
||||
uint drawCount,
|
||||
bool indexed,
|
||||
int resolvedBaseVertex,
|
||||
out uint recordCount)
|
||||
{
|
||||
var baseVertex = (uint)Math.Max(GetBaseVertex(state), 0);
|
||||
var baseVertex = (uint)Math.Max(resolvedBaseVertex, 0);
|
||||
recordCount = Math.Max(
|
||||
baseVertex + drawCount,
|
||||
Math.Max(state.InstanceCount, 1u));
|
||||
|
|
@ -11257,8 +11374,10 @@ private static long _indirectDrawProbeCount;
|
|||
}
|
||||
|
||||
private static IReadOnlyList<GuestVertexBuffer> CreateGuestVertexBuffers(
|
||||
IReadOnlyList<Gen5VertexInputBinding> bindings)
|
||||
IReadOnlyList<Gen5VertexInputBinding> bindings,
|
||||
int baseVertex)
|
||||
{
|
||||
var baseRecord = baseVertex > 0 ? checked((uint)baseVertex) : 0;
|
||||
var buffers = new GuestVertexBuffer[bindings.Count];
|
||||
for (var index = 0; index < bindings.Count; index++)
|
||||
{
|
||||
|
|
@ -11274,7 +11393,8 @@ private static long _indirectDrawProbeCount;
|
|||
binding.Data,
|
||||
binding.DataLength,
|
||||
binding.DataPooled,
|
||||
binding.PerInstance);
|
||||
binding.PerInstance,
|
||||
baseRecord);
|
||||
}
|
||||
|
||||
return buffers;
|
||||
|
|
@ -14590,7 +14710,12 @@ GuestImageWriteTracker.Track(
|
|||
|
||||
if (shRegistersAddress == 0 || registerCount < 2)
|
||||
{
|
||||
return false;
|
||||
TraceCreateShader(
|
||||
0,
|
||||
headerAddress,
|
||||
codeAddress,
|
||||
$"skip-pgm-patch type={shaderType} shRegs=0x{shRegistersAddress:X16} regCount={registerCount}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Type bytes follow the Prospero half/fused enum used by fuse-shader
|
||||
|
|
@ -14619,9 +14744,10 @@ GuestImageWriteTracker.Track(
|
|||
_ => 0u,
|
||||
};
|
||||
|
||||
// GTA V Enhanced hull shaders (type 5) put RSRC1/RSRC2 (0x10A/0x10B) at
|
||||
// the front of the SH default table; PGM_LO/HI sit elsewhere (or are
|
||||
// filled later via SetShRegisterDirect).
|
||||
// Some shader headers (e.g. GTA V Enhanced hull shaders, GS front shaders,
|
||||
// or UE4 mesh/task/vertex shaders where type=8 or user data registers are placed first)
|
||||
// omit PGM_LO/HI from the default SH table. Still succeed: the code VA
|
||||
// lives at ShaderCodeOffset and later binder paths republish it.
|
||||
if (!TryFindShaderProgramRegisterPair(
|
||||
ctx,
|
||||
shRegistersAddress,
|
||||
|
|
@ -14634,28 +14760,12 @@ GuestImageWriteTracker.Track(
|
|||
out var foundHi))
|
||||
{
|
||||
TryReadUInt32(ctx, shRegistersAddress, out var firstLo);
|
||||
// GTA V Enhanced HS headers start at RSRC1/RSRC2 (0x10A/0x10B) and
|
||||
// omit PGM_LO/HI from the default table. Still succeed: the code VA
|
||||
// lives at ShaderCodeOffset and later binder paths republish it.
|
||||
// GS front headers can likewise start at RSRC1_GS (0x8A) instead of
|
||||
// PGM_LO_GS (0x88) - same deal, skip the patch here.
|
||||
if ((shaderType == HsFrontShaderType && firstLo is SpiShaderPgmRsrc1Hs or SpiShaderPgmLoHs) ||
|
||||
(shaderType == GsFrontShaderType && firstLo is SpiShaderPgmRsrc1Gs or SpiShaderPgmLoGs))
|
||||
{
|
||||
TraceCreateShader(
|
||||
0,
|
||||
headerAddress,
|
||||
codeAddress,
|
||||
$"skip-pgm-patch type={shaderType} first_lo=0x{firstLo:X8}");
|
||||
return true;
|
||||
}
|
||||
|
||||
TraceCreateShader(
|
||||
0,
|
||||
headerAddress,
|
||||
codeAddress,
|
||||
$"unexpected-registers type={shaderType} expected_lo=0x{expectedLo:X8} first_lo=0x{firstLo:X8}");
|
||||
return false;
|
||||
$"skip-pgm-patch type={shaderType} expected_lo=0x{expectedLo:X8} first_lo=0x{firstLo:X8}");
|
||||
return true;
|
||||
}
|
||||
|
||||
var loValue = (uint)((codeAddress >> 8) & 0xFFFF_FFFFUL);
|
||||
|
|
|
|||
|
|
@ -90,7 +90,11 @@ internal sealed record GuestVertexBuffer(
|
|||
byte[] Data,
|
||||
int Length,
|
||||
bool Pooled,
|
||||
bool PerInstance = false);
|
||||
bool PerInstance = false,
|
||||
uint BaseRecord = 0)
|
||||
{
|
||||
public ulong BindingOffsetBytes => (ulong)BaseRecord * Stride;
|
||||
}
|
||||
|
||||
internal sealed record GuestIndexBuffer(
|
||||
byte[] Data,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ internal static partial class MetalVideoPresenter
|
|||
var candidate = vertexBuffers[prior];
|
||||
if (candidate.BaseAddress == buffer.BaseAddress &&
|
||||
candidate.Stride == buffer.Stride &&
|
||||
candidate.Length == buffer.Length)
|
||||
candidate.Length == buffer.Length &&
|
||||
candidate.BaseRecord == buffer.BaseRecord)
|
||||
{
|
||||
slots[index] = slots[prior];
|
||||
shared = true;
|
||||
|
|
@ -981,7 +982,11 @@ internal static partial class MetalVideoPresenter
|
|||
// selects the field inside the interleaved vertex; the bind
|
||||
// offset is just the arena slice.
|
||||
MetalNative.SendSetBuffer(
|
||||
encoder, selSetVertexBuffer, buffer, (nuint)offset, vertexSlots[index]);
|
||||
encoder,
|
||||
selSetVertexBuffer,
|
||||
buffer,
|
||||
(nuint)offset + (nuint)vertexBuffer.BindingOffsetBytes,
|
||||
vertexSlots[index]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4919,6 +4919,12 @@ public static partial class KernelMemoryCompatExports
|
|||
return false;
|
||||
}
|
||||
|
||||
if (KernelVirtualRangeAllocator.TryResolveAddressSpace(ctx.Memory, out var addressSpace) &&
|
||||
addressSpace.TryCommitRange(address, length))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Span<byte> probe = stackalloc byte[1];
|
||||
return ctx.Memory.TryRead(address, probe) &&
|
||||
ctx.Memory.TryRead(address + length - 1, probe);
|
||||
|
|
|
|||
|
|
@ -3677,6 +3677,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||
public uint Stride;
|
||||
public uint OffsetBytes;
|
||||
public bool PerInstance;
|
||||
public uint BaseRecord;
|
||||
}
|
||||
|
||||
private const Format DepthFormat = Format.D32Sfloat;
|
||||
|
|
@ -10777,6 +10778,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||
Stride = guestBuffer.Stride,
|
||||
OffsetBytes = guestBuffer.OffsetBytes,
|
||||
PerInstance = guestBuffer.PerInstance,
|
||||
BaseRecord = guestBuffer.BaseRecord,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -10795,6 +10797,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||
Stride = guestBuffer.Stride,
|
||||
OffsetBytes = guestBuffer.OffsetBytes,
|
||||
PerInstance = guestBuffer.PerInstance,
|
||||
BaseRecord = guestBuffer.BaseRecord,
|
||||
};
|
||||
|
||||
private VkBuffer CreateHostBuffer(
|
||||
|
|
@ -11033,18 +11036,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||
_ => Format.R32Sfloat,
|
||||
};
|
||||
|
||||
private static ulong GetVertexBindingOffset(VertexBufferResource vertexBuffer)
|
||||
{
|
||||
if (vertexBuffer.OffsetBytes < vertexBuffer.Size)
|
||||
{
|
||||
return vertexBuffer.OffsetBytes;
|
||||
}
|
||||
|
||||
TraceVulkanShader(
|
||||
$"vk.vertex_offset_oob loc={vertexBuffer.Location} " +
|
||||
$"offset={vertexBuffer.OffsetBytes} size={vertexBuffer.Size}");
|
||||
return 0;
|
||||
}
|
||||
// OffsetBytes selects the field within each interleaved record. Some
|
||||
// guest fetch prologs apply firstVertex to their own vertex ID, so the
|
||||
// Vulkan draw stays relative and the host stream starts at BaseRecord.
|
||||
private static ulong GetVertexBindingOffset(VertexBufferResource vertexBuffer) =>
|
||||
(ulong)vertexBuffer.BaseRecord * vertexBuffer.Stride;
|
||||
|
||||
private static uint GetDrawVertexCount(
|
||||
uint primitiveType,
|
||||
|
|
|
|||
|
|
@ -836,6 +836,18 @@ public static partial class Gen5SpirvTranslator
|
|||
Ext(37, _floatType, high, right)));
|
||||
break;
|
||||
}
|
||||
case "VSadU32":
|
||||
{
|
||||
// Unsigned absolute difference cannot overflow when it is
|
||||
// expressed as max(a, b) - min(a, b).
|
||||
var left = GetRawSource(instruction, 0);
|
||||
var right = GetRawSource(instruction, 1);
|
||||
var accumulator = GetRawSource(instruction, 2);
|
||||
var maximum = Ext(41, _uintType, left, right);
|
||||
var minimum = Ext(38, _uintType, left, right);
|
||||
result = IAdd(ISubU(maximum, minimum), accumulator);
|
||||
break;
|
||||
}
|
||||
case "VCubeidF32":
|
||||
result = EmitCubeCoordinate(instruction, CubeCoordinate.Id);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,112 @@ namespace SharpEmu.ShaderCompiler;
|
|||
public static class Gen5ShaderTranslator
|
||||
{
|
||||
private static int _dppVectorsValidated;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the user SGPR that an embedded vertex-fetch prolog adds to its
|
||||
/// vertex index. PS5 NGG shaders use v5 and V_SAD_U32; legacy fetch
|
||||
/// shaders use v0 and V_ADD_I32. Only instructions before the first fetch
|
||||
/// qualify, and conflicting candidates fail closed.
|
||||
/// </summary>
|
||||
public static bool TryGetEmbeddedFetchVertexOffsetRegister(
|
||||
Gen5ShaderProgram program,
|
||||
uint userDataBase,
|
||||
int userDataCount,
|
||||
out uint register)
|
||||
{
|
||||
register = 0;
|
||||
uint? candidate = null;
|
||||
foreach (var instruction in program.Instructions)
|
||||
{
|
||||
if (instruction.Control is Gen5BufferMemoryControl)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (instruction.Destinations.Count == 0 ||
|
||||
instruction.Destinations[0] is not
|
||||
{ Kind: Gen5OperandKind.VectorRegister } destination)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var accumulator = destination.Value;
|
||||
var isLegacyAdd =
|
||||
accumulator == 0 &&
|
||||
instruction.Opcode == "VAddI32" &&
|
||||
instruction.Sources.Count >= 2 &&
|
||||
instruction.Sources[1] is
|
||||
{ Kind: Gen5OperandKind.VectorRegister, Value: 0 };
|
||||
var isNggSad =
|
||||
userDataBase == 8 &&
|
||||
accumulator == 5 &&
|
||||
instruction.Opcode == "VSadU32" &&
|
||||
instruction.Sources.Count >= 3 &&
|
||||
instruction.Sources[2] is
|
||||
{ Kind: Gen5OperandKind.VectorRegister, Value: 5 } &&
|
||||
TryGetInlineConstant(instruction.Sources[1], out var sadValue) &&
|
||||
sadValue == 0;
|
||||
if ((!isLegacyAdd && !isNggSad) ||
|
||||
instruction.Sources[0] is not
|
||||
{ Kind: Gen5OperandKind.ScalarRegister } scalar ||
|
||||
scalar.Value < userDataBase ||
|
||||
scalar.Value - userDataBase >= userDataCount)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (candidate.HasValue && candidate.Value != scalar.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
candidate = scalar.Value;
|
||||
}
|
||||
|
||||
if (!candidate.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
register = candidate.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetEmbeddedFetchVertexOffset(
|
||||
Gen5ShaderState state,
|
||||
out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
if (!TryGetEmbeddedFetchVertexOffsetRegister(
|
||||
state.Program,
|
||||
state.UserDataScalarRegisterBase,
|
||||
state.UserData.Count,
|
||||
out var register))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var userDataIndex = register - state.UserDataScalarRegisterBase;
|
||||
offset = unchecked((int)state.UserData[(int)userDataIndex]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetInlineConstant(Gen5Operand operand, out uint value)
|
||||
{
|
||||
if (operand.Kind == Gen5OperandKind.LiteralConstant)
|
||||
{
|
||||
value = operand.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operand.Kind == Gen5OperandKind.EncodedConstant)
|
||||
{
|
||||
return Gen5InlineConstants.TryDecode(operand.Value, out value);
|
||||
}
|
||||
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Bitmask (256 bits) of scalar registers whose values the program can
|
||||
/// observe: scalar source operands (widened for 64-bit pairs), the
|
||||
|
|
@ -2187,7 +2293,11 @@ public static class Gen5ShaderTranslator
|
|||
{
|
||||
var extra = words[1];
|
||||
var vectorAddress = extra & 0xFF;
|
||||
var vectorData = (extra >> 8) & 0xFF;
|
||||
// FLAT/GLOBAL encodes the load destination in the high byte
|
||||
// of the second dword. MUBUF uses bits [15:8] instead, so
|
||||
// sharing that extraction here silently clobbers the wrong
|
||||
// VGPR range for GLOBAL_LOAD_* instructions.
|
||||
var vectorData = (extra >> 24) & 0xFF;
|
||||
var scalarAddress = (extra >> 16) & 0x7F;
|
||||
var usesFlatAddress = opcode.StartsWith(
|
||||
"Flat",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,167 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcCreateShaderTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x3_0000_0000;
|
||||
private const int MemorySize = 0x4000;
|
||||
|
||||
private const ulong DestinationAddress = BaseAddress + 0x0000;
|
||||
private const ulong HeaderAddress = BaseAddress + 0x0100;
|
||||
private const ulong CodeAddress = BaseAddress + 0x1000;
|
||||
private const ulong RegistersAddress = BaseAddress + 0x0500;
|
||||
|
||||
private const uint ShaderFileHeader = 0x34333231;
|
||||
private const uint ShaderVersion = 0x18;
|
||||
|
||||
private const ulong ShaderCodeOffset = 0x10;
|
||||
private const ulong ShaderShRegistersOffset = 0x20;
|
||||
private const ulong ShaderTypeOffset = 0x5A;
|
||||
private const ulong ShaderNumShRegistersOffset = 0x5C;
|
||||
|
||||
private const uint SpiShaderPgmLoPs = 0x8;
|
||||
private const uint SpiShaderPgmHiPs = 0x9;
|
||||
|
||||
[Fact]
|
||||
public void CreateShader_StandardPsShader_PatchesPgmLoHi()
|
||||
{
|
||||
var (memory, ctx) = SetupContext();
|
||||
SetupHeader(memory, shaderType: 1, registerCount: 2);
|
||||
|
||||
// Put PS PGM_LO and PGM_HI registers in the table
|
||||
WriteUInt32(memory, RegistersAddress + 0, SpiShaderPgmLoPs);
|
||||
WriteUInt32(memory, RegistersAddress + 4, 0);
|
||||
WriteUInt32(memory, RegistersAddress + 8, SpiShaderPgmHiPs);
|
||||
WriteUInt32(memory, RegistersAddress + 12, 0);
|
||||
|
||||
ctx[CpuRegister.Rdi] = DestinationAddress;
|
||||
ctx[CpuRegister.Rsi] = HeaderAddress;
|
||||
ctx[CpuRegister.Rdx] = CodeAddress;
|
||||
|
||||
var result = AgcExports.CreateShader(ctx);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(HeaderAddress, ReadUInt64(memory, DestinationAddress));
|
||||
Assert.Equal(CodeAddress, ReadUInt64(memory, HeaderAddress + ShaderCodeOffset));
|
||||
|
||||
var expectedLo = (uint)((CodeAddress >> 8) & 0xFFFF_FFFFUL);
|
||||
var expectedHi = (uint)((CodeAddress >> 40) & 0xFFUL);
|
||||
Assert.Equal(expectedLo, ReadUInt32(memory, RegistersAddress + 4));
|
||||
Assert.Equal(expectedHi, ReadUInt32(memory, RegistersAddress + 12));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateShader_ShaderType8WithoutPgmRegisters_SucceedsAndSkipsPatch()
|
||||
{
|
||||
var (memory, ctx) = SetupContext();
|
||||
SetupHeader(memory, shaderType: 8, registerCount: 2);
|
||||
|
||||
// Registers are user data registers (e.g. 0x0 and 0x1), not PGM_LO
|
||||
WriteUInt32(memory, RegistersAddress + 0, 0x0);
|
||||
WriteUInt32(memory, RegistersAddress + 4, 0x1111);
|
||||
WriteUInt32(memory, RegistersAddress + 8, 0x1);
|
||||
WriteUInt32(memory, RegistersAddress + 12, 0x2222);
|
||||
|
||||
ctx[CpuRegister.Rdi] = DestinationAddress;
|
||||
ctx[CpuRegister.Rsi] = HeaderAddress;
|
||||
ctx[CpuRegister.Rdx] = CodeAddress;
|
||||
|
||||
var result = AgcExports.CreateShader(ctx);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(HeaderAddress, ReadUInt64(memory, DestinationAddress));
|
||||
Assert.Equal(CodeAddress, ReadUInt64(memory, HeaderAddress + ShaderCodeOffset));
|
||||
// Registers table is unchanged
|
||||
Assert.Equal(0x1111u, ReadUInt32(memory, RegistersAddress + 4));
|
||||
Assert.Equal(0x2222u, ReadUInt32(memory, RegistersAddress + 12));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateShader_ZeroRegisters_Succeeds()
|
||||
{
|
||||
var (memory, ctx) = SetupContext();
|
||||
SetupHeader(memory, shaderType: 8, registerCount: 0);
|
||||
|
||||
ctx[CpuRegister.Rdi] = DestinationAddress;
|
||||
ctx[CpuRegister.Rsi] = HeaderAddress;
|
||||
ctx[CpuRegister.Rdx] = CodeAddress;
|
||||
|
||||
var result = AgcExports.CreateShader(ctx);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(HeaderAddress, ReadUInt64(memory, DestinationAddress));
|
||||
Assert.Equal(CodeAddress, ReadUInt64(memory, HeaderAddress + ShaderCodeOffset));
|
||||
}
|
||||
|
||||
private static (FakeCpuMemory Memory, CpuContext Ctx) SetupContext()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
return (memory, ctx);
|
||||
}
|
||||
|
||||
private static void SetupHeader(FakeCpuMemory memory, byte shaderType, byte registerCount)
|
||||
{
|
||||
WriteUInt32(memory, HeaderAddress + 0, ShaderFileHeader);
|
||||
WriteUInt32(memory, HeaderAddress + 4, ShaderVersion);
|
||||
|
||||
// Pointers are relative to their field offset
|
||||
if (registerCount > 0)
|
||||
{
|
||||
var relativeRegisters = RegistersAddress - (HeaderAddress + ShaderShRegistersOffset);
|
||||
WriteUInt64(memory, HeaderAddress + ShaderShRegistersOffset, relativeRegisters);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteUInt64(memory, HeaderAddress + ShaderShRegistersOffset, 0);
|
||||
}
|
||||
|
||||
WriteByte(memory, HeaderAddress + ShaderTypeOffset, shaderType);
|
||||
WriteByte(memory, HeaderAddress + ShaderNumShRegistersOffset, registerCount);
|
||||
}
|
||||
|
||||
private static byte ReadByte(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[1];
|
||||
Assert.True(memory.TryRead(address, buf));
|
||||
return buf[0];
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[sizeof(uint)];
|
||||
Assert.True(memory.TryRead(address, buf));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(buf);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[sizeof(ulong)];
|
||||
Assert.True(memory.TryRead(address, buf));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(buf);
|
||||
}
|
||||
|
||||
private static void WriteByte(FakeCpuMemory memory, ulong address, byte value) =>
|
||||
Assert.True(memory.TryWrite(address, [value]));
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buf, value);
|
||||
Assert.True(memory.TryWrite(address, buf));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buf, value);
|
||||
Assert.True(memory.TryWrite(address, buf));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcDcbDirectRegisterTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x4_0000_0000;
|
||||
private const ulong CommandBufferAddress = BaseAddress + 0x1000;
|
||||
private const ulong CommandsStart = BaseAddress + 0x2000;
|
||||
private const ulong CommandsEnd = BaseAddress + 0x4000;
|
||||
|
||||
[Fact]
|
||||
public void DcbSetShRegisterDirect_WritesPacketAndAdvancesCursor()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x10000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x10, CommandsStart);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x18, CommandsEnd);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x20, 0);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x28, 0);
|
||||
WriteUInt32(memory, CommandBufferAddress + 0x30, 0);
|
||||
|
||||
const uint registerOffset = 0x1234;
|
||||
const uint registerValue = 0xDEADBEEF;
|
||||
context[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
context[CpuRegister.Rsi] = registerOffset | ((ulong)registerValue << 32);
|
||||
|
||||
var result = AgcExports.DcbSetShRegisterDirect(context);
|
||||
|
||||
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
|
||||
Assert.Equal(CommandsStart, context[CpuRegister.Rax]);
|
||||
Assert.Equal(CommandsStart + 12, ReadUInt64(memory, CommandBufferAddress + 0x10));
|
||||
Assert.Equal(0xC0017600u, ReadUInt32(memory, CommandsStart));
|
||||
Assert.Equal(registerOffset, ReadUInt32(memory, CommandsStart + 4));
|
||||
Assert.Equal(registerValue, ReadUInt32(memory, CommandsStart + 8));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DcbSetShRegisterDirectGetSize_ReturnsPacketSize()
|
||||
{
|
||||
var context = new CpuContext(
|
||||
new FakeCpuMemory(BaseAddress, 0x1000),
|
||||
Generation.Gen5);
|
||||
|
||||
Assert.Equal(12, AgcExports.DcbSetShRegisterDirectGetSize(context));
|
||||
Assert.Equal(12UL, context[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
}
|
||||
|
||||
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
Assert.True(memory.TryWrite(address, bytes));
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
|
||||
Assert.True(memory.TryWrite(address, bytes));
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +93,19 @@ public sealed class AgcWaitRegMemTests
|
|||
Assert.Equal(0xDEAD_BEEFu, ReadUInt32(memory, PacketAddress + 16));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u, 56)]
|
||||
[InlineData(1u, 64)]
|
||||
[InlineData(2u, 0)]
|
||||
public void DcbWaitOnAddressGetSize_ReturnsNativePacketSize(uint size, int expected)
|
||||
{
|
||||
CreateMemory(out var ctx);
|
||||
ctx[CpuRegister.Rdi] = size;
|
||||
|
||||
Assert.Equal(expected, AgcExports.DcbWaitOnAddressGetSize(ctx));
|
||||
Assert.Equal((ulong)expected, ctx[CpuRegister.Rax]);
|
||||
}
|
||||
|
||||
private static FakeCpuMemory CreateMemory(out CpuContext ctx)
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x2000);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class Gen5SadSpirvTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
|
||||
[Fact]
|
||||
public void SadU32_EmitsUnsignedAbsoluteDifferenceAndAdd()
|
||||
{
|
||||
// V_SAD_U32 v3, v0, v1, v2
|
||||
var spirv = Compile([0xD15D0003u, 0x040A0300u]);
|
||||
var instructions = EnumerateInstructions(spirv).ToArray();
|
||||
|
||||
Assert.Contains(instructions, instruction =>
|
||||
instruction.Op == (ushort)SpirvOp.ExtInst &&
|
||||
ReadWord(spirv, instruction.Offset + 16) == 41); // UMax
|
||||
Assert.Contains(instructions, instruction =>
|
||||
instruction.Op == (ushort)SpirvOp.ExtInst &&
|
||||
ReadWord(spirv, instruction.Offset + 16) == 38); // UMin
|
||||
Assert.Contains(instructions, instruction => instruction.Op == (ushort)SpirvOp.ISub);
|
||||
Assert.Contains(instructions, instruction => instruction.Op == (ushort)SpirvOp.IAdd);
|
||||
}
|
||||
|
||||
private static byte[] Compile(uint[] programWords)
|
||||
{
|
||||
var memory = new FakeCpuMemory(ShaderAddress, 0x2000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords);
|
||||
var shaderRegisters = new Dictionary<uint, uint>
|
||||
{
|
||||
[Gen5ShaderAtomicDecodeTests.ComputePgmRsrc2Register] = 16u << 1,
|
||||
};
|
||||
|
||||
Assert.True(
|
||||
Gen5ShaderTranslator.TryCreateState(
|
||||
context,
|
||||
ShaderAddress,
|
||||
0,
|
||||
shaderRegisters,
|
||||
Gen5ShaderAtomicDecodeTests.ComputeUserDataRegister,
|
||||
out var state,
|
||||
out var error),
|
||||
error);
|
||||
Assert.True(
|
||||
Gen5ShaderScalarEvaluator.TryEvaluate(context, state, out var evaluation, out error),
|
||||
error);
|
||||
Assert.True(
|
||||
Gen5SpirvTranslator.TryCompileComputeShader(
|
||||
state, evaluation, 1, 1, 1, out var shader, out error),
|
||||
error);
|
||||
return shader.Spirv;
|
||||
}
|
||||
|
||||
private static IEnumerable<(ushort Op, int Offset)> EnumerateInstructions(byte[] spirv)
|
||||
{
|
||||
for (var offset = 5 * sizeof(uint); offset + sizeof(uint) <= spirv.Length;)
|
||||
{
|
||||
var word = ReadWord(spirv, offset);
|
||||
var wordCount = (int)(word >> 16);
|
||||
if (wordCount <= 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return ((ushort)word, offset);
|
||||
offset += wordCount * sizeof(uint);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint ReadWord(byte[] spirv, int offset) =>
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint)));
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class Gen5VertexOffsetTests
|
||||
{
|
||||
[Fact]
|
||||
public void NggSadProlog_ResolvesVertexOffsetFromUserData()
|
||||
{
|
||||
var state = CreateState(
|
||||
new Gen5ShaderInstruction(
|
||||
0,
|
||||
Gen5ShaderEncoding.Vop3,
|
||||
"VSadU32",
|
||||
[],
|
||||
[Gen5Operand.Scalar(18), Gen5Operand.Source(128), Gen5Operand.Vector(5)],
|
||||
[Gen5Operand.Vector(5)],
|
||||
null),
|
||||
userData: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0]);
|
||||
|
||||
Assert.True(Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset(state, out var offset));
|
||||
Assert.Equal(8, offset);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NggSadProlog_WithNonzeroMiddleOperand_IsNotClassified()
|
||||
{
|
||||
var state = CreateState(
|
||||
new Gen5ShaderInstruction(
|
||||
0,
|
||||
Gen5ShaderEncoding.Vop3,
|
||||
"VSadU32",
|
||||
[],
|
||||
[Gen5Operand.Scalar(18), Gen5Operand.Source(129), Gen5Operand.Vector(5)],
|
||||
[Gen5Operand.Vector(5)],
|
||||
null),
|
||||
userData: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0]);
|
||||
|
||||
Assert.False(Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset(state, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VertexOffsetAfterFirstFetch_IsNotClassified()
|
||||
{
|
||||
var fetch = new Gen5ShaderInstruction(
|
||||
0,
|
||||
Gen5ShaderEncoding.Mubuf,
|
||||
"BufferLoadFormatX",
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
new Gen5BufferMemoryControl(1, 0, 0, 0, 0, true, false, false, false));
|
||||
var state = CreateState(
|
||||
fetch,
|
||||
userData: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0],
|
||||
new Gen5ShaderInstruction(
|
||||
4,
|
||||
Gen5ShaderEncoding.Vop3,
|
||||
"VSadU32",
|
||||
[],
|
||||
[Gen5Operand.Scalar(18), Gen5Operand.Source(128), Gen5Operand.Vector(5)],
|
||||
[Gen5Operand.Vector(5)],
|
||||
null));
|
||||
|
||||
Assert.False(Gen5ShaderTranslator.TryGetEmbeddedFetchVertexOffset(state, out _));
|
||||
}
|
||||
|
||||
private static Gen5ShaderState CreateState(
|
||||
Gen5ShaderInstruction first,
|
||||
IReadOnlyList<uint> userData,
|
||||
params Gen5ShaderInstruction[] rest) =>
|
||||
new(
|
||||
new Gen5ShaderProgram(0, [first, .. rest]),
|
||||
userData,
|
||||
Metadata: null,
|
||||
UserDataScalarRegisterBase: 8);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Core.Cpu.Native;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Cpu;
|
||||
|
||||
public sealed class ImportLoopGuardBoundaryTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("n88vx3C5nW8")] // gettimeofday
|
||||
[InlineData("-2IRUCO--PM")] // clock_gettime
|
||||
[InlineData("0V5nU-Z6t4U")] // sceKernelGetProcessTime
|
||||
[InlineData("aI6lQW5v57k")] // sceKernelGetProcessTimeCounter
|
||||
public void TimeObservationResetsRepeatingImportPattern(string nid)
|
||||
{
|
||||
Assert.True(DirectExecutionBackend.IsImportLoopGuardBoundary(nid));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrdinaryImportDoesNotResetRepeatingImportPattern()
|
||||
{
|
||||
Assert.False(DirectExecutionBackend.IsImportLoopGuardBoundary("ordinary-import"));
|
||||
}
|
||||
}
|
||||
|
|
@ -91,6 +91,47 @@ public sealed class VehTrampolineTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public unsafe void ManagedCallbackPreservesWin64NonvolatileRegisters()
|
||||
{
|
||||
if (!Supported) return;
|
||||
|
||||
using var backend = new DirectExecutionBackend(new ModuleManager());
|
||||
Handler handler = _ =>
|
||||
{
|
||||
// Force ordinary managed work and volatile-register traffic while
|
||||
// the generated thunk is active. The wrapper below checks the ABI
|
||||
// registers after this reverse P/Invoke returns.
|
||||
var text = string.Concat("VEH-", Environment.CurrentManagedThreadId, "-", DateTime.UtcNow.Ticks);
|
||||
GC.KeepAlive(text);
|
||||
return -1;
|
||||
};
|
||||
var thunk = backend.CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(handler));
|
||||
Assert.NotEqual(0, thunk);
|
||||
var wrapper = CreateRegisterProbe(thunk);
|
||||
var output = stackalloc ulong[8];
|
||||
var exceptionInfo = CreateExceptionInfo(0xC000001D);
|
||||
try
|
||||
{
|
||||
var result = ((delegate* unmanaged<nint, nint, int>)wrapper)(exceptionInfo, (nint)output);
|
||||
Assert.Equal(-1, result);
|
||||
Assert.Equal(0x1111111111111111UL, output[0]); // rbx
|
||||
Assert.Equal(0x2222222222222222UL, output[1]); // rbp
|
||||
Assert.Equal(0x3333333333333333UL, output[2]); // rsi
|
||||
Assert.Equal(0x4444444444444444UL, output[3]); // rdi
|
||||
Assert.Equal(0x5555555555555555UL, output[4]); // r12
|
||||
Assert.Equal(0x6666666666666666UL, output[5]); // r13
|
||||
Assert.Equal(0x7777777777777777UL, output[6]); // r14
|
||||
Assert.Equal(0x8888888888888888UL, output[7]); // r15
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMemory.Free((void*)exceptionInfo);
|
||||
HostMemory.Free((void*)wrapper, 0, HostMemory.MEM_RELEASE);
|
||||
GC.KeepAlive(handler);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<int> StartCallback(nint thunk) => Task.Factory.StartNew(
|
||||
() => Invoke(thunk, 0xC000001D), CancellationToken.None,
|
||||
TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
|
|
@ -109,4 +150,54 @@ public sealed class VehTrampolineTests
|
|||
pointers[1] = (nint)context;
|
||||
return ((delegate* unmanaged<nint, int>)thunk)((nint)pointers);
|
||||
}
|
||||
|
||||
private static unsafe nint CreateExceptionInfo(uint code)
|
||||
{
|
||||
// The wrapper only needs a valid ExceptionCode field and ContextRecord
|
||||
// pointer; the thunk's native pre-filter reads exactly those members.
|
||||
var pointers = (nint*)NativeMemory.Alloc((nuint)(2 * sizeof(nint) + 0xA0 + 0x4D0));
|
||||
var record = (byte*)(pointers + 2);
|
||||
var context = record + 0xA0;
|
||||
new Span<byte>(record, 0xA0).Clear();
|
||||
new Span<byte>(context, 0x4D0).Clear();
|
||||
*(uint*)record = code;
|
||||
pointers[0] = (nint)record;
|
||||
pointers[1] = (nint)context;
|
||||
return (nint)pointers;
|
||||
}
|
||||
|
||||
private static unsafe nint CreateRegisterProbe(nint thunk)
|
||||
{
|
||||
var code = new List<byte>();
|
||||
void U64(ulong value) { code.AddRange(BitConverter.GetBytes(value)); }
|
||||
void MovReg(byte rex, byte opcode, ulong value) { code.Add(rex); code.Add(opcode); U64(value); }
|
||||
// Preserve caller nonvolatiles, then install sentinels and call thunk.
|
||||
code.AddRange(new byte[] { 0x53, 0x56, 0x57, 0x55, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57 });
|
||||
code.AddRange(new byte[] { 0x48, 0x83, 0xEC, 0x38 });
|
||||
code.AddRange(new byte[] { 0x48, 0x89, 0x54, 0x24, 0x30 }); // [rsp+30] = output
|
||||
MovReg(0x49, 0xBC, 0x5555555555555555UL);
|
||||
MovReg(0x49, 0xBD, 0x6666666666666666UL);
|
||||
MovReg(0x49, 0xBE, 0x7777777777777777UL);
|
||||
MovReg(0x49, 0xBF, 0x8888888888888888UL);
|
||||
MovReg(0x48, 0xBB, 0x1111111111111111UL);
|
||||
MovReg(0x48, 0xBD, 0x2222222222222222UL);
|
||||
MovReg(0x48, 0xBE, 0x3333333333333333UL);
|
||||
MovReg(0x48, 0xBF, 0x4444444444444444UL);
|
||||
code.AddRange(new byte[] { 0x48, 0xB8 }); U64((ulong)thunk); code.AddRange(new byte[] { 0xFF, 0xD0 });
|
||||
code.AddRange(new byte[] { 0x89, 0x44, 0x24, 0x28 }); // [rsp+28] = callback result
|
||||
code.AddRange(new byte[] { 0x48, 0x8B, 0x44, 0x24, 0x30 }); // rax = output
|
||||
code.AddRange(new byte[] { 0x48, 0x89, 0x98, 0, 0, 0, 0 });
|
||||
code.AddRange(new byte[] { 0x48, 0x89, 0xA8, 8, 0, 0, 0 });
|
||||
code.AddRange(new byte[] { 0x48, 0x89, 0xB0, 16, 0, 0, 0 });
|
||||
code.AddRange(new byte[] { 0x48, 0x89, 0xB8, 24, 0, 0, 0 });
|
||||
code.AddRange(new byte[] { 0x4C, 0x89, 0xA0, 32, 0, 0, 0 });
|
||||
code.AddRange(new byte[] { 0x4C, 0x89, 0xA8, 40, 0, 0, 0 });
|
||||
code.AddRange(new byte[] { 0x4C, 0x89, 0xB0, 48, 0, 0, 0 });
|
||||
code.AddRange(new byte[] { 0x4C, 0x89, 0xB8, 56, 0, 0, 0 });
|
||||
code.AddRange(new byte[] { 0x8B, 0x44, 0x24, 0x28 }); // eax = callback result
|
||||
code.AddRange(new byte[] { 0x48, 0x83, 0xC4, 0x38, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5D, 0x5F, 0x5E, 0x5B, 0xC3 });
|
||||
var ptr = (byte*)HostMemory.Alloc(null, (nuint)code.Count, HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE, HostMemory.PAGE_EXECUTE_READWRITE);
|
||||
code.ToArray().AsSpan().CopyTo(new Span<byte>(ptr, code.Count));
|
||||
return (nint)ptr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,25 @@ public sealed class PhysicalVirtualMemoryTests
|
|||
Assert.Single(host.CommitCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCommitRangeCommitsEveryPageInReserveOnlyMapping()
|
||||
{
|
||||
using var host = new LazyZeroedHostMemory();
|
||||
using var memory = new PhysicalVirtualMemory(host);
|
||||
|
||||
var address = memory.AllocateAt(0, (4UL << 30) + 0x1000, executable: false);
|
||||
host.CommitCalls.Clear();
|
||||
|
||||
Assert.True(memory.TryCommitRange(address + 0x1000, 0x3000));
|
||||
Assert.Equal(
|
||||
[
|
||||
(address + 0x1000, 0x1000UL, HostPageProtection.ReadWrite),
|
||||
(address + 0x2000, 0x1000UL, HostPageProtection.ReadWrite),
|
||||
(address + 0x3000, 0x1000UL, HostPageProtection.ReadWrite),
|
||||
],
|
||||
host.CommitCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCopyHandlesOverlappingIdentityMappedRanges()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -102,6 +102,53 @@ public sealed class Gen5FlatMemoryTests
|
|||
ReadSpirvOpcodes(compiled.Spirv));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GlobalLoadUsesHighByteForVectorDestination()
|
||||
{
|
||||
var memory = new TestCpuMemory(ShaderAddress, 0x4000);
|
||||
uint[] words =
|
||||
[
|
||||
// global_load_dwordx4 v[8:11], v9, s[16:17]
|
||||
0xDC388000,
|
||||
0x08100009,
|
||||
SEndpgm,
|
||||
];
|
||||
var shader = new byte[words.Length * sizeof(uint)];
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
shader.AsSpan(index * sizeof(uint)),
|
||||
words[index]);
|
||||
}
|
||||
Assert.True(memory.TryWrite(ShaderAddress, shader));
|
||||
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
Assert.True(
|
||||
Gen5ShaderTranslator.TryDecodeProgram(
|
||||
ctx,
|
||||
ShaderAddress,
|
||||
out var program,
|
||||
out var decodeError),
|
||||
decodeError);
|
||||
|
||||
var instruction = Assert.Single(
|
||||
program.Instructions,
|
||||
item => item.Opcode == "GlobalLoadDwordx4");
|
||||
var control = Assert.IsType<Gen5GlobalMemoryControl>(
|
||||
instruction.Control);
|
||||
Assert.Equal(9u, control.VectorAddress);
|
||||
Assert.Equal(8u, control.VectorData);
|
||||
Assert.Equal(16u, control.ScalarAddress);
|
||||
Assert.Equal(
|
||||
[
|
||||
Gen5Operand.Vector(8),
|
||||
Gen5Operand.Vector(9),
|
||||
Gen5Operand.Vector(10),
|
||||
Gen5Operand.Vector(11),
|
||||
],
|
||||
instruction.Destinations);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ushort> ReadSpirvOpcodes(byte[] spirv)
|
||||
{
|
||||
Assert.Equal(0, spirv.Length % sizeof(uint));
|
||||
|
|
|
|||
Loading…
Reference in New Issue