Files
sharpemu/src/SharpEmu.Libs/Codec/Videodec2Exports.cs
T
Foued Attar 7521295ee1 [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.
2026-08-18 00:00:01 +03:00

247 lines
8.5 KiB
C#

// 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;
/// <summary>
/// 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.
/// </summary>
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<ulong, Videodec2Decoder?> 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;
}
}