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);