mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf6964710a | |||
| 4f028d0483 | |||
| 4db98bd8fe | |||
| 4600a2ed1f | |||
| c5c5ee1f36 | |||
| b48b1a5e09 | |||
| 511a01e03a | |||
| aaebfe017b |
@@ -3,8 +3,8 @@
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import struct
|
||||
import hashlib
|
||||
import struct
|
||||
from base64 import b64encode as base64enc
|
||||
from binascii import unhexlify as uhx
|
||||
from pathlib import Path
|
||||
@@ -21,7 +21,7 @@ def name2nid(name):
|
||||
def generate():
|
||||
names_path = Path(NAMES)
|
||||
output_path = Path(OUTPUT)
|
||||
|
||||
|
||||
entries = []
|
||||
with open(names_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
@@ -29,12 +29,12 @@ def generate():
|
||||
if name:
|
||||
nid = name2nid(name)
|
||||
entries.append((nid, name))
|
||||
|
||||
|
||||
print(f"Found {len(entries)} entries")
|
||||
|
||||
data = bytearray()
|
||||
data.extend(struct.pack('<I', len(entries)))
|
||||
|
||||
|
||||
for nid, name in entries:
|
||||
nid_bytes = nid.encode('utf-8')
|
||||
name_bytes = name.encode('utf-8')
|
||||
@@ -42,10 +42,11 @@ def generate():
|
||||
data.extend(nid_bytes)
|
||||
data.extend(struct.pack('<H', len(name_bytes)))
|
||||
data.extend(name_bytes)
|
||||
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
print(f"Generated: {output_path} ({len(data):,} bytes)")
|
||||
print(f"Total entries: {len(entries)}")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,7 @@ internal static partial class Program
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
|
||||
Log.Info(BuildInfo.Banner);
|
||||
Log.Info(HostSystemInfo.Summary);
|
||||
|
||||
ebootPath = Path.GetFullPath(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||
|
||||
@@ -48,7 +48,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<TargetPath>LICENSE.txt</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
<Content Include="..\SharpEmu.GUI\Languages\*.json">
|
||||
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
|
||||
@@ -1076,7 +1076,9 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
|
||||
private static bool IsImportLoopGuardBoundary(string nid) =>
|
||||
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
|
||||
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal) ||
|
||||
string.Equals(nid, "Zxa0VhQVTsk", StringComparison.Ordinal) ||
|
||||
string.Equals(nid, "T72hz6ffq08", StringComparison.Ordinal);
|
||||
|
||||
private void ResetImportLoopPattern()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"_languageName": "Italiano",
|
||||
|
||||
"Page.Library": "Libreria",
|
||||
"Page.Options": "Opzioni",
|
||||
"Page.GameCount.One": "1 gioco",
|
||||
"Page.GameCount.Other": "{0} giochi",
|
||||
|
||||
"Library.SearchWatermark": "Cerca nella libreria…",
|
||||
"Library.AddFolder": "+ Aggiungi cartella",
|
||||
"Library.Rescan": "⟳ Riscansiona",
|
||||
"Library.OpenFile": "Apri file…",
|
||||
|
||||
"Library.Context.Launch": "Avvia",
|
||||
"Library.Context.OpenFolder": "Apri cartella gioco",
|
||||
"Library.Context.CopyPath": "Copia percorso",
|
||||
"Library.Context.CopyTitleId": "Copia ID titolo",
|
||||
"Library.Context.Remove": "Rimuovi dalla libreria",
|
||||
|
||||
"Library.Empty.Title": "La tua libreria è vuota",
|
||||
"Library.Empty.Hint": "Aggiungi la cartella che contiene i tuoi giochi per partire.",
|
||||
"Library.Empty.SearchTitle": "Nessun gioco corrisponde alla ricerca",
|
||||
"Library.Empty.SearchHint": "Nulla nella libreria corrisponde a “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Aggiungi cartella giochi",
|
||||
|
||||
"Library.Loading": "Caricamento libreria…",
|
||||
|
||||
"Options.General": "Generale",
|
||||
"Options.Section.Emulation": "EMULAZIONE",
|
||||
"Options.Section.Logging": "LOG",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU engine",
|
||||
"Options.CpuEngine.Desc": "Motore di esecuzione utilizzato per eseguire il codice del gioco.",
|
||||
"Options.CpuEngine.Native": "Nativo",
|
||||
|
||||
"Options.Strict.Label": "Risoluzione rigorosa dynlib",
|
||||
"Options.Strict.Desc": "Interrompi l'avvio quando un simbolo importato non può essere trovato.",
|
||||
|
||||
"Options.LogLevel.Label": "Livello Log",
|
||||
"Options.LogLevel.Desc": "Livello di dettaglio dell'output console dell'emulatore.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Limite tracciamento import",
|
||||
"Options.TraceImports.Desc": "Traccia i primi N import per modulo (0 = disattivato).",
|
||||
|
||||
|
||||
"Options.LogToFile.Label": "Salva log su file",
|
||||
"Options.LogToFile.Desc": "Duplica l'output dell'emulatore in un file di log.",
|
||||
|
||||
|
||||
"Options.LogFilePath.Label": "Percorso file di log",
|
||||
"Options.LogFilePath.Default": "Nessun percorso personalizzato — i log vengono salvati in user/logs accanto all'emulatore.",
|
||||
"Options.LogFilePath.Select": "Seleziona…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Sovrascrivi file di log",
|
||||
"Options.OverrideLogFile.Desc": "Usa esattamente il percorso specificato invece di aggiungere Title ID e timestamp.",
|
||||
|
||||
|
||||
"Options.TitleMusic.Label": "Musica del titolo",
|
||||
"Options.TitleMusic.Desc": "Riproduci in loop la musica di anteprima del gioco selezionato nella libreria.",
|
||||
|
||||
"Options.Discord.Label": "Presenza Discord",
|
||||
"Options.Discord.Desc": "Mostra il gioco in esecuzione sul tuo profilo Discord.",
|
||||
|
||||
"Options.Language.Label": "Lingua dell'emulatore",
|
||||
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
|
||||
|
||||
"Common.On": "On",
|
||||
"Common.Off": "Off",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Cerca...",
|
||||
"Console.AutoScroll": "Scorrimento automatico",
|
||||
"Console.Split": "Dividi",
|
||||
"Console.Copy": "Copia",
|
||||
"Console.Clear": "Cancella",
|
||||
"Console.WindowTitle": "Console SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Nessun gioco selezionato",
|
||||
"Launch.NoGameHint": "Scegli un gioco dalla libreria, oppure apri direttamente un eboot.bin.",
|
||||
"Launch.Idle": "Inattivo",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Avvia",
|
||||
"Launch.Stop": "■ Ferma",
|
||||
"Launch.Running": "In esecuzione — {0}",
|
||||
"Launch.Stopping": "Arresto in corso…",
|
||||
"Launch.Exited": "Terminato con codice {0} ({1})",
|
||||
"Launch.ExeNotFound": "Eseguibile SharpEmu non trovato. Compila prima il progetto SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "File di log: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Impossibile avviare l'emulatore: {0}",
|
||||
"Launch.ProcessExited": "Processo terminato con codice {0} ({1}).",
|
||||
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "argomenti non validi",
|
||||
"Exit.EbootNotFound": "eboot non trovato",
|
||||
"Exit.RuntimeException": "errore di runtime",
|
||||
"Exit.EmulationError": "errore di emulazione",
|
||||
"Exit.Unknown": "sconosciuto",
|
||||
|
||||
|
||||
"Status.EmulatorLocating": "Emulatore: ricerca in corso…",
|
||||
"Status.EmulatorPath": "Emulatore: {0}",
|
||||
"Status.EmulatorNotFound": "Emulatore: eseguibile SharpEmu non trovato — compila prima SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Scansione della libreria…",
|
||||
"Status.AddFolderPrompt": "Aggiungi una cartella di giochi per popolare la libreria.",
|
||||
"Status.LibraryScanned": "Libreria scansionata: {0} gioco/giochi in {1} cartella/e.",
|
||||
"Status.CouldNotOpenFolder": "Impossibile aprire la cartella: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copiato negli appunti.",
|
||||
"Status.RemovedFromLibrary": "“{0}” rimosso dalla libreria. Riaggiungi la sua cartella per ripristinarlo.",
|
||||
"Status.Running": "In esecuzione: {0}",
|
||||
"Status.Stopping": "Arresto in corso…",
|
||||
"Status.Idle": "Inattivo",
|
||||
|
||||
"Clipboard.Path": "Percorso",
|
||||
"Clipboard.TitleId": "ID Titolo",
|
||||
|
||||
"Discord.Playing": "Sta giocando a {0}",
|
||||
"Discord.Browsing": "Sta esplorando la libreria",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Scegli una cartella contenente giochi",
|
||||
"Dialog.OpenExecutable": "Apri un eseguibile da avviare",
|
||||
"Dialog.PsExecutables": "Eseguibili PS",
|
||||
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
|
||||
"Dialog.PlainTextFiles": "File di testo semplice",
|
||||
"Dialog.LogFiles": "File di log"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Русский",
|
||||
|
||||
"Page.Library": "Библиотека",
|
||||
"Page.Options": "Настройки",
|
||||
"Page.GameCount.One": "1 игра",
|
||||
"Page.GameCount.Other": "Игр: {0}",
|
||||
|
||||
"Library.SearchWatermark": "Поиск…",
|
||||
"Library.AddFolder": "+ Добавить папку",
|
||||
"Library.Rescan": "⟳ Сканировать",
|
||||
"Library.OpenFile": "Открыть файл…",
|
||||
|
||||
"Library.Context.Launch": "Запустить",
|
||||
"Library.Context.OpenFolder": "Открыть папку с игрой",
|
||||
"Library.Context.CopyPath": "Скопировать путь",
|
||||
"Library.Context.CopyTitleId": "Скопировать ID игры",
|
||||
"Library.Context.Remove": "Удалить из библиотеки",
|
||||
|
||||
"Library.Empty.Title": "Ваша библиотека пуста",
|
||||
"Library.Empty.Hint": "Добавьте папку с играми, чтобы начать.",
|
||||
"Library.Empty.SearchTitle": "По вашему запросу ничего не найдено",
|
||||
"Library.Empty.SearchHint": "В библиотеке нет игр, соответствующих запросу «{0}».",
|
||||
"Library.Empty.AddFolder": "+ Добавить папку с играми",
|
||||
|
||||
"Library.Loading": "Загрузка библиотеки…",
|
||||
|
||||
"Options.General": "Основные",
|
||||
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
|
||||
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
|
||||
"Options.Section.Launcher": "ЛАУНЧЕР",
|
||||
|
||||
"Options.CpuEngine.Label": "Движок ЦП",
|
||||
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
|
||||
"Options.CpuEngine.Native": "Нативный",
|
||||
|
||||
"Options.Strict.Label": "Строгое разрешение динамических библиотек",
|
||||
"Options.Strict.Desc": "Не запускать игру, если не удается найти или связать импортируемую функцию/переменную.",
|
||||
|
||||
"Options.LogLevel.Label": "Уровень логгирования",
|
||||
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Лимит трассировки импортов",
|
||||
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
|
||||
|
||||
"Options.LogToFile.Label": "Запись лога в файл",
|
||||
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
|
||||
|
||||
"Options.LogFilePath.Label": "Путь к файлу лога",
|
||||
"Options.LogFilePath.Default": "Пользовательский путь не задан: логи сохраняются в user/logs рядом с эмулятором.",
|
||||
"Options.LogFilePath.Select": "Выбрать…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Переопределить файл лога",
|
||||
"Options.OverrideLogFile.Desc": "Использовать указанный путь к файлу без добавления ID игры и метки времени.",
|
||||
|
||||
"Options.TitleMusic.Label": "Музыка игры",
|
||||
"Options.TitleMusic.Desc": "Зацикленно воспроизводить музыку предпросмотра выбранной игры в библиотеке.",
|
||||
|
||||
"Options.Discord.Label": "Статус Discord",
|
||||
"Options.Discord.Desc": "Показывать запущенную игру в профиле Discord.",
|
||||
|
||||
"Options.Language.Label": "Язык эмулятора",
|
||||
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
|
||||
|
||||
"Common.On": "Включено",
|
||||
"Common.Off": "Выключено",
|
||||
|
||||
"Console.Title": "КОНСОЛЬ",
|
||||
"Console.SearchWatermark": "Поиск...",
|
||||
"Console.AutoScroll": "Авто-прокрутка",
|
||||
"Console.Split": "Разделить",
|
||||
"Console.Copy": "Скопировать",
|
||||
"Console.Clear": "Очистить",
|
||||
"Console.WindowTitle": "Консоль SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Ничего не выбрано",
|
||||
"Launch.NoGameHint": "Выберите игру из библиотеки или откройте eboot.bin напрямую.",
|
||||
"Launch.Idle": "Ожидание",
|
||||
"Launch.Console": "≡ Консоль",
|
||||
"Launch.Launch": "▶ Запустить",
|
||||
"Launch.Stop": "■ Остановить",
|
||||
"Launch.Running": "Запущено - {0}",
|
||||
"Launch.Stopping": "Остановка…",
|
||||
"Launch.Exited": "Завершено с кодом {0} ({1})",
|
||||
"Launch.ExeNotFound": "Исполняемый файл SharpEmu не найден. Скомпилируйте сначала проект SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Лог: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Не удалось запустить эмулятор: {0}",
|
||||
"Launch.ProcessExited": "Процесс завершился с кодом {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "некорректные аргументы",
|
||||
"Exit.EbootNotFound": "eboot не найден",
|
||||
"Exit.RuntimeException": "ошибка выполнения",
|
||||
"Exit.EmulationError": "ошибка эмуляции",
|
||||
"Exit.Unknown": "неизвестная ошибка",
|
||||
|
||||
"Status.EmulatorLocating": "Эмулятор: поиск…",
|
||||
"Status.EmulatorPath": "Эмулятор: {0}",
|
||||
"Status.EmulatorNotFound": "Эмулятор: исполняемый файл SharpEmu не был найден - скомпилируйте сначала SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Сканирование библиотеки…",
|
||||
"Status.AddFolderPrompt": "Добавьте папку с играми, чтобы заполнить библиотеку.",
|
||||
"Status.LibraryScanned": "Сканирование библиотеки завершено: игр: {0}, папок: {1}.",
|
||||
"Status.CouldNotOpenFolder": "Не удалось открыть папку: {0}",
|
||||
"Status.CopiedToClipboard": "{0}: скопировано в буфер обмена.",
|
||||
"Status.RemovedFromLibrary": "Игра «{0}» удалена из библиотеки. Добавьте её папку заново, чтобы вернуть.",
|
||||
"Status.Running": "Запущено: {0}",
|
||||
"Status.Stopping": "Остановка…",
|
||||
"Status.Idle": "Ожидание",
|
||||
|
||||
"Clipboard.Path": "Путь",
|
||||
"Clipboard.TitleId": "ID игры",
|
||||
|
||||
"Discord.Playing": "Играет в {0}",
|
||||
"Discord.Browsing": "Просматривает библиотеку",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Выберите папку, содержащую игры",
|
||||
"Dialog.OpenExecutable": "Открыть исполняемый файл для запуска",
|
||||
"Dialog.PsExecutables": "Исполняемые файлы PS",
|
||||
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
|
||||
"Dialog.PlainTextFiles": "Текстовые файлы",
|
||||
"Dialog.LogFiles": "Логи"
|
||||
}
|
||||
@@ -6,10 +6,11 @@ using System.Text.Json;
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Loads UI strings for the launcher from Languages/<code>.json next to the
|
||||
/// executable. The files are plain, pretty-printed JSON (a flat key/value
|
||||
/// map) so they stay easy to open and translate by hand; a missing or
|
||||
/// unreadable key falls back to the key name itself.
|
||||
/// Loads UI strings for the launcher. Every language ships embedded in the
|
||||
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
|
||||
/// self-contained; an optional Languages/<code>.json file next to the
|
||||
/// executable overrides the embedded copy for that code, so a translation
|
||||
/// fix or a brand-new language never needs a rebuild.
|
||||
/// </summary>
|
||||
public sealed class Localization
|
||||
{
|
||||
@@ -17,13 +18,16 @@ public sealed class Localization
|
||||
|
||||
public sealed record LanguageInfo(string Code, string NativeName);
|
||||
|
||||
private const string EmbeddedResourcePrefix = "Languages.";
|
||||
private const string EmbeddedResourceSuffix = ".json";
|
||||
|
||||
private Dictionary<string, string> _strings = new();
|
||||
|
||||
private Localization()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Directory holding the *.json language files; user-editable.</summary>
|
||||
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
|
||||
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
|
||||
|
||||
public string CurrentCode { get; private set; } = "en";
|
||||
@@ -32,32 +36,73 @@ public sealed class Localization
|
||||
|
||||
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
||||
|
||||
/// <summary>Languages discovered under <see cref="LanguagesDirectory"/>, sorted by code.</summary>
|
||||
/// <summary>
|
||||
/// Languages available either embedded in the binary or as a loose
|
||||
/// override file, sorted by code. A loose file's declared name wins when
|
||||
/// the same code exists in both places.
|
||||
/// </summary>
|
||||
public List<LanguageInfo> DiscoverLanguages()
|
||||
{
|
||||
var languages = new List<LanguageInfo>();
|
||||
var languages = new Dictionary<string, LanguageInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var code in EmbeddedLanguageCodes())
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is not null)
|
||||
{
|
||||
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
|
||||
{
|
||||
var code = Path.GetFileNameWithoutExtension(file);
|
||||
languages.Add(new LanguageInfo(code, ReadLanguageName(file) ?? code));
|
||||
using var stream = File.OpenRead(file);
|
||||
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Missing Languages directory: no languages to offer.
|
||||
// No loose Languages directory: the embedded languages still stand.
|
||||
}
|
||||
|
||||
languages.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
|
||||
return languages;
|
||||
var result = languages.Values.ToList();
|
||||
result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? ReadLanguageName(string path)
|
||||
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
||||
public void Load(string code)
|
||||
{
|
||||
if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
|
||||
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EmbeddedLanguageCodes()
|
||||
{
|
||||
foreach (var name in typeof(Localization).Assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (name.StartsWith(EmbeddedResourcePrefix, StringComparison.Ordinal) &&
|
||||
name.EndsWith(EmbeddedResourceSuffix, StringComparison.Ordinal))
|
||||
{
|
||||
yield return name[EmbeddedResourcePrefix.Length..^EmbeddedResourceSuffix.Length];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream? OpenEmbeddedLanguageStream(string code) =>
|
||||
typeof(Localization).Assembly.GetManifestResourceStream($"{EmbeddedResourcePrefix}{code}{EmbeddedResourceSuffix}");
|
||||
|
||||
private static string? ReadLanguageName(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(path));
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
|
||||
name.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
@@ -66,44 +111,54 @@ public sealed class Localization
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Malformed file: fall back to the file's code as its display name.
|
||||
// Malformed file: fall back to the code as its own display name.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Loads a language by file code (e.g. "en"), falling back to English.</summary>
|
||||
public void Load(string code)
|
||||
{
|
||||
if (!TryLoadFile(code) && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
TryLoadFile("en");
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoadFile(string code)
|
||||
private bool TryLoadLooseFile(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(path));
|
||||
if (loaded is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_strings = loaded;
|
||||
CurrentCode = code;
|
||||
return true;
|
||||
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoadEmbedded(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
return TryLoad(code, reader.ReadToEnd());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoad(string code, string json)
|
||||
{
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (loaded is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_strings = loaded;
|
||||
CurrentCode = code;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Languages\*.json">
|
||||
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||
with the emulator's pad HLE. They are dependency-free, so they are
|
||||
compiled in directly rather than pulling a reference to all of
|
||||
|
||||
Binary file not shown.
@@ -461,66 +461,10 @@ public static class KernelExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Unidentified kernel NIDs observed at runtime; stubbed to return 0 until they are
|
||||
// resolved against scripts/ps5_names.txt.
|
||||
[SysAbiExport(
|
||||
Nid = "9T2pDF2Ryqg",
|
||||
ExportName = "sceKernelUnknown9T2p",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknown9T2p(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TU-d9PfIHPM",
|
||||
ExportName = "sceKernelUnknownTU",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownTU(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "iWQWrwiSt8A",
|
||||
ExportName = "sceKernelUnknowniWQW",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknowniWQW(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KuOmgKoqCdY",
|
||||
ExportName = "sceKernelUnknownKuOmg",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownKuOmg(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RenI1lL1WFk",
|
||||
ExportName = "sceKernelUnknownRenI",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownRenI(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pQGpHYopAIY",
|
||||
ExportName = "sceKernelUnknownpQGp",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownpQGp(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Rbvt+5Y2iEw",
|
||||
ExportName = "sceKernelUnknownRbvt",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownRbvt(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
|
||||
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
|
||||
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
|
||||
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
|
||||
// shadowing them breaks every C++ static-init guard in the game.
|
||||
|
||||
private static int SetZeroReturn(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,15 @@ public static class KernelMemoryCompatExports
|
||||
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static long _nextFileDescriptor = 2;
|
||||
|
||||
internal static int AllocateGuestFileDescriptor()
|
||||
{
|
||||
lock (_fdGate)
|
||||
{
|
||||
return (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong _nextPhysicalAddress;
|
||||
private static ulong _nextVirtualAddress;
|
||||
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
|
||||
@@ -1555,9 +1564,10 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var directoryFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||
int directoryFd;
|
||||
lock (_fdGate)
|
||||
{
|
||||
directoryFd = AllocateGuestFileDescriptor();
|
||||
_openDirectories[directoryFd] = new OpenDirectory
|
||||
{
|
||||
Path = hostPath,
|
||||
@@ -1578,9 +1588,10 @@ public static class KernelMemoryCompatExports
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
}
|
||||
|
||||
var fd = (int)Interlocked.Increment(ref _nextFileDescriptor);
|
||||
int fd;
|
||||
lock (_fdGate)
|
||||
{
|
||||
fd = AllocateGuestFileDescriptor();
|
||||
_openFiles[fd] = stream;
|
||||
}
|
||||
|
||||
@@ -1953,6 +1964,12 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (KernelSocketCompatExports.TryCloseSocketFd(fd))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
FileStream? stream;
|
||||
lock (_fdGate)
|
||||
{
|
||||
@@ -1996,6 +2013,17 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (KernelSocketCompatExports.TryReadSocketFd(ctx, fd, bufferAddress, requested, out var socketBytesRead, out var socketError))
|
||||
{
|
||||
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return (int)socketError;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = socketBytesRead;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
FileStream? stream;
|
||||
lock (_fdGate)
|
||||
{
|
||||
@@ -2225,6 +2253,17 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (KernelSocketCompatExports.TryWriteSocketFd(ctx, fd, payload, out var socketError))
|
||||
{
|
||||
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return (int)socketError;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
FileStream? stream;
|
||||
lock (_fdGate)
|
||||
{
|
||||
@@ -3441,6 +3480,18 @@ public static class KernelMemoryCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryFormatStringFromVaList(CpuContext ctx, string format, ulong vaListAddress, out string rendered)
|
||||
{
|
||||
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
|
||||
{
|
||||
rendered = format;
|
||||
return false;
|
||||
}
|
||||
|
||||
rendered = FormatString(ctx, format, ref vaCursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int WriteSnprintfOutput(
|
||||
CpuContext ctx,
|
||||
ulong destination,
|
||||
|
||||
@@ -137,7 +137,7 @@ public static class KernelPthreadCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadYield(CpuContext ctx)
|
||||
{
|
||||
_ = ctx;
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "scePthreadYield");
|
||||
Thread.Yield();
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -85,22 +85,29 @@ public static class KernelRuntimeCompatExports
|
||||
ExportName = "sceKernelNanosleep",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelNanosleep(CpuContext ctx)
|
||||
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yS8U2TGCe1A",
|
||||
ExportName = "nanosleep",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: true);
|
||||
|
||||
private static int NanosleepCore(CpuContext ctx, bool posix)
|
||||
{
|
||||
var requestAddress = ctx[CpuRegister.Rdi];
|
||||
var remainAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (requestAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Einval;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Efault;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return NanosleepFailure(ctx, posix, Efault, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
|
||||
@@ -108,8 +115,7 @@ public static class KernelRuntimeCompatExports
|
||||
|
||||
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Einval;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (tvSec == 0 && tvNsec == 0)
|
||||
@@ -119,7 +125,7 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelNanosleep");
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
|
||||
|
||||
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
|
||||
// a single tick rather than collapsing to a zero-length (no-op) sleep.
|
||||
@@ -138,6 +144,21 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int NanosleepFailure(CpuContext ctx, bool posix, int errnoValue, OrbisGen2Result sceResult)
|
||||
{
|
||||
if (posix)
|
||||
{
|
||||
TrySetErrno(ctx, errnoValue);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)errnoValue);
|
||||
}
|
||||
|
||||
return (int)sceResult;
|
||||
}
|
||||
|
||||
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
|
||||
{
|
||||
if (remainAddress == 0)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -12,6 +13,9 @@ public static class KernelSemaphoreCompatExports
|
||||
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
||||
private static int _nextSemaphoreHandle = 1;
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _semaPollBackoffCount;
|
||||
|
||||
private sealed class KernelSemaphoreState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
@@ -28,6 +32,7 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
public required int NeedCount { get; init; }
|
||||
public required int CancelEpochAtBlock { get; init; }
|
||||
public bool Timed { get; init; }
|
||||
|
||||
// Written and read only under the owning semaphore's Gate.
|
||||
public int? Result { get; set; }
|
||||
@@ -120,6 +125,7 @@ public static class KernelSemaphoreCompatExports
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var pollTimedOut = false;
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count >= needCount)
|
||||
@@ -131,36 +137,82 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(timeoutAddress, out _))
|
||||
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
if (timeoutMicros == 0)
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
pollTimedOut = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
|
||||
var timedWaiter = new SemaphoreWaiter
|
||||
{
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
Timed = true,
|
||||
};
|
||||
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
|
||||
blockDeadlineTimestamp: deadline))
|
||||
{
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Host-owned threads cannot park in the guest scheduler; degrade to the
|
||||
// immediate-timeout poll the callers already tolerate.
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
pollTimedOut = true;
|
||||
}
|
||||
}
|
||||
|
||||
var waiter = new SemaphoreWaiter
|
||||
if (!pollTimedOut)
|
||||
{
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
};
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
var waiter = new SemaphoreWaiter
|
||||
{
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
};
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
|
||||
if ((++_semaPollBackoffCount & 255) == 0)
|
||||
{
|
||||
Thread.Sleep(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Yield();
|
||||
}
|
||||
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -368,6 +420,42 @@ public static class KernelSemaphoreCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private static int CompleteBlockedTimedSemaWait(
|
||||
CpuContext ctx,
|
||||
KernelSemaphoreState semaphore,
|
||||
SemaphoreWaiter waiter,
|
||||
ulong timeoutAddress,
|
||||
long deadlineTimestamp)
|
||||
{
|
||||
int result;
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
}
|
||||
|
||||
result = waiter.Result!.Value;
|
||||
}
|
||||
|
||||
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
|
||||
var remainingMicros = remainingTicks <= 0
|
||||
? 0u
|
||||
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
internal static class KernelSocketCompatExports
|
||||
{
|
||||
private sealed class EmulatedSocketState
|
||||
{
|
||||
public TcpClient? Client;
|
||||
public NetworkStream? Stream;
|
||||
public IPAddress BoundAddress = IPAddress.Any;
|
||||
public int BoundPort;
|
||||
public bool Bound;
|
||||
public bool Connected;
|
||||
}
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static readonly Dictionary<int, EmulatedSocketState> Sockets = new();
|
||||
|
||||
internal static bool IsEmulatedSocketFd(int fd)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return Sockets.ContainsKey(fd);
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryCloseSocketFd(int fd)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (!Sockets.Remove(fd, out var state))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DisposeEmulatedSocket(state);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryReadSocketFd(
|
||||
CpuContext ctx,
|
||||
int fd,
|
||||
ulong bufferAddress,
|
||||
int requested,
|
||||
out ulong bytesRead,
|
||||
out OrbisGen2Result error)
|
||||
{
|
||||
bytesRead = 0;
|
||||
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out var state) ||
|
||||
state is null ||
|
||||
!state.Connected ||
|
||||
state.Stream is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var socketBuffer = GC.AllocateUninitializedArray<byte>(requested);
|
||||
int socketRead;
|
||||
try
|
||||
{
|
||||
socketRead = state.Stream.Read(socketBuffer, 0, requested);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (socketRead > 0 && !ctx.Memory.TryWrite(bufferAddress, socketBuffer.AsSpan(0, socketRead)))
|
||||
{
|
||||
error = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return true;
|
||||
}
|
||||
|
||||
bytesRead = unchecked((ulong)socketRead);
|
||||
error = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryWriteSocketFd(
|
||||
CpuContext ctx,
|
||||
int fd,
|
||||
byte[] payload,
|
||||
out OrbisGen2Result error)
|
||||
{
|
||||
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out var state) ||
|
||||
state is null ||
|
||||
!state.Connected ||
|
||||
state.Stream is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
state.Stream.Write(payload, 0, payload.Length);
|
||||
state.Stream.Flush();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Out.Write(Encoding.UTF8.GetString(payload));
|
||||
Console.Out.Flush();
|
||||
}
|
||||
|
||||
error = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TU-d9PfIHPM",
|
||||
ExportName = "socket",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Socket(CpuContext ctx)
|
||||
{
|
||||
var fd = KernelMemoryCompatExports.AllocateGuestFileDescriptor();
|
||||
lock (Gate)
|
||||
{
|
||||
Sockets[fd] = new EmulatedSocketState();
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)fd);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XVL8So3QJUk",
|
||||
ExportName = "connect",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Connect(CpuContext ctx)
|
||||
{
|
||||
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var sockaddrAddress = ctx[CpuRegister.Rsi];
|
||||
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out _))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
|
||||
{
|
||||
LogNet($"connect sockaddr parse failed: fd={fd} addr=0x{sockaddrAddress:X} len={addrlen}");
|
||||
RemoveEmulatedSocketFd(fd);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var redirectApplied = TryApplyNetRedirect(ref ipAddress);
|
||||
if (redirectApplied)
|
||||
{
|
||||
LogNet($"connect redirect: fd={fd} ip={ipAddress} port={port}");
|
||||
}
|
||||
|
||||
if (!IsGuestTcpOutboundAllowed(ipAddress, redirectApplied))
|
||||
{
|
||||
LogNet($"connect denied by outbound policy: fd={fd} ip={ipAddress} port={port}");
|
||||
RemoveEmulatedSocketFd(fd);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!TryEstablishHostTcpConnection(ipAddress, port, out var client, out var stream))
|
||||
{
|
||||
LogNet($"connect failed: fd={fd} ip={ipAddress} port={port}");
|
||||
RemoveEmulatedSocketFd(fd);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
LogNet($"connect ok: fd={fd} ip={ipAddress} port={port}");
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (!Sockets.TryGetValue(fd, out var state) || state is null)
|
||||
{
|
||||
try { stream.Dispose(); } catch (IOException) { }
|
||||
try { client.Dispose(); } catch (IOException) { }
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
DisposeEmulatedSocket(state);
|
||||
state.Client = client;
|
||||
state.Stream = stream;
|
||||
state.Connected = true;
|
||||
state.BoundAddress = ipAddress;
|
||||
state.BoundPort = port;
|
||||
state.Bound = true;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KuOmgKoqCdY",
|
||||
ExportName = "bind",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Bind(CpuContext ctx)
|
||||
{
|
||||
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var sockaddrAddress = ctx[CpuRegister.Rsi];
|
||||
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out var state) || state is null)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
state.BoundAddress = ipAddress;
|
||||
state.BoundPort = port;
|
||||
state.Bound = true;
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RenI1lL1WFk",
|
||||
ExportName = "getsockname",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Getsockname(CpuContext ctx)
|
||||
{
|
||||
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var sockaddrAddress = ctx[CpuRegister.Rsi];
|
||||
var addrlenAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!TryGetEmulatedSocketState(fd, out var state) || state is null || !state.Bound)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
Span<byte> addrlenBuffer = stackalloc byte[4];
|
||||
if (!ctx.Memory.TryRead(addrlenAddress, addrlenBuffer))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var addrlen = BinaryPrimitives.ReadInt32LittleEndian(addrlenBuffer);
|
||||
if (addrlen < 8)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
Span<byte> sockaddr = stackalloc byte[16];
|
||||
sockaddr[0] = 16;
|
||||
sockaddr[1] = 2;
|
||||
BinaryPrimitives.WriteUInt16BigEndian(sockaddr.Slice(2, 2), (ushort)state.BoundPort);
|
||||
var addressBytes = state.BoundAddress.GetAddressBytes();
|
||||
if (addressBytes.Length == 4)
|
||||
{
|
||||
addressBytes.CopyTo(sockaddr.Slice(4, 4));
|
||||
}
|
||||
|
||||
var writeLength = Math.Min(addrlen, 16);
|
||||
if (!ctx.Memory.TryWrite(sockaddrAddress, sockaddr.Slice(0, writeLength)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt32LittleEndian(addrlenBuffer, writeLength);
|
||||
if (!ctx.Memory.TryWrite(addrlenAddress, addrlenBuffer))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "9oiX1kyeedA",
|
||||
ExportName = "bzero",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Bzero(CpuContext ctx)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
var length = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
if (length > 0 && address != 0)
|
||||
{
|
||||
var zeros = new byte[length];
|
||||
if (!ctx.Memory.TryWrite(address, zeros))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4n51s0zEf0c",
|
||||
ExportName = "inet_pton",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int InetPton(CpuContext ctx)
|
||||
{
|
||||
var af = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var srcAddress = ctx[CpuRegister.Rsi];
|
||||
var dstAddress = ctx[CpuRegister.Rdx];
|
||||
if (af != 2 || srcAddress == 0 || dstAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryReadCString(srcAddress, ctx, out var text) ||
|
||||
!TryParseIpv4Address(text, out var octets))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
Span<byte> packed = stackalloc byte[4];
|
||||
packed[0] = octets[0];
|
||||
packed[1] = octets[1];
|
||||
packed[2] = octets[2];
|
||||
packed[3] = octets[3];
|
||||
if (!ctx.Memory.TryWrite(dstAddress, packed))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 1;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jogUIsOV3-U",
|
||||
ExportName = "htons",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Htons(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||
var swapped = (ushort)(((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF));
|
||||
ctx[CpuRegister.Rax] = swapped;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryGetEmulatedSocketState(int fd, out EmulatedSocketState? state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return Sockets.TryGetValue(fd, out state);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseGuestSockaddrIn(
|
||||
ulong address,
|
||||
int addrlen,
|
||||
CpuContext ctx,
|
||||
out IPAddress ipAddress,
|
||||
out int port)
|
||||
{
|
||||
ipAddress = IPAddress.None;
|
||||
port = 0;
|
||||
if (address == 0 || addrlen < 8)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> buffer = stackalloc byte[16];
|
||||
var readLength = Math.Min(addrlen, buffer.Length);
|
||||
if (!ctx.Memory.TryRead(address, buffer.Slice(0, readLength)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[1] != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
port = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2, 2));
|
||||
ipAddress = new IPAddress(buffer.Slice(4, 4).ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void DisposeEmulatedSocket(EmulatedSocketState state)
|
||||
{
|
||||
try { state.Stream?.Dispose(); } catch (IOException) { }
|
||||
try { state.Client?.Dispose(); } catch (IOException) { }
|
||||
state.Stream = null;
|
||||
state.Client = null;
|
||||
state.Connected = false;
|
||||
}
|
||||
|
||||
private static void RemoveEmulatedSocketFd(int fd)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (Sockets.Remove(fd, out var socketState))
|
||||
{
|
||||
DisposeEmulatedSocket(socketState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogNet(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][DEBUG] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryApplyNetRedirect(ref IPAddress ipAddress)
|
||||
{
|
||||
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
|
||||
if (string.IsNullOrWhiteSpace(redirect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IPAddress.TryParse(redirect.Trim(), out var redirectAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ipAddress = redirectAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsNetRedirectConfigured()
|
||||
{
|
||||
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
|
||||
return !string.IsNullOrWhiteSpace(redirect);
|
||||
}
|
||||
|
||||
private static bool IsGuestTcpOutboundAllowed(IPAddress ipAddress, bool redirectApplied)
|
||||
{
|
||||
return redirectApplied || IsNetRedirectConfigured() || IPAddress.IsLoopback(ipAddress);
|
||||
}
|
||||
|
||||
private static bool TryEstablishHostTcpConnection(
|
||||
IPAddress ipAddress,
|
||||
int port,
|
||||
out TcpClient client,
|
||||
out NetworkStream stream)
|
||||
{
|
||||
client = null!;
|
||||
stream = null!;
|
||||
if (!TryConnectTcpClient(ipAddress, port, out client))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
stream = client.GetStream();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryConnectTcpClient(IPAddress ipAddress, int port, out TcpClient client)
|
||||
{
|
||||
client = new TcpClient();
|
||||
try
|
||||
{
|
||||
var connectTask = client.ConnectAsync(ipAddress, port);
|
||||
if (!connectTask.Wait(TimeSpan.FromMilliseconds(500)))
|
||||
{
|
||||
client.Dispose();
|
||||
client = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (SocketException)
|
||||
{
|
||||
client.Dispose();
|
||||
client = null!;
|
||||
return false;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
client.Dispose();
|
||||
client = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadCString(ulong address, CpuContext ctx, out string text)
|
||||
{
|
||||
const int maxLength = 64;
|
||||
var buffer = new byte[maxLength];
|
||||
var length = 0;
|
||||
for (; length < maxLength; length++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(address + (ulong)length, buffer.AsSpan(length, 1)))
|
||||
{
|
||||
text = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer[length] == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
text = Encoding.ASCII.GetString(buffer, 0, length);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseIpv4Address(string text, out byte[] octets)
|
||||
{
|
||||
octets = Array.Empty<byte>();
|
||||
var parts = text.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (parts.Length != 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parsed = new byte[4];
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
if (!byte.TryParse(parts[i], out parsed[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
octets = parsed;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -338,6 +338,359 @@ public static class LibcStdioExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8Q60JLJ6Rv4",
|
||||
ExportName = "getc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Getc(CpuContext ctx) => FgetcCore(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AEuF3F2f8TA",
|
||||
ExportName = "fgetc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fgetc(CpuContext ctx) => FgetcCore(ctx);
|
||||
|
||||
private static int FgetcCore(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (!_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var value = stream.ReadByte();
|
||||
ctx[CpuRegister.Rax] = value < 0 ? unchecked((ulong)(-1L)) : (ulong)value;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "LxcEU+ICu8U",
|
||||
ExportName = "feof",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Feof(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var atEnd = _fileHandles.TryGetValue(handle, out var stream) &&
|
||||
stream.CanRead && stream.Position >= stream.Length;
|
||||
ctx[CpuRegister.Rax] = atEnd ? 1UL : 0UL;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AHxyhN96dy4",
|
||||
ExportName = "ferror",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Ferror(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3QIPIh-GDjw",
|
||||
ExportName = "rewind",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Rewind(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aZK8lNei-Qw",
|
||||
ExportName = "fputc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fputc(CpuContext ctx)
|
||||
{
|
||||
var character = unchecked((byte)ctx[CpuRegister.Rdi]);
|
||||
var handle = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.WriteByte(character);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unknown streams are the bundled libc's real stdout/stderr FILE objects;
|
||||
// mirror the fputs HLE and forward them to the host console.
|
||||
Console.Out.Write((char)character);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = character;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MpxhMh8QFro",
|
||||
ExportName = "fwrite",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fwrite(CpuContext ctx)
|
||||
{
|
||||
var source = ctx[CpuRegister.Rdi];
|
||||
var elementSize = ctx[CpuRegister.Rsi];
|
||||
var elementCount = ctx[CpuRegister.Rdx];
|
||||
var handle = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (elementSize == 0 || elementCount == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var knownHandle = _fileHandles.TryGetValue(handle, out var stream);
|
||||
if (source == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var totalRequested = elementSize * elementCount;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
|
||||
ulong totalWritten = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (totalWritten < totalRequested)
|
||||
{
|
||||
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalWritten);
|
||||
if (!ctx.Memory.TryRead(source + totalWritten, buffer.AsSpan(0, request)))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = totalWritten / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (knownHandle)
|
||||
{
|
||||
stream!.Write(buffer, 0, request);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Out.Write(System.Text.Encoding.UTF8.GetString(buffer, 0, request));
|
||||
}
|
||||
|
||||
totalWritten += (ulong)request;
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = totalWritten / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = totalWritten / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MUjC4lbHrK4",
|
||||
ExportName = "fflush",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fflush(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
|
||||
try
|
||||
{
|
||||
if (handle == 0)
|
||||
{
|
||||
foreach (var stream in _fileHandles.Values)
|
||||
{
|
||||
stream.Flush();
|
||||
}
|
||||
}
|
||||
else if (_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
stream.Flush();
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fffwELXNVFA",
|
||||
ExportName = "fprintf",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fprintf(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var formatAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
|
||||
return WriteRenderedText(ctx, handle, rendered);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pDBDcY6uLSA",
|
||||
ExportName = "vfprintf",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Vfprintf(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var formatAddress = ctx[CpuRegister.Rsi];
|
||||
var vaListAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
KernelMemoryCompatExports.TryFormatStringFromVaList(ctx, format, vaListAddress, out var rendered);
|
||||
return WriteRenderedText(ctx, handle, rendered);
|
||||
}
|
||||
|
||||
private static int WriteRenderedText(CpuContext ctx, ulong handle, string rendered)
|
||||
{
|
||||
var payload = System.Text.Encoding.UTF8.GetBytes(rendered);
|
||||
|
||||
if (_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.Write(payload, 0, payload.Length);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Out.Write(rendered);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = (ulong)payload.Length;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gkWgn0p1AfU",
|
||||
ExportName = "freopen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Freopen(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var modeAddress = ctx[CpuRegister.Rsi];
|
||||
var handle = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (pathAddress == 0 || modeAddress == 0 || handle == 0 ||
|
||||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
|
||||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
|
||||
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (_fileHandles.TryRemove(handle, out var previousStream))
|
||||
{
|
||||
previousStream.Dispose();
|
||||
}
|
||||
|
||||
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
|
||||
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (fileAccess != FileAccess.Read)
|
||||
{
|
||||
var parentDirectory = Path.GetDirectoryName(hostPath);
|
||||
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
// freopen keeps the caller's FILE* identity, so rebind the same handle value.
|
||||
_fileHandles[handle] = stream;
|
||||
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = handle;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return ex is UnauthorizedAccessException
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sUP1hBaouOw",
|
||||
ExportName = "_Getpctype",
|
||||
|
||||
@@ -95,6 +95,54 @@ public static class NetExports
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "9T2pDF2Ryqg",
|
||||
ExportName = "sceNetHtonl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetHtonl(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "iWQWrwiSt8A",
|
||||
ExportName = "sceNetHtons",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetHtons(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pQGpHYopAIY",
|
||||
ExportName = "sceNetNtohl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetNtohl(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Rbvt+5Y2iEw",
|
||||
ExportName = "sceNetNtohs",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNet")]
|
||||
public static int NetNtohs(CpuContext ctx)
|
||||
{
|
||||
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
|
||||
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "C4UgDHHPvdw",
|
||||
ExportName = "sceNetResolverCreate",
|
||||
|
||||
@@ -22,6 +22,17 @@ public static class NpManagerExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "S7QTn72PrDw",
|
||||
ExportName = "sceNpDeleteRequest",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpDeleteRequest(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JELHf4xPufo",
|
||||
ExportName = "sceNpCheckCallbackForLib",
|
||||
|
||||
@@ -178,6 +178,16 @@ public static class PadExports
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "2JgFB2n9oUM",
|
||||
ExportName = "scePadSetTriggerEffect",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadSetTriggerEffect(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yFVnOdGxvZY",
|
||||
ExportName = "scePadSetVibration",
|
||||
|
||||
@@ -14,6 +14,17 @@ internal static class PngSplashLoader
|
||||
];
|
||||
|
||||
public static bool TryLoad(out byte[] pixels, out uint width, out uint height)
|
||||
=> TryLoad("pic0.png", requestRgba: false, out pixels, out width, out height);
|
||||
|
||||
public static bool TryLoadIcon(out byte[] pixels, out uint width, out uint height)
|
||||
=> TryLoad("icon0.png", requestRgba: true, out pixels, out width, out height);
|
||||
|
||||
private static bool TryLoad(
|
||||
string fileName,
|
||||
bool requestRgba,
|
||||
out byte[] pixels,
|
||||
out uint width,
|
||||
out uint height)
|
||||
{
|
||||
pixels = [];
|
||||
width = 0;
|
||||
@@ -27,13 +38,18 @@ internal static class PngSplashLoader
|
||||
return false;
|
||||
}
|
||||
|
||||
var path = Path.Combine(app0Root, "sce_sys", "pic0.png");
|
||||
var path = Path.Combine(app0Root, "sce_sys", fileName);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryDecode(File.ReadAllBytes(path), out pixels, out width, out height);
|
||||
return TryDecode(
|
||||
File.ReadAllBytes(path),
|
||||
out pixels,
|
||||
out width,
|
||||
out height,
|
||||
requestRgba);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -48,7 +64,8 @@ internal static class PngSplashLoader
|
||||
ReadOnlySpan<byte> png,
|
||||
out byte[] pixels,
|
||||
out uint width,
|
||||
out uint height)
|
||||
out uint height,
|
||||
bool requestRgba)
|
||||
{
|
||||
pixels = [];
|
||||
width = 0;
|
||||
@@ -154,9 +171,13 @@ internal static class PngSplashLoader
|
||||
sourceOffset < reconstructed.Length;
|
||||
sourceOffset += sourceBytesPerPixel, targetOffset += 4)
|
||||
{
|
||||
pixels[targetOffset] = reconstructed[sourceOffset + 2];
|
||||
pixels[targetOffset] = requestRgba
|
||||
? reconstructed[sourceOffset]
|
||||
: reconstructed[sourceOffset + 2];
|
||||
pixels[targetOffset + 1] = reconstructed[sourceOffset + 1];
|
||||
pixels[targetOffset + 2] = reconstructed[sourceOffset];
|
||||
pixels[targetOffset + 2] = requestRgba
|
||||
? reconstructed[sourceOffset + 2]
|
||||
: reconstructed[sourceOffset];
|
||||
pixels[targetOffset + 3] = sourceBytesPerPixel == 4
|
||||
? reconstructed[sourceOffset + 3]
|
||||
: (byte)0xFF;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Logging;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -133,9 +134,10 @@ public static class VideoOutExports
|
||||
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
|
||||
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
|
||||
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
|
||||
var hardwareSuffix = $" \u00b7 {HostSystemInfo.CpuName} \u00b7 {HostSystemInfo.GpuName}";
|
||||
lock (_stateGate)
|
||||
{
|
||||
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
|
||||
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}{hardwareSuffix}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1138,6 +1138,12 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
if (PngSplashLoader.TryLoadIcon(out var iconPixels, out var iconWidth, out var iconHeight))
|
||||
{
|
||||
var icon = new RawImage((int)iconWidth, (int)iconHeight, iconPixels);
|
||||
_window.SetWindowIcon(ref icon);
|
||||
}
|
||||
|
||||
WaitForRenderDocAttachIfRequested();
|
||||
_vk = Vk.GetApi();
|
||||
CreateInstance();
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Microsoft.Win32;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
/// <summary>Provides best-effort information about the host system.</summary>
|
||||
public static class HostSystemInfo
|
||||
{
|
||||
private static readonly Lazy<string> CpuNameValue = new(GetCpuName);
|
||||
private static readonly Lazy<string> GpuNameValue = new(GetPreferredGpuName);
|
||||
private static readonly Lazy<string> MemoryDescriptionValue = new(GetMemoryDescription);
|
||||
|
||||
/// <summary>Host CPU name, or a safe fallback when it cannot be determined.</summary>
|
||||
public static string CpuName => CpuNameValue.Value;
|
||||
|
||||
/// <summary>Preferred physical GPU name, or a safe fallback when it cannot be determined.</summary>
|
||||
public static string GpuName => GpuNameValue.Value;
|
||||
|
||||
/// <summary>Returns a concise description of the host hardware for diagnostic logs.</summary>
|
||||
public static string Summary =>
|
||||
$"Host hardware: CPU: {CpuName}; GPU: {GpuName}; RAM: {MemoryDescriptionValue.Value}.";
|
||||
|
||||
private static string GetCpuName()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return $"{Environment.ProcessorCount} logical processors";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
|
||||
if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return Regex.Replace(
|
||||
name.Trim(),
|
||||
@"\s+\d+-Core Processor$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Hardware information is diagnostic only.
|
||||
}
|
||||
|
||||
return $"{Environment.ProcessorCount} logical processors";
|
||||
}
|
||||
|
||||
private static string GetPreferredGpuName()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var preferredName = "unknown";
|
||||
var preferredScore = int.MinValue;
|
||||
for (uint index = 0; ; index++)
|
||||
{
|
||||
var device = new DisplayDevice
|
||||
{
|
||||
cb = Marshal.SizeOf<DisplayDevice>(),
|
||||
};
|
||||
|
||||
if (!EnumDisplayDevices(null, index, ref device, 0))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var name = device.DeviceString?.Trim();
|
||||
var score = ScoreGpu(name);
|
||||
if (score > preferredScore)
|
||||
{
|
||||
preferredName = name!;
|
||||
preferredScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
return preferredScore > 0 ? preferredName : "unknown";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Hardware information is diagnostic only.
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private static int ScoreGpu(string? name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name) ||
|
||||
name.Contains("virtual", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("remote", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("basic display", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("geforce", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("amd", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Contains("radeon", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static string GetMemoryDescription()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = new MemoryStatusEx
|
||||
{
|
||||
dwLength = (uint)Marshal.SizeOf<MemoryStatusEx>(),
|
||||
};
|
||||
if (GlobalMemoryStatusEx(ref status))
|
||||
{
|
||||
var megabytes = status.ullTotalPhys / (1024 * 1024);
|
||||
var gigabytes = status.ullTotalPhys / (1024d * 1024 * 1024);
|
||||
return $"{megabytes:N0} MB ({gigabytes:N1} GB)";
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Hardware information is diagnostic only.
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct DisplayDevice
|
||||
{
|
||||
public int cb;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string? DeviceName;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string? DeviceString;
|
||||
public int StateFlags;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string? DeviceId;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string? DeviceKey;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||
private struct MemoryStatusEx
|
||||
{
|
||||
public uint dwLength;
|
||||
public uint dwMemoryLoad;
|
||||
public ulong ullTotalPhys;
|
||||
public ulong ullAvailPhys;
|
||||
public ulong ullTotalPageFile;
|
||||
public ulong ullAvailPageFile;
|
||||
public ulong ullTotalVirtual;
|
||||
public ulong ullAvailVirtual;
|
||||
public ulong ullAvailExtendedVirtual;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumDisplayDevices(string? deviceName, uint deviceNum, ref DisplayDevice displayDevice, uint flags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx buffer);
|
||||
}
|
||||
Reference in New Issue
Block a user