sharpemu/tests/SharpEmu.Libs.Tests/Kernel/SemaphoreSignalProfileTests.cs

135 lines
6.1 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using SharpEmu.HLE;
using SharpEmu.Libs.Diagnostics;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.VideoOut;
using Xunit;
namespace SharpEmu.Libs.Tests.Kernel;
[Collection("GuestThreadFlowProfile")]
public sealed class SemaphoreSignalProfileTests
{
[Fact]
public void RingRetainsWholeEventsAndReopensForTheNextSession()
{
var buffer = new SemaphoreSignalProfile.EventBuffer(2);
for (var index = 0; index < 5; index++) buffer.Record(CreateEvent(index));
var snapshot = buffer.Close();
Assert.Equal(5, snapshot.TotalEvents);
Assert.Equal(new[] { CreateEvent(3), CreateEvent(4) }, snapshot.Events);
buffer.Record(CreateEvent(5));
Assert.Empty(buffer.Close().Events);
buffer.Reset();
buffer.Record(CreateEvent(6));
Assert.Equal(CreateEvent(6), Assert.Single(buffer.Close().Events));
}
[Fact]
public void ConcurrentWritesPreserveSignalIdentityAndValues()
{
var buffer = new SemaphoreSignalProfile.EventBuffer(128);
Parallel.For(0, 10000, index => buffer.Record(CreateEvent(index)));
var snapshot = buffer.Close();
Assert.Equal(10000, snapshot.TotalEvents);
Assert.Equal(128, snapshot.Events.Length);
Assert.All(snapshot.Events, item => Assert.Equal(CreateEvent((int)item.Timestamp), item));
}
[Theory]
[InlineData(0, 2, true)]
[InlineData(0, 0, true)]
[InlineData(1, -1, false)]
[InlineData(1, int.MaxValue, false)]
public void SignalKeepsTokenSemanticsAndRecordsStagesWhenEnabled(uint initial, int count, bool succeeds)
{
const ulong memoryBase = 0x100000000;
var context = new CpuContext(new FakeCpuMemory(memoryBase, 0x1000), Generation.Gen5);
context[CpuRegister.Rdi] = memoryBase;
context[CpuRegister.Rdx] = initial;
Assert.Equal(0, KernelSemaphoreCompatExports.PosixSemInit(context));
Assert.True(context.TryReadUInt32(memoryBase, out var handle));
var previousScheduler = GuestThreadExecution.Scheduler;
var scheduler = DispatchProxy.Create<IGuestThreadScheduler, SignalScheduler>();
var proxy = (SignalScheduler)(object)scheduler;
var previousGuest = GuestThreadExecution.EnterGuestThread(0x555);
var previousFrame = GuestThreadExecution.EnterImportCallFrame(0x12345678, 0x87654321, 0);
try
{
GuestThreadExecution.Scheduler = scheduler;
SemaphoreSignalProfile.StartSession();
var result = KernelSemaphoreCompatExports.KernelSignalSema(context, handle, count);
Assert.Equal(succeeds ? 0 : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result);
Assert.Equal(succeeds ? 1 : 0, proxy.WakeCalls);
if (succeeds) Assert.Equal($"sceKernelWaitSema:{handle:X8}", proxy.WakeKey);
context[CpuRegister.Rdi] = memoryBase;
context[CpuRegister.Rsi] = memoryBase + 16;
Assert.Equal(0, KernelSemaphoreCompatExports.PosixSemGetValue(context));
Assert.True(context.TryReadUInt32(memoryBase + 16, out var remaining));
Assert.Equal(initial + (succeeds ? (uint)count : 0), remaining);
var snapshot = SemaphoreSignalProfile.Close();
if (!RenderPhaseProfile.FrameTraceEnabled)
{
Assert.Empty(snapshot.Events);
}
else
{
var expectedStages = succeeds
? new[] { SemaphoreSignalProfile.Stage.Entered, SemaphoreSignalProfile.Stage.Published,
SemaphoreSignalProfile.Stage.WakeStarted, SemaphoreSignalProfile.Stage.WakeFinished }
: new[] { SemaphoreSignalProfile.Stage.Entered, SemaphoreSignalProfile.Stage.Rejected };
Assert.Equal(expectedStages, snapshot.Events.Select(item => item.Stage));
Assert.All(snapshot.Events, item =>
{
Assert.Equal(handle, item.Signal.Handle);
Assert.Equal(count, item.Signal.Count);
Assert.Equal(0x555UL, item.Signal.GuestThread);
Assert.Equal(0x12345678UL, item.Signal.ReturnAddress);
Assert.Equal(snapshot.Events[0].Signal.Sequence, item.Signal.Sequence);
});
Assert.Equal(succeeds ? 3 : result, snapshot.Events[^1].Result);
if (succeeds) Assert.Equal((int)remaining, snapshot.Events[1].Available);
}
}
finally
{
GuestThreadExecution.Scheduler = previousScheduler;
GuestThreadExecution.RestoreImportCallFrame(previousFrame);
GuestThreadExecution.RestoreGuestThread(previousGuest);
context[CpuRegister.Rdi] = handle;
KernelSemaphoreCompatExports.KernelDeleteSema(context);
SemaphoreSignalProfile.StartSession();
}
}
[Fact]
public void SnapshotOutputIncludesSignalCorrelationAndCallSite()
{
using var output = new StringWriter();
SemaphoreSignalProfile.WriteTrace(output, new SemaphoreSignalProfile.Snapshot([CreateEvent(1)], 5));
Assert.Contains("retained=1 overwritten=4", output.ToString());
Assert.Contains("signal=2 stage=Published handle=0x5 add=6 available=7 waiting=8 result=9 return=0x4", output.ToString());
}
public class SignalScheduler : DispatchProxy
{
public int WakeCalls { get; private set; }
public string? WakeKey { get; private set; }
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
Assert.Equal(nameof(IGuestThreadScheduler.WakeBlockedThreads), targetMethod!.Name);
WakeCalls++;
WakeKey = (string)args![0]!;
return 3;
}
}
private static SemaphoreSignalProfile.TraceEvent CreateEvent(int index) => new(index, index + 1,
new SemaphoreSignalProfile.Signal(index + 1, (ulong)index + 2, (ulong)index + 3,
(uint)index + 4, index + 5), SemaphoreSignalProfile.Stage.Published, index + 6, index + 7, index + 8);
}