mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-24 10:06:57 +08:00
[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.
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<byte[]?> _workChannel =
|
||||
Channel.CreateUnbounded<byte[]?>(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();
|
||||
}
|
||||
|
||||
/// <summary>Opens a new H.264 session, or null if FFmpeg is unavailable or the decoder couldn't open.</summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
|
||||
public void EnqueueAccessUnit(byte[] accessUnit)
|
||||
{
|
||||
_workChannel.Writer.TryWrite(accessUnit);
|
||||
}
|
||||
|
||||
/// <summary>Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any.</summary>
|
||||
public void RequestDrain()
|
||||
{
|
||||
_workChannel.Writer.TryWrite(null);
|
||||
}
|
||||
|
||||
/// <summary>Non-blocking: true exactly once per frame the worker has produced, in order.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Feeds one access unit and converts the resulting picture to BGRA, if any. Decode-worker thread only.</summary>
|
||||
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>((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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Signals end-of-stream and pulls one remaining buffered frame, if any. Decode-worker thread only.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Converts <see cref="_frame"/> to a tightly packed width*height*4 BGRA buffer, or null on failure.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
@@ -526,6 +526,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
// render thread reaches the previous image, which otherwise starves
|
||||
// presentation indefinitely.
|
||||
private static readonly Queue<Presentation> _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<Presentation> _pendingVideoPresentations = new();
|
||||
private static readonly Dictionary<ulong, long> _guestImageWorkSequences = new();
|
||||
private static readonly Dictionary<ulong, uint> _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))
|
||||
|
||||
Reference in New Issue
Block a user