mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-01 23:49:44 +08:00
531e35b6d5
sceAgcDriverSubmitAcb submissions never produced a completion interrupt. NotifySubmittedDcbCompleted returned early for anything that was not the graphics queue, so an ACB reaching its ordered-queue fence published nothing. UE 4.27's dynamic-resolution GPU-timing heuristic parks the game thread on exactly that interrupt, so the render side never advanced and Silent Hill: The Short Message deadlocked after its first frame. Give every queue a CompletionEventId: 0 for graphics (what it already published) and the owner handle from sceAgcDriverSubmitAcb rdi for a compute queue, which is the same value the guest passes to sceAgcDriverAddEqEvent. Publish under that ident from the existing fence point, which both PumpSubmittedQueue and ResumeSuspendedDcb already reach only after the submission is fully parsed. Delivery is synchronous on the ordered guest-action queue rather than on a ThreadPool hop with a sleep. That action runs after the logical queue has flushed and waited for its latest fence, which is the moment hardware would raise end-of-pipe. Deferring past it can only make the interrupt late and reorder it against registration changes. Gating: the per-queue completion event is unconditional, because completion interrupts do fire on real hardware and because delivery is registration-gated -- TriggerRegisteredEvents only queues onto equeues that registered this exact (ident, graphics filter) pair, and sceAgcDriverAddEqEvent is the only producer of graphics registrations. A title that never registers its ACB owner handle observes no change. SHARPEMU_AGC_SUBMIT_COMPLETION_EVENT is left to gate only the broad ident-ignoring fan-out (TriggerRegisteredEventsDistinct), which is a compatibility guess rather than hardware behavior; it also stays scoped to the graphics queue where it was measured, so enabling the flag does not newly fan out across compute queues.
155 lines
6.4 KiB
C#
155 lines
6.4 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.Kernel;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Agc;
|
|
|
|
// The kernel event-queue registry is process-wide static state, and these tests assert over
|
|
// every graphics registration in it, so they cannot run beside another suite that registers
|
|
// graphics events.
|
|
[CollectionDefinition(GraphicsEventQueueStateCollection.Name, DisableParallelization = true)]
|
|
public sealed class GraphicsEventQueueStateCollection
|
|
{
|
|
public const string Name = "GraphicsEventQueueState";
|
|
}
|
|
|
|
// IT_EVENT_WRITE carries a 6-bit hardware EVENT_TYPE, but sceAgcDriverAddEqEvent registers the
|
|
// listener with a guest-defined eventId. Those two values are not the same numbering scheme, so
|
|
// exact ident matching never wakes anything (issue #173). TriggerRegisteredEventsByFilter wakes
|
|
// every graphics registration instead.
|
|
[Collection(GraphicsEventQueueStateCollection.Name)]
|
|
public sealed class AgcEventQueueTests
|
|
{
|
|
private const ulong BaseAddress = 0x1_0000_0000;
|
|
private const int MemorySize = 0x2000;
|
|
|
|
[Fact]
|
|
public void TriggerRegisteredEventsByFilter_DifferentIdentThanEventType_WakesGraphicsWaiter()
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
|
|
const ulong handleOutAddress = BaseAddress + 0x100;
|
|
const ulong eventsAddress = BaseAddress + 0x200;
|
|
const ulong outCountAddress = BaseAddress + 0x300;
|
|
const ulong timeoutAddress = BaseAddress + 0x400;
|
|
|
|
// Create an event queue.
|
|
ctx[CpuRegister.Rdi] = handleOutAddress;
|
|
var createResult = KernelEventQueueCompatExports.KernelCreateEqueue(ctx);
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, createResult);
|
|
|
|
var handle = ReadUInt64(memory, handleOutAddress);
|
|
|
|
// Register a graphics event with eventId 0x20 (as Poppy Playtime does).
|
|
const ulong registeredEventId = 0x20;
|
|
const ulong userData = 0xDEAD_BEEF;
|
|
var registered = KernelEventQueueCompatExports.RegisterEvent(
|
|
handle,
|
|
registeredEventId,
|
|
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
|
userData);
|
|
Assert.True(registered);
|
|
|
|
// The command buffer fires EVENT_WRITE with eventType 0x07. This does not match 0x20.
|
|
const ulong eventType = 0x07;
|
|
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter(
|
|
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
|
eventType);
|
|
Assert.Equal(1, triggered);
|
|
|
|
// Wait with timeout=0. The event is already pending, so this returns immediately.
|
|
WriteUInt64(memory, timeoutAddress, 0);
|
|
ctx[CpuRegister.Rdi] = handle;
|
|
ctx[CpuRegister.Rsi] = eventsAddress;
|
|
ctx[CpuRegister.Rdx] = 4;
|
|
ctx[CpuRegister.Rcx] = outCountAddress;
|
|
ctx[CpuRegister.R8] = timeoutAddress;
|
|
var waitResult = KernelEventQueueCompatExports.KernelWaitEqueue(ctx);
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, waitResult);
|
|
Assert.Equal(1u, ReadUInt32(memory, outCountAddress));
|
|
|
|
// Verify the queued event carries the registered ident and the event type as data.
|
|
Assert.Equal(registeredEventId, ReadUInt64(memory, eventsAddress + 0x00));
|
|
Assert.Equal(KernelEventQueueCompatExports.KernelEventFilterGraphics, ReadInt16(memory, eventsAddress + 0x08));
|
|
Assert.Equal(0u, ReadUInt16(memory, eventsAddress + 0x0A));
|
|
Assert.Equal(1u, ReadUInt32(memory, eventsAddress + 0x0C));
|
|
Assert.Equal(eventType, ReadUInt64(memory, eventsAddress + 0x10));
|
|
Assert.Equal(userData, ReadUInt64(memory, eventsAddress + 0x18));
|
|
|
|
// Registrations live in process-wide static state, and a sibling test asserts that
|
|
// no graphics registration exists at all. Drop this one instead of relying on
|
|
// execution order.
|
|
ctx[CpuRegister.Rdi] = handle;
|
|
Assert.Equal(
|
|
(int)OrbisGen2Result.ORBIS_GEN2_OK,
|
|
KernelEventQueueCompatExports.KernelDeleteEqueue(ctx));
|
|
}
|
|
|
|
[Fact]
|
|
public void TriggerRegisteredEventsByFilter_NoGraphicsRegistrations_ReturnsZero()
|
|
{
|
|
var memory = new FakeCpuMemory(BaseAddress, MemorySize);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
|
|
const ulong handleOutAddress = BaseAddress + 0x100;
|
|
ctx[CpuRegister.Rdi] = handleOutAddress;
|
|
var createResult = KernelEventQueueCompatExports.KernelCreateEqueue(ctx);
|
|
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, createResult);
|
|
|
|
var handle = ReadUInt64(memory, handleOutAddress);
|
|
|
|
// Register a user event, not a graphics event.
|
|
KernelEventQueueCompatExports.RegisterEvent(
|
|
handle,
|
|
0x1,
|
|
KernelEventQueueCompatExports.KernelEventFilterUser,
|
|
0);
|
|
|
|
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEventsByFilter(
|
|
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
|
0x07);
|
|
|
|
Assert.Equal(0, triggered);
|
|
}
|
|
|
|
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 ushort ReadUInt16(FakeCpuMemory memory, ulong address)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[2];
|
|
Assert.True(memory.TryRead(address, buffer));
|
|
return BinaryPrimitives.ReadUInt16LittleEndian(buffer);
|
|
}
|
|
|
|
private static short ReadInt16(FakeCpuMemory memory, ulong address)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[2];
|
|
Assert.True(memory.TryRead(address, buffer));
|
|
return BinaryPrimitives.ReadInt16LittleEndian(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));
|
|
}
|
|
}
|