From 996de70f52e14a1ad9b919d3320b997e75a69fb6 Mon Sep 17 00:00:00 2001
From: Berk <12572227+par274@users.noreply.github.com>
Date: Wed, 29 Jul 2026 15:59:57 +0300
Subject: [PATCH] Game compat updates (#691)
* [agc] Add indirect draws
* [video] Match render target views
* [savedata] Allow dialog reinit
* [ime] Add default profile
* [GUI] update missing translations for profile box
---
src/SharpEmu.GUI/GuiSettings.cs | 21 ++
src/SharpEmu.GUI/Languages/ar.json | 2 +
src/SharpEmu.GUI/Languages/br.json | 2 +
src/SharpEmu.GUI/Languages/de.json | 2 +
src/SharpEmu.GUI/Languages/dk.json | 2 +
src/SharpEmu.GUI/Languages/en.json | 2 +
src/SharpEmu.GUI/Languages/es.json | 2 +
src/SharpEmu.GUI/Languages/fr.json | 2 +
src/SharpEmu.GUI/Languages/hu.json | 2 +
src/SharpEmu.GUI/Languages/it.json | 2 +
src/SharpEmu.GUI/Languages/ja.json | 2 +
src/SharpEmu.GUI/Languages/ko.json | 2 +
src/SharpEmu.GUI/Languages/nl.json | 2 +
src/SharpEmu.GUI/Languages/pt.json | 2 +
src/SharpEmu.GUI/Languages/ru.json | 2 +
src/SharpEmu.GUI/Languages/tr.json | 2 +
src/SharpEmu.GUI/MainWindow.axaml | 10 +
src/SharpEmu.GUI/MainWindow.axaml.cs | 15 +
src/SharpEmu.Libs/Agc/AgcExports.cs | 292 +++++++++++++++++-
src/SharpEmu.Libs/Ime/ImeDialogExports.cs | 175 +++++++++++
.../SaveData/SaveDataDialogExports.cs | 11 +-
.../VideoOut/VulkanVideoPresenter.cs | 61 +++-
22 files changed, 597 insertions(+), 18 deletions(-)
create mode 100644 src/SharpEmu.Libs/Ime/ImeDialogExports.cs
diff --git a/src/SharpEmu.GUI/GuiSettings.cs b/src/SharpEmu.GUI/GuiSettings.cs
index 73e98be2..2634830c 100644
--- a/src/SharpEmu.GUI/GuiSettings.cs
+++ b/src/SharpEmu.GUI/GuiSettings.cs
@@ -45,6 +45,9 @@ public sealed class GuiSettings
/// UI language, matching a file code under Languages/ (e.g. "en", "tr").
public string Language { get; set; } = "en";
+ /// Default text-entry profile exposed to games.
+ public string DefaultProfile { get; set; } = "Sharp";
+
/// Publish launcher/game status to Discord Rich Presence.
public bool DiscordRichPresence { get; set; } = true;
@@ -112,6 +115,18 @@ public sealed class GuiSettings
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles);
settings.LogLevel ??= "Info";
settings.Language ??= "en";
+ var legacyProfile = settings.EnvironmentToggles
+ .Select(entry => entry.Split('=', 2, StringSplitOptions.TrimEntries))
+ .FirstOrDefault(parts =>
+ parts.Length == 2 &&
+ string.Equals(parts[0], "SHARPEMU_DEFAULT_PROFILE", StringComparison.OrdinalIgnoreCase));
+ settings.EnvironmentToggles.RemoveAll(entry =>
+ string.Equals(
+ entry.Split('=', 2, StringSplitOptions.TrimEntries)[0],
+ "SHARPEMU_DEFAULT_PROFILE",
+ StringComparison.OrdinalIgnoreCase));
+ settings.DefaultProfile = NormalizeDefaultProfile(
+ legacyProfile is { Length: 2 } ? legacyProfile[1] : settings.DefaultProfile);
settings.DiscordClientId ??= "1525606762248540221";
if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0)
{
@@ -152,6 +167,12 @@ public sealed class GuiSettings
return $"{width}x{height}";
}
+ internal static string NormalizeDefaultProfile(string? value)
+ {
+ var trimmed = value?.Trim();
+ return string.IsNullOrEmpty(trimmed) ? "Sharp" : trimmed;
+ }
+
public void Save()
{
try
diff --git a/src/SharpEmu.GUI/Languages/ar.json b/src/SharpEmu.GUI/Languages/ar.json
index 83f9f562..115c3155 100644
--- a/src/SharpEmu.GUI/Languages/ar.json
+++ b/src/SharpEmu.GUI/Languages/ar.json
@@ -66,6 +66,8 @@
"Options.Language.Label": "لغة المحاكي",
"Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.",
+ "Options.DefaultProfile.Label": "اسم الملف الشخصي الافتراضي",
+ "Options.DefaultProfile.Desc": "الاسم المستخدم عندما تطلب لعبة إدخال نص. القيمة الافتراضية هي Sharp.",
"Common.On": "تشغيل",
"Common.Off": "إيقاف",
diff --git a/src/SharpEmu.GUI/Languages/br.json b/src/SharpEmu.GUI/Languages/br.json
index 6c9a087b..dcc0e219 100644
--- a/src/SharpEmu.GUI/Languages/br.json
+++ b/src/SharpEmu.GUI/Languages/br.json
@@ -76,6 +76,8 @@
"Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.",
+ "Options.DefaultProfile.Label": "Nome de perfil padrão",
+ "Options.DefaultProfile.Desc": "Nome usado quando um jogo solicita entrada de texto. O padrão é Sharp.",
"Common.On": "Ativado",
"Common.Off": "Desativado",
diff --git a/src/SharpEmu.GUI/Languages/de.json b/src/SharpEmu.GUI/Languages/de.json
index 2b8bffd7..cc936913 100644
--- a/src/SharpEmu.GUI/Languages/de.json
+++ b/src/SharpEmu.GUI/Languages/de.json
@@ -66,6 +66,8 @@
"Options.Language.Label": "Emulator-Sprache",
"Options.Language.Desc": "Sprache der Benutzeroberfläche. Wird sofort angewendet.",
+ "Options.DefaultProfile.Label": "Standardprofilname",
+ "Options.DefaultProfile.Desc": "Name für Texteingaben in Spielen. Der Standardwert ist Sharp.",
"Common.On": "An",
"Common.Off": "Aus",
diff --git a/src/SharpEmu.GUI/Languages/dk.json b/src/SharpEmu.GUI/Languages/dk.json
index ecc2f426..479f8d59 100644
--- a/src/SharpEmu.GUI/Languages/dk.json
+++ b/src/SharpEmu.GUI/Languages/dk.json
@@ -66,6 +66,8 @@
"Options.Language.Label": "Emulatorsprog",
"Options.Language.Desc": "Sprog der bruges i hele launcheren. Anvendes med det samme.",
+ "Options.DefaultProfile.Label": "Standardprofilnavn",
+ "Options.DefaultProfile.Desc": "Navn der bruges, når et spil beder om tekstinput. Standardværdien er Sharp.",
"Common.On": "Til",
"Common.Off": "Fra",
diff --git a/src/SharpEmu.GUI/Languages/en.json b/src/SharpEmu.GUI/Languages/en.json
index fc5cc2a8..8a59c75e 100644
--- a/src/SharpEmu.GUI/Languages/en.json
+++ b/src/SharpEmu.GUI/Languages/en.json
@@ -38,6 +38,8 @@
"Options.Env.LogIo.Desc": "Log file open, read, and path-resolve activity to the console.\nUse when a game cannot find its data files during boot.",
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
"Options.Env.GuestImageCpuSync.Desc": "Re-upload guest surfaces the game's own CPU code rewrites.\nLeave off normally. Turn on for titles whose CPU-drawn surfaces never reach the screen.\nCosts performance and regresses some titles, such as GTA V.",
+ "Options.DefaultProfile.Label": "Default profile name",
+ "Options.DefaultProfile.Desc": "Name used when a game asks for text input. Defaults to Sharp.",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
diff --git a/src/SharpEmu.GUI/Languages/es.json b/src/SharpEmu.GUI/Languages/es.json
index 817becee..53d6d473 100644
--- a/src/SharpEmu.GUI/Languages/es.json
+++ b/src/SharpEmu.GUI/Languages/es.json
@@ -66,6 +66,8 @@
"Options.Language.Label": "Idioma del emulador",
"Options.Language.Desc": "Idioma utilizado en todo el launcher. Se aplica inmediatamente.",
+ "Options.DefaultProfile.Label": "Nombre de perfil predeterminado",
+ "Options.DefaultProfile.Desc": "Nombre utilizado cuando un juego solicita introducir texto. El valor predeterminado es Sharp.",
"Common.On": "Encendido",
"Common.Off": "Apagado",
diff --git a/src/SharpEmu.GUI/Languages/fr.json b/src/SharpEmu.GUI/Languages/fr.json
index 8220c165..5686ed9c 100644
--- a/src/SharpEmu.GUI/Languages/fr.json
+++ b/src/SharpEmu.GUI/Languages/fr.json
@@ -76,6 +76,8 @@
"Options.Language.Label": "Langue de l’émulateur",
"Options.Language.Desc": "Langue utilisée dans l’ensemble du lanceur. Le changement est immédiat.",
+ "Options.DefaultProfile.Label": "Nom de profil par défaut",
+ "Options.DefaultProfile.Desc": "Nom utilisé lorsqu’un jeu demande une saisie de texte. La valeur par défaut est Sharp.",
"Common.On": "Activé",
"Common.Off": "Désactivé",
diff --git a/src/SharpEmu.GUI/Languages/hu.json b/src/SharpEmu.GUI/Languages/hu.json
index fd404f28..b2594ee0 100644
--- a/src/SharpEmu.GUI/Languages/hu.json
+++ b/src/SharpEmu.GUI/Languages/hu.json
@@ -76,6 +76,8 @@
"Options.Language.Label": "Emulátor nyelve",
"Options.Language.Desc": "Az indítóban használt nyelv. Azonnal érvénybe lép.",
+ "Options.DefaultProfile.Label": "Alapértelmezett profilnév",
+ "Options.DefaultProfile.Desc": "A játékok szövegbeviteli kéréseinél használt név. Az alapértelmezett érték Sharp.",
"Common.On": "Be",
"Common.Off": "Ki",
diff --git a/src/SharpEmu.GUI/Languages/it.json b/src/SharpEmu.GUI/Languages/it.json
index 30b3f5df..216dda7f 100644
--- a/src/SharpEmu.GUI/Languages/it.json
+++ b/src/SharpEmu.GUI/Languages/it.json
@@ -69,6 +69,8 @@
"Options.Language.Label": "Lingua dell'emulatore",
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
+ "Options.DefaultProfile.Label": "Nome profilo predefinito",
+ "Options.DefaultProfile.Desc": "Nome usato quando un gioco richiede l'inserimento di testo. Il valore predefinito è Sharp.",
"Common.On": "On",
"Common.Off": "Off",
diff --git a/src/SharpEmu.GUI/Languages/ja.json b/src/SharpEmu.GUI/Languages/ja.json
index e5cf1b61..80a5afab 100644
--- a/src/SharpEmu.GUI/Languages/ja.json
+++ b/src/SharpEmu.GUI/Languages/ja.json
@@ -66,6 +66,8 @@
"Options.Language.Label": "エミュレータの言語",
"Options.Language.Desc": "ランチャー全体で使用される言語。変更はすぐに適用されます。",
+ "Options.DefaultProfile.Label": "既定のプロフィール名",
+ "Options.DefaultProfile.Desc": "ゲームがテキスト入力を要求したときに使用する名前です。既定値は Sharp です。",
"Common.On": "オン",
"Common.Off": "オフ",
diff --git a/src/SharpEmu.GUI/Languages/ko.json b/src/SharpEmu.GUI/Languages/ko.json
index fa64dd09..b66deee8 100644
--- a/src/SharpEmu.GUI/Languages/ko.json
+++ b/src/SharpEmu.GUI/Languages/ko.json
@@ -66,6 +66,8 @@
"Options.Language.Label": "에뮬레이터 언어",
"Options.Language.Desc": "런처 전체에 사용되는 언어입니다. 변경 사항은 즉시 적용됩니다.",
+ "Options.DefaultProfile.Label": "기본 프로필 이름",
+ "Options.DefaultProfile.Desc": "게임에서 텍스트 입력을 요청할 때 사용할 이름입니다. 기본값은 Sharp입니다.",
"Common.On": "켬",
"Common.Off": "끔",
diff --git a/src/SharpEmu.GUI/Languages/nl.json b/src/SharpEmu.GUI/Languages/nl.json
index 1d8a60fd..424ec6c3 100644
--- a/src/SharpEmu.GUI/Languages/nl.json
+++ b/src/SharpEmu.GUI/Languages/nl.json
@@ -66,6 +66,8 @@
"Options.Language.Label": "Taal van de emulator",
"Options.Language.Desc": "Taal die in de hele launcher wordt gebruikt. Wordt direct toegepast.",
+ "Options.DefaultProfile.Label": "Standaardprofielnaam",
+ "Options.DefaultProfile.Desc": "Naam die wordt gebruikt wanneer een game om tekstinvoer vraagt. De standaardwaarde is Sharp.",
"Common.On": "Aan",
"Common.Off": "Uit",
diff --git a/src/SharpEmu.GUI/Languages/pt.json b/src/SharpEmu.GUI/Languages/pt.json
index d764259f..f8e93d9c 100644
--- a/src/SharpEmu.GUI/Languages/pt.json
+++ b/src/SharpEmu.GUI/Languages/pt.json
@@ -76,6 +76,8 @@
"Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.",
+ "Options.DefaultProfile.Label": "Nome de perfil predefinido",
+ "Options.DefaultProfile.Desc": "Nome utilizado quando um jogo solicita a introdução de texto. O valor predefinido é Sharp.",
"Common.On": "Ativado",
"Common.Off": "Desativado",
diff --git a/src/SharpEmu.GUI/Languages/ru.json b/src/SharpEmu.GUI/Languages/ru.json
index 6d3ff135..423cb206 100644
--- a/src/SharpEmu.GUI/Languages/ru.json
+++ b/src/SharpEmu.GUI/Languages/ru.json
@@ -109,6 +109,8 @@
"Options.Language.Label": "Язык эмулятора",
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
+ "Options.DefaultProfile.Label": "Имя профиля по умолчанию",
+ "Options.DefaultProfile.Desc": "Имя, используемое, когда игра запрашивает ввод текста. Значение по умолчанию — Sharp.",
"Common.On": "Включено",
"Common.Off": "Выключено",
diff --git a/src/SharpEmu.GUI/Languages/tr.json b/src/SharpEmu.GUI/Languages/tr.json
index aa6fa458..3a26330a 100644
--- a/src/SharpEmu.GUI/Languages/tr.json
+++ b/src/SharpEmu.GUI/Languages/tr.json
@@ -173,6 +173,8 @@
"Options.Env.LogIo.Desc": "Dosya açma, okuma ve yol çözümleme etkinliğini konsola günlükle.\nBir oyun açılışta veri dosyalarını bulamıyorsa kullanın.",
"Options.Env.LogNp.Desc": "NP (PlayStation Network) kütüphane çağrılarını konsola günlükle.",
"Options.Env.GuestImageCpuSync.Desc": "Oyunun kendi CPU kodunun yeniden yazdığı misafir yüzeyleri tekrar yükler.\nNormalde kapalı bırakın. CPU ile çizilen yüzeyleri ekrana ulaşmayan oyunlarda açın.\nPerformansa mal olur ve GTA V gibi bazı oyunlarda soruna yol açar.",
+ "Options.DefaultProfile.Label": "Varsayilan profil adi",
+ "Options.DefaultProfile.Desc": "Oyun metin girisi istediginde kullanilacak ad. Varsayilan deger Sharp'tir.",
"Common.Save": "Kaydet",
"Common.Cancel": "İptal",
"PerGame.Title": "Oyuna özel ayarlar — {0} ({1})",
diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml
index f398c3a8..74ead40a 100644
--- a/src/SharpEmu.GUI/MainWindow.axaml
+++ b/src/SharpEmu.GUI/MainWindow.axaml
@@ -346,6 +346,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
+
+
+
+
@@ -646,6 +655,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
VerticalAlignment="Center" />
+
diff --git a/src/SharpEmu.GUI/MainWindow.axaml.cs b/src/SharpEmu.GUI/MainWindow.axaml.cs
index 086a5dca..8188e75a 100644
--- a/src/SharpEmu.GUI/MainWindow.axaml.cs
+++ b/src/SharpEmu.GUI/MainWindow.axaml.cs
@@ -298,6 +298,8 @@ public partial class MainWindow : Window
SetEnvironmentToggle(
"SHARPEMU_GUEST_IMAGE_CPU_SYNC",
EnvGuestImageCpuSyncToggle.IsChecked == true);
+ DefaultProfileBox.TextChanged += (_, _) =>
+ _settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
@@ -883,6 +885,7 @@ public partial class MainWindow : Window
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
EnvGuestImageCpuSyncToggle.IsChecked =
_settings.EnvironmentToggles.Contains("SHARPEMU_GUEST_IMAGE_CPU_SYNC");
+ DefaultProfileBox.Text = _settings.DefaultProfile;
WindowModeBox.SelectedIndex = ChoiceIndex(_settings.WindowMode, "Windowed", "Borderless", "Exclusive");
LoadHostDisplayOptions();
ScalingModeBox.SelectedIndex = ChoiceIndex(_settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
@@ -1109,6 +1112,8 @@ public partial class MainWindow : Window
}
}
+ private const string DefaultProfileEnvironmentName = "SHARPEMU_DEFAULT_PROFILE";
+
private string SelectedLogLevel()
{
return LogLevelBox.SelectedIndex switch
@@ -2006,10 +2011,20 @@ public partial class MainWindow : Window
continue;
}
+ if (string.Equals(name, DefaultProfileEnvironmentName, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
Environment.SetEnvironmentVariable(name, value);
_appliedEnvironmentVariables.Add(name);
}
+ Environment.SetEnvironmentVariable(
+ DefaultProfileEnvironmentName,
+ GuiSettings.NormalizeDefaultProfile(_settings.DefaultProfile));
+ _appliedEnvironmentVariables.Add(DefaultProfileEnvironmentName);
+
Environment.SetEnvironmentVariable(
"SHARPEMU_RENDER_SCALE",
_settings.RenderResolutionScale.ToString(
diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs
index cc919c0e..0af15bce 100644
--- a/src/SharpEmu.Libs/Agc/AgcExports.cs
+++ b/src/SharpEmu.Libs/Agc/AgcExports.cs
@@ -46,6 +46,9 @@ public static partial class AgcExports
private const uint ItNumInstances = 0x2F;
private const uint ItDrawIndexMultiAuto = 0x30;
private const uint ItDrawIndexOffset2 = 0x35;
+ private const uint ItDrawIndexIndirectMulti = 0x38;
+ private const uint DrawIndexedIndirectArgsSize = 20;
+ private const uint DrawIndexedIndirectMaxScan = 1024;
private const uint ItWriteData = 0x37;
private const uint ItDispatchDirect = 0x15;
private const uint ItDispatchIndirect = 0x16;
@@ -319,6 +322,7 @@ public static partial class AgcExports
private static int _tracedVertexRangeCount;
private static long _dcbWaitRegMemTraceCount;
private static long _createShaderTraceCount;
+ private static long _duplicateTargetTraceCount;
private static long _cbMetadataSkipTraceCount;
private static long _packetPayloadTraceCount;
private static bool _tracedMissingPixelShaderBindings;
@@ -1994,6 +1998,65 @@ public static partial class AgcExports
return ReturnPointer(ctx, drawCommand);
}
+ [SysAbiExport(
+ Nid = "1q1titRBL6o",
+ ExportName = "sceAgcDcbDrawIndirect",
+ Target = Generation.Gen5,
+ LibraryName = "libSceAgc")]
+ public static int DcbDrawIndirect(CpuContext ctx)
+ {
+ var commandBufferAddress = ctx[CpuRegister.Rdi];
+ var dataOffset = (uint)ctx[CpuRegister.Rsi];
+ var emit = Interlocked.Increment(ref _indirectDrawEmitCount);
+
+ if (emit <= 12 || emit % 250 == 0)
+ {
+ var rcx = ctx[CpuRegister.Rcx];
+ var dump = string.Empty;
+ for (var word = 0; word < 8; word++)
+ {
+ dump += TryReadUInt32(ctx, rcx + dataOffset + ((ulong)word * 4), out var raw)
+ ? $" {raw}"
+ : " ?";
+ }
+
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] agc.emit_indirect#{emit} buf=0x{commandBufferAddress:X16} " +
+ $"off=0x{dataOffset:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{rcx:X} " +
+ $"r8=0x{ctx[CpuRegister.R8]:X} rcx_words:{dump}");
+ }
+
+ if (commandBufferAddress == 0)
+ {
+ Interlocked.Increment(ref _indirectDrawEmitRejectCount);
+ return ReturnPointer(ctx, 0);
+ }
+
+ if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var drawCommand) ||
+ !TryWriteUInt32(ctx, drawCommand, Pm4(5, ItDrawIndirect, 0)) ||
+ !TryWriteUInt32(ctx, drawCommand + 4, dataOffset) ||
+ !TryWriteUInt32(ctx, drawCommand + 8, 0) ||
+ !TryWriteUInt32(ctx, drawCommand + 12, 0) ||
+ !TryWriteUInt32(ctx, drawCommand + 16, 0))
+ {
+ var rejects = Interlocked.Increment(ref _indirectDrawEmitRejectCount);
+ if (rejects <= 8 || rejects % 250 == 0)
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] agc.emit_indirect_reject#{rejects} " +
+ $"buf=0x{commandBufferAddress:X16} reason=alloc_or_write");
+ }
+
+ return ReturnPointer(ctx, 0);
+ }
+
+ TraceAgc(
+ $"agc.dcb_draw_indirect buf=0x{commandBufferAddress:X16} " +
+ $"draw=0x{drawCommand:X16} offset=0x{dataOffset:X}");
+
+ return ReturnPointer(ctx, drawCommand);
+ }
+
[SysAbiExport(
Nid = "Yw0jKSqop+E",
ExportName = "sceAgcDcbDrawIndexAuto",
@@ -2052,6 +2115,39 @@ public static partial class AgcExports
return ReturnPointer(ctx, commandAddress);
}
+ [SysAbiExport(
+ Nid = "ypVBz4uPKcQ",
+ ExportName = "sceAgcDcbDrawIndexIndirectMulti",
+ Target = Generation.Gen5,
+ LibraryName = "libSceAgc")]
+ public static int DcbDrawIndexIndirectMulti(CpuContext ctx)
+ {
+ var commandBufferAddress = ctx[CpuRegister.Rdi];
+ var dataOffset = (uint)ctx[CpuRegister.Rsi];
+ var drawCount = (uint)ctx[CpuRegister.Rdx];
+ var stride = DrawIndexedIndirectArgsSize;
+ var modifier = (uint)ctx[CpuRegister.R8];
+ if (commandBufferAddress == 0 ||
+ !TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) ||
+ !TryWriteUInt32(ctx, commandAddress, Pm4(8, ItDrawIndexIndirectMulti, 0)) ||
+ !TryWriteUInt32(ctx, commandAddress + 4, dataOffset) ||
+ !TryWriteUInt32(ctx, commandAddress + 8, 0) ||
+ !TryWriteUInt32(ctx, commandAddress + 12, 0) ||
+ !TryWriteUInt32(ctx, commandAddress + 16, 0) ||
+ !TryWriteUInt32(ctx, commandAddress + 20, drawCount) ||
+ !TryWriteUInt32(ctx, commandAddress + 24, stride) ||
+ !TryWriteUInt32(ctx, commandAddress + 28, modifier))
+ {
+ return ReturnPointer(ctx, 0);
+ }
+
+ TraceAgc(
+ $"agc.dcb_draw_index_indirect_multi buf=0x{commandBufferAddress:X16} " +
+ $"cmd=0x{commandAddress:X16} offset=0x{dataOffset:X8} draws={drawCount} " +
+ $"stride={stride} modifier=0x{modifier:X8}");
+ return ReturnPointer(ctx, commandAddress);
+ }
+
[SysAbiExport(
Nid = "mStuvI0zOtc",
ExportName = "sceAgcDcbDrawIndexIndirectGetSize",
@@ -3812,6 +3908,7 @@ public static partial class AgcExports
if (TryReadSubmittedDrawCount(
ctx,
+ gpuState,
state,
currentAddress,
length,
@@ -3835,7 +3932,8 @@ public static partial class AgcExports
var indexed = op is
ItDrawIndex2 or
ItDrawIndexOffset2 or
- ItDrawIndexIndirect;
+ ItDrawIndexIndirect or
+ ItDrawIndexIndirectMulti;
state.SawIndexedDraw |= indexed;
TryTranslateGuestDraw(ctx, gpuState, state, indexCount, indexed);
}
@@ -4195,6 +4293,7 @@ public static partial class AgcExports
op is ItDispatchDirect or ItDispatchIndirect ||
op is ItDrawIndirect or
ItDrawIndexIndirect or
+ ItDrawIndexIndirectMulti or
ItDrawIndex2 or
ItDrawIndexAuto or
ItDrawIndexMultiAuto or
@@ -6164,8 +6263,24 @@ public static partial class AgcExports
}
}
+ private const int IndirectArgsFlushTimeoutMilliseconds = 250;
+
+ private static void FlushGpuWorkForIndirectArgs(SubmittedGpuState gpuState)
+ {
+ var pending = gpuState.WorkSequence;
+ if (pending == 0 || pending > long.MaxValue)
+ {
+ return;
+ }
+
+ GuestGpu.Current.WaitForGuestWork(
+ (long)pending,
+ IndirectArgsFlushTimeoutMilliseconds);
+ }
+
private static bool TryReadSubmittedDrawCount(
CpuContext ctx,
+ SubmittedGpuState gpuState,
SubmittedDcbState state,
ulong packetAddress,
uint packetLength,
@@ -6196,17 +6311,89 @@ public static partial class AgcExports
drawCount = (control >> 21) & 0x7FFu;
return true;
- case ItDrawIndirect or ItDrawIndexIndirect
- when packetLength >= 5 && state.IndirectArgsAddress != 0:
- if (!TryReadUInt32(ctx, packetAddress + 4, out var dataOffset))
+ case ItDrawIndexIndirectMulti when packetLength >= 8 &&
+ state.IndirectArgsAddress != 0:
+ if (!TryReadUInt32(ctx, packetAddress + 4, out var multiOffset) ||
+ !TryReadUInt32(ctx, packetAddress + 20, out var multiDraws) ||
+ !TryReadUInt32(ctx, packetAddress + 24, out var multiStride))
{
return false;
}
- return TryReadUInt32(
- ctx,
- state.IndirectArgsAddress + dataOffset,
- out drawCount);
+
+ if (multiStride < DrawIndexedIndirectArgsSize)
+ {
+ multiStride = DrawIndexedIndirectArgsSize;
+ }
+
+ var multiTotal = 0UL;
+ var multiCapped = multiDraws == 0
+ ? DrawIndexedIndirectMaxScan
+ : Math.Min(multiDraws, 4096u);
+ for (var draw = 0u; draw < multiCapped; draw++)
+ {
+ if (!TryReadUInt32(
+ ctx,
+ state.IndirectArgsAddress + multiOffset + ((ulong)draw * multiStride),
+ out var subCount))
+ {
+ break;
+ }
+
+ if (subCount == 0 && multiDraws == 0)
+ {
+ break;
+ }
+
+ multiTotal += subCount;
+ }
+
+ var multiProbe = Interlocked.Increment(ref _indirectMultiProbeCount);
+ if (multiProbe <= 12 || multiProbe % 250 == 0)
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] agc.draw_multi#{multiProbe} args=0x{state.IndirectArgsAddress:X} " +
+ $"off=0x{multiOffset:X} draws={multiDraws} stride={multiStride} " +
+ $"total={multiTotal}");
+ }
+
+ drawCount = (uint)Math.Min(multiTotal, uint.MaxValue);
+ return drawCount != 0;
+ case ItDrawIndirect or ItDrawIndexIndirect:
+ var probe = Interlocked.Increment(ref _indirectDrawProbeCount);
+ if (!TryReadUInt32(ctx, packetAddress + 4, out var dataOffset))
+ {
+ dataOffset = 0xFFFFFFFFu;
+ }
+
+ var readable = packetLength >= 5 &&
+ state.IndirectArgsAddress != 0 &&
+ dataOffset != 0xFFFFFFFFu;
+ var resolved = readable &&
+ TryReadUInt32(
+ ctx,
+ state.IndirectArgsAddress + dataOffset,
+ out drawCount);
+ if (probe <= 12 || probe % 100 == 0)
+ {
+ var dump = string.Empty;
+ for (var word = 0; word < 8; word++)
+ {
+ dump += TryReadUInt32(
+ ctx,
+ state.IndirectArgsAddress + dataOffset + ((ulong)word * 4),
+ out var raw)
+ ? $" {raw}"
+ : " ?";
+ }
+
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] agc.draw_indirect#{probe} op=0x{op:X} len={packetLength} " +
+ $"args=0x{state.IndirectArgsAddress:X} off=0x{dataOffset:X} " +
+ $"resolved={resolved} count={drawCount} words:{dump}");
+ }
+
+ return resolved;
default:
return false;
}
@@ -8056,6 +8243,61 @@ public static partial class AgcExports
source.Format == destination.Format;
}
+ private static readonly HashSet _renderTargetAddresses = new();
+ private static readonly HashSet _sampledRenderTargets = new();
+ private static readonly object _renderTargetProbeGate = new();
+ private static long _renderTargetSampleTraceCount;
+ private static long _indirectDrawProbeCount;
+ private static long _indirectDrawEmitCount;
+ private static long _indirectDrawEmitRejectCount;
+ private static long _indirectMultiProbeCount;
+
+ private static void NoteRenderTargetAddress(ulong address)
+ {
+ if (address == 0)
+ {
+ return;
+ }
+
+ lock (_renderTargetProbeGate)
+ {
+ if (_renderTargetAddresses.Count < 512)
+ {
+ _renderTargetAddresses.Add(address);
+ }
+ }
+ }
+
+ private static void NoteSampledAddress(ulong address, uint format = 0, uint numberType = 0)
+ {
+ if (address == 0)
+ {
+ return;
+ }
+
+ bool firstTime;
+ int distinctTargets;
+ lock (_renderTargetProbeGate)
+ {
+ if (!_renderTargetAddresses.Contains(address))
+ {
+ return;
+ }
+
+ firstTime = _sampledRenderTargets.Add(address);
+ distinctTargets = _renderTargetAddresses.Count;
+ }
+
+ var count = Interlocked.Increment(ref _renderTargetSampleTraceCount);
+ if (firstTime || count % 2000 == 0)
+ {
+ var gpuResident = GuestGpu.Current.IsGpuGuestImageAvailable(address, format, numberType);
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] agc.rt_sampled#{count} addr=0x{address:X} first={firstTime} " +
+ $"gpu_resident={gpuResident} fmt={format}/{numberType} known_targets={distinctTargets}");
+ }
+ }
+
private static IReadOnlyList GetRenderTargets(
IReadOnlyDictionary registers,
bool includeMaskedTargets = false)
@@ -8082,6 +8324,13 @@ public static partial class AgcExports
continue;
}
+ if (targets.Exists(existing => existing.Address == address))
+ {
+ continue;
+ }
+
+ NoteRenderTargetAddress(address);
+
targets.Add(new RenderTargetDescriptor(
slot,
address,
@@ -8092,6 +8341,21 @@ public static partial class AgcExports
(attrib3 >> 14) & 0x1Fu));
}
+ if (targets.Count > 1 &&
+ targets.Select(t => t.Address).Distinct().Count() != targets.Count)
+ {
+ var dupCount = Interlocked.Increment(ref _duplicateTargetTraceCount);
+ if (dupCount <= 12 || dupCount % 500 == 0)
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] agc.rt_duplicate#{dupCount} has_mask={hasTargetMask} " +
+ $"mask=0x{targetMask:X8} slots=[" +
+ string.Join(",", targets.Select(t =>
+ $"{t.Slot}:0x{t.Address:X}:m{(targetMask >> ((int)t.Slot * 4)) & 0xFu}")) +
+ "]");
+ }
+ }
+
return targets;
}
@@ -9331,6 +9595,7 @@ public static partial class AgcExports
descriptor.Format,
descriptor.NumberType))
{
+ NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
@@ -9410,6 +9675,7 @@ public static partial class AgcExports
$"tile={descriptor.TileMode} mip={mipLevel}");
}
+ NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
@@ -9470,6 +9736,7 @@ public static partial class AgcExports
descriptor.Type,
textureDepth)))
{
+ NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
@@ -9532,7 +9799,8 @@ public static partial class AgcExports
if (readAllLayers)
{
- texture = new GuestDrawTexture(
+ NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
+ texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
@@ -9594,7 +9862,8 @@ public static partial class AgcExports
if (uploadedLayers == arrayLayers)
{
- texture = new GuestDrawTexture(
+ NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
+ texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
@@ -9697,7 +9966,8 @@ public static partial class AgcExports
if (IsGpuDetileEquation(gpuDetileParams.Equation) &&
(long)elementsWide * elementsHigh * bytesPerElement <= source.Length)
{
- texture = new GuestDrawTexture(
+ NoteSampledAddress(descriptor.Address, descriptor.Format, descriptor.NumberType);
+ texture = new GuestDrawTexture(
descriptor.Address,
descriptor.Width,
descriptor.Height,
diff --git a/src/SharpEmu.Libs/Ime/ImeDialogExports.cs b/src/SharpEmu.Libs/Ime/ImeDialogExports.cs
new file mode 100644
index 00000000..bd756b9a
--- /dev/null
+++ b/src/SharpEmu.Libs/Ime/ImeDialogExports.cs
@@ -0,0 +1,175 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System;
+using System.Buffers.Binary;
+using System.Text;
+using SharpEmu.HLE;
+
+namespace SharpEmu.Libs.Ime;
+
+public static class ImeDialogExports
+{
+ private const int StatusNone = 0;
+ private const int StatusRunning = 1;
+ private const int StatusFinished = 2;
+
+ private const int EndStatusOk = 0;
+
+ private const ulong ParamMaxTextLengthOffset = 0x24;
+ private const ulong ParamInputTextBufferOffset = 0x28;
+
+ private const int ImeDialogErrorInvalidAddress = unchecked((int)0x80BC0001);
+
+ private const string DefaultInputText = "Sharp";
+
+ private static readonly object _gate = new();
+ private static int _status = StatusNone;
+
+ [SysAbiExport(
+ Nid = "NUeBrN7hzf0",
+ ExportName = "sceImeDialogInit",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceImeDialog")]
+ public static int ImeDialogInit(CpuContext ctx)
+ {
+ var parameterAddress = ctx[CpuRegister.Rdi];
+ if (parameterAddress == 0)
+ {
+ return SetReturn(ctx, ImeDialogErrorInvalidAddress);
+ }
+
+ TryWriteInputText(ctx, parameterAddress);
+
+ lock (_gate)
+ {
+ _status = StatusFinished;
+ }
+
+ return SetReturn(ctx, 0);
+ }
+
+ [SysAbiExport(
+ Nid = "IADmD4tScBY",
+ ExportName = "sceImeDialogGetStatus",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceImeDialog")]
+ public static int ImeDialogGetStatus(CpuContext ctx)
+ {
+ int status;
+ lock (_gate)
+ {
+ status = _status;
+ }
+
+ ctx[CpuRegister.Rax] = unchecked((ulong)(long)status);
+ return status;
+ }
+
+ [SysAbiExport(
+ Nid = "x01jxu+vxlc",
+ ExportName = "sceImeDialogGetResult",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceImeDialog")]
+ public static int ImeDialogGetResult(CpuContext ctx)
+ {
+ var resultAddress = ctx[CpuRegister.Rdi];
+ if (resultAddress == 0)
+ {
+ return SetReturn(ctx, ImeDialogErrorInvalidAddress);
+ }
+
+ Span result = stackalloc byte[8];
+ result.Clear();
+ BinaryPrimitives.WriteInt32LittleEndian(result, EndStatusOk);
+ ctx.Memory.TryWrite(resultAddress, result);
+
+ return SetReturn(ctx, 0);
+ }
+
+ [SysAbiExport(
+ Nid = "oBmw4xrmfKs",
+ ExportName = "sceImeDialogAbort",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceImeDialog")]
+ public static int ImeDialogAbort(CpuContext ctx)
+ {
+ lock (_gate)
+ {
+ _status = StatusFinished;
+ }
+
+ return SetReturn(ctx, 0);
+ }
+
+ [SysAbiExport(
+ Nid = "gyTyVn+bXMw",
+ ExportName = "sceImeDialogTerm",
+ Target = Generation.Gen4 | Generation.Gen5,
+ LibraryName = "libSceImeDialog")]
+ public static int ImeDialogTerm(CpuContext ctx)
+ {
+ lock (_gate)
+ {
+ _status = StatusNone;
+ }
+
+ return SetReturn(ctx, 0);
+ }
+
+ private static void TryWriteInputText(CpuContext ctx, ulong parameterAddress)
+ {
+ Span field = stackalloc byte[8];
+ if (!ctx.Memory.TryRead(parameterAddress + ParamInputTextBufferOffset, field))
+ {
+ return;
+ }
+
+ var bufferAddress = BinaryPrimitives.ReadUInt64LittleEndian(field);
+ if (bufferAddress == 0)
+ {
+ return;
+ }
+
+ var maxTextLength = 16u;
+ Span lengthField = stackalloc byte[4];
+ if (ctx.Memory.TryRead(parameterAddress + ParamMaxTextLengthOffset, lengthField))
+ {
+ var declared = BinaryPrimitives.ReadUInt32LittleEndian(lengthField);
+ if (declared is > 0 and <= 256)
+ {
+ maxTextLength = declared;
+ }
+ }
+
+ var text = Environment.GetEnvironmentVariable("SHARPEMU_DEFAULT_PROFILE");
+ if (string.IsNullOrEmpty(text))
+ {
+ text = DefaultInputText;
+ }
+
+ if ((uint)text.Length > maxTextLength)
+ {
+ text = text[..(int)maxTextLength];
+ }
+
+ var encoded = Encoding.Unicode.GetBytes(text);
+ Span payload = stackalloc byte[encoded.Length + 2];
+ encoded.CopyTo(payload);
+ payload[^2] = 0;
+ payload[^1] = 0;
+
+ if (ctx.Memory.TryWrite(bufferAddress, payload))
+ {
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] ime.dialog_autofill buf=0x{bufferAddress:X16} " +
+ $"max={maxTextLength} text=\"{text}\"");
+ }
+ }
+
+ private static int SetReturn(CpuContext ctx, int result)
+ {
+ ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
+ return result;
+ }
+}
diff --git a/src/SharpEmu.Libs/SaveData/SaveDataDialogExports.cs b/src/SharpEmu.Libs/SaveData/SaveDataDialogExports.cs
index 795338bc..85d150c0 100644
--- a/src/SharpEmu.Libs/SaveData/SaveDataDialogExports.cs
+++ b/src/SharpEmu.Libs/SaveData/SaveDataDialogExports.cs
@@ -33,12 +33,17 @@ public static class SaveDataDialogExports
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogInitialize(CpuContext ctx)
{
- if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
+ var previous = Interlocked.CompareExchange(
+ ref _status,
+ StatusInitialized,
+ StatusNone);
+ if (previous == StatusNone || previous == StatusFinished)
{
- return ctx.SetReturn(ErrorAlreadyInitialized);
+ Interlocked.Exchange(ref _status, StatusInitialized);
+ return ctx.SetReturn(ErrorOk);
}
- return ctx.SetReturn(ErrorOk);
+ return ctx.SetReturn(ErrorAlreadyInitialized);
}
[SysAbiExport(
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
index 853caefe..5a1b3943 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
@@ -1046,14 +1046,50 @@ internal static unsafe class VulkanVideoPresenter
targets.Count > 8 ||
AnyRenderTargetInvalid(targets))
{
+ var dropCount = Interlocked.Increment(ref _offscreenDropTraceCount);
+ if (dropCount <= 16 || dropCount % 500 == 0)
+ {
+ var detail = targets.Count == 0
+ ? ""
+ : string.Join(
+ " ",
+ targets.Select(t => $"0x{t.Address:X}:{t.Width}x{t.Height}"));
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] vk.offscreen_drop#{dropCount} spirv={pixelSpirv.Length} " +
+ $"mrt={targets.Count} vs=0x{shaderAddress:X16} {detail}");
+ }
+
return;
}
var firstTarget = targets[0];
if (RenderTargetsMismatchedOrAliased(targets, firstTarget))
{
- Console.Error.WriteLine(
- "[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets.");
+ var skipCount = Interlocked.Increment(ref _mrtSkipTraceCount);
+ if (skipCount <= 16 || skipCount % 200 == 0)
+ {
+ var aliased = false;
+ for (var i = 0; i < targets.Count && !aliased; i++)
+ {
+ for (var j = i + 1; j < targets.Count; j++)
+ {
+ if (targets[i].Address == targets[j].Address)
+ {
+ aliased = true;
+ break;
+ }
+ }
+ }
+
+ var detail = string.Join(
+ " ",
+ targets.Select(t =>
+ $"0x{t.Address:X}:{t.Width}x{t.Height}:f{t.Format}/{t.NumberType}"));
+ Console.Error.WriteLine(
+ $"[LOADER][WARN] vk.mrt_skip#{skipCount} mrt={targets.Count} " +
+ $"aliased={aliased} vs=0x{shaderAddress:X16} {detail}");
+ }
+
return;
}
@@ -1298,6 +1334,8 @@ internal static unsafe class VulkanVideoPresenter
}
}
+ private static long _mrtSkipTraceCount;
+ private static long _offscreenDropTraceCount;
private static long _perfDrawCount;
private static long _perfDrawTicks;
private static long _perfPipelineCreations;
@@ -1834,8 +1872,22 @@ internal static unsafe class VulkanVideoPresenter
lock (_gate)
{
- return _availableGuestImages.TryGetValue(address, out var availableFormat) &&
- availableFormat == guestFormat;
+ if (!_availableGuestImages.TryGetValue(address, out var availableFormat))
+ {
+ return false;
+ }
+
+ if (availableFormat == guestFormat)
+ {
+ return true;
+ }
+
+ return TryDecodeRenderTargetFormat(
+ (availableFormat >> 8) & 0x1FFu,
+ availableFormat & 0xFFu,
+ out var resident) &&
+ TryDecodeRenderTargetFormat(format, numberType, out var requested) &&
+ Presenter.IsCompatibleViewFormat(resident.Format, requested.Format);
}
}
@@ -12075,6 +12127,7 @@ internal static unsafe class VulkanVideoPresenter
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped storage render-target feedback loop " +
+ $"vs=0x{work.ShaderAddress:X16} " +
$"targets={string.Join(',', work.Targets.Where(target => target.Address != 0).Select(target => $"0x{target.Address:X16}"))}; " +
"sampled aliases use ordered snapshots");
ReturnPooledGuestData(work.Draw);