From 7521295ee1d57e8e30f93a71e65f75e8482f918c Mon Sep 17 00:00:00 2001 From: Foued Attar Date: Mon, 17 Aug 2026 23:00:01 +0200 Subject: [PATCH] [Codec/Native] Real H.264 decode for sceVideodec2, fix TLS loader missing the main module (#824) * [Codec/Native] Real H.264 decode for sceVideodec2, fix TLS loader missing the main module sceVideodec2 was a capability-only stub: the game's video pipeline worked end-to-end but never produced a picture, so intro/cinematic videos stayed black even though playback "completed" without errors (confirmed on Ghost of Yotei's intro cinematic). - Videodec2Decoder: owns an FFmpeg H.264 session per decoder handle, running decode and presentation pacing on their own threads (never the guest thread) so a whole clip isn't decoded faster than it can be displayed. Converts to BGRA and submits straight to VulkanVideoPresenter, bypassing guest memory the same way the existing Bink2 path does. - Videodec2Exports: wires the real decoder into sceVideodec2CreateDecoder/Decode/Flush/Reset/DeleteDecoder, falling back to the original no-picture stub whenever FFmpeg is unavailable or a given decoder failed to open. - VulkanVideoPresenter: decoded frames were being dropped under a single "latest wins" slot the render loop didn't always poll in time before the next frame overwrote it. Queues pending video presentations the same way guest-image flips already are. - DirectExecutionBackend: the TLS load patcher's one-shot scan missed the main game module when the entry point resolves to a separate bootstrap allocation, and never re-scanned lazily-committed pages patched in afterward -- both left FS:[0] TLS loads unpatched, causing an early mutex-spin boot stall (reproduced on Demon's Souls: stuck at import #256). Now scans both the entry point's own allocation and the standard PS5/PS4 image base, and re-scans each newly committed executable range as it's touched. * [GUI] Add a toggle for SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES This flag already existed as an AGC workaround (forces queued GPU command-buffer preambles through when their target queue never picks them up, unblocking titles stuck on a WAIT_REG_MEM that never signals) but was only reachable by setting the environment variable by hand. Expose it as a checkbox next to the other env toggles, in both the global Options panel and the per-game settings panel, so it can be turned on for a specific title without touching a shell. * [GUI] Add remaining language translations for the new env toggle The previous commit only added the new key to en.json/fr.json, relying on Localization's runtime fallback to English -- but LocalizationTests.EmbeddedLanguages_ContainEveryEnglishOptionsKey requires every Options.* key to exist in every embedded language file, which broke CI on all three build jobs. Fills in the remaining 13 languages. --- .../DirectExecutionBackend.Exceptions.cs | 14 + .../Cpu/Native/DirectExecutionBackend.cs | 35 +- src/SharpEmu.GUI/Languages/ar.json | 1 + src/SharpEmu.GUI/Languages/br.json | 1 + src/SharpEmu.GUI/Languages/de.json | 1 + src/SharpEmu.GUI/Languages/dk.json | 1 + src/SharpEmu.GUI/Languages/en.json | 1 + src/SharpEmu.GUI/Languages/es.json | 1 + src/SharpEmu.GUI/Languages/fr.json | 1 + src/SharpEmu.GUI/Languages/hu.json | 1 + src/SharpEmu.GUI/Languages/it.json | 1 + src/SharpEmu.GUI/Languages/ja.json | 1 + src/SharpEmu.GUI/Languages/ko.json | 1 + src/SharpEmu.GUI/Languages/nl.json | 1 + src/SharpEmu.GUI/Languages/pt.json | 1 + src/SharpEmu.GUI/Languages/ru.json | 1 + src/SharpEmu.GUI/Languages/tr.json | 1 + src/SharpEmu.GUI/MainWindow.GameOptions.cs | 1 + src/SharpEmu.GUI/MainWindow.axaml | 13 + src/SharpEmu.GUI/MainWindow.axaml.cs | 6 + src/SharpEmu.Libs/Codec/Videodec2Decoder.cs | 567 ++++++++++++++++++ src/SharpEmu.Libs/Codec/Videodec2Exports.cs | 246 ++++++++ .../VideoOut/VulkanVideoPresenter.cs | 28 +- 23 files changed, 921 insertions(+), 4 deletions(-) create mode 100644 src/SharpEmu.Libs/Codec/Videodec2Decoder.cs create mode 100644 src/SharpEmu.Libs/Codec/Videodec2Exports.cs diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index fb3812e2..492370e0 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -1453,6 +1453,7 @@ public sealed partial class DirectExecutionBackend } TryCommitRange(pageBase + 4096, 4096uL, commitProtect); + RescanTlsPatternsIfExecutable(committedBase, committedSize + 4096uL, commitProtect); if (traceLazyCommit) { Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}"); @@ -1512,6 +1513,7 @@ public sealed partial class DirectExecutionBackend } TryCommitRange(pageBase + 4096, 4096uL, commitProtect); + RescanTlsPatternsIfExecutable(committedBase, committedSize + 4096uL, commitProtect); if (traceLazyCommit) { Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}"); @@ -1613,6 +1615,18 @@ public sealed partial class DirectExecutionBackend } } + // Re-scans a just-committed window for FS:[0] TLS loads; skips non-executable commits. + private unsafe void RescanTlsPatternsIfExecutable(ulong committedBase, ulong committedSize, uint commitProtect) + { + const uint executableProtectionMask = PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; + if ((commitProtect & executableProtectionMask) == 0 || committedSize == 0) + { + return; + } + + PatchTlsPatternsInRange(committedBase, committedBase + committedSize, announce: false); + } + private static bool ShouldTraceLazyCommit(int traceIndex) { if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal)) diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 18427542..35d797e2 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -3145,8 +3145,33 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I // Large Gen5 executables can keep valid code well past the first 32 MiB. // Astro Bot, for example, has an FS:[0] TLS load near +0x70A0000. const ulong MaxScanBytes = 134217728uL; - ulong num = _entryPoint; - ulong num2 = num + MaxScanBytes; + + // _entryPoint can be a separate bootstrap allocation, not the main module — + // always also scan the standard PS5/PS4 image base. + const ulong Ps5MainImageBase = 0x0000000800000000UL; + const ulong Ps4MainImageBase = 0x0000000000400000UL; + ulong scanStart = _entryPoint; + if (VirtualQuery((void*)_entryPoint, out var entryRegion, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0 && + entryRegion.AllocationBase != 0 && + entryRegion.AllocationBase <= _entryPoint) + { + scanStart = entryRegion.AllocationBase; + } + + PatchTlsPatternsInRange(scanStart, scanStart + MaxScanBytes, announce: true); + + // Scan both windows unconditionally; overlap is safe, patched bytes just stop matching. + var mainImageBase = _entryPoint >= Ps5MainImageBase ? Ps5MainImageBase : Ps4MainImageBase; + if (mainImageBase < scanStart) + { + PatchTlsPatternsInRange(mainImageBase, mainImageBase + MaxScanBytes, announce: false); + } + } + + private unsafe void PatchTlsPatternsInRange(ulong rangeStart, ulong rangeEnd, bool announce) + { + ulong num = rangeStart; + ulong num2 = rangeEnd; int num3 = 0; int num4 = 0; int num9 = 0; @@ -3195,7 +3220,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } num = num6 > num ? num6 : num + 4096uL; } - Console.Error.WriteLine($"[LOADER][INFO] Patched {num3} TLS loads, {num9} TLS stores, {num4} stack-canary accesses, {sse4aPatchCount} SSE4a EXTRQ blends"); + if (announce || num3 + num4 + num9 + sse4aPatchCount > 0) + { + Console.Error.WriteLine($"[LOADER][INFO] Patched {num3} TLS loads, {num9} TLS stores, {num4} stack-canary accesses, {sse4aPatchCount} SSE4a EXTRQ blends" + + (announce ? string.Empty : $" (lazy-commit rescan 0x{rangeStart:X16}-0x{rangeEnd:X16})")); + } } private unsafe bool TryPatchSse4aExtrqBlend(nint address, byte* source) diff --git a/src/SharpEmu.GUI/Languages/ar.json b/src/SharpEmu.GUI/Languages/ar.json index c0b4f21f..06e7d38a 100644 --- a/src/SharpEmu.GUI/Languages/ar.json +++ b/src/SharpEmu.GUI/Languages/ar.json @@ -149,6 +149,7 @@ "Options.Env.Group.General": "عام", "Options.Env.RenderDoc.Desc": "يحمّل واجهة RenderDoc داخل التطبيق حتى يمكن التقاط الإطارات من داخل المحاكي.\nاضغط F10 أثناء تشغيل اللعبة لالتقاط إطار واحد؛ تُحفظ اللقطات في user/logs/capture_logs/.\nيتطلب تثبيت RenderDoc. يبطئ وحدة معالجة الرسوميات ويسبب تعليق بعض الألعاب، لذا اتركه معطلاً ما لم تكن تصحح الأخطاء.", "Options.Env.GuestImageCpuSync.Desc": "إعادة رفع أسطح الضيف التي تعيد كتابتها شيفرة المعالج الخاصة باللعبة.\nاتركه مغلقًا عادة. شغّله للألعاب التي لا تصل أسطحها المرسومة بالمعالج إلى الشاشة.\nيكلّف أداءً ويسبب مشاكل في بعض الألعاب مثل GTA V.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "تسليم مقدّمات المخازن المؤقتة لأوامر GPU حتى عندما لا تلتقطها قائمة الانتظار المستهدفة أبدًا.\nاتركه مغلقًا عادة. شغّله للألعاب التي تتجمد أثناء انتظار حاجز GPU لا يُشار إليه أبدًا.", "Common.Save": "حفظ", "Common.Cancel": "إلغاء", "Options.About": "حول", diff --git a/src/SharpEmu.GUI/Languages/br.json b/src/SharpEmu.GUI/Languages/br.json index 392aaa05..e9725bd2 100644 --- a/src/SharpEmu.GUI/Languages/br.json +++ b/src/SharpEmu.GUI/Languages/br.json @@ -43,6 +43,7 @@ "Options.Env.Group.General": "Geral", "Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames de dentro do emulador.\nPressione F10 enquanto o jogo roda para capturar um frame; as capturas vão para user/logs/capture_logs/.\nExige o RenderDoc instalado. Deixa a GPU mais lenta e trava alguns títulos, então mantenha desativado a menos que esteja depurando.", "Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixe desativado normalmente. Ative para títulos cujas superfícies desenhadas pela CPU nunca chegam à tela.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "Entrega os preâmbulos do buffer de comandos da GPU mesmo quando a fila de destino nunca os recolhe.\nDeixe desativado normalmente. Ative para títulos que travam esperando por uma fence de GPU que nunca sinaliza.", "Options.Section.Emulation": "EMULAÇÃO", "Options.Section.Logging": "LOGS", "Options.Section.Launcher": "INICIALIZADOR", diff --git a/src/SharpEmu.GUI/Languages/de.json b/src/SharpEmu.GUI/Languages/de.json index a86c9709..3a0ec386 100644 --- a/src/SharpEmu.GUI/Languages/de.json +++ b/src/SharpEmu.GUI/Languages/de.json @@ -149,6 +149,7 @@ "Options.Env.Group.General": "Allgemein", "Options.Env.RenderDoc.Desc": "Lädt die RenderDoc-In-Application-API, damit Frames aus dem Emulator heraus aufgezeichnet werden können.\nDrücke F10 während das Spiel läuft, um ein Frame aufzuzeichnen; Aufzeichnungen landen in user/logs/capture_logs/.\nErfordert eine RenderDoc-Installation. Verlangsamt die GPU und lässt manche Titel hängen, lass es also aus, wenn du nicht debuggst.", "Options.Env.GuestImageCpuSync.Desc": "Gast-Oberflächen neu hochladen, die der eigene CPU-Code des Spiels überschreibt.\nNormalerweise aus lassen. Für Titel aktivieren, deren CPU-gezeichnete Oberflächen nie auf dem Bildschirm erscheinen.\nKostet Leistung und verursacht bei einigen Titeln wie GTA V Regressionen.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "Liefert GPU-Befehlspuffer-Präambeln auch dann aus, wenn die Zielwarteschlange sie nie abholt.\nNormalerweise aus lassen. Für Titel aktivieren, die beim Warten auf einen GPU-Fence hängen bleiben, der nie signalisiert.", "Common.Save": "Speichern", "Common.Cancel": "Abbrechen", "Options.About": "Über", diff --git a/src/SharpEmu.GUI/Languages/dk.json b/src/SharpEmu.GUI/Languages/dk.json index 26d00fe8..bb444a94 100644 --- a/src/SharpEmu.GUI/Languages/dk.json +++ b/src/SharpEmu.GUI/Languages/dk.json @@ -149,6 +149,7 @@ "Options.Env.Group.General": "Generelt", "Options.Env.RenderDoc.Desc": "Indlæser RenderDocs in-application-API, så frames kan optages inde fra emulatoren.\nTryk på F10 mens spillet kører for at optage ét frame; optagelser havner i user/logs/capture_logs/.\nKræver at RenderDoc er installeret. Gør GPU'en langsommere og får nogle titler til at hænge, så lad den være slået fra, medmindre du fejlfinder.", "Options.Env.GuestImageCpuSync.Desc": "Genindlæs gæsteoverflader, som spillets egen CPU-kode omskriver.\nLad den være slået fra normalt. Slå til for titler, hvis CPU-tegnede overflader aldrig når skærmen.\nKoster ydeevne og giver regressioner i nogle titler, såsom GTA V.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "Leverer GPU-kommandobuffer-præambler, selv når målkøen aldrig henter dem.\nLad den være slået fra normalt. Slå til for titler, der hænger og venter på en GPU-fence, der aldrig signalerer.", "Common.Save": "Gem", "Common.Cancel": "Annuller", "Options.About": "Om", diff --git a/src/SharpEmu.GUI/Languages/en.json b/src/SharpEmu.GUI/Languages/en.json index d1a54b1d..1c0254fd 100644 --- a/src/SharpEmu.GUI/Languages/en.json +++ b/src/SharpEmu.GUI/Languages/en.json @@ -48,6 +48,7 @@ "Options.Env.Group.General": "General", "Options.Env.RenderDoc.Desc": "Load the RenderDoc in-application API so frames can be captured from inside the emulator.\nPress F10 while the game runs to capture one frame; captures land in user/logs/capture_logs/.\nRequires RenderDoc to be installed. Slows the GPU down and hangs some titles, so leave it off unless you are debugging.", "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.Env.ForceSubmitOrphanPreambles.Desc": "Deliver GPU command-buffer preambles even when their target queue never picks them up.\nLeave off normally. Turn on for titles that hang waiting on a GPU fence that never signals.", "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", diff --git a/src/SharpEmu.GUI/Languages/es.json b/src/SharpEmu.GUI/Languages/es.json index 4a2c6177..e4098b4c 100644 --- a/src/SharpEmu.GUI/Languages/es.json +++ b/src/SharpEmu.GUI/Languages/es.json @@ -159,6 +159,7 @@ "Options.Env.Group.General": "General", "Options.Env.RenderDoc.Desc": "Carga la API in-application de RenderDoc para poder capturar fotogramas desde el emulador.\nPulsa F10 mientras el juego se ejecuta para capturar un fotograma; las capturas se guardan en user/logs/capture_logs/.\nRequiere tener RenderDoc instalado. Ralentiza la GPU y bloquea algunos títulos, así que déjalo desactivado salvo que estés depurando.", "Options.Env.GuestImageCpuSync.Desc": "Volver a subir las superficies del invitado que reescribe el propio código de CPU del juego.\nDejar desactivado normalmente. Activar en títulos cuyas superficies dibujadas por CPU nunca llegan a la pantalla.\nCuesta rendimiento y causa regresiones en algunos títulos, como GTA V.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "Entrega los preámbulos del búfer de comandos de la GPU incluso cuando la cola de destino nunca los recoge.\nDejar desactivado normalmente. Activar en títulos que se cuelgan esperando una fence de GPU que nunca se señaliza.", "Common.Save": "Guardar", "Common.Cancel": "Cancelar", "Updater.Auto.Label": "Buscar actualizaciones al iniciar", diff --git a/src/SharpEmu.GUI/Languages/fr.json b/src/SharpEmu.GUI/Languages/fr.json index f900b85c..310c1d6d 100644 --- a/src/SharpEmu.GUI/Languages/fr.json +++ b/src/SharpEmu.GUI/Languages/fr.json @@ -43,6 +43,7 @@ "Options.Env.Group.General": "Général", "Options.Env.RenderDoc.Desc": "Charge l'API in-application de RenderDoc afin de capturer des images depuis l'émulateur.\nAppuyez sur F10 pendant le jeu pour capturer une image ; les captures sont écrites dans user/logs/capture_logs/.\nNécessite l'installation de RenderDoc. Ralentit le GPU et bloque certains jeux : laissez cette option désactivée sauf en cas de débogage.", "Options.Env.GuestImageCpuSync.Desc": "Recharger les surfaces invité que le code CPU du jeu réécrit lui-même.\nLaisser désactivé normalement. Activer pour les titres dont les surfaces dessinées par le CPU n'atteignent jamais l'écran.\nCoûte des performances et provoque des régressions sur certains titres, comme GTA V.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "Soumettre les préambules de tampon de commandes GPU même quand leur file cible ne les récupère jamais.\nLaisser désactivé normalement. Activer pour les titres qui se figent en attendant une fence GPU qui ne se déclenche jamais.", "Options.Section.Emulation": "ÉMULATION", "Options.Section.Logging": "JOURNALISATION", "Options.Section.Launcher": "LANCEUR", diff --git a/src/SharpEmu.GUI/Languages/hu.json b/src/SharpEmu.GUI/Languages/hu.json index 247b681e..8664a8e0 100644 --- a/src/SharpEmu.GUI/Languages/hu.json +++ b/src/SharpEmu.GUI/Languages/hu.json @@ -43,6 +43,7 @@ "Options.Env.Group.General": "Általános", "Options.Env.RenderDoc.Desc": "Betölti a RenderDoc alkalmazáson belüli API-ját, így képkockák rögzíthetők az emulátorból.\nNyomd meg az F10-et futó játék közben egy képkocka rögzítéséhez; a felvételek a user/logs/capture_logs/ mappába kerülnek.\nTelepített RenderDocot igényel. Lassítja a GPU-t és egyes játékokat lefagyaszt, ezért hagyd kikapcsolva, hacsak nem hibakeresel.", "Options.Env.GuestImageCpuSync.Desc": "Újratölti azokat a vendégfelületeket, amelyeket a játék saját CPU-kódja ír felül.\nNormál esetben hagyd kikapcsolva. Kapcsold be azoknál a címeknél, amelyek CPU-val rajzolt felületei sosem jutnak ki a képernyőre.\nTeljesítménybe kerül, és egyes címeknél, például a GTA V-nél regressziót okoz.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "GPU parancspuffer-előtagokat is kézbesít, ha a célsor sosem veszi fel őket.\nNormál esetben hagyd kikapcsolva. Kapcsold be azoknál a címeknél, amelyek egy sosem jelző GPU-fence-re várva lefagynak.", "Options.Section.Emulation": "EMULÁCIÓ", "Options.Section.Logging": "LOGOLÁS", "Options.Section.Launcher": "INDITÓ", diff --git a/src/SharpEmu.GUI/Languages/it.json b/src/SharpEmu.GUI/Languages/it.json index 419c5998..26a3787f 100644 --- a/src/SharpEmu.GUI/Languages/it.json +++ b/src/SharpEmu.GUI/Languages/it.json @@ -154,6 +154,7 @@ "Options.Env.Group.General": "Generale", "Options.Env.RenderDoc.Desc": "Carica l'API in-application di RenderDoc per catturare i frame dall'interno dell'emulatore.\nPremi F10 mentre il gioco è in esecuzione per catturare un frame; le catture finiscono in user/logs/capture_logs/.\nRichiede RenderDoc installato. Rallenta la GPU e blocca alcuni titoli, quindi lascialo disattivato se non stai facendo debug.", "Options.Env.GuestImageCpuSync.Desc": "Ricarica le superfici guest riscritte dal codice CPU del gioco.\nLasciare disattivato normalmente. Attivare per i titoli le cui superfici disegnate dalla CPU non raggiungono mai lo schermo.\nCosta prestazioni e causa regressioni in alcuni titoli, come GTA V.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "Consegna i preamboli del buffer di comandi GPU anche quando la coda di destinazione non li preleva mai.\nLasciare disattivato normalmente. Attivare per i titoli che si bloccano in attesa di una fence GPU che non segnala mai.", "Common.Save": "Salva", "Common.Cancel": "Annulla", "Options.About": "Informazioni", diff --git a/src/SharpEmu.GUI/Languages/ja.json b/src/SharpEmu.GUI/Languages/ja.json index dacab0b5..2a02f9e5 100644 --- a/src/SharpEmu.GUI/Languages/ja.json +++ b/src/SharpEmu.GUI/Languages/ja.json @@ -149,6 +149,7 @@ "Options.Env.Group.General": "一般", "Options.Env.RenderDoc.Desc": "RenderDoc のアプリ内 API を読み込み、エミュレーター内からフレームをキャプチャできるようにします。\nゲーム実行中に F10 を押すと 1 フレームをキャプチャします。保存先は user/logs/capture_logs/ です。\nRenderDoc のインストールが必要です。GPU が遅くなり一部のタイトルはハングするため、デバッグ時以外はオフのままにしてください。", "Options.Env.GuestImageCpuSync.Desc": "ゲーム自身の CPU コードが書き換えるゲスト表面を再アップロードします。\n通常はオフのままにしてください。CPU で描画した表面が画面に反映されないタイトルで有効にします。\n性能を犠牲にし、GTA V など一部のタイトルでは不具合が生じます。", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "対象キューが受け取らない場合でも GPU コマンドバッファのプリアンブルを配信します。\n通常はオフのままにしてください。決してシグナルされない GPU フェンスを待ってハングするタイトルで有効にします。", "Common.Save": "保存", "Common.Cancel": "キャンセル", "Options.About": "情報", diff --git a/src/SharpEmu.GUI/Languages/ko.json b/src/SharpEmu.GUI/Languages/ko.json index b4ee1e02..617a82f8 100644 --- a/src/SharpEmu.GUI/Languages/ko.json +++ b/src/SharpEmu.GUI/Languages/ko.json @@ -149,6 +149,7 @@ "Options.Env.Group.General": "일반", "Options.Env.RenderDoc.Desc": "RenderDoc의 인앱 API를 로드하여 에뮬레이터 내부에서 프레임을 캡처할 수 있게 합니다.\n게임 실행 중 F10을 누르면 한 프레임을 캡처하며, 캡처 파일은 user/logs/capture_logs/에 저장됩니다.\nRenderDoc이 설치되어 있어야 합니다. GPU 속도가 느려지고 일부 타이틀은 멈추므로 디버깅할 때가 아니면 꺼 두세요.", "Options.Env.GuestImageCpuSync.Desc": "게임의 자체 CPU 코드가 다시 쓰는 게스트 표면을 다시 업로드합니다.\n평소에는 꺼 두세요. CPU로 그린 표면이 화면에 나타나지 않는 타이틀에서 켜세요.\n성능을 소모하며 GTA V 등 일부 타이틀에서는 문제가 생깁니다.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "대상 큐가 절대 가져가지 않아도 GPU 명령 버퍼 프리앰블을 전달합니다.\n평소에는 꺼 두세요. 절대 신호를 보내지 않는 GPU 펜스를 기다리며 멈추는 타이틀에서 켜세요.", "Common.Save": "저장", "Common.Cancel": "취소", "Options.About": "정보", diff --git a/src/SharpEmu.GUI/Languages/nl.json b/src/SharpEmu.GUI/Languages/nl.json index 1efe1978..49c1f596 100644 --- a/src/SharpEmu.GUI/Languages/nl.json +++ b/src/SharpEmu.GUI/Languages/nl.json @@ -149,6 +149,7 @@ "Options.Env.Group.General": "Algemeen", "Options.Env.RenderDoc.Desc": "Laadt de RenderDoc in-application-API zodat frames vanuit de emulator kunnen worden vastgelegd.\nDruk op F10 terwijl het spel draait om één frame vast te leggen; opnamen komen in user/logs/capture_logs/.\nVereist een geïnstalleerde RenderDoc. Vertraagt de GPU en laat sommige games vastlopen, dus laat dit uit tenzij je aan het debuggen bent.", "Options.Env.GuestImageCpuSync.Desc": "Gastoppervlakken opnieuw uploaden die de eigen CPU-code van de game herschrijft.\nNormaal uit laten. Inschakelen voor titels waarvan de door de CPU getekende oppervlakken nooit het scherm bereiken.\nKost prestaties en veroorzaakt regressies in sommige titels, zoals GTA V.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "Levert GPU-commandobuffer-preambules af, zelfs wanneer de doelwachtrij ze nooit ophaalt.\nNormaal uit laten. Inschakelen voor titels die vasthangen in afwachting van een GPU-fence die nooit signaleert.", "Common.Save": "Opslaan", "Common.Cancel": "Annuleren", "Options.About": "Over", diff --git a/src/SharpEmu.GUI/Languages/pt.json b/src/SharpEmu.GUI/Languages/pt.json index 47a3fac9..421792de 100644 --- a/src/SharpEmu.GUI/Languages/pt.json +++ b/src/SharpEmu.GUI/Languages/pt.json @@ -43,6 +43,7 @@ "Options.Env.Group.General": "Geral", "Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames a partir do emulador.\nPrime F10 enquanto o jogo corre para capturar um frame; as capturas vão para user/logs/capture_logs/.\nRequer o RenderDoc instalado. Torna a GPU mais lenta e bloqueia alguns títulos, por isso deixa desativado a menos que estejas a depurar.", "Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixar desativado normalmente. Ativar para títulos cujas superfícies desenhadas pela CPU nunca chegam ao ecrã.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "Entrega os preâmbulos do buffer de comandos da GPU mesmo quando a fila de destino nunca os recolhe.\nDeixar desativado normalmente. Ativar para títulos que bloqueiam à espera de uma fence de GPU que nunca sinaliza.", "Options.Section.Emulation": "EMULAÇÃO", "Options.Section.Logging": "REGISTOS", "Options.Section.Launcher": "LANÇADOR", diff --git a/src/SharpEmu.GUI/Languages/ru.json b/src/SharpEmu.GUI/Languages/ru.json index 330d2657..3c34683f 100644 --- a/src/SharpEmu.GUI/Languages/ru.json +++ b/src/SharpEmu.GUI/Languages/ru.json @@ -46,6 +46,7 @@ "Options.Env.Group.General": "Общие", "Options.Env.RenderDoc.Desc": "Загружает внутренний API RenderDoc, чтобы кадры можно было захватывать из эмулятора.\nНажмите F10 во время игры, чтобы захватить один кадр; захваты сохраняются в user/logs/capture_logs/.\nТребует установленного RenderDoc. Замедляет GPU и подвешивает некоторые игры, поэтому оставьте выключенным, если не занимаетесь отладкой.", "Options.Env.GuestImageCpuSync.Desc": "Повторно загружать гостевые поверхности, которые переписывает собственный код ЦП игры.\nОбычно оставляйте выключенным. Включайте для игр, чьи отрисованные ЦП поверхности не попадают на экран.\nСнижает производительность и вызывает регрессии в некоторых играх, например в GTA V.", + "Options.Env.ForceSubmitOrphanPreambles.Desc": "Доставляет преамбулы буфера команд GPU, даже если целевая очередь их так и не забирает.\nОбычно оставляйте выключенным. Включайте для игр, которые зависают в ожидании GPU-fence, который никогда не срабатывает.", "Options.Section.Emulation": "ЭМУЛЯЦИЯ", "Options.Section.Logging": "ЛОГГИРОВАНИЕ", "Options.Section.Launcher": "ЛАУНЧЕР", diff --git a/src/SharpEmu.GUI/Languages/tr.json b/src/SharpEmu.GUI/Languages/tr.json index 795cd425..f87c6d5b 100644 --- a/src/SharpEmu.GUI/Languages/tr.json +++ b/src/SharpEmu.GUI/Languages/tr.json @@ -183,6 +183,7 @@ "Options.Env.Group.General": "Genel", "Options.Env.RenderDoc.Desc": "RenderDoc uygulama içi API'sini yükler, böylece kareler emülatörün içinden yakalanabilir.\nOyun çalışırken F10'a basınca bir kare yakalanır; kayıtlar user/logs/capture_logs/ altına düşer.\nRenderDoc'un kurulu olmasını gerektirir. GPU'yu yavaşlatır ve bazı oyunları kilitler, hata ayıklamıyorsanız kapalı bırakın.", "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.Env.ForceSubmitOrphanPreambles.Desc": "Hedef kuyruk onları asla almasa bile GPU komut arabelleği önsözlerini teslim eder.\nNormalde kapalı bırakın. Asla sinyal vermeyen bir GPU fence'ini bekleyerek takılan oyunlarda açın.", "Options.DefaultProfile.Label": "Varsayilan profil adi", "Options.DefaultProfile.Desc": "Oyun metin girisi istediginde kullanilacak ad. Varsayilan deger Sharp'tir.", "Common.Save": "Kaydet", diff --git a/src/SharpEmu.GUI/MainWindow.GameOptions.cs b/src/SharpEmu.GUI/MainWindow.GameOptions.cs index 3c32c88c..dd4e0a17 100644 --- a/src/SharpEmu.GUI/MainWindow.GameOptions.cs +++ b/src/SharpEmu.GUI/MainWindow.GameOptions.cs @@ -483,6 +483,7 @@ public partial class MainWindow ("SHARPEMU_LOG_IO", GameEnvLogIoToggle), ("SHARPEMU_LOG_NP", GameEnvLogNpToggle), ("SHARPEMU_GUEST_IMAGE_CPU_SYNC", GameEnvGuestImageCpuSyncToggle), + ("SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES", GameEnvForceSubmitOrphanPreamblesToggle), ("SHARPEMU_RENDERDOC", GameEnvRenderDocToggle), ]; diff --git a/src/SharpEmu.GUI/MainWindow.axaml b/src/SharpEmu.GUI/MainWindow.axaml index 4feac877..b92d3504 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml +++ b/src/SharpEmu.GUI/MainWindow.axaml @@ -977,6 +977,14 @@ SPDX-License-Identifier: GPL-2.0-or-later + + + @@ -1493,6 +1501,11 @@ SPDX-License-Identifier: GPL-2.0-or-later Description="{Binding [Options.Env.GuestImageCpuSync.Desc], Source={x:Static local:Localization.Instance}}"> + + + + diff --git a/src/SharpEmu.GUI/MainWindow.axaml.cs b/src/SharpEmu.GUI/MainWindow.axaml.cs index ade9f5f1..62dfe5d8 100644 --- a/src/SharpEmu.GUI/MainWindow.axaml.cs +++ b/src/SharpEmu.GUI/MainWindow.axaml.cs @@ -290,6 +290,10 @@ public partial class MainWindow : Window SetEnvironmentToggle( "SHARPEMU_GUEST_IMAGE_CPU_SYNC", EnvGuestImageCpuSyncToggle.IsChecked == true); + EnvForceSubmitOrphanPreamblesToggle.IsCheckedChanged += (_, _) => + SetEnvironmentToggle( + "SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES", + EnvForceSubmitOrphanPreamblesToggle.IsChecked == true); DefaultProfileBox.TextChanged += (_, _) => _settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text); LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged(); @@ -1213,6 +1217,8 @@ public partial class MainWindow : Window EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP"); EnvGuestImageCpuSyncToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_GUEST_IMAGE_CPU_SYNC"); + EnvForceSubmitOrphanPreamblesToggle.IsChecked = + _settings.EnvironmentToggles.Contains("SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES"); EnvRenderDocToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_RENDERDOC"); DefaultProfileBox.Text = _settings.DefaultProfile; diff --git a/src/SharpEmu.Libs/Codec/Videodec2Decoder.cs b/src/SharpEmu.Libs/Codec/Videodec2Decoder.cs new file mode 100644 index 00000000..02fdbc15 --- /dev/null +++ b/src/SharpEmu.Libs/Codec/Videodec2Decoder.cs @@ -0,0 +1,567 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Threading.Channels; +using FFmpeg.AutoGen; +using SharpEmu.Libs.VideoOut; + +namespace SharpEmu.Libs.Codec; + +/// +/// Owns one FFmpeg H.264 decode session for a single sceVideodec2 decoder +/// handle, feeding it pre-demuxed Annex-B access units from guest memory. +/// +/// Three-stage pipeline, none of it on the guest thread: +/// Decode() -> AU queue -> decode worker -> frame queue -> scheduler -> Submit +/// +/// The scheduler paces presentation to the stream's own framerate (no PTS +/// is available) instead of draining as fast as it decodes. Neither worker +/// thread may write to guest memory directly (the guest's stack slot may +/// already be reused by the time they finish), so readiness is reported via +/// TryConsumeProtocolReadySignal (metadata only) while pixels go straight +/// to VulkanVideoPresenter.Submit from the scheduler thread. +/// +internal sealed unsafe class Videodec2Decoder : IDisposable +{ + // BGRA matches VulkanVideoPresenter.Submit; decode bypasses guest memory entirely. + private const AVPixelFormat OutputPixelFormat = AVPixelFormat.AV_PIX_FMT_BGRA; + + // Enough lookahead to absorb decode jitter without adding visible latency. + private const int FrameQueueCapacity = 4; + + // Fallback when the stream doesn't declare a usable framerate. + private const double FallbackFps = 30.0; + + private static bool _rootPathInitialized; + private static readonly object InitGate = new(); + + private readonly object _gate = new(); + private AVCodecContext* _codecContext; + private AVFrame* _frame; + private AVPacket* _packet; + private SwsContext* _swsContext; + private int _swsSourceWidth; + private int _swsSourceHeight; + private AVPixelFormat _swsSourceFormat = AVPixelFormat.AV_PIX_FMT_NONE; + private bool _disposed; + + // Unbounded: access units are small, backpressure lives on the frame queue below. + private readonly Channel _workChannel = + Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true, + }); + + // Bounded and blocking-on-full: the backpressure that keeps decode paced to playback. + private readonly Channel<(byte[] Bgra, uint Width, uint Height)> _frameQueue = + Channel.CreateBounded<(byte[], uint, uint)>(new BoundedChannelOptions(FrameQueueCapacity) + { + FullMode = BoundedChannelFullMode.Wait, + SingleReader = true, + SingleWriter = true, + }); + + private readonly Thread _worker; + private readonly Thread _scheduler; + + // Cancelled (not just completed) on Dispose so both loops stop promptly instead of draining a backlog. + private readonly CancellationTokenSource _workerCts = new(); + + private readonly object _protocolGate = new(); + private long _producedCount; + private long _reportedCount; + private uint _lastWidth; + private uint _lastHeight; + + private Videodec2Decoder(AVCodecContext* codecContext, AVFrame* frame, AVPacket* packet) + { + _codecContext = codecContext; + _frame = frame; + _packet = packet; + _worker = new Thread(WorkerLoop) + { + IsBackground = true, + Name = "SharpEmu Videodec2 Worker", + }; + _scheduler = new Thread(SchedulerLoop) + { + IsBackground = true, + Name = "SharpEmu Videodec2 Scheduler", + }; + _worker.Start(); + _scheduler.Start(); + } + + /// Opens a new H.264 session, or null if FFmpeg is unavailable or the decoder couldn't open. + public static Videodec2Decoder? TryCreate() + { + EnsureRootPathInitialized(); + + AVCodecContext* codecContext = null; + AVFrame* frame = null; + AVPacket* packet = null; + try + { + var codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_H264); + if (codec == null) + { + return null; + } + + codecContext = ffmpeg.avcodec_alloc_context3(codec); + if (codecContext == null) + { + return null; + } + + if (ffmpeg.avcodec_open2(codecContext, codec, null) < 0) + { + ffmpeg.avcodec_free_context(&codecContext); + return null; + } + + frame = ffmpeg.av_frame_alloc(); + packet = ffmpeg.av_packet_alloc(); + if (frame == null || packet == null) + { + if (frame != null) + { + ffmpeg.av_frame_free(&frame); + } + + if (packet != null) + { + ffmpeg.av_packet_free(&packet); + } + + ffmpeg.avcodec_free_context(&codecContext); + return null; + } + + return new Videodec2Decoder(codecContext, frame, packet); + } + catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or TypeInitializationException) + { + // FFmpeg's native libraries are optional; missing ones degrade to the stub, not a crash. + if (codecContext != null) + { + ffmpeg.avcodec_free_context(&codecContext); + } + + return null; + } + } + + private static void EnsureRootPathInitialized() + { + if (_rootPathInitialized) + { + return; + } + + lock (InitGate) + { + if (_rootPathInitialized) + { + return; + } + + _rootPathInitialized = true; + // Must be set before any ffmpeg.* call, or bindings resolve against the empty default RootPath. + ffmpeg.RootPath = Path.Combine(AppContext.BaseDirectory, "plugins"); + DynamicallyLoadedBindings.Initialize(); + } + } + + /// Hands one Annex-B access unit to the decode worker and returns immediately. + public void EnqueueAccessUnit(byte[] accessUnit) + { + _workChannel.Writer.TryWrite(accessUnit); + } + + /// Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any. + public void RequestDrain() + { + _workChannel.Writer.TryWrite(null); + } + + /// Non-blocking: true exactly once per frame the worker has produced, in order. + public bool TryConsumeProtocolReadySignal(out uint width, out uint height) + { + lock (_protocolGate) + { + if (_reportedCount >= _producedCount) + { + width = 0; + height = 0; + return false; + } + + _reportedCount++; + width = _lastWidth; + height = _lastHeight; + return true; + } + } + + private void WorkerLoop() + { + var reader = _workChannel.Reader; + var token = _workerCts.Token; + while (true) + { + byte[]? item; + try + { + if (!reader.WaitToReadAsync(token).AsTask().GetAwaiter().GetResult()) + { + return; + } + + if (!reader.TryRead(out item)) + { + continue; + } + } + catch (ChannelClosedException) + { + return; + } + catch (OperationCanceledException) + { + return; + } + + var decodedOk = item is null + ? DrainCoreLocked(out var bgraFrame, out var hasPicture, out var width, out var height) + : DecodeCoreLocked(item, out bgraFrame, out hasPicture, out width, out height); + + if (!decodedOk || !hasPicture || bgraFrame is null) + { + continue; + } + + try + { + // Blocks if the scheduler hasn't kept up; deliberate backpressure. + _frameQueue.Writer.WriteAsync((bgraFrame, width, height), token).AsTask().GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + return; + } + catch (ChannelClosedException) + { + return; + } + + lock (_protocolGate) + { + _producedCount++; + _lastWidth = width; + _lastHeight = height; + } + } + } + + private void SchedulerLoop() + { + var reader = _frameQueue.Reader; + var token = _workerCts.Token; + var haveDeadline = false; + var nextDeadline = DateTime.MinValue; + var frameInterval = TimeSpan.FromSeconds(1.0 / FallbackFps); + + while (true) + { + (byte[] Bgra, uint Width, uint Height) item; + try + { + if (!reader.WaitToReadAsync(token).AsTask().GetAwaiter().GetResult()) + { + return; + } + + if (!reader.TryRead(out item)) + { + continue; + } + } + catch (ChannelClosedException) + { + return; + } + catch (OperationCanceledException) + { + return; + } + + if (!haveDeadline) + { + // Framerate isn't known until FFmpeg parses the first frame's SPS/VUI. + var rate = _codecContext->framerate; + var fps = rate.den > 0 && rate.num > 0 + ? (double)rate.num / rate.den + : FallbackFps; + frameInterval = TimeSpan.FromSeconds(1.0 / fps); + nextDeadline = DateTime.UtcNow; + haveDeadline = true; + } + + var now = DateTime.UtcNow; + if (nextDeadline > now) + { + try + { + Task.Delay(nextDeadline - now, token).GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + return; + } + } + + VulkanVideoPresenter.Submit(item.Bgra, item.Width, item.Height); + nextDeadline += frameInterval; + + // Resync to "now" if we fell behind, instead of burning through a deadline backlog unpaced. + if (nextDeadline < DateTime.UtcNow) + { + nextDeadline = DateTime.UtcNow; + } + } + } + + /// Feeds one access unit and converts the resulting picture to BGRA, if any. Decode-worker thread only. + private bool DecodeCoreLocked( + byte[] accessUnit, + out byte[]? bgraFrame, + out bool hasPicture, + out uint width, + out uint height) + { + bgraFrame = null; + hasPicture = false; + width = 0; + height = 0; + + lock (_gate) + { + if (_disposed) + { + return false; + } + + ffmpeg.av_packet_unref(_packet); + var buffer = ffmpeg.av_malloc((nuint)accessUnit.Length + (nuint)ffmpeg.AV_INPUT_BUFFER_PADDING_SIZE); + if (buffer == null) + { + return false; + } + + fixed (byte* source = accessUnit) + { + Buffer.MemoryCopy(source, buffer, accessUnit.Length, accessUnit.Length); + } + + new Span((byte*)buffer + accessUnit.Length, ffmpeg.AV_INPUT_BUFFER_PADDING_SIZE).Clear(); + + _packet->data = (byte*)buffer; + _packet->size = accessUnit.Length; + + var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet); + ffmpeg.av_freep(&buffer); + _packet->data = null; + _packet->size = 0; + if (sendResult < 0 && sendResult != ffmpeg.AVERROR(ffmpeg.EAGAIN)) + { + return false; + } + + var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame); + if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) || receiveResult == ffmpeg.AVERROR_EOF) + { + return true; + } + + if (receiveResult < 0) + { + return false; + } + + try + { + bgraFrame = ConvertFrameToBgraLocked(out width, out height); + if (bgraFrame == null) + { + return false; + } + + hasPicture = true; + return true; + } + finally + { + ffmpeg.av_frame_unref(_frame); + } + } + } + + /// Signals end-of-stream and pulls one remaining buffered frame, if any. Decode-worker thread only. + private bool DrainCoreLocked(out byte[]? bgraFrame, out bool hasPicture, out uint width, out uint height) + { + bgraFrame = null; + hasPicture = false; + width = 0; + height = 0; + + lock (_gate) + { + if (_disposed) + { + return false; + } + + var sendResult = ffmpeg.avcodec_send_packet(_codecContext, null); + if (sendResult < 0 && sendResult != ffmpeg.AVERROR_EOF) + { + return false; + } + + var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame); + if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) || receiveResult == ffmpeg.AVERROR_EOF) + { + return true; + } + + if (receiveResult < 0) + { + return false; + } + + try + { + bgraFrame = ConvertFrameToBgraLocked(out width, out height); + if (bgraFrame == null) + { + return false; + } + + hasPicture = true; + return true; + } + finally + { + ffmpeg.av_frame_unref(_frame); + } + } + } + + /// Converts to a tightly packed width*height*4 BGRA buffer, or null on failure. + private byte[]? ConvertFrameToBgraLocked(out uint width, out uint height) + { + width = (uint)_frame->width; + height = (uint)_frame->height; + var sourceFormat = (AVPixelFormat)_frame->format; + + if (_swsContext == null || + _swsSourceWidth != _frame->width || + _swsSourceHeight != _frame->height || + _swsSourceFormat != sourceFormat) + { + if (_swsContext != null) + { + ffmpeg.sws_freeContext(_swsContext); + } + + _swsContext = ffmpeg.sws_getContext( + _frame->width, _frame->height, sourceFormat, + _frame->width, _frame->height, OutputPixelFormat, + ffmpeg.SWS_BILINEAR, null, null, null); + if (_swsContext == null) + { + return null; + } + + _swsSourceWidth = _frame->width; + _swsSourceHeight = _frame->height; + _swsSourceFormat = sourceFormat; + } + + var bgraFrame = new byte[checked((int)(width * height * 4))]; + fixed (byte* destinationPtr = bgraFrame) + { + var dstData = new byte_ptrArray4(); + var dstLinesize = new int_array4(); + ffmpeg.av_image_fill_arrays( + ref dstData, ref dstLinesize, destinationPtr, + OutputPixelFormat, _frame->width, _frame->height, 1); + + var srcData = new byte_ptrArray8(); + var srcLinesize = new int_array8(); + for (var i = 0; i < 4; i++) + { + srcData[(uint)i] = _frame->data[(uint)i]; + srcLinesize[(uint)i] = _frame->linesize[(uint)i]; + } + + ffmpeg.sws_scale( + _swsContext, srcData, srcLinesize, 0, _frame->height, + dstData, dstLinesize); + } + + return bgraFrame; + } + + public void Dispose() + { + lock (_gate) + { + if (_disposed) + { + return; + } + + _disposed = true; + } + + // Outside _gate: the worker needs it to finish whatever item it's mid-call on. + _workerCts.Cancel(); + _workChannel.Writer.TryComplete(); + _frameQueue.Writer.TryComplete(); + _worker.Join(TimeSpan.FromSeconds(2)); + _scheduler.Join(TimeSpan.FromSeconds(2)); + _workerCts.Dispose(); + + lock (_gate) + { + if (_swsContext != null) + { + ffmpeg.sws_freeContext(_swsContext); + _swsContext = null; + } + + if (_packet != null) + { + var packet = _packet; + ffmpeg.av_packet_free(&packet); + _packet = null; + } + + if (_frame != null) + { + var frame = _frame; + ffmpeg.av_frame_free(&frame); + _frame = null; + } + + if (_codecContext != null) + { + var codecContext = _codecContext; + ffmpeg.avcodec_free_context(&codecContext); + _codecContext = null; + } + } + } +} diff --git a/src/SharpEmu.Libs/Codec/Videodec2Exports.cs b/src/SharpEmu.Libs/Codec/Videodec2Exports.cs new file mode 100644 index 00000000..c88812f1 --- /dev/null +++ b/src/SharpEmu.Libs/Codec/Videodec2Exports.cs @@ -0,0 +1,246 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections.Concurrent; +using SharpEmu.HLE; + +namespace SharpEmu.Libs.Codec; + +/// +/// libSceVideodec2 (hardware compute-based decoder). sceVideodec2Decode +/// feeds a real FFmpeg H.264 session (Videodec2Decoder) when one can be +/// opened, falling back to the original "no picture" stub otherwise. +/// +public static class Videodec2Exports +{ + private const int Ok = 0; + + // Null entry = TryCreate() failed; every export falls back to the stub for that handle. + private static readonly ConcurrentDictionary Decoders = new(); + private static long _nextDecoderHandle = unchecked((long)DecoderToken); + + [SysAbiExport( + Nid = "RnDibcGCPKw", + ExportName = "sceVideodec2QueryComputeMemoryInfo", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx) + { + var paramAddress = ctx[CpuRegister.Rdi]; + if (paramAddress == 0) + { + return SetReturn(ctx, VideodecErrorInvalidArg); + } + + // Success needs no memory writes; the game initializes from its own fields. + return SetReturn(ctx, Ok); + } + + private const int VideodecErrorInvalidArg = unchecked((int)0x80620801); + + // Reject garbage/not-yet-primed struct reads before they reach `new byte[...]`. + private const ulong MaxPlausibleAuBytes = 32UL * 1024 * 1024; + private const ulong MaxPlausibleSlotBytes = 64UL * 1024 * 1024; + + // Opaque token the game hands back unmodified to later Videodec2 calls. + private const ulong ComputeQueueToken = 0x56D2_C0DE_0001UL; + + [SysAbiExport( + Nid = "eD+X2SmxUt4", + ExportName = "sceVideodec2AllocateComputeQueue", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2AllocateComputeQueue(CpuContext ctx) + { + var queueAddress = ctx[CpuRegister.Rdi]; + if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken)) + { + return SetReturn(ctx, VideodecErrorInvalidArg); + } + + return SetReturn(ctx, Ok); + } + + // A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly. + [SysAbiExport( + Nid = "qqMCwlULR+E", + ExportName = "sceVideodec2QueryDecoderMemoryInfo", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx) + { + var memoryInfoAddress = ctx[CpuRegister.Rsi]; + if (memoryInfoAddress == 0 || + !ctx.TryWriteUInt64(memoryInfoAddress + 0x08, 0) || + !ctx.TryWriteUInt64(memoryInfoAddress + 0x28, 0) || + // Frame-slot size: must be nonzero or the game divides its arena by zero. + !ctx.TryWriteUInt64(memoryInfoAddress + 0x38, 0x1000)) + { + return SetReturn(ctx, VideodecErrorInvalidArg); + } + + return SetReturn(ctx, Ok); + } + + private const ulong DecoderToken = 0x56D2_C0DE_0002UL; + + // Handle is opaque to the game; a monotonic counter seeded at the old fixed token. + [SysAbiExport( + Nid = "CNNRoRYd8XI", + ExportName = "sceVideodec2CreateDecoder", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2CreateDecoder(CpuContext ctx) + { + var decoderAddress = ctx[CpuRegister.Rdx]; + if (decoderAddress == 0) + { + return SetReturn(ctx, VideodecErrorInvalidArg); + } + + var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle)); + Decoders[handle] = Videodec2Decoder.TryCreate(); + + if (!ctx.TryWriteUInt64(decoderAddress, handle)) + { + Decoders.TryRemove(handle, out var created); + created?.Dispose(); + return SetReturn(ctx, VideodecErrorInvalidArg); + } + + return SetReturn(ctx, Ok); + } + + // Clearing the picture-ready byte at [rdx] tells the player "no buffered pictures remain". + [SysAbiExport( + Nid = "l1hXwscLuCY", + ExportName = "sceVideodec2Flush", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2Flush(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + var outputInfoAddress = ctx[CpuRegister.Rdx]; + if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture)) + { + return SetReturn(ctx, VideodecErrorInvalidArg); + } + + if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null) + { + // Drain in order: report an already-finished frame before queuing a new drain request. + if (decoder.TryConsumeProtocolReadySignal(out var width, out var height)) + { + if (ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) && + ctx.TryWriteUInt64(outputInfoAddress + 0x10, height)) + { + _ = ctx.Memory.TryWrite(outputInfoAddress, PictureReady); + } + } + else + { + decoder.RequestDrain(); + } + } + + return SetReturn(ctx, Ok); + } + + // No state to reset. + [SysAbiExport( + Nid = "wJXikG6QFN8", + ExportName = "sceVideodec2Reset", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2Reset(CpuContext ctx) + { + return SetReturn(ctx, Ok); + } + + [SysAbiExport( + Nid = "jwImxXRGSKA", + ExportName = "sceVideodec2DeleteDecoder", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2DeleteDecoder(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + if (Decoders.TryRemove(handle, out var decoder)) + { + decoder?.Dispose(); + } + + return SetReturn(ctx, Ok); + } + + // rcx[0] is the picture-ready flag (1 = frame published); it lives in + // uninitialized stack and must always be written explicitly. + [SysAbiExport( + Nid = "852F5+q6+iM", + ExportName = "sceVideodec2Decode", + Target = Generation.Gen5, + LibraryName = "libSceVideodec2")] + public static int Videodec2Decode(CpuContext ctx) + { + var handle = ctx[CpuRegister.Rdi]; + var inputAuStruct = ctx[CpuRegister.Rsi]; + var outputSlotObj = ctx[CpuRegister.Rdx]; + var outputInfoAddress = ctx[CpuRegister.Rcx]; + + if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture)) + { + return SetReturn(ctx, VideodecErrorInvalidArg); + } + + if (!Decoders.TryGetValue(handle, out var decoder) || decoder is null) + { + // No real decoder for this handle: stub behavior, "fed the AU, no picture". + return SetReturn(ctx, Ok); + } + + if (inputAuStruct == 0 || + !ctx.TryReadUInt64(inputAuStruct + 0x08, out var auDataPtr) || + !ctx.TryReadUInt64(inputAuStruct + 0x10, out var auDataSize) || + auDataPtr == 0 || auDataSize == 0 || auDataSize > MaxPlausibleAuBytes || + outputSlotObj == 0 || + !ctx.TryReadUInt64(outputSlotObj + 0x08, out var slotPtr) || + !ctx.TryReadUInt64(outputSlotObj + 0x10, out var slotSize) || + slotPtr == 0 || slotSize == 0 || slotSize > MaxPlausibleSlotBytes) + { + // Nothing sane to feed/fill this call; not an error. + return SetReturn(ctx, Ok); + } + + var auBuffer = new byte[auDataSize]; + if (!ctx.Memory.TryRead(auDataPtr, auBuffer)) + { + return SetReturn(ctx, Ok); + } + + // Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads. + decoder.EnqueueAccessUnit(auBuffer); + + if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height)) + { + return SetReturn(ctx, Ok); + } + + if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) || + !ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) || + !ctx.Memory.TryWrite(outputInfoAddress, PictureReady)) + { + return SetReturn(ctx, Ok); + } + + return SetReturn(ctx, Ok); + } + + private static readonly byte[] NoPicture = [0]; + private static readonly byte[] PictureReady = [1]; + + private static int SetReturn(CpuContext ctx, int result) + { + ctx[CpuRegister.Rax] = unchecked((ulong)result); + return result; + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index fa39aede..cfd482ec 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -526,6 +526,9 @@ internal static unsafe class VulkanVideoPresenter // render thread reaches the previous image, which otherwise starves // presentation indefinitely. private static readonly Queue _pendingGuestImagePresentations = new(); + // Same fix as _pendingGuestImagePresentations above, for Submit()'s decoded video + // frames: a single "latest wins" slot dropped frames the render loop didn't poll in time. + private static readonly Queue _pendingVideoPresentations = new(); private static readonly Dictionary _guestImageWorkSequences = new(); private static readonly Dictionary _availableGuestImages = new(); // Write-tracker generation last uploaded for a CPU-backed guest image. @@ -805,6 +808,7 @@ internal static unsafe class VulkanVideoPresenter _pendingSyncGuestWorkCount = 0; _pendingGuestWorkBytes = 0; _pendingGuestImagePresentations.Clear(); + _pendingVideoPresentations.Clear(); _guestImageWorkSequences.Clear(); _availableGuestImages.Clear(); _cpuBackedUploadGenerations.Clear(); @@ -860,7 +864,7 @@ internal static unsafe class VulkanVideoPresenter } var sequence = (_latestPresentation?.Sequence ?? 0) + 1; - _latestPresentation = new Presentation( + var presentation = new Presentation( bgraFrame, width, height, @@ -869,6 +873,15 @@ internal static unsafe class VulkanVideoPresenter TranslatedDraw: null, RequiredGuestWorkSequence: 0, IsSplash: false); + + // Also dual-written to _latestPresentation as a fallback once the queue drains. + _pendingVideoPresentations.Enqueue(presentation); + while (_pendingVideoPresentations.Count > MaxPendingGuestFlipVersions) + { + _pendingVideoPresentations.Dequeue(); + } + + _latestPresentation = presentation; if (_thread is not null) { return; @@ -2435,6 +2448,19 @@ internal static unsafe class VulkanVideoPresenter return false; } + // Video's RequiredGuestWorkSequence is always 0, so this never blocks like the guest-image queue can. + while (_pendingVideoPresentations.Count > 0 && + _pendingVideoPresentations.Peek().Sequence <= presentedSequence) + { + _pendingVideoPresentations.Dequeue(); + } + + if (_pendingVideoPresentations.Count > 0) + { + presentation = _pendingVideoPresentations.Dequeue(); + return true; + } + if (_latestPresentation is not { } latest || latest.Sequence == presentedSequence || !IsGuestWorkCompletedLocked(latest.RequiredGuestWorkSequence))