337 lines
13 KiB
C#
337 lines
13 KiB
C#
// 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;
|
|
|
|
// The inline command stream is keyed by memory; serialize the suites that fault it.
|
|
[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 = 0x14000;
|
|
|
|
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;
|
|
|
|
private static SharpEmu.Libs.Gpu.GpuCommands.CommandStreamQueue StreamOf(FakeCpuMemory memory) =>
|
|
AgcExports.GetHeadlessCommandStreamForTests(memory).Queue;
|
|
|
|
[Fact]
|
|
public void SubmittedDcb_FollowsIndirectBufferIntoTheChainedBuffer()
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
|
|
// 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.Equal(1, StreamOf(memory).BlockedQueueCount);
|
|
Assert.Equal(SecondLinkAddress, StreamOf(memory).SnapshotBlocked().SampleWaitAddress);
|
|
}
|
|
|
|
// 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 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.Equal(1, StreamOf(memory).BlockedQueueCount);
|
|
Assert.Equal(FirstLinkAddress + (paddingDwords * sizeof(uint)), StreamOf(memory).SnapshotBlocked().SampleWaitAddress);
|
|
}
|
|
|
|
[Fact]
|
|
public void DcbJump_EncodesModeCachePolicyAndSize()
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
|
|
PointCommandBufferAt(memory, FirstLinkAddress);
|
|
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
|
ctx[CpuRegister.Rsi] = 0; // call
|
|
ctx[CpuRegister.Rdx] = 2; // bypass
|
|
ctx[CpuRegister.Rcx] = SecondLinkAddress;
|
|
ctx[CpuRegister.R8] = 0x123;
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.DcbJump(ctx));
|
|
Assert.Equal(FirstLinkAddress, ctx[CpuRegister.Rax]);
|
|
Assert.Equal(unchecked((uint)SecondLinkAddress), ReadUInt32(memory, FirstLinkAddress + 4));
|
|
Assert.Equal(0x2F20_0123u, ReadUInt32(memory, FirstLinkAddress + 12));
|
|
}
|
|
|
|
[Fact]
|
|
public void AcbJump_AlwaysEncodesChainMode()
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
|
|
PointCommandBufferAt(memory, FirstLinkAddress);
|
|
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
|
ctx[CpuRegister.Rsi] = SecondLinkAddress;
|
|
ctx[CpuRegister.Rdx] = 0x123;
|
|
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.AcbJump(ctx));
|
|
Assert.Equal(FirstLinkAddress, ctx[CpuRegister.Rax]);
|
|
Assert.Equal(unchecked((uint)SecondLinkAddress), ReadUInt32(memory, FirstLinkAddress + 4));
|
|
Assert.Equal(0x0F30_0123u, ReadUInt32(memory, FirstLinkAddress + 12));
|
|
}
|
|
|
|
[Fact]
|
|
public void SubmittedDcb_ReturnsToParentAfterIndirectCall()
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
WriteUInt32(memory, SecondLinkAddress, 0x8000_0000u);
|
|
var callDwords = WriteJump(
|
|
ctx,
|
|
memory,
|
|
FirstLinkAddress,
|
|
mode: 0,
|
|
SecondLinkAddress,
|
|
targetDwords: 1);
|
|
var waitDwords = WriteUnsatisfiedWait(
|
|
ctx,
|
|
memory,
|
|
FirstLinkAddress + (callDwords * sizeof(uint)));
|
|
|
|
SubmitDcb(ctx, memory, FirstLinkAddress, callDwords + waitDwords);
|
|
|
|
// The wait after the call is reached only when the call returned to its parent.
|
|
Assert.Equal(1, StreamOf(memory).BlockedQueueCount);
|
|
Assert.Equal(FirstLinkAddress + (callDwords * sizeof(uint)), StreamOf(memory).SnapshotBlocked().SampleWaitAddress);
|
|
}
|
|
|
|
[Fact]
|
|
public void SubmittedDcb_IndirectCallRestoresParentRingBase()
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
{
|
|
WriteUInt32(memory, SecondLinkAddress, 0x8000_0000u);
|
|
var continuation = FirstLinkAddress + 0x10000;
|
|
_ = WriteUnsatisfiedWait(ctx, memory, continuation);
|
|
var callDwords = WriteJump(
|
|
ctx,
|
|
memory,
|
|
FirstLinkAddress,
|
|
mode: 0,
|
|
SecondLinkAddress,
|
|
targetDwords: 1);
|
|
var sentinelDwords = WriteChain(
|
|
ctx,
|
|
memory,
|
|
FirstLinkAddress + (callDwords * sizeof(uint)),
|
|
target: 1,
|
|
targetDwords: 0);
|
|
|
|
SubmitDcb(
|
|
ctx,
|
|
memory,
|
|
FirstLinkAddress,
|
|
callDwords + sentinelDwords);
|
|
|
|
// The chunk advance after the call continues from the parent's ring chunk.
|
|
Assert.Equal(1, StreamOf(memory).BlockedQueueCount);
|
|
Assert.Equal(continuation, StreamOf(memory).SnapshotBlocked().SampleWaitAddress);
|
|
}
|
|
}
|
|
|
|
// 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 paddingDwords = WriteChain(ctx, memory, FirstLinkAddress, target: 0, targetDwords: 0);
|
|
|
|
SubmitDcb(ctx, memory, FirstLinkAddress, paddingDwords);
|
|
|
|
Assert.False(StreamOf(memory).HasPending);
|
|
}
|
|
|
|
private static uint WriteChain(
|
|
CpuContext ctx,
|
|
FakeCpuMemory memory,
|
|
ulong linkAddress,
|
|
ulong target,
|
|
uint targetDwords)
|
|
=> WriteJump(ctx, memory, linkAddress, 1, target, targetDwords);
|
|
|
|
[Fact]
|
|
public void PatchedPlaceholderChain_ReachesContinuation()
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
var nextDwords = WriteUnsatisfiedWait(ctx, memory, SecondLinkAddress);
|
|
var rootDwords = WriteChain(ctx, memory, FirstLinkAddress, 0, 0);
|
|
ctx[CpuRegister.Rdi] = FirstLinkAddress;
|
|
ctx[CpuRegister.Rsi] = 2;
|
|
ctx[CpuRegister.Rdx] = SecondLinkAddress;
|
|
ctx[CpuRegister.Rcx] = nextDwords;
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.UnknownIkfdt(ctx));
|
|
Assert.Equal(0x2F30_0000u | nextDwords, ReadUInt32(memory, FirstLinkAddress + 12));
|
|
SubmitDcb(ctx, memory, FirstLinkAddress, rootDwords);
|
|
Assert.Equal(1, StreamOf(memory).BlockedQueueCount);
|
|
Assert.Equal(SecondLinkAddress, StreamOf(memory).SnapshotBlocked().SampleWaitAddress);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0u)]
|
|
[InlineData(1u)]
|
|
public void JumpPatch_PreservesModeAndEncodesAddress(uint mode)
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
WriteJump(ctx, memory, FirstLinkAddress, mode, 0, 0);
|
|
ctx[CpuRegister.Rdi] = FirstLinkAddress;
|
|
ctx[CpuRegister.Rsi] = 3;
|
|
ctx[CpuRegister.Rdx] = 0xABCD_1234_5678;
|
|
ctx[CpuRegister.Rcx] = 0x123;
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, AgcExports.UnknownIkfdt(ctx));
|
|
Assert.Equal(0x1234_5678u, ReadUInt32(memory, FirstLinkAddress + 4));
|
|
Assert.Equal(0xABCDu, ReadUInt32(memory, FirstLinkAddress + 8));
|
|
Assert.Equal(0x3F20_0123u | (mode << 20), ReadUInt32(memory, FirstLinkAddress + 12));
|
|
}
|
|
|
|
[Fact]
|
|
public void JumpPatch_RejectsOtherPacketWithoutChangingIt()
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
WriteUInt32(memory, FirstLinkAddress, 0xC002_1000);
|
|
WriteUInt64(memory, FirstLinkAddress + 4, 0xDEAD_BEEF);
|
|
ctx[CpuRegister.Rdi] = FirstLinkAddress;
|
|
ctx[CpuRegister.Rsi] = 2;
|
|
ctx[CpuRegister.Rdx] = SecondLinkAddress;
|
|
ctx[CpuRegister.Rcx] = 7;
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, AgcExports.UnknownIkfdt(ctx));
|
|
Assert.Equal(0xDEAD_BEEFul, ReadUInt64(memory, FirstLinkAddress + 4));
|
|
}
|
|
|
|
private static uint WriteJump(
|
|
CpuContext ctx,
|
|
FakeCpuMemory memory,
|
|
ulong linkAddress,
|
|
uint mode,
|
|
ulong target,
|
|
uint targetDwords)
|
|
{
|
|
PointCommandBufferAt(memory, linkAddress);
|
|
ctx[CpuRegister.Rdi] = CommandBufferAddress;
|
|
ctx[CpuRegister.Rsi] = mode;
|
|
ctx[CpuRegister.Rdx] = 0; // LRU
|
|
ctx[CpuRegister.Rcx] = target;
|
|
ctx[CpuRegister.R8] = 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] = 0; // ME wait
|
|
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 ReadUInt64(FakeCpuMemory memory, ulong address)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[8];
|
|
Assert.True(memory.TryRead(address, buffer));
|
|
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
|
}
|
|
|
|
private static uint ReadUInt32(FakeCpuMemory memory, ulong address)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[4];
|
|
Assert.True(memory.TryRead(address, buffer));
|
|
return BinaryPrimitives.ReadUInt32LittleEndian(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));
|
|
}
|
|
}
|