Compare commits

...

9 Commits

Author SHA1 Message Date
brbrhuehue-matrix d2f3511002 Add Brazilian Portuguese translation (#153) 2026-07-15 00:42:44 +03:00
Nolan 90a5d5176f Add Korean (ko-KR) localization (#154) 2026-07-15 00:42:31 +03:00
Nolan 28a43e09c7 Add Japanese (ja) localization (#160) 2026-07-15 00:42:17 +03:00
AlexC 093cfa1f3e Fallback to english if it doesnt find the string in current language (#161) 2026-07-15 00:42:10 +03:00
Spooks d8397b022e Performance Improvements and Optimization Tweaks (#156)
* Improve Gen5 rendering performance and compatibility

* Pin .NET SDK for locked restore

---------

Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
2026-07-14 20:22:52 +03:00
anesr5 85cc2b9892 added french support (#147)
Co-authored-by: anes <anesrachedi@outlook.fr>
2026-07-14 18:06:57 +03:00
AlexC 293194c40b [GUI] Add Spanish language (#148)
* Added localization to spanish language

* Changed Options.Strict.Desc because i didnt like the way i localized it first
2026-07-14 18:06:48 +03:00
tensorcrush 1f09de8896 [AGC] Complete gfx10 v_cmpx_f32 decode and emit ordered/unordered float compares (#122)
* [AGC] Complete gfx10 v_cmpx_f32 decode and emit ordered/unordered float compares

Add the missing v_cmpx_*_f32 VOPC decode entries (0x17-0x1C, 0x1F) and
emission for the ordered/unordered predicates: nlg maps to OpFUnordEqual,
while o/u are lowered from OpIsNan (unordered = isnan(a) || isnan(b),
ordered = !unordered) because SPIR-V's OpOrdered/OpUnordered require the
Kernel capability and are invalid in Vulkan shader modules.

Opcode numbers cross-checked against LLVM's llvm-mc regression tests
(llvm/test/MC/AMDGPU/gfx10_asm_vopc.s, gfx10_asm_vopcx.s); emitted
lowering validated with spirv-val --target-env vulkan1.1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AGC] Write VCC only for non-X vector compares

On gfx10 the VCmpx encodings have no sdst and define EXEC only, so the
unconditional VCC store clobbered VCC on every VCmpx. Move the VCC store
to the non-X path; EXEC keeps the existing old-EXEC & condition update.

Addresses review feedback on #122.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: tensorcrush <tensorcrush@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 18:05:33 +03:00
Dafenx ddc452b4fc [Pad] Approximate trigger vibration on XInput (#140)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-14 18:02:20 +03:00
19 changed files with 1644 additions and 134 deletions
@@ -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;
+129
View File
@@ -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"
}
+129
View File
@@ -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"
}
+129
View File
@@ -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 lidentifiant 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 dexé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 lorsquun 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 dajouter lidentifiant du jeu et lhorodatage.",
"Options.TitleMusic.Label": "Musique du jeu",
"Options.TitleMusic.Desc": "Lire en boucle la musique daperçu du jeu sélectionné dans la bibliothèque.",
"Options.Discord.Label": "Présence Discord",
"Options.Discord.Desc": "Afficher le jeu en cours dexécution sur votre profil Discord.",
"Options.Language.Label": "Langue de l’émulateur",
"Options.Language.Desc": "Langue utilisée dans lensemble 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 dexécution — {0}",
"Launch.Stopping": "Arrêt en cours…",
"Launch.Exited": "Processus terminé avec le code {0} ({1})",
"Launch.ExeNotFound": "Lexécutable SharpEmu est introuvable. Compilez dabord 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 sest terminé avec le code {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "arguments non valides",
"Exit.EbootNotFound": "eboot introuvable",
"Exit.RuntimeException": "exception dexécution",
"Exit.EmulationError": "erreur d’émulation",
"Exit.Unknown": "inconnu",
"Status.EmulatorLocating": "Émulateur : recherche en cours…",
"Status.EmulatorPath": "Émulateur : {0}",
"Status.EmulatorNotFound": "Émulateur : lexécutable SharpEmu est introuvable — compilez dabord 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 douvrir 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 lemplacement du fichier journal",
"Dialog.PlainTextFiles": "Fichiers texte brut",
"Dialog.LogFiles": "Fichiers journaux"
}
+129
View File
@@ -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": "ログファイル"
}
+129
View File
@@ -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": "로그 파일"
}
+100 -8
View File
@@ -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;
}
}
+2
View File
@@ -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;
} }
+146 -33
View File
@@ -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;
} }
+31 -13
View File
@@ -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}";
+21
View File
@@ -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",
+44 -5
View File
@@ -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",
+31 -2
View File
@@ -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);
} }