Compare commits

..

6 Commits

Author SHA1 Message Date
ParantezTech 39d7ccf2f7 [GUI] update missing translations for profile box 2026-07-29 15:54:14 +03:00
ParantezTech c2cee59fda [ime] Add default profile 2026-07-29 15:39:01 +03:00
ParantezTech 0324d5561b [savedata] Allow dialog reinit 2026-07-29 15:38:53 +03:00
ParantezTech 1aef9a0e6e [video] Match render target views 2026-07-29 15:38:47 +03:00
ParantezTech 1cfc0239b8 [agc] Add indirect draws 2026-07-29 15:38:41 +03:00
Berk d5108e854d chore: bump version to 0.0.3-hotfix-2 (#687) 2026-07-29 01:50:26 +03:00
22 changed files with 597 additions and 18 deletions
+21
View File
@@ -45,6 +45,9 @@ public sealed class GuiSettings
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
public string Language { get; set; } = "en";
/// <summary>Default text-entry profile exposed to games.</summary>
public string DefaultProfile { get; set; } = "Sharp";
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
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
+2
View File
@@ -66,6 +66,8 @@
"Options.Language.Label": "لغة المحاكي",
"Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.",
"Options.DefaultProfile.Label": "اسم الملف الشخصي الافتراضي",
"Options.DefaultProfile.Desc": "الاسم المستخدم عندما تطلب لعبة إدخال نص. القيمة الافتراضية هي Sharp.",
"Common.On": "تشغيل",
"Common.Off": "إيقاف",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -76,6 +76,8 @@
"Options.Language.Label": "Langue de l’émulateur",
"Options.Language.Desc": "Langue utilisée dans lensemble du lanceur. Le changement est immédiat.",
"Options.DefaultProfile.Label": "Nom de profil par défaut",
"Options.DefaultProfile.Desc": "Nom utilisé lorsquun jeu demande une saisie de texte. La valeur par défaut est Sharp.",
"Common.On": "Activé",
"Common.Off": "Désactivé",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -66,6 +66,8 @@
"Options.Language.Label": "エミュレータの言語",
"Options.Language.Desc": "ランチャー全体で使用される言語。変更はすぐに適用されます。",
"Options.DefaultProfile.Label": "既定のプロフィール名",
"Options.DefaultProfile.Desc": "ゲームがテキスト入力を要求したときに使用する名前です。既定値は Sharp です。",
"Common.On": "オン",
"Common.Off": "オフ",
+2
View File
@@ -66,6 +66,8 @@
"Options.Language.Label": "에뮬레이터 언어",
"Options.Language.Desc": "런처 전체에 사용되는 언어입니다. 변경 사항은 즉시 적용됩니다.",
"Options.DefaultProfile.Label": "기본 프로필 이름",
"Options.DefaultProfile.Desc": "게임에서 텍스트 입력을 요청할 때 사용할 이름입니다. 기본값은 Sharp입니다.",
"Common.On": "켬",
"Common.Off": "끔",
+2
View File
@@ -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",
+2
View File
@@ -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",
+2
View File
@@ -109,6 +109,8 @@
"Options.Language.Label": "Язык эмулятора",
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
"Options.DefaultProfile.Label": "Имя профиля по умолчанию",
"Options.DefaultProfile.Desc": "Имя, используемое, когда игра запрашивает ввод текста. Значение по умолчанию — Sharp.",
"Common.On": "Включено",
"Common.Off": "Выключено",
+2
View File
@@ -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})",
+10
View File
@@ -346,6 +346,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ComboBox>
</local:SettingRow>
<local:SettingRow x:Name="DefaultProfileRow"
Label="{Binding [Options.DefaultProfile.Label], Source={x:Static local:Localization.Instance}}"
Description="{Binding [Options.DefaultProfile.Desc], Source={x:Static local:Localization.Instance}}">
<TextBox x:Name="DefaultProfileBox"
Width="180"
MaxLength="16"
VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="TitleMusicRow"
Label="{Binding [Options.TitleMusic.Label], Source={x:Static local:Localization.Instance}}"
Description="{Binding [Options.TitleMusic.Desc], Source={x:Static local:Localization.Instance}}">
@@ -646,6 +655,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
OffContent="{Binding [Common.Off], Source={x:Static local:Localization.Instance}}"
VerticalAlignment="Center" />
</local:SettingRow>
</StackPanel>
</Border>
+15
View File
@@ -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(
+281 -11
View File
@@ -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<ulong> _renderTargetAddresses = new();
private static readonly HashSet<ulong> _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<RenderTargetDescriptor> GetRenderTargets(
IReadOnlyDictionary<uint, uint> 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,
+175
View File
@@ -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<byte> 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<byte> 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<byte> 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<byte> 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;
}
}
@@ -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(
@@ -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
? "<no targets>"
: 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);