mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-23 03:16:22 +08:00
52d2874fa8
## What The native backend runs guest code directly on the host CPU. When the host doesn't implement a BMI1/BMI2/ABM instruction that the PS5's Zen 2 cores do, it raises #UD (STATUS_ILLEGAL_INSTRUCTION). Today the vectored handler just logs the faulting bytes and gives up, so the title dies. This adds a software fallback. On an illegal-instruction fault we decode the opcode with Iced (the decoder already used elsewhere in the backend), evaluate it against the trapped register/memory state, write the result and flags back into the CONTEXT record, step RIP past the instruction, and resume. Instructions covered (32- and 64-bit): ANDN, BLSI, BLSMSK, BLSR, BEXTR, BZHI, TZCNT, LZCNT, RORX, SARX, SHLX, SHRX, PDEP, PEXT. ## Why Users on CPUs without these extensions currently can't get past code that uses them. This is a generic fix (no game-specific hacks) that improves compatibility on older hosts. MULX is intentionally left out for now — its dest_hi/dest_lo operand ordering is easy to get subtly wrong, so I'd rather add it separately with its own tests. ## How it's structured - `BmiInstructionEmulator` holds the pure bit/flag semantics with no dependency on the unsafe CONTEXT plumbing, so it can be unit-tested directly. - `DirectExecutionBackend.IllegalInstruction.cs` is the thin unsafe adapter (decode → read operands → emulate → write back → advance RIP). Anything it doesn't fully model returns false and falls through to the existing diagnostics unchanged, so it can never mis-handle an opcode it doesn't recognize. - One hook in `DirectExecutionBackend.Exceptions.cs`, next to the other TryRecover* calls. - Emits a single one-time "emulating in software" log line, not per-instruction spam. ## How I verified - Added xUnit tests covering every instruction in both widths plus the CF/ZF/SF/OF edge cases (src == 0, shift-count masking, index beyond operand width, etc.). - Cross-checked all the expected values against an independent reference implementation written from the Intel/AMD definitions; results match. - `dotnet build` + `dotnet test` pass locally. ## Notes New files follow .editorconfig (4-space, SPDX headers, REUSE-compliant).
244 lines
8.5 KiB
C#
244 lines
8.5 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.Core.Cpu.Emulation;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Cpu;
|
|
|
|
// These exercise the pure BMI1/BMI2/ABM semantics used by the illegal-instruction software
|
|
// fallback. The expected values were computed by hand from the Intel/AMD definitions so a
|
|
// regression in the bit math or the flag handling fails here without needing a live guest.
|
|
public sealed class BmiInstructionEmulatorTests
|
|
{
|
|
private const uint Carry = 1u << 0;
|
|
private const uint Zero = 1u << 6;
|
|
private const uint Sign = 1u << 7;
|
|
private const uint Overflow = 1u << 11;
|
|
private const uint DirectionFlag = 1u << 10; // Unrelated flag; must be preserved.
|
|
|
|
[Fact]
|
|
public void Andn_ClearsSourceBitsAndFlags()
|
|
{
|
|
uint flags = Overflow | Carry | DirectionFlag;
|
|
var result = BmiInstructionEmulator.Andn(0x0000_00F0, 0x0000_00FF, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0x0000_000FUL, result);
|
|
Assert.Equal(0u, flags & Carry);
|
|
Assert.Equal(0u, flags & Overflow);
|
|
Assert.Equal(0u, flags & Zero);
|
|
Assert.Equal(0u, flags & Sign);
|
|
Assert.Equal(DirectionFlag, flags & DirectionFlag);
|
|
}
|
|
|
|
[Fact]
|
|
public void Andn_SetsZeroFlagWhenResultIsZero()
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Andn(0xFF, 0xFF, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0UL, result);
|
|
Assert.Equal(Zero, flags & Zero);
|
|
}
|
|
|
|
[Fact]
|
|
public void Andn_SetsSignFlagOnHighBit32()
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Andn(0, 0x8000_0000, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0x8000_0000UL, result);
|
|
Assert.Equal(Sign, flags & Sign);
|
|
}
|
|
|
|
[Fact]
|
|
public void Andn_MasksTo64Bits()
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Andn(0xFFFF_FFFF_FFFF_FF00, 0xFF, GprOperandSize.Bits64, ref flags);
|
|
|
|
Assert.Equal(0xFFUL, result);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x0000_000CUL, 0x0000_0004UL, false)] // isolate lowest set bit
|
|
[InlineData(0x0000_0000UL, 0x0000_0000UL, true)] // src == 0 -> CF clear, ZF set
|
|
public void Blsi_IsolatesLowestBit(ulong src, ulong expected, bool zero)
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Blsi(src, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(expected, result);
|
|
Assert.Equal(src != 0 ? Carry : 0u, flags & Carry);
|
|
Assert.Equal(zero ? Zero : 0u, flags & Zero);
|
|
}
|
|
|
|
[Fact]
|
|
public void Blsmsk_MasksThroughLowestBit()
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Blsmsk(0x0000_000C, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0x0000_0007UL, result);
|
|
Assert.Equal(0u, flags & Carry);
|
|
}
|
|
|
|
[Fact]
|
|
public void Blsmsk_SetsCarryWhenSourceZero32()
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Blsmsk(0, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0xFFFF_FFFFUL, result);
|
|
Assert.Equal(Carry, flags & Carry);
|
|
Assert.Equal(Sign, flags & Sign);
|
|
}
|
|
|
|
[Fact]
|
|
public void Blsr_ResetsLowestBit()
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Blsr(0x0000_000C, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0x0000_0008UL, result);
|
|
Assert.Equal(0u, flags & Carry);
|
|
}
|
|
|
|
[Fact]
|
|
public void Blsr_SetsCarryAndZeroWhenSourceZero()
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Blsr(0, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0UL, result);
|
|
Assert.Equal(Carry, flags & Carry);
|
|
Assert.Equal(Zero, flags & Zero);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bextr_ExtractsBitField()
|
|
{
|
|
uint flags = 0;
|
|
// start = 4, len = 8 -> bits [11:4] of 0x12345678 == 0x67
|
|
var control = (8UL << 8) | 4UL;
|
|
var result = BmiInstructionEmulator.Bextr(0x1234_5678, control, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0x0000_0067UL, result);
|
|
Assert.Equal(0u, flags & Zero);
|
|
Assert.Equal(0u, flags & Carry);
|
|
Assert.Equal(0u, flags & Overflow);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bextr_StartBeyondWidthYieldsZero()
|
|
{
|
|
uint flags = 0;
|
|
var control = (8UL << 8) | 40UL; // start = 40 >= 32
|
|
var result = BmiInstructionEmulator.Bextr(0xFFFF_FFFF, control, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0UL, result);
|
|
Assert.Equal(Zero, flags & Zero);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bzhi_ZeroesHighBits()
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Bzhi(0xFFFF_FFFF, 8, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0x0000_00FFUL, result);
|
|
Assert.Equal(0u, flags & Carry);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bzhi_SetsCarryWhenIndexAtOrBeyondWidth()
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Bzhi(0xFFFF_FFFF, 32, GprOperandSize.Bits32, ref flags);
|
|
|
|
Assert.Equal(0xFFFF_FFFFUL, result);
|
|
Assert.Equal(Carry, flags & Carry);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x0000_0008UL, GprOperandSize.Bits32, 3UL, false)]
|
|
[InlineData(0x0000_0001UL, GprOperandSize.Bits32, 0UL, false)]
|
|
[InlineData(0x0000_0000UL, GprOperandSize.Bits32, 32UL, true)]
|
|
[InlineData(0x1_0000_0000UL, GprOperandSize.Bits64, 32UL, false)]
|
|
public void Tzcnt_CountsTrailingZeros(ulong src, GprOperandSize size, ulong expected, bool carry)
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Tzcnt(src, size, ref flags);
|
|
|
|
Assert.Equal(expected, result);
|
|
Assert.Equal(carry ? Carry : 0u, flags & Carry);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x0000_0001UL, GprOperandSize.Bits32, 31UL, false)]
|
|
[InlineData(0x8000_0000UL, GprOperandSize.Bits32, 0UL, false)]
|
|
[InlineData(0x0000_0000UL, GprOperandSize.Bits32, 32UL, true)]
|
|
[InlineData(0x0000_0001UL, GprOperandSize.Bits64, 63UL, false)]
|
|
[InlineData(0x0000_0000UL, GprOperandSize.Bits64, 64UL, true)]
|
|
public void Lzcnt_CountsLeadingZeros(ulong src, GprOperandSize size, ulong expected, bool carry)
|
|
{
|
|
uint flags = 0;
|
|
var result = BmiInstructionEmulator.Lzcnt(src, size, ref flags);
|
|
|
|
Assert.Equal(expected, result);
|
|
Assert.Equal(carry ? Carry : 0u, flags & Carry);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x1234_5678UL, 8, GprOperandSize.Bits32, 0x7812_3456UL)]
|
|
[InlineData(0x1234_5678UL, 0, GprOperandSize.Bits32, 0x1234_5678UL)]
|
|
[InlineData(0x0123_4567_89AB_CDEFUL, 4, GprOperandSize.Bits64, 0xF012_3456_789A_BCDEUL)]
|
|
public void Rorx_RotatesRight(ulong src, int count, GprOperandSize size, ulong expected)
|
|
{
|
|
Assert.Equal(expected, BmiInstructionEmulator.Rorx(src, count, size));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x8000_0000UL, 4, GprOperandSize.Bits32, 0xF800_0000UL)]
|
|
[InlineData(0x8000_0000UL, 36, GprOperandSize.Bits32, 0xF800_0000UL)] // count masked to 4
|
|
[InlineData(0x8000_0000_0000_0000UL, 4, GprOperandSize.Bits64, 0xF800_0000_0000_0000UL)]
|
|
public void Sarx_ArithmeticShiftRight(ulong src, int count, GprOperandSize size, ulong expected)
|
|
{
|
|
Assert.Equal(expected, BmiInstructionEmulator.Sarx(src, count, size));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x0000_0001UL, 4, GprOperandSize.Bits32, 0x0000_0010UL)]
|
|
[InlineData(0x0000_0001UL, 33, GprOperandSize.Bits32, 0x0000_0002UL)] // count masked to 1
|
|
public void Shlx_LogicalShiftLeft(ulong src, int count, GprOperandSize size, ulong expected)
|
|
{
|
|
Assert.Equal(expected, BmiInstructionEmulator.Shlx(src, count, size));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x8000_0000UL, 4, GprOperandSize.Bits32, 0x0800_0000UL)]
|
|
[InlineData(0x8000_0000_0000_0000UL, 4, GprOperandSize.Bits64, 0x0800_0000_0000_0000UL)]
|
|
public void Shrx_LogicalShiftRight(ulong src, int count, GprOperandSize size, ulong expected)
|
|
{
|
|
Assert.Equal(expected, BmiInstructionEmulator.Shrx(src, count, size));
|
|
}
|
|
|
|
[Fact]
|
|
public void PdepAndPext_AreInverseForContiguousSource()
|
|
{
|
|
var deposited = BmiInstructionEmulator.Pdep(0x0000_000F, 0x0000_00AA, GprOperandSize.Bits32);
|
|
Assert.Equal(0x0000_00AAUL, deposited);
|
|
|
|
var extracted = BmiInstructionEmulator.Pext(0x0000_00AA, 0x0000_00AA, GprOperandSize.Bits32);
|
|
Assert.Equal(0x0000_000FUL, extracted);
|
|
}
|
|
|
|
[Fact]
|
|
public void Pext_GathersSelectedBits()
|
|
{
|
|
// mask selects bit positions 1,3,5,7; source has 1s only at 5 and 7 -> packed 0b1100
|
|
var extracted = BmiInstructionEmulator.Pext(0xF0, 0xAA, GprOperandSize.Bits32);
|
|
Assert.Equal(0x0000_000CUL, extracted);
|
|
}
|
|
}
|