mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2f3511002 | |||
| 90a5d5176f | |||
| 28a43e09c7 | |||
| 093cfa1f3e | |||
| d8397b022e | |||
| 85cc2b9892 | |||
| 293194c40b | |||
| 1f09de8896 | |||
| ddc452b4fc |
@@ -814,10 +814,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return !_logUsleep;
|
return !_logUsleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
|
// Mutex lock uses this block-capable leaf path. Keep it out of the no-block subset.
|
||||||
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
|
|
||||||
// slows guest spinlocks enough to livelock (Demon's Souls).
|
|
||||||
return nid is
|
return nid is
|
||||||
|
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||||
|
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||||
|
|||||||
@@ -550,6 +550,47 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected)
|
||||||
|
{
|
||||||
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var region = FindRegion(virtualAddress, (ulong)expected.Length);
|
||||||
|
if (region is null ||
|
||||||
|
!TryResolveRegionOffset(
|
||||||
|
virtualAddress,
|
||||||
|
(ulong)expected.Length,
|
||||||
|
region,
|
||||||
|
out var offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expected.IsEmpty)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (region.IsReservedOnly &&
|
||||||
|
!EnsureRangeCommitted((ulong)srcPtr, (ulong)expected.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)expected.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReadOnlySpan<byte>(srcPtr, expected.Length).SequenceEqual(expected);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
var requiresExclusiveAccess = false;
|
var requiresExclusiveAccess = false;
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Português (Brasil)",
|
||||||
|
|
||||||
|
"Page.Library": "Biblioteca",
|
||||||
|
"Page.Options": "Opções",
|
||||||
|
"Page.GameCount.One": "1 Jogo",
|
||||||
|
"Page.GameCount.Other": "{0} jogos",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Pesquisar na biblioteca…",
|
||||||
|
"Library.AddFolder": "+ Adicionar pasta",
|
||||||
|
"Library.Rescan": "⟳ Atualizar biblioteca",
|
||||||
|
"Library.OpenFile": "Abrir arquivo…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Jogar",
|
||||||
|
"Library.Context.OpenFolder": "Abrir pasta do jogo",
|
||||||
|
"Library.Context.CopyPath": "Copiar o caminho",
|
||||||
|
"Library.Context.CopyTitleId": "Copiar ID do título",
|
||||||
|
"Library.Context.Remove": "Remover da biblioteca",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Sua biblioteca está vazia",
|
||||||
|
"Library.Empty.Hint": "Adicione uma pasta contendo seus jogos para começar.",
|
||||||
|
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua busca",
|
||||||
|
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Adicionar pasta do jogo",
|
||||||
|
|
||||||
|
"Library.Loading": "Carregando biblioteca…",
|
||||||
|
|
||||||
|
"Options.General": "Opções Gerais",
|
||||||
|
"Options.Section.Emulation": "EMULAÇÃO",
|
||||||
|
"Options.Section.Logging": "LOGS",
|
||||||
|
"Options.Section.Launcher": "INICIALIZADOR",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Motor da CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Motor de execução usado para executar o código do jogo.",
|
||||||
|
"Options.CpuEngine.Native": "Nativo",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Resolução estrita de bibliotecas dinâmicas",
|
||||||
|
"Options.Strict.Desc": "Interrompe a inicialização caso um símbolo importado não possa ser vinculado.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Nível de log",
|
||||||
|
"Options.LogLevel.Desc": "Nível de detalhamento das mensagens exibidas no console do emulador.",
|
||||||
|
"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 de rastreamento de importações",
|
||||||
|
"Options.TraceImports.Desc": "Rastreia as primeiras N importações de cada módulo (0 = desativado).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Salvar log em arquivo",
|
||||||
|
"Options.LogToFile.Desc": "Copia a saída do emulador para um arquivo de log.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Caminho do arquivo de log",
|
||||||
|
"Options.LogFilePath.Default": "Nenhum caminho definido — logs vão para user/logs na pasta do emulador.",
|
||||||
|
"Options.LogFilePath.Select": "Selecionar…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Sobrescrever arquivo de log",
|
||||||
|
"Options.OverrideLogFile.Desc": "Use o caminho exato do arquivo em vez de adicionar o ID do título e o log de data e hora.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Música de prévia",
|
||||||
|
"Options.TitleMusic.Desc": "Reproduz em loop a música de prévia do jogo selecionado na biblioteca.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Status do Discord",
|
||||||
|
"Options.Discord.Desc": "Exibir o jogo em execução no seu perfil do Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Idioma do emulador",
|
||||||
|
"Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.",
|
||||||
|
|
||||||
|
"Common.On": "Ativado",
|
||||||
|
"Common.Off": "Desativado",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Pesquisar...",
|
||||||
|
"Console.AutoScroll": "Rolagem automática",
|
||||||
|
"Console.Split": "Recortar",
|
||||||
|
"Console.Copy": "Copiar",
|
||||||
|
"Console.Clear": "Limpar",
|
||||||
|
"Console.WindowTitle": "Console do SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Nenhum jogo selecionado",
|
||||||
|
"Launch.NoGameHint": "Selecione um jogo na biblioteca ou abra um arquivo eboot.bin diretamente.",
|
||||||
|
"Launch.Idle": "Ocioso",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Iniciar",
|
||||||
|
"Launch.Stop": "■ Parar",
|
||||||
|
"Launch.Running": "Em execução — {0}",
|
||||||
|
"Launch.Stopping": "Encerrando…",
|
||||||
|
"Launch.Exited": "Encerrado com código {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Arquivo de log: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
|
||||||
|
"Launch.ProcessExited": "O processo foi encerrado com código {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "argumentos inválidos",
|
||||||
|
"Exit.EbootNotFound": "eboot.bin não encontrado",
|
||||||
|
"Exit.RuntimeException": "exceção em tempo de execução",
|
||||||
|
"Exit.EmulationError": "erro de emulação",
|
||||||
|
"Exit.Unknown": "desconhecido",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||||
|
"Status.EmulatorPath": "Emulador: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile o SharpEmu.CLI primeiro.",
|
||||||
|
"Status.ScanningLibrary": "Verificando biblioteca…",
|
||||||
|
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
|
||||||
|
"Status.LibraryScanned": "Biblioteca verificada: {0} jogo(s) em {1} pasta(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
|
||||||
|
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente sua pasta para restaurá-lo.",
|
||||||
|
"Status.Running": "Executando {0}",
|
||||||
|
"Status.Stopping": "Encerrando…",
|
||||||
|
"Status.Idle": "Ocioso",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Caminho",
|
||||||
|
"Clipboard.TitleId": "ID do título",
|
||||||
|
|
||||||
|
"Discord.Playing": "Jogando {0}",
|
||||||
|
"Discord.Browsing": "Navegando pela biblioteca",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Escolha uma pasta contendo jogos",
|
||||||
|
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
|
||||||
|
"Dialog.PsExecutables": "Executáveis de PS",
|
||||||
|
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
||||||
|
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
||||||
|
"Dialog.LogFiles": "Arquivos de log"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Español",
|
||||||
|
|
||||||
|
"Page.Library": "Biblioteca",
|
||||||
|
"Page.Options": "Opciones",
|
||||||
|
"Page.GameCount.One": "1 juego",
|
||||||
|
"Page.GameCount.Other": "{0} juegos",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Buscar en la biblioteca…",
|
||||||
|
"Library.AddFolder": "+ Añadir carpeta",
|
||||||
|
"Library.Rescan": "⟳ Volver a escanear",
|
||||||
|
"Library.OpenFile": "Abrir archivo…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Iniciar",
|
||||||
|
"Library.Context.OpenFolder": "Abrir carpeta de juegos",
|
||||||
|
"Library.Context.CopyPath": "Copiar ruta",
|
||||||
|
"Library.Context.CopyTitleId": "Copiar ID del título",
|
||||||
|
"Library.Context.Remove": "Eliminar de la biblioteca",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Tu biblioteca está vacía",
|
||||||
|
"Library.Empty.Hint": "Añade una carpeta que contenga tus juegos para empezar.",
|
||||||
|
"Library.Empty.SearchTitle": "Ningún juego coincide con la búsqueda",
|
||||||
|
"Library.Empty.SearchHint": "No se ha encontrado nada en la biblioteca que coincida con “{0}”.",
|
||||||
|
"Library.Empty.AddFolder": "+ Añadir carpeta de juegos",
|
||||||
|
|
||||||
|
"Library.Loading": "Cargando biblioteca…",
|
||||||
|
|
||||||
|
"Options.General": "General",
|
||||||
|
"Options.Section.Emulation": "EMULACIÓN",
|
||||||
|
"Options.Section.Logging": "LOGS",
|
||||||
|
"Options.Section.Launcher": "LAUNCHER",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Motor de CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Motor utilizado para ejecutar el código del juego.",
|
||||||
|
"Options.CpuEngine.Native": "Nativo",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Resolución estricta de dynlib (Bibliotecas dinámicas)",
|
||||||
|
"Options.Strict.Desc": "Detener la ejecución cuando un símbolo importado no se pueda resolver.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Nivel de Log",
|
||||||
|
"Options.LogLevel.Desc": "Verbosidad de la salida en consola del emulador.",
|
||||||
|
"Options.LogLevel.Trace": "Trazas",
|
||||||
|
"Options.LogLevel.Debug": "Depuración",
|
||||||
|
"Options.LogLevel.Info": "Información",
|
||||||
|
"Options.LogLevel.Warning": "Advertencia",
|
||||||
|
"Options.LogLevel.Error": "Error",
|
||||||
|
"Options.LogLevel.Critical": "Crítico",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Límite de trazado de importaciones",
|
||||||
|
"Options.TraceImports.Desc": "Trazar las primeras N importaciones por módulo (0 = off).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Registrar log en archivo",
|
||||||
|
"Options.LogToFile.Desc": "Duplicar la salida del emulador en un archivo de logs.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Ruta del archivo de Log",
|
||||||
|
"Options.LogFilePath.Default": "Sin ruta personalizada — los logs van a user/logs al lado del emulador.",
|
||||||
|
"Options.LogFilePath.Select": "Seleccionar…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Sobreescribir archivo de logs",
|
||||||
|
"Options.OverrideLogFile.Desc": "Utilizar la misma ruta para el archivo de logs en vez de añadir la ID del título y marca de tiempo.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Música del título",
|
||||||
|
"Options.TitleMusic.Desc": "Repetir en bucle la preview de la música del juego seleccionado en la biblioteca.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Actividad de Discord",
|
||||||
|
"Options.Discord.Desc": "Mostrar juego en ejecución en tu perfil de Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Idioma del emulador",
|
||||||
|
"Options.Language.Desc": "Idioma utilizado en todo el launcher. Se aplica inmediatamente.",
|
||||||
|
|
||||||
|
"Common.On": "Encendido",
|
||||||
|
"Common.Off": "Apagado",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLA",
|
||||||
|
"Console.SearchWatermark": "Buscar...",
|
||||||
|
"Console.AutoScroll": "Desplazamiento automático",
|
||||||
|
"Console.Split": "Desacoplar",
|
||||||
|
"Console.Copy": "Copiar",
|
||||||
|
"Console.Clear": "Limpiar",
|
||||||
|
"Console.WindowTitle": "Consola SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "No hay ningún juego seleccionado",
|
||||||
|
"Launch.NoGameHint": "Selecciona un juego de la biblioteca o abre un eboot.bin directamente.",
|
||||||
|
"Launch.Idle": "Inactivo",
|
||||||
|
"Launch.Console": "≡ Consola",
|
||||||
|
"Launch.Launch": "▶ Iniciar",
|
||||||
|
"Launch.Stop": "■ Detener",
|
||||||
|
"Launch.Running": "En ejecución — {0}",
|
||||||
|
"Launch.Stopping": "Deteniendo…",
|
||||||
|
"Launch.Exited": "Finalizó con el código {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "No se ha encontrado el ejecutable de SharpEmu. Compila previamente el proyecto SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Archivo de Log: {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Error al iniciar el emulador: {0}",
|
||||||
|
"Launch.ProcessExited": "El proceso finalizó con el código {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "argumentos no válidos",
|
||||||
|
"Exit.EbootNotFound": "no se encontró eboot",
|
||||||
|
"Exit.RuntimeException": "excepción en tiempo de ejecución",
|
||||||
|
"Exit.EmulationError": "error de emulación",
|
||||||
|
"Exit.Unknown": "desconocido",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||||
|
"Status.EmulatorPath": "Emulador: {0}",
|
||||||
|
"Status.EmulatorNotFound": "Emulador: No se encontró el ejecutable de SharpEmu — compila previamente SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Escaneando biblioteca…",
|
||||||
|
"Status.AddFolderPrompt": "Añade una carpeta de juegos para poblar la biblioteca.",
|
||||||
|
"Status.LibraryScanned": "Biblioteca escaneada: Se encontraron {0} juego(s) en {1} carpeta(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "No se ha podido abrir la carpeta: {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copiado al portapapeles.",
|
||||||
|
"Status.RemovedFromLibrary": "Se eliminó “{0}” de la biblioteca. Vuelve a añadir su carpeta para restaurarlo.",
|
||||||
|
"Status.Running": "Ejecutando {0}",
|
||||||
|
"Status.Stopping": "Deteniendo…",
|
||||||
|
"Status.Idle": "Inactivo",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Ruta",
|
||||||
|
"Clipboard.TitleId": "ID del título",
|
||||||
|
|
||||||
|
"Discord.Playing": "Jugando a {0}",
|
||||||
|
"Discord.Browsing": "Navegando en la biblioteca, buscando un juego para divertirse.",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Selecciona una carpeta que contenga juegos",
|
||||||
|
"Dialog.OpenExecutable": "Abrir un ejecutable para iniciar",
|
||||||
|
"Dialog.PsExecutables": "Ejecutables de PS",
|
||||||
|
"Dialog.SaveLogFile": "Selecciona dónde guardar el archivo de Logs",
|
||||||
|
"Dialog.PlainTextFiles": "Archivos en texto plano",
|
||||||
|
"Dialog.LogFiles": "Archivos de Log"
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"_languageName": "Français",
|
||||||
|
|
||||||
|
"Page.Library": "Bibliothèque",
|
||||||
|
"Page.Options": "Options",
|
||||||
|
"Page.GameCount.One": "1 jeu",
|
||||||
|
"Page.GameCount.Other": "{0} jeux",
|
||||||
|
|
||||||
|
"Library.SearchWatermark": "Rechercher dans la bibliothèque…",
|
||||||
|
"Library.AddFolder": "+ Ajouter un dossier",
|
||||||
|
"Library.Rescan": "⟳ Analyser à nouveau",
|
||||||
|
"Library.OpenFile": "Ouvrir un fichier…",
|
||||||
|
|
||||||
|
"Library.Context.Launch": "Lancer",
|
||||||
|
"Library.Context.OpenFolder": "Ouvrir le dossier du jeu",
|
||||||
|
"Library.Context.CopyPath": "Copier le chemin",
|
||||||
|
"Library.Context.CopyTitleId": "Copier l’identifiant du jeu",
|
||||||
|
"Library.Context.Remove": "Retirer de la bibliothèque",
|
||||||
|
|
||||||
|
"Library.Empty.Title": "Votre bibliothèque est vide",
|
||||||
|
"Library.Empty.Hint": "Ajoutez un dossier contenant vos jeux pour commencer.",
|
||||||
|
"Library.Empty.SearchTitle": "Aucun jeu ne correspond à votre recherche",
|
||||||
|
"Library.Empty.SearchHint": "Aucun élément de la bibliothèque ne correspond à « {0} ».",
|
||||||
|
"Library.Empty.AddFolder": "+ Ajouter un dossier de jeux",
|
||||||
|
|
||||||
|
"Library.Loading": "Chargement de la bibliothèque…",
|
||||||
|
|
||||||
|
"Options.General": "Général",
|
||||||
|
"Options.Section.Emulation": "ÉMULATION",
|
||||||
|
"Options.Section.Logging": "JOURNALISATION",
|
||||||
|
"Options.Section.Launcher": "LANCEUR",
|
||||||
|
|
||||||
|
"Options.CpuEngine.Label": "Moteur CPU",
|
||||||
|
"Options.CpuEngine.Desc": "Moteur d’exécution utilisé pour exécuter le code du jeu.",
|
||||||
|
"Options.CpuEngine.Native": "Natif",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "Résolution stricte des bibliothèques dynamiques",
|
||||||
|
"Options.Strict.Desc": "Interrompre le lancement lorsqu’un symbole importé ne peut pas être résolu.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "Niveau de journalisation",
|
||||||
|
"Options.LogLevel.Desc": "Niveau de détail des messages affichés dans la console de l’émulateur.",
|
||||||
|
"Options.LogLevel.Trace": "Traçage",
|
||||||
|
"Options.LogLevel.Debug": "Débogage",
|
||||||
|
"Options.LogLevel.Info": "Informations",
|
||||||
|
"Options.LogLevel.Warning": "Avertissements",
|
||||||
|
"Options.LogLevel.Error": "Erreurs",
|
||||||
|
"Options.LogLevel.Critical": "Erreurs critiques",
|
||||||
|
|
||||||
|
"Options.TraceImports.Label": "Limite de traçage des imports",
|
||||||
|
"Options.TraceImports.Desc": "Tracer les N premiers imports de chaque module (0 = désactivé).",
|
||||||
|
|
||||||
|
"Options.LogToFile.Label": "Enregistrer dans un fichier",
|
||||||
|
"Options.LogToFile.Desc": "Copier la sortie de l’émulateur dans un fichier journal.",
|
||||||
|
|
||||||
|
"Options.LogFilePath.Label": "Chemin du fichier journal",
|
||||||
|
"Options.LogFilePath.Default": "Aucun chemin personnalisé — les journaux sont enregistrés dans user/logs à côté de l’émulateur.",
|
||||||
|
"Options.LogFilePath.Select": "Sélectionner…",
|
||||||
|
|
||||||
|
"Options.OverrideLogFile.Label": "Remplacer le fichier journal",
|
||||||
|
"Options.OverrideLogFile.Desc": "Utiliser exactement ce chemin au lieu d’ajouter l’identifiant du jeu et l’horodatage.",
|
||||||
|
|
||||||
|
"Options.TitleMusic.Label": "Musique du jeu",
|
||||||
|
"Options.TitleMusic.Desc": "Lire en boucle la musique d’aperçu du jeu sélectionné dans la bibliothèque.",
|
||||||
|
|
||||||
|
"Options.Discord.Label": "Présence Discord",
|
||||||
|
"Options.Discord.Desc": "Afficher le jeu en cours d’exécution sur votre profil Discord.",
|
||||||
|
|
||||||
|
"Options.Language.Label": "Langue de l’émulateur",
|
||||||
|
"Options.Language.Desc": "Langue utilisée dans l’ensemble du lanceur. Le changement est immédiat.",
|
||||||
|
|
||||||
|
"Common.On": "Activé",
|
||||||
|
"Common.Off": "Désactivé",
|
||||||
|
|
||||||
|
"Console.Title": "CONSOLE",
|
||||||
|
"Console.SearchWatermark": "Rechercher…",
|
||||||
|
"Console.AutoScroll": "Défilement automatique",
|
||||||
|
"Console.Split": "Détacher",
|
||||||
|
"Console.Copy": "Copier",
|
||||||
|
"Console.Clear": "Effacer",
|
||||||
|
"Console.WindowTitle": "Console SharpEmu",
|
||||||
|
|
||||||
|
"Launch.NoGameSelected": "Aucun jeu sélectionné",
|
||||||
|
"Launch.NoGameHint": "Choisissez un jeu dans la bibliothèque ou ouvrez directement un fichier eboot.bin.",
|
||||||
|
"Launch.Idle": "Inactif",
|
||||||
|
"Launch.Console": "≡ Console",
|
||||||
|
"Launch.Launch": "▶ Lancer",
|
||||||
|
"Launch.Stop": "■ Arrêter",
|
||||||
|
"Launch.Running": "En cours d’exécution — {0}",
|
||||||
|
"Launch.Stopping": "Arrêt en cours…",
|
||||||
|
"Launch.Exited": "Processus terminé avec le code {0} ({1})",
|
||||||
|
"Launch.ExeNotFound": "L’exécutable SharpEmu est introuvable. Compilez d’abord le projet SharpEmu.CLI (dotnet build).",
|
||||||
|
"Launch.LogFile": "Fichier journal : {0}",
|
||||||
|
"Launch.Command": "$ SharpEmu {0}",
|
||||||
|
"Launch.StartFailed": "Impossible de démarrer l’émulateur : {0}",
|
||||||
|
"Launch.ProcessExited": "Le processus s’est terminé avec le code {0} ({1}).",
|
||||||
|
|
||||||
|
"Exit.Ok": "OK",
|
||||||
|
"Exit.InvalidArguments": "arguments non valides",
|
||||||
|
"Exit.EbootNotFound": "eboot introuvable",
|
||||||
|
"Exit.RuntimeException": "exception d’exécution",
|
||||||
|
"Exit.EmulationError": "erreur d’émulation",
|
||||||
|
"Exit.Unknown": "inconnu",
|
||||||
|
|
||||||
|
"Status.EmulatorLocating": "Émulateur : recherche en cours…",
|
||||||
|
"Status.EmulatorPath": "Émulateur : {0}",
|
||||||
|
"Status.EmulatorNotFound": "Émulateur : l’exécutable SharpEmu est introuvable — compilez d’abord SharpEmu.CLI.",
|
||||||
|
"Status.ScanningLibrary": "Analyse de la bibliothèque…",
|
||||||
|
"Status.AddFolderPrompt": "Ajoutez un dossier de jeux pour remplir la bibliothèque.",
|
||||||
|
"Status.LibraryScanned": "Bibliothèque analysée : {0} jeu(x) dans {1} dossier(s).",
|
||||||
|
"Status.CouldNotOpenFolder": "Impossible d’ouvrir le dossier : {0}",
|
||||||
|
"Status.CopiedToClipboard": "{0} copié dans le presse-papiers.",
|
||||||
|
"Status.RemovedFromLibrary": "« {0} » a été retiré de la bibliothèque. Ajoutez à nouveau son dossier pour le restaurer.",
|
||||||
|
"Status.Running": "Exécution de {0}",
|
||||||
|
"Status.Stopping": "Arrêt en cours…",
|
||||||
|
"Status.Idle": "Inactif",
|
||||||
|
|
||||||
|
"Clipboard.Path": "Chemin",
|
||||||
|
"Clipboard.TitleId": "Identifiant du jeu",
|
||||||
|
|
||||||
|
"Discord.Playing": "Joue à {0}",
|
||||||
|
"Discord.Browsing": "Parcourt la bibliothèque",
|
||||||
|
|
||||||
|
"Dialog.ChooseGameFolder": "Choisir un dossier contenant des jeux",
|
||||||
|
"Dialog.OpenExecutable": "Ouvrir un exécutable à lancer",
|
||||||
|
"Dialog.PsExecutables": "Exécutables PlayStation",
|
||||||
|
"Dialog.SaveLogFile": "Choisir l’emplacement du fichier journal",
|
||||||
|
"Dialog.PlainTextFiles": "Fichiers texte brut",
|
||||||
|
"Dialog.LogFiles": "Fichiers journaux"
|
||||||
|
}
|
||||||
@@ -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": "CPUエンジン",
|
||||||
|
"Options.CpuEngine.Desc": "ゲームコードを実行するために使用される実行エンジン。",
|
||||||
|
"Options.CpuEngine.Native": "ネイティブ",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "厳格な動的ライブラリ解決",
|
||||||
|
"Options.Strict.Desc": "インポートされたシンボルが解決できない場合、起動を中断します。",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "ログレベル",
|
||||||
|
"Options.LogLevel.Desc": "エミュレータコンソールに表示されるメッセージの詳細度。",
|
||||||
|
"Options.LogLevel.Trace": "トレース",
|
||||||
|
"Options.LogLevel.Debug": "デバッグ",
|
||||||
|
"Options.LogLevel.Info": "情報",
|
||||||
|
"Options.LogLevel.Warning": "警告",
|
||||||
|
"Options.LogLevel.Error": "エラー",
|
||||||
|
"Options.LogLevel.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": "ライブラリのスキャン完了: {1} 個のフォルダーから {0} 本のゲームを検出。",
|
||||||
|
"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": "PlayStation 実行ファイル",
|
||||||
|
"Dialog.SaveLogFile": "ログファイルの保存先を選択",
|
||||||
|
"Dialog.PlainTextFiles": "プレーンテキストファイル",
|
||||||
|
"Dialog.LogFiles": "ログファイル"
|
||||||
|
}
|
||||||
@@ -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": "CPU 엔진",
|
||||||
|
"Options.CpuEngine.Desc": "게임 코드를 실행하는 데 사용되는 실행 엔진입니다.",
|
||||||
|
"Options.CpuEngine.Native": "네이티브",
|
||||||
|
|
||||||
|
"Options.Strict.Label": "엄격한 동적 라이브러리 해석",
|
||||||
|
"Options.Strict.Desc": "가져온 심볼을 해석할 수 없는 경우 실행을 중단합니다.",
|
||||||
|
|
||||||
|
"Options.LogLevel.Label": "로그 수준",
|
||||||
|
"Options.LogLevel.Desc": "에뮬레이터 콘솔에 표시할 메시지의 세부 정보 수준입니다.",
|
||||||
|
"Options.LogLevel.Trace": "트레이스",
|
||||||
|
"Options.LogLevel.Debug": "디버그",
|
||||||
|
"Options.LogLevel.Info": "정보",
|
||||||
|
"Options.LogLevel.Warning": "경고",
|
||||||
|
"Options.LogLevel.Error": "오류",
|
||||||
|
"Options.LogLevel.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": "디스코드 상태 표시",
|
||||||
|
"Options.Discord.Desc": "디스코드 프로필에 현재 실행 중인 게임을 표시합니다.",
|
||||||
|
|
||||||
|
"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": "확인",
|
||||||
|
"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": "라이브러리 스캔 완료: {1}개 폴더에서 {0}개 게임 발견.",
|
||||||
|
"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": "PlayStation 실행 파일",
|
||||||
|
"Dialog.SaveLogFile": "로그 파일 저장 위치 선택",
|
||||||
|
"Dialog.PlainTextFiles": "일반 텍스트 파일",
|
||||||
|
"Dialog.LogFiles": "로그 파일"
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ public sealed class Localization
|
|||||||
private const string EmbeddedResourceSuffix = ".json";
|
private const string EmbeddedResourceSuffix = ".json";
|
||||||
|
|
||||||
private Dictionary<string, string> _strings = new();
|
private Dictionary<string, string> _strings = new();
|
||||||
|
private Dictionary<string, string> _fallbackStrings = new();
|
||||||
|
|
||||||
|
|
||||||
private Localization()
|
private Localization()
|
||||||
{
|
{
|
||||||
@@ -32,7 +34,16 @@ public sealed class Localization
|
|||||||
|
|
||||||
public string CurrentCode { get; private set; } = "en";
|
public string CurrentCode { get; private set; } = "en";
|
||||||
|
|
||||||
public string Get(string key) => _strings.TryGetValue(key, out var value) ? value : key;
|
public string Get(string key)
|
||||||
|
{
|
||||||
|
if (_strings.TryGetValue(key, out var value))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
if (_fallbackStrings.TryGetValue(key, out var fallbackValue))
|
||||||
|
return fallbackValue;
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
||||||
|
|
||||||
@@ -74,13 +85,46 @@ public sealed class Localization
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
||||||
|
/// english is the fallback language
|
||||||
public void Load(string code)
|
public void Load(string code)
|
||||||
{
|
{
|
||||||
if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
|
if (_fallbackStrings.Count == 0 && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||||
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
|
if (!TryLoadLooseFile("en", out var fallback) && !TryLoadEmbedded("en", out fallback))
|
||||||
|
{
|
||||||
|
fallback = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
_fallbackStrings = fallback;
|
||||||
}
|
}
|
||||||
|
else if (string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (TryLoadLooseFile("en", out var enDict) || TryLoadEmbedded("en", out enDict))
|
||||||
|
{
|
||||||
|
_strings = enDict;
|
||||||
|
_fallbackStrings = enDict;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_strings = new Dictionary<string, string>();
|
||||||
|
_fallbackStrings = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
CurrentCode = "en";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the requested language
|
||||||
|
if (TryLoadLooseFile(code, out var loaded) || TryLoadEmbedded(code, out loaded))
|
||||||
|
{
|
||||||
|
_strings = loaded;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_fallbackStrings.Count > 0)
|
||||||
|
_strings = new Dictionary<string, string>(_fallbackStrings);
|
||||||
|
else
|
||||||
|
_strings = new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
CurrentCode = code;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> EmbeddedLanguageCodes()
|
private static IEnumerable<string> EmbeddedLanguageCodes()
|
||||||
@@ -149,16 +193,64 @@ public sealed class Localization
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryLoad(string code, string json)
|
private bool TryLoadLooseFile(string code, out Dictionary<string, string> result)
|
||||||
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
return TryLoad(json, out result);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryLoadEmbedded(string code, out Dictionary<string, string> result)
|
||||||
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = OpenEmbeddedLanguageStream(code);
|
||||||
|
if (stream is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
var json = reader.ReadToEnd();
|
||||||
|
return TryLoad(json, out result);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryLoad(string json, out Dictionary<string, string> result)
|
||||||
{
|
{
|
||||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||||
if (loaded is null)
|
if (loaded is null)
|
||||||
{
|
{
|
||||||
|
result = new Dictionary<string, string>();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_strings = loaded;
|
result = loaded;
|
||||||
CurrentCode = code;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryLoad(string code, string json)
|
||||||
|
{
|
||||||
|
if (TryLoad(json, out var dict))
|
||||||
|
{
|
||||||
|
_strings = dict;
|
||||||
|
CurrentCode = code;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,6 @@ public interface ICpuMemory
|
|||||||
bool TryRead(ulong virtualAddress, Span<byte> destination);
|
bool TryRead(ulong virtualAddress, Span<byte> destination);
|
||||||
|
|
||||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||||
|
|
||||||
|
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ public static class AgcExports
|
|||||||
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
|
||||||
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output),
|
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output, uint Attributes),
|
||||||
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
|
||||||
private static readonly Dictionary<
|
private static readonly Dictionary<
|
||||||
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
|
||||||
@@ -2019,8 +2019,16 @@ public static class AgcExports
|
|||||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
lock (gpuState.Gate)
|
lock (gpuState.Gate)
|
||||||
{
|
{
|
||||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
try
|
||||||
|
{
|
||||||
|
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||||
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2050,27 +2058,35 @@ public static class AgcExports
|
|||||||
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
lock (gpuState.Gate)
|
lock (gpuState.Gate)
|
||||||
{
|
{
|
||||||
for (uint i = 0; i < bufferCount; i++)
|
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
for (uint i = 0; i < bufferCount; i++)
|
||||||
commandAddress == 0 ||
|
|
||||||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
|
||||||
dwordCount == 0)
|
|
||||||
{
|
{
|
||||||
continue;
|
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
||||||
|
commandAddress == 0 ||
|
||||||
|
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
||||||
|
dwordCount == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
|
||||||
|
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracePackets)
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
{
|
}
|
||||||
TraceAgc(
|
finally
|
||||||
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
|
{
|
||||||
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||||
}
|
|
||||||
|
|
||||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2118,8 +2134,16 @@ public static class AgcExports
|
|||||||
gpuState.ComputeQueues.Add(ownerHandle, queueState);
|
gpuState.ComputeQueues.Add(ownerHandle, queueState);
|
||||||
}
|
}
|
||||||
|
|
||||||
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
|
||||||
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
try
|
||||||
|
{
|
||||||
|
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
||||||
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -3418,7 +3442,8 @@ public static class AgcExports
|
|||||||
exportState,
|
exportState,
|
||||||
out var exportEvaluation,
|
out var exportEvaluation,
|
||||||
out error,
|
out error,
|
||||||
resolveVertexInputs: true) ||
|
resolveVertexInputs: true,
|
||||||
|
vertexRecordLimit: indexed ? null : vertexCount) ||
|
||||||
!Gen5ShaderTranslator.TryCreateState(
|
!Gen5ShaderTranslator.TryCreateState(
|
||||||
ctx,
|
ctx,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
@@ -3442,14 +3467,19 @@ public static class AgcExports
|
|||||||
HasPixelColorExport(pixelState, target.Slot))
|
HasPixelColorExport(pixelState, target.Slot))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
|
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
|
||||||
var exportStateFingerprint = ComputeShaderStateFingerprint(exportEvaluation);
|
var attributeCount = GetInterpolatedAttributeCount(pixelState);
|
||||||
var pixelStateFingerprint = ComputeShaderStateFingerprint(pixelEvaluation);
|
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
|
||||||
|
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
|
||||||
var shaderKey = (
|
var shaderKey = (
|
||||||
exportShaderAddress,
|
exportShaderAddress,
|
||||||
exportStateFingerprint,
|
exportStateFingerprint,
|
||||||
pixelShaderAddress,
|
pixelShaderAddress,
|
||||||
pixelStateFingerprint,
|
pixelStateFingerprint,
|
||||||
outputKind);
|
outputKind,
|
||||||
|
attributeCount);
|
||||||
|
var totalGlobalBuffers =
|
||||||
|
pixelEvaluation.GlobalMemoryBindings.Count +
|
||||||
|
exportEvaluation.GlobalMemoryBindings.Count;
|
||||||
(byte[] Vertex, byte[] Pixel) compiled;
|
(byte[] Vertex, byte[] Pixel) compiled;
|
||||||
lock (_submitTraceGate)
|
lock (_submitTraceGate)
|
||||||
{
|
{
|
||||||
@@ -3458,9 +3488,6 @@ public static class AgcExports
|
|||||||
|
|
||||||
if (compiled.Vertex is null || compiled.Pixel is null)
|
if (compiled.Vertex is null || compiled.Pixel is null)
|
||||||
{
|
{
|
||||||
var totalGlobalBuffers =
|
|
||||||
pixelEvaluation.GlobalMemoryBindings.Count +
|
|
||||||
exportEvaluation.GlobalMemoryBindings.Count;
|
|
||||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
if (!Gen5SpirvTranslator.TryCompilePixelShader(
|
||||||
pixelState,
|
pixelState,
|
||||||
pixelEvaluation,
|
pixelEvaluation,
|
||||||
@@ -3468,16 +3495,18 @@ public static class AgcExports
|
|||||||
out var pixelShader,
|
out var pixelShader,
|
||||||
out error,
|
out error,
|
||||||
globalBufferBase: 0,
|
globalBufferBase: 0,
|
||||||
totalGlobalBufferCount: totalGlobalBuffers,
|
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||||
imageBindingBase: 0) ||
|
imageBindingBase: 0,
|
||||||
|
scalarRegisterBufferIndex: totalGlobalBuffers) ||
|
||||||
!Gen5SpirvTranslator.TryCompileVertexShader(
|
!Gen5SpirvTranslator.TryCompileVertexShader(
|
||||||
exportState,
|
exportState,
|
||||||
exportEvaluation,
|
exportEvaluation,
|
||||||
out var vertexShader,
|
out var vertexShader,
|
||||||
out error,
|
out error,
|
||||||
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
|
||||||
totalGlobalBufferCount: totalGlobalBuffers,
|
totalGlobalBufferCount: totalGlobalBuffers + 2,
|
||||||
imageBindingBase: pixelEvaluation.ImageBindings.Count))
|
imageBindingBase: pixelEvaluation.ImageBindings.Count,
|
||||||
|
scalarRegisterBufferIndex: totalGlobalBuffers + 1))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3529,6 +3558,8 @@ public static class AgcExports
|
|||||||
|
|
||||||
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
|
||||||
.Concat(exportEvaluation.GlobalMemoryBindings)
|
.Concat(exportEvaluation.GlobalMemoryBindings)
|
||||||
|
.Append(CreateScalarRegisterBinding(pixelEvaluation))
|
||||||
|
.Append(CreateScalarRegisterBinding(exportEvaluation))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
|
||||||
exportEvaluation.VertexInputs ?? [];
|
exportEvaluation.VertexInputs ?? [];
|
||||||
@@ -3539,7 +3570,7 @@ public static class AgcExports
|
|||||||
primitiveType,
|
primitiveType,
|
||||||
compiled.Vertex,
|
compiled.Vertex,
|
||||||
compiled.Pixel,
|
compiled.Pixel,
|
||||||
GetInterpolatedAttributeCount(pixelState),
|
attributeCount,
|
||||||
vertexCount,
|
vertexCount,
|
||||||
state.InstanceCount,
|
state.InstanceCount,
|
||||||
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
|
||||||
@@ -3651,6 +3682,88 @@ public static class AgcExports
|
|||||||
return (uint)(maxAttribute + 1);
|
return (uint)(maxAttribute + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const int ShaderScalarRegisterCount = 256;
|
||||||
|
|
||||||
|
private static Gen5GlobalMemoryBinding CreateScalarRegisterBinding(
|
||||||
|
Gen5ShaderEvaluation evaluation)
|
||||||
|
{
|
||||||
|
var data = new byte[ShaderScalarRegisterCount * sizeof(uint)];
|
||||||
|
var registers = evaluation.InitialScalarRegisters;
|
||||||
|
var count = Math.Min(registers.Count, ShaderScalarRegisterCount);
|
||||||
|
for (var index = 0; index < count; index++)
|
||||||
|
{
|
||||||
|
var value = registers[index];
|
||||||
|
var offset = index * sizeof(uint);
|
||||||
|
data[offset] = (byte)value;
|
||||||
|
data[offset + 1] = (byte)(value >> 8);
|
||||||
|
data[offset + 2] = (byte)(value >> 16);
|
||||||
|
data[offset + 3] = (byte)(value >> 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Gen5GlobalMemoryBinding(0, 0, [], data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ComputeShaderStructureFingerprint(Gen5ShaderEvaluation evaluation)
|
||||||
|
{
|
||||||
|
const ulong offsetBasis = 14695981039346656037UL;
|
||||||
|
const ulong prime = 1099511628211UL;
|
||||||
|
var hash = offsetBasis;
|
||||||
|
void Mix(ulong value) => hash = (hash ^ value) * prime;
|
||||||
|
|
||||||
|
Mix((ulong)evaluation.GlobalMemoryBindings.Count);
|
||||||
|
foreach (var binding in evaluation.GlobalMemoryBindings)
|
||||||
|
{
|
||||||
|
Mix(binding.ScalarAddress);
|
||||||
|
Mix((ulong)binding.InstructionPcs.Count);
|
||||||
|
foreach (var pc in binding.InstructionPcs)
|
||||||
|
{
|
||||||
|
Mix(pc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Mix((ulong)evaluation.ImageBindings.Count);
|
||||||
|
foreach (var image in evaluation.ImageBindings)
|
||||||
|
{
|
||||||
|
Mix(image.Pc);
|
||||||
|
Mix((ulong)(uint)image.Opcode.GetHashCode());
|
||||||
|
foreach (var word in image.ResourceDescriptor)
|
||||||
|
{
|
||||||
|
Mix(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var word in image.SamplerDescriptor)
|
||||||
|
{
|
||||||
|
Mix(word);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mix(image.MipLevel ?? uint.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaluation.VertexInputs is { } vertexInputs)
|
||||||
|
{
|
||||||
|
Mix((ulong)vertexInputs.Count);
|
||||||
|
foreach (var input in vertexInputs)
|
||||||
|
{
|
||||||
|
Mix(input.Pc);
|
||||||
|
Mix(input.Location);
|
||||||
|
Mix(input.ComponentCount);
|
||||||
|
Mix(input.DataFormat);
|
||||||
|
Mix(input.NumberFormat);
|
||||||
|
Mix(input.Stride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters)
|
||||||
|
{
|
||||||
|
Mix(computeSystemRegisters.WorkGroupXRegister ?? uint.MaxValue);
|
||||||
|
Mix(computeSystemRegisters.WorkGroupYRegister ?? uint.MaxValue);
|
||||||
|
Mix(computeSystemRegisters.WorkGroupZRegister ?? uint.MaxValue);
|
||||||
|
Mix(computeSystemRegisters.ThreadGroupSizeRegister ?? uint.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation)
|
private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation)
|
||||||
{
|
{
|
||||||
const ulong offsetBasis = 14695981039346656037UL;
|
const ulong offsetBasis = 14695981039346656037UL;
|
||||||
|
|||||||
@@ -13,6 +13,24 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
private const int ImageDescriptorDwords = 8;
|
private const int ImageDescriptorDwords = 8;
|
||||||
private const int SamplerDescriptorDwords = 4;
|
private const int SamplerDescriptorDwords = 4;
|
||||||
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
|
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
|
||||||
|
private static readonly int DefaultGlobalMemoryBindingBytes =
|
||||||
|
int.TryParse(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_GLOBAL_BINDING_BYTES"),
|
||||||
|
out var configured) && configured >= sizeof(uint)
|
||||||
|
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
|
||||||
|
: 1 * 1024 * 1024;
|
||||||
|
|
||||||
|
internal static long GlobalMemoryReadCount;
|
||||||
|
internal static long GlobalMemoryReadBytes;
|
||||||
|
internal static long GlobalMemoryReadCacheHits;
|
||||||
|
internal static long GlobalMemoryReadPvmBytes;
|
||||||
|
internal static long GlobalMemoryReadLibcBytes;
|
||||||
|
internal static long GlobalMemoryReadReuses;
|
||||||
|
|
||||||
|
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
|
||||||
|
private static readonly object _crossFrameReadGate = new();
|
||||||
|
private static readonly Dictionary<(ulong BaseAddress, int SizeBytes), byte[]> _crossFrameReadCache = new();
|
||||||
|
private static long _crossFrameReadCacheBytes;
|
||||||
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
|
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
|
||||||
|
|
||||||
private readonly record struct BufferDescriptor(
|
private readonly record struct BufferDescriptor(
|
||||||
@@ -44,7 +62,8 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
Gen5ShaderState state,
|
Gen5ShaderState state,
|
||||||
out Gen5ShaderEvaluation evaluation,
|
out Gen5ShaderEvaluation evaluation,
|
||||||
out string error,
|
out string error,
|
||||||
bool resolveVertexInputs = false)
|
bool resolveVertexInputs = false,
|
||||||
|
uint? vertexRecordLimit = null)
|
||||||
{
|
{
|
||||||
evaluation = default!;
|
evaluation = default!;
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
@@ -255,10 +274,28 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
if (resolveVertexInputs &&
|
if (resolveVertexInputs &&
|
||||||
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
|
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
|
||||||
{
|
{
|
||||||
|
var vertexReadSize = bufferDescriptor.SizeBytes;
|
||||||
|
if (vertexRecordLimit is { } recordLimit &&
|
||||||
|
instruction.Sources.Count > 2 &&
|
||||||
|
TryEvaluateScalarOperand(
|
||||||
|
instruction.Sources[2],
|
||||||
|
scalarRegisters,
|
||||||
|
out var scalarOffset))
|
||||||
|
{
|
||||||
|
var bindingOffset = unchecked((uint)bufferMemory.OffsetBytes + scalarOffset);
|
||||||
|
var requiredBytes =
|
||||||
|
(ulong)bindingOffset +
|
||||||
|
(ulong)(Math.Max(recordLimit, 1u) - 1u) * bufferDescriptor.Stride +
|
||||||
|
(ulong)bufferMemory.DwordCount * sizeof(uint);
|
||||||
|
vertexReadSize = Math.Min(
|
||||||
|
bufferDescriptor.SizeBytes,
|
||||||
|
Math.Max(requiredBytes, sizeof(uint)));
|
||||||
|
}
|
||||||
|
|
||||||
if (!TryReadGlobalMemory(
|
if (!TryReadGlobalMemory(
|
||||||
ctx,
|
ctx,
|
||||||
bufferDescriptor.BaseAddress,
|
bufferDescriptor.BaseAddress,
|
||||||
bufferDescriptor.SizeBytes,
|
vertexReadSize,
|
||||||
out var vertexData))
|
out var vertexData))
|
||||||
{
|
{
|
||||||
error =
|
error =
|
||||||
@@ -533,22 +570,29 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
|
||||||
|
|
||||||
|
internal static void BeginGlobalMemoryReadScope()
|
||||||
|
{
|
||||||
|
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void EndGlobalMemoryReadScope()
|
||||||
|
{
|
||||||
|
_globalMemoryReadCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadGlobalMemory(
|
private static bool TryReadGlobalMemory(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong baseAddress,
|
ulong baseAddress,
|
||||||
out byte[] data)
|
out byte[] data)
|
||||||
{
|
{
|
||||||
for (var size = MaxGlobalMemoryBindingBytes; size >= 4096; size >>= 1)
|
return TryReadGlobalMemory(
|
||||||
{
|
ctx,
|
||||||
data = GC.AllocateUninitializedArray<byte>(size);
|
baseAddress,
|
||||||
if (ctx.Memory.TryRead(baseAddress, data))
|
(ulong)DefaultGlobalMemoryBindingBytes,
|
||||||
{
|
out data);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data = [];
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadGlobalMemory(
|
private static bool TryReadGlobalMemory(
|
||||||
@@ -570,13 +614,74 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cache = _globalMemoryReadCache;
|
||||||
|
var cacheKey = (baseAddress, (int)cappedSize);
|
||||||
|
if (cache is not null && cache.TryGetValue(cacheKey, out var cached))
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref GlobalMemoryReadCacheHits);
|
||||||
|
data = cached;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[]? previous;
|
||||||
|
lock (_crossFrameReadGate)
|
||||||
|
{
|
||||||
|
_crossFrameReadCache.TryGetValue(cacheKey, out previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous is not null && ctx.Memory.TryCompare(baseAddress, previous))
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref GlobalMemoryReadReuses);
|
||||||
|
if (cache is not null)
|
||||||
|
{
|
||||||
|
cache[cacheKey] = previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = previous;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var candidateSize = (int)cappedSize;
|
var candidateSize = (int)cappedSize;
|
||||||
while (candidateSize >= sizeof(uint))
|
while (candidateSize >= sizeof(uint))
|
||||||
{
|
{
|
||||||
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
data = GC.AllocateUninitializedArray<byte>(candidateSize);
|
||||||
if (ctx.Memory.TryRead(baseAddress, data) ||
|
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
|
||||||
|
if (readFromPvm ||
|
||||||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
|
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
|
||||||
{
|
{
|
||||||
|
Interlocked.Increment(ref GlobalMemoryReadCount);
|
||||||
|
Interlocked.Add(ref GlobalMemoryReadBytes, data.Length);
|
||||||
|
if (readFromPvm)
|
||||||
|
{
|
||||||
|
Interlocked.Add(ref GlobalMemoryReadPvmBytes, data.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Interlocked.Add(ref GlobalMemoryReadLibcBytes, data.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache is not null)
|
||||||
|
{
|
||||||
|
cache[cacheKey] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_crossFrameReadGate)
|
||||||
|
{
|
||||||
|
if (_crossFrameReadCache.TryGetValue(cacheKey, out var replaced))
|
||||||
|
{
|
||||||
|
_crossFrameReadCacheBytes -= replaced.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_crossFrameReadCacheBytes + data.Length > CrossFrameReadCacheMaxBytes)
|
||||||
|
{
|
||||||
|
_crossFrameReadCache.Clear();
|
||||||
|
_crossFrameReadCacheBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_crossFrameReadCache[cacheKey] = data;
|
||||||
|
_crossFrameReadCacheBytes += data.Length;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -802,8 +802,15 @@ internal static class Gen5ShaderTranslator
|
|||||||
0x14 => "VCmpxGtF32",
|
0x14 => "VCmpxGtF32",
|
||||||
0x15 => "VCmpxLgF32",
|
0x15 => "VCmpxLgF32",
|
||||||
0x16 => "VCmpxGeF32",
|
0x16 => "VCmpxGeF32",
|
||||||
|
0x17 => "VCmpxOF32",
|
||||||
|
0x18 => "VCmpxUF32",
|
||||||
|
0x19 => "VCmpxNgeF32",
|
||||||
|
0x1A => "VCmpxNlgF32",
|
||||||
|
0x1B => "VCmpxNgtF32",
|
||||||
|
0x1C => "VCmpxNleF32",
|
||||||
0x1D => "VCmpxNeqF32",
|
0x1D => "VCmpxNeqF32",
|
||||||
0x1E => "VCmpxNltF32",
|
0x1E => "VCmpxNltF32",
|
||||||
|
0x1F => "VCmpxTruF32",
|
||||||
0x80 => "VCmpFI32",
|
0x80 => "VCmpFI32",
|
||||||
0x81 => "VCmpLtI32",
|
0x81 => "VCmpLtI32",
|
||||||
0x82 => "VCmpEqI32",
|
0x82 => "VCmpEqI32",
|
||||||
|
|||||||
@@ -861,6 +861,25 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
{
|
{
|
||||||
condition = _module.ConstantBool(true);
|
condition = _module.ConstantBool(true);
|
||||||
}
|
}
|
||||||
|
else if (opcode is "VCmpOF32" or "VCmpxOF32" or "VCmpUF32" or "VCmpxUF32")
|
||||||
|
{
|
||||||
|
// The ordered/unordered predicates only test whether either
|
||||||
|
// operand is NaN. SPIR-V's OpOrdered/OpUnordered are Kernel-only,
|
||||||
|
// so build the same result from OpIsNan, which needs no extra
|
||||||
|
// capability: unordered = isnan(a) || isnan(b), ordered = !that.
|
||||||
|
var left = GetFloatSource(instruction, 0);
|
||||||
|
var right = GetFloatSource(instruction, 1);
|
||||||
|
var nanLeft = _module.AddInstruction(SpirvOp.IsNan, _boolType, left);
|
||||||
|
var nanRight = _module.AddInstruction(SpirvOp.IsNan, _boolType, right);
|
||||||
|
var unordered = _module.AddInstruction(
|
||||||
|
SpirvOp.LogicalOr,
|
||||||
|
_boolType,
|
||||||
|
nanLeft,
|
||||||
|
nanRight);
|
||||||
|
condition = opcode is "VCmpUF32" or "VCmpxUF32"
|
||||||
|
? unordered
|
||||||
|
: _module.AddInstruction(SpirvOp.LogicalNot, _boolType, unordered);
|
||||||
|
}
|
||||||
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
|
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
|
||||||
opcode.EndsWith("F32", StringComparison.Ordinal))
|
opcode.EndsWith("F32", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -875,6 +894,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
"VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual,
|
"VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual,
|
||||||
"VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual,
|
"VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual,
|
||||||
"VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual,
|
"VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual,
|
||||||
|
"VCmpNlgF32" or "VCmpxNlgF32" => SpirvOp.FUnordEqual,
|
||||||
"VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual,
|
"VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual,
|
||||||
"VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan,
|
"VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan,
|
||||||
"VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual,
|
"VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual,
|
||||||
@@ -925,7 +945,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
condition = _module.AddInstruction(operation, _boolType, left, right);
|
condition = _module.AddInstruction(operation, _boolType, left, right);
|
||||||
}
|
}
|
||||||
|
|
||||||
StoreWaveMask(106, condition);
|
// On gfx10, VCmpx writes EXEC only and preserves VCC; the sdst
|
||||||
|
// operand was removed from the cmpx encodings on this generation.
|
||||||
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
|
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var active = _module.AddInstruction(
|
var active = _module.AddInstruction(
|
||||||
@@ -935,6 +956,10 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
condition);
|
condition);
|
||||||
StoreWaveMask(126, active);
|
StoreWaveMask(126, active);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StoreWaveMask(106, condition);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
out string error,
|
out string error,
|
||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0)
|
int imageBindingBase = 0,
|
||||||
|
int scalarRegisterBufferIndex = -1)
|
||||||
{
|
{
|
||||||
var context = new CompilationContext(
|
var context = new CompilationContext(
|
||||||
Gen5SpirvStage.Pixel,
|
Gen5SpirvStage.Pixel,
|
||||||
@@ -30,7 +31,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
1,
|
1,
|
||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase);
|
imageBindingBase,
|
||||||
|
scalarRegisterBufferIndex);
|
||||||
return context.TryCompile(out shader, out error);
|
return context.TryCompile(out shader, out error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +43,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
out string error,
|
out string error,
|
||||||
int globalBufferBase = 0,
|
int globalBufferBase = 0,
|
||||||
int totalGlobalBufferCount = -1,
|
int totalGlobalBufferCount = -1,
|
||||||
int imageBindingBase = 0)
|
int imageBindingBase = 0,
|
||||||
|
int scalarRegisterBufferIndex = -1)
|
||||||
{
|
{
|
||||||
var context = new CompilationContext(
|
var context = new CompilationContext(
|
||||||
Gen5SpirvStage.Vertex,
|
Gen5SpirvStage.Vertex,
|
||||||
@@ -53,7 +56,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
1,
|
1,
|
||||||
globalBufferBase,
|
globalBufferBase,
|
||||||
totalGlobalBufferCount,
|
totalGlobalBufferCount,
|
||||||
imageBindingBase);
|
imageBindingBase,
|
||||||
|
scalarRegisterBufferIndex);
|
||||||
return context.TryCompile(out shader, out error);
|
return context.TryCompile(out shader, out error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +80,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
Math.Max(localSizeZ, 1),
|
Math.Max(localSizeZ, 1),
|
||||||
0,
|
0,
|
||||||
-1,
|
-1,
|
||||||
0);
|
0,
|
||||||
|
-1);
|
||||||
return context.TryCompile(out shader, out error);
|
return context.TryCompile(out shader, out error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +98,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
private readonly int _globalBufferBase;
|
private readonly int _globalBufferBase;
|
||||||
private readonly int _totalGlobalBufferCount;
|
private readonly int _totalGlobalBufferCount;
|
||||||
private readonly int _imageBindingBase;
|
private readonly int _imageBindingBase;
|
||||||
|
private readonly int _scalarRegisterBufferIndex;
|
||||||
private readonly List<uint> _interfaces = [];
|
private readonly List<uint> _interfaces = [];
|
||||||
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
||||||
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
||||||
@@ -167,7 +173,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
uint localSizeZ,
|
uint localSizeZ,
|
||||||
int globalBufferBase,
|
int globalBufferBase,
|
||||||
int totalGlobalBufferCount,
|
int totalGlobalBufferCount,
|
||||||
int imageBindingBase)
|
int imageBindingBase,
|
||||||
|
int scalarRegisterBufferIndex)
|
||||||
{
|
{
|
||||||
_stage = stage;
|
_stage = stage;
|
||||||
_state = state;
|
_state = state;
|
||||||
@@ -181,6 +188,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
? evaluation.GlobalMemoryBindings.Count
|
? evaluation.GlobalMemoryBindings.Count
|
||||||
: totalGlobalBufferCount;
|
: totalGlobalBufferCount;
|
||||||
_imageBindingBase = imageBindingBase;
|
_imageBindingBase = imageBindingBase;
|
||||||
|
_scalarRegisterBufferIndex = scalarRegisterBufferIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryCompile(out Gen5SpirvShader shader, out string error)
|
public bool TryCompile(out Gen5SpirvShader shader, out string error)
|
||||||
@@ -767,15 +775,25 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
|
|
||||||
private void EmitInitialState()
|
private void EmitInitialState()
|
||||||
{
|
{
|
||||||
for (uint index = 0;
|
if (_scalarRegisterBufferIndex >= 0)
|
||||||
index < _evaluation.InitialScalarRegisters.Count &&
|
|
||||||
index < ScalarRegisterCount;
|
|
||||||
index++)
|
|
||||||
{
|
{
|
||||||
var value = _evaluation.InitialScalarRegisters[(int)index];
|
for (uint index = 0; index < ScalarRegisterCount; index++)
|
||||||
if (value != 0)
|
|
||||||
{
|
{
|
||||||
StoreS(index, UInt(value));
|
StoreS(index, LoadBufferWord(_scalarRegisterBufferIndex, UInt(index)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (uint index = 0;
|
||||||
|
index < _evaluation.InitialScalarRegisters.Count &&
|
||||||
|
index < ScalarRegisterCount;
|
||||||
|
index++)
|
||||||
|
{
|
||||||
|
var value = _evaluation.InitialScalarRegisters[(int)index];
|
||||||
|
if (value != 0)
|
||||||
|
{
|
||||||
|
StoreS(index, UInt(value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
@@ -33,6 +34,11 @@ public static class KernelEventFlagCompatExports
|
|||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class EventFlagWaiter
|
||||||
|
{
|
||||||
|
public OrbisGen2Result? Result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "BpFoboUJoZU",
|
Nid = "BpFoboUJoZU",
|
||||||
ExportName = "sceKernelCreateEventFlag",
|
ExportName = "sceKernelCreateEventFlag",
|
||||||
@@ -233,9 +239,47 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
|
if (timeoutUsec == 0)
|
||||||
|
{
|
||||||
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
|
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
|
||||||
|
TimeSpan.FromTicks((long)timeoutUsec * 10L));
|
||||||
|
var timedWaiter = new EventFlagWaiter();
|
||||||
|
if (GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitEventFlag",
|
||||||
|
GetEventFlagWakeKey(handle),
|
||||||
|
resumeHandler: () => CompleteBlockedTimedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
timedWaiter,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress,
|
||||||
|
timeoutAddress,
|
||||||
|
deadline),
|
||||||
|
wakeHandler: () => TryCompleteBlockedTimedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
timedWaiter,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress),
|
||||||
|
blockDeadlineTimestamp: deadline))
|
||||||
|
{
|
||||||
|
state.WaitingThreads++;
|
||||||
|
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +490,92 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryCompleteBlockedTimedWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
EventFlagState state,
|
||||||
|
EventFlagWaiter waiter,
|
||||||
|
ulong pattern,
|
||||||
|
uint waitMode,
|
||||||
|
ulong resultAddress)
|
||||||
|
{
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (waiter.Result is not null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CompleteBlockedTimedWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
EventFlagState state,
|
||||||
|
EventFlagWaiter waiter,
|
||||||
|
ulong pattern,
|
||||||
|
uint waitMode,
|
||||||
|
ulong resultAddress,
|
||||||
|
ulong timeoutAddress,
|
||||||
|
long deadlineTimestamp)
|
||||||
|
{
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (waiter.Result is null)
|
||||||
|
{
|
||||||
|
if (IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
|
{
|
||||||
|
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waiter.Result == 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 (int)waiter.Result.Value;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetEventFlagWakeKey(ulong handle) =>
|
private static string GetEventFlagWakeKey(ulong handle) =>
|
||||||
$"event_flag:0x{handle:X16}";
|
$"event_flag:0x{handle:X16}";
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,27 @@ public static class Ngs2Exports
|
|||||||
LibraryName = "libSceNgs2")]
|
LibraryName = "libSceNgs2")]
|
||||||
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1WsleK-MTkE",
|
||||||
|
ExportName = "sceNgs2GeomCalcListener",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0lbbayqDNoE",
|
||||||
|
ExportName = "sceNgs2GeomResetSourceParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "7Lcfo8SmpsU",
|
||||||
|
ExportName = "sceNgs2GeomResetListenerParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "i0VnXM-C9fc",
|
Nid = "i0VnXM-C9fc",
|
||||||
ExportName = "sceNgs2SystemRender",
|
ExportName = "sceNgs2SystemRender",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public static class PadExports
|
|||||||
private static PadState _cachedInputState;
|
private static PadState _cachedInputState;
|
||||||
|
|
||||||
private static bool _initialized;
|
private static bool _initialized;
|
||||||
|
private static int _controlsAnnouncementLogged;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hv1luiJrqQM",
|
Nid = "hv1luiJrqQM",
|
||||||
@@ -70,11 +71,15 @@ public static class PadExports
|
|||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
DualSenseReader.EnsureStarted();
|
||||||
XInputReader.EnsureStarted();
|
XInputReader.EnsureStarted();
|
||||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
|
||||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
{
|
||||||
: XInputReader.TryGetState(out _)
|
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
||||||
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
||||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
: XInputReader.TryGetState(out _)
|
||||||
|
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
||||||
|
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(PrimaryPadHandle);
|
return ctx.SetReturn(PrimaryPadHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,9 +197,43 @@ public static class PadExports
|
|||||||
LibraryName = "libScePad")]
|
LibraryName = "libScePad")]
|
||||||
public static int PadSetTriggerEffect(CpuContext ctx)
|
public static int PadSetTriggerEffect(CpuContext ctx)
|
||||||
{
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> parameter = stackalloc byte[120];
|
||||||
|
if (!ctx.Memory.TryRead(parameterAddress, parameter))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var triggerMask = parameter[0];
|
||||||
|
XInputReader.SetTriggerRumble(
|
||||||
|
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
|
||||||
|
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte DecodeTriggerVibration(ReadOnlySpan<byte> command)
|
||||||
|
{
|
||||||
|
var mode = BinaryPrimitives.ReadUInt32LittleEndian(command);
|
||||||
|
var amplitude = mode switch
|
||||||
|
{
|
||||||
|
3 when command[10] != 0 => command[9],
|
||||||
|
6 when command[8] != 0 => command[9..19].ToArray().Max(),
|
||||||
|
_ => (byte)0,
|
||||||
|
};
|
||||||
|
return (byte)(Math.Min(amplitude, (byte)8) * 255 / 8);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "yFVnOdGxvZY",
|
Nid = "yFVnOdGxvZY",
|
||||||
ExportName = "scePadSetVibration",
|
ExportName = "scePadSetVibration",
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ internal static class XInputReader
|
|||||||
private static int _slot = -1; // connected XInput user index, -1 when none
|
private static int _slot = -1; // connected XInput user index, -1 when none
|
||||||
private static byte _motorLeft;
|
private static byte _motorLeft;
|
||||||
private static byte _motorRight;
|
private static byte _motorRight;
|
||||||
|
private static byte _triggerLeft;
|
||||||
|
private static byte _triggerRight;
|
||||||
|
|
||||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||||
internal static void EnsureStarted()
|
internal static void EnsureStarted()
|
||||||
@@ -99,6 +101,31 @@ internal static class XInputReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Approximates per-trigger vibration on the two XInput body motors.</summary>
|
||||||
|
internal static void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
if (leftTrigger is { } left)
|
||||||
|
{
|
||||||
|
changed |= _triggerLeft != left;
|
||||||
|
_triggerLeft = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightTrigger is { } right)
|
||||||
|
{
|
||||||
|
changed |= _triggerRight != right;
|
||||||
|
_triggerRight = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
SendRumbleLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void SendRumbleLocked()
|
private static void SendRumbleLocked()
|
||||||
{
|
{
|
||||||
if (_slot < 0)
|
if (_slot < 0)
|
||||||
@@ -108,8 +135,8 @@ internal static class XInputReader
|
|||||||
|
|
||||||
var vibration = new XInputVibration
|
var vibration = new XInputVibration
|
||||||
{
|
{
|
||||||
LeftMotorSpeed = (ushort)(_motorLeft * 257), // 0..255 -> 0..65535
|
LeftMotorSpeed = (ushort)(Math.Max(_motorLeft, _triggerLeft) * 257),
|
||||||
RightMotorSpeed = (ushort)(_motorRight * 257),
|
RightMotorSpeed = (ushort)(Math.Max(_motorRight, _triggerRight) * 257),
|
||||||
};
|
};
|
||||||
_ = XInputSetState((uint)_slot, ref vibration);
|
_ = XInputSetState((uint)_slot, ref vibration);
|
||||||
}
|
}
|
||||||
@@ -147,6 +174,8 @@ internal static class XInputReader
|
|||||||
_slot = -1;
|
_slot = -1;
|
||||||
_motorLeft = 0;
|
_motorLeft = 0;
|
||||||
_motorRight = 0;
|
_motorRight = 0;
|
||||||
|
_triggerLeft = 0;
|
||||||
|
_triggerRight = 0;
|
||||||
_state = default;
|
_state = default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Silk.NET.Vulkan;
|
|||||||
using Silk.NET.Vulkan.Extensions.KHR;
|
using Silk.NET.Vulkan.Extensions.KHR;
|
||||||
using Silk.NET.Vulkan.Extensions.EXT;
|
using Silk.NET.Vulkan.Extensions.EXT;
|
||||||
using Silk.NET.Windowing;
|
using Silk.NET.Windowing;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
@@ -277,6 +278,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TranslatedDraw: null,
|
TranslatedDraw: null,
|
||||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash");
|
Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,6 +312,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TranslatedDraw: null,
|
TranslatedDraw: null,
|
||||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -359,6 +362,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TranslatedDraw: null,
|
TranslatedDraw: null,
|
||||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -429,6 +433,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
renderState ?? VulkanGuestRenderState.Default),
|
renderState ?? VulkanGuestRenderState.Default),
|
||||||
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
RequiredGuestWorkSequence: _enqueuedGuestWorkSequence,
|
||||||
IsSplash: false);
|
IsSplash: false);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -643,6 +648,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
RequiredGuestWorkSequence: 0,
|
RequiredGuestWorkSequence: 0,
|
||||||
IsSplash: false,
|
IsSplash: false,
|
||||||
GuestImageAddress: address);
|
GuestImageAddress: address);
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@@ -883,6 +889,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
_pendingGuestWork.Enqueue(work);
|
_pendingGuestWork.Enqueue(work);
|
||||||
_enqueuedGuestWorkSequence++;
|
_enqueuedGuestWorkSequence++;
|
||||||
|
System.Threading.Monitor.PulseAll(_gate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryTakeGuestWork(out object work)
|
private static bool TryTakeGuestWork(out object work)
|
||||||
@@ -923,6 +930,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private readonly IWindow _window;
|
private readonly IWindow _window;
|
||||||
private const int MaxInFlightGuestSubmissions = 8;
|
private const int MaxInFlightGuestSubmissions = 8;
|
||||||
|
private const double PerformanceHudSampleSeconds = 0.5;
|
||||||
|
private const uint ThreadQueryLimitedInformation = 0x0800;
|
||||||
|
private static readonly bool _performanceHudEnabled =
|
||||||
|
!string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_PERF_HUD"),
|
||||||
|
"0",
|
||||||
|
StringComparison.Ordinal);
|
||||||
private Vk _vk = null!;
|
private Vk _vk = null!;
|
||||||
private KhrSurface _surfaceApi = null!;
|
private KhrSurface _surfaceApi = null!;
|
||||||
private KhrSwapchain _swapchainApi = null!;
|
private KhrSwapchain _swapchainApi = null!;
|
||||||
@@ -961,6 +975,17 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private DeviceMemory _stagingMemory;
|
private DeviceMemory _stagingMemory;
|
||||||
private ulong _stagingSize;
|
private ulong _stagingSize;
|
||||||
private long _presentedSequence;
|
private long _presentedSequence;
|
||||||
|
private long _performanceHudLastTimestamp;
|
||||||
|
private TimeSpan _performanceHudLastProcessCpu;
|
||||||
|
private long _performanceHudPresentedFrames;
|
||||||
|
private long _performanceHudLastPresentedFrames;
|
||||||
|
private long _performanceHudLastReadCount;
|
||||||
|
private long _performanceHudLastReadBytes;
|
||||||
|
private long _performanceHudLastReadHits;
|
||||||
|
private long _performanceHudLastReadPvmBytes;
|
||||||
|
private long _performanceHudLastReadLibcBytes;
|
||||||
|
private readonly Dictionary<int, TimeSpan> _performanceHudThreadCpu = [];
|
||||||
|
private readonly Dictionary<int, string> _performanceHudThreadNames = [];
|
||||||
private bool _vulkanReady;
|
private bool _vulkanReady;
|
||||||
private bool _firstFramePresented;
|
private bool _firstFramePresented;
|
||||||
private bool _firstGuestDrawPresented;
|
private bool _firstGuestDrawPresented;
|
||||||
@@ -1121,9 +1146,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
options.Size = new Vector2D<int>((int)DefaultWindowWidth, (int)DefaultWindowHeight);
|
options.Size = new Vector2D<int>((int)DefaultWindowWidth, (int)DefaultWindowHeight);
|
||||||
options.Title = VideoOutExports.GetWindowTitle();
|
options.Title = VideoOutExports.GetWindowTitle();
|
||||||
options.WindowBorder = WindowBorder.Fixed;
|
options.WindowBorder = WindowBorder.Fixed;
|
||||||
options.VSync = true;
|
// FIFO already provides the presentation clock. Throttling Silk's render loop
|
||||||
options.FramesPerSecond = 60;
|
// as well can miss alternating vblanks and collapse delivery to 30 FPS or less.
|
||||||
options.UpdatesPerSecond = 60;
|
options.VSync = false;
|
||||||
|
options.FramesPerSecond = 0;
|
||||||
|
options.UpdatesPerSecond = 0;
|
||||||
_window = Window.Create(options);
|
_window = Window.Create(options);
|
||||||
_window.Load += Initialize;
|
_window.Load += Initialize;
|
||||||
_window.Render += Render;
|
_window.Render += Render;
|
||||||
@@ -1883,6 +1910,30 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var surfaceFormat = ChooseSurfaceFormat(formats);
|
var surfaceFormat = ChooseSurfaceFormat(formats);
|
||||||
_swapchainFormat = surfaceFormat.Format;
|
_swapchainFormat = surfaceFormat.Format;
|
||||||
_extent = ChooseExtent(capabilities);
|
_extent = ChooseExtent(capabilities);
|
||||||
|
uint presentModeCount = 0;
|
||||||
|
Check(
|
||||||
|
_surfaceApi.GetPhysicalDeviceSurfacePresentModes(
|
||||||
|
_physicalDevice,
|
||||||
|
_surface,
|
||||||
|
&presentModeCount,
|
||||||
|
null),
|
||||||
|
"vkGetPhysicalDeviceSurfacePresentModesKHR");
|
||||||
|
var presentModes = new PresentModeKHR[presentModeCount];
|
||||||
|
fixed (PresentModeKHR* presentModePointer = presentModes)
|
||||||
|
{
|
||||||
|
Check(
|
||||||
|
_surfaceApi.GetPhysicalDeviceSurfacePresentModes(
|
||||||
|
_physicalDevice,
|
||||||
|
_surface,
|
||||||
|
&presentModeCount,
|
||||||
|
presentModePointer),
|
||||||
|
"vkGetPhysicalDeviceSurfacePresentModesKHR");
|
||||||
|
}
|
||||||
|
|
||||||
|
var presentMode = presentModes.Contains(PresentModeKHR.MailboxKhr)
|
||||||
|
? PresentModeKHR.MailboxKhr
|
||||||
|
: PresentModeKHR.FifoKhr;
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Vulkan present mode: {presentMode}");
|
||||||
var imageCount = capabilities.MinImageCount + 1;
|
var imageCount = capabilities.MinImageCount + 1;
|
||||||
if (capabilities.MaxImageCount != 0)
|
if (capabilities.MaxImageCount != 0)
|
||||||
{
|
{
|
||||||
@@ -1906,7 +1957,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ImageSharingMode = SharingMode.Exclusive,
|
ImageSharingMode = SharingMode.Exclusive,
|
||||||
PreTransform = capabilities.CurrentTransform,
|
PreTransform = capabilities.CurrentTransform,
|
||||||
CompositeAlpha = compositeAlpha,
|
CompositeAlpha = compositeAlpha,
|
||||||
PresentMode = PresentModeKHR.FifoKhr,
|
PresentMode = presentMode,
|
||||||
Clipped = true,
|
Clipped = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -5260,6 +5311,219 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private void UpdatePerformanceHud()
|
||||||
|
{
|
||||||
|
if (!_performanceHudEnabled || !OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (_performanceHudLastTimestamp != 0 &&
|
||||||
|
Stopwatch.GetElapsedTime(_performanceHudLastTimestamp, now).TotalSeconds <
|
||||||
|
PerformanceHudSampleSeconds)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = Process.GetCurrentProcess();
|
||||||
|
var processCpu = process.TotalProcessorTime;
|
||||||
|
var currentThreadCpu = new Dictionary<int, TimeSpan>();
|
||||||
|
var currentThreadIds = new HashSet<int>();
|
||||||
|
var hottestThreadId = 0;
|
||||||
|
var hottestThreadCpuSeconds = 0.0;
|
||||||
|
|
||||||
|
foreach (ProcessThread thread in process.Threads)
|
||||||
|
{
|
||||||
|
using (thread)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var threadId = thread.Id;
|
||||||
|
var cpu = thread.TotalProcessorTime;
|
||||||
|
currentThreadIds.Add(threadId);
|
||||||
|
currentThreadCpu[threadId] = cpu;
|
||||||
|
if (_performanceHudThreadCpu.TryGetValue(threadId, out var previousCpu))
|
||||||
|
{
|
||||||
|
var deltaSeconds = Math.Max(0.0, (cpu - previousCpu).TotalSeconds);
|
||||||
|
if (deltaSeconds > hottestThreadCpuSeconds)
|
||||||
|
{
|
||||||
|
hottestThreadCpuSeconds = deltaSeconds;
|
||||||
|
hottestThreadId = threadId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_performanceHudLastTimestamp != 0)
|
||||||
|
{
|
||||||
|
var elapsedSeconds = Math.Max(
|
||||||
|
Stopwatch.GetElapsedTime(_performanceHudLastTimestamp, now).TotalSeconds,
|
||||||
|
0.001);
|
||||||
|
var processCpuPercent = Math.Max(
|
||||||
|
0.0,
|
||||||
|
(processCpu - _performanceHudLastProcessCpu).TotalSeconds /
|
||||||
|
elapsedSeconds /
|
||||||
|
Math.Max(Environment.ProcessorCount, 1) *
|
||||||
|
100.0);
|
||||||
|
var hottestThreadPercent = hottestThreadCpuSeconds / elapsedSeconds * 100.0;
|
||||||
|
var presentedFrames = _performanceHudPresentedFrames;
|
||||||
|
var fps = (presentedFrames - _performanceHudLastPresentedFrames) / elapsedSeconds;
|
||||||
|
var hotName = hottestThreadId == 0
|
||||||
|
? "idle"
|
||||||
|
: GetPerformanceThreadName(hottestThreadId);
|
||||||
|
long guestBacklog;
|
||||||
|
int queuedGuestWork;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
guestBacklog = Math.Max(
|
||||||
|
0,
|
||||||
|
_enqueuedGuestWorkSequence - _completedGuestWorkSequence);
|
||||||
|
queuedGuestWork = _pendingGuestWork.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gpuInFlight = _pendingGuestSubmissions.Count +
|
||||||
|
(_presentationInFlight ? 1 : 0);
|
||||||
|
var readCount = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCount);
|
||||||
|
var readBytes = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadBytes);
|
||||||
|
var readHits = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadCacheHits);
|
||||||
|
var readPvmBytes = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadPvmBytes);
|
||||||
|
var readLibcBytes = Interlocked.Read(
|
||||||
|
ref Agc.Gen5ShaderScalarEvaluator.GlobalMemoryReadLibcBytes);
|
||||||
|
var readsPerSecond =
|
||||||
|
(readCount - _performanceHudLastReadCount) / elapsedSeconds;
|
||||||
|
var readMbPerSecond =
|
||||||
|
(readBytes - _performanceHudLastReadBytes) /
|
||||||
|
elapsedSeconds /
|
||||||
|
(1024.0 * 1024.0);
|
||||||
|
var readHitsPerSecond =
|
||||||
|
(readHits - _performanceHudLastReadHits) / elapsedSeconds;
|
||||||
|
var readPvmMbPerSecond =
|
||||||
|
(readPvmBytes - _performanceHudLastReadPvmBytes) /
|
||||||
|
elapsedSeconds /
|
||||||
|
(1024.0 * 1024.0);
|
||||||
|
var readLibcMbPerSecond =
|
||||||
|
(readLibcBytes - _performanceHudLastReadLibcBytes) /
|
||||||
|
elapsedSeconds /
|
||||||
|
(1024.0 * 1024.0);
|
||||||
|
_performanceHudLastReadCount = readCount;
|
||||||
|
_performanceHudLastReadBytes = readBytes;
|
||||||
|
_performanceHudLastReadHits = readHits;
|
||||||
|
_performanceHudLastReadPvmBytes = readPvmBytes;
|
||||||
|
_performanceHudLastReadLibcBytes = readLibcBytes;
|
||||||
|
_window.Title =
|
||||||
|
$"FPS {fps:0.0} CPU {processCpuPercent:0}% | " +
|
||||||
|
$"HOT {hotName}#{hottestThreadId} {hottestThreadPercent:0}% | " +
|
||||||
|
$"WORK {guestBacklog} (q{queuedGuestWork}/gpu{gpuInFlight}) | " +
|
||||||
|
$"RD {readsPerSecond:0}/s {readMbPerSecond:0}MB/s h{readHitsPerSecond:0}/s " +
|
||||||
|
$"P{readPvmMbPerSecond:0} L{readLibcMbPerSecond:0} | " +
|
||||||
|
VideoOutExports.GetWindowTitle();
|
||||||
|
_performanceHudLastPresentedFrames = presentedFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
_performanceHudThreadCpu.Clear();
|
||||||
|
foreach (var (threadId, cpu) in currentThreadCpu)
|
||||||
|
{
|
||||||
|
_performanceHudThreadCpu[threadId] = cpu;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var staleThreadId in _performanceHudThreadNames.Keys
|
||||||
|
.Where(threadId => !currentThreadIds.Contains(threadId))
|
||||||
|
.ToArray())
|
||||||
|
{
|
||||||
|
_performanceHudThreadNames.Remove(staleThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_performanceHudLastProcessCpu = processCpu;
|
||||||
|
_performanceHudLastTimestamp = now;
|
||||||
|
}
|
||||||
|
catch (Exception exception) when (
|
||||||
|
exception is InvalidOperationException or System.ComponentModel.Win32Exception)
|
||||||
|
{
|
||||||
|
_performanceHudLastTimestamp = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPerformanceThreadName(int threadId)
|
||||||
|
{
|
||||||
|
if (_performanceHudThreadNames.TryGetValue(threadId, out var cached))
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = "tid";
|
||||||
|
var handle = OpenThread(ThreadQueryLimitedInformation, false, (uint)threadId);
|
||||||
|
if (handle != 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (GetThreadDescription(handle, out var description) >= 0 && description != 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var described = Marshal.PtrToStringUni(description);
|
||||||
|
if (!string.IsNullOrWhiteSpace(described))
|
||||||
|
{
|
||||||
|
name = described.Length <= 28 ? described : described[..28];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
LocalFree(description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CloseHandle(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_performanceHudThreadNames[threadId] = name;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern nint OpenThread(uint desiredAccess, bool inheritHandle, uint threadId);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern int GetThreadDescription(nint thread, out nint description);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern nint LocalFree(nint memory);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||||
|
private static extern bool CloseHandle(nint handle);
|
||||||
|
|
||||||
|
private void WaitForRenderWork()
|
||||||
|
{
|
||||||
|
var gpuWorkInFlight = _pendingGuestSubmissions.Count > 0 || _presentationInFlight;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_closed ||
|
||||||
|
_pendingGuestWork.Count > 0 ||
|
||||||
|
(_latestPresentation is { } latest &&
|
||||||
|
latest.Sequence != _presentedSequence &&
|
||||||
|
latest.RequiredGuestWorkSequence <= _completedGuestWorkSequence))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Threading.Monitor.Wait(_gate, gpuWorkInFlight ? 1 : 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void Render(double _)
|
private void Render(double _)
|
||||||
{
|
{
|
||||||
if (!_vulkanReady)
|
if (!_vulkanReady)
|
||||||
@@ -5267,6 +5531,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WaitForRenderWork();
|
||||||
|
UpdatePerformanceHud();
|
||||||
|
|
||||||
_commandBuffer = _presentationCommandBuffer;
|
_commandBuffer = _presentationCommandBuffer;
|
||||||
if (!_deviceLost)
|
if (!_deviceLost)
|
||||||
{
|
{
|
||||||
@@ -5530,6 +5797,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
CheckSwapchainResult(presentResult, "vkQueuePresentKHR");
|
CheckSwapchainResult(presentResult, "vkQueuePresentKHR");
|
||||||
recreateAfterPresent |= presentResult == Result.SuboptimalKhr;
|
recreateAfterPresent |= presentResult == Result.SuboptimalKhr;
|
||||||
VideoOutExports.ReportPresentedFrame();
|
VideoOutExports.ReportPresentedFrame();
|
||||||
|
_performanceHudPresentedFrames++;
|
||||||
if (_swapchainReadbackPending)
|
if (_swapchainReadbackPending)
|
||||||
{
|
{
|
||||||
CompletePendingPresentation(wait: true);
|
CompletePendingPresentation(wait: true);
|
||||||
@@ -6219,59 +6487,34 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
offsets);
|
offsets);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint maxPixelsPerDraw = 512 * 512;
|
var scissor = new Rect2D(
|
||||||
var rowsPerDraw = Math.Max(
|
new Offset2D(drawScissor.X, drawScissor.Y),
|
||||||
1u,
|
new Extent2D(drawScissor.Width, drawScissor.Height));
|
||||||
Math.Min(drawScissor.Height, maxPixelsPerDraw / Math.Max(drawScissor.Width, 1u)));
|
_vk.CmdSetScissor(_commandBuffer, 0, 1, &scissor);
|
||||||
var drawCount = 0u;
|
|
||||||
for (var y = 0u; y < drawScissor.Height; y += rowsPerDraw)
|
if (resources.IndexBuffer.Handle != 0)
|
||||||
{
|
{
|
||||||
var scissor = new Rect2D(
|
_vk.CmdBindIndexBuffer(
|
||||||
new Offset2D(
|
_commandBuffer,
|
||||||
drawScissor.X,
|
resources.IndexBuffer,
|
||||||
checked(drawScissor.Y + (int)y)),
|
0,
|
||||||
new Extent2D(
|
resources.Index32Bit ? IndexType.Uint32 : IndexType.Uint16);
|
||||||
drawScissor.Width,
|
_vk.CmdDrawIndexed(
|
||||||
Math.Min(rowsPerDraw, drawScissor.Height - y)));
|
_commandBuffer,
|
||||||
_vk.CmdSetScissor(_commandBuffer, 0, 1, &scissor);
|
resources.VertexCount,
|
||||||
|
resources.InstanceCount,
|
||||||
if (resources.IndexBuffer.Handle != 0)
|
0,
|
||||||
{
|
0,
|
||||||
_vk.CmdBindIndexBuffer(
|
0);
|
||||||
_commandBuffer,
|
|
||||||
resources.IndexBuffer,
|
|
||||||
0,
|
|
||||||
resources.Index32Bit ? IndexType.Uint32 : IndexType.Uint16);
|
|
||||||
_vk.CmdDrawIndexed(
|
|
||||||
_commandBuffer,
|
|
||||||
resources.VertexCount,
|
|
||||||
resources.InstanceCount,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_vk.CmdDraw(
|
|
||||||
_commandBuffer,
|
|
||||||
resources.VertexCount,
|
|
||||||
resources.InstanceCount,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
|
|
||||||
drawCount++;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (drawCount > 1)
|
|
||||||
{
|
{
|
||||||
TraceVulkanShader(
|
_vk.CmdDraw(
|
||||||
$"vk.graphics_chunked target={extent.Width}x{extent.Height} " +
|
_commandBuffer,
|
||||||
$"draws={drawCount} rows={rowsPerDraw} " +
|
resources.VertexCount,
|
||||||
$"scissor={drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height} " +
|
resources.InstanceCount,
|
||||||
$"viewport={drawViewport.X:0.###},{drawViewport.Y:0.###}," +
|
0,
|
||||||
$"{drawViewport.Width:0.###}x{drawViewport.Height:0.###} " +
|
0);
|
||||||
$"name={resources.DebugName}");
|
|
||||||
}
|
}
|
||||||
_vk.CmdEndRenderPass(_commandBuffer);
|
_vk.CmdEndRenderPass(_commandBuffer);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user