[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:
Foued Attar
2026-08-17 23:00:01 +02:00
committed by GitHub
parent 1660111189
commit 7521295ee1
23 changed files with 921 additions and 4 deletions
+567
View File
@@ -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;
}
}
}
}
+246
View File
@@ -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))