sharpemu/tests/SharpEmu.Libs.Tests/Sse4aExtrqBlendPatchTests.cs

131 lines
4.7 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Native;
using Xunit;
namespace SharpEmu.Libs.Tests;
/// <summary>
/// The SSE4a EXTRQ+blend idiom raises #UD -> SIGILL under Rosetta 2, so the
/// loader rewrites it to SSE4.1 at boot. Sony's toolchain allocates both the
/// blend destination and the scratch source register freely (one Dead Cells
/// build uses dest=xmm0, another dest=xmm3), so the matcher must read both from
/// the encoding rather than assume fixed registers.
/// </summary>
public sealed class Sse4aExtrqBlendPatchTests
{
// EXTRQ xmmSrc, 0x28, 0x00 ; VPBLENDD xmmDest, xmmDest, xmmSrc, 2.
private static byte[] Idiom(int dest, int src) =>
[
0x66, 0x0F, 0x78, (byte)(0xC0 | src), 0x28, 0x00,
0xC4, 0xE3, (byte)(((~dest & 0xF) << 3) | 0x01), 0x02, (byte)(0xC0 | (dest << 3) | src), 0x02,
];
// PEXTRB eax, xmmSrc, 4 ; PINSRD xmmDest, eax, 1.
private static byte[] Replacement(int dest, int src) =>
[
0x66, 0x0F, 0x3A, 0x14, (byte)(0xC0 | (src << 3)), 0x04,
0x66, 0x0F, 0x3A, 0x22, (byte)(0xC0 | (dest << 3)), 0x01,
];
[Fact]
public void MatchesEveryDestinationAndSourceCombination()
{
for (var dest = 0; dest <= 7; dest++)
{
for (var src = 0; src <= 7; src++)
{
Assert.True(
Sse4aExtrqBlendPatch.TryMatch(Idiom(dest, src), out var matchedDest, out var matchedSrc),
$"dest={dest} src={src}");
Assert.Equal(dest, matchedDest);
Assert.Equal(src, matchedSrc);
}
}
}
[Fact]
public void MatchesTheDeadCellsXmm3Xmm4Idiom()
{
// The exact bytes that faulted: EXTRQ xmm4,0x28,0x00 ; VPBLENDD xmm3,xmm3,xmm4,2.
byte[] bytes = [0x66, 0x0F, 0x78, 0xC4, 0x28, 0x00, 0xC4, 0xE3, 0x61, 0x02, 0xDC, 0x02];
Assert.True(Sse4aExtrqBlendPatch.TryMatch(bytes, out var dest, out var src));
Assert.Equal(3, dest);
Assert.Equal(4, src);
}
[Fact]
public void RoundTripsEveryCombinationThroughMatchThenEncode()
{
for (var dest = 0; dest <= 7; dest++)
{
for (var src = 0; src <= 7; src++)
{
Assert.True(Sse4aExtrqBlendPatch.TryMatch(Idiom(dest, src), out var matchedDest, out var matchedSrc));
var buffer = new byte[Sse4aExtrqBlendPatch.SequenceLength];
Assert.True(Sse4aExtrqBlendPatch.TryEncode(matchedDest, matchedSrc, buffer));
Assert.Equal(Replacement(dest, src), buffer);
}
}
}
[Fact]
public void PreservesTheOriginalXmm0DestinationEncoding()
{
// Guards the behaviour the previous matcher (dest fixed to xmm0) produced.
var buffer = new byte[Sse4aExtrqBlendPatch.SequenceLength];
Assert.True(Sse4aExtrqBlendPatch.TryEncode(destRegister: 0, srcRegister: 2, buffer));
Assert.Equal(
new byte[] { 0x66, 0x0F, 0x3A, 0x14, 0xD0, 0x04, 0x66, 0x0F, 0x3A, 0x22, 0xC0, 0x01 },
buffer);
}
[Fact]
public void RejectsMismatchedSourceAcrossTheTwoInstructions()
{
// EXTRQ masks xmm1 but the blend reads xmm2 — not the paired idiom.
var mixed = Idiom(dest: 0, src: 1);
mixed[10] = 0xC0 | 2;
Assert.False(Sse4aExtrqBlendPatch.TryMatch(mixed, out _, out _));
}
[Theory]
[InlineData(0)] // wrong first byte
[InlineData(2)] // wrong opcode
[InlineData(4)] // wrong EXTRQ length immediate
[InlineData(9)] // wrong blend opcode
[InlineData(11)] // wrong blend immediate
public void RejectsSequencesThatDifferFromTheIdiom(int corruptIndex)
{
var bytes = Idiom(dest: 0, src: 2);
bytes[corruptIndex] ^= 0xFF;
Assert.False(Sse4aExtrqBlendPatch.TryMatch(bytes, out _, out _));
}
[Fact]
public void RejectsNonModRmRegisterEncodings()
{
// A memory-form ModRM (mod != 11) is not the register-to-register idiom.
var bytes = Idiom(dest: 0, src: 2);
bytes[3] = 0x02; // mod=00, rm=010 — memory operand, not xmm2 direct
Assert.False(Sse4aExtrqBlendPatch.TryMatch(bytes, out _, out _));
}
[Fact]
public void RejectsTooShortWindows()
{
Assert.False(Sse4aExtrqBlendPatch.TryMatch(Idiom(dest: 0, src: 2).AsSpan(0, 11), out _, out _));
}
[Theory]
[InlineData(-1, 0)]
[InlineData(8, 0)]
[InlineData(0, 8)]
public void EncodeRejectsRegistersOutsideXmm0Through7(int dest, int src)
{
var buffer = new byte[Sse4aExtrqBlendPatch.SequenceLength];
Assert.False(Sse4aExtrqBlendPatch.TryEncode(dest, src, buffer));
}
}