From 8260a59bcf1d958786fea1057f34b130ebbe2ebc Mon Sep 17 00:00:00 2001 From: CasualPokePlayer <50538166+CasualPokePlayer@users.noreply.github.com> Date: Thu, 10 Aug 2023 00:05:45 -0700 Subject: [PATCH] fix #3726 --- src/BizHawk.BizInvoke/FPCtrl.cs | 67 +++++++++ src/BizHawk.Client.EmuHawk/Program.cs | 3 + .../Consoles/Nintendo/QuickNES/QuickNES.cs | 138 ++++++------------ 3 files changed, 117 insertions(+), 91 deletions(-) create mode 100644 src/BizHawk.BizInvoke/FPCtrl.cs diff --git a/src/BizHawk.BizInvoke/FPCtrl.cs b/src/BizHawk.BizInvoke/FPCtrl.cs new file mode 100644 index 0000000000..dff5efbaed --- /dev/null +++ b/src/BizHawk.BizInvoke/FPCtrl.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace BizHawk.BizInvoke +{ + // .NET is weird and sets the x87 control register to double precision + // It does this even on Linux which normally expects it set to extended x87 precision + // Even stranger is that this appears to be completely unneeded on x86-64 + // On Windows, x87 registers are prohibited to be used in kernel code, and + // MSVC will not use the x87 registers (and presumably this extends to userland OS code) + // .NET presumbly follows MSVC and would not use the x87 registers (why would it? SSE is available! long double doesn't exist!) + // This thus only screws over code compiled with MinGW (which use extended x87 precision for long double) + // Or screws over any unmanaged code on Linux, which doubly includes all waterbox cores (!!!) + // Of course in practice, this only applies to any code using long double, which is rarely used typically + // But musl (used for waterbox cores) does end up using it for float formating in the printf family + // This can extend to issues in games: https://github.com/TASEmulators/BizHawk/issues/3726 + public static class FPCtrl + { + private const CallingConvention cc = CallingConvention.Cdecl; + + [UnmanagedFunctionPointer(cc)] + private delegate void FixFPCtrlDelegate(); + + private static readonly MemoryBlock? _memory; + private static readonly FixFPCtrlDelegate? _fixFpCtrl; + + static FPCtrl() + { + // not usable outside of x64 + if (RuntimeInformation.ProcessArchitecture != Architecture.X64) + { + return; + } + + // generate assembly for fixing the x87 control register + _memory = new(4096); + _memory.Protect(_memory.Start, _memory.Size, MemoryBlock.Protection.RW); + var ss = _memory.GetStream(_memory.Start, 64, true); + var bw = new BinaryWriter(ss); + + // FYI: The push/pop is only needed on Windows, but doesn't do any harm on Linux + + bw.Write((byte)0x50); // push rax + bw.Write(0x06247CD9); // fnstcw word[rsp + 6] + bw.Write(0x07244C80); // or byte[rsp + 7], 3 + bw.Write((byte)0x03); + bw.Write(0x06246CD9); // fldcw word[rsp + 6] + bw.Write((byte)0x58); // pop rax + bw.Write((byte)0xC3); // ret + + _memory.Protect(_memory.Start, _memory.Size, MemoryBlock.Protection.RX); + _fixFpCtrl = Marshal.GetDelegateForFunctionPointer((IntPtr)_memory.Start); + } + + public static void FixFPCtrl() + { + // not usable outside of x64 + if (RuntimeInformation.ProcessArchitecture != Architecture.X64) + { + return; + } + + _fixFpCtrl!(); + } + } +} diff --git a/src/BizHawk.Client.EmuHawk/Program.cs b/src/BizHawk.Client.EmuHawk/Program.cs index b49bdc8c8a..70fc48c399 100644 --- a/src/BizHawk.Client.EmuHawk/Program.cs +++ b/src/BizHawk.Client.EmuHawk/Program.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Windows.Forms; +using BizHawk.BizInvoke; using BizHawk.Bizware.BizwareGL; using BizHawk.Bizware.Graphics; using BizHawk.Common; @@ -285,6 +286,8 @@ namespace BizHawk.Client.EmuHawk } } + FPCtrl.FixFPCtrl(); + var exitCode = 0; try { diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.cs index 651f999c11..d49579cd0a 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/QuickNES/QuickNES.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -30,48 +29,41 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES [CoreConstructor(VSystemID.Raw.NES, Priority = CorePriority.Low)] public QuickNES(byte[] file, QuickNESSettings settings, QuickNESSyncSettings syncSettings) { - FP = OSTailoredCode.IsUnixHost - ? (IFPCtrl) new Unix_FPCtrl() - : new Win32_FPCtrl(); - - using (FP.Save()) + ServiceProvider = new BasicServiceProvider(this); + Context = QN.qn_new(); + if (Context == IntPtr.Zero) { - ServiceProvider = new BasicServiceProvider(this); - Context = QN.qn_new(); - if (Context == IntPtr.Zero) - { - throw new InvalidOperationException($"{nameof(QN.qn_new)}() returned NULL"); - } + throw new InvalidOperationException($"{nameof(QN.qn_new)}() returned NULL"); + } - try - { - file = FixInesHeader(file); - LibQuickNES.ThrowStringError(QN.qn_loadines(Context, file, file.Length)); + try + { + file = FixInesHeader(file); + LibQuickNES.ThrowStringError(QN.qn_loadines(Context, file, file.Length)); - InitSaveRamBuff(); - InitSaveStateBuff(); - InitAudio(); - InitMemoryDomains(); + InitSaveRamBuff(); + InitSaveStateBuff(); + InitAudio(); + InitMemoryDomains(); - int mapper = 0; - string mappername = Marshal.PtrToStringAnsi(QN.qn_get_mapper(Context, ref mapper)); - Console.WriteLine("QuickNES: Booted with Mapper #{0} \"{1}\"", mapper, mappername); - BoardName = mappername; - PutSettings((QuickNESSettings)settings ?? new QuickNESSettings()); + int mapper = 0; + string mappername = Marshal.PtrToStringAnsi(QN.qn_get_mapper(Context, ref mapper)); + Console.WriteLine("QuickNES: Booted with Mapper #{0} \"{1}\"", mapper, mappername); + BoardName = mappername; + PutSettings((QuickNESSettings)settings ?? new QuickNESSettings()); - _syncSettings = (QuickNESSyncSettings)syncSettings ?? new QuickNESSyncSettings(); - _syncSettingsNext = _syncSettings.Clone(); + _syncSettings = (QuickNESSyncSettings)syncSettings ?? new QuickNESSyncSettings(); + _syncSettingsNext = _syncSettings.Clone(); - SetControllerDefinition(); - ComputeBootGod(); + SetControllerDefinition(); + ComputeBootGod(); - ConnectTracer(); - } - catch - { - Dispose(); - throw; - } + ConnectTracer(); + } + catch + { + Dispose(); + throw; } } @@ -83,40 +75,6 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES int IVideoLogicalOffsets.ScreenY => _settings.ClipTopAndBottom ? 8 : 0; - private interface IFPCtrl : IDisposable - { - IDisposable Save(); - } - - private class Win32_FPCtrl : IFPCtrl - { - [Conditional("DEBUG")] - public static void PrintCurrentFP() => Console.WriteLine($"Current FP word: 0x{Win32Imports._control87(0, 0):X8}"); - - private uint cw; - - public IDisposable Save() - { - cw = Win32Imports._control87(0, 0); - Win32Imports._control87(0x00000, 0x30000); - return this; - } - - public void Dispose() - { - Win32Imports._control87(cw, 0x30000); - } - } - - private class Unix_FPCtrl : IFPCtrl - { - public IDisposable Save() => this; - - public void Dispose() {} - } - - private readonly IFPCtrl FP; - public ControllerDefinition ControllerDefinition { get; private set; } private void SetControllerDefinition() @@ -185,34 +143,32 @@ namespace BizHawk.Emulation.Cores.Consoles.Nintendo.QuickNES public bool FrameAdvance(IController controller, bool render, bool rendersound = true) { CheckDisposed(); - using (FP.Save()) - { - if (controller.IsPressed("Power")) - QN.qn_reset(Context, true); - if (controller.IsPressed("Reset")) - QN.qn_reset(Context, false); - SetPads(controller, out var j1, out var j2); + if (controller.IsPressed("Power")) + QN.qn_reset(Context, true); + if (controller.IsPressed("Reset")) + QN.qn_reset(Context, false); - QN.qn_set_tracecb(Context, Tracer.IsEnabled() ? _traceCb : null); + SetPads(controller, out var j1, out var j2); - LibQuickNES.ThrowStringError(QN.qn_emulate_frame(Context, j1, j2)); - IsLagFrame = QN.qn_get_joypad_read_count(Context) == 0; - if (IsLagFrame) - LagCount++; + QN.qn_set_tracecb(Context, Tracer.IsEnabled() ? _traceCb : null); - if (render) - Blit(); - if (rendersound) - DrainAudio(); + LibQuickNES.ThrowStringError(QN.qn_emulate_frame(Context, j1, j2)); + IsLagFrame = QN.qn_get_joypad_read_count(Context) == 0; + if (IsLagFrame) + LagCount++; - _callBack1?.Invoke(); - _callBack2?.Invoke(); + if (render) + Blit(); + if (rendersound) + DrainAudio(); - Frame++; + _callBack1?.Invoke(); + _callBack2?.Invoke(); - return true; - } + Frame++; + + return true; } private IntPtr Context;