mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 06:29:46 +08:00
fix(agc): implement Rewind/Jump writers and IT_REWIND waits
GTA Subrender AVs came from AcbJumpGetSize / DcbRewind returning NOT_FOUND as packet sizes. Add IT_REWIND and INDIRECT_BUFFER writers, patch SetRewindState into the GPU wait registry, and nest-parse 4-dword jumps. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -55,6 +55,8 @@ public static partial class AgcExports
|
|||||||
private const uint ItEventWrite = 0x46;
|
private const uint ItEventWrite = 0x46;
|
||||||
private const uint ItReleaseMem = 0x49;
|
private const uint ItReleaseMem = 0x49;
|
||||||
private const uint ItDmaData = 0x50;
|
private const uint ItDmaData = 0x50;
|
||||||
|
// Prospero IT_REWIND — 2-dword packet; body dword bit 31 carries RewindState.
|
||||||
|
private const uint ItRewind = 0x59;
|
||||||
private const uint ItSetContextReg = 0x69;
|
private const uint ItSetContextReg = 0x69;
|
||||||
private const uint ItSetShReg = 0x76;
|
private const uint ItSetShReg = 0x76;
|
||||||
private const uint ItSetUconfigReg = 0x79;
|
private const uint ItSetUconfigReg = 0x79;
|
||||||
@@ -3403,6 +3405,52 @@ public static partial class AgcExports
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IT_REWIND: valid bit (body dword bit 31) means subsequent packets
|
||||||
|
// are ready. Hardware polls until SetRewindState patches it; we
|
||||||
|
// suspend the DCB on the same wait registry used by WAIT_REG_MEM.
|
||||||
|
if (op == ItRewind && length >= 2)
|
||||||
|
{
|
||||||
|
if (HandleSubmittedRewind(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
commandAddress,
|
||||||
|
currentAddress,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
dwordCount,
|
||||||
|
tracePackets))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IT_INDIRECT_BUFFER (4-dword jump): run the nested buffer, then
|
||||||
|
// resume. 14-dword conditional branch form is not implemented yet.
|
||||||
|
if (op == ItIndirectBuffer && length == 4)
|
||||||
|
{
|
||||||
|
if (!TryRunSubmittedIndirectBuffer(
|
||||||
|
ctx,
|
||||||
|
gpuState,
|
||||||
|
state,
|
||||||
|
currentAddress,
|
||||||
|
tracePackets,
|
||||||
|
out var nestedSuspended))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nestedSuspended)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += length;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (op == ItNop &&
|
if (op == ItNop &&
|
||||||
register is RDrawReset or RAcbReset &&
|
register is RDrawReset or RAcbReset &&
|
||||||
length >= 2)
|
length >= 2)
|
||||||
@@ -5106,6 +5154,148 @@ public static partial class AgcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true when the DCB should suspend until the rewind valid bit is set.
|
||||||
|
private static bool HandleSubmittedRewind(
|
||||||
|
CpuContext ctx,
|
||||||
|
SubmittedDcbState state,
|
||||||
|
ulong commandAddress,
|
||||||
|
ulong packetAddress,
|
||||||
|
uint offset,
|
||||||
|
uint length,
|
||||||
|
uint dwordCount,
|
||||||
|
bool tracePacket)
|
||||||
|
{
|
||||||
|
var bodyAddress = packetAddress + sizeof(uint);
|
||||||
|
if (!TryReadUInt32(ctx, bodyAddress, out var body))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint validMask = 1u << 31;
|
||||||
|
if ((body & validMask) != 0)
|
||||||
|
{
|
||||||
|
if (tracePacket)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.rewind queue={state.QueueName} " +
|
||||||
|
$"packet=0x{packetAddress:X16} valid=1");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracePacket)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.rewind_wait queue={state.QueueName} " +
|
||||||
|
$"packet=0x{packetAddress:X16} body=0x{bodyAddress:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var waiter = new GpuWaitRegistry.WaitingDcb
|
||||||
|
{
|
||||||
|
CommandBufferAddress = commandAddress,
|
||||||
|
ResumeAddress = packetAddress + ((ulong)length * sizeof(uint)),
|
||||||
|
TotalDwords = dwordCount,
|
||||||
|
ResumeOffset = offset + length,
|
||||||
|
ReferenceValue = validMask,
|
||||||
|
Mask = validMask,
|
||||||
|
CompareFunction = 3, // equal
|
||||||
|
ControlValue = 0,
|
||||||
|
Is64Bit = false,
|
||||||
|
IsStandard = false,
|
||||||
|
WaitAddress = bodyAddress,
|
||||||
|
Memory = ctx.Memory,
|
||||||
|
QueueName = state.QueueName,
|
||||||
|
SubmissionId = state.ActiveSubmissionId,
|
||||||
|
RegisteredTicks = System.Diagnostics.Stopwatch.GetTimestamp(),
|
||||||
|
State = state,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!_gpuWaitSuspendEnabled)
|
||||||
|
{
|
||||||
|
// Force-open: treat as valid so parsing continues. Titles that rely
|
||||||
|
// on patch-before-execute still get a correct packet from the HLE
|
||||||
|
// writers; only the wait-for-valid race is skipped.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GpuWaitRegistry.Register(bodyAddress, waiter);
|
||||||
|
var gpuState = _submittedGpuStates.GetValue(
|
||||||
|
ctx.Memory,
|
||||||
|
static _ => new SubmittedGpuState());
|
||||||
|
EnsureGpuWaitMonitor(ctx, gpuState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryRunSubmittedIndirectBuffer(
|
||||||
|
CpuContext ctx,
|
||||||
|
SubmittedGpuState gpuState,
|
||||||
|
SubmittedDcbState state,
|
||||||
|
ulong packetAddress,
|
||||||
|
bool tracePackets,
|
||||||
|
out bool suspended)
|
||||||
|
{
|
||||||
|
suspended = false;
|
||||||
|
if (!TryReadUInt32(ctx, packetAddress + 4, out var addressLo) ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 8, out var addressHi) ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 12, out var control))
|
||||||
|
{
|
||||||
|
TracePacketParseFailure(state, packetAddress, 0, 0, "indirect-buffer-read");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nestedAddress = ((ulong)addressHi << 32) | (addressLo & ~0x3u);
|
||||||
|
var nestedDwords = control & 0xF_FFFFu;
|
||||||
|
if (nestedDwords == 0 || nestedAddress == 0)
|
||||||
|
{
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.indirect_buffer queue={state.QueueName} " +
|
||||||
|
$"packet=0x{packetAddress:X16} empty " +
|
||||||
|
$"control=0x{control:X8}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.indirect_buffer queue={state.QueueName} " +
|
||||||
|
$"packet=0x{packetAddress:X16} " +
|
||||||
|
$"target=0x{nestedAddress:X16} dwords={nestedDwords} " +
|
||||||
|
$"control=0x{control:X8}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested reads are outside the parent DCB window — clear it so
|
||||||
|
// TryRead* falls through to guest memory for the jump target.
|
||||||
|
var savedWindow = _dcbWindowBuffer;
|
||||||
|
var savedStart = _dcbWindowStart;
|
||||||
|
var savedLength = _dcbWindowByteLength;
|
||||||
|
_dcbWindowBuffer = null;
|
||||||
|
_dcbWindowByteLength = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Nested jump shares queue state with the parent buffer.
|
||||||
|
suspended = ParseSubmittedDcbCore(
|
||||||
|
ctx,
|
||||||
|
gpuState,
|
||||||
|
state,
|
||||||
|
nestedAddress,
|
||||||
|
nestedDwords,
|
||||||
|
tracePackets);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dcbWindowBuffer = savedWindow;
|
||||||
|
_dcbWindowStart = savedStart;
|
||||||
|
_dcbWindowByteLength = savedLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool HandleSubmittedWaitRegMem(
|
private static bool HandleSubmittedWaitRegMem(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
SubmittedDcbState state,
|
SubmittedDcbState state,
|
||||||
@@ -12730,9 +12920,8 @@ public static partial class AgcExports
|
|||||||
$"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}");
|
$"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hardware REWIND is a fixed 2-dword header + valid-bit packet (same floor
|
// Hardware REWIND is a fixed 2-dword IT_REWIND packet. Body dword bit 31
|
||||||
// as CbNopGetSize). No Rewind writer is implemented yet; size-only is enough
|
// holds RewindState (patched later via sceAgcRewindPatchSetRewindState).
|
||||||
// for callers that allocate the packet before filling it.
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "QIXCsbipds0",
|
Nid = "QIXCsbipds0",
|
||||||
ExportName = "sceAgcDcbRewindGetSize",
|
ExportName = "sceAgcDcbRewindGetSize",
|
||||||
@@ -12744,7 +12933,96 @@ public static partial class AgcExports
|
|||||||
return (int)ctx[CpuRegister.Rax];
|
return (int)ctx[CpuRegister.Rax];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches the 4-dword INDIRECT_BUFFER packet DcbJump writes below.
|
[SysAbiExport(
|
||||||
|
Nid = "0ZOG0jc9nRg",
|
||||||
|
ExportName = "sceAgcAcbRewindGetSize",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int AcbRewindGetSize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 2u * sizeof(uint);
|
||||||
|
return (int)ctx[CpuRegister.Rax];
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "zfcxg-ewMK8",
|
||||||
|
ExportName = "sceAgcDcbRewind",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbRewind(CpuContext ctx) =>
|
||||||
|
WriteRewindPacket(ctx, ctx[CpuRegister.Rdi], (uint)ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DwICrVxerkY",
|
||||||
|
ExportName = "sceAgcAcbRewind",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int AcbRewind(CpuContext ctx) =>
|
||||||
|
WriteRewindPacket(ctx, ctx[CpuRegister.Rdi], (uint)ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
private static int WriteRewindPacket(CpuContext ctx, ulong commandBufferAddress, uint initialState)
|
||||||
|
{
|
||||||
|
if (commandBufferAddress == 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var cmd) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd, Pm4(2, ItRewind, RZero)) ||
|
||||||
|
!ctx.TryWriteUInt32(cmd + 4, (initialState & 1u) << 31))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReturnPointer(ctx, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patches dword 1 of an IT_REWIND packet previously returned by DcbRewind /
|
||||||
|
// AcbRewind. Leaving this unresolved returned NOT_FOUND which titles then
|
||||||
|
// treated as a packet pointer (write AV on Subrender).
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ziVA3whp3p4",
|
||||||
|
ExportName = "sceAgcRewindPatchSetRewindState",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int RewindPatchSetRewindState(CpuContext ctx) =>
|
||||||
|
SetRewindPacketState(ctx, ctx[CpuRegister.Rdi], (uint)ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "eWaWyFegzgQ",
|
||||||
|
ExportName = "sceAgcAsyncRewindPatchSetRewindState",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int AsyncRewindPatchSetRewindState(CpuContext ctx) =>
|
||||||
|
SetRewindPacketState(ctx, ctx[CpuRegister.Rdi], (uint)ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
private static int SetRewindPacketState(CpuContext ctx, ulong packetAddress, uint state)
|
||||||
|
{
|
||||||
|
if (packetAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bodyAddress = packetAddress + sizeof(uint);
|
||||||
|
if (!TryReadUInt32(ctx, bodyAddress, out var body))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
body = (body & ~(1u << 31)) | ((state & 1u) << 31);
|
||||||
|
if (!ctx.TryWriteUInt32(bodyAddress, body))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latch any DCB suspended on this rewind valid bit (registered when
|
||||||
|
// the parser hit IT_REWIND with valid=0). The GPU wait monitor drains
|
||||||
|
// latched waiters on the next CollectSatisfied pass.
|
||||||
|
GpuWaitRegistry.RecordProduced(ctx.Memory, bodyAddress, body);
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches the 4-dword INDIRECT_BUFFER packet DcbJump / AcbJump write.
|
||||||
// Returning NOT_FOUND here left callers with a null packet pointer and an
|
// Returning NOT_FOUND here left callers with a null packet pointer and an
|
||||||
// immediate write AV on RenderThread.
|
// immediate write AV on RenderThread.
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -12758,26 +13036,76 @@ public static partial class AgcExports
|
|||||||
return (int)ctx[CpuRegister.Rax];
|
return (int)ctx[CpuRegister.Rax];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "b-oySn+G2tE",
|
||||||
|
ExportName = "sceAgcAcbJumpGetSize",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int AcbJumpGetSize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 4u * sizeof(uint);
|
||||||
|
return (int)ctx[CpuRegister.Rax];
|
||||||
|
}
|
||||||
|
|
||||||
|
// sceAgcDcbJump(CommandBuffer* buf, uint8_t mode, uint8_t cachePolicy,
|
||||||
|
// const uint32_t* target, uint32_t sizeInDwords)
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "xSAR0LTcRKM",
|
Nid = "xSAR0LTcRKM",
|
||||||
ExportName = "sceAgcDcbJump",
|
ExportName = "sceAgcDcbJump",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAgc")]
|
LibraryName = "libSceAgc")]
|
||||||
public static int DcbJump(CpuContext ctx)
|
public static int DcbJump(CpuContext ctx) =>
|
||||||
|
WriteIndirectBufferJump(
|
||||||
|
ctx,
|
||||||
|
ctx[CpuRegister.Rdi],
|
||||||
|
(uint)(ctx[CpuRegister.Rsi] & 0xFFu),
|
||||||
|
(uint)(ctx[CpuRegister.Rdx] & 0xFFu),
|
||||||
|
ctx[CpuRegister.Rcx],
|
||||||
|
(uint)ctx[CpuRegister.R8]);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "e1DFTg+Sd8U",
|
||||||
|
ExportName = "sceAgcAcbJump",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int AcbJump(CpuContext ctx) =>
|
||||||
|
WriteIndirectBufferJump(
|
||||||
|
ctx,
|
||||||
|
ctx[CpuRegister.Rdi],
|
||||||
|
(uint)(ctx[CpuRegister.Rsi] & 0xFFu),
|
||||||
|
(uint)(ctx[CpuRegister.Rdx] & 0xFFu),
|
||||||
|
ctx[CpuRegister.Rcx],
|
||||||
|
(uint)ctx[CpuRegister.R8]);
|
||||||
|
|
||||||
|
private static int WriteIndirectBufferJump(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong commandBufferAddress,
|
||||||
|
uint mode,
|
||||||
|
uint cachePolicy,
|
||||||
|
ulong target,
|
||||||
|
uint sizeDwords)
|
||||||
{
|
{
|
||||||
var dcb = ctx[CpuRegister.Rdi];
|
if (commandBufferAddress == 0)
|
||||||
var target = ctx[CpuRegister.Rsi];
|
|
||||||
var sizeDwords = (uint)ctx[CpuRegister.Rdx];
|
|
||||||
if (dcb == 0)
|
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
|
// Prospero INDIRECT_BUFFER control dword layout:
|
||||||
|
// bits[19:0] = size in dwords
|
||||||
|
// bit 20 = mode
|
||||||
|
// bits[29:28] = cache policy
|
||||||
|
// remaining high bits use the 0x0F200000 base flags
|
||||||
|
var control =
|
||||||
|
0x0F20_0000u |
|
||||||
|
((cachePolicy & 0x3u) << 28) |
|
||||||
|
((mode & 0x1u) << 20) |
|
||||||
|
(sizeDwords & 0xF_FFFFu);
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 4, out var cmd) ||
|
||||||
!ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) ||
|
!ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) ||
|
||||||
!ctx.TryWriteUInt32(cmd + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
|
!ctx.TryWriteUInt32(cmd + 4, (uint)(target & ~0x3UL)) ||
|
||||||
!ctx.TryWriteUInt32(cmd + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
|
!ctx.TryWriteUInt32(cmd + 8, (uint)(target >> 32)) ||
|
||||||
!ctx.TryWriteUInt32(cmd + 12, sizeDwords & 0xFFFFF))
|
!ctx.TryWriteUInt32(cmd + 12, control))
|
||||||
{
|
{
|
||||||
return ReturnPointer(ctx, 0);
|
return ReturnPointer(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user