diff --git a/src/SharpEmu.Core/Cpu/Emulation/Sse4aBitFieldEmulator.cs b/src/SharpEmu.Core/Cpu/Emulation/Sse4aBitFieldEmulator.cs new file mode 100644 index 00000000..9663b770 --- /dev/null +++ b/src/SharpEmu.Core/Cpu/Emulation/Sse4aBitFieldEmulator.cs @@ -0,0 +1,71 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Core.Cpu.Emulation; + +/// +/// Pure software implementation of the bit-field math behind AMD's SSE4a EXTRQ/INSERTQ +/// (immediate-form) instructions. +/// +/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's Zen 2 +/// cores implement AMD-only SSE4a (EXTRQ/INSERTQ), but Intel hosts - and Rosetta 2 on Apple +/// Silicon - do not, so they raise #UD (STATUS_ILLEGAL_INSTRUCTION) instead of executing the +/// opcode. SharpEmu already rewrites one specific compiled EXTRQ+VPBLENDD idiom at load time +/// (see ), but any other occurrence of EXTRQ/INSERTQ - +/// a different register allocation, a title built with a different compiler version, and so on +/// - still aborts the title. This class ported from Kyty's +/// Loader::X64InstructionEmulator::TryEmulateSse4a provides the general bit-field +/// extract/insert so the illegal-instruction handler can finish *any* immediate-form +/// EXTRQ/INSERTQ in software and resume, instead of relying on a single hard-coded byte pattern. +/// +/// The methods operate on plain 64-bit integers rather than the OS CONTEXT record so the bit +/// math can be unit-tested in isolation; the unsafe CONTEXT/XMM plumbing lives in the backend +/// adapter (). +/// +public static class Sse4aBitFieldEmulator +{ + public static bool IsValidBitField(int length, int index) + { + var len = length & 0x3F; + var idx = index & 0x3F; + return (len != 0 || idx == 0) && (len == 0 ? idx == 0 : idx + len <= 64); + } + + public static ulong ExtractBitField(ulong value, int length, int index) + { + var len = length & 0x3F; + var idx = index & 0x3F; + if (!IsValidBitField(length, index)) + { + return 0; + } + + if (len == 0) + { + return value; + } + + var mask = len == 64 ? ulong.MaxValue : (1UL << len) - 1; + return (value >> idx) & mask; + } + + public static ulong InsertBitField(ulong destination, ulong source, int length, int index) + { + var len = length & 0x3F; + var idx = index & 0x3F; + if (!IsValidBitField(length, index)) + { + return destination; + } + + if (len == 0) + { + return source; + } + + var fieldMask = len == 64 ? ulong.MaxValue : (1UL << len) - 1; + var destinationClearMask = fieldMask << idx; + var sourceField = (source & fieldMask) << idx; + return (destination & ~destinationClearMask) | sourceField; + } +} diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Amd64Compat.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Amd64Compat.cs new file mode 100644 index 00000000..213c76b2 --- /dev/null +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Amd64Compat.cs @@ -0,0 +1,197 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Threading; +using Iced.Intel; +using SharpEmu.Core.Cpu.Emulation; + +namespace SharpEmu.Core.Cpu.Native; + +// General software fallback for the AMD-only instructions PS5 titles occasionally emit that a +// Zen 2-only host implements but Intel hosts (and Rosetta 2 on Apple Silicon) do not: +// - SSE4a EXTRQ/INSERTQ, immediate form +// - MONITORX/MWAITX +// +// This is a direct port of Kyty's Loader::X64InstructionEmulator (TryEmulateSse4a / +// TryEmulateMonitorxMwaitx). SharpEmu already special-cases exactly one compiled EXTRQ+VPBLENDD +// byte sequence at load time (Sse4aExtrqBlendPatch), which only helps the one idiom it was +// reverse-engineered from. This file is a general, fault-time fallback that engages for any +// immediate-form EXTRQ/INSERTQ or MONITORX/MWAITX the narrower patch (or a title using a +// different compiler/register allocation) does not cover, complementing rather than replacing +// it: the load-time patch still avoids paying the fault-and-recover cost on the hot path it was +// built for, while this method is the safety net for everything else. +// +// This is deliberately additive: DirectExecutionBackend.IllegalInstruction.cs (the BMI1/BMI2/ABM +// fallback) is untouched, and this method is only reached from VectoredHandler after that one +// has already declined to handle the fault. +public sealed partial class DirectExecutionBackend +{ + // Byte offset of Xmm0 within the Win64 CONTEXT record: FltSave (the XMM_SAVE_AREA32/FXSAVE + // image) starts right after Rip at offset 256, and XmmRegisters[0] sits 160 bytes into that + // area (32-byte header + 8 legacy x87/MMX slots x 16 bytes). 256 + 160 = 416 (0x1A0). Cross- + // checked against this file's own Win64ContextSize (0x4D0): rebuilding the whole CONTEXT + // layout field-by-field from offset 0 lands on the same 0x4D0 total, which would not happen + // if this offset (or anything before it) were wrong. + private const int Win64ContextXmm0Offset = 0x1A0; + + private static int _sse4aSoftwareFallbackAnnounced; + private static long _sse4aInstructionsEmulated; + private static int _monitorxSoftwareFallbackAnnounced; + private static long _monitorxInstructionsEmulated; + + private unsafe bool TryRecoverAmdCompatInstruction(void* contextRecord, ulong rip) + { + if (TryRecoverMonitorxMwaitx(contextRecord, rip)) + { + return true; + } + + // MONITORX/MWAITX above only ever reads guest code memory and rewrites RIP, both of + // which the POSIX signal bridge (DirectExecutionBackend.PosixSignals.cs) faithfully + // round-trips through the real ucontext, so it works on every supported OS. EXTRQ/ + // INSERTQ additionally read and write an XMM register: on Windows contextRecord is the + // live CONTEXT the OS resumes the thread from, so touching the Xmm0.. slots is visible + // to the guest, but on POSIX contextRecord is a CONTEXT-shaped scratch buffer that the + // bridge only populates with the 17 general-purpose registers - the XMM region is never + // read from or written back to the real mcontext/ucontext. Running this on POSIX would + // silently compute a result from stale/zeroed XMM bytes and then discard whatever it + // "wrote", so keep it Windows-only, matching Kyty's own scope for the identical fix. + return OperatingSystem.IsWindows() && TryRecoverSse4aExtractInsert(contextRecord, rip); + } + + private unsafe bool TryRecoverMonitorxMwaitx(void* contextRecord, ulong rip) + { + // MONITORX (0F 01 FA) and MWAITX (0F 01 FB) are fixed 3-byte encodings with no + // ModRM/SIB/displacement/immediate, so a raw byte compare is sufficient and unambiguous. + var opcode = new byte[3]; + if (!TryReadHostBytes(rip, opcode) || + opcode[0] != 0x0F || opcode[1] != 0x01 || (opcode[2] != 0xFA && opcode[2] != 0xFB)) + { + return false; + } + + // PS5 titles use this pair in idle/wait loops: MONITORX arms a monitor on a cache line + // and MWAITX blocks until that line is written (or a timeout elapses). Hosts without + // the extension raise #UD on either one. We do not model the monitor itself, only its + // observable effect on guest forward progress: MONITORX becomes a no-op (arming a + // watch we never honour has no side effect of its own) and MWAITX becomes a plain + // thread yield, i.e. treat the awaited condition as already satisfied so the guest + // loop keeps making progress instead of executing an illegal opcode forever. + if (opcode[2] == 0xFB) + { + Thread.Yield(); + } + + WriteCtxU64(contextRecord, CTX_RIP, rip + 3); + + Interlocked.Increment(ref _monitorxInstructionsEmulated); + if (Interlocked.Exchange(ref _monitorxSoftwareFallbackAnnounced, 1) == 0) + { + Console.Error.WriteLine( + "[LOADER][INFO] Host lacks AMD MONITORX/MWAITX used by the guest; " + + "emulating those instructions in software."); + } + + return true; + } + + private unsafe bool TryRecoverSse4aExtractInsert(void* contextRecord, ulong rip) + { + if (!OperatingSystem.IsWindows() || !TryReadFaultingInstruction(rip, out var instruction)) + { + return false; + } + + var isExtrq = instruction.Mnemonic == Mnemonic.Extrq; + var isInsertq = instruction.Mnemonic == Mnemonic.Insertq; + if (!isExtrq && !isInsertq) + { + return false; + } + + if (isExtrq && instruction.OpCount != 3 || isInsertq && instruction.OpCount != 4) + { + return false; + } + + if (instruction.GetOpKind(0) != OpKind.Register || + !TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset)) + { + return false; + } + + var destLow = ReadCtxU64(contextRecord, destOffset); + if (isExtrq) + { + var length = (int)instruction.GetImmediate(1); + var index = (int)instruction.GetImmediate(2); + if (!Sse4aBitFieldEmulator.IsValidBitField(length, index)) + { + return false; + } + + WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.ExtractBitField(destLow, length, index)); + WriteCtxU64(contextRecord, destOffset + 8, 0); + } + else + { + if (instruction.GetOpKind(1) != OpKind.Register || + !TryGetXmmOffset(instruction.GetOpRegister(1), out var srcOffset)) + { + return false; + } + + var length = (int)instruction.GetImmediate(2); + var index = (int)instruction.GetImmediate(3); + if (!Sse4aBitFieldEmulator.IsValidBitField(length, index)) + { + return false; + } + + WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.InsertBitField( + destLow, ReadCtxU64(contextRecord, srcOffset), length, index)); + WriteCtxU64(contextRecord, destOffset + 8, 0); + } + + WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length); + + Interlocked.Increment(ref _sse4aInstructionsEmulated); + if (Interlocked.Exchange(ref _sse4aSoftwareFallbackAnnounced, 1) == 0) + { + Console.Error.WriteLine( + "[LOADER][INFO] Host lacks SSE4a EXTRQ/INSERTQ used by the guest; " + + "emulating those instructions in software."); + } + + return true; + } + + // Maps an Iced XMM register to its byte offset in the Win64 CONTEXT record. Written as an + // explicit switch (rather than arithmetic on the Register enum) to match the style already + // used by TryGetGprSlot/TryGetGpr64Offset in DirectExecutionBackend.IllegalInstruction.cs. + private static bool TryGetXmmOffset(Register register, out int offset) + { + switch (register) + { + case Register.XMM0: offset = Win64ContextXmm0Offset + 16 * 0; return true; + case Register.XMM1: offset = Win64ContextXmm0Offset + 16 * 1; return true; + case Register.XMM2: offset = Win64ContextXmm0Offset + 16 * 2; return true; + case Register.XMM3: offset = Win64ContextXmm0Offset + 16 * 3; return true; + case Register.XMM4: offset = Win64ContextXmm0Offset + 16 * 4; return true; + case Register.XMM5: offset = Win64ContextXmm0Offset + 16 * 5; return true; + case Register.XMM6: offset = Win64ContextXmm0Offset + 16 * 6; return true; + case Register.XMM7: offset = Win64ContextXmm0Offset + 16 * 7; return true; + case Register.XMM8: offset = Win64ContextXmm0Offset + 16 * 8; return true; + case Register.XMM9: offset = Win64ContextXmm0Offset + 16 * 9; return true; + case Register.XMM10: offset = Win64ContextXmm0Offset + 16 * 10; return true; + case Register.XMM11: offset = Win64ContextXmm0Offset + 16 * 11; return true; + case Register.XMM12: offset = Win64ContextXmm0Offset + 16 * 12; return true; + case Register.XMM13: offset = Win64ContextXmm0Offset + 16 * 13; return true; + case Register.XMM14: offset = Win64ContextXmm0Offset + 16 * 14; return true; + case Register.XMM15: offset = Win64ContextXmm0Offset + 16 * 15; return true; + default: + offset = 0; + return false; + } + } +} diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index 00dd19bd..f5da11c7 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -133,6 +133,11 @@ public sealed partial class DirectExecutionBackend { return -1; } + if (exceptionCode == StatusIllegalInstruction && + TryRecoverAmdCompatInstruction(contextRecord, rip)) + { + return -1; + } if (IsBenignHostDebugException(exceptionCode)) { return -1; diff --git a/tests/SharpEmu.Libs.Tests/Cpu/Sse4aBitFieldEmulatorTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/Sse4aBitFieldEmulatorTests.cs new file mode 100644 index 00000000..82942f0f --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/Sse4aBitFieldEmulatorTests.cs @@ -0,0 +1,147 @@ +// 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 EXTRQ/INSERTQ bit-field semantics used by the general SSE4a +// illegal-instruction software fallback (DirectExecutionBackend.Amd64Compat.cs). Expected values +// were computed from the AMD64 Architecture Programmer's Manual definitions and cross-checked +// with an independent Python re-implementation before being written here, so a regression in the +// ported bit math fails in this file without needing a live guest or a Windows host. +public sealed class Sse4aBitFieldEmulatorTests +{ + [Fact] + public void ExtractBitField_ExtractsLowByte() + { + var result = Sse4aBitFieldEmulator.ExtractBitField(0x1234_5678_9ABC_DEF0, length: 8, index: 0); + + Assert.Equal(0xF0UL, result); + } + + [Fact] + public void ExtractBitField_ExtractsMidFieldAtNonZeroIndex() + { + // bits [31:16] of 0x1234_5678_9ABC_DEF0 == 0x9ABC + var result = Sse4aBitFieldEmulator.ExtractBitField(0x1234_5678_9ABC_DEF0, length: 16, index: 16); + + Assert.Equal(0x9ABCUL, result); + } + + [Fact] + public void ExtractBitField_LengthZeroMeansSixtyFour() + { + var result = Sse4aBitFieldEmulator.ExtractBitField(0xFFFF_FFFF_FFFF_FFFF, length: 0, index: 0); + + Assert.Equal(0xFFFF_FFFF_FFFF_FFFFUL, result); + } + + [Fact] + public void ExtractBitField_MasksImmediatesToLowSixBits() + { + // length=0x28 (40) and index=0 is exactly the idiom SharpEmu's load-time + // Sse4aExtrqBlendPatch already recognizes; the general emulator must agree with it. + var result = Sse4aBitFieldEmulator.ExtractBitField(0x0000_0000_0000_00FF, length: 0x28, index: 0); + + Assert.Equal(0xFFUL, result); + } + + [Theory] + [InlineData(0x1234_5678_9ABC_DEF0UL)] + [InlineData(0x0000_0000_0000_0000UL)] + [InlineData(0xFFFF_FFFF_FFFF_FFFFUL)] + [InlineData(0x00FF_00FF_00FF_00FFUL)] + public void ExtractBitField_AgreesWithSse4aExtrqBlendPatchsByteFourRule(ulong value) + { + // Sse4aExtrqBlendPatch's own comment states that after "EXTRQ xmmN, 0x28, 0x00", dword + // lane 1 (bits 63:32) of the result equals byte 4 of the source zero-extended. The + // general emulator (used for every other EXTRQ occurrence) must produce a result + // consistent with that independently-reverse-engineered rule for the one idiom both + // paths can be checked against. + var extractedLow64 = Sse4aBitFieldEmulator.ExtractBitField(value, length: 0x28, index: 0); + var dword1 = (uint)(extractedLow64 >> 32); + var byteFourZeroExtended = (uint)((value >> 32) & 0xFF); + + Assert.Equal(byteFourZeroExtended, dword1); + } + + [Fact] + public void ExtractBitField_RejectsUndefinedFieldPastRegisterEnd() + { + Assert.False(Sse4aBitFieldEmulator.IsValidBitField(length: 8, index: 60)); + Assert.Equal(0UL, Sse4aBitFieldEmulator.ExtractBitField( + 0xFFFF_FFFF_FFFF_FFFF, + length: 8, + index: 60)); + } + + [Fact] + public void ExtractBitField_RejectsZeroLengthAtNonZeroIndex() + { + Assert.False(Sse4aBitFieldEmulator.IsValidBitField(length: 0, index: 1)); + } + + [Fact] + public void InsertBitField_InsertsFieldAtNonZeroIndexWithoutDisturbingOtherBits() + { + var result = Sse4aBitFieldEmulator.InsertBitField( + destination: 0x0000_0000_0000_0000, + source: 0xFFFF_FFFF_FFFF_FFFF, + length: 8, + index: 8); + + Assert.Equal(0x0000_0000_0000_FF00UL, result); + } + + [Fact] + public void InsertBitField_ClearsExactlyTheDestinationWindowBeforeInserting() + { + var result = Sse4aBitFieldEmulator.InsertBitField( + destination: 0xFFFF_FFFF_FFFF_FFFF, + source: 0x0000_0000_0000_0000, + length: 16, + index: 16); + + Assert.Equal(0xFFFF_FFFF_0000_FFFFUL, result); + } + + [Fact] + public void InsertBitField_InsertsLowByteAtIndexZero() + { + var result = Sse4aBitFieldEmulator.InsertBitField( + destination: 0x1122_3344_5566_7788, + source: 0xAABB_CCDD_EEFF_0011, + length: 8, + index: 0); + + Assert.Equal(0x1122_3344_5566_7711UL, result); + } + + [Fact] + public void InsertBitField_LengthZeroMeansSixtyFourAndOverwritesEverything() + { + var result = Sse4aBitFieldEmulator.InsertBitField( + destination: 0x1111_1111_1111_1111, + source: 0xFFFF_FFFF_FFFF_FFFF, + length: 0, + index: 0); + + Assert.Equal(0xFFFF_FFFF_FFFF_FFFFUL, result); + } + + [Fact] + public void InsertBitField_ZeroSourceFieldClearsOnlyItsOwnWindow() + { + // A zero-valued 12-bit field inserted at index 20 clears exactly bits [31:20] + // (0x234 -> 0x000) and leaves every bit outside that window untouched. + var result = Sse4aBitFieldEmulator.InsertBitField( + destination: 0xABCD_EF01_2345_6789, + source: 0, + length: 12, + index: 20); + + Assert.Equal(0xABCD_EF01_0005_6789UL, result); + } +}