Compare commits

...

4 Commits

Author SHA1 Message Date
Deeptanshu Lal 4b7df8623a [AGC] Fix v_fmac_f32 family decoding in Gen5 VOP2 table (#103)
VOP2 opcode 0x2B was mapped to v_ldexp_f32, which is its gfx6/gfx7
assignment. On gfx10-class hardware 0x2B is v_fmac_f32, so any shader
using it silently computed ldexp(a, b) instead of dst += a * b.
v_ldexp_f32 on gfx10 only exists as VOP3 0x362, which the VOP3 table
already maps correctly.

Also add the remaining members of the fmac family:
- v_fmamk_f32 (0x2C) and v_fmaak_f32 (0x2D), including their mandatory
  literal dword in instruction sizing and operand construction, reusing
  the existing v_madmk/v_madak handling.
- The VOP3-encoded form of v_fmac_f32 (0x12B), emitted when source
  modifiers are present.

SPIR-V emission reuses the existing v_mac_f32 body (fma with the
destination register as addend) and the v_mad/v_fma case group.

Opcode assignments verified against LLVM's AMDGPU backend
(VOP2Instructions.td): V_FMAC_F32 gfx10 = 0x02b, V_FMAMK_F32 = 0x02c,
V_FMAAK_F32 = 0x02d; V_LDEXP_F32 is 0x02b only on gfx6/gfx7 and is
VOP3-only 0x362 on gfx10. Decode verified by feeding hand-assembled
gfx1013 words through Gen5ShaderTranslator: 0x560A0501 previously
decoded as VLdexpF32 and a v_fmamk_f32 program failed with
unknown-vop2 op=0x2C; both now decode correctly, and VOP3 0x362 still
decodes as VLdexpF32.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:20:12 +03:00
Digote 3c2134474d [HLE] Avoid duplicate export registration (#100)
Co-authored-by: diego <diego@DIGOTE-PC>
2026-07-13 17:18:21 +03:00
Spooks 298ef01809 [VideoOut] Prefer NVIDIA/discrete GPUs over integrated (#97)
SelectPhysicalDevice took the first device exposing a graphics+present
queue. On hybrid-graphics laptops that is the integrated GPU, so the
discrete card went unused - and AMD's integrated driver access-violates
inside vkCreateGraphicsPipelines while compiling some translated guest
shaders, killing the process. The CLR surfaced that native AV as
"Invalid Program: attempted to call a UnmanagedCallersOnly method from
managed code", which made it look like a CPU/threading fault.

Score the candidates instead: NVIDIA parts win, other discrete GPUs come
next, and an integrated GPU is only chosen when nothing else can present.
SHARPEMU_VK_DEVICE=<substring> pins a specific adapter, and the selected
device is logged.
2026-07-13 15:05:59 +03:00
Berk 2060dacaf1 [emulator] Fix mitigated child process handling & log improvements (#96)
* [emulator] Fix mitigated child process handling

* reuse

* [log] log to file for CLI and fix some issues
2026-07-13 14:34:26 +03:00
8 changed files with 455 additions and 57 deletions
+5
View File
@@ -1,3 +1,8 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Contributing # Contributing
Contributions are always welcome! Contributions are always welcome!
+372 -13
View File
@@ -8,12 +8,15 @@ using SharpEmu.HLE;
using SharpEmu.Logging; using SharpEmu.Logging;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.Json;
namespace SharpEmu.CLI; namespace SharpEmu.CLI;
internal static partial class Program internal static partial class Program
{ {
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI"); private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI");
private static readonly object ConsoleMirrorSync = new();
private static StreamWriter? _consoleMirrorFile;
private const int DefaultImportTraceLimit = 32; private const int DefaultImportTraceLimit = 32;
private const string MitigatedChildFlag = "--sharpemu-mitigated-child"; private const string MitigatedChildFlag = "--sharpemu-mitigated-child";
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000; private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
@@ -21,11 +24,15 @@ internal static partial class Program
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007; private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
private const int JobObjectExtendedLimitInformation = 9; private const int JobObjectExtendedLimitInformation = 9;
private const int STARTF_USESTDHANDLES = 0x00000100;
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
private const string MitigatedChildEnvironment = "SHARPEMU_MITIGATED_CHILD";
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40; private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28; private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32; private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40; private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const int ATTACH_PARENT_PROCESS = -1; private const int ATTACH_PARENT_PROCESS = -1;
private const int STD_INPUT_HANDLE = -10;
private const int STD_OUTPUT_HANDLE = -11; private const int STD_OUTPUT_HANDLE = -11;
private const int STD_ERROR_HANDLE = -12; private const int STD_ERROR_HANDLE = -12;
private const uint GENERIC_READ = 0x80000000; private const uint GENERIC_READ = 0x80000000;
@@ -43,6 +50,7 @@ internal static partial class Program
} }
finally finally
{ {
DropConsoleFileMirror();
SharpEmuLog.Shutdown(); SharpEmuLog.Shutdown();
} }
} }
@@ -61,6 +69,10 @@ internal static partial class Program
// itself to a console before the first write. // itself to a console before the first write.
EnsureCliConsole(); EnsureCliConsole();
UseUtf8ConsoleOutput(); UseUtf8ConsoleOutput();
if (isMitigatedChild && TryGetLogFileArgument(args, out var earlyLogFilePath))
{
TryEnableConsoleFileMirror(earlyLogFilePath);
}
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args"); Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
@@ -69,12 +81,17 @@ internal static partial class Program
return childExitCode; return childExitCode;
} }
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel)) if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath))
{ {
PrintUsage(); PrintUsage();
return 1; return 1;
} }
if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath))
{
TryEnableConsoleFileMirror(logFilePath);
}
SharpEmuLog.MinimumLevel = logLevel; SharpEmuLog.MinimumLevel = logLevel;
Log.Info(BuildInfo.Banner); Log.Info(BuildInfo.Banner);
@@ -234,6 +251,10 @@ internal static partial class Program
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild) private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
{ {
isMitigatedChild = false; isMitigatedChild = false;
var trustedMitigatedChild = string.Equals(
Environment.GetEnvironmentVariable(MitigatedChildEnvironment),
"1",
StringComparison.Ordinal);
if (args.Length == 0) if (args.Length == 0)
{ {
return args; return args;
@@ -244,7 +265,7 @@ internal static partial class Program
{ {
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal)) if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
{ {
isMitigatedChild = true; isMitigatedChild = trustedMitigatedChild;
continue; continue;
} }
@@ -283,9 +304,11 @@ internal static partial class Program
var commandLine = BuildCommandLine(processPath, childArgs); var commandLine = BuildCommandLine(processPath, childArgs);
var startupInfoEx = new STARTUPINFOEX(); var startupInfoEx = new STARTUPINFOEX();
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>(); startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
nint attributeList = 0; nint attributeList = 0;
nint mitigationPolicies = 0; nint mitigationPolicies = 0;
var previousChildEnvironment = Environment.GetEnvironmentVariable(MitigatedChildEnvironment);
try try
{ {
nuint attributeListSize = 0; nuint attributeListSize = 0;
@@ -293,7 +316,9 @@ internal static partial class Program
attributeList = Marshal.AllocHGlobal((nint)attributeListSize); attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize)) if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
{ {
return false; childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to initialize mitigation attributes: {Marshal.GetLastWin32Error()}");
return true;
} }
startupInfoEx.lpAttributeList = attributeList; startupInfoEx.lpAttributeList = attributeList;
@@ -301,8 +326,7 @@ internal static partial class Program
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF; var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
var policy2 = var policy2 =
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF | PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF | PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2); mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1)); Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
@@ -317,24 +341,31 @@ internal static partial class Program
0, 0,
0)) 0))
{ {
return false; childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to apply mitigation attributes: {Marshal.GetLastWin32Error()}");
return true;
} }
var cmdLineBuilder = new StringBuilder(commandLine); var cmdLineBuilder = new StringBuilder(commandLine);
nint jobHandle = 0; nint jobHandle = 0;
if (!CreateProcessW( Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
var created = CreateProcessW(
processPath, processPath,
cmdLineBuilder, cmdLineBuilder,
0, 0,
0, 0,
false, true,
EXTENDED_STARTUPINFO_PRESENT, EXTENDED_STARTUPINFO_PRESENT,
0, 0,
Environment.CurrentDirectory, Environment.CurrentDirectory,
ref startupInfoEx, ref startupInfoEx,
out var processInfo)) out var processInfo);
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (!created)
{ {
return false; childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to launch mitigated child process: {Marshal.GetLastWin32Error()}");
return true;
} }
try try
@@ -388,6 +419,8 @@ internal static partial class Program
} }
finally finally
{ {
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (attributeList != 0) if (attributeList != 0)
{ {
DeleteProcThreadAttributeList(attributeList); DeleteProcThreadAttributeList(attributeList);
@@ -401,6 +434,238 @@ internal static partial class Program
} }
} }
private static bool TryGetLogFileArgument(IReadOnlyList<string> args, out string path)
{
for (var i = 0; i < args.Count; i++)
{
var argument = args[i];
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
{
if (i + 1 < args.Count &&
!string.IsNullOrWhiteSpace(args[i + 1]) &&
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
ShouldConsumeLogFilePath(args, i + 1))
{
path = args[i + 1];
return true;
}
path = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
return true;
}
const string logFilePrefix = "--log-file=";
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrWhiteSpace(argument[logFilePrefix.Length..]))
{
path = argument[logFilePrefix.Length..];
return true;
}
}
path = string.Empty;
return false;
}
private static string BuildDefaultLogFilePath(string? ebootPath)
{
var baseDirectory = AppContext.BaseDirectory;
var logsDirectory = Path.Combine(baseDirectory, "user", "logs");
var name = TryReadTitleId(ebootPath) ?? "UNKNOWN";
foreach (var invalid in Path.GetInvalidFileNameChars())
{
name = name.Replace(invalid, '_');
}
return Path.Combine(logsDirectory, $"{name}-{DateTime.Now:yyyyMMdd-HHmmss}.log");
}
private static string? TryReadTitleId(string? ebootPath)
{
if (string.IsNullOrWhiteSpace(ebootPath))
{
return null;
}
try
{
var directory = Path.GetDirectoryName(Path.GetFullPath(ebootPath));
if (string.IsNullOrEmpty(directory))
{
return null;
}
foreach (var paramPath in new[]
{
Path.Combine(directory, "sce_sys", "param.json"),
Path.Combine(directory, "param.json"),
})
{
if (!File.Exists(paramPath))
{
continue;
}
using var stream = File.OpenRead(paramPath);
using var document = JsonDocument.Parse(stream);
if (document.RootElement.TryGetProperty("titleId", out var titleIdElement) &&
titleIdElement.ValueKind == JsonValueKind.String)
{
var titleId = titleIdElement.GetString();
if (!string.IsNullOrWhiteSpace(titleId))
{
return titleId.Trim();
}
}
}
}
catch
{
// Logging should never block launch; unknown title ids use a stable fallback.
}
return null;
}
private static string? TryFindEbootPathToken(IReadOnlyList<string> args)
{
for (var i = args.Count - 1; i >= 0; i--)
{
var argument = args[i];
if (string.IsNullOrWhiteSpace(argument) ||
argument.StartsWith("--", StringComparison.Ordinal))
{
continue;
}
return argument;
}
return null;
}
private static bool ShouldConsumeLogFilePath(IReadOnlyList<string> args, int candidateIndex)
{
var candidate = args[candidateIndex];
if (LooksLikeLogFilePath(candidate))
{
return true;
}
for (var i = candidateIndex + 1; i < args.Count; i++)
{
var argument = args[i];
if (!string.IsNullOrWhiteSpace(argument) &&
!argument.StartsWith("--", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static bool LooksLikeLogFilePath(string path)
{
var extension = Path.GetExtension(path);
return string.Equals(extension, ".log", StringComparison.OrdinalIgnoreCase) ||
string.Equals(extension, ".txt", StringComparison.OrdinalIgnoreCase);
}
private static void TryEnableConsoleFileMirror(string path)
{
lock (ConsoleMirrorSync)
{
if (_consoleMirrorFile is not null)
{
return;
}
try
{
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var stream = new FileStream(
path,
FileMode.Create,
FileAccess.Write,
FileShare.ReadWrite,
bufferSize: 4096,
FileOptions.SequentialScan);
_consoleMirrorFile = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
{
AutoFlush = true,
};
Console.SetOut(new TeeTextWriter(Console.Out, _consoleMirrorFile));
Console.SetError(new TeeTextWriter(Console.Error, _consoleMirrorFile));
Console.Error.WriteLine($"[DEBUG] Log file: {Path.GetFullPath(path)}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[WARN] Could not open log file '{path}': {ex.Message}");
}
}
}
private static void DropConsoleFileMirror()
{
lock (ConsoleMirrorSync)
{
try
{
_consoleMirrorFile?.Flush();
_consoleMirrorFile?.Dispose();
}
catch
{
}
_consoleMirrorFile = null;
}
}
private static void ConfigureInheritedStdHandles(ref STARTUPINFO startupInfo)
{
if (!OperatingSystem.IsWindows())
{
return;
}
var input = GetStdHandle(STD_INPUT_HANDLE);
var output = GetStdHandle(STD_OUTPUT_HANDLE);
var error = GetStdHandle(STD_ERROR_HANDLE);
if (!IsHandleValid(output) && !IsHandleValid(error))
{
return;
}
if (IsHandleValid(input))
{
_ = SetHandleInformation(input, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
startupInfo.hStdInput = input;
}
if (IsHandleValid(output))
{
_ = SetHandleInformation(output, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
startupInfo.hStdOutput = output;
}
if (IsHandleValid(error))
{
_ = SetHandleInformation(error, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
startupInfo.hStdError = error;
}
startupInfo.dwFlags |= STARTF_USESTDHANDLES;
}
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args) private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
@@ -503,27 +768,30 @@ internal static partial class Program
private static void PrintUsage() private static void PrintUsage()
{ {
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] <path-to-eboot.bin>"); Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] [--log-file[=<path>]] <path-to-eboot.bin>");
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug ""E:\Games\...\eboot.bin"""); Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
} }
private static bool TryParseArguments( private static bool TryParseArguments(
string[] args, string[] args,
out string ebootPath, out string ebootPath,
out SharpEmuRuntimeOptions runtimeOptions, out SharpEmuRuntimeOptions runtimeOptions,
out LogLevel logLevel) out LogLevel logLevel,
out string? logFilePath)
{ {
if (args.Length == 0) if (args.Length == 0)
{ {
ebootPath = string.Empty; ebootPath = string.Empty;
runtimeOptions = default; runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel; logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false; return false;
} }
var strictDynlibResolution = false; var strictDynlibResolution = false;
var importTraceLimit = 0; var importTraceLimit = 0;
var cpuEngine = CpuExecutionEngine.NativeOnly; var cpuEngine = CpuExecutionEngine.NativeOnly;
logFilePath = null;
logLevel = SharpEmuLog.MinimumLevel; logLevel = SharpEmuLog.MinimumLevel;
var pathTokens = new List<string>(args.Length); var pathTokens = new List<string>(args.Length);
for (var i = 0; i < args.Length; i++) for (var i = 0; i < args.Length; i++)
@@ -553,6 +821,7 @@ internal static partial class Program
{ {
ebootPath = string.Empty; ebootPath = string.Empty;
runtimeOptions = default; runtimeOptions = default;
logFilePath = null;
return false; return false;
} }
@@ -566,6 +835,7 @@ internal static partial class Program
{ {
ebootPath = string.Empty; ebootPath = string.Empty;
runtimeOptions = default; runtimeOptions = default;
logFilePath = null;
return false; return false;
} }
@@ -573,6 +843,23 @@ internal static partial class Program
continue; continue;
} }
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
{
if (i + 1 < args.Length &&
!string.IsNullOrWhiteSpace(args[i + 1]) &&
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
ShouldConsumeLogFilePath(args, i + 1))
{
logFilePath = args[++i];
}
else
{
logFilePath = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
}
continue;
}
const string logLevelPrefix = "--log-level="; const string logLevelPrefix = "--log-level=";
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase)) if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
{ {
@@ -596,6 +883,7 @@ internal static partial class Program
ebootPath = string.Empty; ebootPath = string.Empty;
runtimeOptions = default; runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel; logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false; return false;
} }
@@ -618,11 +906,27 @@ internal static partial class Program
continue; continue;
} }
const string logFilePrefix = "--log-file=";
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase))
{
logFilePath = argument[logFilePrefix.Length..];
if (string.IsNullOrWhiteSpace(logFilePath))
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
return false;
}
continue;
}
if (argument.StartsWith("--", StringComparison.Ordinal)) if (argument.StartsWith("--", StringComparison.Ordinal))
{ {
ebootPath = string.Empty; ebootPath = string.Empty;
runtimeOptions = default; runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel; logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false; return false;
} }
@@ -634,6 +938,7 @@ internal static partial class Program
ebootPath = string.Empty; ebootPath = string.Empty;
runtimeOptions = default; runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel; logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false; return false;
} }
@@ -735,6 +1040,56 @@ internal static partial class Program
public nuint PeakJobMemoryUsed; public nuint PeakJobMemoryUsed;
} }
private sealed class TeeTextWriter : TextWriter
{
private readonly TextWriter _primary;
private readonly TextWriter _mirror;
public TeeTextWriter(TextWriter primary, TextWriter mirror)
{
_primary = primary;
_mirror = mirror;
}
public override Encoding Encoding => _primary.Encoding;
public override void Write(char value)
{
lock (ConsoleMirrorSync)
{
_primary.Write(value);
_mirror.Write(value);
}
}
public override void Write(string? value)
{
lock (ConsoleMirrorSync)
{
_primary.Write(value);
_mirror.Write(value);
}
}
public override void WriteLine(string? value)
{
lock (ConsoleMirrorSync)
{
_primary.WriteLine(value);
_mirror.WriteLine(value);
}
}
public override void Flush()
{
lock (ConsoleMirrorSync)
{
_primary.Flush();
_mirror.Flush();
}
}
}
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
private static extern bool InitializeProcThreadAttributeList( private static extern bool InitializeProcThreadAttributeList(
@@ -819,6 +1174,10 @@ internal static partial class Program
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetStdHandle(int stdHandle, nint handle); private static extern bool SetStdHandle(int stdHandle, nint handle);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetHandleInformation(nint handle, uint mask, uint flags);
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)] [DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern nint CreateFileW( private static extern nint CreateFileW(
string fileName, string fileName,
@@ -83,7 +83,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit), ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
}; };
var moduleManager = new ModuleManager(); var moduleManager = new ModuleManager();
moduleManager.RegisterFromAssembly(typeof(VideoOutExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance); moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.Freeze(); moduleManager.Freeze();
+2 -22
View File
@@ -168,8 +168,7 @@ internal sealed class EmulatorProcess : IDisposable
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF; var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
var policy2 = var policy2 =
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF | PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF | PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2); mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1)); Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
@@ -200,29 +199,10 @@ internal sealed class EmulatorProcess : IDisposable
ref startupInfoEx, ref startupInfoEx,
out var processInfo); out var processInfo);
if (!created)
{
// Some mitigation policy bits (e.g. XFG) are not supported on
// older Windows builds. Mirror the CLI's behavior and fall back
// to launching without the mitigation attribute list.
startupInfoEx.lpAttributeList = 0;
created = CreateProcessW(
exePath,
new StringBuilder(BuildCommandLine(exePath, arguments)),
0,
0,
true,
CREATE_NO_WINDOW,
0,
currentDirectory,
ref startupInfoEx,
out processInfo);
}
if (!created) if (!created)
{ {
var error = Marshal.GetLastWin32Error(); var error = Marshal.GetLastWin32Error();
throw new Win32Exception(error, $"Failed to start '{exePath}' (Win32 error {error}: {new Win32Exception(error).Message})."); throw new Win32Exception(error, $"Failed to start '{exePath}' with CET/CFG mitigation disabled (Win32 error {error}: {new Win32Exception(error).Message}).");
} }
CloseHandle(processInfo.hThread); CloseHandle(processInfo.hThread);
+7 -13
View File
@@ -1061,13 +1061,11 @@ public partial class MainWindow : Window
arguments.Add($"--trace-imports={_settings.ImportTraceLimit}"); arguments.Add($"--trace-imports={_settings.ImportTraceLimit}");
} }
arguments.Add(ebootPath);
_consoleLines.Clear(); _consoleLines.Clear();
ConsoleToggle.IsChecked = true; ConsoleToggle.IsChecked = true;
// Mirror everything the console pane shows into a log file for the // Let the CLI mirror stdout/stderr itself; it sees loader/native
// duration of the run, regardless of the emulator's log level. // diagnostics before the GUI pipe reader can filter or batch them.
DropFileLog(); DropFileLog();
if (_settings.LogToFile) if (_settings.LogToFile)
{ {
@@ -1105,18 +1103,14 @@ public partial class MainWindow : Window
if (!string.IsNullOrEmpty(filePath)) if (!string.IsNullOrEmpty(filePath))
{ {
try arguments.Add("--log-file");
{ arguments.Add(filePath);
_fileLog = new StreamWriter(filePath, append: false); AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
}
catch (Exception ex)
{
AppendConsoleLine($"Could not open the log file: {ex.Message}", WarningLineBrush);
}
} }
} }
arguments.Add(ebootPath);
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush); AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
var emulator = new EmulatorProcess(); var emulator = new EmulatorProcess();
@@ -718,7 +718,7 @@ internal static class Gen5ShaderTranslator
} }
var src0 = word & 0x1FF; var src0 = word & 0x1FF;
sizeDwords = opcode is 0x20 or 0x21 || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u; sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
error = string.Empty; error = string.Empty;
name = opcode switch name = opcode switch
{ {
@@ -757,7 +757,9 @@ internal static class Gen5ShaderTranslator
0x28 => "VAddcU32", 0x28 => "VAddcU32",
0x29 => "VSubbU32", 0x29 => "VSubbU32",
0x2A => "VSubbrevU32", 0x2A => "VSubbrevU32",
0x2B => "VLdexpF32", 0x2B => "VFmacF32",
0x2C => "VFmamkF32",
0x2D => "VFmaakF32",
0x2F => "VCvtPkrtzF16F32", 0x2F => "VCvtPkrtzF16F32",
0x30 => "VCvtPkU16U32", 0x30 => "VCvtPkU16U32",
0x31 => "VCvtPkI16I32", 0x31 => "VCvtPkI16I32",
@@ -870,6 +872,7 @@ internal static class Gen5ShaderTranslator
0x10F => "VMinF32", 0x10F => "VMinF32",
0x110 => "VMaxF32", 0x110 => "VMaxF32",
0x11F => "VMacF32", 0x11F => "VMacF32",
0x12B => "VFmacF32",
0x12F => "VCvtPkrtzF16F32", 0x12F => "VCvtPkrtzF16F32",
0x141 => "VMadF32", 0x141 => "VMadF32",
0x143 => "VMadU32U24", 0x143 => "VMadU32U24",
@@ -1379,7 +1382,7 @@ internal static class Gen5ShaderTranslator
Gen5Operand.Source(word & 0x1FF, literal), Gen5Operand.Source(word & 0x1FF, literal),
Gen5Operand.Vector((word >> 9) & 0xFF), Gen5Operand.Vector((word >> 9) & 0xFF),
]; ];
if (opcode == "VMadMkF32" && literal.HasValue) if (opcode is "VMadMkF32" or "VFmamkF32" && literal.HasValue)
{ {
sources = sources =
[ [
@@ -1388,7 +1391,7 @@ internal static class Gen5ShaderTranslator
sources[1], sources[1],
]; ];
} }
else if (opcode == "VMadAkF32" && literal.HasValue) else if (opcode is "VMadAkF32" or "VFmaakF32" && literal.HasValue)
{ {
sources = sources =
[ [
@@ -255,6 +255,8 @@ internal static partial class Gen5SpirvTranslator
case "VFmaF32": case "VFmaF32":
case "VMadMkF32": case "VMadMkF32":
case "VMadAkF32": case "VMadAkF32":
case "VFmamkF32":
case "VFmaakF32":
result = EmitFloatResult( result = EmitFloatResult(
instruction, instruction,
Ext( Ext(
@@ -265,6 +267,7 @@ internal static partial class Gen5SpirvTranslator
GetFloatSource(instruction, 2))); GetFloatSource(instruction, 2)));
break; break;
case "VMacF32": case "VMacF32":
case "VFmacF32":
{ {
var addend = Bitcast(_floatType, LoadV(destination)); var addend = Bitcast(_floatType, LoadV(destination));
result = EmitFloatResult( result = EmitFloatResult(
@@ -178,6 +178,7 @@ internal static unsafe class VulkanVideoPresenter
private static uint _windowHeight; private static uint _windowHeight;
private static bool _closed; private static bool _closed;
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils"; private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
private const uint NvidiaVendorId = 0x10DE;
private static bool _splashHidden; private static bool _splashHidden;
private static long _enqueuedGuestWorkSequence; private static long _enqueuedGuestWorkSequence;
private static long _completedGuestWorkSequence; private static long _completedGuestWorkSequence;
@@ -1502,6 +1503,13 @@ internal static unsafe class VulkanVideoPresenter
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices"); Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices");
} }
// Hybrid laptops enumerate the iGPU first, and AMD's integrated driver
// segfaults compiling some translated shaders, so rank rather than take
// the first hit. SHARPEMU_VK_DEVICE=<substring> pins an adapter by name.
var deviceOverride = Environment.GetEnvironmentVariable("SHARPEMU_VK_DEVICE");
var bestScore = int.MinValue;
var found = false;
foreach (var device in devices) foreach (var device in devices)
{ {
uint queueCount = 0; uint queueCount = 0;
@@ -1521,13 +1529,60 @@ internal static unsafe class VulkanVideoPresenter
continue; continue;
} }
_physicalDevice = device; _vk.GetPhysicalDeviceProperties(device, out var properties);
_queueFamilyIndex = index; var name = SilkMarshal.PtrToString((nint)properties.DeviceName) ?? string.Empty;
return; var score = ScorePhysicalDevice(properties, name, deviceOverride);
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan candidate: {name} ({properties.DeviceType}) score={score}");
if (score > bestScore)
{
bestScore = score;
_physicalDevice = device;
_queueFamilyIndex = index;
found = true;
}
break;
} }
} }
throw new InvalidOperationException("No Vulkan graphics/present queue was found."); if (!found)
{
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
}
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan device: {SilkMarshal.PtrToString((nint)selected.DeviceName)} ({selected.DeviceType})");
}
private static int ScorePhysicalDevice(
PhysicalDeviceProperties properties,
string name,
string? deviceOverride)
{
if (!string.IsNullOrWhiteSpace(deviceOverride))
{
return name.Contains(deviceOverride, StringComparison.OrdinalIgnoreCase) ? 1000 : -1000;
}
var score = properties.DeviceType switch
{
PhysicalDeviceType.DiscreteGpu => 300,
PhysicalDeviceType.VirtualGpu => 100,
PhysicalDeviceType.Cpu => 50,
// Last resort: only picked when nothing else can present.
PhysicalDeviceType.IntegratedGpu => -100,
_ => 10,
};
if (properties.VendorID == NvidiaVendorId)
{
score += 500;
}
return score;
} }
private void CreateDevice() private void CreateDevice()