mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
105c58b380
ScalarLoadReadsTrackedFallbackMemory swaps the process-global static Gen5ShaderScalarEvaluator.FallbackMemoryReader under a lock private to the test class. The SharpEmu.Libs [ModuleInitializer] (AgcShaderCompilerHooks) assigns the same static to TryReadShaderGuestMemory the first time any Libs type is touched, and it does not take that lock. Under xUnit's default cross-class parallelism a concurrent Libs test could fire the initializer mid-test, clobbering the swapped-in reader — observed on CI (linux-x64) as the fallback returning all zeros: Expected [1181044592, 4, 1319632096, 4], Actual [0, 0, 0, 0]. Put the test in a DisableParallelization collection, matching the existing convention for shared-mutable-static tests (KernelMemoryCompatState, AjmState, AvPlayerPathState). The collection runs alone in the non-parallel phase, so no other test can mutate the static while this one holds it. Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
114 lines
3.8 KiB
C#
114 lines
3.8 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.ShaderCompiler;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Agc;
|
|
|
|
// Gen5ShaderScalarEvaluator.FallbackMemoryReader is a process-global static. This
|
|
// test swaps it, but the SharpEmu.Libs [ModuleInitializer] (AgcShaderCompilerHooks)
|
|
// reassigns the same static the first time any Libs type is touched. Under xUnit's
|
|
// default cross-class parallelism a Libs test running concurrently can fire that
|
|
// initializer mid-test and clobber the swapped-in reader (observed as all-zero
|
|
// reads on CI). A DisableParallelization collection runs alone in the non-parallel
|
|
// phase, so nothing else can mutate the static while this test holds it.
|
|
[CollectionDefinition(Gen5ScalarEvaluatorStateCollection.Name, DisableParallelization = true)]
|
|
public sealed class Gen5ScalarEvaluatorStateCollection
|
|
{
|
|
public const string Name = "Gen5ScalarEvaluatorState";
|
|
}
|
|
|
|
[Collection(Gen5ScalarEvaluatorStateCollection.Name)]
|
|
public sealed class Gen5ScalarMemoryFallbackTests
|
|
{
|
|
private const ulong ScalarTableAddress = 0x4_4665_4FD0;
|
|
private static readonly object FallbackReaderGate = new();
|
|
|
|
[Fact]
|
|
public void ScalarLoadReadsTrackedFallbackMemory()
|
|
{
|
|
var expected = new uint[]
|
|
{
|
|
0x4665_4F70,
|
|
0x0000_0004,
|
|
0x4EA7_FCE0,
|
|
0x0000_0004,
|
|
};
|
|
var table = new byte[expected.Length * sizeof(uint)];
|
|
for (var index = 0; index < expected.Length; index++)
|
|
{
|
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
|
table.AsSpan(index * sizeof(uint), sizeof(uint)),
|
|
expected[index]);
|
|
}
|
|
|
|
var load = new Gen5ShaderInstruction(
|
|
0,
|
|
Gen5ShaderEncoding.Smem,
|
|
"SLoadDwordx4",
|
|
[],
|
|
[Gen5Operand.Scalar(0)],
|
|
[
|
|
Gen5Operand.Scalar(16),
|
|
Gen5Operand.Scalar(17),
|
|
Gen5Operand.Scalar(18),
|
|
Gen5Operand.Scalar(19),
|
|
],
|
|
new Gen5ScalarMemoryControl(4, 0, null));
|
|
var end = new Gen5ShaderInstruction(
|
|
8,
|
|
Gen5ShaderEncoding.Sopp,
|
|
"SEndpgm",
|
|
[],
|
|
[],
|
|
[],
|
|
null);
|
|
var state = new Gen5ShaderState(
|
|
new Gen5ShaderProgram(0, [load, end]),
|
|
[unchecked((uint)ScalarTableAddress), (uint)(ScalarTableAddress >> 32)],
|
|
null);
|
|
var ctx = new CpuContext(new FakeCpuMemory(0x1000, 0x100), Generation.Gen5);
|
|
|
|
lock (FallbackReaderGate)
|
|
{
|
|
var previousReader = Gen5ShaderScalarEvaluator.FallbackMemoryReader;
|
|
try
|
|
{
|
|
Gen5ShaderScalarEvaluator.FallbackMemoryReader = ReadFallback;
|
|
Assert.True(
|
|
Gen5ShaderScalarEvaluator.TryEvaluate(
|
|
ctx,
|
|
state,
|
|
out var evaluation,
|
|
out var error),
|
|
error);
|
|
Assert.Equal(expected, evaluation.ScalarRegisters.Skip(16).Take(4));
|
|
}
|
|
finally
|
|
{
|
|
Gen5ShaderScalarEvaluator.FallbackMemoryReader = previousReader;
|
|
}
|
|
}
|
|
|
|
bool ReadFallback(ulong address, Span<byte> destination)
|
|
{
|
|
if (address < ScalarTableAddress)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var offset = address - ScalarTableAddress;
|
|
if (offset + (ulong)destination.Length > (ulong)table.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
table.AsSpan((int)offset, destination.Length).CopyTo(destination);
|
|
return true;
|
|
}
|
|
}
|
|
}
|