From daaeb6213e1acbd9ab04f37dd1035eae32b46e29 Mon Sep 17 00:00:00 2001 From: Spooks <62370103+Spooks4576@users.noreply.github.com> Date: Sat, 18 Jul 2026 12:50:59 -0600 Subject: [PATCH] Fix Massive Bug Preventing UE5 Titles From Booting (#406) * Fix cross platform memcpy bug --- src/SharpEmu.CLI/packages.lock.json | 6 +- .../Cpu/Native/DirectExecutionBackend.cs | 19 ++- .../Kernel/KernelMemoryCompatExports.cs | 1 + .../Cpu/MemcpyHleRoutingTests.cs | 153 ++++++++++++++++++ 4 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs diff --git a/src/SharpEmu.CLI/packages.lock.json b/src/SharpEmu.CLI/packages.lock.json index f41e08b8..9d62b12a 100644 --- a/src/SharpEmu.CLI/packages.lock.json +++ b/src/SharpEmu.CLI/packages.lock.json @@ -4,9 +4,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.3, )", - "resolved": "10.0.3", - "contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA==" + "requested": "[10.0.8, )", + "resolved": "10.0.8", + "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw==" }, "Avalonia.Angle.Windows.Natives": { "type": "Transitive", diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 4b40ecbd..cac25765 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -1291,6 +1291,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private unsafe bool TryCreateNativeImportIntrinsic(string nid, out nint address) { + if (IsHlePreferredNid(nid)) + { + address = 0; + return false; + } + if (nid == "1jfXLRVzisc" && string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal)) { @@ -1466,8 +1472,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I "Q3VBxCXhUHs" => [ 0x48, 0x89, 0xF8, - 0x48, 0x89, 0xD1, - 0xF3, 0xA4, + 0x48, 0x85, 0xD2, + 0x74, 0x11, + 0x44, 0x8A, 0x06, + 0x44, 0x88, 0x07, + 0x48, 0xFF, 0xC6, + 0x48, 0xFF, 0xC7, + 0x48, 0xFF, 0xCA, + 0x75, 0xEF, 0xC3, ], "8zTFvBIAIN8" => @@ -1601,7 +1613,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private static bool IsHlePreferredNid(string nid) { - return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal); + return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) || + string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal); } private static bool IsLibcLibrary(string libraryName) diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 0a6c46e7..dac33c6e 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -1111,6 +1111,7 @@ public static partial class KernelMemoryCompatExports return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } + ctx[CpuRegister.Rax] = destination; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } diff --git a/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs new file mode 100644 index 00000000..48e7acdb --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/MemcpyHleRoutingTests.cs @@ -0,0 +1,153 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Core.Cpu.Native; +using SharpEmu.HLE; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Xunit; + +namespace SharpEmu.Libs.Tests.Cpu; + +public sealed class MemcpyHleRoutingTests +{ + private const string MemcpyNid = "Q3VBxCXhUHs"; + private const string MemsetNid = "QrZZdJ8XsX0"; + private const string RdtscNid = "-2IRUCO--PM"; + + [Fact] + public void IsHlePreferredNid_PrefersHleForMemcpy_OnEveryPlatform() + { + Assert.True( + InvokeIsHlePreferredNid(MemcpyNid), + $"memcpy ({MemcpyNid}) must route through HLE on every platform. It was previously " + + "gated behind OperatingSystem.IsWindows(), which left Linux and macOS on the LLE " + + "intrinsic stub and faulted in guest code. Do not reintroduce an OS condition here."); + } + + [Fact] + public void IsHlePreferredNid_PrefersHleForMemset() + { + Assert.True( + InvokeIsHlePreferredNid(MemsetNid), + $"memset ({MemsetNid}) must route through HLE on every platform."); + } + + [Fact] + public void TryCreateNativeImportIntrinsic_DoesNotClaimMemcpy() + { + if (RuntimeInformation.ProcessArchitecture != Architecture.X64) + { + return; + } + + var claimed = InvokeTryCreateNativeImportIntrinsic(MemcpyNid, out var address); + + Assert.False( + claimed, + $"memcpy ({MemcpyNid}) must fall through to the HLE trampoline. SetupImportStubs tries " + + "the intrinsic stub before the trampoline, so without an IsHlePreferredNid guard here " + + "the intrinsic claims memcpy and the HLE routing never takes effect."); + Assert.Equal(0, address); + } + + [Fact] + public void TryCreateNativeImportIntrinsic_StillClaimsNonHleNids() + { + if (RuntimeInformation.ProcessArchitecture != Architecture.X64) + { + return; + } + + var claimed = InvokeTryCreateNativeImportIntrinsic(RdtscNid, out var address); + + Assert.True( + claimed, + $"rdtsc ({RdtscNid}) has no HLE handler and must still receive an intrinsic stub. If " + + "this fails the memcpy assertions above may be passing vacuously."); + Assert.NotEqual(0, address); + + unsafe + { + Assert.True(HostMemory.Free((void*)address, 0, HostMemory.MEM_RELEASE)); + } + } + + [Fact] + public void IsHlePreferredNid_DoesNotBranchOnHostOperatingSystem() + { + var method = typeof(DirectExecutionBackend).GetMethod( + "IsHlePreferredNid", + BindingFlags.Static | BindingFlags.NonPublic); + Assert.NotNull(method); + + var callees = ResolveCallees(method); + + Assert.DoesNotContain(callees, static name => + name is "IsWindows" or "IsLinux" or "IsMacOS" or "IsOSPlatform" or "IsFreeBSD"); + } + + private static HashSet ResolveCallees(MethodBase method) + { + var il = method.GetMethodBody()?.GetILAsByteArray(); + Assert.NotNull(il); + + var module = method.Module; + var generic = method.DeclaringType?.GetGenericArguments(); + var callees = new HashSet(StringComparer.Ordinal); + + for (var i = 0; i + 4 < il.Length; i++) + { + if (il[i] is not (0x28 or 0x6F)) + { + continue; + } + + var token = BitConverter.ToInt32(il, i + 1); + try + { + var callee = module.ResolveMethod(token, generic, null); + if (callee?.Name is { } name) + { + callees.Add(name); + } + } + catch (ArgumentException) + { + } + } + + return callees; + } + + private static bool InvokeIsHlePreferredNid(string nid) + { + var method = typeof(DirectExecutionBackend).GetMethod( + "IsHlePreferredNid", + BindingFlags.Static | BindingFlags.NonPublic); + Assert.NotNull(method); + return (bool)method.Invoke(null, [nid])!; + } + + private static bool InvokeTryCreateNativeImportIntrinsic(string nid, out nint address) + { + var backend = (DirectExecutionBackend)RuntimeHelpers.GetUninitializedObject( + typeof(DirectExecutionBackend)); + var trampolineList = typeof(DirectExecutionBackend).GetField( + "_importHandlerTrampolines", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(trampolineList); + trampolineList.SetValue(backend, new List()); + + var method = typeof(DirectExecutionBackend).GetMethod( + "TryCreateNativeImportIntrinsic", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(method); + + object?[] args = [nid, null]; + var claimed = (bool)method.Invoke(backend, args)!; + address = (nint)args[1]!; + return claimed; + } +}