mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 06:29:46 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b7df8623a | |||
| 3c2134474d | |||
| 298ef01809 | |||
| 2060dacaf1 |
@@ -1,3 +1,8 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are always welcome!
|
||||
|
||||
+372
-13
@@ -8,12 +8,15 @@ using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.CLI;
|
||||
|
||||
internal static partial class Program
|
||||
{
|
||||
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 string MitigatedChildFlag = "--sharpemu-mitigated-child";
|
||||
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 uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||
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_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_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
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_ERROR_HANDLE = -12;
|
||||
private const uint GENERIC_READ = 0x80000000;
|
||||
@@ -43,6 +50,7 @@ internal static partial class Program
|
||||
}
|
||||
finally
|
||||
{
|
||||
DropConsoleFileMirror();
|
||||
SharpEmuLog.Shutdown();
|
||||
}
|
||||
}
|
||||
@@ -61,6 +69,10 @@ internal static partial class Program
|
||||
// itself to a console before the first write.
|
||||
EnsureCliConsole();
|
||||
UseUtf8ConsoleOutput();
|
||||
if (isMitigatedChild && TryGetLogFileArgument(args, out var earlyLogFilePath))
|
||||
{
|
||||
TryEnableConsoleFileMirror(earlyLogFilePath);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
|
||||
@@ -69,12 +81,17 @@ internal static partial class Program
|
||||
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();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath))
|
||||
{
|
||||
TryEnableConsoleFileMirror(logFilePath);
|
||||
}
|
||||
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
|
||||
Log.Info(BuildInfo.Banner);
|
||||
@@ -234,6 +251,10 @@ internal static partial class Program
|
||||
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||
{
|
||||
isMitigatedChild = false;
|
||||
var trustedMitigatedChild = string.Equals(
|
||||
Environment.GetEnvironmentVariable(MitigatedChildEnvironment),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
if (args.Length == 0)
|
||||
{
|
||||
return args;
|
||||
@@ -244,7 +265,7 @@ internal static partial class Program
|
||||
{
|
||||
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
|
||||
{
|
||||
isMitigatedChild = true;
|
||||
isMitigatedChild = trustedMitigatedChild;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -283,9 +304,11 @@ internal static partial class Program
|
||||
var commandLine = BuildCommandLine(processPath, childArgs);
|
||||
var startupInfoEx = new STARTUPINFOEX();
|
||||
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
|
||||
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
|
||||
|
||||
nint attributeList = 0;
|
||||
nint mitigationPolicies = 0;
|
||||
var previousChildEnvironment = Environment.GetEnvironmentVariable(MitigatedChildEnvironment);
|
||||
try
|
||||
{
|
||||
nuint attributeListSize = 0;
|
||||
@@ -293,7 +316,9 @@ internal static partial class Program
|
||||
attributeList = Marshal.AllocHGlobal((nint)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;
|
||||
@@ -301,8 +326,7 @@ internal static partial class Program
|
||||
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
var policy2 =
|
||||
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_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
|
||||
|
||||
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||
@@ -317,24 +341,31 @@ internal static partial class Program
|
||||
0,
|
||||
0))
|
||||
{
|
||||
return false;
|
||||
childExitCode = 5;
|
||||
Console.Error.WriteLine($"[ERROR] Failed to apply mitigation attributes: {Marshal.GetLastWin32Error()}");
|
||||
return true;
|
||||
}
|
||||
|
||||
var cmdLineBuilder = new StringBuilder(commandLine);
|
||||
nint jobHandle = 0;
|
||||
if (!CreateProcessW(
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||
var created = CreateProcessW(
|
||||
processPath,
|
||||
cmdLineBuilder,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
EXTENDED_STARTUPINFO_PRESENT,
|
||||
0,
|
||||
Environment.CurrentDirectory,
|
||||
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
|
||||
@@ -388,6 +419,8 @@ internal static partial class Program
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
|
||||
|
||||
if (attributeList != 0)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
@@ -503,27 +768,30 @@ internal static partial class Program
|
||||
|
||||
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(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug ""E:\Games\...\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 --log-file ""E:\Games\...\eboot.bin""");
|
||||
}
|
||||
|
||||
private static bool TryParseArguments(
|
||||
string[] args,
|
||||
out string ebootPath,
|
||||
out SharpEmuRuntimeOptions runtimeOptions,
|
||||
out LogLevel logLevel)
|
||||
out LogLevel logLevel,
|
||||
out string? logFilePath)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var strictDynlibResolution = false;
|
||||
var importTraceLimit = 0;
|
||||
var cpuEngine = CpuExecutionEngine.NativeOnly;
|
||||
logFilePath = null;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
var pathTokens = new List<string>(args.Length);
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
@@ -553,6 +821,7 @@ internal static partial class Program
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -566,6 +835,7 @@ internal static partial class Program
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -573,6 +843,23 @@ internal static partial class Program
|
||||
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=";
|
||||
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -596,6 +883,7 @@ internal static partial class Program
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -618,11 +906,27 @@ internal static partial class Program
|
||||
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))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -634,6 +938,7 @@ internal static partial class Program
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -735,6 +1040,56 @@ internal static partial class Program
|
||||
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)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool InitializeProcThreadAttributeList(
|
||||
@@ -819,6 +1174,10 @@ internal static partial class Program
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
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)]
|
||||
private static extern nint CreateFileW(
|
||||
string fileName,
|
||||
|
||||
@@ -83,7 +83,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
|
||||
};
|
||||
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.Freeze();
|
||||
|
||||
|
||||
@@ -168,8 +168,7 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
var policy2 =
|
||||
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_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
|
||||
|
||||
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||
@@ -200,29 +199,10 @@ internal sealed class EmulatorProcess : IDisposable
|
||||
ref startupInfoEx,
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -1061,13 +1061,11 @@ public partial class MainWindow : Window
|
||||
arguments.Add($"--trace-imports={_settings.ImportTraceLimit}");
|
||||
}
|
||||
|
||||
arguments.Add(ebootPath);
|
||||
|
||||
_consoleLines.Clear();
|
||||
ConsoleToggle.IsChecked = true;
|
||||
|
||||
// Mirror everything the console pane shows into a log file for the
|
||||
// duration of the run, regardless of the emulator's log level.
|
||||
// Let the CLI mirror stdout/stderr itself; it sees loader/native
|
||||
// diagnostics before the GUI pipe reader can filter or batch them.
|
||||
DropFileLog();
|
||||
if (_settings.LogToFile)
|
||||
{
|
||||
@@ -1105,18 +1103,14 @@ public partial class MainWindow : Window
|
||||
|
||||
if (!string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
_fileLog = new StreamWriter(filePath, append: false);
|
||||
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendConsoleLine($"Could not open the log file: {ex.Message}", WarningLineBrush);
|
||||
}
|
||||
arguments.Add("--log-file");
|
||||
arguments.Add(filePath);
|
||||
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
|
||||
}
|
||||
}
|
||||
|
||||
arguments.Add(ebootPath);
|
||||
|
||||
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
||||
|
||||
var emulator = new EmulatorProcess();
|
||||
|
||||
@@ -718,7 +718,7 @@ internal static class Gen5ShaderTranslator
|
||||
}
|
||||
|
||||
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;
|
||||
name = opcode switch
|
||||
{
|
||||
@@ -757,7 +757,9 @@ internal static class Gen5ShaderTranslator
|
||||
0x28 => "VAddcU32",
|
||||
0x29 => "VSubbU32",
|
||||
0x2A => "VSubbrevU32",
|
||||
0x2B => "VLdexpF32",
|
||||
0x2B => "VFmacF32",
|
||||
0x2C => "VFmamkF32",
|
||||
0x2D => "VFmaakF32",
|
||||
0x2F => "VCvtPkrtzF16F32",
|
||||
0x30 => "VCvtPkU16U32",
|
||||
0x31 => "VCvtPkI16I32",
|
||||
@@ -870,6 +872,7 @@ internal static class Gen5ShaderTranslator
|
||||
0x10F => "VMinF32",
|
||||
0x110 => "VMaxF32",
|
||||
0x11F => "VMacF32",
|
||||
0x12B => "VFmacF32",
|
||||
0x12F => "VCvtPkrtzF16F32",
|
||||
0x141 => "VMadF32",
|
||||
0x143 => "VMadU32U24",
|
||||
@@ -1379,7 +1382,7 @@ internal static class Gen5ShaderTranslator
|
||||
Gen5Operand.Source(word & 0x1FF, literal),
|
||||
Gen5Operand.Vector((word >> 9) & 0xFF),
|
||||
];
|
||||
if (opcode == "VMadMkF32" && literal.HasValue)
|
||||
if (opcode is "VMadMkF32" or "VFmamkF32" && literal.HasValue)
|
||||
{
|
||||
sources =
|
||||
[
|
||||
@@ -1388,7 +1391,7 @@ internal static class Gen5ShaderTranslator
|
||||
sources[1],
|
||||
];
|
||||
}
|
||||
else if (opcode == "VMadAkF32" && literal.HasValue)
|
||||
else if (opcode is "VMadAkF32" or "VFmaakF32" && literal.HasValue)
|
||||
{
|
||||
sources =
|
||||
[
|
||||
|
||||
@@ -255,6 +255,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
case "VFmaF32":
|
||||
case "VMadMkF32":
|
||||
case "VMadAkF32":
|
||||
case "VFmamkF32":
|
||||
case "VFmaakF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(
|
||||
@@ -265,6 +267,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
GetFloatSource(instruction, 2)));
|
||||
break;
|
||||
case "VMacF32":
|
||||
case "VFmacF32":
|
||||
{
|
||||
var addend = Bitcast(_floatType, LoadV(destination));
|
||||
result = EmitFloatResult(
|
||||
|
||||
@@ -178,6 +178,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private static uint _windowHeight;
|
||||
private static bool _closed;
|
||||
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
|
||||
private const uint NvidiaVendorId = 0x10DE;
|
||||
private static bool _splashHidden;
|
||||
private static long _enqueuedGuestWorkSequence;
|
||||
private static long _completedGuestWorkSequence;
|
||||
@@ -1502,6 +1503,13 @@ internal static unsafe class VulkanVideoPresenter
|
||||
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)
|
||||
{
|
||||
uint queueCount = 0;
|
||||
@@ -1521,13 +1529,60 @@ internal static unsafe class VulkanVideoPresenter
|
||||
continue;
|
||||
}
|
||||
|
||||
_physicalDevice = device;
|
||||
_queueFamilyIndex = index;
|
||||
return;
|
||||
_vk.GetPhysicalDeviceProperties(device, out var properties);
|
||||
var name = SilkMarshal.PtrToString((nint)properties.DeviceName) ?? string.Empty;
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user