diff --git a/src/SharpEmu.Core/Cpu/Emulation/BmiInstructionEmulator.cs b/src/SharpEmu.Core/Cpu/Emulation/BmiInstructionEmulator.cs new file mode 100644 index 00000000..13fbd3a2 --- /dev/null +++ b/src/SharpEmu.Core/Cpu/Emulation/BmiInstructionEmulator.cs @@ -0,0 +1,305 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Numerics; + +namespace SharpEmu.Core.Cpu.Emulation; + +/// +/// Pure software implementations of the BMI1, BMI2 and ABM general-purpose-register +/// bit-manipulation instructions. +/// +/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's +/// Zen 2 cores implement BMI1/BMI2/ABM, but a host CPU that predates those extensions raises +/// #UD (STATUS_ILLEGAL_INSTRUCTION) when it meets one of these opcodes. This class provides the +/// register-only arithmetic so the exception handler can finish the instruction in software and +/// resume, instead of aborting the title. +/// +/// The methods deliberately operate on plain integers rather than on the OS CONTEXT record so the +/// semantics can be unit-tested in isolation; the unsafe register/memory plumbing lives in the +/// backend adapter. Each method returns the result already masked to the operand width and, where +/// the instruction is defined to touch flags, updates in place. Flags +/// documented as "undefined" by the vendor manuals are left untouched so behaviour stays +/// deterministic across hosts. +/// +public static class BmiInstructionEmulator +{ + private const uint FlagCarry = 1u << 0; + private const uint FlagZero = 1u << 6; + private const uint FlagSign = 1u << 7; + private const uint FlagOverflow = 1u << 11; + + private static ulong WidthMask(GprOperandSize size) => + size == GprOperandSize.Bits64 ? ulong.MaxValue : 0xFFFF_FFFFUL; + + private static int WidthBits(GprOperandSize size) => (int)size; + + private static bool SignSet(ulong value, GprOperandSize size) => + size == GprOperandSize.Bits64 ? (value >> 63) != 0 : (value & 0x8000_0000UL) != 0; + + private static uint WithFlag(uint eflags, uint flag, bool set) => + set ? eflags | flag : eflags & ~flag; + + // Applies the CF/ZF/SF/OF set shared by ANDN/BLS*/BZHI: OF is always cleared, ZF and SF follow + // the result, and the caller supplies CF because each instruction defines it differently. + private static uint ApplyLogicFlags(uint eflags, ulong result, GprOperandSize size, bool carry) + { + eflags = WithFlag(eflags, FlagCarry, carry); + eflags = WithFlag(eflags, FlagZero, result == 0); + eflags = WithFlag(eflags, FlagSign, SignSet(result, size)); + eflags = WithFlag(eflags, FlagOverflow, false); + return eflags; + } + + /// ANDN: dest = (~src1) & src2. CF and OF are cleared. + public static ulong Andn(ulong src1, ulong src2, GprOperandSize size, ref uint eflags) + { + var result = (~src1 & src2) & WidthMask(size); + eflags = ApplyLogicFlags(eflags, result, size, carry: false); + return result; + } + + /// BLSI: isolate the lowest set bit, dest = (-src) & src. CF = (src != 0). + public static ulong Blsi(ulong src, GprOperandSize size, ref uint eflags) + { + var mask = WidthMask(size); + var s = src & mask; + var result = ((0UL - s) & s) & mask; + eflags = ApplyLogicFlags(eflags, result, size, carry: s != 0); + return result; + } + + /// BLSMSK: mask up to and including the lowest set bit, dest = (src - 1) ^ src. CF = (src == 0). + public static ulong Blsmsk(ulong src, GprOperandSize size, ref uint eflags) + { + var mask = WidthMask(size); + var s = src & mask; + var result = ((s - 1) ^ s) & mask; + eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0); + return result; + } + + /// BLSR: reset the lowest set bit, dest = (src - 1) & src. CF = (src == 0). + public static ulong Blsr(ulong src, GprOperandSize size, ref uint eflags) + { + var mask = WidthMask(size); + var s = src & mask; + var result = ((s - 1) & s) & mask; + eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0); + return result; + } + + /// + /// BEXTR: extract len bits of starting at bit start, where + /// start = control[7:0] and len = control[15:8]. Only ZF (per result) and cleared CF/OF are defined. + /// + public static ulong Bextr(ulong src, ulong control, GprOperandSize size, ref uint eflags) + { + var bits = WidthBits(size); + var start = (int)(control & 0xFF); + var length = (int)((control >> 8) & 0xFF); + + ulong result; + if (start >= bits) + { + result = 0; + } + else + { + var shifted = (src & WidthMask(size)) >> start; + if (length == 0) + { + result = 0; + } + else if (length >= bits) + { + result = shifted; + } + else + { + result = shifted & ((1UL << length) - 1); + } + } + + result &= WidthMask(size); + eflags = WithFlag(eflags, FlagZero, result == 0); + eflags = WithFlag(eflags, FlagCarry, false); + eflags = WithFlag(eflags, FlagOverflow, false); + return result; + } + + /// + /// BZHI: zero the bits of from bit position index[7:0] upward. + /// CF is set when the requested position is at or beyond the operand width. + /// + public static ulong Bzhi(ulong src, ulong index, GprOperandSize size, ref uint eflags) + { + var bits = WidthBits(size); + var mask = WidthMask(size); + var s = src & mask; + var n = (int)(index & 0xFF); + + ulong result; + bool carry; + if (n >= bits) + { + result = s; + carry = true; + } + else + { + result = s & ((1UL << n) - 1); + carry = false; + } + + eflags = ApplyLogicFlags(eflags, result, size, carry); + return result; + } + + /// TZCNT: count trailing zero bits. If src == 0 the result is the operand width and CF is set. + public static ulong Tzcnt(ulong src, GprOperandSize size, ref uint eflags) + { + var bits = WidthBits(size); + var s = src & WidthMask(size); + + ulong result; + bool carry; + if (s == 0) + { + result = (ulong)bits; + carry = true; + } + else + { + result = (ulong)(size == GprOperandSize.Bits64 + ? BitOperations.TrailingZeroCount(s) + : BitOperations.TrailingZeroCount((uint)s)); + carry = false; + } + + eflags = WithFlag(eflags, FlagCarry, carry); + eflags = WithFlag(eflags, FlagZero, result == 0); + return result; + } + + /// LZCNT: count leading zero bits. If src == 0 the result is the operand width and CF is set. + public static ulong Lzcnt(ulong src, GprOperandSize size, ref uint eflags) + { + var bits = WidthBits(size); + var s = src & WidthMask(size); + + ulong result; + bool carry; + if (s == 0) + { + result = (ulong)bits; + carry = true; + } + else + { + result = (ulong)(size == GprOperandSize.Bits64 + ? BitOperations.LeadingZeroCount(s) + : BitOperations.LeadingZeroCount((uint)s)); + carry = false; + } + + eflags = WithFlag(eflags, FlagCarry, carry); + eflags = WithFlag(eflags, FlagZero, result == 0); + return result; + } + + /// RORX: rotate right by (masked to the operand width). No flags. + public static ulong Rorx(ulong src, int count, GprOperandSize size) + { + var bits = WidthBits(size); + var mask = WidthMask(size); + var s = src & mask; + var rotate = count & (bits - 1); + if (rotate == 0) + { + return s; + } + + return ((s >> rotate) | (s << (bits - rotate))) & mask; + } + + /// SARX: arithmetic shift right by (masked to the operand width). No flags. + public static ulong Sarx(ulong src, int count, GprOperandSize size) + { + var bits = WidthBits(size); + var mask = WidthMask(size); + var shift = count & (bits - 1); + if (size == GprOperandSize.Bits64) + { + return (ulong)((long)src >> shift); + } + + return (ulong)(uint)((int)(uint)src >> shift) & mask; + } + + /// SHLX: logical shift left by (masked to the operand width). No flags. + public static ulong Shlx(ulong src, int count, GprOperandSize size) + { + var bits = WidthBits(size); + var mask = WidthMask(size); + var shift = count & (bits - 1); + return (src << shift) & mask; + } + + /// SHRX: logical shift right by (masked to the operand width). No flags. + public static ulong Shrx(ulong src, int count, GprOperandSize size) + { + var bits = WidthBits(size); + var mask = WidthMask(size); + var s = src & mask; + var shift = count & (bits - 1); + return s >> shift; + } + + /// PDEP: deposit contiguous low bits of into the positions selected by . No flags. + public static ulong Pdep(ulong src, ulong mask, GprOperandSize size) + { + var bits = WidthBits(size); + var selector = mask & WidthMask(size); + ulong result = 0; + var bit = 0; + for (var i = 0; i < bits; i++) + { + var position = 1UL << i; + if ((selector & position) != 0) + { + if (((src >> bit) & 1UL) != 0) + { + result |= position; + } + + bit++; + } + } + + return result & WidthMask(size); + } + + /// PEXT: gather the bits of selected by into contiguous low bits. No flags. + public static ulong Pext(ulong src, ulong mask, GprOperandSize size) + { + var bits = WidthBits(size); + var selector = mask & WidthMask(size); + ulong result = 0; + var bit = 0; + for (var i = 0; i < bits; i++) + { + if ((selector & (1UL << i)) != 0) + { + if (((src >> i) & 1UL) != 0) + { + result |= 1UL << bit; + } + + bit++; + } + } + + return result & WidthMask(size); + } +} diff --git a/src/SharpEmu.Core/Cpu/Emulation/GprOperandSize.cs b/src/SharpEmu.Core/Cpu/Emulation/GprOperandSize.cs new file mode 100644 index 00000000..137ab21a --- /dev/null +++ b/src/SharpEmu.Core/Cpu/Emulation/GprOperandSize.cs @@ -0,0 +1,14 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Core.Cpu.Emulation; + +/// +/// Operand width for the emulated general-purpose-register instructions. The numeric value is the +/// bit width, so it can double as the "count leading/trailing zeros of an all-zero source" result. +/// +public enum GprOperandSize +{ + Bits32 = 32, + Bits64 = 64, +} diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index 9ea5199b..00dd19bd 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -128,6 +128,11 @@ public sealed partial class DirectExecutionBackend { return -1; } + if (exceptionCode == StatusIllegalInstruction && + TryRecoverIllegalInstruction(contextRecord, rip)) + { + return -1; + } if (IsBenignHostDebugException(exceptionCode)) { return -1; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.IllegalInstruction.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.IllegalInstruction.cs new file mode 100644 index 00000000..1ee832c6 --- /dev/null +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.IllegalInstruction.cs @@ -0,0 +1,362 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using System.Threading; +using Iced.Intel; +using SharpEmu.Core.Cpu.Emulation; + +namespace SharpEmu.Core.Cpu.Native; + +// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions. +// +// Guest code runs natively, so the guest and host share the same virtual address space and the +// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks +// one of these extensions it raises #UD instead of executing the opcode; without this the title +// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register +// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask +// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false +// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it +// does not fully model. +public sealed partial class DirectExecutionBackend +{ + // Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares + // with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs. + private const int CTX_EFLAGS = 68; + + // STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler). + private const uint StatusIllegalInstruction = 0xC000001Du; + + private const int MaxInstructionBytes = 15; + + // Instruction-window sizes tried in turn so a fault near a page boundary still decodes. + private static readonly int[] DecodeWindowSizes = { MaxInstructionBytes, 11, 8, 4, 2 }; + + private static int _bmiSoftwareFallbackAnnounced; + private static long _bmiInstructionsEmulated; + + private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip) + { + if (!TryReadFaultingInstruction(rip, out var instruction)) + { + return false; + } + + if (instruction.Op0Kind != OpKind.Register || + !TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size)) + { + return false; + } + + if (!TryEvaluate(contextRecord, in instruction, size, out var result, out var flagsChanged, out var eflags)) + { + return false; + } + + WriteCtxU64(contextRecord, destOffset, result); + if (flagsChanged) + { + WriteCtxU32(contextRecord, CTX_EFLAGS, eflags); + } + + WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length); + + Interlocked.Increment(ref _bmiInstructionsEmulated); + if (Interlocked.Exchange(ref _bmiSoftwareFallbackAnnounced, 1) == 0) + { + Console.Error.WriteLine( + "[LOADER][INFO] Host lacks a BMI/ABM extension used by the guest; " + + "emulating those instructions in software."); + } + + return true; + } + + private unsafe bool TryEvaluate( + void* contextRecord, + in Instruction instruction, + GprOperandSize size, + out ulong result, + out bool flagsChanged, + out uint eflags) + { + result = 0; + flagsChanged = false; + eflags = ReadCtxU32(contextRecord, CTX_EFLAGS); + + switch (instruction.Mnemonic) + { + case Mnemonic.Andn: + if (!TryReadOperand(contextRecord, in instruction, 1, size, out var andnSrc1) || + !TryReadOperand(contextRecord, in instruction, 2, size, out var andnSrc2)) + { + return false; + } + + result = BmiInstructionEmulator.Andn(andnSrc1, andnSrc2, size, ref eflags); + flagsChanged = true; + return true; + + case Mnemonic.Blsi: + case Mnemonic.Blsmsk: + case Mnemonic.Blsr: + if (!TryReadOperand(contextRecord, in instruction, 1, size, out var blsSrc)) + { + return false; + } + + result = instruction.Mnemonic switch + { + Mnemonic.Blsi => BmiInstructionEmulator.Blsi(blsSrc, size, ref eflags), + Mnemonic.Blsmsk => BmiInstructionEmulator.Blsmsk(blsSrc, size, ref eflags), + _ => BmiInstructionEmulator.Blsr(blsSrc, size, ref eflags), + }; + flagsChanged = true; + return true; + + case Mnemonic.Bextr: + if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bextrSrc) || + !TryReadOperand(contextRecord, in instruction, 2, size, out var bextrControl)) + { + return false; + } + + result = BmiInstructionEmulator.Bextr(bextrSrc, bextrControl, size, ref eflags); + flagsChanged = true; + return true; + + case Mnemonic.Bzhi: + if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bzhiSrc) || + !TryReadOperand(contextRecord, in instruction, 2, size, out var bzhiIndex)) + { + return false; + } + + result = BmiInstructionEmulator.Bzhi(bzhiSrc, bzhiIndex, size, ref eflags); + flagsChanged = true; + return true; + + case Mnemonic.Tzcnt: + case Mnemonic.Lzcnt: + if (!TryReadOperand(contextRecord, in instruction, 1, size, out var cntSrc)) + { + return false; + } + + result = instruction.Mnemonic == Mnemonic.Tzcnt + ? BmiInstructionEmulator.Tzcnt(cntSrc, size, ref eflags) + : BmiInstructionEmulator.Lzcnt(cntSrc, size, ref eflags); + flagsChanged = true; + return true; + + case Mnemonic.Rorx: + if (instruction.Op2Kind != OpKind.Immediate8 || + !TryReadOperand(contextRecord, in instruction, 1, size, out var rorxSrc)) + { + return false; + } + + result = BmiInstructionEmulator.Rorx(rorxSrc, instruction.Immediate8, size); + return true; + + case Mnemonic.Sarx: + case Mnemonic.Shlx: + case Mnemonic.Shrx: + if (!TryReadOperand(contextRecord, in instruction, 1, size, out var shiftSrc) || + !TryReadOperand(contextRecord, in instruction, 2, size, out var shiftCount)) + { + return false; + } + + result = instruction.Mnemonic switch + { + Mnemonic.Sarx => BmiInstructionEmulator.Sarx(shiftSrc, (int)shiftCount, size), + Mnemonic.Shlx => BmiInstructionEmulator.Shlx(shiftSrc, (int)shiftCount, size), + _ => BmiInstructionEmulator.Shrx(shiftSrc, (int)shiftCount, size), + }; + return true; + + case Mnemonic.Pdep: + case Mnemonic.Pext: + if (!TryReadOperand(contextRecord, in instruction, 1, size, out var packSrc) || + !TryReadOperand(contextRecord, in instruction, 2, size, out var packMask)) + { + return false; + } + + result = instruction.Mnemonic == Mnemonic.Pdep + ? BmiInstructionEmulator.Pdep(packSrc, packMask, size) + : BmiInstructionEmulator.Pext(packSrc, packMask, size); + return true; + + default: + return false; + } + } + + private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction) + { + // Try the full instruction window first, then shrink so a fault near the end of a mapped + // page (where fewer than 15 bytes are readable) still decodes. + foreach (var attempt in DecodeWindowSizes) + { + var buffer = new byte[attempt]; + if (!TryReadHostBytes(rip, buffer)) + { + continue; + } + + var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer)); + decoder.IP = rip; + decoder.Decode(out instruction); + if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt) + { + return true; + } + } + + instruction = default; + return false; + } + + private unsafe bool TryReadOperand( + void* contextRecord, + in Instruction instruction, + int operandIndex, + GprOperandSize size, + out ulong value) + { + value = 0; + switch (instruction.GetOpKind(operandIndex)) + { + case OpKind.Register: + if (!TryGetGprSlot(instruction.GetOpRegister(operandIndex), out var offset, out _)) + { + return false; + } + + var raw = ReadCtxU64(contextRecord, offset); + value = size == GprOperandSize.Bits64 ? raw : raw & 0xFFFF_FFFFUL; + return true; + + case OpKind.Memory: + if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address)) + { + return false; + } + + var byteCount = size == GprOperandSize.Bits64 ? 8 : 4; + var buffer = new byte[byteCount]; + if (!TryReadHostBytes(address, buffer)) + { + return false; + } + + value = byteCount == 8 + ? BinaryPrimitives.ReadUInt64LittleEndian(buffer) + : BinaryPrimitives.ReadUInt32LittleEndian(buffer); + return true; + + default: + return false; + } + } + + private unsafe bool TryComputeMemoryAddress(void* contextRecord, in Instruction instruction, out ulong address) + { + address = 0; + + // FS/GS-relative operands need the guest segment base, which is not modelled here. + if (instruction.SegmentPrefix != Register.None) + { + return false; + } + + if (instruction.IsIPRelativeMemoryOperand) + { + address = instruction.IPRelativeMemoryAddress; + return true; + } + + var effective = instruction.MemoryDisplacement64; + if (instruction.MemoryBase != Register.None) + { + if (!TryGetGpr64Offset(instruction.MemoryBase, out var baseOffset)) + { + return false; + } + + effective += ReadCtxU64(contextRecord, baseOffset); + } + + if (instruction.MemoryIndex != Register.None) + { + if (!TryGetGpr64Offset(instruction.MemoryIndex, out var indexOffset)) + { + return false; + } + + effective += ReadCtxU64(contextRecord, indexOffset) * (ulong)instruction.MemoryIndexScale; + } + + address = effective; + return true; + } + + // Maps a 32- or 64-bit GPR to its CONTEXT offset and reports the operand width it implies. + private static bool TryGetGprSlot(Register register, out int offset, out GprOperandSize size) + { + switch (register) + { + case Register.EAX: offset = CTX_RAX; size = GprOperandSize.Bits32; return true; + case Register.ECX: offset = CTX_RCX; size = GprOperandSize.Bits32; return true; + case Register.EDX: offset = CTX_RDX; size = GprOperandSize.Bits32; return true; + case Register.EBX: offset = CTX_RBX; size = GprOperandSize.Bits32; return true; + case Register.ESP: offset = CTX_RSP; size = GprOperandSize.Bits32; return true; + case Register.EBP: offset = CTX_RBP; size = GprOperandSize.Bits32; return true; + case Register.ESI: offset = CTX_RSI; size = GprOperandSize.Bits32; return true; + case Register.EDI: offset = CTX_RDI; size = GprOperandSize.Bits32; return true; + case Register.R8D: offset = CTX_R8; size = GprOperandSize.Bits32; return true; + case Register.R9D: offset = CTX_R9; size = GprOperandSize.Bits32; return true; + case Register.R10D: offset = CTX_R10; size = GprOperandSize.Bits32; return true; + case Register.R11D: offset = CTX_R11; size = GprOperandSize.Bits32; return true; + case Register.R12D: offset = CTX_R12; size = GprOperandSize.Bits32; return true; + case Register.R13D: offset = CTX_R13; size = GprOperandSize.Bits32; return true; + case Register.R14D: offset = CTX_R14; size = GprOperandSize.Bits32; return true; + case Register.R15D: offset = CTX_R15; size = GprOperandSize.Bits32; return true; + default: + if (TryGetGpr64Offset(register, out offset)) + { + size = GprOperandSize.Bits64; + return true; + } + + size = GprOperandSize.Bits32; + return false; + } + } + + private static bool TryGetGpr64Offset(Register register, out int offset) + { + switch (register) + { + case Register.RAX: offset = CTX_RAX; return true; + case Register.RCX: offset = CTX_RCX; return true; + case Register.RDX: offset = CTX_RDX; return true; + case Register.RBX: offset = CTX_RBX; return true; + case Register.RSP: offset = CTX_RSP; return true; + case Register.RBP: offset = CTX_RBP; return true; + case Register.RSI: offset = CTX_RSI; return true; + case Register.RDI: offset = CTX_RDI; return true; + case Register.R8: offset = CTX_R8; return true; + case Register.R9: offset = CTX_R9; return true; + case Register.R10: offset = CTX_R10; return true; + case Register.R11: offset = CTX_R11; return true; + case Register.R12: offset = CTX_R12; return true; + case Register.R13: offset = CTX_R13; return true; + case Register.R14: offset = CTX_R14; return true; + case Register.R15: offset = CTX_R15; return true; + default: offset = 0; return false; + } + } +} diff --git a/tests/SharpEmu.Libs.Tests/Cpu/BmiInstructionEmulatorTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/BmiInstructionEmulatorTests.cs new file mode 100644 index 00000000..08b2c9f2 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/BmiInstructionEmulatorTests.cs @@ -0,0 +1,243 @@ +// 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); + } +}