mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-03 16:39:51 +08:00
AGC: follow command-buffer branches across arena switches (#720)
Implement sceAgcCbBranch and walk INDIRECT_BUFFER so submissions that continue in a linked buffer keep their flip and end-of-frame labels.
This commit is contained in:
@@ -608,6 +608,11 @@ public static partial class AgcExports
|
|||||||
public Queue<PendingSubmission> PendingSubmissions { get; } = new();
|
public Queue<PendingSubmission> PendingSubmissions { get; } = new();
|
||||||
public bool HasActiveSubmission { get; set; }
|
public bool HasActiveSubmission { get; set; }
|
||||||
public bool IsSuspended { get; set; }
|
public bool IsSuspended { get; set; }
|
||||||
|
|
||||||
|
// Set when parsing stops on an INDIRECT_BUFFER packet so the caller can
|
||||||
|
// continue into the buffer it links to.
|
||||||
|
public ulong PendingChainAddress { get; set; }
|
||||||
|
public uint PendingChainDwords { get; set; }
|
||||||
public ulong CompletionEventNotifiedSubmissionId { get; set; }
|
public ulong CompletionEventNotifiedSubmissionId { get; set; }
|
||||||
public Dictionary<(uint Op, uint Register), uint> FramePacketCounts { get; } = new();
|
public Dictionary<(uint Op, uint Register), uint> FramePacketCounts { get; } = new();
|
||||||
public uint FramePacketCount { get; set; }
|
public uint FramePacketCount { get; set; }
|
||||||
@@ -3250,6 +3255,7 @@ public static partial class AgcExports
|
|||||||
!TryReadUInt64(ctx, packetAddress, out var commandAddress) ||
|
!TryReadUInt64(ctx, packetAddress, out var commandAddress) ||
|
||||||
!TryReadUInt32(ctx, packetAddress + 8, out var dwordCount))
|
!TryReadUInt32(ctx, packetAddress + 8, out var dwordCount))
|
||||||
{
|
{
|
||||||
|
TraceAgc($"agc.driver_submit_dcb_rejected packet=0x{packetAddress:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3262,10 +3268,9 @@ public static partial class AgcExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracePackets)
|
TraceAgc(
|
||||||
{
|
$"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} " +
|
||||||
TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}");
|
$"dwords={dwordCount} end=0x{commandAddress + ((ulong)dwordCount * sizeof(uint)):X16}");
|
||||||
}
|
|
||||||
|
|
||||||
GuestGpu.Current.AttachGuestMemory(ctx.Memory);
|
GuestGpu.Current.AttachGuestMemory(ctx.Memory);
|
||||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
@@ -3312,12 +3317,10 @@ public static partial class AgcExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracePackets)
|
TraceAgc(
|
||||||
{
|
$"agc.driver_submit_acb owner={ownerHandle} packet=0x{packetAddress:X16} " +
|
||||||
TraceAgc(
|
$"addr=0x{commandAddress:X16} dwords={dwordCount} " +
|
||||||
$"agc.driver_submit_acb owner={ownerHandle} packet=0x{packetAddress:X16} " +
|
$"end=0x{commandAddress + ((ulong)dwordCount * sizeof(uint)):X16}");
|
||||||
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
|
||||||
}
|
|
||||||
|
|
||||||
GuestGpu.Current.AttachGuestMemory(ctx.Memory);
|
GuestGpu.Current.AttachGuestMemory(ctx.Memory);
|
||||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
@@ -3574,33 +3577,72 @@ public static partial class AgcExports
|
|||||||
using var guestQueueScope = GuestGpu.Current.EnterGuestQueue(
|
using var guestQueueScope = GuestGpu.Current.EnterGuestQueue(
|
||||||
state.QueueName,
|
state.QueueName,
|
||||||
state.ActiveSubmissionId);
|
state.ActiveSubmissionId);
|
||||||
var windowByteCount = checked((int)(dwordCount * sizeof(uint)));
|
// A submission is one link of a chain, not necessarily the whole stream:
|
||||||
var rented = GuestDataPool.Shared.Rent(windowByteCount);
|
// when a title's command arena fills mid-frame it continues in a fresh
|
||||||
try
|
// buffer and links the two with an INDIRECT_BUFFER packet, then submits
|
||||||
|
// only the first link. Stopping at the end of the submitted window drops
|
||||||
|
// every packet past the switch -- including the flip and the end-of-frame
|
||||||
|
// completion labels the guest is waiting on.
|
||||||
|
for (var chainDepth = 0; ; chainDepth++)
|
||||||
{
|
{
|
||||||
if (ctx.Memory.TryRead(commandAddress, rented.AsSpan(0, windowByteCount)))
|
if (chainDepth > MaxSubmittedChainDepth)
|
||||||
{
|
{
|
||||||
_dcbWindowBuffer = rented;
|
TraceAgc(
|
||||||
_dcbWindowStart = commandAddress;
|
$"agc.dcb_chain_depth_exceeded queue={state.QueueName} " +
|
||||||
_dcbWindowByteLength = windowByteCount;
|
$"submission={state.ActiveSubmissionId} addr=0x{commandAddress:X16}");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseSubmittedDcbCore(
|
state.PendingChainAddress = 0;
|
||||||
ctx,
|
state.PendingChainDwords = 0;
|
||||||
gpuState,
|
var windowByteCount = checked((int)(dwordCount * sizeof(uint)));
|
||||||
state,
|
var rented = GuestDataPool.Shared.Rent(windowByteCount);
|
||||||
commandAddress,
|
bool suspended;
|
||||||
dwordCount,
|
try
|
||||||
tracePackets);
|
{
|
||||||
}
|
if (ctx.Memory.TryRead(commandAddress, rented.AsSpan(0, windowByteCount)))
|
||||||
finally
|
{
|
||||||
{
|
_dcbWindowBuffer = rented;
|
||||||
_dcbWindowBuffer = null;
|
_dcbWindowStart = commandAddress;
|
||||||
_dcbWindowByteLength = 0;
|
_dcbWindowByteLength = windowByteCount;
|
||||||
GuestDataPool.Shared.Return(rented);
|
}
|
||||||
|
|
||||||
|
suspended = ParseSubmittedDcbCore(
|
||||||
|
ctx,
|
||||||
|
gpuState,
|
||||||
|
state,
|
||||||
|
commandAddress,
|
||||||
|
dwordCount,
|
||||||
|
tracePackets);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dcbWindowBuffer = null;
|
||||||
|
_dcbWindowByteLength = 0;
|
||||||
|
GuestDataPool.Shared.Return(rented);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suspended)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chainAddress = state.PendingChainAddress;
|
||||||
|
var chainDwords = state.PendingChainDwords;
|
||||||
|
if (chainAddress == 0 || chainDwords == 0 || chainDwords > 1_000_000)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
commandAddress = chainAddress;
|
||||||
|
dwordCount = chainDwords;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deep enough for a title that links one continuation buffer per frame,
|
||||||
|
// shallow enough that a self-referencing chain cannot spin forever.
|
||||||
|
private const int MaxSubmittedChainDepth = 64;
|
||||||
|
|
||||||
private static bool ParseSubmittedDcbCore(
|
private static bool ParseSubmittedDcbCore(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
SubmittedGpuState gpuState,
|
SubmittedGpuState gpuState,
|
||||||
@@ -3727,6 +3769,32 @@ public static partial class AgcExports
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (op == ItIndirectBuffer &&
|
||||||
|
length >= 4 &&
|
||||||
|
TryReadUInt32(ctx, currentAddress + 4, out var chainLow) &&
|
||||||
|
TryReadUInt32(ctx, currentAddress + 8, out var chainHigh) &&
|
||||||
|
TryReadUInt32(ctx, currentAddress + 12, out var chainDwords))
|
||||||
|
{
|
||||||
|
var chainAddress = ((ulong)(chainHigh & 0xFFFFu) << 32) | chainLow;
|
||||||
|
var chainLength = chainDwords & 0xFFFFFu;
|
||||||
|
// Titles emit a zeroed INDIRECT_BUFFER as padding for a branch they
|
||||||
|
// decided not to take. Only a populated one redirects the stream.
|
||||||
|
if (chainAddress != 0 && chainLength != 0)
|
||||||
|
{
|
||||||
|
state.PendingChainAddress = chainAddress;
|
||||||
|
state.PendingChainDwords = chainLength;
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb_chain queue={state.QueueName} " +
|
||||||
|
$"submission={state.ActiveSubmissionId} " +
|
||||||
|
$"packet=0x{currentAddress:X16} " +
|
||||||
|
$"target=0x{chainAddress:X16} dwords={chainLength}");
|
||||||
|
|
||||||
|
// The link is a jump, not a call: whatever follows it in this
|
||||||
|
// buffer is unreachable padding.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (op == ItNop &&
|
if (op == ItNop &&
|
||||||
register is RDrawReset or RAcbReset &&
|
register is RDrawReset or RAcbReset &&
|
||||||
length >= 2)
|
length >= 2)
|
||||||
@@ -13582,6 +13650,58 @@ public static partial class AgcExports
|
|||||||
return ReturnPointer(ctx, cmd);
|
return ReturnPointer(ctx, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matches the 4-dword INDIRECT_BUFFER packet CbBranch writes below.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uZW-mqsxkrM",
|
||||||
|
ExportName = "sceAgcCbBranchGetSize",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int CbBranchGetSize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 4u * sizeof(uint);
|
||||||
|
return (int)ctx[CpuRegister.Rax];
|
||||||
|
}
|
||||||
|
|
||||||
|
// How a title continues a frame whose command arena filled: it branches from
|
||||||
|
// the tail of the exhausted buffer into a fresh one and submits only the first
|
||||||
|
// buffer, leaving the driver to follow the link. Dropping this packet strands
|
||||||
|
// everything written after the switch -- for UE 4.27 that is the rest of the
|
||||||
|
// frame, including its flip and the end-of-frame labels the guest's AGC
|
||||||
|
// interrupt thread needs before it will trigger the backbuffer event.
|
||||||
|
//
|
||||||
|
// The branch target and its length arrive on the stack, past six register
|
||||||
|
// arguments (verified against a live call: the values matched the continuation
|
||||||
|
// buffer the title had already written into).
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "w1KFAHVqpaU",
|
||||||
|
ExportName = "sceAgcCbBranch",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int CbBranch(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (commandBufferAddress == 0 ||
|
||||||
|
!TryReadUInt64(ctx, ctx[CpuRegister.Rsp] + (2 * sizeof(ulong)), out var target) ||
|
||||||
|
!TryReadUInt64(ctx, ctx[CpuRegister.Rsp] + (3 * sizeof(ulong)), out var targetDwords))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 4, out var commandAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress, Pm4(4, ItIndirectBuffer, RZero)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)targetDwords & 0xFFFFFu))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.cb_branch buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " +
|
||||||
|
$"target=0x{target:X16} dwords={targetDwords}");
|
||||||
|
return ReturnPointer(ctx, commandAddress);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "b-oySn+G2tE",
|
Nid = "b-oySn+G2tE",
|
||||||
ExportName = "sceAgcAcbJumpGetSize",
|
ExportName = "sceAgcAcbJumpGetSize",
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
// 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 SharpEmu.Libs.Kernel;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Tests.Agc;
|
||||||
|
|
||||||
|
// The kernel event-queue registry is process-wide static state; serialize against
|
||||||
|
// other suites that register graphics events.
|
||||||
|
[CollectionDefinition(AgcCommandBufferChainCollection.Name, DisableParallelization = true)]
|
||||||
|
public sealed class AgcCommandBufferChainCollection
|
||||||
|
{
|
||||||
|
public const string Name = "AgcCommandBufferChainState";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A submission is one link of a chain, not always the whole command stream. When a
|
||||||
|
// title's command arena fills mid-frame it continues in a fresh buffer, links the two
|
||||||
|
// with an INDIRECT_BUFFER packet and submits only the first link, so a parser that
|
||||||
|
// stops at the end of the submitted window silently drops the rest of that frame --
|
||||||
|
// including its flip and the end-of-frame labels the guest waits on.
|
||||||
|
[Collection(AgcCommandBufferChainCollection.Name)]
|
||||||
|
public sealed class AgcCommandBufferChainTests
|
||||||
|
{
|
||||||
|
private const ulong BaseAddress = 0x1_0000_0000;
|
||||||
|
private const int MemorySize = 0x4000;
|
||||||
|
|
||||||
|
private const ulong HandleOutAddress = BaseAddress + 0x100;
|
||||||
|
private const ulong EventsAddress = BaseAddress + 0x200;
|
||||||
|
private const ulong OutCountAddress = BaseAddress + 0x300;
|
||||||
|
private const ulong TimeoutAddress = BaseAddress + 0x400;
|
||||||
|
private const ulong SubmitPacketAddress = BaseAddress + 0x500;
|
||||||
|
private const ulong StackAddress = BaseAddress + 0x600;
|
||||||
|
|
||||||
|
private const ulong CommandBufferAddress = BaseAddress + 0x800;
|
||||||
|
private const ulong FirstLinkAddress = BaseAddress + 0x1000;
|
||||||
|
private const ulong SecondLinkAddress = BaseAddress + 0x2000;
|
||||||
|
private const ulong WaitLabelAddress = BaseAddress + 0x3000;
|
||||||
|
|
||||||
|
// Graphics-queue completions land on ident 0, so its absence is how a suspended
|
||||||
|
// queue is observed without standing up a GPU backend.
|
||||||
|
private const ulong GraphicsCompletionIdent = 0;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SubmittedDcb_FollowsIndirectBufferIntoTheChainedBuffer()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
var equeue = CreateEqueue(ctx, memory);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RegisterGraphicsCompletion(equeue);
|
||||||
|
|
||||||
|
// The chained buffer parks on a label that never reaches its reference,
|
||||||
|
// so reaching it at all suspends the queue.
|
||||||
|
var secondLinkDwords = WriteUnsatisfiedWait(ctx, memory, SecondLinkAddress);
|
||||||
|
var firstLinkDwords = WriteChain(
|
||||||
|
ctx,
|
||||||
|
memory,
|
||||||
|
FirstLinkAddress,
|
||||||
|
SecondLinkAddress,
|
||||||
|
secondLinkDwords);
|
||||||
|
|
||||||
|
SubmitDcb(ctx, memory, FirstLinkAddress, firstLinkDwords);
|
||||||
|
|
||||||
|
Assert.NotEqual(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
WaitEqueue(ctx, memory, equeue));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DeleteEqueue(ctx, equeue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Titles emit a zeroed INDIRECT_BUFFER as padding for a branch they decided not to
|
||||||
|
// take. Treating that as a redirect truncates the frame it appears in.
|
||||||
|
[Fact]
|
||||||
|
public void SubmittedDcb_KeepsParsingPastAnEmptyIndirectBuffer()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
var equeue = CreateEqueue(ctx, memory);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RegisterGraphicsCompletion(equeue);
|
||||||
|
|
||||||
|
var paddingDwords = WriteChain(ctx, memory, FirstLinkAddress, target: 0, targetDwords: 0);
|
||||||
|
var waitDwords = WriteUnsatisfiedWait(
|
||||||
|
ctx,
|
||||||
|
memory,
|
||||||
|
FirstLinkAddress + (paddingDwords * sizeof(uint)));
|
||||||
|
|
||||||
|
SubmitDcb(ctx, memory, FirstLinkAddress, paddingDwords + waitDwords);
|
||||||
|
|
||||||
|
Assert.NotEqual(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
WaitEqueue(ctx, memory, equeue));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DeleteEqueue(ctx, equeue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A chain whose target is never satisfied must not be mistaken for a completed
|
||||||
|
// submission: without the redirect the empty first link completes immediately.
|
||||||
|
[Fact]
|
||||||
|
public void SubmittedDcb_WithoutAChain_CompletesImmediately()
|
||||||
|
{
|
||||||
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
||||||
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||||
|
var equeue = CreateEqueue(ctx, memory);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RegisterGraphicsCompletion(equeue);
|
||||||
|
|
||||||
|
var paddingDwords = WriteChain(ctx, memory, FirstLinkAddress, target: 0, targetDwords: 0);
|
||||||
|
SubmitDcb(ctx, memory, FirstLinkAddress, paddingDwords);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
WaitEqueue(ctx, memory, equeue));
|
||||||
|
Assert.Equal(GraphicsCompletionIdent, ReadUInt64(memory, EventsAddress + 0x00));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
DeleteEqueue(ctx, equeue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterGraphicsCompletion(ulong equeue) =>
|
||||||
|
Assert.True(KernelEventQueueCompatExports.RegisterEvent(
|
||||||
|
equeue,
|
||||||
|
GraphicsCompletionIdent,
|
||||||
|
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||||
|
userData: 0));
|
||||||
|
|
||||||
|
private static uint WriteChain(
|
||||||
|
CpuContext ctx,
|
||||||
|
FakeCpuMemory memory,
|
||||||
|
ulong linkAddress,
|
||||||
|
ulong target,
|
||||||
|
uint targetDwords)
|
||||||
|
{
|
||||||
|
PointCommandBufferAt(memory, linkAddress);
|
||||||
|
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
||||||
|
ctx[CpuRegister.Rsi] = target;
|
||||||
|
ctx[CpuRegister.Rdx] = targetDwords;
|
||||||
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DcbJump(ctx));
|
||||||
|
Assert.Equal(linkAddress, ctx[CpuRegister.Rax]);
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint WriteUnsatisfiedWait(CpuContext ctx, FakeCpuMemory memory, ulong linkAddress)
|
||||||
|
{
|
||||||
|
PointCommandBufferAt(memory, linkAddress);
|
||||||
|
WriteUInt32(memory, WaitLabelAddress, 0);
|
||||||
|
ctx[CpuRegister.Rsp] = StackAddress;
|
||||||
|
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
||||||
|
ctx[CpuRegister.Rsi] = 0; // 32-bit compare
|
||||||
|
ctx[CpuRegister.Rdx] = 3; // equal
|
||||||
|
ctx[CpuRegister.Rcx] = 4; // memory space
|
||||||
|
ctx[CpuRegister.R8] = 2;
|
||||||
|
ctx[CpuRegister.R9] = WaitLabelAddress;
|
||||||
|
WriteUInt64(memory, StackAddress + 8, 1); // reference the label never reaches
|
||||||
|
WriteUInt64(memory, StackAddress + 16, 0xFFFF_FFFF); // mask
|
||||||
|
WriteUInt32(memory, StackAddress + 24, 0x10); // poll interval
|
||||||
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DcbWaitRegMem(ctx));
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PointCommandBufferAt(FakeCpuMemory memory, ulong linkAddress)
|
||||||
|
{
|
||||||
|
WriteUInt64(memory, CommandBufferAddress + 0x10, linkAddress);
|
||||||
|
WriteUInt64(memory, CommandBufferAddress + 0x18, linkAddress + 0x400);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SubmitDcb(
|
||||||
|
CpuContext ctx,
|
||||||
|
FakeCpuMemory memory,
|
||||||
|
ulong commandAddress,
|
||||||
|
uint dwordCount)
|
||||||
|
{
|
||||||
|
WriteUInt64(memory, SubmitPacketAddress, commandAddress);
|
||||||
|
WriteUInt32(memory, SubmitPacketAddress + 8, dwordCount);
|
||||||
|
ctx[CpuRegister.Rdi] = SubmitPacketAddress;
|
||||||
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DriverSubmitDcb(ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong CreateEqueue(CpuContext ctx, FakeCpuMemory memory)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rdi] = HandleOutAddress;
|
||||||
|
Assert.Equal(
|
||||||
|
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
KernelEventQueueCompatExports.KernelCreateEqueue(ctx));
|
||||||
|
return ReadUInt64(memory, HandleOutAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteEqueue(CpuContext ctx, ulong equeue)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rdi] = equeue;
|
||||||
|
_ = KernelEventQueueCompatExports.KernelDeleteEqueue(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WaitEqueue(CpuContext ctx, FakeCpuMemory memory, ulong equeue)
|
||||||
|
{
|
||||||
|
WriteUInt64(memory, TimeoutAddress, 0);
|
||||||
|
ctx[CpuRegister.Rdi] = equeue;
|
||||||
|
ctx[CpuRegister.Rsi] = EventsAddress;
|
||||||
|
ctx[CpuRegister.Rdx] = 4;
|
||||||
|
ctx[CpuRegister.Rcx] = OutCountAddress;
|
||||||
|
ctx[CpuRegister.R8] = TimeoutAddress;
|
||||||
|
return KernelEventQueueCompatExports.KernelWaitEqueue(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ReadUInt64(FakeCpuMemory memory, ulong address)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[8];
|
||||||
|
Assert.True(memory.TryRead(address, buffer));
|
||||||
|
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[8];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
Assert.True(memory.TryWrite(address, buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteUInt32(FakeCpuMemory memory, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[4];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
Assert.True(memory.TryWrite(address, buffer));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user